- Introduced a new `.env.test.example` file for integration test configuration. - Added a `Makefile` to streamline test commands for unit and integration tests. - Implemented an integration testing harness with support for PostgreSQL, MinIO, and Redis using testcontainers. - Created a suite of integration tests covering health checks and user management functionalities. - Enhanced CI workflow to include integration tests with necessary environment variables.
156 lines
4.1 KiB
Go
156 lines
4.1 KiB
Go
//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 "<empty>"
|
|
}
|
|
return sub
|
|
}
|