Skip to content

test(concurrent-keys): integration coverage for view-scoped key rejection - #796

Merged
aaron-zeisler merged 5 commits into
feat/concurrent-keysfrom
aaronz/SDK-2860/view-scoped-keys-integration-tests
Aug 12, 2026
Merged

test(concurrent-keys): integration coverage for view-scoped key rejection#796
aaron-zeisler merged 5 commits into
feat/concurrent-keysfrom
aaronz/SDK-2860/view-scoped-keys-integration-tests

Conversation

@aaron-zeisler

@aaron-zeisler aaron-zeisler commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Integration coverage for the view-scoped key rejection added in #795. Stacked on that branch — review #795 first, and this will retarget to feat/concurrent-keys automatically once it merges.

Jira: SDK-2860

Background

#795 proves the filter at the BuildAcceptedSet unit boundary: given params, the right keys land in the accepted set and the right identifiers are reported. That leaves the questions a unit test structurally cannot answer — whether a rejected credential actually fails downstream authentication, whether it really carries no state anywhere in the environment, and whether a live SDK connection on a key that gains a view is genuinely torn down.

These run through the two real harnesses in the existing concurrent_keys_*_test.go family (autoConfTest for the auto-config stream, offlineModeTest for the archive), so the assertions hit actual HTTP endpoints, a real /status response, and live event streams.

Changes

Seven tests, mirroring the four cases named in the ticket:

  • Rejection, both sources. A view-scoped SDK key and mobile key fail every SDK endpoint while the anchor and their non-view-scoped siblings keep working, the anchor still opens the single upstream client, and the rejected keys are absent from both the accepted set and the /status sdkKeys[]/mobileKeys[] arrays (asserted by exact count, so a key surfacing under an unexpected obscured value is still caught).
  • The designated-key carve-out, both sources. A marker on the anchor or primary mobile key is ignored rather than fatal: the environment comes up normally, every key still authenticates, no error is logged, and the ignored marker is reported as such.
  • Mid-session view acquisition. On the offline path (which uses a real SDK client that serves stream data), a connected SDK on a non-anchor key has its stream closed when the key gains a view, stops authenticating, and is re-admitted when the view is removed again — the filter is stateless, not a one-way latch. Covered for both an SDK key and a mobile key. The auto-config equivalent holds an open stream on the anchor and asserts the revocation leaves it undisturbed.
  • No misleading WARN on malformed payloads. A payload that is both malformed and carrying view-scoped keys logs the malformed error and stays silent about the view-scoped ones, since the handler discarded the payload and never rejected anything.

The ingestion WARN is also asserted to fire exactly once per payload. That is what makes the decision to keep StreamManager.validateCredentialPayload silent regress loudly — it runs on every environment of every payload, so logging from there would double every message.

Every test was mutation-checked: disabling the non-anchor filter fails four of them, disabling the designated-key branch fails the other two, and the malformed-payload case fails only when both of its independent defenses are broken.


Note

