Implements NatsConfParser with Parse, ParseFile, and ParseFileWithDigest methods. Supports nested maps/arrays, variable resolution with block scoping and environment fallback, bcrypt password literals, integer suffix multipliers, include directives, and cycle detection.
185 lines
5.8 KiB
C#
185 lines
5.8 KiB
C#
using NATS.Server.Configuration;
|
|
|
|
namespace NATS.Server.Tests;
|
|
|
|
public class NatsConfParserTests
|
|
{
|
|
[Fact]
|
|
public void Parse_SimpleTopLevel_ReturnsCorrectTypes()
|
|
{
|
|
var result = NatsConfParser.Parse("foo = '1'; bar = 2.2; baz = true; boo = 22");
|
|
result["foo"].ShouldBe("1");
|
|
result["bar"].ShouldBe(2.2);
|
|
result["baz"].ShouldBe(true);
|
|
result["boo"].ShouldBe(22L);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_Booleans_AllVariants()
|
|
{
|
|
foreach (var (input, expected) in new[] {
|
|
("true", true), ("TRUE", true), ("yes", true), ("on", true),
|
|
("false", false), ("FALSE", false), ("no", false), ("off", false)
|
|
})
|
|
{
|
|
var result = NatsConfParser.Parse($"flag = {input}");
|
|
result["flag"].ShouldBe(expected);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_IntegerWithSuffix_AppliesMultiplier()
|
|
{
|
|
var result = NatsConfParser.Parse("a = 1k; b = 2mb; c = 3gb; d = 4kb");
|
|
result["a"].ShouldBe(1000L);
|
|
result["b"].ShouldBe(2L * 1024 * 1024);
|
|
result["c"].ShouldBe(3L * 1024 * 1024 * 1024);
|
|
result["d"].ShouldBe(4L * 1024);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_NestedMap_ReturnsDictionary()
|
|
{
|
|
var result = NatsConfParser.Parse("auth { user: admin, pass: secret }");
|
|
var auth = result["auth"].ShouldBeOfType<Dictionary<string, object?>>();
|
|
auth["user"].ShouldBe("admin");
|
|
auth["pass"].ShouldBe("secret");
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_Array_ReturnsList()
|
|
{
|
|
var result = NatsConfParser.Parse("items = [1, 2, 3]");
|
|
var items = result["items"].ShouldBeOfType<List<object?>>();
|
|
items.Count.ShouldBe(3);
|
|
items[0].ShouldBe(1L);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_Variable_ResolvesFromContext()
|
|
{
|
|
var result = NatsConfParser.Parse("index = 22\nfoo = $index");
|
|
result["foo"].ShouldBe(22L);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_NestedVariable_UsesBlockScope()
|
|
{
|
|
var input = "index = 22\nnest {\n index = 11\n foo = $index\n}\nbar = $index";
|
|
var result = NatsConfParser.Parse(input);
|
|
var nest = result["nest"].ShouldBeOfType<Dictionary<string, object?>>();
|
|
nest["foo"].ShouldBe(11L);
|
|
result["bar"].ShouldBe(22L);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_EnvironmentVariable_ResolvesFromEnv()
|
|
{
|
|
Environment.SetEnvironmentVariable("NATS_TEST_VAR_12345", "hello");
|
|
try
|
|
{
|
|
var result = NatsConfParser.Parse("val = $NATS_TEST_VAR_12345");
|
|
result["val"].ShouldBe("hello");
|
|
}
|
|
finally
|
|
{
|
|
Environment.SetEnvironmentVariable("NATS_TEST_VAR_12345", null);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_UndefinedVariable_Throws()
|
|
{
|
|
Should.Throw<FormatException>(() =>
|
|
NatsConfParser.Parse("val = $UNDEFINED_VAR_XYZZY_99999"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_IncludeDirective_MergesFile()
|
|
{
|
|
var dir = Path.Combine(Path.GetTempPath(), $"nats_test_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(dir);
|
|
try
|
|
{
|
|
File.WriteAllText(Path.Combine(dir, "main.conf"), "port = 4222\ninclude \"sub.conf\"");
|
|
File.WriteAllText(Path.Combine(dir, "sub.conf"), "host = \"localhost\"");
|
|
var result = NatsConfParser.ParseFile(Path.Combine(dir, "main.conf"));
|
|
result["port"].ShouldBe(4222L);
|
|
result["host"].ShouldBe("localhost");
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(dir, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_MultipleKeySeparators_AllWork()
|
|
{
|
|
var r1 = NatsConfParser.Parse("a = 1");
|
|
var r2 = NatsConfParser.Parse("a : 1");
|
|
var r3 = NatsConfParser.Parse("a 1");
|
|
r1["a"].ShouldBe(1L);
|
|
r2["a"].ShouldBe(1L);
|
|
r3["a"].ShouldBe(1L);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_ErrorOnInvalidInput_Throws()
|
|
{
|
|
Should.Throw<FormatException>(() => NatsConfParser.Parse("= invalid"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_CommentsInsideBlocks_AreIgnored()
|
|
{
|
|
var input = "auth {\n # comment\n user: admin\n // another comment\n pass: secret\n}";
|
|
var result = NatsConfParser.Parse(input);
|
|
var auth = result["auth"].ShouldBeOfType<Dictionary<string, object?>>();
|
|
auth["user"].ShouldBe("admin");
|
|
auth["pass"].ShouldBe("secret");
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_ArrayOfMaps_Works()
|
|
{
|
|
var input = "users = [\n { user: alice, pass: pw1 }\n { user: bob, pass: pw2 }\n]";
|
|
var result = NatsConfParser.Parse(input);
|
|
var users = result["users"].ShouldBeOfType<List<object?>>();
|
|
users.Count.ShouldBe(2);
|
|
var first = users[0].ShouldBeOfType<Dictionary<string, object?>>();
|
|
first["user"].ShouldBe("alice");
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_BcryptPassword_HandledAsString()
|
|
{
|
|
var input = "pass = $2a$04$P/.bd.7unw9Ew7yWJqXsl.f4oNRLQGvadEL2YnqQXbbb.IVQajRdK";
|
|
var result = NatsConfParser.Parse(input);
|
|
((string)result["pass"]!).ShouldStartWith("$2a$");
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseFile_WithDigest_ReturnsStableHash()
|
|
{
|
|
var dir = Path.Combine(Path.GetTempPath(), $"nats_test_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(dir);
|
|
try
|
|
{
|
|
var conf = Path.Combine(dir, "test.conf");
|
|
File.WriteAllText(conf, "port = 4222\nhost = \"localhost\"");
|
|
var (result, digest) = NatsConfParser.ParseFileWithDigest(conf);
|
|
result["port"].ShouldBe(4222L);
|
|
digest.ShouldStartWith("sha256:");
|
|
digest.Length.ShouldBeGreaterThan(10);
|
|
|
|
var (_, digest2) = NatsConfParser.ParseFileWithDigest(conf);
|
|
digest2.ShouldBe(digest);
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(dir, true);
|
|
}
|
|
}
|
|
}
|