Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/JwtTokenServiceTests.cs
T
Joseph Doherty 64e3fbe035
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
docs: backfill XML documentation across 756 files
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
2026-05-28 08:10:17 -04:00

85 lines
3.0 KiB
C#

using System.Security.Claims;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Security.Jwt;
namespace ZB.MOM.WW.OtOpcUa.Security.Tests;
public sealed class JwtTokenServiceTests
{
private const string TestKey = "this-is-a-32-byte-test-signing-key!!";
private static JwtTokenService NewService(string key = TestKey, int expiryMinutes = 15) =>
new(Options.Create(new JwtOptions
{
SigningKey = key,
Issuer = "otopcua-test",
Audience = "otopcua-test",
ExpiryMinutes = expiryMinutes,
}), NullLogger<JwtTokenService>.Instance);
/// <summary>Verifies that a short signing key throws InvalidOperationException.</summary>
[Fact]
public void Short_signing_key_throws()
{
Should.Throw<InvalidOperationException>(() => NewService("too-short"));
}
/// <summary>Verifies that issued tokens validate and preserve claims.</summary>
[Fact]
public void Issue_then_validate_roundtrips_claims()
{
var jwt = NewService();
var token = jwt.Issue("Joe User", "joe", new[] { "ReadOnly", "AlarmAck" });
jwt.TryValidate(token, out var principal).ShouldBeTrue();
principal.ShouldNotBeNull();
principal!.FindFirst(JwtTokenService.UsernameClaimType)!.Value.ShouldBe("joe");
principal.FindFirst(JwtTokenService.DisplayNameClaimType)!.Value.ShouldBe("Joe User");
principal.FindAll(JwtTokenService.RoleClaimType)
.Select(c => c.Value).ShouldBe(new[] { "ReadOnly", "AlarmAck" }, ignoreOrder: true);
}
/// <summary>Verifies that tampered tokens are rejected during validation.</summary>
[Fact]
public void Tampered_token_is_rejected()
{
var jwt = NewService();
var token = jwt.Issue("Joe", "joe", Array.Empty<string>());
// Corrupt the payload — flip a char in the middle segment.
var parts = token.Split('.');
parts[1] = parts[1][..^2] + "AA";
var tampered = string.Join('.', parts);
jwt.TryValidate(tampered, out var principal).ShouldBeFalse();
principal.ShouldBeNull();
}
/// <summary>Verifies that expired tokens are rejected during validation.</summary>
[Fact]
public void Expired_token_is_rejected()
{
// Issue with -1 min expiry so it's already past at validate time.
var jwt = NewService(expiryMinutes: -1);
var token = jwt.Issue("Joe", "joe", Array.Empty<string>());
jwt.TryValidate(token, out var principal).ShouldBeFalse();
principal.ShouldBeNull();
}
/// <summary>Verifies that tokens signed with a different key are rejected.</summary>
[Fact]
public void Cross_key_token_is_rejected()
{
var issuer = NewService();
var token = issuer.Issue("Joe", "joe", Array.Empty<string>());
var different = NewService("a-different-32-byte-signing-key!!!");
different.TryValidate(token, out var principal).ShouldBeFalse();
principal.ShouldBeNull();
}
}