Overview
Adds relay/concurrent_keys_views_test.go — integration tests for ingestion-time rejection of view-scoped credentials (behavior from #795), using the existing autoConfTest and offlineModeTest harnesses.

Rejection on ingest (RAC and offline): Non-anchor SDK/mobile keys marked with views fail SDK endpoints, stay out of the accepted set and /status key arrays (with exact counts), while anchor and other siblings keep working. Asserts the ingestion WARN names wire identifiers and fires once per payload.

Malformed payloads: When the archive is structurally invalid, logs the malformed error and does not emit the view-scoped rejection WARN.

Mid-session view acquisition: Offline tests close live streams when a connected non-anchor key gains a view, revoke auth, then re-admit the key when the view is removed (SDK /all and mobile /meval subtests). RAC test patches keys to view-scoped while an anchor stream is open, expects routing revocation and reconnect rejection without disturbing the anchor connection.

Reviewed by Cursor Bugbot for commit 9f89000. Bugbot is set up for automated code reviews on this repo. Configure here.

@aaron-zeisler
aaron-zeisler force-pushed the aaronz/SDK-2860/reject-view-scoped-keys branch from c6bcde9 to 677f834 Compare August 5, 2026 20:40
@aaron-zeisler
aaron-zeisler force-pushed the aaronz/SDK-2860/view-scoped-keys-integration-tests branch 5 times, most recently from 9479d41 to bd308e1 Compare August 6, 2026 17:55
…ial wire format

Adds hasViews to each sdkKeys[]/mobileKeys[] entry and carries it onto the
accepted-key params. Nothing reads it yet — the filter that consumes it
follows in the next commit.

A plain bool rather than a pointer: an absent field means "not view-scoped"
and there is no third state, unlike expiry where nil genuinely means
permanent. Old-format payloads synthesize their accepted keys from the
singular sdkKey/mobKey and so never set it, which is correct — the backend
forbids views on a default key.
The sdkKeys[]/mobileKeys[] arrays hand relay every non-default credential
for an environment, including keys scoped to a view. A view-scoped key is
only entitled to a subset of the environment's flags, but relay serves the
whole environment payload and has no view support — so accepting one would
silently over-deliver every flag to an SDK that must see only a subset.

Filter them in BuildAcceptedSet, the single funnel for both the auto-config
stream and the offline archive. They never enter the accepted set, so an SDK
presenting one is rejected because the credential is absent from the lookup
map, and no state for it reaches /status, event forwarding, or the expiry
ticker. BuildAcceptedSet returns the identifiers it dropped so the handlers
that apply a set can WARN; it takes no logger because the stream parse
boundary validates every payload and would double-log each one.

Rejection is keyed by credential value rather than evaluated per entry. The
accepted-set builder is first-wins, so if two entries carry the same value
and only one is marked, per-entry filtering would admit the credential via
the unmarked entry while still reporting it rejected. Tainting the value
makes one marked entry sufficient whichever position it holds. Excluding the
designated key from the taint set is also what keeps the anchor guard
structural: the marker is disregarded on the anchor and the primary mobile
key, because dropping a designated key would take the whole environment down
and the backend forbids views on a default key in the first place.

Acquiring a view mid-session needed no new code: reconcileAcceptedKeys
already revokes any key absent from the desired set immediately rather than
on an expiry timestamp, and RemoveConnectionMapping unmaps before the
streams are torn down so a reconnect is rejected.
@aaron-zeisler
aaron-zeisler force-pushed the aaronz/SDK-2860/reject-view-scoped-keys branch from 706ca7a to ffc86d5 Compare August 6, 2026 20:12
@aaron-zeisler
aaron-zeisler force-pushed the aaronz/SDK-2860/view-scoped-keys-integration-tests branch from bd308e1 to 279df24 Compare August 6, 2026 20:15
@aaron-zeisler
aaron-zeisler marked this pull request as ready for review August 10, 2026 18:49
@aaron-zeisler
aaron-zeisler requested a review from a team as a code owner August 10, 2026 18:49
@aaron-zeisler
aaron-zeisler force-pushed the aaronz/SDK-2860/reject-view-scoped-keys branch from ffc86d5 to 69bd4de Compare August 11, 2026 18:32
@aaron-zeisler
aaron-zeisler force-pushed the aaronz/SDK-2860/view-scoped-keys-integration-tests branch from 279df24 to 76b7dad Compare August 11, 2026 18:34
@aaron-zeisler
aaron-zeisler force-pushed the aaronz/SDK-2860/reject-view-scoped-keys branch from 69bd4de to f4d56e7 Compare August 11, 2026 18:36
@aaron-zeisler
aaron-zeisler force-pushed the aaronz/SDK-2860/view-scoped-keys-integration-tests branch from 76b7dad to f268417 Compare August 11, 2026 18:47
Judge each entry on its own marker instead of collecting the set of
view-scoped credential values in a preliminary pass. This drops a helper, a
map, and an iteration over both key arrays.

The pre-pass existed so that one marked entry would reject a credential value
even if another entry carried the same value unmarked — the accepted-set
builder is first-wins, so per-entry filtering would otherwise admit such a
value through the unmarked entry while still reporting it as dropped. That
shape is not reachable: the backend enforces one row per credential value with
a unique index, and the offline archive is generated from the same data.

Also drops the placeholder for an empty key identifier. Only entries from the
sdkKeys[]/mobileKeys[] arrays can be marked view-scoped, and the backend
requires a non-empty identifier on each of those, so there was nothing for it
to substitute for.
…ormed-payload error

AcceptedSetBuilder.Build returned a bare sentinel when no SDK key survived,
while every other rejection was a *MalformedCredentialSetError. That framed
an empty set as a caller mistake, which it no longer is: the payload reaches
it by combining an undefined anchor with an sdkKeys[] array that is either
empty or made up entirely of keys relay excludes — the view-scoped case being
new. Return the same type, with a message describing the payload rather than
the builder, since that is what an operator reading the log can act on.

With the type now uniform, the offline handlers no longer branch on it. Both
branches already preserved previous credentials and differed only in message
text, and the fallback text was the wrong one: it claimed the environment was
auto-configured, and in UpdateEnvironment that the environment failed to
initialize when it had already been running. Log the accurate message
unconditionally instead.

Stop documenting the error type as a contract. Nothing in production inspects
it now, and BuildAcceptedSet cannot honor such a promise on its own — the
guarantee would depend on a function in another package whose author has no
reason to know about it. The taxonomy stays pinned by tests, which is where a
claim like this can actually be enforced; TestBuildAcceptedSet_NoSDKKeys now
covers both shapes that reach the empty-set path, as it is the only test that
would catch this classification regressing.
@aaron-zeisler
aaron-zeisler force-pushed the aaronz/SDK-2860/reject-view-scoped-keys branch from f4d56e7 to cef8393 Compare August 12, 2026 16:46
…tion

Exercises the ingestion filter through both real harnesses — the auto-config
stream and the offline archive — rather than at the BuildAcceptedSet unit
boundary, so the assertions run against actual downstream authentication and
live SDK connections.

Covers the cases the unit tests cannot reach: that a rejected key really does
fail auth on every SDK endpoint while its siblings keep working, that it is
absent from the /status arrays, that a key which gains a view mid-session has
its live stream torn down and cannot reconnect while other keys' connections
stay up, that losing the view again re-admits it, and that a malformed payload
carrying view-scoped keys logs the malformed error without claiming to have
rejected anything.

Also pins that the ingestion WARN fires exactly once per payload, which is
what makes the decision to keep the stream parse boundary silent regress
loudly rather than silently doubling every message.
@aaron-zeisler
aaron-zeisler force-pushed the aaronz/SDK-2860/view-scoped-keys-integration-tests branch from f268417 to 9f89000 Compare August 12, 2026 16:47
Base automatically changed from aaronz/SDK-2860/reject-view-scoped-keys to feat/concurrent-keys August 12, 2026 17:04
@aaron-zeisler
aaron-zeisler merged commit 4474286 into feat/concurrent-keys Aug 12, 2026
6 checks passed
@aaron-zeisler
aaron-zeisler deleted the aaronz/SDK-2860/view-scoped-keys-integration-tests branch August 12, 2026 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants