08fb915b5e
走 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>
76 lines
2.6 KiB
Go
76 lines
2.6 KiB
Go
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)
|
|
}
|