fix(api): 收到 API 时钟的 repo 写操作一律用它打时间戳,不再和 PG now() 混用
This commit is contained in:
9 files changed
+211
-38
No files matched your search
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user