1f0c8be812
模拟发布暴露的第 8 个契约缺陷:PG 通道改写资源行时只替换了外层 storage://<backend-id> 前缀,内层 oss://<bucket>/<key> 照搬源桶。 WeKnora v0.7.1 官方 GetKnowledgeFile 从路径内层桶名直接 GetObject, 导致目标端所有文档预览以生产凭据读源桶失败(403),前端表现为预览空白。 - PostgresImporterConfig 新增 TargetBucket;nested locator 导入时改写 内层桶名为目标桶,location_hash 随改写后的路径重算 - 未配置目标桶时保持旧行为透传,不做猜测 - 4 个单测锁定:桶改写+hash重算、非嵌套 locator 不变、无配置透传、非法桶名拒绝 - 端到端复验:重封 canary 批次重新发布 4/4 verified,目标端资源行 全部指向 dobot-weknora-test,location_hash 与路径逐行比对一致
133 lines
5.0 KiB
Go
133 lines
5.0 KiB
Go
package v071
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// nilPGDB is a non-nil postgresTransactionalDB whose Begin is never invoked by
|
|
// prepareResourceRow; it exists only to satisfy NewPostgresImporter's nil check.
|
|
type nilPGDB struct{}
|
|
|
|
func (nilPGDB) Begin(context.Context) (pgx.Tx, error) { panic("unused") }
|
|
|
|
func resourceRowJSON(t *testing.T, fields map[string]any) json.RawMessage {
|
|
t.Helper()
|
|
encoded, err := json.Marshal(fields)
|
|
if err != nil {
|
|
t.Fatalf("encode resource row: %v", err)
|
|
}
|
|
return encoded
|
|
}
|
|
|
|
func rewriteResource(t *testing.T, config PostgresImporterConfig, row json.RawMessage) map[string]any {
|
|
t.Helper()
|
|
importer, err := NewPostgresImporter(nilPGDB{}, config)
|
|
if err != nil {
|
|
t.Fatalf("new importer: %v", err)
|
|
}
|
|
rewritten, pair, err := importer.prepareResourceRow(row, 10000)
|
|
if err != nil {
|
|
t.Fatalf("prepare resource row: %v", err)
|
|
}
|
|
var out map[string]any
|
|
if err := json.Unmarshal(rewritten, &out); err != nil {
|
|
t.Fatalf("decode rewritten row: %v", err)
|
|
}
|
|
if pair.id != out["id"] {
|
|
t.Fatalf("pair identity mismatch: %q vs %v", pair.id, out["id"])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// WeKnora's official file service resolves GetFile from the bucket embedded in
|
|
// the nested oss:// locator, not from the storage backend row. If the release
|
|
// import carried the source bucket into the target row, every preview on the
|
|
// target would read the source bucket with target credentials (403). This test
|
|
// locks the rewrite to the configured target bucket and recomputes
|
|
// location_hash exactly like the official resourceLocationHash.
|
|
func TestPostgresResourceRewriteReplacesNestedSourceBucket(t *testing.T) {
|
|
backendID := "6129cfd1-a26c-41dd-88cb-e6a800daa3bb"
|
|
row := resourceRowJSON(t, map[string]any{
|
|
"id": "res-1",
|
|
"handle": "ABCDEFGHIJKLMNOPQRSTUV",
|
|
"tenant_id": 10000,
|
|
"provider": "oss",
|
|
"storage_backend_id": "source-backend",
|
|
"physical_path": "storage://source-backend/oss://yzh-weknora-test/weknora/10000/knowledge-1/source.md",
|
|
"location_hash": "stale",
|
|
})
|
|
out := rewriteResource(t, PostgresImporterConfig{
|
|
TargetStorageBackendID: backendID, TargetProvider: "oss", TargetBucket: "dobot-weknora-test",
|
|
}, row)
|
|
want := "storage://" + backendID + "/oss://dobot-weknora-test/weknora/10000/knowledge-1/source.md"
|
|
if out["physical_path"] != want {
|
|
t.Fatalf("physical_path=%v want %v", out["physical_path"], want)
|
|
}
|
|
if out["storage_backend_id"] != backendID || out["provider"] != "oss" {
|
|
t.Fatalf("backend/provider not rewritten: %v %v", out["storage_backend_id"], out["provider"])
|
|
}
|
|
digest := sha256.Sum256([]byte(want))
|
|
if out["location_hash"] != hex.EncodeToString(digest[:]) {
|
|
t.Fatalf("location_hash not recomputed for rewritten path: %v", out["location_hash"])
|
|
}
|
|
}
|
|
|
|
// Locators without an embedded bucket (plain provider paths used by some
|
|
// community deployments) must keep their key unchanged while still receiving
|
|
// the backend rewrite.
|
|
func TestPostgresResourceRewriteKeepsNonNestedLocator(t *testing.T) {
|
|
backendID := "target-backend"
|
|
row := resourceRowJSON(t, map[string]any{
|
|
"id": "res-2",
|
|
"handle": "ABCDEFGHIJKLMNOPQRSTUV",
|
|
"tenant_id": 10000,
|
|
"provider": "minio",
|
|
"storage_backend_id": "source-backend",
|
|
"physical_path": "storage://source-backend/10000/knowledge-2/source.txt",
|
|
"location_hash": "stale",
|
|
})
|
|
out := rewriteResource(t, PostgresImporterConfig{
|
|
TargetStorageBackendID: backendID, TargetProvider: "minio", TargetBucket: "target-bucket",
|
|
}, row)
|
|
want := "storage://" + backendID + "/10000/knowledge-2/source.txt"
|
|
if out["physical_path"] != want {
|
|
t.Fatalf("physical_path=%v want %v", out["physical_path"], want)
|
|
}
|
|
}
|
|
|
|
// Without a configured target bucket the rewrite must stay conservative:
|
|
// the nested bucket passes through untouched (legacy behaviour) instead of
|
|
// guessing a bucket name.
|
|
func TestPostgresResourceRewriteWithoutTargetBucketKeepsLocator(t *testing.T) {
|
|
row := resourceRowJSON(t, map[string]any{
|
|
"id": "res-3",
|
|
"handle": "ABCDEFGHIJKLMNOPQRSTUV",
|
|
"tenant_id": 10000,
|
|
"provider": "oss",
|
|
"storage_backend_id": "source-backend",
|
|
"physical_path": "storage://source-backend/oss://legacy-bucket/weknora/10000/knowledge-3/source.md",
|
|
"location_hash": "stale",
|
|
})
|
|
out := rewriteResource(t, PostgresImporterConfig{
|
|
TargetStorageBackendID: "target-backend", TargetProvider: "oss",
|
|
}, row)
|
|
want := "storage://target-backend/oss://legacy-bucket/weknora/10000/knowledge-3/source.md"
|
|
if out["physical_path"] != want {
|
|
t.Fatalf("physical_path=%v want %v", out["physical_path"], want)
|
|
}
|
|
}
|
|
|
|
func TestPostgresImporterRejectsInvalidTargetBucket(t *testing.T) {
|
|
if _, err := NewPostgresImporter(nilPGDB{}, PostgresImporterConfig{
|
|
TargetStorageBackendID: "backend", TargetProvider: "oss", TargetBucket: "bad bucket!",
|
|
}); err == nil {
|
|
t.Fatal("expected invalid target bucket rejection")
|
|
}
|
|
}
|