test(devices): #27 同机换账号登录注册 E2E(SQLite,服务层)

走 login 完全相同的路径 devices.Service.RegisterIfAbsent{MaxDevices:0}:
同一 device uuid 两账号各得一行、A 重登幂等刷新、共享 uuid 恰 2 行。
补 SQLite(生产同引擎)服务层覆盖,与既有 store 层 TestSQLite_DevicesUserScopedUUID
及 MySQL 集成 TestSameDeviceUUIDTwoAccounts 三层互证 F3 修复。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-05 13:11:25 +08:00
parent eb7c3c1062
commit 08fb915b5e
@@ -0,0 +1,75 @@
package store_test
import (
"context"
"testing"
"github.com/wangjia/pangolin/server/internal/devices"
)
// TestSQLite_LoginRegistersSameDeviceForTwoAccounts reproduces the #27 (F3) fix
// at the exact code path login drives: recordLogin → devReg.RegisterDevice →
// devices.Service.RegisterIfAbsent{MaxDevices:0}. Two accounts on the SAME physical
// device (same device uuid) must each get their own devices row.
//
// Under the old global UNIQUE(uuid) the second account's registration failed
// (silently on login, best-effort) → the device row for account B never existed →
// ConnectNode later reported DEVICE_NOT_REGISTERED (the 403 deadlock, F3).
func TestSQLite_LoginRegistersSameDeviceForTwoAccounts(t *testing.T) {
ctx := context.Background()
db := openSQLite(t)
mkUser := func(u, email string) int64 {
res, err := db.ExecContext(ctx,
`INSERT INTO users (uuid, email, pw_hash, dp_uuid) VALUES (?, ?, 'h', ?)`,
u, email, "dp-"+u)
if err != nil {
t.Fatalf("user %s: %v", u, err)
}
id, _ := res.LastInsertId()
return id
}
userA := mkUser("u-a", "a@x.c")
userB := mkUser("u-b", "b@x.c")
svc := devices.NewService(devices.NewStore(db), nil)
const sharedUUID = "shared-install-uuid"
// Account A "logs in" on the device.
idA, _, apiErr := svc.RegisterIfAbsent(ctx, devices.RegisterInput{
UserID: userA, DeviceUUID: sharedUUID, Platform: "macos", MaxDevices: 0,
})
if apiErr != nil || idA == 0 {
t.Fatalf("A register: id=%d err=%v", idA, apiErr)
}
// Account B "logs in" on the SAME physical device (same uuid) — the F3 case.
idB, _, apiErr := svc.RegisterIfAbsent(ctx, devices.RegisterInput{
UserID: userB, DeviceUUID: sharedUUID, Platform: "macos", MaxDevices: 0,
})
if apiErr != nil {
t.Fatalf("B register on same device uuid must succeed (F3 fix), got %v", apiErr)
}
if idB == 0 || idB == idA {
t.Fatalf("B must get its own device row, idA=%d idB=%d", idA, idB)
}
// A logs in again → idempotent refresh of A's own row (not a new row).
idA2, _, apiErr := svc.RegisterIfAbsent(ctx, devices.RegisterInput{
UserID: userA, DeviceUUID: sharedUUID, Platform: "macos", MaxDevices: 0,
})
if apiErr != nil || idA2 != idA {
t.Fatalf("A re-register must refresh same row: id=%d (want %d) err=%v", idA2, idA, apiErr)
}
// Both rows coexist for the shared uuid, one per account.
var n int
if err := db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM devices WHERE uuid=?`, sharedUUID).Scan(&n); err != nil {
t.Fatal(err)
}
if n != 2 {
t.Fatalf("shared uuid must have 2 rows (one per account), got %d", n)
}
t.Logf("OK: device uuid %q shared by account A (row #%d) + account B (row #%d)", sharedUUID, idA, idB)
}