Files
paliad/internal/services/cansee_test.go
m 460736ad1e refactor(t-paliad-092): rename Go module path patholo → paliad
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.
2026-04-30 16:46:31 +02:00

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)
}
}
})
})
}