//go:build integration package integrationtest import ( "context" "fmt" "net/http/httptest" "os" "sync" "testing" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" "github.com/redis/go-redis/v9" "github.com/ultisuite/ulti-backend/internal/auth" "github.com/ultisuite/ulti-backend/internal/config" "github.com/ultisuite/ulti-backend/internal/dbmigrate" "github.com/ultisuite/ulti-backend/internal/server" ) // Harness is the shared integration test environment. type Harness struct { Env Env Infra *infra OIDC *OIDCServer App *server.App Server *httptest.Server Pool *pgxpool.Pool Redis *redis.Client } var ( harnessMu sync.Mutex harnessInst *Harness harnessRefCount int harnessErr error ) // Get returns the shared harness, initializing containers on first call. func Get() (*Harness, error) { harnessMu.Lock() defer harnessMu.Unlock() if harnessInst != nil { return harnessInst, nil } if harnessErr != nil { return nil, harnessErr } harnessInst, harnessErr = newHarness(context.Background()) return harnessInst, harnessErr } func releaseHarness(ctx context.Context) { harnessMu.Lock() defer harnessMu.Unlock() harnessRefCount-- if harnessRefCount > 0 || harnessInst == nil { return } harnessInst.Close(ctx) harnessInst = nil harnessErr = nil } // RunMain is called from TestMain in integration test packages. func RunMain(m *testing.M) int { env := LoadEnv() if !env.Enabled { fmt.Fprintln(os.Stderr, "integration tests skipped: set ULTI_TEST_INTEGRATION=1") return 0 } harnessMu.Lock() harnessRefCount++ harnessMu.Unlock() h, err := Get() if err != nil { fmt.Fprintf(os.Stderr, "integration harness setup failed: %v\n", err) return 1 } _ = h code := m.Run() releaseHarness(context.Background()) return code } func newHarness(ctx context.Context) (*Harness, error) { env := LoadEnv() if !env.Enabled { return nil, fmt.Errorf("ULTI_TEST_INTEGRATION is not set") } infra, err := sharedInfra(ctx, env) if err != nil { return nil, err } if env.AutoMigrate { if err := dbmigrate.Up(infra.dbURL); err != nil { return nil, fmt.Errorf("migrate: %w", err) } } oidc, err := NewOIDCServer() if err != nil { return nil, err } holder, err := oidc.Holder(ctx) if err != nil { return nil, fmt.Errorf("oidc holder: %w", err) } pool, err := pgxpool.New(ctx, infra.dbURL) if err != nil { return nil, fmt.Errorf("pg pool: %w", err) } rdb := redis.NewClient(&redis.Options{Addr: infra.redisAddr}) cfg := buildTestConfig(env, infra, oidc) app, err := server.New(ctx, cfg, server.Options{ WithoutWorkers: true, SkipAuthentikProvisioner: true, VerifierHolder: holder, Pool: pool, Redis: rdb, }) if err != nil { pool.Close() _ = rdb.Close() return nil, fmt.Errorf("server bootstrap: %w", err) } ts := httptest.NewServer(app.Router) return &Harness{ Env: env, Infra: infra, OIDC: oidc, App: app, Server: ts, Pool: pool, Redis: rdb, }, nil } func buildTestConfig(env Env, infra *infra, oidc *OIDCServer) *config.Config { cfg := &config.Config{ Port: 0, Domain: "localhost", AppEnv: "development", DatabaseURL: infra.dbURL, KeyDBAddr: infra.redisAddr, KeyDBDB: 0, RustFSEndpoint: normalizeEndpoint(infra.s3Endpoint), RustFSAccessKey: infra.s3AccessKey, RustFSSecretKey: infra.s3SecretKey, RustFSUseSSL: env.S3UseSSL, RustFSRegion: "us-east-1", OIDCIssuer: oidc.Issuer, OIDCClientID: oidc.ClientID, NextcloudEnabled: env.Nextcloud && env.NextcloudURL != "", NextcloudURL: env.NextcloudURL, NextcloudPublicURL: env.NextcloudURL, NCAdminUser: "admin", NCAdminPass: "test", OnlyOfficeEnabled: false, JitsiEnabled: env.Jitsi, JitsiDomain: "meet.localhost", JitsiAppID: "ulti", JitsiAppSecret: "test-jitsi-secret", ImmichEnabled: env.Immich && env.ImmichURL != "", ImmichAPIURL: env.ImmichURL, MailAttachmentsBucket: "mail-attachments-test", MailSyncInterval: time.Minute, MailOutboxInterval: time.Minute, MailOutboxMaxRetries: 3, MailSendRatePerMinute: 100, MailSendBurst: 50, MailSMTPCircuitFailures: 5, MailSMTPCircuitCooldown: time.Minute, MailCredentialKeys: "v1:MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=", MailActiveCredentialKeyID: "v1", MailWebhookSharedSecret: "test-webhook-secret", MailAppURL: "http://localhost:3004", SearchEngine: "postgres", MeilisearchURL: env.MeilisearchURL, MeilisearchKey: env.MeilisearchKey, HealthHTTPTimeout: 2 * time.Second, } if env.Meilisearch && env.MeilisearchURL != "" { cfg.SearchEngine = "meilisearch" } return cfg } // Close releases test resources. func (h *Harness) Close(ctx context.Context) { if h == nil { return } if h.Server != nil { h.Server.Close() } if h.App != nil { h.App.Close() } else { if h.Pool != nil { h.Pool.Close() } if h.Redis != nil { _ = h.Redis.Close() } } if h.OIDC != nil { h.OIDC.Close() } if h.Infra != nil { h.Infra.Close(ctx) } } // NewExternalID returns a unique OIDC subject for test isolation. func NewExternalID(prefix string) string { if prefix == "" { prefix = "test" } return fmt.Sprintf("%s-%s", prefix, uuid.NewString()) } // IssueToken signs an OIDC ID token for the given claims. func (h *Harness) IssueToken(claims *auth.Claims) (string, error) { return h.OIDC.IssueToken(claims) } // Client returns an authenticated HTTP client for the given claims. func (h *Harness) Client(claims *auth.Claims) (*Client, error) { token, err := h.IssueToken(claims) if err != nil { return nil, err } return NewClient(h.Server.URL, token), nil } // RequireHarness is a test helper that fails the test if harness setup fails. func RequireHarness(t *testing.T) *Harness { t.Helper() if !LoadEnv().Enabled { t.Skip("integration tests disabled: set ULTI_TEST_INTEGRATION=1") } h, err := Get() if err != nil { t.Fatalf("harness: %v", err) } return h }