WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,468 @@
|
||||
package accesscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/accesspolicy"
|
||||
"github.com/Tencent/WeKnora/internal/knowledgeversion"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type versionCopyFixture struct {
|
||||
db *gorm.DB
|
||||
dir string
|
||||
fail bool
|
||||
afterDocument func(int)
|
||||
afterCopy func()
|
||||
}
|
||||
|
||||
func (f *versionCopyFixture) CloneKnowledgeVersion(ctx context.Context, source, target string, progress func(int, int) error) error {
|
||||
if !knowledgeversion.IsWorker(ctx) {
|
||||
return fmt.Errorf("missing worker context")
|
||||
}
|
||||
var docs []types.Knowledge
|
||||
if err := f.db.Where("knowledge_base_id=?", source).Find(&docs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := progress(0, len(docs)); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, src := range docs {
|
||||
dst := src
|
||||
dst.ID = uuid.NewString()
|
||||
dst.KnowledgeBaseID = target
|
||||
if src.FilePath != "" {
|
||||
b, err := os.ReadFile(src.FilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dst.FilePath = filepath.Join(f.dir, dst.ID)
|
||||
if err = os.WriteFile(dst.FilePath, b, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := f.db.Create(&dst).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if f.fail {
|
||||
return fmt.Errorf("injected storage outage")
|
||||
}
|
||||
var chunks []types.Chunk
|
||||
if err := f.db.Where("knowledge_id=?", src.ID).Find(&chunks).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range chunks {
|
||||
c.ID = uuid.NewString()
|
||||
c.SeqID = 0
|
||||
c.KnowledgeID = dst.ID
|
||||
c.KnowledgeBaseID = target
|
||||
if err := f.db.Create(&c).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if f.afterDocument != nil {
|
||||
f.afterDocument(i + 1)
|
||||
}
|
||||
if err := progress(i+1, len(docs)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if f.afterCopy != nil {
|
||||
f.afterCopy()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func knowledgeVersionFixture(t *testing.T) (*Service, *versionCopyFixture, string) {
|
||||
t.Helper()
|
||||
s := versionDB(t)
|
||||
if err := s.db.AutoMigrate(&types.TenantMember{}, &types.Chunk{}, &types.KnowledgeTag{}, &types.KnowledgeTagRelation{}, &KnowledgeVersionJob{}, &KnowledgeVersionAlias{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, statement := range []string{
|
||||
"ALTER TABLE users ADD COLUMN tenant_id INTEGER DEFAULT 1",
|
||||
"ALTER TABLE users ADD COLUMN is_active BOOLEAN DEFAULT TRUE",
|
||||
"ALTER TABLE users ADD COLUMN is_system_admin BOOLEAN DEFAULT FALSE",
|
||||
"ALTER TABLE users ADD COLUMN can_access_all_tenants BOOLEAN DEFAULT FALSE",
|
||||
} {
|
||||
if err := s.db.Exec(statement).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, id := range []string{"owner", "alice", "bob"} {
|
||||
if err := s.db.Table("users").Create(map[string]any{"id": id, "username": id, "is_active": true}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.db.Create(&types.TenantMember{TenantID: 1, UserID: id, Role: types.TenantRoleContributor, Status: types.TenantMemberStatusActive}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
fake := &versionCopyFixture{db: s.db, dir: t.TempDir()}
|
||||
s.SetKnowledgeVersionAdapter(fake)
|
||||
kb := types.KnowledgeBase{ID: uuid.NewString(), TenantID: 1, CreatorID: "owner", Name: "知识原件", Type: "document"}
|
||||
if err := s.db.Create(&kb).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.db.Create(&Publication{TenantID: 1, Kind: "knowledge", ResourceID: kb.ID, Global: true}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(fake.dir, "source.txt")
|
||||
if err := os.WriteFile(path, []byte("independent content"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
doc := types.Knowledge{ID: uuid.NewString(), TenantID: 1, KnowledgeBaseID: kb.ID, Title: "原文", Type: "file", FilePath: path, ParseStatus: types.ParseStatusCompleted, CustomMetadata: types.JSON(`{}`)}
|
||||
if err := s.db.Create(&doc).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.db.Create(&types.Chunk{ID: uuid.NewString(), TenantID: 1, KnowledgeBaseID: kb.ID, KnowledgeID: doc.ID, Content: "原片段", SourceContent: "原片段", ChunkType: types.ChunkTypeText, IndexStatus: "ready"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return s, fake, kb.ID
|
||||
}
|
||||
|
||||
func refreshFork(t *testing.T, s *Service, f *ResourceFork) {
|
||||
t.Helper()
|
||||
if err := s.db.First(f, "id=?", f.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
func runVersionJob(t *testing.T, s *Service) {
|
||||
t.Helper()
|
||||
if err := s.RunKnowledgeVersionJob(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
func forkVersionJob(t *testing.T, s *Service, f *ResourceFork) KnowledgeVersionJob {
|
||||
t.Helper()
|
||||
var j KnowledgeVersionJob
|
||||
if err := s.db.Where("fork_id=? AND purpose='fork'", f.ID).First(&j).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return j
|
||||
}
|
||||
|
||||
func TestKnowledgeVersionImmutableApprovalAndAlias(t *testing.T) {
|
||||
s, _, id := knowledgeVersionFixture(t)
|
||||
ctx := versionContext(1, "alice", false)
|
||||
admin := versionContext(1, "owner", true)
|
||||
f, err := s.forkResource(ctx, "knowledge", id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.Status != "copying" {
|
||||
t.Fatal("copy was exposed synchronously")
|
||||
}
|
||||
if _, err = s.submitResourceVersion(ctx, f.ID, ""); !errors.Is(err, errConflict) {
|
||||
t.Fatal("unfinished fork submitted", err)
|
||||
}
|
||||
runVersionJob(t, s)
|
||||
refreshFork(t, s, f)
|
||||
if f.Status != "ready" {
|
||||
t.Fatal("fork not completed", forkVersionJob(t, s, f))
|
||||
}
|
||||
s.db.Model(&types.Chunk{}).Where("knowledge_base_id=?", f.PersonalID).Update("content", "submitted edit")
|
||||
r, err := s.submitResourceVersion(ctx, f.ID, "review")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.Status != "snapshotting" {
|
||||
t.Fatal("submission exposed before file copy")
|
||||
}
|
||||
if err = s.reviewResourceVersion(admin, r.ID, true, ""); !errors.Is(err, errConflict) {
|
||||
t.Fatal("unfinished snapshot approved", err)
|
||||
}
|
||||
runVersionJob(t, s)
|
||||
s.db.First(r, "id=?", r.ID)
|
||||
if r.Status != "pending" {
|
||||
t.Fatal("snapshot not pending", r.Status)
|
||||
}
|
||||
if _, err := knowledgeVersionSnapshot(ctx, s.db, r.SnapshotID); !errors.Is(err, errConflict) {
|
||||
t.Fatal("snapshot leaked to normal context", err)
|
||||
}
|
||||
s.db.Model(&types.Chunk{}).Where("knowledge_base_id=?", f.PersonalID).Update("content", "later personal edit")
|
||||
if err = s.reviewResourceVersion(ctx, r.ID, true, ""); !errors.Is(err, errVersionDenied) {
|
||||
t.Fatal("ordinary user approved", err)
|
||||
}
|
||||
if err = s.reviewResourceVersion(admin, r.ID, true, "通过"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.db.First(r, "id=?", r.ID)
|
||||
if r.PublishedID == "" || r.PublishedID == id {
|
||||
t.Fatal("missing distinct published revision")
|
||||
}
|
||||
var original, published types.KnowledgeBase
|
||||
s.db.First(&original, "id=?", id)
|
||||
s.db.First(&published, "id=?", r.PublishedID)
|
||||
if !original.IsTemporary || published.IsTemporary || published.CreatorID != "owner" {
|
||||
t.Fatal("catalog promotion incorrect")
|
||||
}
|
||||
var chunk types.Chunk
|
||||
s.db.Where("knowledge_base_id=?", r.PublishedID).First(&chunk)
|
||||
if chunk.Content != "submitted edit" {
|
||||
t.Fatal("approval used mutable personal version", chunk.Content)
|
||||
}
|
||||
var oldDoc, newDoc types.Knowledge
|
||||
s.db.Where("knowledge_base_id=?", id).First(&oldDoc)
|
||||
s.db.Where("knowledge_base_id=?", r.PublishedID).First(&newDoc)
|
||||
if oldDoc.FilePath == newDoc.FilePath {
|
||||
t.Fatal("shared raw file")
|
||||
}
|
||||
if err = os.Remove(oldDoc.FilePath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b, err := os.ReadFile(newDoc.FilePath); err != nil || string(b) != "independent content" {
|
||||
t.Fatal("source deletion damaged published file", err)
|
||||
}
|
||||
latest, err := resolveKnowledgeVersion(s.db, 1, id)
|
||||
if err != nil || latest != r.PublishedID {
|
||||
t.Fatal("alias not advanced", latest, err)
|
||||
}
|
||||
again, err := s.forkResource(ctx, "knowledge", id)
|
||||
if err != nil || again.ID != f.ID {
|
||||
t.Fatal("old catalog link created duplicate fork", err)
|
||||
}
|
||||
if _, err = sourceView(s.db, 1, "knowledge", id); !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
t.Fatal("old revision remained in new selections", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeVersionDurableRetryAndIsolation(t *testing.T) {
|
||||
s, fake, id := knowledgeVersionFixture(t)
|
||||
ctx := versionContext(1, "alice", false)
|
||||
f, err := s.forkResource(ctx, "knowledge", id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
oldTarget := f.PersonalID
|
||||
fake.fail = true
|
||||
runVersionJob(t, s)
|
||||
refreshFork(t, s, f)
|
||||
j := forkVersionJob(t, s, f)
|
||||
if j.Status != "failed" || f.Status != "failed" {
|
||||
t.Fatal("failure was not persisted")
|
||||
}
|
||||
if err = s.retryKnowledgeVersionJob(versionContext(2, "alice", true), j.ID); !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
t.Fatal("cross tenant retry", err)
|
||||
}
|
||||
if err = s.retryKnowledgeVersionJob(versionContext(1, "bob", false), j.ID); !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
t.Fatal("cross user retry", err)
|
||||
}
|
||||
denied := accesspolicy.WithSnapshot(ctx, &accesspolicy.Snapshot{Restricted: true, Permissions: map[string]bool{"knowledge.read": true}})
|
||||
if err = s.retryKnowledgeVersionJob(denied, j.ID); !errors.Is(err, errVersionDenied) {
|
||||
t.Fatal("module denial bypass", err)
|
||||
}
|
||||
s.db.Model(&types.Chunk{}).Where("knowledge_base_id=?", id).Update("content", "source changed before retry")
|
||||
if err = s.retryKnowledgeVersionJob(ctx, j.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fake.fail = false
|
||||
// A fresh service instance simulates process restart; no in-memory queue is needed.
|
||||
restarted := NewService(s.db)
|
||||
restarted.SetKnowledgeVersionAdapter(fake)
|
||||
runVersionJob(t, restarted)
|
||||
refreshFork(t, s, f)
|
||||
j = forkVersionJob(t, s, f)
|
||||
if f.Status != "ready" || f.PersonalID == oldTarget || j.Attempts != 2 || len(j.AbandonedIDs) != 1 {
|
||||
t.Fatal("retry reused partial target", j)
|
||||
}
|
||||
var old types.KnowledgeBase
|
||||
s.db.First(&old, "id=?", oldTarget)
|
||||
if old.VersionState != "copying" || !old.IsTemporary {
|
||||
t.Fatal("partial version became visible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeVersionDetectsChangesDuringSnapshot(t *testing.T) {
|
||||
s, fake, id := knowledgeVersionFixture(t)
|
||||
ctx := versionContext(1, "alice", false)
|
||||
f, err := s.forkResource(ctx, "knowledge", id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runVersionJob(t, s)
|
||||
refreshFork(t, s, f)
|
||||
r, err := s.submitResourceVersion(ctx, f.ID, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fake.afterCopy = func() {
|
||||
s.db.Model(&types.Chunk{}).Where("knowledge_base_id=?", f.PersonalID).Update("content", "edited during snapshot")
|
||||
}
|
||||
runVersionJob(t, s)
|
||||
s.db.First(r, "id=?", r.ID)
|
||||
if r.Status != "snapshot_failed" {
|
||||
t.Fatal("torn snapshot exposed", r.Status)
|
||||
}
|
||||
var j KnowledgeVersionJob
|
||||
s.db.Where("submission_id=?", r.ID).First(&j)
|
||||
if err = s.retryKnowledgeVersionJob(ctx, j.ID); !errors.Is(err, errConflict) {
|
||||
t.Fatal("changed immutable proposal retried", err)
|
||||
}
|
||||
if _, err = s.submitResourceVersion(ctx, f.ID, "new submission"); err != nil {
|
||||
t.Fatal("failed snapshot prevented new submission", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeVersionStaleLeaseCannotExposeTarget(t *testing.T) {
|
||||
s, fake, id := knowledgeVersionFixture(t)
|
||||
ctx := versionContext(1, "alice", false)
|
||||
f, err := s.forkResource(ctx, "knowledge", id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fake.afterCopy = func() {
|
||||
s.db.Model(&KnowledgeVersionJob{}).Where("fork_id=?", f.ID).Updates(map[string]any{"lease_token": "replacement", "lease_until": time.Now().Add(-time.Minute)})
|
||||
}
|
||||
if err = s.RunKnowledgeVersionJob(context.Background()); !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
t.Fatal("stale worker finalized", err)
|
||||
}
|
||||
refreshFork(t, s, f)
|
||||
if f.Status != "copying" {
|
||||
t.Fatal("stale copy exposed")
|
||||
}
|
||||
oldTarget := f.PersonalID
|
||||
fake.afterCopy = nil
|
||||
runVersionJob(t, s)
|
||||
refreshFork(t, s, f)
|
||||
if f.Status != "ready" || f.PersonalID == oldTarget {
|
||||
t.Fatal("expired lease did not allocate independent target")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeVersionFingerprintCoversCategoriesAndSourceText(t *testing.T) {
|
||||
s, _, id := knowledgeVersionFixture(t)
|
||||
ctx := versionContext(1, "alice", false)
|
||||
_, a, err := s.resourceSnapshot(s.db, ctx, "knowledge", id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.db.Model(&types.Chunk{}).Where("knowledge_base_id=?", id).Update("source_content", "changed raw parser output")
|
||||
_, b, _ := s.resourceSnapshot(s.db, ctx, "knowledge", id)
|
||||
if a == b {
|
||||
t.Fatal("raw source change ignored")
|
||||
}
|
||||
if err = s.db.Create(&types.KnowledgeTag{ID: uuid.NewString(), TenantID: 1, KnowledgeBaseID: id, Name: "分类"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, c, _ := s.resourceSnapshot(s.db, ctx, "knowledge", id)
|
||||
if b == c {
|
||||
t.Fatal("category change ignored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeVersionMissingSourceDoesNotBlockQueue(t *testing.T) {
|
||||
s, fake, id := knowledgeVersionFixture(t)
|
||||
ctx := versionContext(1, "alice", false)
|
||||
f, err := s.forkResource(ctx, "knowledge", id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fake.fail = true
|
||||
runVersionJob(t, s)
|
||||
j := forkVersionJob(t, s, f)
|
||||
if err = s.retryKnowledgeVersionJob(ctx, j.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = s.db.Delete(&types.KnowledgeBase{}, "id=?", id).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runVersionJob(t, s)
|
||||
j = forkVersionJob(t, s, f)
|
||||
if j.Status != "failed" || j.LeaseToken != "" {
|
||||
t.Fatal("missing source left a blocking queue entry", j)
|
||||
}
|
||||
if err = s.RunKnowledgeVersionJob(context.Background()); !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
t.Fatal("failed source blocked queue", err)
|
||||
}
|
||||
var log types.AuditLog
|
||||
if err = s.db.Where("target_id=? AND outcome=?", j.ID, types.AuditOutcomeFailed).First(&log).Error; err != nil {
|
||||
t.Fatal("failure audit missing", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeVersionRejectsUnsupportedBeforeReservation(t *testing.T) {
|
||||
s, _, id := knowledgeVersionFixture(t)
|
||||
if err := s.db.Model(&types.Chunk{}).Where("knowledge_base_id=?", id).Update("chunk_type", types.ChunkTypeTableSummary).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.forkResource(versionContext(1, "alice", false), "knowledge", id); !errors.Is(err, errInvalid) {
|
||||
t.Fatal("unsupported copy reserved", err)
|
||||
}
|
||||
var count int64
|
||||
if err := s.db.Model(&KnowledgeVersionJob{}).Count(&count).Error; err != nil || count != 0 {
|
||||
t.Fatal("unsupported request created a job", count, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeVersionApprovalConflictAndAliasChain(t *testing.T) {
|
||||
s, _, id := knowledgeVersionFixture(t)
|
||||
ctx, admin := versionContext(1, "alice", false), versionContext(1, "owner", true)
|
||||
f, err := s.forkResource(ctx, "knowledge", id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runVersionJob(t, s)
|
||||
refreshFork(t, s, f)
|
||||
r, err := s.submitResourceVersion(ctx, f.ID, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runVersionJob(t, s)
|
||||
if err = s.db.Model(&types.Chunk{}).Where("knowledge_base_id=?", id).Update("content", "global edit").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = s.reviewResourceVersion(admin, r.ID, true, ""); !errors.Is(err, errConflict) {
|
||||
t.Fatal("changed global source overwritten", err)
|
||||
}
|
||||
for _, action := range []struct {
|
||||
id string
|
||||
handler gin.HandlerFunc
|
||||
}{{r.ID, s.WithdrawResourceVersion}, {f.ID, s.RebaseResourceVersion}} {
|
||||
response := httptest.NewRecorder()
|
||||
request, _ := gin.CreateTestContext(response)
|
||||
request.Request = httptest.NewRequest("POST", "/", nil).WithContext(ctx)
|
||||
request.Params = gin.Params{{Key: "id", Value: action.id}}
|
||||
action.handler(request)
|
||||
if response.Code != 200 {
|
||||
t.Fatal("version action failed", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
if err = s.db.Create(&ResourceHidden{TenantID: 1, OwnerID: "bob", Kind: "knowledge", ResourceID: id}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
previous := id
|
||||
for i := 0; i < 2; i++ {
|
||||
r, err = s.submitResourceVersion(ctx, f.ID, fmt.Sprintf("revision %d", i))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runVersionJob(t, s)
|
||||
if err = s.reviewResourceVersion(admin, r.ID, true, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = s.db.First(r, "id=?", r.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, old := range []string{id, previous} {
|
||||
latest, err := resolveKnowledgeVersion(s.db, 1, old)
|
||||
if err != nil || latest != r.PublishedID {
|
||||
t.Fatal("alias chain not advanced", old, latest, err)
|
||||
}
|
||||
}
|
||||
var hidden ResourceHidden
|
||||
if err = s.db.Where("tenant_id=1 AND owner_id='bob' AND resource_id=?", r.PublishedID).First(&hidden).Error; err != nil {
|
||||
t.Fatal("personal hiding lost on approval", err)
|
||||
}
|
||||
previous = r.PublishedID
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user