Connector Fixture Discipline
Audience: engineers building or maintaining Stella Ops advisory (Concelier) and VEX (Excititor) connectors, plus reviewers checking that a new connector carries the required test layers.
This document defines the testing discipline for Stella Ops Concelier and Excititor connectors. All connectors must follow these patterns to ensure consistent, deterministic, and offline-capable testing.
Overview
Connector tests follow Model C1 (Connector/External) from the testing strategy:
- Fixture-based parser tests — Raw upstream payload → normalized internal model (offline)
- Resilience tests — Partial/bad input → deterministic failure classification
- Security tests — URL allowlist, redirect handling, payload limits
- Live smoke tests — Schema drift detection (opt-in, non-gating)
The shared base classes and helpers live in StellaOps.TestKit.Connectors (src/__Libraries/StellaOps.TestKit/Connectors/). The base classes referenced below are the actual types in that namespace:
| Concern | Base class | Notes |
|---|---|---|
| Parser snapshot | ConnectorParserTestBase<TRaw, TNormalized> | Abstract DeserializeRaw/Parse/DeserializeNormalized; VerifyParseSnapshot/VerifyDeterministicParse helpers |
| Fetch + parse | ConnectorFetchTestBase<TConnector, TNormalized> | Wires ConnectorHttpFixture for canned responses |
| Resilience | ConnectorResilienceTestBase | Non-generic; abstract TryParse/TryFetchAsync return (bool Success, string? ErrorCategory) |
| Security | ConnectorSecurityTestBase | Non-generic; abstract ConnectorName/AllowedUrlPatterns/TryFetchUrlAsync; CreateGzipBomb/CreateNestedGzipBomb helpers |
| Live schema drift | ConnectorLiveSchemaTestBase | Abstract FixturesDirectory/ConnectorName/GetTestCases; gated by STELLAOPS_LIVE_TESTS=true |
Note: connectors are not required to inherit these bases. Several (e.g. the NVD resilience tests and the GHSA security tests) test the connector’s actual mapper or the live
ConnectorTestHarnessdirectly with plain assertions, while still carrying the lane/category traits described below. The bases standardize the common patterns but do not gate.
1. Directory Structure
Each connector test project follows this structure:
src/<Module>/__Tests/StellaOps.<Module>.Connector.<Source>.Tests/
├── StellaOps.<Module>.Connector.<Source>.Tests.csproj
├── <Source>/
│ ├── Fixtures/ # Raw upstream payloads (copied to output)
│ │ ├── <source>-typical.json # Typical advisory payload
│ │ ├── <source>-edge-<case>.json # Edge case payloads
│ │ └── <source>-invalid-...json # Malformed/invalid payloads
│ ├── <Source>ParserSnapshotTests.cs # Parser snapshot + determinism tests
│ └── <Source>ResilienceTests.cs # Resilience tests
└── Expected/
└── <source>-<id>.canonical.json # Canonical JSON snapshots
The fixture/snapshot file names above are conventions, not enforced rules — actual projects vary (e.g. NVD uses
nvd-window-N.json,nvd-invalid-schema.json,<id>.canonical.v2.json). What is load-bearing is thatFixturesDirectoryandExpectedDirectoryresolve under the test assembly’sAppContext.BaseDirectory, so the fixture/expected files must be copied to output (<CopyToOutputDirectory>/Content/Noneitems in the.csproj).
Example: NVD Connector (actual on-disk layout)
Note that the connector subfolder owns its own Fixtures/ directory, while expected canonical snapshots live in a sibling top-level Expected/ directory. This is the layout NvdParserSnapshotTests resolves via FixturesDirectory => Nvd/Fixtures and ExpectedDirectory => Expected.
src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/
├── Nvd/
│ ├── Fixtures/
│ │ ├── nvd-window-1.json
│ │ ├── nvd-window-2.json
│ │ ├── nvd-multipage-1.json
│ │ ├── nvd-multipage-2.json
│ │ ├── nvd-invalid-schema.json
│ │ └── conflict-nvd.json
│ ├── NvdParserSnapshotTests.cs
│ └── NvdResilienceTests.cs
└── Expected/
├── nvd-window-1-CVE-2024-0001.canonical.v2.json
└── conflict-nvd.canonical.v2.json
2. Fixture-Based Parser Tests
Purpose
Test that the parser correctly transforms raw upstream payloads into normalized internal models without network access.
Pattern
ConnectorParserTestBase<TRaw, TNormalized> is abstract: a subclass supplies FixturesDirectory, the (de)serialization hooks (DeserializeRaw, Parse, DeserializeNormalized), and an optional SerializeToCanonical override, then calls the VerifyParseSnapshot / VerifyDeterministicParse helpers from [Fact] methods. The base reads fixtures with ReadFixture(name) and expected snapshots with ReadExpected(name) — there is no LoadFixture<T>() / AssertMatchesSnapshot() / Parser.Parse() surface.
using System.Text.Json;
using StellaOps.Concelier.Models;
using StellaOps.TestKit.Connectors;
using Xunit;
public sealed class NvdParserSnapshotTests
: ConnectorParserTestBase<JsonDocument, IReadOnlyList<Advisory>>
{
private static readonly string BaseDirectory = AppContext.BaseDirectory;
protected override string FixturesDirectory =>
Path.Combine(BaseDirectory, "Nvd", "Fixtures");
protected override string ExpectedDirectory =>
Path.Combine(BaseDirectory, "Expected");
protected override JsonDocument DeserializeRaw(string json) =>
JsonDocument.Parse(json);
protected override IReadOnlyList<Advisory> Parse(JsonDocument raw) =>
NvdMapper.Map(raw, CreateTestDocumentRecord(), recordedAt: FixedTime);
protected override IReadOnlyList<Advisory> DeserializeNormalized(string json) =>
JsonSerializer.Deserialize<List<Advisory>>(json) ?? new();
[Fact]
[Trait("Lane", "Unit")]
[Trait("Category", "Snapshot")]
public void ParseNvdWindow1_CVE20240001_ProducesExpectedOutput()
{
VerifyParseSnapshot("nvd-window-1.json", "nvd-window-1-CVE-2024-0001.canonical.v2.json");
}
[Fact]
[Trait("Lane", "Unit")]
[Trait("Category", "Determinism")]
public void ParseNvdWindow1_IsDeterministic()
{
VerifyDeterministicParse("nvd-window-1.json");
}
}
Tests carry both a
Lanetrait (Unit/Security/Integration/Live, consumed by the CI lane filters — see CI Lane Filters) and aCategorytrait drawn fromStellaOps.TestKit.TestCategories(e.g.Snapshot,Determinism,Resilience,Security,Live). The actual NVD tests apply both.
Fixture Requirements
Recommended naming conventions (actual projects vary — see the note in §1):
| Type | Naming Convention | Purpose |
|---|---|---|
| Typical | <source>-typical.json (NVD: nvd-window-N.json) | Normal advisory with all common fields |
| Edge case | <source>-edge-<case>.json (NVD: nvd-multipage-N.json) | Unusual but valid payloads |
| Error | <source>-error-<type>.json (NVD: nvd-invalid-schema.json) | Malformed/invalid payloads |
| Expected | <id>.canonical.json under Expected/ | Expected normalized output |
| Canonical | <source>-<id>.canonical.v2.json | Deterministic JSON snapshot |
Minimum Coverage
Each connector must have fixtures for:
- [ ] At least 1 typical payload
- [ ] At least 2 edge cases (e.g., multi-vendor, unusual CVSS, missing optional fields)
- [ ] At least 2 error cases (e.g., missing required fields, invalid schema)
3. Resilience Tests
Purpose
Verify that connectors handle malformed input gracefully with deterministic failure classification.
Pattern
ConnectorResilienceTestBase is non-generic. A subclass implements two abstract hooks that classify outcomes deterministically:
(bool Success, string? ErrorCategory) TryParse(string json)Task<(bool Success, string? ErrorCategory)> TryFetchAsync(string url, CancellationToken ct)
The base then contributes ready-made [Fact]s that assert the error category is deterministic across repeated runs (MissingRequiredFields_…, MalformedJson_…, EmptyInput_…, NullInput_…, HttpError_…, HttpNotFound_…, Timeout_…). There is no ConnectorErrorKind enum and no result.Error / result.Warnings surface — failure classification is a free-form string? ErrorCategory and the invariant under test is determinism, not a fixed taxonomy.
public sealed class NvdResilienceTests : ConnectorResilienceTestBase
{
protected override string FixturesDirectory =>
Path.Combine(AppContext.BaseDirectory, "Nvd", "Fixtures");
protected override (bool Success, string? ErrorCategory) TryParse(string json)
{
try
{
using var doc = JsonDocument.Parse(json);
var advisories = NvdMapper.Map(doc, CreateTestDocumentRecord(), FixedTime);
return (true, null);
}
catch (JsonException)
{
return (false, "malformed-json");
}
}
protected override Task<(bool, string?)> TryFetchAsync(string url, CancellationToken ct = default)
=> /* drive the connector via HttpFixture and classify the failure */;
}
Connectors may also test their mapper directly without inheriting the base — the actual NvdResilienceTests does exactly this, asserting that missing/empty vulnerabilities arrays return an empty list, entries without a CVE object are skipped, invalid dates parse to null, and that error handling is deterministic across three runs. Both styles carry [Trait("Lane", "Unit")] + [Trait("Category", "Resilience")].
Required Test Cases
Express the expected behaviour as a deterministic (Success, ErrorCategory) outcome or a deterministic mapper result; the base’s invariant is that the same input always yields the same classification.
| Case | Expected Behavior | Traits |
|---|---|---|
| Missing required field | Skip/empty result, deterministic | Lane=Unit, Category=Resilience |
| Invalid date format | Parsed with null date, deterministic | Lane=Unit, Category=Resilience |
| Malformed JSON | Success=false, deterministic ErrorCategory | Lane=Unit, Category=Resilience |
| Empty / null input | Success=false, deterministic ErrorCategory | Lane=Unit, Category=Resilience |
| Unknown enum value | Parses without throwing, deterministic | Lane=Unit, Category=Resilience |
| Truncated payload | JsonException on parse, deterministic | Lane=Unit, Category=Resilience |
4. Security Tests
Purpose
Verify that connectors enforce security boundaries for network operations.
Pattern
ConnectorSecurityTestBase is non-generic. A subclass supplies ConnectorName, the AllowedUrlPatterns list (patterns support *, *.suffix, and prefix*), and the abstract TryFetchUrlAsync(url) hook returning (bool Allowed, string? ErrorMessage). It also exposes the virtual knobs MaxPayloadSizeBytes (default 50 MB) and MaxDecompressionRatio (default 100:1), the IsUrlInAllowlist(url) helper, and the static CreateGzipBomb / CreateNestedGzipBomb payload builders. The base ships ready-made [Theory]/[Fact]s — NonAllowlistedUrl_IsRejected, HttpsToHttpDowngrade_IsRejected, InternalUrl_IsRejected (SSRF: localhost, link-local, AWS/GCP metadata), MaxRedirectChain_IsEnforced. There is no SecurityException, PayloadTooLargeException, or CreateConnector(allowedHosts: …) / connector.FetchAsync(url) surface; rejection is expressed as Allowed == false.
public sealed class NvdSecurityTests : ConnectorSecurityTestBase
{
protected override string ConnectorName => "NVD";
protected override IReadOnlyList<string> AllowedUrlPatterns => new[]
{
"services.nvd.nist.gov",
};
protected override Task<(bool Allowed, string? ErrorMessage)> TryFetchUrlAsync(
string url, CancellationToken ct = default)
{
// Drive the connector's actual URL validation; return Allowed=false on reject.
var allowed = IsUrlInAllowlist(url);
return Task.FromResult((allowed, allowed ? null : "url not in allowlist"));
}
}
In practice some connectors (e.g. GHSA) skip the base and assert security properties against the live ConnectorTestHarness instead — verifying that external reference URLs are never followed (no SSRF), that oversized payloads do not OOM, that gzip/nested-gzip bombs are bounded (via CreateGzipBomb/CreateNestedGzipBomb), that malicious IDs are sanitized, and that a 429 response is not retried aggressively. These tests carry [Trait("Category", TestCategories.Unit)] + [Trait("Category", TestCategories.Security)].
Required Security Tests
| Test | Purpose | Trait |
|---|---|---|
| URL allowlist | Block requests to unauthorized hosts | Category=Security |
| HTTPS→HTTP downgrade | Reject insecure scheme | Category=Security |
| Internal/SSRF URLs | Block localhost, link-local, cloud metadata | Category=Security |
| Max payload size | Reject/bound oversized responses | Category=Security |
| Decompression bomb | Reject/bound gzip and nested-gzip bombs | Category=Security |
| Rate limiting | Fail fast on 429, no aggressive retry | Category=Security |
5. Live Smoke Tests (Opt-In)
Purpose
Detect upstream schema drift by comparing live responses against known fixtures.
Pattern
The base class is ConnectorLiveSchemaTestBase (there is no ConnectorLiveTestBase). A subclass overrides FixturesDirectory, ConnectorName, and GetTestCases() (which maps each fixture file to a live URL via the LiveSchemaTestCase record), then calls RunSchemaDriftTestsAsync() from a method marked with the [LiveTest] attribute. The [LiveTest] / [LiveTheory] attributes set Skip automatically unless STELLAOPS_LIVE_TESTS=true, so there is no Skip.IfNot(IsLiveLaneEnabled()) call. Drift is detected by FixtureUpdater.CheckDriftAsync (normalized-JSON comparison), not a hand-rolled AssertSchemaMatches.
using StellaOps.TestKit;
using StellaOps.TestKit.Connectors;
using Xunit;
[Trait("Category", TestCategories.Live)]
public sealed class NvdLiveSchemaTests : ConnectorLiveSchemaTestBase
{
protected override string FixturesDirectory =>
Path.Combine(AppContext.BaseDirectory, "Fixtures");
protected override string ConnectorName => "NVD";
protected override IEnumerable<LiveSchemaTestCase> GetTestCases()
{
yield return new(
"typical-cve.json",
"https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2024-0001",
"Typical NVD CVE structure");
}
[LiveTest]
public Task DetectSchemaDrift() => RunSchemaDriftTestsAsync();
}
Configuration
Live tests are:
- Never PR-gating — run only in scheduled/nightly jobs (the
Livelane is opt-in, per CI Lane Filters) - Opt-in — require
STELLAOPS_LIVE_TESTS=true; the[LiveTest]/[LiveTheory]attributes setSkipotherwise - Alerting on drift — when drift is found and
STELLAOPS_UPDATE_FIXTURES=truethe test warns and auto-rewrites the fixture; without it, the test fails with the list of drifted fixtures
6. Fixture Updater
Purpose
Refresh fixtures from live sources when upstream schemas change intentionally.
Mechanism
FixtureUpdater is a library class in StellaOps.TestKit.Connectors, not a standalone CLI. It is driven by the STELLAOPS_UPDATE_FIXTURES=true environment variable and is consumed two ways:
- From live schema tests —
ConnectorLiveSchemaTestBaseinstantiates aFixtureUpdaterand, when drift is detected andSTELLAOPS_UPDATE_FIXTURES=true, callsUpdateJsonFixtureFromUrlAsyncto rewrite the drifted fixture (pretty-printed). - From snapshot tests —
ConnectorParserTestBase.UpdateSnapshot(fixture, expected)regenerates anExpected/*.canonical.*.jsonsnapshot; it also requiresSTELLAOPS_UPDATE_FIXTURES=trueand throws otherwise.
Public methods include UpdateFixtureFromUrlAsync, UpdateJsonFixtureFromUrlAsync, SaveFixtureAsync, SaveExpectedSnapshotAsync<T>, and CheckDriftAsync (returns a FixtureDriftReport). All write operations are no-ops unless updating is enabled.
Usage
# Refresh fixtures by running the live schema tests with both flags set.
# Drift triggers an auto-rewrite (and a warning) instead of a failure.
STELLAOPS_LIVE_TESTS=true STELLAOPS_UPDATE_FIXTURES=true \
dotnet test --filter "Category=Live&FullyQualifiedName~Nvd"
# Regenerate parser snapshots (Expected/*.canonical.*.json) for a connector.
STELLAOPS_UPDATE_FIXTURES=true \
dotnet test --filter "Category=Snapshot&FullyQualifiedName~Nvd"
Workflow
- The live schema test detects drift (
CheckDriftAsyncreports normalized-JSON delta). - A developer re-runs with
STELLAOPS_UPDATE_FIXTURES=trueto rewrite the fixture. - The developer reviews the diff for intentional vs accidental changes.
- If intentional: update the parser/mapper and commit the refreshed fixtures.
- If accidental: investigate the upstream API issue before committing.
7. Test Traits and CI Integration
Trait Assignment
Connector tests carry two trait families: a Lane trait consumed by the CI lane filters (see CI Lane Filters) and a Category trait drawn from StellaOps.TestKit.TestCategories. The observed assignments in src/Concelier are:
| Test Category | Lane trait | Category trait | PR-Gating |
|---|---|---|---|
| Parser snapshot tests | [Trait("Lane", "Unit")] | [Trait("Category", "Snapshot")] | Yes |
| Determinism tests | [Trait("Lane", "Unit")] | [Trait("Category", "Determinism")] | Yes |
| Resilience tests | [Trait("Lane", "Unit")] | [Trait("Category", "Resilience")] | Yes |
| Security tests | — | [Trait("Category", TestCategories.Unit)] + [Trait("Category", TestCategories.Security)] | Yes |
| Live schema tests | — | [Trait("Category", TestCategories.Live)] + [LiveTest] | No |
The security and live examples in
src/Conceliertag withCategoryrather thanLane. Prefer adding both aLaneand aCategorytrait on new tests so they route under either filter; theLivelane andCategory=Liveare equivalent opt-in selectors.
Running Tests
# Run connector unit-lane tests
dotnet test --filter "Lane=Unit" src/Concelier/__Tests/
# Run security tests (tagged via Category)
dotnet test --filter "Category=Security" src/Concelier/__Tests/
# Run live schema-drift tests (opt-in; gated by STELLAOPS_LIVE_TESTS)
STELLAOPS_LIVE_TESTS=true dotnet test --filter "Category=Live" src/Concelier/__Tests/
8. Connector Inventory
The connector set grows frequently, so a per-connector pass/fail matrix here rots into misinformation. Treat
src/as the source of truth and enumerate the live set with:# Advisory (Concelier) connectors and their test projects ls src/Concelier/__Libraries/StellaOps.Concelier.Connector.* ls src/Concelier/__Tests/StellaOps.Concelier.Connector.*.Tests # VEX (Excititor) connectors test projects ls src/Concelier/__Tests/StellaOps.Excititor.Connectors.*.Tests
Concelier Connectors (Advisory Sources)
These are grouped by family; the list below reflects the projects present under src/Concelier/__Libraries/ / __Connectors/ (verify against the directory before relying on completeness):
- Core feeds: Nvd, Osv, Ghsa, Cve, Kev, Epss, StellaOpsMirror
- Distro: Distro.Alpine, Distro.Arch, Distro.Debian, Distro.Gentoo, Distro.RedHat, Distro.Suse, Distro.Ubuntu
- Vendor (Vndr): Adobe, Amd, Apple, Arm, Chromium, Cisco, Intel, Msrc, Oracle, Siemens, Vmware
- Cloud: Cloud.Aws, Cloud.Azure, Cloud.Gcp
- National CERT / regional: Acsc, Cccs, CertBund, CertCc, CertFr, CertIn, Jvn, Kisa, Ru.Bdu, Ru.Nkcki, Astra
- ICS: Ics.Cisa, Ics.Kaspersky
- Ecosystem (package registries): Npm, PyPi, PyPa, Go, GoVuln, Maven, Crates, RustSec, Packagist, Hex, RubyGems, BundlerAudit
Shared connector logic lives in StellaOps.Concelier.Connector.Common.
Excititor Connectors (VEX Sources)
VEX connector test projects live alongside the Concelier tests under src/Concelier/__Tests/StellaOps.Excititor.Connectors.*.Tests:
- Cisco.CSAF
- MSRC.CSAF
- OCI.OpenVEX.Attest
- Oracle.CSAF
- RedHat.CSAF
- SUSE.RancherVEXHub
- Ubuntu.CSAF
Coverage status
A reliable per-connector coverage table is maintained as generated test-coverage evidence rather than hand-curated here; see TEST_COVERAGE_MATRIX. Verified examples of the patterns in this document include the NVD parser-snapshot + resilience tests (StellaOps.Concelier.Connector.Nvd.Tests), the GHSA security + live-schema tests (StellaOps.Concelier.Connector.Ghsa.Tests), and the RedHat CSAF Excititor live-schema test (StellaOps.Excititor.Connectors.RedHat.CSAF.Tests).
References
Source paths are repo-root-relative (the shared connector test bases live in StellaOps.TestKit.Connectors):
src/__Libraries/StellaOps.TestKit/Connectors/ConnectorHttpFixture.cs—ConnectorHttpFixturesrc/__Libraries/StellaOps.TestKit/Connectors/ConnectorTestBase.cs—ConnectorParserTestBase/ConnectorFetchTestBasesrc/__Libraries/StellaOps.TestKit/Connectors/ConnectorResilienceTestBase.cs—ConnectorResilienceTestBasesrc/__Libraries/StellaOps.TestKit/Connectors/ConnectorSecurityTestBase.cs—ConnectorSecurityTestBasesrc/__Libraries/StellaOps.TestKit/Connectors/ConnectorLiveSchemaTestBase.cs—ConnectorLiveSchemaTestBase/[LiveTest]attributessrc/__Libraries/StellaOps.TestKit/Connectors/FixtureUpdater.cs—FixtureUpdatersrc/__Libraries/StellaOps.TestKit/TestCategories.cs—TestCategories
Related docs:
Last reconciled against src/ 2026-05-30. Originated: Sprint 5100.0007.0005.
