From 08fb915b5ebd1bbcdc7134fdd3193103a5ece8ca Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sun, 5 Jul 2026 13:11:25 +0800 Subject: [PATCH] =?UTF-8?q?test(devices):=20#27=20=E5=90=8C=E6=9C=BA?= =?UTF-8?q?=E6=8D=A2=E8=B4=A6=E5=8F=B7=E7=99=BB=E5=BD=95=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=20E2E(SQLite,=E6=9C=8D=E5=8A=A1=E5=B1=82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 走 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 --- .../internal/store/devices_login_swap_test.go | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 server/internal/store/devices_login_swap_test.go diff --git a/server/internal/store/devices_login_swap_test.go b/server/internal/store/devices_login_swap_test.go new file mode 100644 index 0000000..723314e --- /dev/null +++ b/server/internal/store/devices_login_swap_test.go @@ -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) +}