using System.Net; using System.Net.Http.Json; using System.Text.Json; namespace WordVersioning.Api.IntegrationTests; /// FR-9 — template CRUD + auto-detect SDT tags ตอน upload/replace ไฟล์ (AC-11 ส่วน backend) [Collection(IntegrationTestCollection.Name)] public sealed class TemplateTests(IntegrationTestFixture fixture) { /// สร้าง template จาก BuildTemplateDocx() — คืน (id, fields ที่ detect ได้) — ใช้ร่วมกับ test อื่น public static async Task<(Guid TemplateId, List Fields)> CreateDefaultTemplateAsync( HttpClient client, string name) { using var response = await client.PostAsync( "api/templates", ApiExtensions.TemplateForm(TestDocx.BuildTemplateDocx(), name)); Assert.Equal(HttpStatusCode.Created, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(IntegrationTestFixture.Json); return (result!.Id, result.Fields); } [Fact] public async Task Create_DetectsAllSdtTags_WithDefaults() { using var client = fixture.CreateClient(); var (templateId, fields) = await CreateDefaultTemplateAsync(client, $"สัญญา {Guid.NewGuid():N}"); // 5 tags ในไฟล์ (employee_name ซ้ำ 2 จุด → dedup เหลือ 4) ตามลำดับที่ปรากฏ Assert.Equal( ["employee_name", "position", "salary", "start_date"], fields.Select(f => f.Key).ToList()); // defaults: label = tag, type = text, required = false (FR-9), editor = default ของ type (FR-14) Assert.All(fields, f => { Assert.Equal(f.Key, f.Label); Assert.Equal("text", f.Type); Assert.Equal("textbox", f.Editor); Assert.False(f.IsRequired); }); // GET detail คืนนิยามชุดเดิม using var detail = await client.GetAsync($"api/templates/{templateId}"); var detailBody = await detail.Content.ReadFromJsonAsync(IntegrationTestFixture.Json); Assert.Equal(4, detailBody!.Fields.Count); } [Fact] public async Task Create_RejectsNonDocxFile() { using var client = fixture.CreateClient(); using var form = new MultipartFormDataContent(); form.Add(new StringContent("ไม่ใช่ template"), "name"); var file = new ByteArrayContent("plain"u8.ToArray()); file.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("text/plain"); form.Add(file, "file", "template.txt"); using var response = await client.PostAsync("api/templates", form); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task Create_RejectsMissingName() { using var client = fixture.CreateClient(); using var form = new MultipartFormDataContent(); var file = new ByteArrayContent(TestDocx.BuildPlainDocx()); file.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(TestDocx.DocxContentType); form.Add(file, "file", "template.docx"); // ไม่ส่ง name using var response = await client.PostAsync("api/templates", form); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task Update_SavesFieldDefinitions_AndMetadata() { using var client = fixture.CreateClient(); var (templateId, fields) = await CreateDefaultTemplateAsync(client, $"แก้นิยาม {Guid.NewGuid():N}"); var updated = new { description = "สัญญาจ้างฉบับมาตรฐาน", fields = fields.Select((f, i) => new { key = f.Key, label = $"ป้ายกำกับ {f.Key}", type = f.Key == "salary" ? "number" : f.Key == "start_date" ? "date" : "text", editor = f.Key == "employee_name" ? "textarea" : null as string, // ส่งเฉพาะ text field (FR-14) isRequired = f.Key == "employee_name", sortOrder = fields.Count - i, // กลับลำดับ }), }; using var put = await client.PutAsJsonAsync($"api/templates/{templateId}", updated, IntegrationTestFixture.Json); Assert.Equal(HttpStatusCode.OK, put.StatusCode); var body = await put.Content.ReadFromJsonAsync(IntegrationTestFixture.Json); Assert.Equal("สัญญาจ้างฉบับมาตรฐาน", body!.Description); Assert.Equal("ป้ายกำกับ employee_name", body.Fields[3].Label); // กลับลำดับแล้ว employee_name ท้ายสุด Assert.Equal("number", body.Fields.First(f => f.Key == "salary").Type); Assert.Equal("date", body.Fields.First(f => f.Key == "start_date").Type); Assert.True(body.Fields.First(f => f.Key == "employee_name").IsRequired); // editor: ที่ส่งคือ textarea, ที่ไม่ส่ง resolve เป็น default ของ type (FR-14) Assert.Equal("textarea", body.Fields.First(f => f.Key == "employee_name").Editor); Assert.Equal("textbox", body.Fields.First(f => f.Key == "position").Editor); Assert.Equal("number", body.Fields.First(f => f.Key == "salary").Editor); Assert.Equal("date", body.Fields.First(f => f.Key == "start_date").Editor); } [Fact] public async Task Update_RejectsUnknownFieldKey() { using var client = fixture.CreateClient(); var (templateId, _) = await CreateDefaultTemplateAsync(client, $"key นอกไฟล์ {Guid.NewGuid():N}"); using var put = await client.PutAsJsonAsync($"api/templates/{templateId}", new { fields = new[] { new { key = "not_in_file", label = "ไม่มีในไฟล์", type = "text", isRequired = false, sortOrder = 0 } }, }, IntegrationTestFixture.Json); Assert.Equal(HttpStatusCode.BadRequest, put.StatusCode); // เพิ่ม/ลบ key ต้องผ่าน replace ไฟล์เท่านั้น } [Fact] public async Task Update_RejectsInvalidFieldType() { using var client = fixture.CreateClient(); var (templateId, fields) = await CreateDefaultTemplateAsync(client, $"type ผิด {Guid.NewGuid():N}"); using var put = await client.PutAsJsonAsync($"api/templates/{templateId}", new { fields = fields.Select(f => new { key = f.Key, label = f.Key, type = "datetime", isRequired = false, sortOrder = f.SortOrder }), }, IntegrationTestFixture.Json); Assert.Equal(HttpStatusCode.BadRequest, put.StatusCode); } [Fact] public async Task ReplaceFile_DetectsAddedAndRemovedTags() { using var client = fixture.CreateClient(); var (templateId, _) = await CreateDefaultTemplateAsync(client, $"replace {Guid.NewGuid():N}"); using var put = 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" }, }); Assert.Equal(HttpStatusCode.OK, put.StatusCode); var body = await put.Content.ReadFromJsonAsync(IntegrationTestFixture.Json); // เดิม: employee_name, position, salary, start_date → ใหม่: employee_name, note Assert.Equal(["note"], body!.AddedTags); Assert.Equal( new[] { "position", "salary", "start_date" }.Order(), body.RemovedTags.Order()); Assert.Equal(["employee_name", "note"], body.Fields.Select(f => f.Key).ToList()); } [Fact] public async Task Deactivate_HidesFromDefaultList_ButVisibleWithIncludeInactive() { using var client = fixture.CreateClient(); var (templateId, _) = await CreateDefaultTemplateAsync(client, $"deactivate {Guid.NewGuid():N}"); using var deactivate = await client.DeleteAsync($"api/templates/{templateId}"); Assert.Equal(HttpStatusCode.NoContent, deactivate.StatusCode); using var activeList = await client.GetAsync("api/templates"); var active = await activeList.Content.ReadFromJsonAsync>(IntegrationTestFixture.Json); Assert.DoesNotContain(active!, t => t.Id == templateId); using var allList = await client.GetAsync("api/templates?includeInactive=true"); var all = await allList.Content.ReadFromJsonAsync>(IntegrationTestFixture.Json); Assert.Contains(all!, t => t.Id == templateId && !t.IsActive); } [Fact] public async Task Deactivate_ThenGenerate_Returns400_ButDocumentsOfTemplateStillAccessible() { using var client = fixture.CreateClient(); var (templateId, _) = await CreateDefaultTemplateAsync(client, $"deactivate+generate {Guid.NewGuid():N}"); // สร้างเอกสาร 1 ฉบับก่อน deactivate var values = new Dictionary { ["employee_name"] = "สมชาย ใจดี", ["position"] = "โปรแกรมเมอร์", ["salary"] = "50000", ["start_date"] = "2026-08-20", }; using var generate = await client.PostAsJsonAsync("api/documents/generate", new { templateId, values }, IntegrationTestFixture.Json); Assert.Equal(HttpStatusCode.Created, generate.StatusCode); var doc = await generate.Content.ReadFromJsonAsync(IntegrationTestFixture.Json); using var deactivate = await client.DeleteAsync($"api/templates/{templateId}"); Assert.Equal(HttpStatusCode.NoContent, deactivate.StatusCode); // สร้างเอกสารใหม่ไม่ได้ (FR-9 policy) using var generateAgain = await client.PostAsJsonAsync("api/documents/generate", new { templateId, values }, IntegrationTestFixture.Json); Assert.Equal(HttpStatusCode.BadRequest, generateAgain.StatusCode); // แต่เอกสารเก่ายังเข้าถึงได้ครบ: list + detail + history + download (FR-9 + FR-12) using var list = await client.GetAsync($"api/templates/{templateId}/documents"); Assert.Equal(HttpStatusCode.OK, list.StatusCode); var documents = await list.Content.ReadFromJsonAsync>(IntegrationTestFixture.Json); var listItem = documents!.Single(); Assert.Equal(doc!.DocumentId, listItem.DocumentId); Assert.Equal(1, listItem.VersionCount); using var detail = await client.GetAsync($"api/documents/{doc!.DocumentId}"); Assert.Equal(HttpStatusCode.OK, detail.StatusCode); Assert.True((await detail.Content.ReadFromJsonAsync(IntegrationTestFixture.Json))!.TemplateIsActive is false); using var history = await client.GetAsync($"api/documents/{doc.DocumentId}/versions"); Assert.Equal(HttpStatusCode.OK, history.StatusCode); using var download = await client.GetAsync($"api/documents/{doc.DocumentId}/versions/{doc.VersionId}/download"); Assert.Equal(HttpStatusCode.OK, download.StatusCode); } [Fact] public async Task Get_UnknownTemplate_Returns404() { using var client = fixture.CreateClient(); using var response = await client.GetAsync($"api/templates/{Guid.NewGuid()}"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task ListDocuments_UnknownTemplate_Returns404() { using var client = fixture.CreateClient(); using var response = await client.GetAsync($"api/templates/{Guid.NewGuid()}/documents"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } }