Skip to content

Commit e5aaa67

Browse files
authored
Merge branch 'main' into fix/680-manual-card-empty-provenance
2 parents e48472c + f071081 commit e5aaa67

File tree

42 files changed

+12049
-26
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+12049
-26
lines changed

backend/tests/Taskdeck.Api.Tests/AuthControllerEdgeCaseTests.cs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ public async Task TokenValidationMiddleware_ShouldPassThrough_WhenClaimsHaveNoUs
275275
public async Task Login_ShouldReturn401_WhenBodyIsNull()
276276
{
277277
var authService = CreateMockAuthService();
278-
var controller = new AuthController(authService.Object, CreateGitHubSettings(false), CreateAnonymousUserContext());
278+
var controller = new AuthController(authService.Object, CreateGitHubSettings(false), CreateMockUserContext().Object);
279279

280280
var result = await controller.Login(null);
281281

@@ -288,7 +288,7 @@ public async Task Login_ShouldReturn401_WhenBodyIsNull()
288288
public async Task Login_ShouldReturn401_WhenFieldsEmpty()
289289
{
290290
var authService = CreateMockAuthService();
291-
var controller = new AuthController(authService.Object, CreateGitHubSettings(false), CreateAnonymousUserContext());
291+
var controller = new AuthController(authService.Object, CreateGitHubSettings(false), CreateMockUserContext().Object);
292292

293293
var result = await controller.Login(new LoginDto("", ""));
294294

@@ -305,16 +305,7 @@ private static AuthController CreateAuthController(bool gitHubConfigured = false
305305
{
306306
var authServiceMock = CreateMockAuthService();
307307
var gitHubSettings = CreateGitHubSettings(gitHubConfigured);
308-
return new AuthController(authServiceMock.Object, gitHubSettings, CreateAnonymousUserContext());
309-
}
310-
311-
private static IUserContext CreateAnonymousUserContext()
312-
{
313-
var mock = new Mock<IUserContext>();
314-
mock.Setup(c => c.IsAuthenticated).Returns(false);
315-
mock.Setup(c => c.UserId).Returns((string?)null);
316-
mock.Setup(c => c.Role).Returns((string?)null);
317-
return mock.Object;
308+
return new AuthController(authServiceMock.Object, gitHubSettings, CreateMockUserContext().Object);
318309
}
319310

320311
private static Mock<AuthenticationService> CreateMockAuthService()
@@ -328,6 +319,14 @@ private static Mock<AuthenticationService> CreateMockAuthService()
328319
return new Mock<AuthenticationService>(unitOfWorkMock.Object, DefaultJwtSettings) { CallBase = true };
329320
}
330321

322+
private static Mock<IUserContext> CreateMockUserContext()
323+
{
324+
var mock = new Mock<IUserContext>();
325+
mock.Setup(u => u.UserId).Returns(Guid.NewGuid().ToString());
326+
mock.Setup(u => u.IsAuthenticated).Returns(true);
327+
return mock;
328+
}
329+
331330
private static GitHubOAuthSettings CreateGitHubSettings(bool configured)
332331
{
333332
return configured

backend/tests/Taskdeck.Api.Tests/BoardsHubIntegrationTests.cs

Lines changed: 524 additions & 0 deletions
Large diffs are not rendered by default.
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)