|
|
|
|
@@ -289,13 +289,15 @@ func setupHarness(t *testing.T) *testHarness {
|
|
|
|
|
|
|
|
|
|
// Knowledge Bases (nil storage/ingester/embedder — CRUD works, upload/rebuild return 503)
|
|
|
|
|
kbH := NewKnowledgeBaseHandler(stores, nil, nil, nil)
|
|
|
|
|
protected.POST("/knowledge-bases", kbH.CreateKB)
|
|
|
|
|
protected.POST("/knowledge-bases", middleware.RequirePermission(authpkg.PermKBCreate, stores), kbH.CreateKB)
|
|
|
|
|
protected.GET("/knowledge-bases", kbH.ListKBs)
|
|
|
|
|
protected.GET("/knowledge-bases/:id", kbH.GetKB)
|
|
|
|
|
protected.PUT("/knowledge-bases/:id", kbH.UpdateKB)
|
|
|
|
|
protected.DELETE("/knowledge-bases/:id", kbH.DeleteKB)
|
|
|
|
|
protected.POST("/knowledge-bases/:id/documents", kbH.UploadDocument)
|
|
|
|
|
protected.POST("/knowledge-bases/:id/documents", middleware.RequirePermission(authpkg.PermKBWrite, stores), kbH.UploadDocument)
|
|
|
|
|
protected.GET("/knowledge-bases/:id/documents", kbH.ListDocuments)
|
|
|
|
|
protected.GET("/knowledge-bases/:id/documents/:docId/status", kbH.GetDocumentStatus)
|
|
|
|
|
protected.DELETE("/knowledge-bases/:id/documents/:docId", kbH.DeleteDocument)
|
|
|
|
|
protected.POST("/knowledge-bases/:id/search", kbH.SearchKB)
|
|
|
|
|
protected.POST("/knowledge-bases/:id/rebuild", kbH.RebuildKB)
|
|
|
|
|
protected.GET("/channels/:id/knowledge-bases", kbH.GetChannelKBs)
|
|
|
|
|
@@ -2682,6 +2684,141 @@ func TestIntegration_ChannelKBLinking(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── KB Document Status + Delete ─────────────────
|
|
|
|
|
|
|
|
|
|
func TestIntegration_KB_DocumentStatusAndDelete(t *testing.T) {
|
|
|
|
|
h := setupHarness(t)
|
|
|
|
|
adminID, adminToken := h.createAdminUser("admin", "admin@test.com")
|
|
|
|
|
|
|
|
|
|
// Create a KB
|
|
|
|
|
w := h.request("POST", "/api/v1/knowledge-bases", adminToken, map[string]interface{}{
|
|
|
|
|
"name": "Doc Test KB",
|
|
|
|
|
})
|
|
|
|
|
if w.Code != http.StatusCreated {
|
|
|
|
|
t.Fatalf("create KB: want 201, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
var kb map[string]interface{}
|
|
|
|
|
decode(w, &kb)
|
|
|
|
|
kbID := kb["id"].(string)
|
|
|
|
|
|
|
|
|
|
// Seed a document directly (upload handler requires object store)
|
|
|
|
|
docID := uuid.NewString()
|
|
|
|
|
_, err := database.TestDB.Exec(dialectSQL(
|
|
|
|
|
`INSERT INTO kb_documents (id, kb_id, filename, content_type, size_bytes, storage_key, status, uploaded_by, created_at, updated_at)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`),
|
|
|
|
|
docID, kbID, "test.md", "text/markdown", 1234, "kb/"+kbID+"/"+docID+"_test.md",
|
|
|
|
|
"chunking", adminID, time.Now().UTC().Format(time.RFC3339), time.Now().UTC().Format(time.RFC3339))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("seed doc: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GET document status
|
|
|
|
|
w = h.request("GET", fmt.Sprintf("/api/v1/knowledge-bases/%s/documents/%s/status", kbID, docID), adminToken, nil)
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("get doc status: want 200, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
var status map[string]interface{}
|
|
|
|
|
decode(w, &status)
|
|
|
|
|
if status["id"] != docID {
|
|
|
|
|
t.Fatalf("doc status: id mismatch: got %s, want %s", status["id"], docID)
|
|
|
|
|
}
|
|
|
|
|
if status["status"] != "chunking" {
|
|
|
|
|
t.Fatalf("doc status: want 'chunking', got %s", status["status"])
|
|
|
|
|
}
|
|
|
|
|
if status["filename"] != "test.md" {
|
|
|
|
|
t.Fatalf("doc status: want filename 'test.md', got %s", status["filename"])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GET status for wrong KB — should 404
|
|
|
|
|
fakeKB := uuid.NewString()
|
|
|
|
|
w = h.request("GET", fmt.Sprintf("/api/v1/knowledge-bases/%s/documents/%s/status", fakeKB, docID), adminToken, nil)
|
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
|
|
|
t.Fatalf("doc status wrong KB: want 404, got %d", w.Code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GET status for wrong doc — should 404
|
|
|
|
|
w = h.request("GET", fmt.Sprintf("/api/v1/knowledge-bases/%s/documents/%s/status", kbID, uuid.NewString()), adminToken, nil)
|
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
|
|
|
t.Fatalf("doc status wrong doc: want 404, got %d", w.Code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DELETE document
|
|
|
|
|
w = h.request("DELETE", fmt.Sprintf("/api/v1/knowledge-bases/%s/documents/%s", kbID, docID), adminToken, nil)
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("delete doc: want 200, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
var delResp map[string]interface{}
|
|
|
|
|
decode(w, &delResp)
|
|
|
|
|
if delResp["deleted"] != true {
|
|
|
|
|
t.Fatalf("delete doc: want deleted=true, got %v", delResp["deleted"])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify gone
|
|
|
|
|
w = h.request("GET", fmt.Sprintf("/api/v1/knowledge-bases/%s/documents/%s/status", kbID, docID), adminToken, nil)
|
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
|
|
|
t.Fatalf("deleted doc status: want 404, got %d", w.Code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DELETE on wrong KB — should 404
|
|
|
|
|
docID2 := uuid.NewString()
|
|
|
|
|
database.TestDB.Exec(dialectSQL(
|
|
|
|
|
`INSERT INTO kb_documents (id, kb_id, filename, content_type, size_bytes, storage_key, status, uploaded_by, created_at, updated_at)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`),
|
|
|
|
|
docID2, kbID, "other.txt", "text/plain", 100, "placeholder",
|
|
|
|
|
"pending", adminID, time.Now().UTC().Format(time.RFC3339), time.Now().UTC().Format(time.RFC3339))
|
|
|
|
|
|
|
|
|
|
w = h.request("DELETE", fmt.Sprintf("/api/v1/knowledge-bases/%s/documents/%s", fakeKB, docID2), adminToken, nil)
|
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
|
|
|
t.Fatalf("delete doc wrong KB: want 404, got %d", w.Code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── KB Update Empty Body ────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestIntegration_KB_UpdateEmptyBody(t *testing.T) {
|
|
|
|
|
h := setupHarness(t)
|
|
|
|
|
_, adminToken := h.createAdminUser("admin", "admin@test.com")
|
|
|
|
|
|
|
|
|
|
// Create KB
|
|
|
|
|
w := h.request("POST", "/api/v1/knowledge-bases", adminToken, map[string]interface{}{
|
|
|
|
|
"name": "Empty Update KB",
|
|
|
|
|
})
|
|
|
|
|
if w.Code != http.StatusCreated {
|
|
|
|
|
t.Fatalf("create KB: want 201, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
var kb map[string]interface{}
|
|
|
|
|
decode(w, &kb)
|
|
|
|
|
kbID := kb["id"].(string)
|
|
|
|
|
|
|
|
|
|
// Update with empty fields — should 400
|
|
|
|
|
w = h.request("PUT", fmt.Sprintf("/api/v1/knowledge-bases/%s", kbID), adminToken,
|
|
|
|
|
map[string]interface{}{})
|
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
|
|
|
t.Fatalf("empty update: want 400, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── KB Permission Enforcement ───────────────────
|
|
|
|
|
|
|
|
|
|
func TestIntegration_KB_PermissionEnforcement(t *testing.T) {
|
|
|
|
|
h := setupHarness(t)
|
|
|
|
|
_, _ = h.createAdminUser("admin", "admin@test.com")
|
|
|
|
|
|
|
|
|
|
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_registration', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
|
|
|
|
|
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('default_user_active', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
|
|
|
|
|
|
|
|
|
|
_, userToken := h.registerUser("normperm", "normperm@test.com", "password123")
|
|
|
|
|
|
|
|
|
|
// Regular user without kb.create should get 403
|
|
|
|
|
// (TruncateAll wipes the Everyone group, so no default permissions)
|
|
|
|
|
w := h.request("POST", "/api/v1/knowledge-bases", userToken, map[string]interface{}{
|
|
|
|
|
"name": "Should Fail",
|
|
|
|
|
})
|
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
|
|
|
t.Fatalf("kb create without permission: want 403, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
// GROUPS + RESOURCE GRANTS (v0.16.0)
|
|
|
|
|
// ══════════════════════════════════════════════
|
|
|
|
|
@@ -3132,6 +3269,19 @@ func TestIntegration_KB_CreateAuth(t *testing.T) {
|
|
|
|
|
_ = adminToken
|
|
|
|
|
|
|
|
|
|
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('default_user_active', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
|
|
|
|
|
|
|
|
|
|
// Seed Everyone group with kb.create so the permission middleware passes
|
|
|
|
|
// and we can test the handler-level scope checks below.
|
|
|
|
|
_, err := database.TestDB.Exec(dialectSQL(`
|
|
|
|
|
INSERT INTO groups (id, name, description, scope, created_by, source, permissions)
|
|
|
|
|
VALUES ($1, $2, $3, $4, NULL, $5, $6)`),
|
|
|
|
|
"00000000-0000-0000-0000-000000000001", "Everyone",
|
|
|
|
|
"Implicit group — all authenticated users receive these permissions.",
|
|
|
|
|
"global", "system", `["model.use","kb.read","kb.create","channel.create"]`)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("seed Everyone group: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, userToken := h.registerUser("kbuser", "kb@test.com", "password123")
|
|
|
|
|
|
|
|
|
|
// Regular user should NOT be able to create global KBs
|
|
|
|
|
@@ -3163,6 +3313,71 @@ func TestIntegration_KB_CreateAuth(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── KB Team Scope Creation ──────────────────────
|
|
|
|
|
|
|
|
|
|
func TestIntegration_KB_TeamScopeCreate(t *testing.T) {
|
|
|
|
|
h := setupHarness(t)
|
|
|
|
|
_, adminToken := h.createAdminUser("admin", "admin@test.com")
|
|
|
|
|
|
|
|
|
|
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_registration', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
|
|
|
|
|
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('default_user_active', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
|
|
|
|
|
|
|
|
|
|
// Seed Everyone group with kb.create
|
|
|
|
|
database.TestDB.Exec(dialectSQL(`
|
|
|
|
|
INSERT INTO groups (id, name, description, scope, created_by, source, permissions)
|
|
|
|
|
VALUES ($1, $2, $3, $4, NULL, $5, $6)`),
|
|
|
|
|
"00000000-0000-0000-0000-000000000001", "Everyone",
|
|
|
|
|
"Implicit group", "global", "system",
|
|
|
|
|
`["model.use","kb.read","kb.create","channel.create"]`)
|
|
|
|
|
|
|
|
|
|
userID, userToken := h.registerUser("teamkb", "teamkb@test.com", "password123")
|
|
|
|
|
|
|
|
|
|
// Create a team via admin
|
|
|
|
|
w := h.request("POST", "/api/v1/admin/teams", adminToken, map[string]string{
|
|
|
|
|
"name": "KB Team", "description": "team for KB test",
|
|
|
|
|
})
|
|
|
|
|
if w.Code != http.StatusCreated {
|
|
|
|
|
t.Fatalf("create team: want 201, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
var team map[string]interface{}
|
|
|
|
|
decode(w, &team)
|
|
|
|
|
teamID := team["id"].(string)
|
|
|
|
|
|
|
|
|
|
// Add user as regular member — should NOT be able to create team KB
|
|
|
|
|
w = h.request("POST", fmt.Sprintf("/api/v1/admin/teams/%s/members", teamID), adminToken,
|
|
|
|
|
map[string]string{"user_id": userID, "role": "member"})
|
|
|
|
|
if w.Code != http.StatusOK && w.Code != http.StatusCreated {
|
|
|
|
|
t.Fatalf("add member: want 200/201, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w = h.request("POST", "/api/v1/knowledge-bases", userToken, map[string]interface{}{
|
|
|
|
|
"name": "Team KB by Member", "scope": "team", "team_id": teamID,
|
|
|
|
|
})
|
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
|
|
|
t.Fatalf("member create team KB: want 403, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Promote to team admin — should succeed
|
|
|
|
|
database.TestDB.Exec(dialectSQL(
|
|
|
|
|
`UPDATE team_members SET role = 'admin' WHERE team_id = $1 AND user_id = $2`),
|
|
|
|
|
teamID, userID)
|
|
|
|
|
|
|
|
|
|
w = h.request("POST", "/api/v1/knowledge-bases", userToken, map[string]interface{}{
|
|
|
|
|
"name": "Team KB by Admin", "scope": "team", "team_id": teamID,
|
|
|
|
|
})
|
|
|
|
|
if w.Code != http.StatusCreated {
|
|
|
|
|
t.Fatalf("team admin create team KB: want 201, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
var kbResp map[string]interface{}
|
|
|
|
|
decode(w, &kbResp)
|
|
|
|
|
if kbResp["scope"] != "team" {
|
|
|
|
|
t.Fatalf("team KB scope: want 'team', got %s", kbResp["scope"])
|
|
|
|
|
}
|
|
|
|
|
if kbResp["team_id"] != teamID {
|
|
|
|
|
t.Fatalf("team KB team_id: want %s, got %s", teamID, kbResp["team_id"])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── KB Discoverable Flag ──
|
|
|
|
|
|
|
|
|
|
func TestIntegration_KB_Discoverable(t *testing.T) {
|
|
|
|
|
@@ -3196,8 +3411,21 @@ func TestIntegration_KB_Discoverable(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
found := false
|
|
|
|
|
for _, kb := range kbs {
|
|
|
|
|
if kb.(map[string]interface{})["id"] == kbID {
|
|
|
|
|
kbMap := kb.(map[string]interface{})
|
|
|
|
|
if kbMap["id"] == kbID {
|
|
|
|
|
found = true
|
|
|
|
|
// Verify kbResponse shape (not raw model)
|
|
|
|
|
if _, ok := kbMap["status"]; !ok {
|
|
|
|
|
t.Fatal("discoverable response missing 'status' field (not using kbResponse?)")
|
|
|
|
|
}
|
|
|
|
|
if _, ok := kbMap["chunk_count"]; !ok {
|
|
|
|
|
t.Fatal("discoverable response missing 'chunk_count' field (not using kbResponse?)")
|
|
|
|
|
}
|
|
|
|
|
// created_at should be ISO format (kbResponse), not Go default with nanoseconds
|
|
|
|
|
ts, _ := kbMap["created_at"].(string)
|
|
|
|
|
if len(ts) > 0 && strings.Contains(ts, ".") {
|
|
|
|
|
t.Fatalf("discoverable created_at has nanoseconds (raw model leak): %s", ts)
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -3244,6 +3472,94 @@ func TestIntegration_KB_DirectAccessPolicy(t *testing.T) {
|
|
|
|
|
_ = w
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── KB SetDiscoverable Auth Enforcement ─────────
|
|
|
|
|
|
|
|
|
|
func TestIntegration_KB_SetDiscoverableAuth(t *testing.T) {
|
|
|
|
|
h := setupHarness(t)
|
|
|
|
|
adminID, adminToken := h.createAdminUser("admin", "admin@test.com")
|
|
|
|
|
_ = adminID
|
|
|
|
|
|
|
|
|
|
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_registration', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
|
|
|
|
|
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('default_user_active', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
|
|
|
|
|
|
|
|
|
|
_, aliceToken := h.registerUser("alice", "alice@test.com", "password123")
|
|
|
|
|
_, bobToken := h.registerUser("bob", "bob@test.com", "password123")
|
|
|
|
|
|
|
|
|
|
// Seed Everyone group with kb.create so users can create KBs
|
|
|
|
|
database.TestDB.Exec(dialectSQL(`
|
|
|
|
|
INSERT INTO groups (id, name, description, scope, created_by, source, permissions)
|
|
|
|
|
VALUES ($1, $2, $3, $4, NULL, $5, $6)`),
|
|
|
|
|
"00000000-0000-0000-0000-000000000001", "Everyone",
|
|
|
|
|
"Implicit group", "global", "system",
|
|
|
|
|
`["model.use","kb.read","kb.create","channel.create"]`)
|
|
|
|
|
|
|
|
|
|
// Alice creates a personal KB
|
|
|
|
|
w := h.request("POST", "/api/v1/knowledge-bases", aliceToken, map[string]interface{}{
|
|
|
|
|
"name": "Alice KB",
|
|
|
|
|
})
|
|
|
|
|
if w.Code != http.StatusCreated {
|
|
|
|
|
t.Fatalf("alice create KB: want 201, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
var aliceKB map[string]interface{}
|
|
|
|
|
decode(w, &aliceKB)
|
|
|
|
|
aliceKBID := aliceKB["id"].(string)
|
|
|
|
|
|
|
|
|
|
// Alice CAN toggle her own KB
|
|
|
|
|
w = h.request("PUT", fmt.Sprintf("/api/v1/knowledge-bases/%s/discoverable", aliceKBID),
|
|
|
|
|
aliceToken, map[string]interface{}{"discoverable": false})
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("owner toggle discoverable: want 200, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bob CANNOT toggle Alice's KB (not owner, not admin)
|
|
|
|
|
w = h.request("PUT", fmt.Sprintf("/api/v1/knowledge-bases/%s/discoverable", aliceKBID),
|
|
|
|
|
bobToken, map[string]interface{}{"discoverable": true})
|
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
|
|
|
// loadAndAuthorize returns 404 for personal KBs the user doesn't own
|
|
|
|
|
t.Fatalf("non-owner toggle discoverable: want 404, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Admin creates a global KB
|
|
|
|
|
w = h.request("POST", "/api/v1/knowledge-bases", adminToken, map[string]interface{}{
|
|
|
|
|
"name": "Global KB", "scope": "global",
|
|
|
|
|
})
|
|
|
|
|
if w.Code != http.StatusCreated {
|
|
|
|
|
t.Fatalf("admin create global KB: want 201, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
var globalKB map[string]interface{}
|
|
|
|
|
decode(w, &globalKB)
|
|
|
|
|
globalKBID := globalKB["id"].(string)
|
|
|
|
|
|
|
|
|
|
// Alice CANNOT toggle a global KB she doesn't own
|
|
|
|
|
w = h.request("PUT", fmt.Sprintf("/api/v1/knowledge-bases/%s/discoverable", globalKBID),
|
|
|
|
|
aliceToken, map[string]interface{}{"discoverable": false})
|
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
|
|
|
t.Fatalf("non-admin toggle global discoverable: want 403, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Admin CAN toggle the global KB
|
|
|
|
|
w = h.request("PUT", fmt.Sprintf("/api/v1/knowledge-bases/%s/discoverable", globalKBID),
|
|
|
|
|
adminToken, map[string]interface{}{"discoverable": false})
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("admin toggle global discoverable: want 200, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Admin CANNOT access Alice's personal KB (loadAndAuthorize returns 404
|
|
|
|
|
// for personal KBs not owned by the caller — consistent with Get/Update/Delete)
|
|
|
|
|
w = h.request("PUT", fmt.Sprintf("/api/v1/knowledge-bases/%s/discoverable", aliceKBID),
|
|
|
|
|
adminToken, map[string]interface{}{"discoverable": true})
|
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
|
|
|
t.Fatalf("admin toggle alice personal KB: want 404 (not owner), got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Non-existent KB returns 404
|
|
|
|
|
w = h.request("PUT", fmt.Sprintf("/api/v1/knowledge-bases/%s/discoverable", uuid.NewString()),
|
|
|
|
|
adminToken, map[string]interface{}{"discoverable": false})
|
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
|
|
|
t.Fatalf("toggle nonexistent KB: want 404, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════
|
|
|
|
|
// Messages + Treepath tests (SQLite compat)
|
|
|
|
|
// ═══════════════════════════════════════════════
|
|
|
|
|
|