using NATS.Server.Configuration; namespace NATS.Server.Core.Tests; public class NatsConfLexerTests { [Fact] public void Lex_SimpleKeyStringValue_ReturnsKeyAndString() { var tokens = NatsConfLexer.Tokenize("foo = \"bar\"").ToList(); tokens[0].Type.ShouldBe(TokenType.Key); tokens[0].Value.ShouldBe("foo"); tokens[1].Type.ShouldBe(TokenType.String); tokens[1].Value.ShouldBe("bar"); tokens[2].Type.ShouldBe(TokenType.Eof); } [Fact] public void Lex_SingleQuotedString_ReturnsString() { var tokens = NatsConfLexer.Tokenize("foo = 'bar'").ToList(); tokens[1].Type.ShouldBe(TokenType.String); tokens[1].Value.ShouldBe("bar"); } [Fact] public void Lex_IntegerValue_ReturnsInteger() { var tokens = NatsConfLexer.Tokenize("port = 4222").ToList(); tokens[0].Type.ShouldBe(TokenType.Key); tokens[0].Value.ShouldBe("port"); tokens[1].Type.ShouldBe(TokenType.Integer); tokens[1].Value.ShouldBe("4222"); } [Fact] public void Lex_IntegerWithSuffix_ReturnsInteger() { var tokens = NatsConfLexer.Tokenize("size = 64mb").ToList(); tokens[1].Type.ShouldBe(TokenType.Integer); tokens[1].Value.ShouldBe("64mb"); } [Fact] public void Lex_BooleanValues_ReturnsBool() { foreach (var val in new[] { "true", "false", "yes", "no", "on", "off" }) { var tokens = NatsConfLexer.Tokenize($"flag = {val}").ToList(); tokens[1].Type.ShouldBe(TokenType.Bool); } } [Fact] public void Lex_FloatValue_ReturnsFloat() { var tokens = NatsConfLexer.Tokenize("rate = 2.5").ToList(); tokens[1].Type.ShouldBe(TokenType.Float); tokens[1].Value.ShouldBe("2.5"); } [Fact] public void Lex_NegativeNumber_ReturnsInteger() { var tokens = NatsConfLexer.Tokenize("offset = -10").ToList(); tokens[1].Type.ShouldBe(TokenType.Integer); tokens[1].Value.ShouldBe("-10"); } [Fact] public void Lex_DatetimeValue_ReturnsDatetime() { var tokens = NatsConfLexer.Tokenize("ts = 2024-01-15T10:30:00Z").ToList(); tokens[1].Type.ShouldBe(TokenType.DateTime); } [Fact] public void Lex_HashComment_IsIgnored() { var tokens = NatsConfLexer.Tokenize("# this is a comment\nfoo = 1").ToList(); var keys = tokens.Where(t => t.Type == TokenType.Key).ToList(); keys.Count.ShouldBe(1); keys[0].Value.ShouldBe("foo"); } [Fact] public void Lex_CommentBody_EmitsTextToken() { var tokens = NatsConfLexer.Tokenize("# this is a comment\nfoo = 1").ToList(); var commentBody = tokens.Single(t => t.Type == TokenType.Text); commentBody.Value.ShouldBe(" this is a comment"); } [Fact] public void Lex_SlashComment_IsIgnored() { var tokens = NatsConfLexer.Tokenize("// comment\nfoo = 1").ToList(); var keys = tokens.Where(t => t.Type == TokenType.Key).ToList(); keys.Count.ShouldBe(1); } [Fact] public void Lex_MapBlock_ReturnsMapStartEnd() { var tokens = NatsConfLexer.Tokenize("auth { user: admin }").ToList(); tokens[0].Type.ShouldBe(TokenType.Key); tokens[0].Value.ShouldBe("auth"); tokens[1].Type.ShouldBe(TokenType.MapStart); tokens[2].Type.ShouldBe(TokenType.Key); tokens[2].Value.ShouldBe("user"); tokens[3].Type.ShouldBe(TokenType.String); tokens[3].Value.ShouldBe("admin"); tokens[4].Type.ShouldBe(TokenType.MapEnd); } [Fact] public void Lex_Array_ReturnsArrayStartEnd() { var tokens = NatsConfLexer.Tokenize("items = [1, 2, 3]").ToList(); tokens[1].Type.ShouldBe(TokenType.ArrayStart); tokens[2].Type.ShouldBe(TokenType.Integer); tokens[2].Value.ShouldBe("1"); tokens[5].Type.ShouldBe(TokenType.ArrayEnd); } [Fact] public void Lex_Variable_ReturnsVariable() { var tokens = NatsConfLexer.Tokenize("secret = $MY_VAR").ToList(); tokens[1].Type.ShouldBe(TokenType.Variable); tokens[1].Value.ShouldBe("MY_VAR"); } [Fact] public void Lex_Include_ReturnsInclude() { var tokens = NatsConfLexer.Tokenize("include \"auth.conf\"").ToList(); tokens[0].Type.ShouldBe(TokenType.Include); tokens[0].Value.ShouldBe("auth.conf"); } [Fact] public void Lex_EscapeSequences_AreProcessed() { var tokens = NatsConfLexer.Tokenize("msg = \"hello\\tworld\\n\"").ToList(); tokens[1].Type.ShouldBe(TokenType.String); tokens[1].Value.ShouldBe("hello\tworld\n"); } [Fact] public void Lex_HexEscape_IsProcessed() { var tokens = NatsConfLexer.Tokenize("val = \"\\x41\\x42\"").ToList(); tokens[1].Value.ShouldBe("AB"); } [Fact] public void Lex_ColonSeparator_Works() { var tokens = NatsConfLexer.Tokenize("foo: bar").ToList(); tokens[0].Type.ShouldBe(TokenType.Key); tokens[1].Type.ShouldBe(TokenType.String); } [Fact] public void Lex_WhitespaceSeparator_Works() { var tokens = NatsConfLexer.Tokenize("foo bar").ToList(); tokens[0].Type.ShouldBe(TokenType.Key); tokens[1].Type.ShouldBe(TokenType.String); } [Fact] public void Lex_SemicolonTerminator_IsHandled() { var tokens = NatsConfLexer.Tokenize("foo = 1; bar = 2").ToList(); var keys = tokens.Where(t => t.Type == TokenType.Key).ToList(); keys.Count.ShouldBe(2); } [Fact] public void Lex_EmptyInput_ReturnsEof() { var tokens = NatsConfLexer.Tokenize("").ToList(); tokens.Count.ShouldBe(1); tokens[0].Type.ShouldBe(TokenType.Eof); } [Fact] public void Lex_BlockString_ReturnsString() { var input = "desc (\nthis is\na block\n)\n"; var tokens = NatsConfLexer.Tokenize(input).ToList(); tokens[0].Type.ShouldBe(TokenType.Key); tokens[1].Type.ShouldBe(TokenType.String); } [Fact] public void Lex_IPAddress_ReturnsString() { var tokens = NatsConfLexer.Tokenize("host = 127.0.0.1").ToList(); tokens[1].Type.ShouldBe(TokenType.String); tokens[1].Value.ShouldBe("127.0.0.1"); } [Fact] public void Lex_TrackLineNumbers() { var tokens = NatsConfLexer.Tokenize("a = 1\nb = 2\nc = 3").ToList(); tokens[0].Line.ShouldBe(1); // a tokens[2].Line.ShouldBe(2); // b tokens[4].Line.ShouldBe(3); // c } [Fact] public void Lex_UnterminatedString_ReturnsError() { var tokens = NatsConfLexer.Tokenize("foo = \"unterminated").ToList(); tokens.ShouldContain(t => t.Type == TokenType.Error); } [Fact] public void Lex_StringStartingWithDigit_TreatedAsString() { var tokens = NatsConfLexer.Tokenize("foo = 3xyz").ToList(); tokens[1].Type.ShouldBe(TokenType.String); tokens[1].Value.ShouldBe("3xyz"); } [Fact] public void Lex_Unicode_surrogate_pairs_in_strings_are_preserved() { var tokens = NatsConfLexer.Tokenize("msg = \"rocket🚀\"\nport = 4222").ToList(); tokens[1].Type.ShouldBe(TokenType.String); tokens[1].Value.ShouldBe("rocket🚀"); tokens[2].Line.ShouldBe(2); } }