Phase 7-8
This commit is contained in:
parent
a196b8f9a8
commit
0148f48204
25 changed files with 2473 additions and 47 deletions
219
backend/WordVersioning.Api.IntegrationTests/GenerateTests.cs
Normal file
219
backend/WordVersioning.Api.IntegrationTests/GenerateTests.cs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WordVersioning.Api.Services;
|
||||
|
||||
namespace WordVersioning.Api.IntegrationTests;
|
||||
|
||||
/// <summary>FR-10 — generate เอกสารจาก template: ค่าครบ + formatting รักษาไว้ + stamp custom properties
|
||||
/// + Document/v1/field values ใน flow เดียว (AC-12 ส่วน backend) + FR-12 (detail / รายการเอกสารต่อ template)</summary>
|
||||
[Collection(IntegrationTestCollection.Name)]
|
||||
public sealed class GenerateTests(IntegrationTestFixture fixture)
|
||||
{
|
||||
[Fact]
|
||||
public async Task Generate_FillsAllFields_PreservesFormatting_StampsProperties_AndCreatesV1()
|
||||
{
|
||||
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(1, 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: ค่าใหม่เขียนที่ Text แรก (ใน Run แรก — สืบทอด rPr bold ของ template)
|
||||
// และ 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 + v1 + 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(1, 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<string, string>(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<string, string>(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<string, string>(TestFlow.DefaultValues)
|
||||
{
|
||||
["field_ที่ไม่มีใน_template"] = "ส่งเกินมา", // policy FR-11: ignore
|
||||
};
|
||||
|
||||
var doc = await TestFlow.GenerateDocumentAsync(client, templateId, values);
|
||||
Assert.Equal(1, 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);
|
||||
}
|
||||
|
||||
/// <summary>FR-12 — detail ของเอกสาร: merge นิยาม × ค่าล่าสุด แบบรองรับ orphan ทั้งสองทาง
|
||||
/// (template replace ไฟล์แล้ว: def ใหม่ไม่มีค่า / ค่าเก่าไม่มี def)</summary>
|
||||
[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<DocDetail>(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) — ค่า v1 ยังอยู่ครบ
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>FR-12 — รายการเอกสารต่อ template: versionCount/Latest ถูกต้อง เรียงกิจกรรมล่าสุดก่อน</summary>
|
||||
[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<List<TemplateDocItem>>(IntegrationTestFixture.Json);
|
||||
|
||||
Assert.Equal(2, items!.Count);
|
||||
Assert.Equal(firstDoc.DocumentId, items[0].DocumentId); // ล่าสุดก่อน
|
||||
Assert.Equal(2, items[0].VersionCount);
|
||||
Assert.Equal(2, items[0].LatestVersionNumber);
|
||||
Assert.Equal(secondDoc.DocumentId, items[1].DocumentId);
|
||||
Assert.Equal(1, items[1].VersionCount);
|
||||
}
|
||||
|
||||
/// <summary>FR-12 — detail ของเอกสารที่ไม่ผูก template (track เองจาก add-in): Fields ว่าง templateId null</summary>
|
||||
[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<DocDetail>(IntegrationTestFixture.Json);
|
||||
|
||||
Assert.Null(detail!.TemplateId);
|
||||
Assert.Null(detail.TemplateName);
|
||||
Assert.Empty(detail.Fields);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue