Files
knowledge-governance/internal/httpapi/library_restore_test.go
T
姚章浩 ac3b395102
build / build (push) Successful in 1m21s
fix(library): 对齐真实 WeKnora Schema + 入口级真实服务验收
为什么:候选环境改用真实 WeKnora 表结构后,暴露出多处"按猜的列名/形状"的假设,
以及若干 varchar 长度、对象定位符、空索引基线问题,导致真实链路跑不通。

- knowledge_tag_relations 无 tenant_id/KB 列 → 归属走父表;resources 作用域=绑定+chunk 引用。
- 写入顺序按真实外键;无代理键表用内容摘要核对。
- 队列作业 ID / 版本记录 ID 改固定长度摘要(列宽 varchar(36)/varchar(64))。
- 对象定位符拆 storage://<backend>/oss://<bucket>/<key>;provider 可配置。
- 空索引基线全链路一致(Current/Export/Backup/Archive),非空仍严格比对映射。
- finalize 阶段:版本登记失败不报成功;崩溃接管复用已持久化预备份。
- 迁移 000069:同一目标的 sync/restore 跨 kind 互斥。
2026-10-28 17:53:43 +08:00

88 lines
3.6 KiB
Go

package httpapi
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"knowledge-governance/internal/governance"
)
func restoreRequest(body, role string) *http.Request {
request := httptest.NewRequest(http.MethodPost, "/api/v1/library/restores", strings.NewReader(body))
request.Header.Set("Content-Type", "application/json")
ctx := context.WithValue(request.Context(), sessionKey{}, sessionIdentity{Username: "tester", Role: role})
return request.WithContext(ctx)
}
// 恢复是写生产:非管理员直接拒绝,任何授权都不会被创建。
func TestLibraryRestoreRequiresAdmin(t *testing.T) {
store := &fakeLibraryStore{}
recorder := httptest.NewRecorder()
libraryRestoreEnqueue(recorder, restoreRequest(`{"scope":"internal","snapshot_id":"snap-1"}`, roleEngineer), Config{Store: store})
if recorder.Code != http.StatusForbidden {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
if store.createN != 0 {
t.Fatal("non-admin must not create a restore job")
}
}
// 闸口关闭 / 缺恢复演练证据 / 缺该快照授权:逐条拒绝且不建作业。
func TestLibraryRestoreRefusesWithoutGatesAndAuthorization(t *testing.T) {
store := &fakeLibraryStore{}
recorder := httptest.NewRecorder()
libraryRestoreEnqueue(recorder, restoreRequest(`{"scope":"internal","snapshot_id":"snap-1"}`, roleAdmin), Config{Store: store})
if recorder.Code != http.StatusServiceUnavailable {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
body := recorder.Body.String()
for _, want := range []string{"生产写入闸口关闭", "恢复演练证据", "恢复授权"} {
if !strings.Contains(body, want) {
t.Fatalf("refusal must name %q: %s", want, body)
}
}
if store.createN != 0 {
t.Fatal("must not create a restore job while blocked")
}
}
// 授权必须绑定"恢复到这个快照":同步授权不能顶替。
func TestLibraryRestoreRequiresSnapshotBoundGrant(t *testing.T) {
store := &fakeLibraryStore{}
syncDigest, err := librarySyncParamsDigest("internal", "snap-1")
if err != nil {
t.Fatal(err)
}
now := time.Now().UTC()
store.grant = &governance.LibraryReleaseAuthorization{ID: "a", Scope: "internal", SnapshotID: "snap-1",
ParamsDigest: syncDigest, ExpiresAt: now.Add(time.Hour), CreatedAt: now}
config := Config{Store: store, ProductionReleaseRequestsEnabled: true, ProductionBackupRestoreVerified: true}
recorder := httptest.NewRecorder()
libraryRestoreEnqueue(recorder, restoreRequest(`{"scope":"internal","snapshot_id":"snap-1"}`, roleAdmin), config)
if recorder.Code != http.StatusServiceUnavailable || !strings.Contains(recorder.Body.String(), "恢复授权") {
t.Fatalf("sync grant must not authorize a restore: %d %s", recorder.Code, recorder.Body.String())
}
if store.createN != 0 {
t.Fatal("must not create a restore job with a sync-only grant")
}
}
// 续作只对失败/取消的同步与恢复作业开放;封存作业走封存入口。
func TestLibraryJobResumeRejectsNonFailedJobs(t *testing.T) {
store := &fakeLibraryStore{jobs: []governance.LibraryJob{{ID: "job-1", Scope: "internal",
Kind: governance.LibraryJobSync, State: governance.LibraryJobSucceeded}}}
request := httptest.NewRequest(http.MethodPost, "/api/v1/library/jobs/job-1/resume", nil)
request.SetPathValue("id", "job-1")
request = request.WithContext(context.WithValue(request.Context(), sessionKey{},
sessionIdentity{Username: "admin", Role: roleAdmin}))
recorder := httptest.NewRecorder()
libraryJobResume(recorder, request, Config{Store: store})
if recorder.Code != http.StatusConflict {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}