using System.Net; using System.Net.Http.Json; using System.Security.Cryptography; using System.Text.Json; using Microsoft.EntityFrameworkCore; namespace WordVersioning.Api.IntegrationTests; /// FR-5 (upload), FR-6 (history), FR-7 (download) — ผ่าน HTTP จริง + ตรวจสถานะใน Postgres ตรงๆ [Collection(IntegrationTestCollection.Name)] public sealed class UploadVersionTests(IntegrationTestFixture fixture) { [Fact] public async Task FirstUpload_CreatesDocumentAndVersionRow_FromJwtClaims() { var documentId = Guid.NewGuid(); var clientVersionId = Guid.NewGuid(); var bytes = TestDocx.BuildPlainDocx("version 1 content"); using var client = fixture.CreateClient(); using var response = await client.PostAsync( $"api/documents/{documentId}/versions", ApiExtensions.VersionForm(bytes, "เอกสารทดสอบ.docx", clientVersionId.ToString())); Assert.Equal(HttpStatusCode.Created, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(IntegrationTestFixture.Json); Assert.Equal(1, result!.VersionNumber); Assert.NotEqual(Guid.Empty, result.VersionId); // ตรวจ DB จริง: Document auto-create + ค่า tracking จาก JWT claims (AC-5) await using var db = fixture.CreateDb(); var doc = await db.Documents.AsNoTracking().SingleAsync(d => d.Id == documentId); Assert.Equal("เอกสารทดสอบ.docx", doc.FileName); Assert.Null(doc.TemplateId); // track เองจาก add-in — ไม่ผูก template var version = await db.DocumentVersions.AsNoTracking().SingleAsync(v => v.DocumentId == documentId); Assert.Equal(1, version.VersionNumber); Assert.Equal(fixture.TestUserId, version.CreatedByUserId); // sub จาก token เท่านั้น Assert.Equal(Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(), version.ContentHash); Assert.Equal($"{documentId}/v0001_{clientVersionId}.docx", version.ObjectKey); } [Fact] public async Task SecondUpload_IncrementsVersionNumber_AndHistorySortsNewestFirst() { var documentId = Guid.NewGuid(); using var client = fixture.CreateClient(); var v1 = TestDocx.BuildPlainDocx("v1"); using var first = await client.PostAsync( $"api/documents/{documentId}/versions", ApiExtensions.VersionForm(v1, "doc.docx", Guid.NewGuid().ToString())); var firstResult = await first.Content.ReadFromJsonAsync(IntegrationTestFixture.Json); var v2 = TestDocx.BuildPlainDocx("v2 — แก้ไขแล้ว"); using var second = await client.PostAsync( $"api/documents/{documentId}/versions", ApiExtensions.VersionForm(v2, "doc.docx", Guid.NewGuid().ToString())); Assert.Equal(HttpStatusCode.Created, second.StatusCode); var secondResult = await second.Content.ReadFromJsonAsync(IntegrationTestFixture.Json); Assert.Equal(2, secondResult!.VersionNumber); // FR-6: เรียงใหม่ → เก่า using var history = await client.GetAsync($"api/documents/{documentId}/versions"); var items = await history.Content.ReadFromJsonAsync>(IntegrationTestFixture.Json); Assert.Equal(2, items!.Count); Assert.Equal(secondResult.VersionId, items[0].VersionId); Assert.Equal(2, items[0].VersionNumber); Assert.Equal(firstResult!.VersionId, items[1].VersionId); Assert.Equal(1, items[1].VersionNumber); Assert.All(items, i => Assert.Equal(fixture.TestUserId, i.CreatedByUserId)); } [Fact] public async Task Download_ReturnsExactUploadedBytes_WithDocxContentType() { var documentId = Guid.NewGuid(); var bytes = TestDocx.BuildPlainDocx("ไบต์ต้องตรงกันตอนดาวน์โหลด (AC-8 ส่วน backend)"); using var client = fixture.CreateClient(); using var upload = await client.PostAsync( $"api/documents/{documentId}/versions", ApiExtensions.VersionForm(bytes, "roundtrip.docx", Guid.NewGuid().ToString())); var uploaded = await upload.Content.ReadFromJsonAsync(IntegrationTestFixture.Json); using var download = await client.GetAsync( $"api/documents/{documentId}/versions/{uploaded!.VersionId}/download"); Assert.Equal(HttpStatusCode.OK, download.StatusCode); Assert.Equal(TestDocx.DocxContentType, download.Content.Headers.ContentType!.MediaType); var downloaded = await download.Content.ReadAsByteArrayAsync(); Assert.Equal(bytes, downloaded); // byte-for-byte ผ่าน MinIO ไป-กลับ } [Fact] public async Task Download_UnknownVersion_Returns404() { using var client = fixture.CreateClient(); using var response = await client.GetAsync( $"api/documents/{Guid.NewGuid()}/versions/{Guid.NewGuid()}/download"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task GetVersions_UnknownDocument_Returns404() { using var client = fixture.CreateClient(); using var response = await client.GetAsync($"api/documents/{Guid.NewGuid()}/versions"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task Upload_NonDocxFile_Returns400() { var documentId = Guid.NewGuid(); using var client = fixture.CreateClient(); using var form = new MultipartFormDataContent(); var file = new ByteArrayContent("นี่ไม่ใช่ word"u8.ToArray()); file.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("text/plain"); form.Add(file, "file", "notes.txt"); // ชื่อไม่ลงท้าย .docx form.Add(new StringContent(Guid.NewGuid().ToString()), "clientVersionId"); using var response = await client.PostAsync($"api/documents/{documentId}/versions", form); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); await using var db = fixture.CreateDb(); Assert.False(await db.Documents.AsNoTracking().AnyAsync(d => d.Id == documentId)); // ไม่มี side effect } [Fact] public async Task Upload_InvalidClientVersionId_Returns400() { using var client = fixture.CreateClient(); using var response = await client.PostAsync( $"api/documents/{Guid.NewGuid()}/versions", ApiExtensions.VersionForm(TestDocx.BuildPlainDocx(), "doc.docx", "ไม่ใช่ GUID")); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task Upload_InvalidFieldsJson_Returns400() { using var client = fixture.CreateClient(); using var response = await client.PostAsync( $"api/documents/{Guid.NewGuid()}/versions", ApiExtensions.VersionForm(TestDocx.BuildPlainDocx(), "doc.docx", Guid.NewGuid().ToString(), "not json")); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } /// เอกสารไม่ผูก template → snapshot เก็บ map ดิบตามที่ส่งมา (behavior ของ Phase 2) [Fact] public async Task Upload_WithoutTemplate_StoresRawFieldsMapInSnapshot() { var documentId = Guid.NewGuid(); using var client = fixture.CreateClient(); using var upload = await client.PostAsync( $"api/documents/{documentId}/versions", ApiExtensions.VersionForm( TestDocx.BuildPlainDocx(), "doc.docx", Guid.NewGuid().ToString(), """{"custom_tag":"ค่าที่ส่งมา"}""")); Assert.Equal(HttpStatusCode.Created, upload.StatusCode); using var history = await client.GetAsync($"api/documents/{documentId}/versions"); var items = await history.Content.ReadFromJsonAsync>(IntegrationTestFixture.Json); var fields = items!.Single().Fields!.Value; Assert.Equal(JsonValueKind.Object, fields.ValueKind); Assert.Equal("ค่าที่ส่งมา", fields.GetProperty("custom_tag").GetString()); } }