Files
xinmi/internal/application/service/knowledge_owner_chain_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

174 lines
6.0 KiB
Go

package service
import (
"context"
stderrors "errors"
"testing"
"github.com/Tencent/WeKnora/internal/application/repository"
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/types/interfaces"
)
// PR 5 (#1303) — service-layer chain helper for the per-KB ownership
// lookups. The handler-level adapters in internal/handler/rbac_lookups.go
// translate this method's repository sentinels into
// middleware.ErrResourceNotFound; this file pins the contract on the
// service side so a future refactor can't quietly drop a sentinel.
// stubKnowledgeRepoForChain implements just GetKnowledgeByID; everything
// else panics so a test that reaches outside the contract fails loudly.
type stubKnowledgeRepoForChain struct {
interfaces.KnowledgeRepository
getByID func(ctx context.Context, tenantID uint64, id string) (*types.Knowledge, error)
}
func (s *stubKnowledgeRepoForChain) GetKnowledgeByID(
ctx context.Context, tenantID uint64, id string,
) (*types.Knowledge, error) {
return s.getByID(ctx, tenantID, id)
}
// stubKBServiceForChain mirrors the handler-side stub but lives in the
// service test package — embedding the interface keeps untouched
// methods nil-panicky on purpose.
type stubKBServiceForChain struct {
interfaces.KnowledgeBaseService
getByID func(ctx context.Context, id string) (*types.KnowledgeBase, error)
}
func (s *stubKBServiceForChain) GetKnowledgeBaseByID(
ctx context.Context, id string,
) (*types.KnowledgeBase, error) {
return s.getByID(ctx, id)
}
func newKnowledgeServiceForChain(
repo interfaces.KnowledgeRepository, kb interfaces.KnowledgeBaseService,
) *knowledgeService {
return &knowledgeService{repo: repo, kbService: kb}
}
func chainCtx(tenantID uint64) context.Context {
return context.WithValue(context.Background(), types.TenantIDContextKey, tenantID)
}
func TestGetOwningKBCreatorID_KnowledgeNotFound(t *testing.T) {
// Repository sentinel passes through unchanged so the handler-level
// translator can map it to middleware.ErrResourceNotFound.
s := newKnowledgeServiceForChain(
&stubKnowledgeRepoForChain{
getByID: func(_ context.Context, _ uint64, _ string) (*types.Knowledge, error) {
return nil, repository.ErrKnowledgeNotFound
},
},
&stubKBServiceForChain{
getByID: func(_ context.Context, _ string) (*types.KnowledgeBase, error) {
t.Fatalf("KB service must not be called when knowledge lookup fails")
return nil, nil
},
},
)
_, err := s.GetOwningKBCreatorID(chainCtx(1), "kn-missing")
if !stderrors.Is(err, repository.ErrKnowledgeNotFound) {
t.Fatalf("expected ErrKnowledgeNotFound, got %v", err)
}
}
func TestGetOwningKBCreatorID_KBNotFound(t *testing.T) {
// Knowledge resolves but its KB has been deleted (or was never
// created — defence-in-depth). The KB sentinel must surface so the
// handler can render 404 rather than 503/500.
s := newKnowledgeServiceForChain(
&stubKnowledgeRepoForChain{
getByID: func(_ context.Context, _ uint64, _ string) (*types.Knowledge, error) {
return &types.Knowledge{ID: "kn-1", KnowledgeBaseID: "kb-missing"}, nil
},
},
&stubKBServiceForChain{
getByID: func(_ context.Context, _ string) (*types.KnowledgeBase, error) {
return nil, repository.ErrKnowledgeBaseNotFound
},
},
)
_, err := s.GetOwningKBCreatorID(chainCtx(1), "kn-1")
if !stderrors.Is(err, repository.ErrKnowledgeBaseNotFound) {
t.Fatalf("expected ErrKnowledgeBaseNotFound, got %v", err)
}
}
func TestGetOwningKBCreatorID_KBNilWithoutErrSurfacesAsNotFound(t *testing.T) {
// Some KB-service implementations return (nil, nil) for "not found"
// instead of a sentinel error. The chain must treat that as
// ErrKnowledgeBaseNotFound rather than dereferencing nil.
s := newKnowledgeServiceForChain(
&stubKnowledgeRepoForChain{
getByID: func(_ context.Context, _ uint64, _ string) (*types.Knowledge, error) {
return &types.Knowledge{ID: "kn-1", KnowledgeBaseID: "kb-1"}, nil
},
},
&stubKBServiceForChain{
getByID: func(_ context.Context, _ string) (*types.KnowledgeBase, error) {
return nil, nil
},
},
)
_, err := s.GetOwningKBCreatorID(chainCtx(1), "kn-1")
if !stderrors.Is(err, repository.ErrKnowledgeBaseNotFound) {
t.Fatalf("nil KB without error must surface as ErrKnowledgeBaseNotFound, got %v", err)
}
}
func TestGetOwningKBCreatorID_HappyPathReturnsKBCreator(t *testing.T) {
s := newKnowledgeServiceForChain(
&stubKnowledgeRepoForChain{
getByID: func(_ context.Context, tenantID uint64, _ string) (*types.Knowledge, error) {
if tenantID != 1 {
t.Fatalf("expected tenant context to flow into repo (got %d)", tenantID)
}
return &types.Knowledge{ID: "kn-1", KnowledgeBaseID: "kb-1"}, nil
},
},
&stubKBServiceForChain{
getByID: func(_ context.Context, id string) (*types.KnowledgeBase, error) {
if id != "kb-1" {
t.Fatalf("KB lookup must use the knowledge's KnowledgeBaseID, got %q", id)
}
return &types.KnowledgeBase{ID: "kb-1", TenantID: 1, CreatorID: "u-creator"}, nil
},
},
)
creator, err := s.GetOwningKBCreatorID(chainCtx(1), "kn-1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if creator != "u-creator" {
t.Fatalf("expected creator=u-creator, got %q", creator)
}
}
func TestGetOwningKBCreatorID_LegacyKBHasEmptyCreatorID(t *testing.T) {
// Legacy KBs created before PR 1 have CreatorID == ""; the chain
// helper must surface that empty string unchanged so the
// middleware's "" -> tenant-owned branch fires (Admin+ only).
s := newKnowledgeServiceForChain(
&stubKnowledgeRepoForChain{
getByID: func(_ context.Context, _ uint64, _ string) (*types.Knowledge, error) {
return &types.Knowledge{ID: "kn-1", KnowledgeBaseID: "kb-legacy"}, nil
},
},
&stubKBServiceForChain{
getByID: func(_ context.Context, _ string) (*types.KnowledgeBase, error) {
return &types.KnowledgeBase{ID: "kb-legacy", TenantID: 1, CreatorID: ""}, nil
},
},
)
creator, err := s.GetOwningKBCreatorID(chainCtx(1), "kn-1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if creator != "" {
t.Fatalf("legacy KB must surface empty creator id, got %q", creator)
}
}