using System.Net; using System.Net.Http.Json; using Microsoft.EntityFrameworkCore; using WordVersioning.Api.Services; namespace WordVersioning.Api.IntegrationTests; /// FR-16 — ไฟล์ร่าง (draft): generate docx จาก template แบบ stateless /// (ค่าครบ + stamp เฉพาะ TemplateId + ไม่เกิด Document/version ใดๆ ในระบบ) [Collection(IntegrationTestCollection.Name)] public sealed class DraftTests(IntegrationTestFixture fixture) { [Fact] public async Task Draft_FillsAllFields_PreservesFormatting_StampsOnlyTemplateId_CreatesNothing() { using var client = fixture.CreateClient(); var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, "draft หลัก"); using var response = await client.PostAsJsonAsync( $"api/templates/{templateId}/generate-draft", new { values = TestFlow.DefaultValues }, IntegrationTestFixture.Json); Assert.Equal(HttpStatusCode.OK, response.StatusCode); // ไม่ใช่ 201 — ไม่มีอะไรถูกสร้าง // คืนไฟล์ docx พร้อมชื่อไฟล์จากชื่อ template (Content-Disposition attachment) Assert.Equal(TestDocx.DocxContentType, response.Content.Headers.ContentType?.MediaType); Assert.NotNull(response.Content.Headers.ContentDisposition?.FileNameStar); var fileBytes = await response.Content.ReadAsByteArrayAsync(); // (1) ค่าทุก field ถูกแทนที่ใน SDT — เหมือน generate ปกติ 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 ยังรักษา formatting ของ run แรก (bold) var positionRuns = TestDocx.ReadSdtRuns(fileBytes, "position"); var runWithText = Assert.Single(positionRuns.Where(r => r.Text.Length > 0)); Assert.True(runWithText.Bold); // (3) stamp เฉพาะ TemplateId — ไม่มี SavedByAddIn/DocumentTrackingId เพื่อให้ add-in เห็นเป็น untracked (FR-16) Assert.Equal(templateId.ToString(), TestDocx.ReadCustomProperty(fileBytes, AddInMetadata.TemplateIdPropertyName)); Assert.Null(TestDocx.ReadCustomProperty(fileBytes, "SavedByAddIn")); Assert.Null(TestDocx.ReadCustomProperty(fileBytes, "DocumentTrackingId")); // (4) stateless จริง: ไม่เกิด Document/version ใดๆ จาก template นี้ await using var db = fixture.CreateDb(); Assert.False(await db.Documents.AsNoTracking().AnyAsync(d => d.TemplateId == templateId)); Assert.False(await db.DocumentVersions.AsNoTracking() .AnyAsync(v => db.Documents.Any(d => d.Id == v.DocumentId && d.TemplateId == templateId))); Assert.False(await db.DocumentFieldValues.AsNoTracking() .AnyAsync(v => db.Documents.Any(d => d.Id == v.DocumentId && d.TemplateId == templateId))); } [Fact] public async Task Draft_MissingRequiredField_Returns400() { using var client = fixture.CreateClient(); var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, "draft required ขาด"); var values = new Dictionary(TestFlow.DefaultValues); values.Remove("employee_name"); // required field หาย using var response = await client.PostAsJsonAsync( $"api/templates/{templateId}/generate-draft", new { 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 Draft_WrongFieldType_Returns400(string key, string badValue) { using var client = fixture.CreateClient(); var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, $"draft type ผิด {key}"); var values = new Dictionary(TestFlow.DefaultValues) { [key] = badValue }; using var response = await client.PostAsJsonAsync( $"api/templates/{templateId}/generate-draft", new { values }, IntegrationTestFixture.Json); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task Draft_UnknownTemplate_Returns404() { using var client = fixture.CreateClient(); using var response = await client.PostAsJsonAsync( $"api/templates/{Guid.NewGuid()}/generate-draft", new { values = TestFlow.DefaultValues }, IntegrationTestFixture.Json); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task Draft_DeactivatedTemplate_Returns400() { using var client = fixture.CreateClient(); var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, "draft deactivate"); using var deactivate = await client.DeleteAsync($"api/templates/{templateId}"); Assert.Equal(HttpStatusCode.NoContent, deactivate.StatusCode); using var response = await client.PostAsJsonAsync( $"api/templates/{templateId}/generate-draft", new { values = TestFlow.DefaultValues }, IntegrationTestFixture.Json); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task Draft_IgnoresUndefinedValues() { using var client = fixture.CreateClient(); var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, "draft ค่าเกินนิยาม"); var values = new Dictionary(TestFlow.DefaultValues) { ["field_ที่ไม่มีใน_template"] = "ส่งเกินมา", // policy FR-11: ignore }; using var response = await client.PostAsJsonAsync( $"api/templates/{templateId}/generate-draft", new { values }, IntegrationTestFixture.Json); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var fileBytes = await response.Content.ReadAsByteArrayAsync(); Assert.Equal(templateId.ToString(), TestDocx.ReadCustomProperty(fileBytes, AddInMetadata.TemplateIdPropertyName)); } }