F-6 from t-paliad-074 architecture audit. The Gitea repo was renamed m/patholo → mAi/paliad → m/paliad, but go.mod still declared `mgit.msbls.de/m/patholo` and every internal import echoed the pre-rebrand name. Sweep: - go.mod: module path → mgit.msbls.de/m/paliad - All *.go files: imports rewritten via sed - README.md, docs/design-kanzlai-integration.md: mAi/paliad → m/paliad - Frontend issue-reference comments (mAi/paliad#N → m/paliad#N) in i18n.ts, theme.ts, sidebar.ts, app.ts, Sidebar.tsx, PWAHead.tsx, global.css Verified: go build/vet/test ./... clean, bun run build clean, no remaining mgit.msbls.de/m/patholo or mAi/paliad references outside docs that intentionally describe the rename history.
233 lines
7.7 KiB
Go
233 lines
7.7 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
_ "github.com/lib/pq"
|
|
|
|
"mgit.msbls.de/m/paliad/internal/db"
|
|
)
|
|
|
|
// TestCanSee covers the two new visibility-only gates added for t-paliad-091:
|
|
//
|
|
// - ProjectService.CanSee
|
|
// - AppointmentService.CanSee
|
|
//
|
|
// Both replace cross-service GetByID-for-visibility calls in NoteService
|
|
// with a single EXISTS round-trip. The acceptance criteria from the task
|
|
// spec are: admin sees everything; team-member sees their team's;
|
|
// non-member sees nothing.
|
|
//
|
|
// Skipped when TEST_DATABASE_URL is unset, mirroring the other live-DB tests.
|
|
func TestCanSee(t *testing.T) {
|
|
url := os.Getenv("TEST_DATABASE_URL")
|
|
if url == "" {
|
|
t.Skip("TEST_DATABASE_URL not set — skipping live DB test")
|
|
}
|
|
if err := db.ApplyMigrations(url); err != nil {
|
|
t.Fatalf("apply migrations: %v", err)
|
|
}
|
|
pool, err := sqlx.Connect("postgres", url)
|
|
if err != nil {
|
|
t.Fatalf("connect: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
adminID := uuid.New()
|
|
memberID := uuid.New()
|
|
outsiderID := uuid.New()
|
|
projectID := uuid.New()
|
|
personalAppointmentID := uuid.New()
|
|
projectAppointmentID := uuid.New()
|
|
|
|
cleanup := func() {
|
|
pool.ExecContext(ctx, `DELETE FROM paliad.appointments WHERE id IN ($1, $2)`, personalAppointmentID, projectAppointmentID)
|
|
pool.ExecContext(ctx, `DELETE FROM paliad.project_teams WHERE project_id = $1`, projectID)
|
|
pool.ExecContext(ctx, `DELETE FROM paliad.projects WHERE id = $1`, projectID)
|
|
pool.ExecContext(ctx, `DELETE FROM paliad.users WHERE id IN ($1, $2, $3)`, adminID, memberID, outsiderID)
|
|
pool.ExecContext(ctx, `DELETE FROM auth.users WHERE id IN ($1, $2, $3)`, adminID, memberID, outsiderID)
|
|
}
|
|
cleanup()
|
|
defer cleanup()
|
|
|
|
if _, err := pool.ExecContext(ctx,
|
|
`INSERT INTO auth.users (id, email)
|
|
VALUES ($1, $2), ($3, $4), ($5, $6)`,
|
|
adminID, "cansee-admin@hlc.com",
|
|
memberID, "cansee-member@hlc.com",
|
|
outsiderID, "cansee-outsider@hlc.com"); err != nil {
|
|
t.Fatalf("seed auth.users: %v", err)
|
|
}
|
|
if _, err := pool.ExecContext(ctx,
|
|
`INSERT INTO paliad.users (id, email, display_name, office, global_role, lang)
|
|
VALUES ($1, $2, 'CanSee Admin', 'munich', 'global_admin', 'de'),
|
|
($3, $4, 'CanSee Member', 'munich', 'standard', 'de'),
|
|
($5, $6, 'CanSee Outsider', 'munich', 'standard', 'de')`,
|
|
adminID, "cansee-admin@hlc.com",
|
|
memberID, "cansee-member@hlc.com",
|
|
outsiderID, "cansee-outsider@hlc.com"); err != nil {
|
|
t.Fatalf("seed paliad.users: %v", err)
|
|
}
|
|
|
|
// Project owned by adminID. memberID joins the project team; outsiderID
|
|
// stays out. project_teams intentionally has no admin row so the
|
|
// global_admin shortcut is what makes adminID see it.
|
|
if _, err := pool.ExecContext(ctx,
|
|
`INSERT INTO paliad.projects (id, type, path, title, reference, status, created_by)
|
|
VALUES ($1, 'project', $1::text, 'CanSee Project', '2026/9991', 'active', $2)`,
|
|
projectID, adminID); err != nil {
|
|
t.Fatalf("seed paliad.projects: %v", err)
|
|
}
|
|
if _, err := pool.ExecContext(ctx,
|
|
`INSERT INTO paliad.project_teams (project_id, user_id, role)
|
|
VALUES ($1, $2, 'associate')`,
|
|
projectID, memberID); err != nil {
|
|
t.Fatalf("seed paliad.project_teams: %v", err)
|
|
}
|
|
|
|
// Personal appointment created by memberID — only memberID may see it
|
|
// (admin's global_admin role does NOT cover personal appointments by
|
|
// design, since they have no project anchor).
|
|
startAt := time.Now().UTC().Add(48 * time.Hour)
|
|
if _, err := pool.ExecContext(ctx,
|
|
`INSERT INTO paliad.appointments
|
|
(id, project_id, title, start_at, appointment_type, created_by)
|
|
VALUES ($1, NULL, 'Personal CanSee', $2, 'meeting', $3)`,
|
|
personalAppointmentID, startAt, memberID); err != nil {
|
|
t.Fatalf("seed personal appointment: %v", err)
|
|
}
|
|
// Project-anchored appointment — visibility flows through the project.
|
|
if _, err := pool.ExecContext(ctx,
|
|
`INSERT INTO paliad.appointments
|
|
(id, project_id, title, start_at, appointment_type, created_by)
|
|
VALUES ($1, $2, 'Project CanSee', $3, 'meeting', $4)`,
|
|
projectAppointmentID, projectID, startAt, adminID); err != nil {
|
|
t.Fatalf("seed project appointment: %v", err)
|
|
}
|
|
|
|
users := NewUserService(pool)
|
|
projects := NewProjectService(pool, users)
|
|
appointments := NewAppointmentService(pool, projects)
|
|
|
|
t.Run("ProjectService.CanSee", func(t *testing.T) {
|
|
t.Run("global_admin sees project even without team membership", func(t *testing.T) {
|
|
got, err := projects.CanSee(ctx, adminID, projectID)
|
|
if err != nil {
|
|
t.Fatalf("CanSee admin: %v", err)
|
|
}
|
|
if !got {
|
|
t.Errorf("CanSee admin = false, want true (global_admin shortcut)")
|
|
}
|
|
})
|
|
t.Run("team member sees project", func(t *testing.T) {
|
|
got, err := projects.CanSee(ctx, memberID, projectID)
|
|
if err != nil {
|
|
t.Fatalf("CanSee member: %v", err)
|
|
}
|
|
if !got {
|
|
t.Errorf("CanSee member = false, want true")
|
|
}
|
|
})
|
|
t.Run("non-member does not see project", func(t *testing.T) {
|
|
got, err := projects.CanSee(ctx, outsiderID, projectID)
|
|
if err != nil {
|
|
t.Fatalf("CanSee outsider: %v", err)
|
|
}
|
|
if got {
|
|
t.Errorf("CanSee outsider = true, want false")
|
|
}
|
|
})
|
|
t.Run("missing project is invisible to all", func(t *testing.T) {
|
|
missing := uuid.New()
|
|
for _, who := range []struct {
|
|
name string
|
|
id uuid.UUID
|
|
}{{"admin", adminID}, {"member", memberID}, {"outsider", outsiderID}} {
|
|
got, err := projects.CanSee(ctx, who.id, missing)
|
|
if err != nil {
|
|
t.Fatalf("CanSee %s missing: %v", who.name, err)
|
|
}
|
|
if got {
|
|
t.Errorf("CanSee %s missing = true, want false", who.name)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
t.Run("AppointmentService.CanSee", func(t *testing.T) {
|
|
t.Run("project-anchored: global_admin sees", func(t *testing.T) {
|
|
got, err := appointments.CanSee(ctx, adminID, projectAppointmentID)
|
|
if err != nil {
|
|
t.Fatalf("CanSee admin: %v", err)
|
|
}
|
|
if !got {
|
|
t.Errorf("CanSee admin = false, want true")
|
|
}
|
|
})
|
|
t.Run("project-anchored: team member sees", func(t *testing.T) {
|
|
got, err := appointments.CanSee(ctx, memberID, projectAppointmentID)
|
|
if err != nil {
|
|
t.Fatalf("CanSee member: %v", err)
|
|
}
|
|
if !got {
|
|
t.Errorf("CanSee member = false, want true")
|
|
}
|
|
})
|
|
t.Run("project-anchored: non-member does not see", func(t *testing.T) {
|
|
got, err := appointments.CanSee(ctx, outsiderID, projectAppointmentID)
|
|
if err != nil {
|
|
t.Fatalf("CanSee outsider: %v", err)
|
|
}
|
|
if got {
|
|
t.Errorf("CanSee outsider = true, want false")
|
|
}
|
|
})
|
|
t.Run("personal: only the creator sees it", func(t *testing.T) {
|
|
got, err := appointments.CanSee(ctx, memberID, personalAppointmentID)
|
|
if err != nil {
|
|
t.Fatalf("CanSee member personal: %v", err)
|
|
}
|
|
if !got {
|
|
t.Errorf("CanSee member personal = false, want true (own appointment)")
|
|
}
|
|
// global_admin does NOT see another user's personal appointment.
|
|
got, err = appointments.CanSee(ctx, adminID, personalAppointmentID)
|
|
if err != nil {
|
|
t.Fatalf("CanSee admin personal: %v", err)
|
|
}
|
|
if got {
|
|
t.Errorf("CanSee admin personal = true, want false (personal appointments are private)")
|
|
}
|
|
got, err = appointments.CanSee(ctx, outsiderID, personalAppointmentID)
|
|
if err != nil {
|
|
t.Fatalf("CanSee outsider personal: %v", err)
|
|
}
|
|
if got {
|
|
t.Errorf("CanSee outsider personal = true, want false")
|
|
}
|
|
})
|
|
t.Run("missing appointment is invisible to all", func(t *testing.T) {
|
|
missing := uuid.New()
|
|
for _, who := range []struct {
|
|
name string
|
|
id uuid.UUID
|
|
}{{"admin", adminID}, {"member", memberID}, {"outsider", outsiderID}} {
|
|
got, err := appointments.CanSee(ctx, who.id, missing)
|
|
if err != nil {
|
|
t.Fatalf("CanSee %s missing: %v", who.name, err)
|
|
}
|
|
if got {
|
|
t.Errorf("CanSee %s missing = true, want false", who.name)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|