package httpapi import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "knowledge-governance/internal/librarysnapshot" ) // 整库只读接口(overview / storage / snapshots)的回归: // // 候选归档里 79 个版本共享少量大 blob。按版本重读全部 blob 会让每个接口都 // 打到请求级超时(实测 10s 后 503),并把"列版本"变成 O(全部版本字节)。 // 这个夹具把形状缩到最小:多版本共享同一个大部件,枚举必须给出全部版本与真实去重, // 而被篡改的字节仍然要在执行路径上被拒绝。 // sealSharedVersions 造 count 个内容完全一样的版本:它们共享全部 blob。 func sealSharedVersions(t *testing.T, root string, count, documents int) []string { t.Helper() archive, err := librarysnapshot.Open(root, 4*1024*1024) if err != nil { t.Fatal(err) } rows := func(n int) string { items := make([]string, n) for i := range items { items[i] = fmt.Sprintf(`{"id":"00000000-0000-4000-8000-%012d"}`, i) } return "[" + strings.Join(items, ",") + "]" } parts := []struct { kind, name string count int64 body string }{ {"database", "knowledges", int64(documents), rows(documents)}, {"database", "chunks", int64(documents) * 8, rows(documents * 8)}, {"database", "resources", 0, rows(0)}, {"database", "resource_bindings", 0, rows(0)}, {"database", "knowledge_tags", 0, rows(0)}, {"database", "knowledge_tag_relations", 0, rows(0)}, {"index", "chunks", int64(documents) * 30, rows(documents * 30)}, {"wiki", "wiki_pages", 3, rows(3)}, {"wiki", "wiki_folders", 1, rows(1)}, {"wiki", "wiki_page_issues", 0, rows(0)}, {"wiki", "wiki_log_entries", 2, rows(2)}, } // 每个版本各封一次;内容相同 ⇒ blob 复用,归档里只有一份字节。 ids := make([]string, 0, count) for version := 0; version < count; version++ { manifest := librarysnapshot.Manifest{ValidationPolicy: librarysnapshot.ArtifactAcceptancePolicy, Format: librarysnapshot.Format, Scope: "internal", TenantID: 10000, KnowledgeBaseID: "0c84a098-d3ed-4b82-a165-e6de9691d6a8", CreatedAt: time.Date(2026, 10, 28, version, 0, 0, 0, time.UTC), Compatibility: librarysnapshot.Compatibility{WeKnoraVersion: "v0.7.1", SchemaVersion: 74, RetrieverEngine: "elasticsearch_v8", EmbeddingModel: "text-embedding-v4", EmbeddingDimension: 1024, IndexMappingSHA256: "137cde2965f3b56521f5fb722b4e0617f46d0e509e571b217fe01e090d5e4d01"}} for _, part := range parts { stored, err := archive.Put(bytes.NewReader([]byte(part.body))) if err != nil { t.Fatal(err) } stored.Kind, stored.Name, stored.Count = part.kind, part.name, part.count manifest.Parts = append(manifest.Parts, stored) } id, err := archive.Seal(manifest) if err != nil { t.Fatal(err) } ids = append(ids, id) } if err := archive.Close(); err != nil { t.Fatal(err) } return ids } func libraryFixtureRoot(t *testing.T) string { t.Helper() root, err := os.MkdirTemp(os.TempDir(), "kglib-read-") if err != nil { t.Fatal(err) } t.Cleanup(func() { os.RemoveAll(root) }) if err = os.Chmod(root, 0700); err != nil { t.Fatal(err) } return root } // 多版本共享大 blob:三个只读接口都必须返回完整正确的数据,不能降级或超时。 func TestLibraryReadPathsListSharedVersions(t *testing.T) { root := libraryFixtureRoot(t) ids := sealSharedVersions(t, root, 12, 200) config := Config{Store: &fakeLibraryStore{}, LibraryArchiveRoot: root, LibraryCapacityBudgetBytes: 1 << 30} list, err := readLibrarySnapshots(t.Context(), root) if err != nil { t.Fatalf("snapshots list: %v", err) } if !list.Available || len(list.Snapshots) != len(ids) { t.Fatalf("list available=%v snapshots=%d want=%d", list.Available, len(list.Snapshots), len(ids)) } // 共享字节只算一次:12 个内容相同的版本,跨版本内容字节必须等于"唯一 blob 之和", // 而不是 12 × 单版逻辑字节(后者会把共享 blob 重复计 12 遍)。 one := list.Snapshots[0].LogicalBytes archive, err := librarysnapshot.Open(root, 4*1024*1024) if err != nil { t.Fatal(err) } manifest, err := archive.LoadManifest(ids[0]) if err != nil { t.Fatal(err) } archive.Close() unique := map[string]int64{} for _, part := range manifest.Parts { unique[part.SHA256] = part.Bytes } var want int64 for _, size := range unique { want += size } if one <= 0 || list.ContentBytes != want { t.Fatalf("content bytes must deduplicate shared blobs across %d versions: content=%d want=%d logical_one=%d", len(ids), list.ContentBytes, want, one) } if want >= int64(len(ids))*one { t.Fatalf("dedup produced no gain: content=%d versions×logical=%d", want, int64(len(ids))*one) } for _, snapshot := range list.Snapshots { if snapshot.Totals["documents"] != 200 || snapshot.Totals["index_documents"] != 6000 { t.Fatalf("snapshot %s lost totals: %+v", snapshot.SnapshotID, snapshot.Totals) } // 枚举只证明清单可读,不能自称完整性已验证。 if snapshot.Verified { t.Fatalf("listing must not claim verified integrity: %+v", snapshot) } } if list.Verification != libraryVerificationManifest || list.VerificationNote == "" { t.Fatalf("listing must state what it verified: %q / %q", list.Verification, list.VerificationNote) } recorder := httptest.NewRecorder() libraryStorageHandler(recorder, httptest.NewRequest(http.MethodGet, "/api/v1/library/storage", nil), config) if recorder.Code != http.StatusOK { t.Fatalf("storage status=%d body=%s", recorder.Code, recorder.Body.String()) } var storage struct { Available bool `json:"available"` Versions []storageVersionView `json:"versions"` Plan json.RawMessage `json:"plan"` Notes []string `json:"notes"` } if err = json.Unmarshal(recorder.Body.Bytes(), &storage); err != nil { t.Fatal(err) } if !storage.Available || len(storage.Versions) != len(ids) { t.Fatalf("storage available=%v versions=%d want=%d", storage.Available, len(storage.Versions), len(ids)) } if len(storage.Plan) == 0 { t.Fatal("storage must still build the retention preview") } if strings.Contains(recorder.Body.String(), "校验失败") { t.Fatalf("storage must not report a verification failure for intact manifests: %s", recorder.Body.String()) } // 单版详情是"要动这个版本"的入口:它仍然逐部件复核字节,所以能给出 content 级结论。 detail := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/api/v1/library/snapshots/"+ids[0], nil) request.SetPathValue("id", ids[0]) librarySnapshotDetail(detail, request, config) if detail.Code != http.StatusOK { t.Fatalf("detail status=%d body=%s", detail.Code, detail.Body.String()) } var body struct { Snapshot struct { SnapshotID string `json:"snapshot_id"` Verified bool `json:"verified"` } `json:"snapshot"` Verification string `json:"verification"` } if err = json.Unmarshal(detail.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body.Snapshot.SnapshotID != ids[0] || !body.Snapshot.Verified || body.Verification != libraryVerificationContent { t.Fatalf("detail must report content-level verification: %+v", body) } } // 坏 blob:枚举照常(它不读字节,且明确标注自己只做了清单级校验), // 但详情这个执行入口必须拒绝,不能给出"完整"的结论。 func TestLibrarySnapshotDetailRejectsTamperedBlob(t *testing.T) { root := libraryFixtureRoot(t) ids := sealSharedVersions(t, root, 2, 40) archive, err := librarysnapshot.Open(root, 4*1024*1024) if err != nil { t.Fatal(err) } manifest, err := archive.LoadManifest(ids[0]) if err != nil { t.Fatal(err) } archive.Close() target := manifest.Parts[0] for _, part := range manifest.Parts { if part.Bytes > target.Bytes { target = part } } path := filepath.Join(root, "blobs", target.SHA256) info, err := os.Stat(path) if err != nil { t.Fatal(err) } corrupted := bytes.Repeat([]byte("x"), int(info.Size())) if err = os.WriteFile(path, corrupted, 0600); err != nil { t.Fatal(err) } config := Config{Store: &fakeLibraryStore{}, LibraryArchiveRoot: root} list, err := readLibrarySnapshots(t.Context(), root) if err != nil || !list.Available || len(list.Snapshots) != len(ids) { t.Fatalf("listing must still enumerate manifests: %+v err=%v", list, err) } recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/api/v1/library/snapshots/"+ids[0], nil) request.SetPathValue("id", ids[0]) librarySnapshotDetail(recorder, request, config) if recorder.Code != http.StatusNotFound { t.Fatalf("detail must fail closed on a tampered blob: status=%d body=%s", recorder.Code, recorder.Body.String()) } } // 坏清单:两个接口都必须报错,不能返回一个"看起来正常"的空/残缺列表。 func TestLibraryReadPathsRejectTamperedManifest(t *testing.T) { root := libraryFixtureRoot(t) ids := sealSharedVersions(t, root, 2, 10) path := filepath.Join(root, "versions", ids[0]+".json") file, err := os.OpenFile(path, os.O_RDWR, 0600) if err != nil { t.Fatal(err) } if _, err = file.WriteAt([]byte{' '}, 0); err != nil { t.Fatal(err) } file.Close() if list, err := readLibrarySnapshots(t.Context(), root); err == nil { t.Fatalf("snapshots list must fail closed: %+v", list) } recorder := httptest.NewRecorder() libraryStorageHandler(recorder, httptest.NewRequest(http.MethodGet, "/api/v1/library/storage", nil), Config{Store: &fakeLibraryStore{}, LibraryArchiveRoot: root}) if recorder.Code != http.StatusServiceUnavailable { t.Fatalf("storage must fail closed: status=%d body=%s", recorder.Code, recorder.Body.String()) } }