Files

120 lines
4.1 KiB
Go
Raw Permalink Normal View History

2026-08-24 04:03:44 +02:00
package auth
import (
"fmt"
"os"
"strings"
"testing"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
// TestAcceptInviteOrphanRecoversPasswordAndOwnsCompany covers the removed-member
// re-invite path: zero active memberships may set a new password, and accept also
// provisions a personal owned workspace alongside the invited company.
func TestAcceptInviteOrphanRecoversPasswordAndOwnsCompany(t *testing.T) {
dsn := strings.TrimSpace(os.Getenv("DATABASE_URL"))
if dsn == "" {
t.Skip("DATABASE_URL not set")
}
ctx := t.Context()
pg, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatalf("postgres: %v", err)
}
t.Cleanup(pg.Close)
svc := &Service{Pool: pg}
prefix := uuid.New().String()[:8]
inviteCompanyID := uuid.New()
userID := uuid.New()
email := fmt.Sprintf("orphan-invite-%s@example.test", prefix)
oldHash, err := HashPassword("OldPassword123!")
if err != nil {
t.Fatalf("hash: %v", err)
}
_, err = pg.Exec(ctx, `INSERT INTO companies (id, name) VALUES ($1, $2)`,
inviteCompanyID, "Invite Target "+prefix)
if err != nil {
t.Fatalf("seed invite company: %v", err)
}
_, err = pg.Exec(ctx, `
INSERT INTO users (id, email, name, password_hash, must_set_password, is_platform_admin, is_active)
VALUES ($1, $2, $3, $4, false, false, true)`,
userID, email, "Orphan", oldHash)
if err != nil {
t.Fatalf("seed user: %v", err)
}
// Inactive membership only — mimics remove-member.
_, err = pg.Exec(ctx, `
INSERT INTO memberships (company_id, user_id, role, status)
VALUES ($1, $2, 'member', 'inactive')`, inviteCompanyID, userID)
if err != nil {
t.Fatalf("seed inactive membership: %v", err)
}
mode, err := svc.InvitePasswordMode(ctx, email)
if err != nil {
t.Fatalf("InvitePasswordMode: %v", err)
}
if mode != InvitePasswordRecover {
t.Fatalf("password mode=%q want %q", mode, InvitePasswordRecover)
}
inv, token, err := svc.CreateInvite(ctx, inviteCompanyID, userID, email, "member")
if err != nil {
t.Fatalf("CreateInvite: %v", err)
}
t.Cleanup(func() {
cctx := t.Context()
_, _ = pg.Exec(cctx, `DELETE FROM invites WHERE id = $1`, inv.ID)
_, _ = pg.Exec(cctx, `DELETE FROM memberships WHERE user_id = $1`, userID)
_, _ = pg.Exec(cctx, `DELETE FROM credit_balances WHERE company_id IN (SELECT id FROM companies WHERE owner_user_id = $1)`, userID)
_, _ = pg.Exec(cctx, `DELETE FROM company_settings WHERE company_id IN (SELECT id FROM companies WHERE owner_user_id = $1)`, userID)
_, _ = pg.Exec(cctx, `DELETE FROM company_plans WHERE company_id IN (SELECT id FROM companies WHERE owner_user_id = $1)`, userID)
_, _ = pg.Exec(cctx, `DELETE FROM companies WHERE owner_user_id = $1 OR id = $2`, userID, inviteCompanyID)
_, _ = pg.Exec(cctx, `DELETE FROM users WHERE id = $1`, userID)
})
res, err := svc.AcceptInvite(ctx, token, "NewPassword123!", "Orphan")
if err != nil {
t.Fatalf("AcceptInvite: %v", err)
}
if res.CompanyID != inviteCompanyID {
t.Fatalf("active company=%s want invited %s", res.CompanyID, inviteCompanyID)
}
if len(res.ProvisionCompanyIDs) != 1 {
t.Fatalf("expected one newly provisioned owned company, got %#v", res.ProvisionCompanyIDs)
}
ownedID := res.ProvisionCompanyIDs[0]
if ownedID == inviteCompanyID {
t.Fatal("owned company must differ from invited company")
}
var owner uuid.UUID
if err := pg.QueryRow(ctx, `SELECT owner_user_id FROM companies WHERE id = $1`, ownedID).Scan(&owner); err != nil {
t.Fatalf("owned company: %v", err)
}
if owner != userID {
t.Fatalf("owner=%s want %s", owner, userID)
}
var activeCount int64
if err := pg.QueryRow(ctx, `
SELECT count(*) FROM memberships WHERE user_id = $1 AND status = 'active'`, userID).Scan(&activeCount); err != nil {
t.Fatalf("active memberships: %v", err)
}
if activeCount < 2 {
t.Fatalf("active memberships=%d want at least 2 (owned + invited)", activeCount)
}
if _, err := svc.Login(ctx, email, "NewPassword123!"); err != nil {
t.Fatalf("login with new password: %v", err)
}
if _, err := svc.Login(ctx, email, "OldPassword123!"); err != ErrInvalidCredentials {
t.Fatalf("old password should fail: err=%v", err)
}
}