Changeset 0.14.0 (#67)
This commit is contained in:
@@ -135,6 +135,20 @@ func setupHarness(t *testing.T) *testHarness {
|
||||
protected.GET("/channels/:id", channels.GetChannel)
|
||||
protected.DELETE("/channels/:id", channels.DeleteChannel)
|
||||
|
||||
// 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.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.GET("/knowledge-bases/:id/documents", kbH.ListDocuments)
|
||||
protected.POST("/knowledge-bases/:id/search", kbH.SearchKB)
|
||||
protected.POST("/knowledge-bases/:id/rebuild", kbH.RebuildKB)
|
||||
protected.GET("/channels/:id/knowledge-bases", kbH.GetChannelKBs)
|
||||
protected.PUT("/channels/:id/knowledge-bases", kbH.SetChannelKBs)
|
||||
|
||||
// Attachments (nil storage = upload returns 503, but metadata works)
|
||||
attachH := NewAttachmentHandler(stores, nil, nil)
|
||||
protected.POST("/channels/:id/attachments", attachH.Upload)
|
||||
@@ -2230,3 +2244,202 @@ func TestIntegration_Roles_GetSingleRole(t *testing.T) {
|
||||
t.Fatalf("get seeded-but-empty role: want 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ── KB CRUD ─────────────────────────────────────
|
||||
|
||||
func TestIntegration_KnowledgeBaseCRUD(t *testing.T) {
|
||||
h := setupHarness(t)
|
||||
_, adminToken := h.createAdminUser("admin", "admin@test.com")
|
||||
|
||||
// Create personal KB
|
||||
w := h.request("POST", "/api/v1/knowledge-bases", adminToken, map[string]interface{}{
|
||||
"name": "Test KB", "description": "A test knowledge base",
|
||||
})
|
||||
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)
|
||||
|
||||
if kb["name"] != "Test KB" {
|
||||
t.Fatalf("create KB: name mismatch: got %s", kb["name"])
|
||||
}
|
||||
if kb["scope"] != "personal" {
|
||||
t.Fatalf("create KB: scope should default to personal: got %s", kb["scope"])
|
||||
}
|
||||
|
||||
// List KBs
|
||||
w = h.request("GET", "/api/v1/knowledge-bases", adminToken, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("list KBs: want 200, got %d", w.Code)
|
||||
}
|
||||
var kbList map[string]interface{}
|
||||
decode(w, &kbList)
|
||||
items := kbList["data"].([]interface{})
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("list KBs: expected 1, got %d", len(items))
|
||||
}
|
||||
|
||||
// Get single KB
|
||||
w = h.request("GET", fmt.Sprintf("/api/v1/knowledge-bases/%s", kbID), adminToken, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("get KB: want 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
// Update KB
|
||||
w = h.request("PUT", fmt.Sprintf("/api/v1/knowledge-bases/%s", kbID), adminToken,
|
||||
map[string]interface{}{"name": "Updated KB"})
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("update KB: want 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// Verify update
|
||||
w = h.request("GET", fmt.Sprintf("/api/v1/knowledge-bases/%s", kbID), adminToken, nil)
|
||||
var updated map[string]interface{}
|
||||
decode(w, &updated)
|
||||
if updated["name"] != "Updated KB" {
|
||||
t.Fatalf("update KB: name not updated, got %s", updated["name"])
|
||||
}
|
||||
|
||||
// Upload document — should 503 (no object store in test harness)
|
||||
w = h.request("POST", fmt.Sprintf("/api/v1/knowledge-bases/%s/documents", kbID), adminToken, nil)
|
||||
if w.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("upload doc without objstore: want 503, got %d", w.Code)
|
||||
}
|
||||
|
||||
// List documents (empty)
|
||||
w = h.request("GET", fmt.Sprintf("/api/v1/knowledge-bases/%s/documents", kbID), adminToken, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("list docs: want 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
// Rebuild — should 503 (no ingester in test harness)
|
||||
w = h.request("POST", fmt.Sprintf("/api/v1/knowledge-bases/%s/rebuild", kbID), adminToken, nil)
|
||||
if w.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("rebuild without ingester: want 503, got %d", w.Code)
|
||||
}
|
||||
|
||||
// Delete KB
|
||||
w = h.request("DELETE", fmt.Sprintf("/api/v1/knowledge-bases/%s", kbID), adminToken, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("delete KB: want 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
// Verify deleted
|
||||
w = h.request("GET", fmt.Sprintf("/api/v1/knowledge-bases/%s", kbID), adminToken, nil)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("get deleted KB: want 404, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ── KB Cross-User Isolation ─────────────────────
|
||||
|
||||
func TestIntegration_KBCrossUserIsolation(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'")
|
||||
|
||||
_, userToken := h.registerUser("alice", "alice@test.com", "password123")
|
||||
|
||||
// Admin creates personal KB
|
||||
w := h.request("POST", "/api/v1/knowledge-bases", adminToken, map[string]interface{}{
|
||||
"name": "Admin Private KB",
|
||||
})
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("admin create KB: want 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var adminKB map[string]interface{}
|
||||
decode(w, &adminKB)
|
||||
adminKBID := adminKB["id"].(string)
|
||||
|
||||
// Alice should NOT see admin's personal KB
|
||||
w = h.request("GET", "/api/v1/knowledge-bases", userToken, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("alice list KBs: want 200, got %d", w.Code)
|
||||
}
|
||||
var aliceList map[string]interface{}
|
||||
decode(w, &aliceList)
|
||||
items := aliceList["data"].([]interface{})
|
||||
if len(items) != 0 {
|
||||
t.Fatalf("alice should see 0 KBs, got %d", len(items))
|
||||
}
|
||||
|
||||
// Alice should NOT be able to access admin's KB directly
|
||||
w = h.request("GET", fmt.Sprintf("/api/v1/knowledge-bases/%s", adminKBID), userToken, nil)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("alice access admin KB: want 404 (hidden), got %d", w.Code)
|
||||
}
|
||||
|
||||
// Alice should NOT be able to delete admin's KB
|
||||
w = h.request("DELETE", fmt.Sprintf("/api/v1/knowledge-bases/%s", adminKBID), userToken, nil)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("alice delete admin KB: want 404 (hidden), got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Channel KB Linking ──────────────────────────
|
||||
|
||||
func TestIntegration_ChannelKBLinking(t *testing.T) {
|
||||
h := setupHarness(t)
|
||||
_, adminToken := h.createAdminUser("admin", "admin@test.com")
|
||||
|
||||
// Create a channel
|
||||
w := h.request("POST", "/api/v1/channels", adminToken, map[string]interface{}{
|
||||
"title": "Test Channel",
|
||||
})
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("create channel: want 201, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var ch map[string]interface{}
|
||||
decode(w, &ch)
|
||||
channelID := ch["id"].(string)
|
||||
|
||||
// Create a KB
|
||||
w = h.request("POST", "/api/v1/knowledge-bases", adminToken, map[string]interface{}{
|
||||
"name": "Channel 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)
|
||||
|
||||
// Link KB to channel
|
||||
w = h.request("PUT", fmt.Sprintf("/api/v1/channels/%s/knowledge-bases", channelID), adminToken,
|
||||
map[string]interface{}{"kb_ids": []string{kbID}})
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("link KB to channel: want 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// Get channel KBs
|
||||
w = h.request("GET", fmt.Sprintf("/api/v1/channels/%s/knowledge-bases", channelID), adminToken, nil)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("get channel KBs: want 200, got %d", w.Code)
|
||||
}
|
||||
var linked map[string]interface{}
|
||||
decode(w, &linked)
|
||||
data := linked["data"].([]interface{})
|
||||
if len(data) != 1 {
|
||||
t.Fatalf("channel should have 1 linked KB, got %d", len(data))
|
||||
}
|
||||
|
||||
// Unlink (set empty)
|
||||
w = h.request("PUT", fmt.Sprintf("/api/v1/channels/%s/knowledge-bases", channelID), adminToken,
|
||||
map[string]interface{}{"kb_ids": []string{}})
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("unlink KBs: want 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// Verify empty
|
||||
w = h.request("GET", fmt.Sprintf("/api/v1/channels/%s/knowledge-bases", channelID), adminToken, nil)
|
||||
var empty map[string]interface{}
|
||||
decode(w, &empty)
|
||||
emptyData := empty["data"].([]interface{})
|
||||
if len(emptyData) != 0 {
|
||||
t.Fatalf("channel should have 0 linked KBs after unlink, got %d", len(emptyData))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user