fix(api): 收到 API 时钟的 repo 写操作一律用它打时间戳,不再和 PG now() 混用

This commit is contained in:
Lemon-miaow committed 2026-09-27 14:15:03 +08:00
1 parent e491e063cf
commit e9e9a7a622
9 files changed
+211 -38

No files matched your search

+1 -3
View File
@@ -1093,10 +1093,8 @@ func (f *fakeRepo) UpsertOwner(_ context.Context, id, username, email string) er
return nil return nil
} }
func (f *fakeRepo) CreateSession(_ context.Context, ns NewSession) error { func (f *fakeRepo) CreateSession(_ context.Context, ns NewSession) error {
// The API clock minted ExpiresAt, so this is the sign-in time on that clock.
now := ns.ExpiresAt.Add(-sessionTTL)
f.sessions[ns.TokenHash] = &fakeSession{ f.sessions[ns.TokenHash] = &fakeSession{
userID: ns.UserID, expiresAt: ns.ExpiresAt, createdAt: now, lastSeen: now, userID: ns.UserID, expiresAt: ns.ExpiresAt, createdAt: ns.CreatedAt, lastSeen: ns.CreatedAt,
userAgent: ns.UserAgent, clientIP: ns.ClientIP, reauthAt: ns.ReauthAt, userAgent: ns.UserAgent, clientIP: ns.ClientIP, reauthAt: ns.ReauthAt,
} }
return nil return nil
+33 -26
View File
@@ -11,6 +11,13 @@ import (
// PGRepo is the production Repo backed by Postgres (spec §6). It owns only the // PGRepo is the production Repo backed by Postgres (spec §6). It owns only the
// business projection the CRD cannot express. The SQL here is exercised by // business projection the CRD cannot express. The SQL here is exercised by
// integration tests against a live database, not the hermetic api_test.go suite. // integration tests against a live database, not the hermetic api_test.go suite.
//
// A method handed the API clock (now, or a NewSession minted on it) stamps the
// times it writes from that clock, column defaults included: a transaction on two
// clocks records one event at two instants, and the fake, which only has the API
// clock, stops describing it. now() is for methods given no clock. The updated_at
// triggers (0010) stay on now(): display bookkeeping no reader compares to the API
// clock.
type PGRepo struct { type PGRepo struct {
db *sql.DB db *sql.DB
} }
@@ -136,9 +143,9 @@ func (p *PGRepo) VerifyLinkCode(ctx context.Context, userID, code string, now ti
return "", "", ErrConflict return "", "", ErrConflict
} }
if _, err := tx.ExecContext(ctx, if _, err := tx.ExecContext(ctx,
`UPDATE account_links SET user_id = $1, auth_source = $2, verified_at = now() `UPDATE account_links SET user_id = $1, auth_source = $2, verified_at = $4
WHERE mc_uuid = $3`, WHERE mc_uuid = $3`,
userID, authSource, mcUUID); err != nil { userID, authSource, mcUUID, now); err != nil {
return "", "", fmt.Errorf("take over retired link: %w", err) return "", "", fmt.Errorf("take over retired link: %w", err)
} }
} }
@@ -149,9 +156,9 @@ func (p *PGRepo) VerifyLinkCode(ctx context.Context, userID, code string, now ti
// Yggdrasil this time gets the latest source stored), keeping the persisted // Yggdrasil this time gets the latest source stored), keeping the persisted
// value equal to the one returned to the caller. // value equal to the one returned to the caller.
if _, err := tx.ExecContext(ctx, if _, err := tx.ExecContext(ctx,
`INSERT INTO account_links (user_id, mc_uuid, auth_source) VALUES ($1, $2, $3) `INSERT INTO account_links (user_id, mc_uuid, auth_source, verified_at) VALUES ($1, $2, $3, $4)
ON CONFLICT (user_id, mc_uuid) DO UPDATE SET auth_source = EXCLUDED.auth_source`, ON CONFLICT (user_id, mc_uuid) DO UPDATE SET auth_source = EXCLUDED.auth_source`,
userID, mcUUID, authSource); err != nil { userID, mcUUID, authSource, now); err != nil {
return "", "", fmt.Errorf("write account link: %w", err) return "", "", fmt.Errorf("write account link: %w", err)
} }
if _, err := tx.ExecContext(ctx, if _, err := tx.ExecContext(ctx,
@@ -209,9 +216,9 @@ func (p *PGRepo) RedeemPlayerBindCode(ctx context.Context, newUserID, code strin
mcUUID).Scan(&userID, &existingRole, &disabled, &deleted); { mcUUID).Scan(&userID, &existingRole, &disabled, &deleted); {
case errors.Is(err, sql.ErrNoRows): case errors.Is(err, sql.ErrNoRows):
if _, err := tx.ExecContext(ctx, if _, err := tx.ExecContext(ctx,
`INSERT INTO users (id, username, role) VALUES ($1, $2, 'user') `INSERT INTO users (id, username, role, created_at) VALUES ($1, $2, 'user', $3)
ON CONFLICT (username) DO NOTHING`, ON CONFLICT (username) DO NOTHING`,
newUserID, mcUUID); err != nil { newUserID, mcUUID, now); err != nil {
return "", "", "", fmt.Errorf("create player: %w", err) return "", "", "", fmt.Errorf("create player: %w", err)
} }
// Re-read by username so a cross-code race converges on the winner's row // Re-read by username so a cross-code race converges on the winner's row
@@ -231,9 +238,9 @@ func (p *PGRepo) RedeemPlayerBindCode(ctx context.Context, newUserID, code strin
return "", "", "", ErrPlayerAccountRetired return "", "", "", ErrPlayerAccountRetired
} }
if _, err := tx.ExecContext(ctx, if _, err := tx.ExecContext(ctx,
`INSERT INTO account_links (user_id, mc_uuid, auth_source) VALUES ($1, $2, $3) `INSERT INTO account_links (user_id, mc_uuid, auth_source, verified_at) VALUES ($1, $2, $3, $4)
ON CONFLICT (mc_uuid) DO NOTHING`, ON CONFLICT (mc_uuid) DO NOTHING`,
userID, mcUUID, authSource); err != nil { userID, mcUUID, authSource, now); err != nil {
return "", "", "", fmt.Errorf("write account link: %w", err) return "", "", "", fmt.Errorf("write account link: %w", err)
} }
case err != nil: case err != nil:
@@ -298,13 +305,13 @@ func (p *PGRepo) CompleteOwnerSetup(ctx context.Context, newUserID, code string,
`SELECT user_id FROM account_links WHERE mc_uuid = $1`, mcUUID).Scan(&userID); { `SELECT user_id FROM account_links WHERE mc_uuid = $1`, mcUUID).Scan(&userID); {
case errors.Is(err, sql.ErrNoRows): case errors.Is(err, sql.ErrNoRows):
if _, err := tx.ExecContext(ctx, if _, err := tx.ExecContext(ctx,
`INSERT INTO users (id, username, role) VALUES ($1, $2, 'owner')`, `INSERT INTO users (id, username, role, created_at) VALUES ($1, $2, 'owner', $3)`,
newUserID, mcUUID); err != nil { newUserID, mcUUID, now); err != nil {
return "", "", "", fmt.Errorf("create owner: %w", err) return "", "", "", fmt.Errorf("create owner: %w", err)
} }
if _, err := tx.ExecContext(ctx, if _, err := tx.ExecContext(ctx,
`INSERT INTO account_links (user_id, mc_uuid, auth_source) VALUES ($1, $2, $3)`, `INSERT INTO account_links (user_id, mc_uuid, auth_source, verified_at) VALUES ($1, $2, $3, $4)`,
newUserID, mcUUID, authSource); err != nil { newUserID, mcUUID, authSource, now); err != nil {
return "", "", "", fmt.Errorf("write account link: %w", err) return "", "", "", fmt.Errorf("write account link: %w", err)
} }
userID = newUserID userID = newUserID
@@ -318,14 +325,14 @@ func (p *PGRepo) CompleteOwnerSetup(ctx context.Context, newUserID, code string,
} }
if _, err := tx.ExecContext(ctx, if _, err := tx.ExecContext(ctx,
`INSERT INTO platform_settings (key, value) VALUES ($1, $2::jsonb) `INSERT INTO platform_settings (key, value, updated_at) VALUES ($1, $2::jsonb, $3)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`, ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at`,
LocalAuthEnabledKey, "true"); err != nil { LocalAuthEnabledKey, "true", now); err != nil {
return "", "", "", fmt.Errorf("enable local auth: %w", err) return "", "", "", fmt.Errorf("enable local auth: %w", err)
} }
if _, err := tx.ExecContext(ctx, if _, err := tx.ExecContext(ctx,
`INSERT INTO setup_tokens (token_hash, user_id, expires_at) VALUES ($1, $2, $3)`, `INSERT INTO setup_tokens (token_hash, user_id, expires_at, created_at) VALUES ($1, $2, $3, $4)`,
tokenHash, userID, tokenExpiresAt); err != nil { tokenHash, userID, tokenExpiresAt, now); err != nil {
return "", "", "", fmt.Errorf("mint setup token: %w", err) return "", "", "", fmt.Errorf("mint setup token: %w", err)
} }
@@ -1427,9 +1434,9 @@ type sqlExecer interface {
func insertSession(ctx context.Context, db sqlExecer, s NewSession) error { func insertSession(ctx context.Context, db sqlExecer, s NewSession) error {
reauth := sql.NullTime{Time: s.ReauthAt, Valid: !s.ReauthAt.IsZero()} reauth := sql.NullTime{Time: s.ReauthAt, Valid: !s.ReauthAt.IsZero()}
_, err := db.ExecContext(ctx, _, err := db.ExecContext(ctx,
`INSERT INTO sessions (token_hash, user_id, expires_at, user_agent, client_ip, reauth_at) `INSERT INTO sessions (token_hash, user_id, created_at, last_seen_at, expires_at, user_agent, client_ip, reauth_at)
VALUES ($1, $2, $3, $4, $5, $6)`, VALUES ($1, $2, $3, $3, $4, $5, $6, $7)`,
s.TokenHash, s.UserID, s.ExpiresAt, s.UserAgent, s.ClientIP, reauth) s.TokenHash, s.UserID, s.CreatedAt, s.ExpiresAt, s.UserAgent, s.ClientIP, reauth)
return err return err
} }
@@ -2615,10 +2622,10 @@ func (p *PGRepo) RedeemMigration(ctx context.Context, targetUserID, codeHash str
// Re-point every server the source owns to the target, collecting the names for // Re-point every server the source owns to the target, collecting the names for
// the audit trail. Server ownership is the only thing that moves. // the audit trail. Server ownership is the only thing that moves.
rows, err := tx.QueryContext(ctx, rows, err := tx.QueryContext(ctx,
`UPDATE servers SET owner_id = $2, claimed_at = now() `UPDATE servers SET owner_id = $2, claimed_at = $3
WHERE owner_id = $1 AND deleted_at IS NULL WHERE owner_id = $1 AND deleted_at IS NULL
RETURNING name`, RETURNING name`,
sourceUserID, targetUserID) sourceUserID, targetUserID, now)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
@@ -2641,13 +2648,13 @@ func (p *PGRepo) RedeemMigration(ctx context.Context, targetUserID, codeHash str
// log in nor start another migration (double-spend defense). The servers just moved // log in nor start another migration (double-spend defense). The servers just moved
// away, so there is nothing left to release. // away, so there is nothing left to release.
if _, err := tx.ExecContext(ctx, if _, err := tx.ExecContext(ctx,
`UPDATE sessions SET revoked_at = now() WHERE user_id = $1 AND revoked_at IS NULL`, `UPDATE sessions SET revoked_at = $2 WHERE user_id = $1 AND revoked_at IS NULL`,
sourceUserID); err != nil { sourceUserID, now); err != nil {
return "", nil, err return "", nil, err
} }
if _, err := tx.ExecContext(ctx, if _, err := tx.ExecContext(ctx,
`UPDATE users SET disabled = true, deleted_at = now() WHERE id = $1 AND deleted_at IS NULL`, `UPDATE users SET disabled = true, deleted_at = $2 WHERE id = $1 AND deleted_at IS NULL`,
sourceUserID); err != nil { sourceUserID, now); err != nil {
return "", nil, err return "", nil, err
} }
+3
View File
@@ -212,6 +212,9 @@ type SessionedUser struct {
type NewSession struct { type NewSession struct {
TokenHash string TokenHash string
UserID string UserID string
// CreatedAt is the sign-in instant on the API clock: the session is created
// and last seen then, so the staff idle cutoff (also API clock) measures it.
CreatedAt time.Time
ExpiresAt time.Time ExpiresAt time.Time
UserAgent string UserAgent string
ClientIP string ClientIP string
+1
View File
@@ -114,6 +114,7 @@ func (a *API) mintSession(r *http.Request, userID string, proof signInProof) (st
return token, NewSession{ return token, NewSession{
TokenHash: hashCookie(token), TokenHash: hashCookie(token),
UserID: userID, UserID: userID,
CreatedAt: now,
ExpiresAt: expires, ExpiresAt: expires,
UserAgent: truncateUTF8(r.UserAgent(), maxSessionUserAgent), UserAgent: truncateUTF8(r.UserAgent(), maxSessionUserAgent),
ClientIP: ip, ClientIP: ip,
+2 -2
View File
@@ -614,7 +614,7 @@ func TestRedeemSetupTokenContract(t *testing.T) {
redeem := func(hash string, at time.Time) (string, string, error) { redeem := func(hash string, at time.Time) (string, string, error) {
mu.Lock() mu.Lock()
minted++ minted++
s := api.NewSession{TokenHash: "st-sess-" + strconv.Itoa(minted) + "-" + suffix(t), UserID: "ignored", ExpiresAt: t0.Add(time.Hour)} s := api.NewSession{TokenHash: "st-sess-" + strconv.Itoa(minted) + "-" + suffix(t), UserID: "ignored", CreatedAt: at, ExpiresAt: t0.Add(time.Hour)}
mu.Unlock() mu.Unlock()
id, err := repo.RedeemSetupToken(ctx, hash, at, s) id, err := repo.RedeemSetupToken(ctx, hash, at, s)
return id, s.TokenHash, err return id, s.TokenHash, err
@@ -660,7 +660,7 @@ func TestRedeemSetupTokenContract(t *testing.T) {
kept := "st-kept-" + suffix(t) kept := "st-kept-" + suffix(t)
create(kept) create(kept)
taken := newSession(t, u.ID, "st-taken", t0.Add(time.Hour)) taken := newSession(t, u.ID, "st-taken", t0.Add(time.Hour))
if _, err := repo.RedeemSetupToken(ctx, kept, t0, api.NewSession{TokenHash: taken, ExpiresAt: t0.Add(time.Hour)}); err == nil || errors.Is(err, api.ErrNotFound) { if _, err := repo.RedeemSetupToken(ctx, kept, t0, api.NewSession{TokenHash: taken, CreatedAt: t0, ExpiresAt: t0.Add(time.Hour)}); err == nil || errors.Is(err, api.ErrNotFound) {
t.Fatalf("redeem into a taken session hash = %v, want the insert failure", err) t.Fatalf("redeem into a taken session hash = %v, want the insert failure", err)
} }
if got, _, err := redeem(kept, t0); err != nil || got != u.ID { if got, _, err := redeem(kept, t0); err != nil || got != u.ID {
+164
View File
@@ -0,0 +1,164 @@
//go:build pgint
package pgint
import (
"context"
"errors"
"testing"
"time"
"felis.lolicon.best/internal/api"
)
// A method handed the API clock writes only that clock (PGRepo's doc). Each case
// runs the API clock an hour behind the database's, so a time taken from now()
// instead lands an hour away from the one the method was given.
func skewedClock() time.Time { return mustNow().Add(-time.Hour).Truncate(time.Microsecond) }
func stampAt(t *testing.T, q string, args ...any) time.Time {
t.Helper()
var at time.Time
if err := db.QueryRow(q, args...).Scan(&at); err != nil {
t.Fatalf("%s: %v", q, err)
}
return at
}
func wantStamp(t *testing.T, what string, got, want time.Time) {
t.Helper()
if !sameMicro(got, want) {
t.Errorf("%s = %v, want the API clock's %v", what, got, want)
}
}
// A session is created and last seen at its sign-in on the API clock, which is
// also the clock the staff idle cutoff reads.
func TestSessionTimesComeFromTheSignIn(t *testing.T) {
ctx := context.Background()
now := skewedClock()
create := func(userID, tag string, signedIn time.Time) string {
t.Helper()
hash := tag + "-" + suffix(t)
if err := repo.CreateSession(ctx, api.NewSession{
TokenHash: hash, UserID: userID, CreatedAt: signedIn, ExpiresAt: now.Add(time.Hour),
}); err != nil {
t.Fatalf("CreateSession(%s): %v", tag, err)
}
return hash
}
admin := newUser(t, "admin", "clk-admin")
stale := create(admin.ID, "clk-stale", now.Add(-31*time.Minute))
fresh := create(admin.ID, "clk-fresh", now.Add(-29*time.Minute))
if _, err := repo.SessionUser(ctx, stale, now); !errors.Is(err, api.ErrNotFound) {
t.Fatalf("staff session signed in 31m ago = %v, want ErrNotFound (idled out)", err)
}
if _, err := repo.SessionUser(ctx, fresh, now); err != nil {
t.Fatalf("staff session signed in 29m ago: %v", err)
}
player := newUser(t, "user", "clk-player")
signedIn := now.Add(-2 * time.Hour)
hash := create(player.ID, "clk-player", signedIn)
ss, err := repo.ListUserSessions(ctx, player.ID, now)
if err != nil || len(ss) != 1 || ss[0].TokenHash != hash {
t.Fatalf("ListUserSessions = %+v, %v; want only %s", ss, err, hash)
}
wantStamp(t, "created_at", ss[0].CreatedAt, signedIn)
wantStamp(t, "last_seen_at", ss[0].LastSeenAt, signedIn)
}
// Every door that writes a link stamps it, and whatever else its transaction
// creates, at the time it was given.
func TestLinkWritesStampTheAPIClock(t *testing.T) {
ctx := context.Background()
now := skewedClock()
code := func(mc string) string {
t.Helper()
c := "clk-" + suffix(t)
if err := repo.CreateLinkCode(ctx, c, mc, "mojang", now.Add(10*time.Minute)); err != nil {
t.Fatalf("CreateLinkCode: %v", err)
}
return c
}
linkedAt := func(mc string) time.Time {
t.Helper()
return stampAt(t, `SELECT verified_at FROM account_links WHERE mc_uuid = $1`, mc)
}
createdAt := func(userID string) time.Time {
t.Helper()
return stampAt(t, `SELECT created_at FROM users WHERE id = $1`, userID)
}
t.Run("verify", func(t *testing.T) {
u := newUser(t, "user", "clk-verify")
mc := testUUID(t)
if _, _, err := repo.VerifyLinkCode(ctx, u.ID, code(mc), now); err != nil {
t.Fatalf("VerifyLinkCode: %v", err)
}
wantStamp(t, "verified_at", linkedAt(mc), now)
})
t.Run("takeover of a retired link", func(t *testing.T) {
retired := newUser(t, "user", "clk-retired")
mc := testUUID(t)
if err := repo.LinkAccount(ctx, retired.ID, mc, "mojang"); err != nil {
t.Fatalf("LinkAccount: %v", err)
}
mustExec(t, `UPDATE users SET disabled = true, deleted_at = now() WHERE id = $1`, retired.ID)
taker := newUser(t, "user", "clk-taker")
if _, _, err := repo.VerifyLinkCode(ctx, taker.ID, code(mc), now); err != nil {
t.Fatalf("takeover VerifyLinkCode: %v", err)
}
wantStamp(t, "verified_at", linkedAt(mc), now)
})
t.Run("player bind", func(t *testing.T) {
mc := testUUID(t)
id := "usr-clk-player-" + suffix(t)
if got, _, _, err := repo.RedeemPlayerBindCode(ctx, id, code(mc), now); err != nil || got != id {
t.Fatalf("RedeemPlayerBindCode = %q, %v; want %s", got, err, id)
}
wantStamp(t, "user created_at", createdAt(id), now)
wantStamp(t, "verified_at", linkedAt(mc), now)
})
t.Run("owner setup", func(t *testing.T) {
mc := testUUID(t)
id := "usr-clk-owner-" + suffix(t)
tok := "tok-clk-" + suffix(t)
if got, _, _, err := repo.CompleteOwnerSetup(ctx, id, code(mc), now, tok, now.Add(time.Hour)); err != nil || got != id {
t.Fatalf("CompleteOwnerSetup = %q, %v; want %s", got, err, id)
}
wantStamp(t, "user created_at", createdAt(id), now)
wantStamp(t, "verified_at", linkedAt(mc), now)
wantStamp(t, "setup token created_at",
stampAt(t, `SELECT created_at FROM setup_tokens WHERE token_hash = $1`, tok), now)
wantStamp(t, "local auth updated_at",
stampAt(t, `SELECT updated_at FROM platform_settings WHERE key = $1`, api.LocalAuthEnabledKey), now)
})
}
// The redeem moves the servers, retires the source and ends its sessions at the
// instant it records the migration redeemed.
func TestMigrationRedeemStampsTheAPIClock(t *testing.T) {
ctx := context.Background()
now := skewedClock()
src, dst := newUser(t, "user", "clk-src"), newUser(t, "user", "clk-dst")
server := "clk-srv-" + suffix(t)
seedOwnedServer(t, server, src.ID, false)
sess := newSession(t, src.ID, "clk-src", now.Add(time.Hour))
hash := "h-clk-" + suffix(t)
startToCode(t, src.ID, dst.ID, hash, now, now.Add(10*time.Minute))
if _, moved, err := repo.RedeemMigration(ctx, dst.ID, hash, now); err != nil || len(moved) != 1 {
t.Fatalf("RedeemMigration = %v, %v; want [%s]", moved, err, server)
}
wantStamp(t, "redeemed_at",
stampAt(t, `SELECT redeemed_at FROM account_migrations WHERE source_user_id = $1 AND state = 'redeemed'`, src.ID), now)
wantStamp(t, "server claimed_at", stampAt(t, `SELECT claimed_at FROM servers WHERE name = $1`, server), now)
wantStamp(t, "source deleted_at", stampAt(t, `SELECT deleted_at FROM users WHERE id = $1`, src.ID), now)
wantStamp(t, "source session revoked_at", stampAt(t, `SELECT revoked_at FROM sessions WHERE token_hash = $1`, sess), now)
}
+4 -4
View File
@@ -127,7 +127,7 @@ func TestSessionLifecycle(t *testing.T) {
hash := "tok-" + suffix(t) hash := "tok-" + suffix(t)
expires := mustNow().Add(time.Hour) expires := mustNow().Add(time.Hour)
if err := repo.CreateSession(ctx, api.NewSession{TokenHash: hash, UserID: u.ID, ExpiresAt: expires}); err != nil { if err := repo.CreateSession(ctx, api.NewSession{TokenHash: hash, UserID: u.ID, CreatedAt: mustNow(), ExpiresAt: expires}); err != nil {
t.Fatalf("CreateSession: %v", err) t.Fatalf("CreateSession: %v", err)
} }
su, err := repo.SessionUser(ctx, hash, mustNow()) su, err := repo.SessionUser(ctx, hash, mustNow())
@@ -371,7 +371,7 @@ func TestAuditAttributionContract(t *testing.T) {
// The session principal carries the username the rows are signed with. // The session principal carries the username the rows are signed with.
hash := "audit-sess-" + suffix(t) hash := "audit-sess-" + suffix(t)
if err := repo.CreateSession(ctx, api.NewSession{TokenHash: hash, UserID: u.ID, ExpiresAt: mustNow().Add(time.Hour)}); err != nil { if err := repo.CreateSession(ctx, api.NewSession{TokenHash: hash, UserID: u.ID, CreatedAt: mustNow(), ExpiresAt: mustNow().Add(time.Hour)}); err != nil {
t.Fatalf("CreateSession: %v", err) t.Fatalf("CreateSession: %v", err)
} }
su, err := repo.SessionUser(ctx, hash, mustNow()) su, err := repo.SessionUser(ctx, hash, mustNow())
@@ -866,7 +866,7 @@ func TestDeadAccountsAreLockedOutInPG(t *testing.T) {
t.Fatalf("alive UserByEmail: %v", err) t.Fatalf("alive UserByEmail: %v", err)
} }
hash := "h-" + suffix(t) hash := "h-" + suffix(t)
if err := repo.CreateSession(ctx, api.NewSession{TokenHash: hash, UserID: u.ID, ExpiresAt: now.Add(time.Hour)}); err != nil { if err := repo.CreateSession(ctx, api.NewSession{TokenHash: hash, UserID: u.ID, CreatedAt: now, ExpiresAt: now.Add(time.Hour)}); err != nil {
t.Fatalf("CreateSession: %v", err) t.Fatalf("CreateSession: %v", err)
} }
if _, err := repo.SessionUser(ctx, hash, now); err != nil { if _, err := repo.SessionUser(ctx, hash, now); err != nil {
@@ -919,7 +919,7 @@ func TestDeadAccountsAreLockedOutInPG(t *testing.T) {
t.Fatalf("deleted UserByEmail = %v, want ErrNotFound", err) t.Fatalf("deleted UserByEmail = %v, want ErrNotFound", err)
} }
hash2 := "h2-" + suffix(t) hash2 := "h2-" + suffix(t)
if err := repo.CreateSession(ctx, api.NewSession{TokenHash: hash2, UserID: u.ID, ExpiresAt: now.Add(time.Hour)}); err != nil { if err := repo.CreateSession(ctx, api.NewSession{TokenHash: hash2, UserID: u.ID, CreatedAt: now, ExpiresAt: now.Add(time.Hour)}); err != nil {
t.Fatalf("CreateSession (deleted): %v", err) t.Fatalf("CreateSession (deleted): %v", err)
} }
if _, err := repo.SessionUser(ctx, hash2, now); !errors.Is(err, api.ErrNotFound) { if _, err := repo.SessionUser(ctx, hash2, now); !errors.Is(err, api.ErrNotFound) {
+2 -2
View File
@@ -15,7 +15,7 @@ func newSession(t *testing.T, userID, tag string, expires time.Time) string {
t.Helper() t.Helper()
hash := tag + "-" + suffix(t) hash := tag + "-" + suffix(t)
if err := repo.CreateSession(context.Background(), api.NewSession{ if err := repo.CreateSession(context.Background(), api.NewSession{
TokenHash: hash, UserID: userID, ExpiresAt: expires, TokenHash: hash, UserID: userID, CreatedAt: mustNow(), ExpiresAt: expires,
UserAgent: "agent " + tag, ClientIP: "192.0.2.1", UserAgent: "agent " + tag, ClientIP: "192.0.2.1",
}); err != nil { }); err != nil {
t.Fatalf("CreateSession(%s): %v", tag, err) t.Fatalf("CreateSession(%s): %v", tag, err)
@@ -207,7 +207,7 @@ func TestSessionReauthProof(t *testing.T) {
proven := "proven-" + suffix(t) proven := "proven-" + suffix(t)
signedIn := now.Add(-time.Minute) signedIn := now.Add(-time.Minute)
if err := repo.CreateSession(ctx, api.NewSession{ if err := repo.CreateSession(ctx, api.NewSession{
TokenHash: proven, UserID: u.ID, ExpiresAt: now.Add(time.Hour), ReauthAt: signedIn, TokenHash: proven, UserID: u.ID, CreatedAt: signedIn, ExpiresAt: now.Add(time.Hour), ReauthAt: signedIn,
}); err != nil { }); err != nil {
t.Fatalf("CreateSession: %v", err) t.Fatalf("CreateSession: %v", err)
} }
+1 -1
View File
@@ -175,7 +175,7 @@ func TestSetUserDisabledIsAtomic(t *testing.T) {
ctx := context.Background() ctx := context.Background()
u := newUser(t, "user", "disable") u := newUser(t, "user", "disable")
hash := "h-" + suffix(t) hash := "h-" + suffix(t)
if err := repo.CreateSession(ctx, api.NewSession{TokenHash: hash, UserID: u.ID, ExpiresAt: mustNow().Add(time.Hour)}); err != nil { if err := repo.CreateSession(ctx, api.NewSession{TokenHash: hash, UserID: u.ID, CreatedAt: mustNow(), ExpiresAt: mustNow().Add(time.Hour)}); err != nil {
t.Fatal(err) t.Fatal(err)
} }