//go:build integration package integrationtest import ( "context" "testing" "github.com/ultisuite/ulti-backend/internal/auth" "github.com/ultisuite/ulti-backend/internal/permission" "github.com/ultisuite/ulti-backend/internal/users" ) // RegularUser returns claims for a standard suite user. func RegularUser(externalID string) *auth.Claims { return &auth.Claims{ Sub: externalID, Email: externalID + "@test.ultimail.local", Name: "Test User", Groups: []string{string(permission.RoleUser)}, } } // AdminUser returns claims for a platform admin. func AdminUser(externalID string) *auth.Claims { return &auth.Claims{ Sub: externalID, Email: externalID + "@admin.ultimail.local", Name: "Test Admin", Groups: []string{string(permission.RoleAdmin), "admin:write"}, } } // EnsureUser provisions a user row directly (optional shortcut). func EnsureUser(ctx context.Context, h *Harness, claims *auth.Claims) (string, error) { return users.EnsureUser(ctx, h.Pool, claims) } // DisableUser marks a user as disabled in the database. func DisableUser(ctx context.Context, h *Harness, externalID string) error { _, err := h.Pool.Exec(ctx, ` UPDATE users SET status = 'disabled', updated_at = NOW() WHERE external_id = $1 `, externalID) return err } // RequireUserClient creates a regular user client, failing the test on error. func RequireUserClient(t *testing.T, h *Harness) (*Client, *auth.Claims) { t.Helper() claims := RegularUser(NewExternalID("user")) client, err := h.Client(claims) if err != nil { t.Fatalf("client: %v", err) } return client, claims } // RequireAdminClient creates an admin user client. func RequireAdminClient(t *testing.T, h *Harness) (*Client, *auth.Claims) { t.Helper() claims := AdminUser(NewExternalID("admin")) client, err := h.Client(claims) if err != nil { t.Fatalf("admin client: %v", err) } return client, claims } // SkipUnlessNextcloud skips the test when Nextcloud integration is not enabled. func SkipUnlessNextcloud(t *testing.T) { t.Helper() env := LoadEnv() if !env.Nextcloud || env.NextcloudURL == "" { t.Skip("skipped: set ULTI_TEST_NEXTCLOUD=1 and ULTI_TEST_NEXTCLOUD_URL") } } // SkipUnlessImmich skips when Immich tests are not enabled. func SkipUnlessImmich(t *testing.T) { t.Helper() env := LoadEnv() if !env.Immich || env.ImmichURL == "" { t.Skip("skipped: set ULTI_TEST_IMMICH=1 and ULTI_TEST_IMMICH_URL") } } // SkipUnlessJitsi skips when Jitsi tests are not enabled. func SkipUnlessJitsi(t *testing.T) { t.Helper() if !LoadEnv().Jitsi { t.Skip("skipped: set ULTI_TEST_JITSI=1") } } // AssertUserProvisioned verifies the user exists after an API call. func AssertUserProvisioned(t *testing.T, h *Harness, externalID string) { t.Helper() var id string err := h.Pool.QueryRow(context.Background(), ` SELECT id FROM users WHERE external_id = $1 `, externalID).Scan(&id) if err != nil { t.Fatalf("user %q not provisioned: %v", externalID, err) } if id == "" { t.Fatalf("empty user id for %q", externalID) } } func AssertErrorCode(t *testing.T, resp *Response, wantStatus int, wantCode string) { t.Helper() if resp.Status != wantStatus { t.Fatalf("status = %d, want %d; body = %s", resp.Status, wantStatus, string(resp.Body)) } var body struct { Code string `json:"code"` } if err := resp.JSON(&body); err != nil { t.Fatalf("decode error body: %v", err) } if wantCode != "" && body.Code != wantCode { t.Fatalf("error code = %q, want %q; body = %s", body.Code, wantCode, string(resp.Body)) } } func FailIf(err error, t *testing.T, msg string) { t.Helper() if err != nil { t.Fatalf("%s: %v", msg, err) } } func FailUnlessStatus(t *testing.T, resp *Response, want int) { t.Helper() if err := resp.MustStatus(want); err != nil { t.Fatal(err) } } // DecodeJSON decodes response body or fails the test. func DecodeJSON(t *testing.T, resp *Response, v any) { t.Helper() if err := resp.JSON(v); err != nil { t.Fatalf("decode json: %v; body = %s", err, string(resp.Body)) } } // FormatSub returns a readable subject for error messages. func FormatSub(sub string) string { if sub == "" { return "" } return sub }