package diag import ( "encoding/json" "strings" "testing" "github.com/duffy/usb-server/internal/usb" ) func TestCollectProducesUsableReport(t *testing.T) { report := Collect("test") if report.Generated == "" { t.Error("no timestamp") } if report.Tool.OS == "" || report.Tool.Arch == "" { t.Error("platform not recorded") } if report.System.Hostname == "" { t.Error("hostname not recorded") } // A report that says nothing about either capability is useless: the // whole point is answering whether this machine can share or use. if report.Sharing.Mechanism == "" && report.Sharing.Reason == "" { t.Error("sharing capability has neither a mechanism nor a reason") } if report.Using.Mechanism == "" && report.Using.Reason == "" { t.Error("using capability has neither a mechanism nor a reason") } } func TestReportRoundTripsThroughJSON(t *testing.T) { report := Collect("test") data, err := report.JSON() if err != nil { t.Fatalf("JSON: %v", err) } var decoded Report if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("decoding the report we just produced: %v", err) } if decoded.Tool.OS != report.Tool.OS { t.Errorf("OS survived as %q, want %q", decoded.Tool.OS, report.Tool.OS) } if len(decoded.Devices) != len(report.Devices) { t.Errorf("device count changed: %d -> %d", len(report.Devices), len(decoded.Devices)) } } // A failed check without a fix leaves the reader stuck, which defeats the // purpose of the report. func TestFailedChecksSuggestAFix(t *testing.T) { report := Collect("test") for _, check := range report.Checks { if !check.Passed && check.Fix == "" && check.Detail == "" { t.Errorf("check %q failed but says nothing about why or what to do", check.Name) } } } func TestStringOutputMentionsEverything(t *testing.T) { report := &Report{ Generated: "2026-01-01T00:00:00Z", Tool: ToolInfo{OS: "linux", Arch: "amd64"}, System: SystemInfo{Hostname: "testhost"}, Sharing: Capability{Available: true, Mechanism: "usbdevfs"}, Using: Capability{Available: false, Reason: "vhci-hcd is not loaded"}, Devices: []DeviceInfo{{ BusID: "1-2", VendorID: "046d", ProductID: "c52b", Name: "Logitech Receiver", Shareable: true, Endpoints: []EndpointInfo{{ Address: "0x81", Direction: "IN", TransferType: "interrupt", MaxPacket: 8, Interval: 10, }}, }}, Checks: []Check{ {Name: "vhci-hcd module", Passed: false, Detail: "not loaded", Fix: "sudo modprobe vhci-hcd"}, }, } out := report.String() for _, want := range []string{ "testhost", "usbdevfs", "vhci-hcd is not loaded", "1-2", "046d", "c52b", "Logitech Receiver", "0x81", "interrupt", "sudo modprobe vhci-hcd", } { if !strings.Contains(out, want) { t.Errorf("output does not mention %q", want) } } } func TestTransferTypeNames(t *testing.T) { tests := []struct { input uint8 want string }{ {usb.TransferTypeControl, "control"}, {usb.TransferTypeIsochronous, "isochronous"}, {usb.TransferTypeBulk, "bulk"}, {usb.TransferTypeInterrupt, "interrupt"}, {99, "unknown(99)"}, } for _, tt := range tests { if got := transferTypeName(tt.input); got != tt.want { t.Errorf("transferTypeName(%d) = %q, want %q", tt.input, got, tt.want) } } } func TestEndpointInfoReportsDirection(t *testing.T) { in := endpointInfo(usb.Endpoint{ Address: 0x81, TransferType: usb.TransferTypeInterrupt, MaxPacketSize: 8, Interval: 10, }) if in.Direction != "IN" { t.Errorf("0x81 reported as %s, want IN", in.Direction) } if in.Address != "0x81" { t.Errorf("address rendered as %q", in.Address) } out := endpointInfo(usb.Endpoint{Address: 0x02, TransferType: usb.TransferTypeBulk}) if out.Direction != "OUT" { t.Errorf("0x02 reported as %s, want OUT", out.Direction) } } func TestDiagURLAcceptsEveryRelayForm(t *testing.T) { tests := []struct { relay string want string }{ {"ws://relay:8443", "http://relay:8443/diag/abc"}, {"wss://relay.example.com", "https://relay.example.com/diag/abc"}, {"http://relay:8443", "http://relay:8443/diag/abc"}, {"https://relay:8443", "https://relay:8443/diag/abc"}, {"relay:8443", "http://relay:8443/diag/abc"}, {"ws://relay:8443/ws", "http://relay:8443/diag/abc"}, {"ws://relay:8443/", "http://relay:8443/diag/abc"}, } for _, tt := range tests { got, err := DiagURL(tt.relay, "abc") if err != nil { t.Errorf("DiagURL(%q): %v", tt.relay, err) continue } if got != tt.want { t.Errorf("DiagURL(%q) = %q, want %q", tt.relay, got, tt.want) } } } func TestDiagURLRejectsBadIDs(t *testing.T) { for _, id := range []string{"", "a/b", "a?b", "a#b"} { if _, err := DiagURL("ws://relay:8443", id); err == nil { t.Errorf("DiagURL accepted the ID %q", id) } } } func TestTruncate(t *testing.T) { tests := []struct { in string max int want string }{ {"short", 10, "short"}, {"exactly-10", 10, "exactly-10"}, {"this is far too long", 10, "this is..."}, {"abc", 2, "ab"}, } for _, tt := range tests { if got := truncate(tt.in, tt.max); got != tt.want { t.Errorf("truncate(%q, %d) = %q, want %q", tt.in, tt.max, got, tt.want) } } }