diff --git a/tests/OpenCode.Sdk.Tests/Common/Errors/DeclaredErrorTests.cs b/tests/OpenCode.Sdk.Tests/Common/Errors/DeclaredErrorTests.cs new file mode 100644 index 000000000..0b938b15b --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/Common/Errors/DeclaredErrorTests.cs @@ -0,0 +1,99 @@ +using System.Globalization; +using System.Net; +using System.Text; +using System.Text.Json; +using OpenCode.Sdk.Models; +using OpenCode.Sdk.Tests.Common.Support; +using OpenCode.Sdk.Tests.Harness; +using OpenCode.Sdk.Tests.Support; + +namespace OpenCode.Sdk.Tests.Common.Errors; + +public sealed class DeclaredErrorTests +{ + public static IEnumerable> EveryDoor() => + [ + static () => new( + "worktree.create:400:InvalidRequestError", + new DeclaredErrorDoor( + nameof(WorktreesClient.CreateWorktreeAsync), + "worktree.create", + "post", + "/api/worktree", + static async (client, options) => await client.Worktrees.CreateWorktreeAsync( + new WorktreeCreateRequest { ProjectId = "prj_1", Directory = "/repo/feature" }, options)), + HttpStatusCode.BadRequest, + WireBodyData.InvalidRequestError, + "InvalidRequestError", + typeof(InvalidRequestError), + "#/paths/~1api~1worktree/post/responses/400/content/application~1json/schema/anyOf/1"), + ]; + + [Test] + [MethodDataSource(nameof(EveryDoor))] + [DisplayName(nameof(Door_Should_Throw_The_Declared_Error) + "($row)")] + public async Task Door_Should_Throw_The_Declared_Error(DeclaredErrorCase row) + { + using var scenario = ContractScenario.Responding(row.Status, row.WireBody); + + var exception = await Assert.That(async () => _ = await row.Door.Invoke(scenario.Client, null)) + .Throws(); + + await Assert.That(exception!.Status).IsEqualTo((int)row.Status); + await Assert.That(exception.Error?.GetType()).IsEqualTo(row.ErrorType); + } + + [Test] + [MethodDataSource(nameof(EveryDoor))] + [DisplayName(nameof(Door_Should_Return_The_Declared_Error_On_NoThrow) + "($row)")] + public async Task Door_Should_Return_The_Declared_Error_On_NoThrow(DeclaredErrorCase row) + { + using var scenario = ContractScenario.Responding(row.Status, row.WireBody); + + var response = await row.Door.Invoke(scenario.Client, OpenCodeRequestOptions.NoThrow); + + await Assert.That(response.IsError).IsTrue(); + await Assert.That(response.Status).IsEqualTo((int)row.Status); + await Assert.That(response.Error?.GetType()).IsEqualTo(row.ErrorType); + } + + [Test] + [MethodDataSource(nameof(EveryDoor))] + [DisplayName(nameof(Every_Row_Should_Match_The_Pinned_Declaration) + "($row)")] + public async Task Every_Row_Should_Match_The_Pinned_Declaration(DeclaredErrorCase row) + { + using var document = JsonDocument.Parse(new FixtureLoader().LoadJson("Pinned.openapi.json")); + var operation = document.RootElement.GetProperty("paths").GetProperty(row.Door.Path) + .GetProperty(row.Door.HttpMethod); + await Assert.That(operation.GetProperty("operationId").GetString()).IsEqualTo(row.Door.OperationId); + + var operationPointer = "#/paths/" + new StringBuilder(row.Door.Path).Replace("~", "~0").Replace("/", "~1") + + "/" + row.Door.HttpMethod + "/responses/"; + await Assert.That(row.JsonPointer).StartsWith(operationPointer); + var status = row.JsonPointer[operationPointer.Length..].Split('/')[0]; + await Assert.That(status).IsEqualTo(((int)row.Status).ToString(CultureInfo.InvariantCulture)); + + var branch = ResolvePointer(document.RootElement, row.JsonPointer); + // GetProperty throws when the branch has no $ref, and a pinned $ref value is a JSON string, so GetString() is not null. + var reference = branch.GetProperty("$ref").GetString()!; + await Assert.That(reference).StartsWith("#/components/schemas/"); + var component = ResolvePointer(document.RootElement, reference); + var tags = component.GetProperty("properties").GetProperty("_tag").GetProperty("enum"); + await Assert.That(tags.GetArrayLength()).IsEqualTo(1); + await Assert.That(tags[0].GetString()).IsEqualTo(row.Tag); + } + + private static JsonElement ResolvePointer(JsonElement root, string pointer) + { + var current = root; + foreach (var segment in pointer.Split('/').Skip(1)) + { + var key = new StringBuilder(segment).Replace("~1", "/").Replace("~0", "~").ToString(); + current = current.ValueKind == JsonValueKind.Array + ? current[int.Parse(key, CultureInfo.InvariantCulture)] + : current.GetProperty(key); + } + + return current; + } +} diff --git a/tests/OpenCode.Sdk.Tests/Common/Support/DeclaredErrorCase.cs b/tests/OpenCode.Sdk.Tests/Common/Support/DeclaredErrorCase.cs new file mode 100644 index 000000000..f5d736118 --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/Common/Support/DeclaredErrorCase.cs @@ -0,0 +1,15 @@ +using System.Net; + +namespace OpenCode.Sdk.Tests.Common.Support; + +public readonly record struct DeclaredErrorCase( + string CaseId, + DeclaredErrorDoor Door, + HttpStatusCode Status, + string WireBody, + string Tag, + Type ErrorType, + string JsonPointer) +{ + public override string ToString() => CaseId + " " + Door.MemberName; +} diff --git a/tests/OpenCode.Sdk.Tests/Common/Support/DeclaredErrorDoor.cs b/tests/OpenCode.Sdk.Tests/Common/Support/DeclaredErrorDoor.cs new file mode 100644 index 000000000..5a4b1ae1c --- /dev/null +++ b/tests/OpenCode.Sdk.Tests/Common/Support/DeclaredErrorDoor.cs @@ -0,0 +1,8 @@ +namespace OpenCode.Sdk.Tests.Common.Support; + +public sealed record DeclaredErrorDoor( + string MemberName, + string OperationId, + string HttpMethod, + string Path, + Func> Invoke); diff --git a/tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj b/tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj index 934d6f9f5..55b3da297 100644 --- a/tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj +++ b/tests/OpenCode.Sdk.Tests/OpenCode.Sdk.Tests.csproj @@ -68,6 +68,7 @@ + diff --git a/tests/OpenCode.Sdk.Tests/Worktrees/WorktreesClientContractTests.cs b/tests/OpenCode.Sdk.Tests/Worktrees/WorktreesClientContractTests.cs index 9c49acea2..f5f8754e7 100644 --- a/tests/OpenCode.Sdk.Tests/Worktrees/WorktreesClientContractTests.cs +++ b/tests/OpenCode.Sdk.Tests/Worktrees/WorktreesClientContractTests.cs @@ -78,19 +78,6 @@ public async Task CreateWorktreeAsync_Should_Throw_The_Declared_400_Worktree_Err await Assert.That(error.Data.ForceRequired).IsTrue(); } - [Test] - public async Task CreateWorktreeAsync_Should_Throw_The_Other_Declared_400_Error() - { - using var scenario = ContractScenario.Responding(HttpStatusCode.BadRequest, WireBodyData.InvalidRequestError); - - var exception = await Assert - .That(async () => _ = await scenario.Client.Worktrees.CreateWorktreeAsync(CreateRequest())) - .Throws(); - - await Assert.That(exception!.Status).IsEqualTo(400); - await Assert.That(exception.Error).IsTypeOf(); - } - [Test] public async Task CreateWorktreeAsync_Should_Return_The_401_Error_On_The_NoThrow_Spine() { diff --git a/tools/jscpd/tests.baseline.json b/tools/jscpd/tests.baseline.json index a8eb5ad05..4ab6bb5f5 100644 --- a/tools/jscpd/tests.baseline.json +++ b/tools/jscpd/tests.baseline.json @@ -77,7 +77,6 @@ "16581e5dac5d2421": 1, "171c27993e2f8380": 1, "17537e2a94773132": 1, - "17ed203ca957f805": 1, "18b20d9b8321f296": 1, "1909d5d413d0fafa": 1, "196f4000011d069a": 1, @@ -347,7 +346,6 @@ "7eda9e729d5869ca": 1, "7f8b8b1b7f8ac7c9": 1, "7ffcd9e9160f6b45": 1, - "804568ff414ee4ab": 1, "80cc7036ff095cb9": 1, "80df9c5748306399": 1, "813356530d4e26c5": 1,