120 lines
4.1 KiB
Go
120 lines
4.1 KiB
Go
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)
|
||
|
|
}
|
||
|
|
}
|