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) }