- Created `StellaOps.TestKit.Tests` project for unit tests related to determinism. - Implemented `DeterminismManifestTests` to validate deterministic output for canonical bytes and strings, file read/write operations, and error handling for invalid schema versions. - Added `SbomDeterminismTests` to ensure identical inputs produce consistent SBOMs across SPDX 3.0.1 and CycloneDX 1.6/1.7 formats, including parallel execution tests. - Updated project references in `StellaOps.Integration.Determinism` to include the new determinism testing library.
426 lines
13 KiB
Markdown
426 lines
13 KiB
Markdown
# Connector Fixture Discipline
|
|
|
|
This document defines the testing discipline for StellaOps 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:
|
|
|
|
1. **Fixture-based parser tests** — Raw upstream payload → normalized internal model (offline)
|
|
2. **Resilience tests** — Partial/bad input → deterministic failure classification
|
|
3. **Security tests** — URL allowlist, redirect handling, payload limits
|
|
4. **Live smoke tests** — Schema drift detection (opt-in, non-gating)
|
|
|
|
---
|
|
|
|
## 1. Directory Structure
|
|
|
|
Each connector test project follows this structure:
|
|
|
|
```
|
|
src/<Module>/__Tests/StellaOps.<Module>.Connector.<Source>.Tests/
|
|
├── StellaOps.<Module>.Connector.<Source>.Tests.csproj
|
|
├── Fixtures/
|
|
│ ├── <source>-typical.json # Typical advisory payload
|
|
│ ├── <source>-edge-<case>.json # Edge case payloads
|
|
│ ├── <source>-error-<type>.json # Malformed/invalid payloads
|
|
│ └── expected-<id>.json # Expected normalized output
|
|
├── <Source>/
|
|
│ ├── <Source>ParserTests.cs # Parser unit tests
|
|
│ ├── <Source>ConnectorTests.cs # Connector integration tests
|
|
│ └── <Source>ResilienceTests.cs # Resilience/security tests
|
|
└── Expected/
|
|
└── <source>-<id>.canonical.json # Canonical JSON snapshots
|
|
```
|
|
|
|
### Example: NVD Connector
|
|
|
|
```
|
|
src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/
|
|
├── Fixtures/
|
|
│ ├── nvd-window-1.json
|
|
│ ├── nvd-window-2.json
|
|
│ ├── nvd-multipage-1.json
|
|
│ ├── nvd-multipage-2.json
|
|
│ ├── nvd-invalid-schema.json
|
|
│ └── expected-CVE-2024-0001.json
|
|
├── Nvd/
|
|
│ ├── NvdParserTests.cs
|
|
│ ├── NvdConnectorTests.cs
|
|
│ └── NvdConnectorHarnessTests.cs
|
|
└── Expected/
|
|
└── conflict-nvd.canonical.json
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Fixture-Based Parser Tests
|
|
|
|
### Purpose
|
|
|
|
Test that the parser correctly transforms raw upstream payloads into normalized internal models without network access.
|
|
|
|
### Pattern
|
|
|
|
```csharp
|
|
using StellaOps.TestKit.Connectors;
|
|
|
|
public class NvdParserTests : ConnectorParserTestBase<NvdRawAdvisory, ConcelierAdvisory>
|
|
{
|
|
public NvdParserTests()
|
|
: base(new NvdParser(), "Nvd/Fixtures")
|
|
{
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Lane", "Unit")]
|
|
public async Task ParseTypicalAdvisory_ProducesExpectedModel()
|
|
{
|
|
// Arrange
|
|
var raw = await LoadFixture<NvdRawAdvisory>("nvd-window-1.json");
|
|
|
|
// Act
|
|
var result = Parser.Parse(raw);
|
|
|
|
// Assert
|
|
await AssertMatchesSnapshot(result, "expected-CVE-2024-0001.json");
|
|
}
|
|
|
|
[Theory]
|
|
[Trait("Lane", "Unit")]
|
|
[InlineData("nvd-multipage-1.json", "expected-multipage-1.json")]
|
|
[InlineData("nvd-multipage-2.json", "expected-multipage-2.json")]
|
|
public async Task ParseAllFixtures_ProducesExpectedModels(string input, string expected)
|
|
{
|
|
var raw = await LoadFixture<NvdRawAdvisory>(input);
|
|
var result = Parser.Parse(raw);
|
|
await AssertMatchesSnapshot(result, expected);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Fixture Requirements
|
|
|
|
| Type | Naming Convention | Purpose |
|
|
|------|-------------------|---------|
|
|
| Typical | `<source>-typical.json` | Normal advisory with all common fields |
|
|
| Edge case | `<source>-edge-<case>.json` | Unusual but valid payloads |
|
|
| Error | `<source>-error-<type>.json` | Malformed/invalid payloads |
|
|
| Expected | `expected-<id>.json` | Expected normalized output |
|
|
| Canonical | `<source>-<id>.canonical.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
|
|
|
|
```csharp
|
|
public class NvdResilienceTests : ConnectorResilienceTestBase<NvdConnector>
|
|
{
|
|
public NvdResilienceTests()
|
|
: base(new NvdConnector(CreateTestHttpClient()))
|
|
{
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Lane", "Unit")]
|
|
public async Task MissingRequiredField_ReturnsParseError()
|
|
{
|
|
// Arrange
|
|
var payload = await LoadFixture("nvd-error-missing-cve-id.json");
|
|
|
|
// Act
|
|
var result = await Connector.ParseAsync(payload);
|
|
|
|
// Assert
|
|
Assert.False(result.IsSuccess);
|
|
Assert.Equal(ConnectorErrorKind.ParseError, result.Error.Kind);
|
|
Assert.Contains("cve_id", result.Error.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Lane", "Unit")]
|
|
public async Task InvalidDateFormat_ReturnsParseError()
|
|
{
|
|
var payload = await LoadFixture("nvd-error-invalid-date.json");
|
|
|
|
var result = await Connector.ParseAsync(payload);
|
|
|
|
Assert.False(result.IsSuccess);
|
|
Assert.Equal(ConnectorErrorKind.ParseError, result.Error.Kind);
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Lane", "Unit")]
|
|
public async Task UnexpectedEnumValue_LogsWarningAndContinues()
|
|
{
|
|
var payload = await LoadFixture("nvd-edge-unknown-severity.json");
|
|
|
|
var result = await Connector.ParseAsync(payload);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Contains(result.Warnings, w => w.Contains("unknown severity"));
|
|
}
|
|
}
|
|
```
|
|
|
|
### Required Test Cases
|
|
|
|
| Case | Expected Behavior | Trait |
|
|
|------|-------------------|-------|
|
|
| Missing required field | `ConnectorErrorKind.ParseError` | Unit |
|
|
| Invalid date format | `ConnectorErrorKind.ParseError` | Unit |
|
|
| Invalid JSON structure | `ConnectorErrorKind.ParseError` | Unit |
|
|
| Unknown enum value | Warning logged, continues | Unit |
|
|
| Empty response | `ConnectorErrorKind.EmptyResponse` | Unit |
|
|
| Truncated payload | `ConnectorErrorKind.ParseError` | Unit |
|
|
|
|
---
|
|
|
|
## 4. Security Tests
|
|
|
|
### Purpose
|
|
|
|
Verify that connectors enforce security boundaries for network operations.
|
|
|
|
### Pattern
|
|
|
|
```csharp
|
|
public class NvdSecurityTests : ConnectorSecurityTestBase<NvdConnector>
|
|
{
|
|
[Fact]
|
|
[Trait("Lane", "Security")]
|
|
public async Task UrlOutsideAllowlist_RejectsRequest()
|
|
{
|
|
// Arrange
|
|
var connector = CreateConnector(allowedHosts: ["services.nvd.nist.gov"]);
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<SecurityException>(
|
|
() => connector.FetchAsync("https://evil.example.com/api"));
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Lane", "Security")]
|
|
public async Task RedirectToDisallowedHost_RejectsRequest()
|
|
{
|
|
var handler = CreateMockHandler(redirectTo: "https://evil.example.com");
|
|
var connector = CreateConnector(handler, allowedHosts: ["services.nvd.nist.gov"]);
|
|
|
|
await Assert.ThrowsAsync<SecurityException>(
|
|
() => connector.FetchAsync("https://services.nvd.nist.gov/api"));
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Lane", "Security")]
|
|
public async Task PayloadExceedsMaxSize_RejectsPayload()
|
|
{
|
|
var handler = CreateMockHandler(responseSize: 100_000_001); // 100MB
|
|
var connector = CreateConnector(handler, maxPayloadBytes: 100_000_000);
|
|
|
|
await Assert.ThrowsAsync<PayloadTooLargeException>(
|
|
() => connector.FetchAsync("https://services.nvd.nist.gov/api"));
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Lane", "Security")]
|
|
public async Task DecompressionBomb_RejectsPayload()
|
|
{
|
|
// 1KB compressed, 1GB decompressed
|
|
var handler = CreateMockHandler(compressedBomb: true);
|
|
var connector = CreateConnector(handler);
|
|
|
|
await Assert.ThrowsAsync<PayloadTooLargeException>(
|
|
() => connector.FetchAsync("https://services.nvd.nist.gov/api"));
|
|
}
|
|
}
|
|
```
|
|
|
|
### Required Security Tests
|
|
|
|
| Test | Purpose | Trait |
|
|
|------|---------|-------|
|
|
| URL allowlist | Block requests to unauthorized hosts | Security |
|
|
| Redirect validation | Block redirects to unauthorized hosts | Security |
|
|
| Max payload size | Reject oversized responses | Security |
|
|
| Decompression bomb | Reject zip bombs | Security |
|
|
| Rate limiting | Respect upstream rate limits | Security |
|
|
|
|
---
|
|
|
|
## 5. Live Smoke Tests (Opt-In)
|
|
|
|
### Purpose
|
|
|
|
Detect upstream schema drift by comparing live responses against known fixtures.
|
|
|
|
### Pattern
|
|
|
|
```csharp
|
|
public class NvdLiveTests : ConnectorLiveTestBase<NvdConnector>
|
|
{
|
|
[Fact]
|
|
[Trait("Lane", "Live")]
|
|
[Trait("Category", "SchemaDrift")]
|
|
public async Task LiveSchema_MatchesFixtureSchema()
|
|
{
|
|
// Skip if not in Live lane
|
|
Skip.IfNot(IsLiveLaneEnabled());
|
|
|
|
// Fetch live response
|
|
var live = await Connector.FetchLatestAsync();
|
|
|
|
// Compare schema (not values) against fixture
|
|
var fixture = await LoadFixture<NvdResponse>("nvd-typical.json");
|
|
|
|
AssertSchemaMatches(live, fixture);
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Lane", "Live")]
|
|
public async Task LiveFetch_ReturnsValidAdvisories()
|
|
{
|
|
Skip.IfNot(IsLiveLaneEnabled());
|
|
|
|
var result = await Connector.FetchLatestAsync();
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.NotEmpty(result.Value.Advisories);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Configuration
|
|
|
|
Live tests are:
|
|
- **Never PR-gating** — run only in scheduled/nightly jobs
|
|
- **Opt-in** — require explicit `LIVE_TESTS_ENABLED=true` environment variable
|
|
- **Alerting** — schema drift triggers notification, not failure
|
|
|
|
---
|
|
|
|
## 6. Fixture Updater
|
|
|
|
### Purpose
|
|
|
|
Refresh fixtures from live sources when upstream schemas change intentionally.
|
|
|
|
### Usage
|
|
|
|
```bash
|
|
# Update all fixtures for NVD connector
|
|
dotnet run --project tools/FixtureUpdater -- \
|
|
--connector nvd \
|
|
--output src/Concelier/__Tests/StellaOps.Concelier.Connector.Nvd.Tests/Nvd/Fixtures/
|
|
|
|
# Update specific fixture
|
|
dotnet run --project tools/FixtureUpdater -- \
|
|
--connector nvd \
|
|
--cve CVE-2024-0001 \
|
|
--output src/Concelier/__Tests/.../Fixtures/nvd-CVE-2024-0001.json
|
|
|
|
# Dry-run mode (show diff without writing)
|
|
dotnet run --project tools/FixtureUpdater -- \
|
|
--connector nvd \
|
|
--dry-run
|
|
```
|
|
|
|
### Workflow
|
|
|
|
1. Live test detects schema drift
|
|
2. CI creates draft PR with fixture update
|
|
3. Developer reviews diff for intentional vs accidental changes
|
|
4. If intentional: update parser and merge
|
|
5. If accidental: investigate upstream API issue
|
|
|
|
---
|
|
|
|
## 7. Test Traits and CI Integration
|
|
|
|
### Trait Assignment
|
|
|
|
| Test Category | Trait | Lane | PR-Gating |
|
|
|---------------|-------|------|-----------|
|
|
| Parser tests | `[Trait("Lane", "Unit")]` | Unit | Yes |
|
|
| Resilience tests | `[Trait("Lane", "Unit")]` | Unit | Yes |
|
|
| Security tests | `[Trait("Lane", "Security")]` | Security | Yes |
|
|
| Live tests | `[Trait("Lane", "Live")]` | Live | No |
|
|
|
|
### Running Tests
|
|
|
|
```bash
|
|
# Run all connector unit tests
|
|
dotnet test --filter "Lane=Unit" src/Concelier/__Tests/
|
|
|
|
# Run security tests
|
|
dotnet test --filter "Lane=Security" src/Concelier/__Tests/
|
|
|
|
# Run live tests (requires LIVE_TESTS_ENABLED=true)
|
|
LIVE_TESTS_ENABLED=true dotnet test --filter "Lane=Live" src/Concelier/__Tests/
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Connector Inventory
|
|
|
|
### Concelier Connectors (Advisory Sources)
|
|
|
|
| Connector | Fixtures | Parser Tests | Resilience | Security | Live |
|
|
|-----------|----------|--------------|------------|----------|------|
|
|
| NVD | ✅ | ✅ | ✅ | ⬜ | ⬜ |
|
|
| OSV | ✅ | ✅ | ⬜ | ⬜ | ⬜ |
|
|
| GHSA | ✅ | ✅ | ⬜ | ⬜ | ⬜ |
|
|
| CVE | ✅ | ✅ | ⬜ | ⬜ | ⬜ |
|
|
| KEV | ✅ | ✅ | ⬜ | ⬜ | ⬜ |
|
|
| EPSS | ✅ | ✅ | ⬜ | ⬜ | ⬜ |
|
|
| Distro.Alpine | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
|
|
| Distro.Debian | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
|
|
| Distro.RedHat | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
|
|
| Distro.Suse | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
|
|
| Distro.Ubuntu | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
|
|
| Vndr.Adobe | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
|
|
| Vndr.Apple | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
|
|
| Vndr.Cisco | ✅ | ✅ | ⬜ | ⬜ | ⬜ |
|
|
| Vndr.Msrc | ✅ | ✅ | ⬜ | ⬜ | ⬜ |
|
|
| Vndr.Oracle | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
|
|
| Vndr.Vmware | ✅ | ✅ | ⬜ | ⬜ | ⬜ |
|
|
| Cert.Bund | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
|
|
| Cert.Cc | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
|
|
| Cert.Fr | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
|
|
| ICS.Cisa | ✅ | ✅ | ⬜ | ⬜ | ⬜ |
|
|
|
|
### Excititor Connectors (VEX Sources)
|
|
|
|
| Connector | Fixtures | Parser Tests | Resilience | Security | Live |
|
|
|-----------|----------|--------------|------------|----------|------|
|
|
| OpenVEX | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
|
|
| CSAF/VEX | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [ConnectorHttpFixture](../../src/__Libraries/StellaOps.TestKit/Connectors/ConnectorHttpFixture.cs)
|
|
- [ConnectorTestBase](../../src/__Libraries/StellaOps.TestKit/Connectors/ConnectorTestBase.cs)
|
|
- [ConnectorResilienceTestBase](../../src/__Libraries/StellaOps.TestKit/Connectors/ConnectorResilienceTestBase.cs)
|
|
- [FixtureUpdater](../../src/__Libraries/StellaOps.TestKit/Connectors/FixtureUpdater.cs)
|
|
- [Testing Strategy Models](./testing-strategy-models.md)
|
|
- [CI Lane Filters](./ci-lane-filters.md)
|
|
|
|
---
|
|
|
|
*Last updated: 2025-06-30 · Sprint 5100.0007.0005*
|