ultisuite-backend/internal/integrationtest/harness.go
R3D347HR4Y 20c4fef3c6
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
docxi import lol
2026-06-10 00:27:21 +02:00

267 lines
6.3 KiB
Go

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