feat: add OCSP peer reject and chain validation events (Gap 10.10)
Add OcspChainValidationEvent DTO, OcspStatus enum, and OcspEventBuilder helper with BuildPeerReject, BuildChainValidation, and ParseStatus methods. Register OcspChainValidationEvent in EventJsonContext source-gen context. Add OcspPeerReject and OcspChainValidation subject constants to EventSubjects. 10 new tests in OcspEventTests cover all DTOs, builder methods, status parsing, and JSON round-trip.
This commit is contained in:
266
tests/NATS.Server.Tests/Events/OcspEventTests.cs
Normal file
266
tests/NATS.Server.Tests/Events/OcspEventTests.cs
Normal file
@@ -0,0 +1,266 @@
|
||||
// Port of Go server/ocsp_test.go — OCSP peer reject and chain validation advisory tests.
|
||||
// Go reference: golang/nats-server/server/ocsp.go — postOCSPPeerRejectEvent,
|
||||
// OCSP chain validation advisory publishing (Gap 10.10).
|
||||
//
|
||||
// Tests cover: OcspPeerRejectEventMsg, OcspChainValidationEvent, OcspEventBuilder,
|
||||
// OcspStatus enum, subject constants, and JSON serialisation round-trip.
|
||||
|
||||
using System.Text.Json;
|
||||
using NATS.Server.Events;
|
||||
|
||||
namespace NATS.Server.Tests.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="OcspPeerRejectEventMsg"/>, <see cref="OcspChainValidationEvent"/>,
|
||||
/// <see cref="OcspEventBuilder"/>, and the <see cref="OcspStatus"/> enum.
|
||||
/// Go reference: ocsp.go — OCSP peer verification advisory events (Gap 10.10).
|
||||
/// </summary>
|
||||
public class OcspEventTests
|
||||
{
|
||||
// ========================================================================
|
||||
// OcspPeerRejectEventMsg
|
||||
// Go reference: ocsp.go postOCSPPeerRejectEvent
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// EventType constant must match the Go advisory type string.
|
||||
/// Go reference: ocsp.go — "io.nats.server.advisory.v1.ocsp_peer_reject".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OcspPeerRejectEventMsg_HasCorrectEventType()
|
||||
{
|
||||
// Go reference: ocsp.go — advisory type constant.
|
||||
OcspPeerRejectEventMsg.EventType.ShouldBe("io.nats.server.advisory.v1.ocsp_peer_reject");
|
||||
var ev = new OcspPeerRejectEventMsg();
|
||||
ev.Type.ShouldBe(OcspPeerRejectEventMsg.EventType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Each OcspPeerRejectEventMsg gets its own unique non-empty Id.
|
||||
/// Go reference: ocsp.go — nuid.Next() generates a unique event ID per advisory.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OcspPeerRejectEventMsg_GeneratesUniqueId()
|
||||
{
|
||||
// Go reference: ocsp.go — each advisory is assigned a unique ID.
|
||||
var ev1 = new OcspPeerRejectEventMsg { Id = EventBuilder.GenerateEventId() };
|
||||
var ev2 = new OcspPeerRejectEventMsg { Id = EventBuilder.GenerateEventId() };
|
||||
ev1.Id.ShouldNotBeNullOrEmpty();
|
||||
ev2.Id.ShouldNotBeNullOrEmpty();
|
||||
ev1.Id.ShouldNotBe(ev2.Id);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// OcspChainValidationEvent
|
||||
// Go reference: ocsp.go — OCSP chain validation advisory
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// EventType constant must match the advisory type string.
|
||||
/// Go reference: ocsp.go — "io.nats.server.advisory.v1.ocsp_chain_validation".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OcspChainValidationEvent_HasCorrectEventType()
|
||||
{
|
||||
// Go reference: ocsp.go — chain validation advisory type constant.
|
||||
OcspChainValidationEvent.EventType.ShouldBe("io.nats.server.advisory.v1.ocsp_chain_validation");
|
||||
var ev = new OcspChainValidationEvent();
|
||||
ev.Type.ShouldBe(OcspChainValidationEvent.EventType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All optional fields on OcspChainValidationEvent can be assigned.
|
||||
/// Go reference: ocsp.go — chain validation advisory carries cert metadata.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OcspChainValidationEvent_AllFieldsSettable()
|
||||
{
|
||||
// Go reference: ocsp.go — OCSP advisory fields: subject, issuer, serial, status.
|
||||
var server = new EventServerInfo { Id = "srv1", Name = "test-server" };
|
||||
var checkedAt = new DateTime(2026, 2, 25, 12, 0, 0, DateTimeKind.Utc);
|
||||
var ev = new OcspChainValidationEvent
|
||||
{
|
||||
Server = server,
|
||||
CertSubject = "CN=leaf.example.com",
|
||||
CertIssuer = "CN=Intermediate CA",
|
||||
SerialNumber = "0123456789abcdef",
|
||||
OcspStatus = "good",
|
||||
CheckedAt = checkedAt,
|
||||
Error = null,
|
||||
};
|
||||
|
||||
ev.Server.ShouldBeSameAs(server);
|
||||
ev.CertSubject.ShouldBe("CN=leaf.example.com");
|
||||
ev.CertIssuer.ShouldBe("CN=Intermediate CA");
|
||||
ev.SerialNumber.ShouldBe("0123456789abcdef");
|
||||
ev.OcspStatus.ShouldBe("good");
|
||||
ev.CheckedAt.ShouldBe(checkedAt);
|
||||
ev.Error.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// OcspEventBuilder
|
||||
// Go reference: ocsp.go — postOCSPPeerRejectEvent, chain validation advisory
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// BuildPeerReject populates all required fields of OcspPeerRejectEventMsg.
|
||||
/// Go reference: ocsp.go postOCSPPeerRejectEvent — kind and reason always set.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildPeerReject_PopulatesAllFields()
|
||||
{
|
||||
// Go reference: ocsp.go — peer reject advisory carries kind + reason.
|
||||
var ev = OcspEventBuilder.BuildPeerReject(
|
||||
serverId: "srv-abc",
|
||||
serverName: "my-server",
|
||||
kind: "client",
|
||||
reason: "certificate revoked");
|
||||
|
||||
ev.Id.ShouldNotBeNullOrEmpty();
|
||||
ev.Type.ShouldBe(OcspPeerRejectEventMsg.EventType);
|
||||
ev.Server.ShouldNotBeNull();
|
||||
ev.Server.Id.ShouldBe("srv-abc");
|
||||
ev.Server.Name.ShouldBe("my-server");
|
||||
ev.Kind.ShouldBe("client");
|
||||
ev.Reason.ShouldBe("certificate revoked");
|
||||
ev.Time.ShouldNotBe(default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BuildChainValidation populates all fields of OcspChainValidationEvent.
|
||||
/// Go reference: ocsp.go — chain validation advisory carries full cert metadata.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildChainValidation_PopulatesAllFields()
|
||||
{
|
||||
// Go reference: ocsp.go — chain validation advisory fields.
|
||||
var before = DateTime.UtcNow;
|
||||
var ev = OcspEventBuilder.BuildChainValidation(
|
||||
serverId: "srv-xyz",
|
||||
serverName: "nats-1",
|
||||
certSubject: "CN=client.example.com",
|
||||
certIssuer: "CN=Root CA",
|
||||
serialNumber: "deadbeef",
|
||||
ocspStatus: "good",
|
||||
error: null);
|
||||
var after = DateTime.UtcNow;
|
||||
|
||||
ev.Id.ShouldNotBeNullOrEmpty();
|
||||
ev.Type.ShouldBe(OcspChainValidationEvent.EventType);
|
||||
ev.Server.ShouldNotBeNull();
|
||||
ev.Server!.Id.ShouldBe("srv-xyz");
|
||||
ev.Server.Name.ShouldBe("nats-1");
|
||||
ev.CertSubject.ShouldBe("CN=client.example.com");
|
||||
ev.CertIssuer.ShouldBe("CN=Root CA");
|
||||
ev.SerialNumber.ShouldBe("deadbeef");
|
||||
ev.OcspStatus.ShouldBe("good");
|
||||
ev.CheckedAt.ShouldNotBeNull();
|
||||
ev.CheckedAt!.Value.ShouldBeInRange(before, after);
|
||||
ev.Error.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// OcspStatus enum — ParseStatus
|
||||
// Go reference: ocsp.go — ocspStatusGood, ocspStatusRevoked constants
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// ParseStatus("good") returns OcspStatus.Good.
|
||||
/// Go reference: ocsp.go — ocspStatusGood = "good".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParseStatus_Good()
|
||||
{
|
||||
// Go reference: ocsp.go — ocspStatusGood.
|
||||
OcspEventBuilder.ParseStatus("good").ShouldBe(OcspStatus.Good);
|
||||
OcspEventBuilder.ParseStatus("Good").ShouldBe(OcspStatus.Good);
|
||||
OcspEventBuilder.ParseStatus("GOOD").ShouldBe(OcspStatus.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ParseStatus("revoked") returns OcspStatus.Revoked.
|
||||
/// Go reference: ocsp.go — ocspStatusRevoked = "revoked".
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParseStatus_Revoked()
|
||||
{
|
||||
// Go reference: ocsp.go — ocspStatusRevoked.
|
||||
OcspEventBuilder.ParseStatus("revoked").ShouldBe(OcspStatus.Revoked);
|
||||
OcspEventBuilder.ParseStatus("Revoked").ShouldBe(OcspStatus.Revoked);
|
||||
OcspEventBuilder.ParseStatus("REVOKED").ShouldBe(OcspStatus.Revoked);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ParseStatus with null or unrecognised string returns OcspStatus.Unknown.
|
||||
/// Go reference: ocsp.go — unknown/unrecognised OCSP status treated as unknown.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParseStatus_Unknown()
|
||||
{
|
||||
// Go reference: ocsp.go — default case maps to unknown status.
|
||||
OcspEventBuilder.ParseStatus(null).ShouldBe(OcspStatus.Unknown);
|
||||
OcspEventBuilder.ParseStatus("unknown").ShouldBe(OcspStatus.Unknown);
|
||||
OcspEventBuilder.ParseStatus("").ShouldBe(OcspStatus.Unknown);
|
||||
OcspEventBuilder.ParseStatus("invalid-status").ShouldBe(OcspStatus.Unknown);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// JSON serialisation round-trip
|
||||
// Go reference: ocsp.go — advisories are published as JSON payloads
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// OcspPeerRejectEventMsg and OcspChainValidationEvent round-trip through JSON
|
||||
/// preserving all set fields.
|
||||
/// Go reference: ocsp.go — OCSP advisories serialised as JSON before publishing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OcspEvents_SerializeToJson()
|
||||
{
|
||||
// Go reference: ocsp.go — advisories are JSON-encoded for publishing.
|
||||
var peerReject = OcspEventBuilder.BuildPeerReject(
|
||||
serverId: "srv-1",
|
||||
serverName: "node-a",
|
||||
kind: "route",
|
||||
reason: "OCSP status revoked");
|
||||
|
||||
var chainValidation = OcspEventBuilder.BuildChainValidation(
|
||||
serverId: "srv-2",
|
||||
serverName: "node-b",
|
||||
certSubject: "CN=server.nats.io",
|
||||
certIssuer: "CN=NATS CA",
|
||||
serialNumber: "cafebabe",
|
||||
ocspStatus: "revoked",
|
||||
error: "certificate has been revoked");
|
||||
|
||||
// Serialise OcspPeerRejectEventMsg
|
||||
var rejectJson = JsonSerializer.Serialize(peerReject);
|
||||
rejectJson.ShouldContain("io.nats.server.advisory.v1.ocsp_peer_reject");
|
||||
rejectJson.ShouldContain("route");
|
||||
rejectJson.ShouldContain("OCSP status revoked");
|
||||
|
||||
var rejectDeserialized = JsonSerializer.Deserialize<OcspPeerRejectEventMsg>(rejectJson);
|
||||
rejectDeserialized.ShouldNotBeNull();
|
||||
rejectDeserialized!.Type.ShouldBe(OcspPeerRejectEventMsg.EventType);
|
||||
rejectDeserialized.Kind.ShouldBe("route");
|
||||
rejectDeserialized.Reason.ShouldBe("OCSP status revoked");
|
||||
rejectDeserialized.Server.Id.ShouldBe("srv-1");
|
||||
rejectDeserialized.Server.Name.ShouldBe("node-a");
|
||||
|
||||
// Serialise OcspChainValidationEvent
|
||||
var chainJson = JsonSerializer.Serialize(chainValidation);
|
||||
chainJson.ShouldContain("io.nats.server.advisory.v1.ocsp_chain_validation");
|
||||
chainJson.ShouldContain("CN=server.nats.io");
|
||||
chainJson.ShouldContain("revoked");
|
||||
|
||||
var chainDeserialized = JsonSerializer.Deserialize<OcspChainValidationEvent>(chainJson);
|
||||
chainDeserialized.ShouldNotBeNull();
|
||||
chainDeserialized!.Type.ShouldBe(OcspChainValidationEvent.EventType);
|
||||
chainDeserialized.CertSubject.ShouldBe("CN=server.nats.io");
|
||||
chainDeserialized.CertIssuer.ShouldBe("CN=NATS CA");
|
||||
chainDeserialized.SerialNumber.ShouldBe("cafebabe");
|
||||
chainDeserialized.OcspStatus.ShouldBe("revoked");
|
||||
chainDeserialized.Error.ShouldBe("certificate has been revoked");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user