Files
natsdotnet/tests/NATS.Server.Tests/Configuration/AuthChangePropagationTests.cs
Joseph Doherty 42e072ad71 feat: add auth change propagation to existing connections (Gap 14.2)
Add PropagateAuthChanges to ConfigReloader that compares Users, Accounts,
and Authorization token between old and new NatsOptions, returning an
AuthChangeResult describing which auth fields changed for connection re-evaluation.
2026-02-25 11:46:28 -05:00

197 lines
6.1 KiB
C#

// Port of Go server/reload.go — auth change propagation tests.
// Reference: golang/nats-server/server/reload.go — authOption.Apply, usersOption.Apply.
using NATS.Server.Auth;
using NATS.Server.Configuration;
using Shouldly;
namespace NATS.Server.Tests.Configuration;
public sealed class AuthChangePropagationTests
{
// ─── helpers ────────────────────────────────────────────────────
private static User MakeUser(string username, string password = "pw") =>
new() { Username = username, Password = password };
private static NatsOptions BaseOpts() => new();
// ─── tests ──────────────────────────────────────────────────────
[Fact]
public void No_changes_returns_no_changes()
{
// Same empty options → nothing changed.
var oldOpts = BaseOpts();
var newOpts = BaseOpts();
var result = ConfigReloader.PropagateAuthChanges(oldOpts, newOpts);
result.HasChanges.ShouldBeFalse();
result.UsersChanged.ShouldBeFalse();
result.AccountsChanged.ShouldBeFalse();
result.TokenChanged.ShouldBeFalse();
}
[Fact]
public void User_added_detected()
{
// Adding a user must set UsersChanged.
var oldOpts = BaseOpts();
var newOpts = BaseOpts();
newOpts.Users = [MakeUser("alice")];
var result = ConfigReloader.PropagateAuthChanges(oldOpts, newOpts);
result.UsersChanged.ShouldBeTrue();
result.HasChanges.ShouldBeTrue();
}
[Fact]
public void User_removed_detected()
{
// Removing a user must set UsersChanged.
var oldOpts = BaseOpts();
oldOpts.Users = [MakeUser("alice")];
var newOpts = BaseOpts();
var result = ConfigReloader.PropagateAuthChanges(oldOpts, newOpts);
result.UsersChanged.ShouldBeTrue();
result.HasChanges.ShouldBeTrue();
}
[Fact]
public void Account_added_detected()
{
// Adding an account must set AccountsChanged.
var oldOpts = BaseOpts();
var newOpts = BaseOpts();
newOpts.Accounts = new Dictionary<string, AccountConfig>
{
["engineering"] = new AccountConfig()
};
var result = ConfigReloader.PropagateAuthChanges(oldOpts, newOpts);
result.AccountsChanged.ShouldBeTrue();
result.HasChanges.ShouldBeTrue();
}
[Fact]
public void Token_changed_detected()
{
// Changing the Authorization token must set TokenChanged.
var oldOpts = BaseOpts();
oldOpts.Authorization = "old-secret-token";
var newOpts = BaseOpts();
newOpts.Authorization = "new-secret-token";
var result = ConfigReloader.PropagateAuthChanges(oldOpts, newOpts);
result.TokenChanged.ShouldBeTrue();
result.HasChanges.ShouldBeTrue();
}
[Fact]
public void Multiple_changes_all_flagged()
{
// Changing both users and accounts must set both flags.
var oldOpts = BaseOpts();
oldOpts.Users = [MakeUser("alice")];
oldOpts.Accounts = new Dictionary<string, AccountConfig>
{
["acct-a"] = new AccountConfig()
};
var newOpts = BaseOpts();
newOpts.Users = [MakeUser("alice"), MakeUser("bob")];
newOpts.Accounts = new Dictionary<string, AccountConfig>
{
["acct-a"] = new AccountConfig(),
["acct-b"] = new AccountConfig()
};
var result = ConfigReloader.PropagateAuthChanges(oldOpts, newOpts);
result.UsersChanged.ShouldBeTrue();
result.AccountsChanged.ShouldBeTrue();
result.HasChanges.ShouldBeTrue();
}
[Fact]
public void Same_users_different_order_no_change()
{
// Users in a different order with the same names must NOT trigger UsersChanged
// because the comparison is set-based.
var oldOpts = BaseOpts();
oldOpts.Users = [MakeUser("alice"), MakeUser("bob")];
var newOpts = BaseOpts();
newOpts.Users = [MakeUser("bob"), MakeUser("alice")];
var result = ConfigReloader.PropagateAuthChanges(oldOpts, newOpts);
result.UsersChanged.ShouldBeFalse();
result.HasChanges.ShouldBeFalse();
}
[Fact]
public void HasChanges_true_when_any_change()
{
// A single changed field (token only) is enough to set HasChanges.
var oldOpts = BaseOpts();
var newOpts = BaseOpts();
newOpts.Authorization = "token-xyz";
var result = ConfigReloader.PropagateAuthChanges(oldOpts, newOpts);
result.HasChanges.ShouldBeTrue();
}
[Fact]
public void Empty_to_non_empty_users_detected()
{
// Going from zero users to one user must be detected.
var oldOpts = BaseOpts();
// No Users assigned — null list.
var newOpts = BaseOpts();
newOpts.Users = [MakeUser("charlie")];
var result = ConfigReloader.PropagateAuthChanges(oldOpts, newOpts);
result.UsersChanged.ShouldBeTrue();
result.HasChanges.ShouldBeTrue();
}
[Fact]
public void No_auth_to_auth_detected()
{
// Going from null Authorization to a token string must be detected.
var oldOpts = BaseOpts();
// Authorization is null by default.
var newOpts = BaseOpts();
newOpts.Authorization = "brand-new-token";
var result = ConfigReloader.PropagateAuthChanges(oldOpts, newOpts);
result.TokenChanged.ShouldBeTrue();
result.HasChanges.ShouldBeTrue();
}
[Fact]
public void Same_token_no_change()
{
// The same token value on both sides must NOT flag TokenChanged.
var oldOpts = BaseOpts();
oldOpts.Authorization = "stable-token";
var newOpts = BaseOpts();
newOpts.Authorization = "stable-token";
var result = ConfigReloader.PropagateAuthChanges(oldOpts, newOpts);
result.TokenChanged.ShouldBeFalse();
result.HasChanges.ShouldBeFalse();
}
}