using ZB.MOM.WW.MxGateway.Server.Security.Authentication; namespace ZB.MOM.WW.MxGateway.Tests.Security.Authentication; public sealed class ApiKeyAdminCommandLineParserTests { /// /// Verifies non-API key commands return not-an-API-key result. /// [Fact] public void Parse_NonApiKeyCommand_ReturnsNotApiKeyCommand() { ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(["--urls=http://localhost:5000"]); Assert.False(result.IsApiKeyCommand); Assert.Null(result.Command); } /// /// Verifies API key create command parsing returns options. /// [Fact] public void Parse_CreateKeyCommand_ReturnsOptions() { ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse( [ "apikey", "create-key", "--key-id", "operator01", "--display-name", "Operator", "--scopes", "session:open,events:read", "--sqlite-path", "auth.db", "--pepper", "pepper", "--json" ]); Assert.True(result.IsApiKeyCommand); Assert.Null(result.Error); Assert.NotNull(result.Command); Assert.Equal(ApiKeyAdminCommandKind.CreateKey, result.Command.Kind); Assert.True(result.Command.Json); Assert.Equal("operator01", result.Command.KeyId); Assert.Equal("Operator", result.Command.DisplayName); Assert.Equal("auth.db", result.Command.SqlitePath); Assert.Equal("pepper", result.Command.Pepper); Assert.Contains("session:open", result.Command.Scopes); Assert.Contains("events:read", result.Command.Scopes); } /// /// Server-004 regression: a create-key command with a non-canonical scope /// string (e.g. CLAUDE.md's stale invoke instead of invoke:read) /// must be rejected at parse time rather than silently persisting an /// unusable scope the authorization resolver never matches. /// [Fact] public void Parse_CreateKeyCommand_RejectsUnknownScope() { ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse( [ "apikey", "create-key", "--key-id", "operator01", "--display-name", "Operator", "--scopes", "session:open,invoke,metadata", ]); Assert.True(result.IsApiKeyCommand); Assert.Null(result.Command); Assert.NotNull(result.Error); Assert.Contains("invoke", result.Error, StringComparison.Ordinal); Assert.Contains("metadata", result.Error, StringComparison.Ordinal); } /// Verifies a create-key command with only canonical scopes parses successfully. [Fact] public void Parse_CreateKeyCommand_AcceptsAllCanonicalScopes() { ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse( [ "apikey", "create-key", "--key-id", "operator01", "--display-name", "Operator", "--scopes", "session:open,session:close,invoke:read,invoke:write,invoke:secure,events:read,metadata:read,admin", ]); Assert.True(result.IsApiKeyCommand); Assert.Null(result.Error); Assert.NotNull(result.Command); Assert.Equal(8, result.Command.Scopes.Count); } /// /// Verifies create key without display name returns error. /// [Fact] public void Parse_CreateKeyCommand_ReturnsConstraints() { ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse( [ "apikey", "create-key", "--key-id", "operator01", "--display-name", "Operator", "--read-subtree", "Area1/*", "--read-subtree", "Area2/*", "--write-tag-glob", "Pump_*", "--max-write-classification", "2", "--browse-subtree", "Area1/*", "--read-alarm-only", "--read-historized-only" ]); Assert.True(result.IsApiKeyCommand); Assert.NotNull(result.Command); ApiKeyConstraints constraints = result.Command.Constraints; Assert.Equal(["Area1/*", "Area2/*"], constraints.ReadSubtrees); Assert.Equal(["Pump_*"], constraints.WriteTagGlobs); Assert.Equal(2, constraints.MaxWriteClassification); Assert.Equal(["Area1/*"], constraints.BrowseSubtrees); Assert.True(constraints.ReadAlarmOnly); Assert.True(constraints.ReadHistorizedOnly); } [Fact] public void Parse_CreateKeyWithoutDisplayName_ReturnsError() { ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse( ["apikey", "create-key", "--key-id", "operator01"]); Assert.True(result.IsApiKeyCommand); Assert.Null(result.Command); Assert.Contains("--display-name", result.Error, StringComparison.Ordinal); } /// /// Verifies key ID with underscore returns error. /// [Fact] public void Parse_KeyIdWithUnderscore_ReturnsError() { ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse( ["apikey", "revoke-key", "--key-id", "operator_01"]); Assert.True(result.IsApiKeyCommand); Assert.Null(result.Command); Assert.Contains("letters, numbers, periods, and hyphens", result.Error, StringComparison.Ordinal); } }