diff --git a/src/Trax.Cli/Program.Coverage.cs b/src/Trax.Cli/Program.Coverage.cs new file mode 100644 index 0000000..3b59399 --- /dev/null +++ b/src/Trax.Cli/Program.Coverage.cs @@ -0,0 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + +// The CLI entry point in Program.cs is a top-level program that's never invoked +// during unit tests (tests target GenerateCommand.Handle directly). The synthesized +// Program class gets a partial declaration here so we can mark the auto-generated +// Main method as excluded from coverage rather than carrying a permanent 0%. +[ExcludeFromCodeCoverage] +internal partial class Program; diff --git a/tests/Trax.Cli.Tests/Fixtures/Schemas/openapi-more-edges.json b/tests/Trax.Cli.Tests/Fixtures/Schemas/openapi-more-edges.json new file mode 100644 index 0000000..6f275d1 --- /dev/null +++ b/tests/Trax.Cli.Tests/Fixtures/Schemas/openapi-more-edges.json @@ -0,0 +1,62 @@ +{ + "openapi": "3.0.0", + "info": { "title": "More Edges", "version": "1.0" }, + "paths": { + "/a/list": { + "get": { + "operationId": "listThings", + "responses": { "200": { "description": "ok" } } + } + }, + "/b/list": { + "get": { + "operationId": "listThings", + "responses": { "200": { "description": "ok" } } + } + }, + "/c/list": { + "get": { + "operationId": "listThings", + "responses": { "200": { "description": "ok" } } + } + }, + "/with-status": { + "get": { + "operationId": "getStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Status" } + } + } + } + } + } + }, + "/with-status-again": { + "get": { + "operationId": "getStatus2", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Status" } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Status": { + "type": "string", + "enum": ["Active", "Inactive", "Pending"] + } + } + } +} diff --git a/tests/Trax.Cli.Tests/UnitTests/OpenApiMoreEdgesTests.cs b/tests/Trax.Cli.Tests/UnitTests/OpenApiMoreEdgesTests.cs new file mode 100644 index 0000000..3452011 --- /dev/null +++ b/tests/Trax.Cli.Tests/UnitTests/OpenApiMoreEdgesTests.cs @@ -0,0 +1,52 @@ +using FluentAssertions; +using Trax.Cli.Schema.OpenApi; + +namespace Trax.Cli.Tests.UnitTests; + +[TestFixture] +public class OpenApiMoreEdgesTests +{ + private static string FixturePath(string name) => + Path.Combine(TestContext.CurrentContext.TestDirectory, "Fixtures", "Schemas", name); + + [Test] + public void Parse_OperationIdCollision_NoPathParams_FallsBackToNumericSuffix() + { + var parser = new OpenApiSchemaParser(); + var schema = parser.Parse(FixturePath("openapi-more-edges.json")); + + // Three GET ops with operationId "listThings" and no path params force the + // numeric fallback in EnsureUniqueOperationName. + var listOps = schema + .Operations.Where(o => o.Name.StartsWith("ListThings", StringComparison.Ordinal)) + .Select(o => o.Name) + .ToList(); + + listOps.Should().HaveCount(3); + listOps.Should().Contain("ListThings"); + listOps.Should().Contain(n => n.EndsWith("2") || n.EndsWith("3")); + } + + [Test] + public void Parse_StringEnumComponent_BuildsApiEnum() + { + var parser = new OpenApiSchemaParser(); + var schema = parser.Parse(FixturePath("openapi-more-edges.json")); + + var statusEnum = schema.Enums.SingleOrDefault(e => e.Name == "Status"); + statusEnum.Should().NotBeNull(); + statusEnum!.Values.Should().BeEquivalentTo("Active", "Inactive", "Pending"); + } + + [Test] + public void Parse_RepeatedRef_ReusesCachedType() + { + // Two operations both reference Status — second resolution hits the + // _resolvedTypes/_resolvedEnums cache rather than building a fresh entry. + var parser = new OpenApiSchemaParser(); + var schema = parser.Parse(FixturePath("openapi-more-edges.json")); + + // One enum entry total even though referenced twice. + schema.Enums.Where(e => e.Name == "Status").Should().HaveCount(1); + } +}