…ntity (#57)
* feat(GameServer): demonstrate [TraxAuthorize] on a [TraxQueryModel] entity
Gates MatchRecord with [TraxAuthorize(Roles = nameof(GameRole.Admin))] so
the sample shows how to expose a query-model entity to admins only. The
attribute attaches @authorize at both the entry field and ObjectType, so
top-level discover.matches.matchRecords and any transitive navigation
that resolves a MatchRecord are gated uniformly; totalCount and pageInfo
side channels are blocked too.
Pins the default authentication scheme to api-key so HC's @authorize
directive sees an authenticated principal when more than one scheme is
registered. With a single scheme, ASP.NET Core auto-selects it as
default; with multiple, no scheme is auto-default and HC reads an
anonymous principal — every gated query rejects every caller, including
admins. The pin is conditional on neither scheme nor environment, so it
applies consistently regardless of whether the Google JWT scheme is
configured.
Tests:
- Existing MatchRecord query / filter / pagination tests now use the
Admin key.
- New rejection tests cover Player and anonymous direct access plus
the totalCount-only side-channel case (proves field-level gating
closes the loophole that type-level alone would leave open).
* feat(GameServer): demonstrate [TraxAuthorize] on a [TraxQueryModel] entity
Gate MatchRecord with [TraxAuthorize(Roles = Admin)] so the sample demonstrates
per-model authorization end-to-end. The attribute attaches the @authorize
directive at both the entry field (discover.matches.matchRecords) and the
ObjectType level — Connection scalars like totalCount and pageInfo are blocked
too, so a Player API key cannot even enumerate match cardinality.
Existing MatchRecord query tests switched from PlayerKey to AdminKey. Three
new rejection-path tests pin the contract: Player and anonymous callers see
TRAX_AUTHORIZATION; totalCount-only queries are blocked even though they
never resolve a node of the gated type.
* test(GameServer.E2E): warmup MatchRecord auth cache before assertions
The three MatchRecords_*_ReturnsAuthorizationError tests fired the first
request against a [TraxAuthorize]-gated entity in the fixture lifecycle.
HotChocolate validates and caches operation documents on first execution.
If the validation pipeline runs while any part of the request pipeline
is still settling, the cached operation can capture a pre-gated state,
and that specific document permanently bypasses the directive. This
showed up in CI as a deterministic-on-cold-start, passes-on-rerun flake:
3 failures clustered in the first 400ms of GameServer.E2E execution,
then 102 other tests against the same entity passed for the next 2 min.
WarmupAuthorizationCache polls both gated query shapes with an anonymous
caller in [OneTimeSetUp]. Each shape is exercised until it returns
"Not authorized." once, which proves the directive is wired AND seeds
the operation cache with a validated entry that enforces it. Tests then
assert against a known steady-state cache.
Bounded by a 30s timeout that throws TimeoutException with a clear
diagnostic if the directive never attaches. Per the project's
determinism rule: polls the actual condition, fails loudly on timeout,
never sleeps as a stand-in for synchronisation.
Verified: 8 consecutive runs of the fixture pass in 253-273ms with no
variance.