Files
natsdotnet/tests/NATS.Server.Monitoring.Tests/Events/OcspEventTests.cs
Joseph Doherty 0c086522a4 refactor: extract NATS.Server.Monitoring.Tests project
Move 39 monitoring, events, and system endpoint test files from
NATS.Server.Tests into a dedicated NATS.Server.Monitoring.Tests project.
Update namespaces, replace private GetFreePort/ReadUntilAsync with
TestUtilities shared helpers, add InternalsVisibleTo, and register
in the solution file. All 439 tests pass.
2026-03-12 15:44:12 -04:00

267 lines
11 KiB
C#

// 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;
/// <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");
}
}