Files
wehub-resource-sync 6d978fe483
Build / Frontend Build (push) Has been cancelled
Build / Backend Build (push) Has been cancelled
Build / Docker Build (push) Has been cancelled
Build / Kubernetes Deploy Smoke Test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:24 +08:00

179 lines
5.8 KiB
Go

package services
import (
"context"
"testing"
"clawreef/internal/models"
)
// stubInstanceVisibilityRepo is a minimal InstanceRepository used by the
// visibility tests below. It only implements the read paths exercised by
// GetByUserID / GetAllInstances; every other method panics so that any
// accidental call surfaces as a test failure instead of a silent stub.
type stubInstanceVisibilityRepo struct {
all []models.Instance
}
func (r *stubInstanceVisibilityRepo) byUser(userID int) []models.Instance {
out := make([]models.Instance, 0, len(r.all))
for _, inst := range r.all {
if inst.UserID == userID {
out = append(out, inst)
}
}
return out
}
func (r *stubInstanceVisibilityRepo) Create(*models.Instance) error { panic("not used") }
func (r *stubInstanceVisibilityRepo) GetByID(int) (*models.Instance, error) {
panic("not used")
}
func (r *stubInstanceVisibilityRepo) GetByAccessToken(string) (*models.Instance, error) {
panic("not used")
}
func (r *stubInstanceVisibilityRepo) GetByAgentBootstrapToken(string) (*models.Instance, error) {
panic("not used")
}
func (r *stubInstanceVisibilityRepo) GetAll(offset, limit int) ([]models.Instance, error) {
return paginate(r.all, offset, limit), nil
}
func (r *stubInstanceVisibilityRepo) CountAll() (int, error) { return len(r.all), nil }
func (r *stubInstanceVisibilityRepo) GetByUserID(userID, offset, limit int) ([]models.Instance, error) {
return paginate(r.byUser(userID), offset, limit), nil
}
func (r *stubInstanceVisibilityRepo) CountByUserID(userID int) (int, error) {
return len(r.byUser(userID)), nil
}
func (r *stubInstanceVisibilityRepo) CountActiveByMode(context.Context, string) (int, error) {
return 0, nil
}
func (r *stubInstanceVisibilityRepo) ExistsByUserIDAndName(int, string) (bool, error) {
return false, nil
}
func (r *stubInstanceVisibilityRepo) GetAllRunning() ([]models.Instance, error) {
return nil, nil
}
func (r *stubInstanceVisibilityRepo) GetV2DesiredRunning(context.Context, int) ([]models.Instance, error) {
panic("not used")
}
func (r *stubInstanceVisibilityRepo) GetV2Creating(context.Context, int) ([]models.Instance, error) {
panic("not used")
}
func (r *stubInstanceVisibilityRepo) UpdateRuntimeState(context.Context, int, string, int, *string) error {
panic("not used")
}
func (r *stubInstanceVisibilityRepo) SetWorkspacePath(context.Context, int, string) error {
panic("not used")
}
func (r *stubInstanceVisibilityRepo) UpdateWorkspaceUsage(context.Context, int, int64) error {
panic("not used")
}
func (r *stubInstanceVisibilityRepo) Update(*models.Instance) error { panic("not used") }
func (r *stubInstanceVisibilityRepo) Delete(int) error { panic("not used") }
func paginate(items []models.Instance, offset, limit int) []models.Instance {
if offset >= len(items) {
return []models.Instance{}
}
end := offset + limit
if end > len(items) {
end = len(items)
}
out := make([]models.Instance, end-offset)
copy(out, items[offset:end])
return out
}
// TestGetByUserIDFiltersByCaller locks in the workspace-view contract: the
// caller-scoped listing must return only the caller's own instances,
// regardless of any role the caller may hold elsewhere. Regression guard
// for the admin-cross-user-leakage bug in which role=admin widened this
// endpoint to every user's instances.
func TestGetByUserIDFiltersByCaller(t *testing.T) {
t.Parallel()
repo := &stubInstanceVisibilityRepo{
all: []models.Instance{
{ID: 1, UserID: 10, Name: "alice-1"},
{ID: 2, UserID: 10, Name: "alice-2"},
{ID: 3, UserID: 20, Name: "bob-1"},
},
}
svc := &instanceService{instanceRepo: repo}
instances, total, err := svc.GetByUserID(10, 0, 50)
if err != nil {
t.Fatalf("GetByUserID returned error: %v", err)
}
if total != 2 {
t.Fatalf("expected total=2 for alice, got %d", total)
}
if len(instances) != 2 || instances[0].Name != "alice-1" || instances[1].Name != "alice-2" {
t.Fatalf("expected alice's instances only, got %+v", instances)
}
instances, total, err = svc.GetByUserID(20, 0, 50)
if err != nil {
t.Fatalf("GetByUserID returned error: %v", err)
}
if total != 1 {
t.Fatalf("expected total=1 for bob, got %d", total)
}
if len(instances) != 1 || instances[0].Name != "bob-1" {
t.Fatalf("expected bob's instance only, got %+v", instances)
}
// A user with no instances sees an empty list, never someone else's.
instances, total, err = svc.GetByUserID(99, 0, 50)
if err != nil {
t.Fatalf("GetByUserID returned error: %v", err)
}
if total != 0 || len(instances) != 0 {
t.Fatalf("expected empty listing for user 99, got total=%d instances=%+v", total, instances)
}
}
// TestGetAllInstancesReturnsEveryUser verifies the admin-console surface:
// cross-user listing, with pagination, independent of caller identity (the
// admin middleware gates reachability at the route layer, not here).
func TestGetAllInstancesReturnsEveryUser(t *testing.T) {
t.Parallel()
repo := &stubInstanceVisibilityRepo{
all: []models.Instance{
{ID: 1, UserID: 10, Name: "alice-1"},
{ID: 2, UserID: 10, Name: "alice-2"},
{ID: 3, UserID: 20, Name: "bob-1"},
},
}
svc := &instanceService{instanceRepo: repo}
instances, total, err := svc.GetAllInstances(0, 50)
if err != nil {
t.Fatalf("GetAllInstances returned error: %v", err)
}
if total != 3 {
t.Fatalf("expected total=3 across users, got %d", total)
}
if len(instances) != 3 {
t.Fatalf("expected 3 instances, got %d", len(instances))
}
// Pagination still respected.
page1, _, err := svc.GetAllInstances(0, 2)
if err != nil {
t.Fatalf("GetAllInstances page1 error: %v", err)
}
if len(page1) != 2 {
t.Fatalf("expected page1 size=2, got %d", len(page1))
}
page2, _, err := svc.GetAllInstances(2, 2)
if err != nil {
t.Fatalf("GetAllInstances page2 error: %v", err)
}
if len(page2) != 1 || page2[0].Name != "bob-1" {
t.Fatalf("expected page2=[bob-1], got %+v", page2)
}
}