// 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.Monitoring.Tests.Events; /// /// Tests for , , /// , and the enum. /// Go reference: ocsp.go — OCSP peer verification advisory events (Gap 10.10). /// public class OcspEventTests { // ======================================================================== // OcspPeerRejectEventMsg // Go reference: ocsp.go postOCSPPeerRejectEvent // ======================================================================== /// /// EventType constant must match the Go advisory type string. /// Go reference: ocsp.go — "io.nats.server.advisory.v1.ocsp_peer_reject". /// [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); } /// /// Each OcspPeerRejectEventMsg gets its own unique non-empty Id. /// Go reference: ocsp.go — nuid.Next() generates a unique event ID per advisory. /// [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 // ======================================================================== /// /// EventType constant must match the advisory type string. /// Go reference: ocsp.go — "io.nats.server.advisory.v1.ocsp_chain_validation". /// [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); } /// /// All optional fields on OcspChainValidationEvent can be assigned. /// Go reference: ocsp.go — chain validation advisory carries cert metadata. /// [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 // ======================================================================== /// /// BuildPeerReject populates all required fields of OcspPeerRejectEventMsg. /// Go reference: ocsp.go postOCSPPeerRejectEvent — kind and reason always set. /// [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); } /// /// BuildChainValidation populates all fields of OcspChainValidationEvent. /// Go reference: ocsp.go — chain validation advisory carries full cert metadata. /// [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 // ======================================================================== /// /// ParseStatus("good") returns OcspStatus.Good. /// Go reference: ocsp.go — ocspStatusGood = "good". /// [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); } /// /// ParseStatus("revoked") returns OcspStatus.Revoked. /// Go reference: ocsp.go — ocspStatusRevoked = "revoked". /// [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); } /// /// ParseStatus with null or unrecognised string returns OcspStatus.Unknown. /// Go reference: ocsp.go — unknown/unrecognised OCSP status treated as unknown. /// [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 // ======================================================================== /// /// OcspPeerRejectEventMsg and OcspChainValidationEvent round-trip through JSON /// preserving all set fields. /// Go reference: ocsp.go — OCSP advisories serialised as JSON before publishing. /// [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(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(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"); } }