Skip to content

Commit d8669a7

Browse files
authored
Merge branch 'main' into test/718-board-metrics-accuracy
2 parents f30b844 + 644153c commit d8669a7

25 files changed

+8365
-6
lines changed

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

Lines changed: 11 additions & 3 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), new Mock<IUserContext>().Object);
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), new Mock<IUserContext>().Object);
291+
var controller = new AuthController(authService.Object, CreateGitHubSettings(false), CreateMockUserContext().Object);
292292

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

@@ -305,7 +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, new Mock<IUserContext>().Object);
308+
return new AuthController(authServiceMock.Object, gitHubSettings, CreateMockUserContext().Object);
309309
}
310310

311311
private static Mock<AuthenticationService> CreateMockAuthService()
@@ -319,6 +319,14 @@ private static Mock<AuthenticationService> CreateMockAuthService()
319319
return new Mock<AuthenticationService>(unitOfWorkMock.Object, DefaultJwtSettings) { CallBase = true };
320320
}
321321

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+
322330
private static GitHubOAuthSettings CreateGitHubSettings(bool configured)
323331
{
324332
return configured

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

Lines changed: 1008 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 551 additions & 0 deletions
Large diffs are not rendered by default.

backend/tests/Taskdeck.Application.Tests/Services/BoardJsonExportImportRoundTripTests.cs

Lines changed: 672 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
using System.Text.Json;
2+
using FluentAssertions;
3+
using Taskdeck.Application.DTOs;
4+
using Taskdeck.Application.Services;
5+
using Taskdeck.Domain.Exceptions;
6+
using Xunit;
7+
8+
namespace Taskdeck.Application.Tests.Services;
9+
10+
/// <summary>
11+
/// Cross-format integrity tests: verifies that feeding the output of one
12+
/// export format into another import format produces appropriate errors,
13+
/// and that format-specific validation detects mismatches.
14+
/// </summary>
15+
public class CrossFormatImportIntegrityTests
16+
{
17+
private readonly CsvExternalImportAdapter _csvAdapter = new();
18+
19+
private static readonly JsonSerializerOptions JsonOptions = new()
20+
{
21+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
22+
WriteIndented = true
23+
};
24+
25+
[Fact]
26+
public void BoardJsonExport_FedAsCsvPayload_ProducesZeroCandidates()
27+
{
28+
// Create a board JSON export payload
29+
var now = DateTimeOffset.UtcNow;
30+
var exportDto = new ExportBoardDto(
31+
new BoardDto(Guid.NewGuid(), "Test Board", "Description", false, now, now),
32+
new[] { new ColumnDto(Guid.NewGuid(), Guid.NewGuid(), "Todo", 0, null, 0, now, now) },
33+
Array.Empty<CardDto>(),
34+
Array.Empty<LabelDto>(),
35+
new List<BoardAccessDto>(),
36+
now, "tester");
37+
38+
var jsonPayload = JsonSerializer.Serialize(exportDto, JsonOptions);
39+
40+
// Try to import this JSON as CSV
41+
var request = new ExternalImportRequestDto(
42+
Provider: "csv",
43+
Payload: jsonPayload,
44+
TargetColumnName: "Todo",
45+
DryRun: true);
46+
47+
var result = _csvAdapter.Parse(request);
48+
49+
// JSON is technically parseable as single-column CSV (the "{" line becomes the header),
50+
// but no recognized column aliases exist, so zero candidates are produced.
51+
result.IsSuccess.Should().BeTrue("JSON is syntactically parseable as degenerate CSV");
52+
result.Value.Candidates.Should().BeEmpty(
53+
"JSON payload should not produce valid CSV candidates because no column aliases match");
54+
}
55+
56+
[Fact]
57+
public void CsvPayload_FedAsBoardJson_FailsDeserialization()
58+
{
59+
var csvPayload = "display_name,email,company\nAlice,alice@test.com,Acme\n";
60+
61+
// Try to deserialize CSV as ImportBoardDto
62+
var result = BoardJsonExportImportService.TryDeserializeImportDto(csvPayload);
63+
result.Should().BeNull("CSV text is not valid JSON and should not deserialize as board import");
64+
}
65+
66+
[Fact]
67+
public void RandomBinaryData_FedAsBoardJson_FailsGracefully()
68+
{
69+
var binaryGarbage = Convert.ToBase64String(new byte[] { 0xFF, 0xFE, 0x00, 0x01, 0xAB, 0xCD });
70+
71+
var result = BoardJsonExportImportService.TryDeserializeImportDto(binaryGarbage);
72+
result.Should().BeNull("binary garbage should not parse as board import JSON");
73+
}
74+
75+
[Fact]
76+
public void ArrayJson_FedAsBoardImport_FailsGracefully()
77+
{
78+
// Valid JSON but wrong shape (array instead of object)
79+
var arrayJson = "[{\"name\":\"test\"}]";
80+
81+
var result = BoardJsonExportImportService.TryDeserializeImportDto(arrayJson);
82+
result.Should().BeNull("JSON array should not parse as a board import DTO");
83+
}
84+
85+
[Fact]
86+
public void NumericJson_FedAsBoardImport_FailsGracefully()
87+
{
88+
var result = BoardJsonExportImportService.TryDeserializeImportDto("42");
89+
result.Should().BeNull("numeric JSON should not parse as board import");
90+
}
91+
92+
[Fact]
93+
public void NullJson_FedAsBoardImport_FailsGracefully()
94+
{
95+
var result = BoardJsonExportImportService.TryDeserializeImportDto("null");
96+
result.Should().BeNull("JSON null should not parse as board import");
97+
}
98+
99+
[Fact]
100+
public void StringJson_FedAsBoardImport_FailsGracefully()
101+
{
102+
var result = BoardJsonExportImportService.TryDeserializeImportDto("\"just a string\"");
103+
result.Should().BeNull("JSON string should not parse as board import");
104+
}
105+
106+
[Fact]
107+
public void ValidImportJson_AcceptedByTryDeserialize()
108+
{
109+
var importJson = JsonSerializer.Serialize(new ImportBoardDto(
110+
"Test Board",
111+
"Description",
112+
new[] { new ImportColumnDto("Todo", 0, null) },
113+
new[] { new ImportCardDto("Card 1", "Desc", "Todo", 0, null, null) },
114+
new[] { new ImportLabelDto("Bug", "#FF0000") }
115+
), JsonOptions);
116+
117+
var result = BoardJsonExportImportService.TryDeserializeImportDto(importJson);
118+
result.Should().NotBeNull("valid import JSON should be accepted");
119+
result!.Name.Should().Be("Test Board");
120+
}
121+
122+
[Fact]
123+
public void ValidExportJson_ConvertedToImportByTryDeserialize()
124+
{
125+
var now = DateTimeOffset.UtcNow;
126+
var colId = Guid.NewGuid();
127+
var exportDto = new ExportBoardDto(
128+
new BoardDto(Guid.NewGuid(), "Exported Board", "Desc", false, now, now),
129+
new[] { new ColumnDto(colId, Guid.NewGuid(), "Backlog", 0, null, 1, now, now) },
130+
new[] { new CardDto(Guid.NewGuid(), Guid.NewGuid(), colId, "Card A", null, null, false, null, 0, new List<LabelDto>(), now, now) },
131+
Array.Empty<LabelDto>(),
132+
new List<BoardAccessDto>(),
133+
now, "tester");
134+
135+
var json = JsonSerializer.Serialize(exportDto, JsonOptions);
136+
137+
var result = BoardJsonExportImportService.TryDeserializeImportDto(json);
138+
result.Should().NotBeNull("export JSON should be auto-converted to import shape");
139+
result!.Name.Should().Be("Exported Board");
140+
result.Columns.Should().ContainSingle(c => c.Name == "Backlog");
141+
result.Cards.Should().ContainSingle(c => c.Title == "Card A");
142+
}
143+
144+
[Fact]
145+
public void JsonWithTrailingComma_RejectedGracefully()
146+
{
147+
var badJson = "{\"name\":\"Test\",\"columns\":[],\"cards\":[],\"labels\":[],}";
148+
149+
var result = BoardJsonExportImportService.TryDeserializeImportDto(badJson);
150+
// System.Text.Json rejects trailing commas by default
151+
result.Should().BeNull("JSON with trailing commas should not parse");
152+
}
153+
154+
[Fact]
155+
public void JsonWithComments_RejectedGracefully()
156+
{
157+
var commentJson = "{ /* comment */ \"name\":\"Test\",\"columns\":[],\"cards\":[],\"labels\":[] }";
158+
159+
var result = BoardJsonExportImportService.TryDeserializeImportDto(commentJson);
160+
// System.Text.Json rejects comments by default
161+
result.Should().BeNull("JSON with comments should not parse");
162+
}
163+
}

0 commit comments

Comments
 (0)