195 lines
6.9 KiB
Go
195 lines
6.9 KiB
Go
package claude
|
|
|
|
import (
|
|
"reflect"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func TestGenerateDeviceIDPool(t *testing.T) {
|
|
deviceIDs, errGenerate := GenerateDeviceIDPool()
|
|
if errGenerate != nil {
|
|
t.Fatalf("GenerateDeviceIDPool() error = %v", errGenerate)
|
|
}
|
|
if len(deviceIDs) != ClaudeDevicePoolSize {
|
|
t.Fatalf("device pool length = %d, want %d", len(deviceIDs), ClaudeDevicePoolSize)
|
|
}
|
|
seen := make(map[string]struct{}, len(deviceIDs))
|
|
for _, deviceID := range deviceIDs {
|
|
if !ValidDeviceID(deviceID) {
|
|
t.Fatalf("device ID = %q, want 64 lowercase hex", deviceID)
|
|
}
|
|
if _, exists := seen[deviceID]; exists {
|
|
t.Fatalf("duplicate device ID %q", deviceID)
|
|
}
|
|
seen[deviceID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// TestReadDeviceIDPoolReturnsDefensiveCopy pins that neither side of the device
|
|
// pool accessors hands out the live stored slice. A caller mutating a result must
|
|
// never be able to rewrite credential identity outside the device pool lock.
|
|
func TestReadDeviceIDPoolReturnsDefensiveCopy(t *testing.T) {
|
|
metadata := map[string]any{}
|
|
input := []string{"device-a", "device-b", "device-c"}
|
|
StoreDeviceIDPool(&metadata, input)
|
|
|
|
// Write side: mutating the caller's input must not affect stored state.
|
|
input[0] = "mutated-input"
|
|
stored, ok := ReadDeviceIDPool(&metadata).([]string)
|
|
if !ok {
|
|
t.Fatalf("ReadDeviceIDPool() type = %T, want []string", ReadDeviceIDPool(&metadata))
|
|
}
|
|
if stored[0] != "device-a" {
|
|
t.Fatalf("stored[0] = %q, want %q; write side is not defensive", stored[0], "device-a")
|
|
}
|
|
|
|
// Read side: mutating the returned slice must not affect stored state.
|
|
stored[0] = "hijacked-device-id"
|
|
reread, _ := ReadDeviceIDPool(&metadata).([]string)
|
|
if reread[0] != "device-a" {
|
|
t.Fatalf("stored[0] = %q after mutating the read result, want %q", reread[0], "device-a")
|
|
}
|
|
|
|
// A []any pool (as produced by JSON unmarshalling) must be copied too.
|
|
jsonMetadata := map[string]any{ClaudeDeviceIDsMetadataKey: []any{"json-a", "json-b"}}
|
|
jsonStored, ok := ReadDeviceIDPool(&jsonMetadata).([]any)
|
|
if !ok {
|
|
t.Fatalf("ReadDeviceIDPool() type = %T, want []any", ReadDeviceIDPool(&jsonMetadata))
|
|
}
|
|
jsonStored[0] = "hijacked"
|
|
jsonReread, _ := ReadDeviceIDPool(&jsonMetadata).([]any)
|
|
if jsonReread[0] != "json-a" {
|
|
t.Fatalf("stored[0] = %v after mutating the read result, want %q", jsonReread[0], "json-a")
|
|
}
|
|
}
|
|
|
|
func TestEnsureDeviceIDPoolRepairsAndStabilizesCredentialMetadata(t *testing.T) {
|
|
const first = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
metadata := map[string]any{
|
|
ClaudeDeviceIDsMetadataKey: []any{
|
|
first,
|
|
first,
|
|
"INVALID",
|
|
},
|
|
}
|
|
|
|
deviceIDs, changed, errEnsure := EnsureDeviceIDPool(metadata)
|
|
if errEnsure != nil {
|
|
t.Fatalf("EnsureDeviceIDPool() error = %v", errEnsure)
|
|
}
|
|
if !changed {
|
|
t.Fatal("EnsureDeviceIDPool() changed = false, want true")
|
|
}
|
|
if len(deviceIDs) != ClaudeDevicePoolSize || deviceIDs[0] != first {
|
|
t.Fatalf("device IDs = %#v, want repaired single-entry pool preserving first", deviceIDs)
|
|
}
|
|
|
|
second, changedAgain, errEnsureAgain := EnsureDeviceIDPool(metadata)
|
|
if errEnsureAgain != nil {
|
|
t.Fatalf("EnsureDeviceIDPool() second error = %v", errEnsureAgain)
|
|
}
|
|
if changedAgain {
|
|
t.Fatal("EnsureDeviceIDPool() second changed = true, want stable canonical pool")
|
|
}
|
|
if !reflect.DeepEqual(second, deviceIDs) {
|
|
t.Fatalf("second device IDs = %#v, want %#v", second, deviceIDs)
|
|
}
|
|
|
|
second[0] = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
stored := metadata[ClaudeDeviceIDsMetadataKey].([]string)
|
|
if stored[0] != first {
|
|
t.Fatal("returned pool aliases credential metadata")
|
|
}
|
|
}
|
|
|
|
func TestEnsureDeviceIDPoolCanonicalizesSingleDevice(t *testing.T) {
|
|
const canonical = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
metadata := map[string]any{ClaudeDeviceIDsMetadataKey: []any{" AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA "}}
|
|
deviceIDs, changed, errEnsure := EnsureDeviceIDPool(metadata)
|
|
if errEnsure != nil {
|
|
t.Fatalf("EnsureDeviceIDPool() error = %v", errEnsure)
|
|
}
|
|
if !changed || len(deviceIDs) != 1 || deviceIDs[0] != canonical {
|
|
t.Fatalf("EnsureDeviceIDPool() = %#v, changed=%v; want canonical single device", deviceIDs, changed)
|
|
}
|
|
if !HasCanonicalDeviceIDPool(metadata[ClaudeDeviceIDsMetadataKey]) {
|
|
t.Fatalf("stored device pool = %#v, want canonical", metadata[ClaudeDeviceIDsMetadataKey])
|
|
}
|
|
}
|
|
|
|
func TestEnsureDeviceIDPoolMigratesFiveSlotsToOne(t *testing.T) {
|
|
metadata := map[string]any{ClaudeDeviceIDsMetadataKey: []string{
|
|
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
"1111111111111111111111111111111111111111111111111111111111111111",
|
|
"2222222222222222222222222222222222222222222222222222222222222222",
|
|
"3333333333333333333333333333333333333333333333333333333333333333",
|
|
"4444444444444444444444444444444444444444444444444444444444444444",
|
|
}}
|
|
|
|
deviceIDs, changed, errEnsure := EnsureDeviceIDPool(metadata)
|
|
if errEnsure != nil {
|
|
t.Fatalf("EnsureDeviceIDPool() error = %v", errEnsure)
|
|
}
|
|
if !changed {
|
|
t.Fatal("EnsureDeviceIDPool() changed = false, want five-slot migration")
|
|
}
|
|
want := []string{"0000000000000000000000000000000000000000000000000000000000000000"}
|
|
if !reflect.DeepEqual(deviceIDs, want) {
|
|
t.Fatalf("device IDs = %#v, want %#v", deviceIDs, want)
|
|
}
|
|
if stored, ok := metadata[ClaudeDeviceIDsMetadataKey].([]string); !ok || !reflect.DeepEqual(stored, want) {
|
|
t.Fatalf("stored device IDs = %#v, want %#v", metadata[ClaudeDeviceIDsMetadataKey], want)
|
|
}
|
|
}
|
|
|
|
func TestEnsureDeviceIDPoolConcurrentInitialization(t *testing.T) {
|
|
metadata := make(map[string]any)
|
|
const workers = 20
|
|
results := make(chan []string, workers)
|
|
errors := make(chan error, workers)
|
|
var group sync.WaitGroup
|
|
for range workers {
|
|
group.Go(func() {
|
|
deviceIDs, _, errEnsure := EnsureDeviceIDPool(metadata)
|
|
results <- deviceIDs
|
|
errors <- errEnsure
|
|
})
|
|
}
|
|
group.Wait()
|
|
close(results)
|
|
close(errors)
|
|
|
|
for errEnsure := range errors {
|
|
if errEnsure != nil {
|
|
t.Fatalf("EnsureDeviceIDPool() concurrent error = %v", errEnsure)
|
|
}
|
|
}
|
|
stored := NormalizeDeviceIDPool(metadata[ClaudeDeviceIDsMetadataKey])
|
|
if len(stored) != ClaudeDevicePoolSize {
|
|
t.Fatalf("stored device pool length = %d, want %d", len(stored), ClaudeDevicePoolSize)
|
|
}
|
|
for result := range results {
|
|
if !reflect.DeepEqual(result, stored) {
|
|
t.Fatalf("concurrent result = %#v, want %#v", result, stored)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSelectDeviceIDUsesOneDeviceAcrossSessions(t *testing.T) {
|
|
deviceIDs := []string{
|
|
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
}
|
|
|
|
first, errFirst := SelectDeviceID(deviceIDs, "11111111-2222-4333-8444-555555555555")
|
|
if errFirst != nil {
|
|
t.Fatalf("SelectDeviceID() error = %v", errFirst)
|
|
}
|
|
second, errSecond := SelectDeviceID(deviceIDs, "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee")
|
|
if errSecond != nil {
|
|
t.Fatalf("SelectDeviceID() second error = %v", errSecond)
|
|
}
|
|
if first != second || first != deviceIDs[0] {
|
|
t.Fatalf("single device selection = %q then %q, want %q", first, second, deviceIDs[0])
|
|
}
|
|
}
|