1598646021
On unlink (RemoveIdentity, reason=unlink) and email change (replaceEmailIdentity, reason=change) write the outgoing credential to retained_identities before removing the live identities row — so the legal dossier survives while the (kind, external_id) frees for reuse. Same transaction, so the dossier and live state cannot diverge. Integration tests cover both reasons.
93 lines
3.1 KiB
Go
93 lines
3.1 KiB
Go
//go:build integration
|
|
|
|
package inttest
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"scrabble/backend/internal/account"
|
|
)
|
|
|
|
// retainedRow is one row of the retention journal, read directly for assertions.
|
|
type retainedRow struct {
|
|
kind, externalID, reason string
|
|
}
|
|
|
|
// retainedRows reads the retention journal for an account, oldest detach first.
|
|
func retainedRows(t *testing.T, accountID uuid.UUID) []retainedRow {
|
|
t.Helper()
|
|
rows, err := testDB.QueryContext(context.Background(),
|
|
"SELECT kind, external_id, reason FROM retained_identities WHERE account_id = $1 ORDER BY detached_at",
|
|
accountID)
|
|
if err != nil {
|
|
t.Fatalf("query retained_identities %s: %v", accountID, err)
|
|
}
|
|
defer rows.Close()
|
|
var out []retainedRow
|
|
for rows.Next() {
|
|
var r retainedRow
|
|
if err := rows.Scan(&r.kind, &r.externalID, &r.reason); err != nil {
|
|
t.Fatalf("scan retained row: %v", err)
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// TestUnlinkJournalsRetainedIdentity: detaching a provider records it in the retention
|
|
// journal (reason=unlink) before the live identity is removed.
|
|
func TestUnlinkJournalsRetainedIdentity(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := account.NewStore(testDB)
|
|
|
|
tgExt := "tg-" + uuid.NewString()
|
|
acc, err := store.ProvisionByIdentity(ctx, account.KindTelegram, tgExt)
|
|
if err != nil {
|
|
t.Fatalf("provision: %v", err)
|
|
}
|
|
email := "keep-" + uuid.NewString() + "@example.com"
|
|
if err := store.AttachIdentity(ctx, acc.ID, account.KindEmail, email, true); err != nil {
|
|
t.Fatalf("attach email: %v", err)
|
|
}
|
|
|
|
if err := store.RemoveIdentity(ctx, acc.ID, account.KindTelegram); err != nil {
|
|
t.Fatalf("unlink telegram: %v", err)
|
|
}
|
|
got := retainedRows(t, acc.ID)
|
|
if len(got) != 1 || got[0].kind != account.KindTelegram || got[0].externalID != tgExt || got[0].reason != "unlink" {
|
|
t.Fatalf("retained rows = %+v, want one unlink telegram %q", got, tgExt)
|
|
}
|
|
}
|
|
|
|
// TestChangeEmailJournalsOldAddress: an email change records the outgoing address in the
|
|
// retention journal (reason=change).
|
|
func TestChangeEmailJournalsOldAddress(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := account.NewStore(testDB)
|
|
mailer := &capturingMailer{}
|
|
svc := account.NewEmailService(store, mailer, "https://erudit-game.ru")
|
|
|
|
acc, err := store.ProvisionByIdentity(ctx, account.KindTelegram, "tg-"+uuid.NewString())
|
|
if err != nil {
|
|
t.Fatalf("provision: %v", err)
|
|
}
|
|
oldAddr := "old-" + uuid.NewString() + "@example.com"
|
|
if err := store.AttachIdentity(ctx, acc.ID, account.KindEmail, oldAddr, true); err != nil {
|
|
t.Fatalf("attach old email: %v", err)
|
|
}
|
|
newAddr := "new-" + uuid.NewString() + "@example.com"
|
|
if err := svc.RequestChangeCode(ctx, acc.ID, newAddr); err != nil {
|
|
t.Fatalf("request change: %v", err)
|
|
}
|
|
if _, err := svc.ConfirmChange(ctx, acc.ID, newAddr, sixDigit.FindString(mailer.lastBody)); err != nil {
|
|
t.Fatalf("confirm change: %v", err)
|
|
}
|
|
got := retainedRows(t, acc.ID)
|
|
if len(got) != 1 || got[0].kind != account.KindEmail || got[0].externalID != oldAddr || got[0].reason != "change" {
|
|
t.Fatalf("retained rows = %+v, want one change email %q", got, oldAddr)
|
|
}
|
|
}
|