Skip to content

Commit 6e7f3d2

Browse files
authored
Merge pull request #753 from Chris0Jeky/test/714-api-error-contract-regression
Test: API error contract regression and boundary validation (#714)
2 parents 1b47f42 + 45acc73 commit 6e7f3d2

7 files changed

Lines changed: 974 additions & 0 deletions
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using System.Net;
2+
using System.Net.Http.Json;
3+
using FluentAssertions;
4+
using Taskdeck.Api.Tests.Support;
5+
using Taskdeck.Application.DTOs;
6+
using Taskdeck.Domain.Exceptions;
7+
using Xunit;
8+
9+
namespace Taskdeck.Api.Tests.ErrorContract;
10+
11+
/// <summary>
12+
/// Verifies GP-03 error contract compliance for board endpoints.
13+
/// Every 4xx response must return a structured ApiErrorResponse with
14+
/// non-empty errorCode and message.
15+
/// </summary>
16+
public class BoardErrorContractTests : IClassFixture<TestWebApplicationFactory>
17+
{
18+
private readonly TestWebApplicationFactory _factory;
19+
20+
public BoardErrorContractTests(TestWebApplicationFactory factory)
21+
{
22+
_factory = factory;
23+
}
24+
25+
[Fact]
26+
public async Task CreateBoard_EmptyName_Returns400WithErrorContract()
27+
{
28+
using var client = _factory.CreateClient();
29+
await ApiTestHarness.AuthenticateAsync(client, "board-err-empty");
30+
31+
var response = await client.PostAsJsonAsync(
32+
"/api/boards",
33+
new CreateBoardDto(string.Empty, "desc"));
34+
35+
await ApiTestHarness.AssertErrorContractAsync(response, HttpStatusCode.BadRequest, ErrorCodes.ValidationError);
36+
}
37+
38+
[Fact]
39+
public async Task CreateBoard_WhitespaceName_Returns400WithErrorContract()
40+
{
41+
using var client = _factory.CreateClient();
42+
await ApiTestHarness.AuthenticateAsync(client, "board-err-ws");
43+
44+
var response = await client.PostAsJsonAsync(
45+
"/api/boards",
46+
new CreateBoardDto(" ", "desc"));
47+
48+
await ApiTestHarness.AssertErrorContractAsync(response, HttpStatusCode.BadRequest, ErrorCodes.ValidationError);
49+
}
50+
51+
[Fact]
52+
public async Task CreateBoard_NameExceeding100Chars_Returns400WithErrorContract()
53+
{
54+
using var client = _factory.CreateClient();
55+
await ApiTestHarness.AuthenticateAsync(client, "board-err-long");
56+
57+
var longName = new string('A', 101);
58+
var response = await client.PostAsJsonAsync(
59+
"/api/boards",
60+
new CreateBoardDto(longName, "desc"));
61+
62+
await ApiTestHarness.AssertErrorContractAsync(response, HttpStatusCode.BadRequest, ErrorCodes.ValidationError);
63+
}
64+
65+
[Fact]
66+
public async Task CreateBoard_NameExactly100Chars_ReturnsCreated()
67+
{
68+
using var client = _factory.CreateClient();
69+
await ApiTestHarness.AuthenticateAsync(client, "board-err-exact");
70+
71+
var exactName = new string('B', 100);
72+
var response = await client.PostAsJsonAsync(
73+
"/api/boards",
74+
new CreateBoardDto(exactName, "desc"));
75+
76+
response.StatusCode.Should().Be(HttpStatusCode.Created);
77+
}
78+
79+
[Fact]
80+
public async Task GetBoard_NonExistentId_Returns404WithErrorContract()
81+
{
82+
using var client = _factory.CreateClient();
83+
await ApiTestHarness.AuthenticateAsync(client, "board-err-404");
84+
85+
var response = await client.GetAsync($"/api/boards/{Guid.NewGuid()}");
86+
87+
await ApiTestHarness.AssertErrorContractAsync(response, HttpStatusCode.NotFound, ErrorCodes.NotFound);
88+
}
89+
90+
[Fact]
91+
public async Task UpdateBoard_NonExistentId_Returns404WithErrorContract()
92+
{
93+
using var client = _factory.CreateClient();
94+
await ApiTestHarness.AuthenticateAsync(client, "board-err-upd404");
95+
96+
var response = await client.PutAsJsonAsync(
97+
$"/api/boards/{Guid.NewGuid()}",
98+
new UpdateBoardDto("new-name", null, null));
99+
100+
await ApiTestHarness.AssertErrorContractAsync(response, HttpStatusCode.NotFound, ErrorCodes.NotFound);
101+
}
102+
103+
[Fact]
104+
public async Task DeleteBoard_NonExistentId_Returns404WithErrorContract()
105+
{
106+
using var client = _factory.CreateClient();
107+
await ApiTestHarness.AuthenticateAsync(client, "board-err-del404");
108+
109+
var response = await client.DeleteAsync($"/api/boards/{Guid.NewGuid()}");
110+
111+
await ApiTestHarness.AssertErrorContractAsync(response, HttpStatusCode.NotFound, ErrorCodes.NotFound);
112+
}
113+
114+
[Fact]
115+
public async Task CreateBoard_SpecialCharactersInName_Succeeds()
116+
{
117+
using var client = _factory.CreateClient();
118+
await ApiTestHarness.AuthenticateAsync(client, "board-err-special");
119+
120+
var response = await client.PostAsJsonAsync(
121+
"/api/boards",
122+
new CreateBoardDto("Board <script>alert('xss')</script>", "desc"));
123+
124+
response.StatusCode.Should().Be(HttpStatusCode.Created);
125+
}
126+
127+
[Fact]
128+
public async Task UpdateBoard_EmptyName_Returns400WithErrorContract()
129+
{
130+
using var client = _factory.CreateClient();
131+
await ApiTestHarness.AuthenticateAsync(client, "board-err-updempty");
132+
var board = await ApiTestHarness.CreateBoardAsync(client, stem: "update-empty");
133+
134+
var response = await client.PutAsJsonAsync(
135+
$"/api/boards/{board.Id}",
136+
new UpdateBoardDto(string.Empty, null, null));
137+
138+
await ApiTestHarness.AssertErrorContractAsync(response, HttpStatusCode.BadRequest, ErrorCodes.ValidationError);
139+
}
140+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
using System.Net;
2+
using System.Net.Http.Json;
3+
using FluentAssertions;
4+
using Taskdeck.Api.Tests.Support;
5+
using Taskdeck.Application.DTOs;
6+
using Taskdeck.Domain.Exceptions;
7+
using Xunit;
8+
9+
namespace Taskdeck.Api.Tests.ErrorContract;
10+
11+
/// <summary>
12+
/// Verifies GP-03 error contract compliance for capture endpoints.
13+
/// Every 4xx response must return a structured ApiErrorResponse with
14+
/// non-empty errorCode and message.
15+
/// </summary>
16+
public class CaptureErrorContractTests : IClassFixture<TestWebApplicationFactory>
17+
{
18+
private readonly TestWebApplicationFactory _factory;
19+
20+
public CaptureErrorContractTests(TestWebApplicationFactory factory)
21+
{
22+
_factory = factory;
23+
}
24+
25+
[Fact]
26+
public async Task CreateCapture_EmptyText_Returns400WithErrorContract()
27+
{
28+
using var client = _factory.CreateClient();
29+
await ApiTestHarness.AuthenticateAsync(client, "cap-err-empty");
30+
31+
var response = await client.PostAsJsonAsync(
32+
"/api/capture/items",
33+
new CreateCaptureItemDto(null, string.Empty));
34+
35+
await ApiTestHarness.AssertErrorContractAsync(response, HttpStatusCode.BadRequest, ErrorCodes.ValidationError);
36+
}
37+
38+
[Fact]
39+
public async Task CreateCapture_WhitespaceText_Returns400WithErrorContract()
40+
{
41+
using var client = _factory.CreateClient();
42+
await ApiTestHarness.AuthenticateAsync(client, "cap-err-ws");
43+
44+
var response = await client.PostAsJsonAsync(
45+
"/api/capture/items",
46+
new CreateCaptureItemDto(null, " "));
47+
48+
await ApiTestHarness.AssertErrorContractAsync(response, HttpStatusCode.BadRequest, ErrorCodes.ValidationError);
49+
}
50+
51+
[Fact]
52+
public async Task CreateCapture_NoBoardContext_Succeeds()
53+
{
54+
using var client = _factory.CreateClient();
55+
await ApiTestHarness.AuthenticateAsync(client, "cap-err-noboard");
56+
57+
var response = await client.PostAsJsonAsync(
58+
"/api/capture/items",
59+
new CreateCaptureItemDto(null, "A capture without board context"));
60+
61+
response.StatusCode.Should().Be(HttpStatusCode.Created);
62+
}
63+
64+
[Fact]
65+
public async Task GetCapture_NonExistentId_Returns404WithErrorContract()
66+
{
67+
using var client = _factory.CreateClient();
68+
await ApiTestHarness.AuthenticateAsync(client, "cap-err-404");
69+
70+
var response = await client.GetAsync($"/api/capture/items/{Guid.NewGuid()}");
71+
72+
await ApiTestHarness.AssertErrorContractAsync(response, HttpStatusCode.NotFound, ErrorCodes.NotFound);
73+
}
74+
75+
[Fact]
76+
public async Task IgnoreCapture_NonExistentId_Returns404WithErrorContract()
77+
{
78+
using var client = _factory.CreateClient();
79+
await ApiTestHarness.AuthenticateAsync(client, "cap-err-ign404");
80+
81+
var response = await client.PostAsync(
82+
$"/api/capture/items/{Guid.NewGuid()}/ignore",
83+
content: null);
84+
85+
await ApiTestHarness.AssertErrorContractAsync(response, HttpStatusCode.NotFound, ErrorCodes.NotFound);
86+
}
87+
88+
[Fact]
89+
public async Task CancelCapture_NonExistentId_Returns404WithErrorContract()
90+
{
91+
using var client = _factory.CreateClient();
92+
await ApiTestHarness.AuthenticateAsync(client, "cap-err-cancel404");
93+
94+
var response = await client.PostAsync(
95+
$"/api/capture/items/{Guid.NewGuid()}/cancel",
96+
content: null);
97+
98+
await ApiTestHarness.AssertErrorContractAsync(response, HttpStatusCode.NotFound, ErrorCodes.NotFound);
99+
}
100+
101+
[Fact]
102+
public async Task ListCaptures_InvalidStatus_Returns400WithErrorContract()
103+
{
104+
using var client = _factory.CreateClient();
105+
await ApiTestHarness.AuthenticateAsync(client, "cap-err-status");
106+
107+
var response = await client.GetAsync("/api/capture/items?status=InvalidStatusValue");
108+
109+
await ApiTestHarness.AssertErrorContractAsync(response, HttpStatusCode.BadRequest, ErrorCodes.ValidationError);
110+
}
111+
112+
[Fact]
113+
public async Task TriageCapture_NonExistentId_Returns404WithErrorContract()
114+
{
115+
using var client = _factory.CreateClient();
116+
await ApiTestHarness.AuthenticateAsync(client, "cap-err-triage404");
117+
118+
var response = await client.PostAsync(
119+
$"/api/capture/items/{Guid.NewGuid()}/triage",
120+
content: null);
121+
122+
await ApiTestHarness.AssertErrorContractAsync(response, HttpStatusCode.NotFound, ErrorCodes.NotFound);
123+
}
124+
}

0 commit comments

Comments
 (0)