Files
xinmi/internal/router/router_knowledge_export_test.go
T
yawan f7fc992f57
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
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

141 lines
6.8 KiB
Go

package router
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/Tencent/WeKnora/internal/accesspolicy"
"github.com/Tencent/WeKnora/internal/config"
"github.com/Tencent/WeKnora/internal/handler"
"github.com/Tencent/WeKnora/internal/middleware"
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/types/interfaces"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
type exportKnowledgeService struct {
interfaces.KnowledgeService
knowledge *types.Knowledge
exported bool
}
func (s *exportKnowledgeService) GetKnowledgeByIDOnly(ctx context.Context, id string) (*types.Knowledge, error) {
return (&downloadKnowledgeLookup{knowledge: s.knowledge}).GetKnowledgeByIDOnly(ctx, id)
}
func (s *exportKnowledgeService) GetKnowledgeFile(context.Context, string) (io.ReadCloser, string, error) {
s.exported = true
return io.NopCloser(strings.NewReader("source document")), "source.txt", nil
}
func (s *exportKnowledgeService) ExportFAQEntries(context.Context, string) ([]byte, error) {
s.exported = true
return []byte("question,answer\nq,a\n"), nil
}
func (s *exportKnowledgeService) ExportFAQEntriesJSON(context.Context, string) ([]byte, error) {
s.exported = true
return []byte(`[{"question":"q","answer":"a"}]`), nil
}
type exportResponseRecorder struct{ *httptest.ResponseRecorder }
func (*exportResponseRecorder) CloseNotify() <-chan bool { return nil }
func TestKnowledgeExportPublicationAndExistingBoundaries(t *testing.T) {
gin.SetMode(gin.TestMode)
retrieveKey := &types.TenantAPIKeyScope{
KeyID: 1, Capabilities: types.StringArray{"retrieve"}, KnowledgeBaseIDs: types.StringArray{"kb-export"},
}
cases := []struct {
name string
role types.TenantRole
global bool
creator string
systemAdmin bool
native bool
foreign bool
grant bool
scope *types.TenantAPIKeyScope
want int
}{
{name: "global contributor", role: types.TenantRoleContributor, global: true, want: http.StatusForbidden},
{name: "global creator", role: types.TenantRoleContributor, global: true, creator: "requester", want: http.StatusForbidden},
{name: "global grant", role: types.TenantRoleContributor, global: true, grant: true, want: http.StatusForbidden},
{name: "global viewer", role: types.TenantRoleViewer, global: true, want: http.StatusForbidden},
{name: "global admin", role: types.TenantRoleAdmin, global: true, want: http.StatusOK},
{name: "global owner role", role: types.TenantRoleOwner, global: true, want: http.StatusOK},
{name: "global system admin", role: types.TenantRoleContributor, systemAdmin: true, global: true, want: http.StatusOK},
{name: "private creator", role: types.TenantRoleContributor, creator: "requester", want: http.StatusOK},
{name: "private granted nonowner", role: types.TenantRoleContributor, creator: "owner", grant: true, want: http.StatusForbidden},
{name: "private granted unknown creator", role: types.TenantRoleContributor, grant: true, want: http.StatusForbidden},
{name: "private granted admin", role: types.TenantRoleAdmin, creator: "owner", grant: true, want: http.StatusOK},
{name: "native without snapshot", role: types.TenantRoleContributor, native: true, want: http.StatusOK},
{name: "foreign private", role: types.TenantRoleContributor, foreign: true, want: http.StatusForbidden},
{name: "foreign admin", role: types.TenantRoleAdmin, foreign: true, global: true, want: http.StatusForbidden},
{name: "foreign system admin", role: types.TenantRoleContributor, systemAdmin: true, foreign: true, global: true, want: http.StatusForbidden},
{name: "scoped retrieve key", role: types.TenantRoleViewer, global: true, scope: retrieveKey, want: http.StatusOK},
{name: "full access key", role: types.TenantRoleViewer, global: true, scope: &types.TenantAPIKeyScope{KeyID: 2, FullAccess: true}, want: http.StatusOK},
{name: "key lacks retrieve", role: types.TenantRoleOwner, global: true, scope: &types.TenantAPIKeyScope{KeyID: 3, Capabilities: types.StringArray{"ingest"}}, want: http.StatusForbidden},
{name: "key outside KB scope", role: types.TenantRoleOwner, global: true, scope: &types.TenantAPIKeyScope{KeyID: 4, Capabilities: types.StringArray{"retrieve"}, KnowledgeBaseIDs: types.StringArray{"other-kb"}}, want: http.StatusForbidden},
{name: "foreign retrieve key", role: types.TenantRoleOwner, foreign: true, global: true, scope: retrieveKey, want: http.StatusForbidden},
}
for _, tc := range cases {
for _, endpoint := range []struct{ name, path string }{
{"document", "/api/v1/knowledge/document-export/download"},
{"FAQ CSV", "/api/v1/knowledge-bases/kb-export/faq/entries/export"},
{"FAQ JSON", "/api/v1/knowledge-bases/kb-export/faq/entries/export?format=json"},
} {
t.Run(tc.name+"/"+endpoint.name, func(t *testing.T) {
enabled := true
cfg := &config.Config{Tenant: &config.TenantConfig{EnableRBAC: &enabled}}
kb := &types.KnowledgeBase{ID: "kb-export", TenantID: 1, CreatorID: tc.creator}
if tc.foreign {
kb.TenantID = 2
}
svc := &exportKnowledgeService{knowledge: &types.Knowledge{ID: "document-export", KnowledgeBaseID: kb.ID, TenantID: kb.TenantID}}
guards := &rbacGuards{
cfg: cfg, knowledgeService: svc,
kbService: &stubWikiKBLookup{kbs: map[string]*types.KnowledgeBase{kb.ID: kb}},
apiKeyAuthorizer: middleware.NewAPIKeyRouteAuthorizer(),
}
r := gin.New()
r.Use(middleware.ErrorHandler())
r.Use(func(c *gin.Context) {
ctx := context.WithValue(c.Request.Context(), types.TenantIDContextKey, uint64(1))
ctx = context.WithValue(ctx, types.UserIDContextKey, "requester")
ctx = context.WithValue(ctx, types.TenantRoleContextKey, tc.role)
ctx = context.WithValue(ctx, types.SystemAdminContextKey, tc.systemAdmin)
if !tc.native {
ctx = accesspolicy.WithSnapshot(ctx, &accesspolicy.Snapshot{
Publications: map[string]bool{"knowledge:" + kb.ID: tc.global},
Grants: map[string]bool{"knowledge:" + kb.ID: tc.grant},
})
}
if tc.scope != nil {
ctx = types.WithTenantAPIKeyScope(ctx, *tc.scope)
}
c.Request = c.Request.WithContext(ctx)
c.Set(types.TenantIDContextKey.String(), uint64(1))
c.Set(types.UserIDContextKey.String(), "requester")
c.Next()
})
r.Use(guards.apiKeyAuthorizer.Middleware())
v1 := r.Group("/api/v1")
RegisterKnowledgeRoutes(v1, handler.NewKnowledgeHandler(cfg, svc, nil, nil, nil, nil, nil), guards)
RegisterFAQRoutes(v1, handler.NewFAQHandler(svc, nil), guards)
rec := &exportResponseRecorder{httptest.NewRecorder()}
r.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, endpoint.path, nil))
require.Equal(t, tc.want, rec.Code, "body=%s", rec.Body.String())
require.Equal(t, tc.want == http.StatusOK, svc.exported, "export must not reach the service after rejection")
})
}
}
}