using System.Net; using System.Net.Http.Json; using System.Text.Json; using Microsoft.EntityFrameworkCore; namespace WordVersioning.Api.IntegrationTests; /// FR-11 — field sync ตอน Save & Version: latest values อัปเดต + snapshot ต่อ version /// + missing-field policy (AC-13: ค่าเปลี่ยนตาม, AC-14: field หาย → ค่าเดิม + mark missing ไม่ fail) [Collection(IntegrationTestCollection.Name)] public sealed class FieldSyncTests(IntegrationTestFixture fixture) { /// สร้างเอกสารจาก template ที่ตั้งค่าแล้ว (v0 ค่า DefaultValues) — จุดตั้งต้นของทุก test ในคลาสนี้ private async Task<(HttpClient Client, Guid DocumentId, Guid V0VersionId)> CreateTrackedDocumentAsync() { var client = fixture.CreateClient(); var (templateId, _) = await TestFlow.CreateConfiguredTemplateAsync(client, "field sync"); var doc = await TestFlow.GenerateDocumentAsync(client, templateId); return (client, doc.DocumentId, doc.VersionId); } /// AC-13 — แก้ค่าใน content control แล้ว Save & Version → latest values เปลี่ยนตาม /// + snapshot ของ version นั้นเก็บค่าใหม่ และ v0 snapshot ยังเก็บค่าเก่าไว้ (diff ได้) [Fact] public async Task UploadWithChangedFields_UpdatesLatestValues_AndSnapshotsPerVersion() { var (client, documentId, v0VersionId) = await CreateTrackedDocumentAsync(); var v1Fields = new Dictionary { ["employee_name"] = "สมหญิง เปลี่ยนชื่อแล้ว", ["position"] = "วิศวกรซอฟต์แวร์", // ไม่เปลี่ยน ["salary"] = "99000", ["start_date"] = "2026-08-01", // ไม่เปลี่ยน }; using var upload = await client.PostAsync( $"api/documents/{documentId}/versions", ApiExtensions.VersionForm( TestDocx.BuildPlainDocx("แก้ไขใน Word"), "doc.docx", Guid.NewGuid().ToString(), JsonSerializer.Serialize(v1Fields))); Assert.Equal(HttpStatusCode.Created, upload.StatusCode); var v1 = await upload.Content.ReadFromJsonAsync(IntegrationTestFixture.Json); // latest values: field ที่ "ส่งมา" ถูก update ทุกตัว (แม้ค่าเดิมไม่เปลี่ยน — ไม่มี dirty check) // จึงชี้ SourceVersionId = v1 ทั้งหมด; ตัวที่ "ไม่ได้ส่ง" เท่านั้นที่คง v0 (ดู test AC-14) await using var db = fixture.CreateDb(); var values = await db.DocumentFieldValues.AsNoTracking() .Where(v => v.DocumentId == documentId) .ToDictionaryAsync(v => v.FieldKey); Assert.Equal("สมหญิง เปลี่ยนชื่อแล้ว", values["employee_name"].Value); Assert.Equal(v1!.VersionId, values["employee_name"].SourceVersionId); Assert.Equal("99000", values["salary"].Value); Assert.Equal(v1.VersionId, values["salary"].SourceVersionId); Assert.Equal("วิศวกรซอฟต์แวร์", values["position"].Value); // ค่าเดิมส่งมาเท่าเดิม → คงเดิมแต่ source ขยับตาม v1 Assert.Equal(v1.VersionId, values["position"].SourceVersionId); Assert.Equal(fixture.TestUserId, values["employee_name"].UpdatedByUserId); // snapshot ต่อ version ผ่าน history API: v1 = ค่าใหม่, v0 = ค่าเดิม (missing=false ทั้งคู่) using var history = await client.GetAsync($"api/documents/{documentId}/versions"); var items = (await history.Content.ReadFromJsonAsync>(IntegrationTestFixture.Json))! .OrderBy(i => i.VersionNumber) .ToList(); var v0Snapshot = Snapshot(items[0].Fields!.Value); var v1Snapshot = Snapshot(items[1].Fields!.Value); Assert.Equal("สมหญิง รักงาน", v0Snapshot["employee_name"].Value); Assert.Equal("สมหญิง เปลี่ยนชื่อแล้ว", v1Snapshot["employee_name"].Value); Assert.Equal("99000", v1Snapshot["salary"].Value); Assert.All(v0Snapshot.Values, e => Assert.False(e.Missing)); Assert.All(v1Snapshot.Values, e => Assert.False(e.Missing)); } /// AC-14 — ลบ content control แล้ว Save & Version: request สำเร็จ, ค่าเดิมคงอยู่ + mark missing /// ใน snapshot; field ที่ส่งเกินนิยามถูก ignore [Fact] public async Task UploadWithMissingFields_KeepsOldValues_MarksMissing_DoesNotFail() { var (client, documentId, v0VersionId) = await CreateTrackedDocumentAsync(); // ผู้ใช้ลบ control ของ position/salary/start_date — เหลือส่ง employee_name เท่านั้น + ส่งเกิน 1 field using var upload = await client.PostAsync( $"api/documents/{documentId}/versions", ApiExtensions.VersionForm( TestDocx.BuildPlainDocx("ลบ control ไป"), "doc.docx", Guid.NewGuid().ToString(), """{"employee_name":"สมหญิง ยังอยู่","อะไรที่ไม่มีในนิยาม":"ส่งเกินมา"}""")); // policy FR-11: ไม่ fail request Assert.Equal(HttpStatusCode.Created, upload.StatusCode); // latest values ของ field ที่หาย: คงค่า v0 เดิมไว้ทั้งหมด (รวม SourceVersionId ที่ยังชี้ v0) await using var db = fixture.CreateDb(); var values = await db.DocumentFieldValues.AsNoTracking() .Where(v => v.DocumentId == documentId) .ToDictionaryAsync(v => v.FieldKey); Assert.Equal("สมหญิง ยังอยู่", values["employee_name"].Value); Assert.Equal("วิศวกรซอฟต์แวร์", values["position"].Value); Assert.Equal(v0VersionId, values["position"].SourceVersionId); // ไม่ถูกแตะเพราะไม่ได้ส่งมา Assert.Equal("75000", values["salary"].Value); Assert.Equal("2026-08-01", values["start_date"].Value); Assert.Equal(4, values.Count); // field เกินไม่ถูกเก็บ // snapshot v1: field ที่หาย missing=true พร้อมค่าเดิม, ไม่มี key ของ field เกิน using var history = await client.GetAsync($"api/documents/{documentId}/versions"); var items = await history.Content.ReadFromJsonAsync>(IntegrationTestFixture.Json); var snapshot = Snapshot(items!.Single(i => i.VersionNumber == 1).Fields!.Value); Assert.False(snapshot["employee_name"].Missing); Assert.True(snapshot["position"].Missing); Assert.Equal("วิศวกรซอฟต์แวร์", snapshot["position"].Value); // ค่าเดิม Assert.True(snapshot["salary"].Missing); Assert.True(snapshot["start_date"].Missing); Assert.Equal(4, snapshot.Count); // ไม่มี "อะไรที่ไม่มีในนิยาม" } /// FR-12 ผ่าน detail — หลัง sync แล้วหน้า detail ต้องเห็นค่าล่าสุด (ไม่ใช่ของ v0) [Fact] public async Task DetailReflectsLatestSyncedValues() { var (client, documentId, _) = await CreateTrackedDocumentAsync(); using var upload = await client.PostAsync( $"api/documents/{documentId}/versions", ApiExtensions.VersionForm( TestDocx.BuildPlainDocx(), "doc.docx", Guid.NewGuid().ToString(), """{"employee_name":"ชื่อใหม่สุด","position":"ตำแหน่งใหม่สุด","salary":"1","start_date":"2026-08-20"}""")); upload.EnsureSuccessStatusCode(); using var detail = await client.GetAsync($"api/documents/{documentId}"); var body = await detail.Content.ReadFromJsonAsync(IntegrationTestFixture.Json); var byKey = body!.Fields.ToDictionary(f => f.Key); Assert.Equal("ชื่อใหม่สุด", byKey["employee_name"].Value); Assert.Equal("ตำแหน่งใหม่สุด", byKey["position"].Value); Assert.Equal("1", byKey["salary"].Value); } /// แปลง snapshot JSON (structured {key:{value,missing}}) เป็น dictionary ใช้ใน test private static Dictionary Snapshot(JsonElement fields) { var result = new Dictionary(); foreach (var prop in fields.EnumerateObject()) result[prop.Name] = ( prop.Value.GetProperty("value").GetString() ?? "", prop.Value.GetProperty("missing").GetBoolean()); return result; } }