using System.Net; using System.Net.Http.Json; using Microsoft.EntityFrameworkCore; using WordVersioning.Api.Services; namespace WordVersioning.Api.IntegrationTests; /// FR-10 — generate เอกสารจาก template: ค่าครบ + formatting รักษาไว้ + stamp custom properties /// + Document/v0/field values ใน flow เดียว (AC-12 ส่วน backend) + FR-12 (detail / รายการเอกสารต่อ template) [Collection(IntegrationTestCollection.Name)] public sealed class GenerateTests(IntegrationTestFixture fixture) { [Fact] public async Task Generate_FillsAllFields_PreservesFormatting_StampsProperties_AndCreatesV0() { using var client = fixture.CreateClient(); var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, "generate หลัก"); var doc = await TestFlow.GenerateDocumentAsync(client, templateId); // response ครบตาม FR-10 + downloadUrl ใช้ได้ทันที Assert.Equal(0, doc.VersionNumber); Assert.Equal($"/api/documents/{doc.DocumentId}/versions/{doc.VersionId}/download", doc.DownloadUrl); using var download = await client.GetAsync(doc.DownloadUrl); Assert.Equal(HttpStatusCode.OK, download.StatusCode); var fileBytes = await download.Content.ReadAsByteArrayAsync(); // (1) ค่าทุก field ถูกแทนที่ใน SDT Assert.Equal("สมหญิง รักงาน", TestDocx.ReadSdtText(fileBytes, "employee_name")); Assert.Equal("วิศวกรซอฟต์แวร์", TestDocx.ReadSdtText(fileBytes, "position")); Assert.Equal("75000", TestDocx.ReadSdtText(fileBytes, "salary")); Assert.Equal("2026-08-01", TestDocx.ReadSdtText(fileBytes, "start_date")); // (2) multi-run SDT: ค่าใหม่เขียนที่ run ข้อความยาวสุด (v9 — ที่นี่คือ run bold ซึ่งชนะด้วย tie แบบเจตนา, // ดู comment ใน TestDocx) และ Text ของ run หลังถูกลบ (เหลือ run โล่งที่มีแต่ rPr — มองไม่เห็น ไม่มีผลกับเนื้อหา) var positionRuns = TestDocx.ReadSdtRuns(fileBytes, "position"); var runWithText = Assert.Single(positionRuns.Where(r => r.Text.Length > 0)); Assert.Equal("วิศวกรซอฟต์แวร์", runWithText.Text); Assert.True(runWithText.Bold); Assert.False(runWithText.Italic); // (3) custom properties ถูก stamp ลงไฟล์ — add-in ใช้ค่านี้ตรวจ tracked document (FR-1/FR-10) Assert.Equal(AddInMetadata.AddInSignatureGuid, TestDocx.ReadCustomProperty(fileBytes, "SavedByAddIn")); Assert.Equal(doc.DocumentId.ToString(), TestDocx.ReadCustomProperty(fileBytes, "DocumentTrackingId")); // (4) สถานะใน Postgres: Document ผูก template + v0 + field values ครบ await using var db = fixture.CreateDb(); var document = await db.Documents.AsNoTracking() .Include(d => d.Versions) .SingleAsync(d => d.Id == doc.DocumentId); Assert.Equal(templateId, document.TemplateId); Assert.Equal(AddInMetadata.AddInSignatureGuid, document.AddInSignature); Assert.EndsWith(".docx", document.FileName); var version = Assert.Single(document.Versions); Assert.Equal(0, version.VersionNumber); Assert.Equal(fixture.TestUserId, version.CreatedByUserId); // จาก claim sub เท่านั้น var fieldValues = await db.DocumentFieldValues.AsNoTracking() .Where(v => v.DocumentId == doc.DocumentId) .ToDictionaryAsync(v => v.FieldKey); Assert.Equal(4, fieldValues.Count); Assert.Equal("สมหญิง รักงาน", fieldValues["employee_name"].Value); Assert.Equal("75000", fieldValues["salary"].Value); Assert.All(fieldValues.Values, v => Assert.Equal(doc.VersionId, v.SourceVersionId)); Assert.All(fieldValues.Values, v => Assert.Equal(fixture.TestUserId, v.UpdatedByUserId)); } [Fact] public async Task Generate_MissingRequiredField_Returns400() { using var client = fixture.CreateClient(); var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, "required ขาด"); var values = new Dictionary(TestFlow.DefaultValues); values.Remove("employee_name"); // required field หาย using var response = await client.PostAsJsonAsync( "api/documents/generate", new { templateId, values }, IntegrationTestFixture.Json); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); var error = await response.Content.ReadAsStringAsync(); Assert.Contains("employee_name", error); } [Theory] [InlineData("salary", "ไม่ใช่เลข")] [InlineData("start_date", "ไม่ใช่วันที่")] public async Task Generate_WrongFieldType_Returns400(string key, string badValue) { using var client = fixture.CreateClient(); var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, $"type ผิด {key}"); var values = new Dictionary(TestFlow.DefaultValues) { [key] = badValue }; using var response = await client.PostAsJsonAsync( "api/documents/generate", new { templateId, values }, IntegrationTestFixture.Json); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task Generate_IgnoresUndefinedValues() { using var client = fixture.CreateClient(); var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, "ค่าเกินนิยาม"); var values = new Dictionary(TestFlow.DefaultValues) { ["field_ที่ไม่มีใน_template"] = "ส่งเกินมา", // policy FR-11: ignore }; var doc = await TestFlow.GenerateDocumentAsync(client, templateId, values); Assert.Equal(0, doc.VersionNumber); await using var db = fixture.CreateDb(); Assert.False(await db.DocumentFieldValues.AsNoTracking() .AnyAsync(v => v.DocumentId == doc.DocumentId && v.FieldKey == "field_ที่ไม่มีใน_template")); } [Fact] public async Task Generate_UnknownTemplate_Returns404() { using var client = fixture.CreateClient(); using var response = await client.PostAsJsonAsync( "api/documents/generate", new { templateId = Guid.NewGuid(), values = TestFlow.DefaultValues }, IntegrationTestFixture.Json); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } /// FR-12 — detail ของเอกสาร: merge นิยาม × ค่าล่าสุด แบบรองรับ orphan ทั้งสองทาง /// (template replace ไฟล์แล้ว: def ใหม่ไม่มีค่า / ค่าเก่าไม่มี def) [Fact] public async Task GetDetail_MergesDefsAndValues_BothOrphanWays() { using var client = fixture.CreateClient(); var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, "orphan merge"); var doc = await TestFlow.GenerateDocumentAsync(client, templateId); // replace ไฟล์ template: เหลือ employee_name + เพิ่ม note → position/salary/start_date กลายเป็นค่า orphan using var replace = await client.PutAsync( $"api/templates/{templateId}/file", new MultipartFormDataContent { { new ByteArrayContent(TestDocx.BuildReplacementTemplateDocx()) { Headers = { ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(TestDocx.DocxContentType) } }, "file", "replaced.docx" }, }); replace.EnsureSuccessStatusCode(); using var detailResponse = await client.GetAsync($"api/documents/{doc.DocumentId}"); Assert.Equal(HttpStatusCode.OK, detailResponse.StatusCode); var detail = await detailResponse.Content.ReadFromJsonAsync(IntegrationTestFixture.Json); Assert.Equal(templateId, detail!.TemplateId); var byKey = detail.Fields.ToDictionary(f => f.Key); // def ใหม่ (note) ยังไม่มีค่า — แสดงเป็นค่าว่าง + ไม่มี source version Assert.Equal("", byKey["note"].Value); Assert.Null(byKey["note"].SourceVersionId); Assert.Equal("note", byKey["note"].Label); // def เดิมที่ยังอยู่ (employee_name) — ค่า v0 ยังอยู่ครบ Assert.Equal("สมหญิง รักงาน", byKey["employee_name"].Value); Assert.Equal(doc.VersionId, byKey["employee_name"].SourceVersionId); // ค่าเก่าที่ def หายไป (position/salary/start_date) — ต่อท้ายโดยไม่มีนิยาม Assert.Null(byKey["position"].Label); Assert.Equal("วิศวกรซอฟต์แวร์", byKey["position"].Value); Assert.Equal("75000", byKey["salary"].Value); Assert.Equal("2026-08-01", byKey["start_date"].Value); } /// FR-12 — รายการเอกสารต่อ template: versionCount/Latest ถูกต้อง เรียงกิจกรรมล่าสุดก่อน [Fact] public async Task ListDocuments_ShowsVersionCounts_LatestFirst() { using var client = fixture.CreateClient(); var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, "รายการเอกสาร"); var firstDoc = await TestFlow.GenerateDocumentAsync(client, templateId); var secondDoc = await TestFlow.GenerateDocumentAsync(client, templateId); // เพิ่ม v2 ให้เอกสารแรก → กิจกรรมล่าสุดเป็นของเอกสารแรก using var upload = await client.PostAsync( $"api/documents/{firstDoc.DocumentId}/versions", ApiExtensions.VersionForm(TestDocx.BuildPlainDocx("แก้ไข"), "doc.docx", Guid.NewGuid().ToString(), $$"""{"employee_name":"สมหญิง แก้ไขแล้ว","position":"วิศวกรซอฟต์แวร์","salary":"80000","start_date":"2026-08-01"}""")); upload.EnsureSuccessStatusCode(); using var listResponse = await client.GetAsync($"api/templates/{templateId}/documents"); Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode); var items = await listResponse.Content.ReadFromJsonAsync>(IntegrationTestFixture.Json); Assert.Equal(2, items!.Count); Assert.Equal(firstDoc.DocumentId, items[0].DocumentId); // ล่าสุดก่อน Assert.Equal(2, items[0].VersionCount); Assert.Equal(1, items[0].LatestVersionNumber); Assert.Equal(secondDoc.DocumentId, items[1].DocumentId); Assert.Equal(1, items[1].VersionCount); } /// FR-12 — detail ของเอกสารที่ไม่ผูก template (track เองจาก add-in): Fields ว่าง templateId null [Fact] public async Task GetDetail_OfStandaloneDocument_HasEmptyFields() { var documentId = Guid.NewGuid(); using var client = fixture.CreateClient(); using var upload = await client.PostAsync( $"api/documents/{documentId}/versions", ApiExtensions.VersionForm(TestDocx.BuildPlainDocx(), "เอกสารอิสระ.docx", Guid.NewGuid().ToString())); upload.EnsureSuccessStatusCode(); using var response = await client.GetAsync($"api/documents/{documentId}"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var detail = await response.Content.ReadFromJsonAsync(IntegrationTestFixture.Json); Assert.Null(detail!.TemplateId); Assert.Null(detail.TemplateName); Assert.Empty(detail.Fields); } }