diff --git a/tests/Trax.Cli.Tests/Fixtures/Schemas/coverage-gaps.json b/tests/Trax.Cli.Tests/Fixtures/Schemas/coverage-gaps.json new file mode 100644 index 0000000..d0ee1b7 --- /dev/null +++ b/tests/Trax.Cli.Tests/Fixtures/Schemas/coverage-gaps.json @@ -0,0 +1,121 @@ +{ + "openapi": "3.0.0", + "info": { "title": "Coverage Gaps", "version": "1.0" }, + "paths": { + "/widgets": { + "get": { + "responses": { "200": { "description": "ok" } } + } + }, + "/things/{thingId}": { + "get": { + "operationId": "getThing", + "parameters": [ + { "name": "thingId", "in": "path", "required": true, "schema": { "type": "string" } } + ], + "responses": { "200": { "description": "ok" } } + } + }, + "/things/{thingId}/copy": { + "get": { + "operationId": "getThing", + "parameters": [ + { "name": "thingId", "in": "path", "required": true, "schema": { "type": "string" } } + ], + "responses": { "200": { "description": "ok" } } + } + }, + "/things/{thingId}/twin": { + "get": { + "operationId": "getThing", + "parameters": [ + { "name": "thingId", "in": "path", "required": true, "schema": { "type": "string" } } + ], + "responses": { "200": { "description": "ok" } } + } + }, + "/empties": { + "get": { + "operationId": "listEmpties", + "responses": { + "200": { + "description": "ok", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "single": { "$ref": "#/components/schemas/EmptyShape" }, + "many": { + "type": "array", + "items": { "$ref": "#/components/schemas/EmptyShape" } + } + } + } + } + } + } + } + } + }, + "/inline-promote": { + "post": { + "operationId": "submitPayload", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "payload": { + "type": "object", + "properties": { + "value": { "type": "string" } + } + } + } + } + } + } + }, + "responses": { "200": { "description": "ok" } } + } + }, + "/inline-promote/again": { + "post": { + "operationId": "submitAnother", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "payload": { + "type": "object", + "properties": { + "value": { "type": "string" } + } + } + } + } + } + } + }, + "responses": { "200": { "description": "ok" } } + } + } + }, + "components": { + "schemas": { + "Widgets": { + "type": "object", + "properties": { + "name": { "type": "string" } + } + }, + "EmptyShape": { + "type": "object" + } + } + } +} diff --git a/tests/Trax.Cli.Tests/UnitTests/OpenApiCoverageGapsExtraTests.cs b/tests/Trax.Cli.Tests/UnitTests/OpenApiCoverageGapsExtraTests.cs new file mode 100644 index 0000000..5f3eb94 --- /dev/null +++ b/tests/Trax.Cli.Tests/UnitTests/OpenApiCoverageGapsExtraTests.cs @@ -0,0 +1,73 @@ +using FluentAssertions; +using Trax.Cli.Schema.OpenApi; + +namespace Trax.Cli.Tests.UnitTests; + +[TestFixture] +public class OpenApiCoverageGapsExtraTests +{ + private static string FixturePath(string name) => + Path.Combine(TestContext.CurrentContext.TestDirectory, "Fixtures", "Schemas", name); + + [Test] + public void Parse_SynthesizedNameCollidesWithComponent_PrefixesWithHttpVerb() + { + // /widgets GET has no operationId → synthesized name "Widgets" matches the + // "Widgets" component schema, so the operation name gets the GET verb prefix. + var parser = new OpenApiSchemaParser(); + var schema = parser.Parse(FixturePath("coverage-gaps.json")); + + schema.Operations.Should().Contain(o => o.Name == "GetWidgets"); + schema.Types.Should().Contain(t => t.Name == "Widgets"); + } + + [Test] + public void Parse_OperationCollisionByPathParam_FallsBackToNumericAfterDisambiguation() + { + // Three operations with id "getThing" each with a single path param "thingId". + // First wins "GetThing", second disambiguates to "GetThingByThingId", + // third has no fresh disambiguation and falls back to numeric suffix. + var parser = new OpenApiSchemaParser(); + var schema = parser.Parse(FixturePath("coverage-gaps.json")); + + var names = schema + .Operations.Where(o => o.Name.StartsWith("GetThing", StringComparison.Ordinal)) + .Select(o => o.Name) + .ToList(); + + names.Should().HaveCount(3); + names.Should().Contain("GetThing"); + names.Should().Contain("GetThingByThingId"); + names.Should().Contain(n => n.EndsWith("2", StringComparison.Ordinal)); + } + + [Test] + public void Parse_EmptyComponentReferencedDirectAndInList_RewrittenToObject() + { + // EmptyShape has no properties → resolved type has zero fields. + // Direct field "single" of type EmptyShape → "object". + // List field "many" → "List". + var parser = new OpenApiSchemaParser(); + var schema = parser.Parse(FixturePath("coverage-gaps.json")); + + var output = schema.Operations.Single(o => o.Name == "ListEmpties").OutputType!; + var single = output.Fields.Single(f => f.Name == "Single"); + var many = output.Fields.Single(f => f.Name == "Many"); + + single.TypeName.Should().Be("object"); + many.TypeName.Should().Be("List"); + } + + [Test] + public void Parse_InlinePromotedNameCollision_GetsNumericSuffix() + { + // Two distinct operations each declare an inline "payload" object. + // PromoteInlineObject → EnsureUniqueName: first "Payload", second "Payload2". + var parser = new OpenApiSchemaParser(); + var schema = parser.Parse(FixturePath("coverage-gaps.json")); + + var typeNames = schema.Types.Select(t => t.Name).ToList(); + typeNames.Should().Contain("Payload"); + typeNames.Should().Contain("Payload2"); + } +}