|
| 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