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,173 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user