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.
267 lines
11 KiB
C#
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");
|
|
}
|
|
}
|