package device import ( "encoding/json" "os" "path/filepath" "strings" "testing" ) func TestIndexDevices(t *testing.T) { tests := []struct { name string devices []Device want []string }{ { name: "empty", devices: nil, want: nil, }, { name: "single", devices: []Device{{Path: "/dev/hidraw7", VendorID: 0x36b0, ProductID: 0x309f}}, want: []string{"/dev/hidraw7"}, }, { name: "sorted by vendor then product then path", devices: []Device{ {Path: "/dev/hidraw9", VendorID: 0x9999, ProductID: 0x0001}, {Path: "/dev/hidraw5", VendorID: 0x1111, ProductID: 0x0002}, {Path: "/dev/hidraw2", VendorID: 0x1111, ProductID: 0x0001}, }, want: []string{"/dev/hidraw2", "/dev/hidraw5", "/dev/hidraw9"}, }, { name: "identical vendor and product sorted by path", devices: []Device{ {Path: "/dev/hidrawB", VendorID: 0x36b0, ProductID: 0x309f}, {Path: "/dev/hidrawA", VendorID: 0x36b0, ProductID: 0x309f}, }, want: []string{"/dev/hidrawA", "/dev/hidrawB"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := indexDevices(tt.devices) if len(got) != len(tt.want) { t.Fatalf("indexDevices() returned %d devices, want %d", len(got), len(tt.want)) } for i, wantPath := range tt.want { if got[i].Path != wantPath { t.Errorf("indexDevices()[%d].Path = %q, want %q", i, got[i].Path, wantPath) } if got[i].Index != i+1 { t.Errorf("indexDevices()[%d].Index = %d, want %d", i, got[i].Index, i+1) } } }) } } func TestIndexDevicesDoesNotMutateInput(t *testing.T) { input := []Device{ {Path: "/dev/hidraw9", VendorID: 0x9999, ProductID: 0x0001}, {Path: "/dev/hidraw2", VendorID: 0x1111, ProductID: 0x0001}, } indexDevices(input) if input[0].Path != "/dev/hidraw9" { t.Errorf("indexDevices() mutated input: input[0].Path = %q, want %q", input[0].Path, "/dev/hidraw9") } if input[0].Index != 0 { t.Errorf("indexDevices() mutated input: input[0].Index = %d, want 0", input[0].Index) } } func TestDeviceJSONFieldNames(t *testing.T) { dev := Device{ Index: 1, Path: "/dev/hidraw7", VendorID: 0x36b0, ProductID: 0x309f, Name: "Wobkey Impact 80", Known: true, } data, err := json.Marshal(dev) if err != nil { t.Fatalf("Marshal() returned error: %v", err) } var got map[string]any if err := json.Unmarshal(data, &got); err != nil { t.Fatalf("Unmarshal() returned error: %v", err) } for _, field := range []string{"index", "path", "vendorId", "productId", "name", "known"} { if _, ok := got[field]; !ok { t.Errorf("Device JSON = %s, missing field %q", data, field) } } if _, ok := got["supported"]; ok { t.Errorf("Device JSON = %s, must not contain the misleading field \"supported\"", data) } } func TestDeviceJSONOmitsUnnamedName(t *testing.T) { data, err := json.Marshal(Device{Index: 2, Path: "/dev/hidraw9", Known: false}) if err != nil { t.Fatalf("Marshal() returned error: %v", err) } if strings.Contains(string(data), `"name"`) { t.Errorf("Device JSON = %s, want the name omitted for an unknown keyboard", data) } } func TestKeyboardIgnoresUnknownFields(t *testing.T) { tmpDir := t.TempDir() tmpFile := filepath.Join(tmpDir, "keyboards.json") // Entries may still carry fields the tool does not use; loading must // succeed and yield only the fields the code actually reads. testData := `[ {"name":"Impact 80","vendorId":14000,"productId":12447, "protocol":"via","viaVersion":3,"ledLayout":"rgblight","futureField":"ignored"} ]` if err := os.WriteFile(tmpFile, []byte(testData), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } origFind := findKeyboardsJSON findKeyboardsJSON = func() (string, error) { return tmpFile, nil } defer func() { findKeyboardsJSON = origFind }() keyboards, err := LoadKeyboards() if err != nil { t.Fatalf("LoadKeyboards() returned error: %v", err) } if len(keyboards) != 1 { t.Fatalf("LoadKeyboards() returned %d keyboards, want 1", len(keyboards)) } if keyboards[0].Name != "Impact 80" { t.Errorf("Name = %q, want %q", keyboards[0].Name, "Impact 80") } } func TestKeyboardJSONOmitsRemovedFields(t *testing.T) { data, err := json.Marshal(Keyboard{Name: "Impact 80", VendorID: 0x36b0, ProductID: 0x309f}) if err != nil { t.Fatalf("Marshal() returned error: %v", err) } for _, dead := range []string{"protocol", "viaVersion", "ledLayout"} { if strings.Contains(string(data), dead) { t.Errorf("Keyboard JSON = %s, want no %q field", data, dead) } } } func TestLoadKeyboards(t *testing.T) { tmpDir := t.TempDir() tmpFile := filepath.Join(tmpDir, "keyboards.json") testData := `[ {"name": "Test Keyboard 1", "vendorId": 22222, "productId": 1, "protocol": "via"}, {"name": "Test Keyboard 2", "vendorId": 33333, "productId": 2, "protocol": "viapro"} ]` if err := os.WriteFile(tmpFile, []byte(testData), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } origFind := findKeyboardsJSON findKeyboardsJSON = func() (string, error) { return tmpFile, nil } defer func() { findKeyboardsJSON = origFind }() keyboards, err := LoadKeyboards() if err != nil { t.Fatalf("LoadKeyboards() returned error: %v", err) } if len(keyboards) != 2 { t.Errorf("LoadKeyboards() returned %d keyboards, want 2", len(keyboards)) } if keyboards[0].Name != "Test Keyboard 1" { t.Errorf("First keyboard name = %q, want %q", keyboards[0].Name, "Test Keyboard 1") } if keyboards[0].VendorID != 22222 { t.Errorf("First keyboard VID = %d, want %d", keyboards[0].VendorID, 22222) } if keyboards[1].ProductID != 2 { t.Errorf("Second keyboard PID = %d, want %d", keyboards[1].ProductID, 2) } } func TestLoadKeyboardsInvalid(t *testing.T) { tmpDir := t.TempDir() tmpFile := filepath.Join(tmpDir, "keyboards.json") invalidData := `[this is not valid json` if err := os.WriteFile(tmpFile, []byte(invalidData), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } origFind := findKeyboardsJSON findKeyboardsJSON = func() (string, error) { return tmpFile, nil } defer func() { findKeyboardsJSON = origFind }() _, err := LoadKeyboards() if err == nil { t.Error("LoadKeyboards() expected error for invalid JSON, got nil") } } func TestLoadKeyboardsEmptyFile(t *testing.T) { tmpDir := t.TempDir() tmpFile := filepath.Join(tmpDir, "keyboards.json") if err := os.WriteFile(tmpFile, []byte("[]"), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } origFind := findKeyboardsJSON findKeyboardsJSON = func() (string, error) { return tmpFile, nil } defer func() { findKeyboardsJSON = origFind }() keyboards, err := LoadKeyboards() if err != nil { t.Fatalf("LoadKeyboards() returned error: %v", err) } if len(keyboards) != 0 { t.Errorf("LoadKeyboards() returned %d keyboards, want 0", len(keyboards)) } }