package main import ( "strings" "testing" ) func TestTheManifestIsTheStackItsToolsAndTheDaemon(t *testing.T) { m := readManifest(t) holdsTheBundle(t, m, "bluetooth") if got := strings.Join(m.packages(), ","); got != "bluez,bluez-utils" { t.Errorf("packages %s: the applet and the TUI are the operator's", got) } s := m.services()["bluetooth.service"] if s == nil || s["state"] != "running" || s["boot"] != "enabled" { t.Errorf("%v", s) } if len(m.Resources) != 3 { t.Errorf("no configuration file: /etc/bluetooth/main.conf is the package's, unchanged on both workstations: %v", m.Resources) } } const show = "Controller 4C:82:A9:97:01:8E (public)\n\tName: g14\n\tAlias: g14\n\tPowered: yes\n\tPowerState: on\n\tDiscoverable: no\n\tPairable: yes\n\tUUID: Headset (00001108-0000-1000-8000-00805f9b34fb)\n\tDiscovering: no\n" func headphones(connected bool) string { c := "no" extra := "" if connected { c, extra = "yes", "\tBattery Percentage: 0x50 (80)\n" } return "Device 80:99:E7:C2:29:DA (public)\n\tName: WH-1000XM4\n\tAlias: WH-1000XM4\n\tIcon: audio-headset\n\tPaired: yes\n\tBonded: yes\n\tTrusted: yes\n\tBlocked: no\n\tConnected: " + c + "\n" + extra + "\tUUID: Headset (00001108-0000-1000-8000-00805f9b34fb)\n" } func TestControllerReadsShow(t *testing.T) { using(t, func(string, Cmd) Result { return ok("\x1b[0;94m" + show) }) c, err := Controller() if err != nil || c.Address != "4C:82:A9:97:01:8E" || c.Name != "g14" || !c.Powered || c.Discoverable || !c.Pairable { t.Fatalf("%+v %v", c, err) } using(t, func(string, Cmd) Result { return ok("No default controller available\n") }) if _, err := Controller(); err == nil || !strings.Contains(err.Error(), "no Bluetooth controller") { t.Fatalf("%v", err) } } func TestDevicesAskEachOneAndReportBatteryWhereGiven(t *testing.T) { f := using(t, func(line string, c Cmd) Result { switch line { case "bluetoothctl devices Paired": return ok("Device 80:99:E7:C2:29:DA WH-1000XM4\nDevice 2C:41:A1:E4:EC:86 Earmuffs\n") case "bluetoothctl info 80:99:E7:C2:29:DA": return ok(headphones(true)) } return ok("Device 2C:41:A1:E4:EC:86 (public)\n\tName: Earmuffs\n\tPaired: yes\n\tConnected: no\n") }) got, err := Devices("paired") devices := got["devices"].([]Device) if err != nil || got["count"] != 2 || !devices[0].Connected || devices[0].Battery == nil || *devices[0].Battery != 80 || devices[1].Battery != nil { t.Fatalf("%+v %v", got, err) } if f.lines()[0] != "bluetoothctl devices Paired" { t.Errorf("%v", f.lines()) } if _, err := Devices("nearby"); err == nil { t.Error("an unknown which") } } func TestAnActThatFailsIsAnErrorWhateverTheExitStatus(t *testing.T) { using(t, func(line string, c Cmd) Result { return ok("Attempting to connect to 80:99:E7:C2:29:DA\nFailed to connect: org.bluez.Error.Failed br-connection-page-timeout\n") }) if _, err := Act("connect", "80:99:E7:C2:29:DA"); err == nil || !strings.Contains(err.Error(), "page-timeout") { t.Fatalf("%v", err) } using(t, func(string, Cmd) Result { return Result{Status: 1, Stdout: "Device 00:11:22:33:44:55 not available\n"} }) if _, err := Act("trust", "00:11:22:33:44:55"); err == nil || !strings.Contains(err.Error(), "not available") { t.Fatalf("%v", err) } } func TestConnectWaitsWithBluetoothctlsOwnTimeoutAndAnswersTheState(t *testing.T) { f := using(t, func(line string, c Cmd) Result { if strings.Contains(line, "connect") { return ok("Attempting to connect\n[CHG] Device Connected: yes\nConnection successful\n") } return ok(headphones(true)) }) got, err := Act("connect", "80:99:E7:C2:29:DA") if err != nil || !got["device"].(Device).Connected { t.Fatalf("%v %v", got, err) } if f.lines()[0] != "bluetoothctl --timeout 15 connect 80:99:E7:C2:29:DA" || f.asked[0].Timeout != CallTimeout { t.Errorf("%v", f.lines()) } } func TestAnActBluezRefusesTheAccountIsRetriedThroughSudo(t *testing.T) { f := using(t, func(line string, c Cmd) Result { if strings.HasPrefix(line, "sudo") { return ok("Changing power off succeeded\n" + show) } return ok("Failed to set power off: org.freedesktop.DBus.Error.AccessDenied\n") }) if _, err := Power(false); err != nil { t.Fatal(err) } if l := f.lines(); l[0] != "bluetoothctl power off" || l[1] != "sudo -n bluetoothctl power off" { t.Errorf("%v", l) } } func TestScanIsBoundedAndAnswersUnpairedDevicesStrongestFirst(t *testing.T) { f := using(t, func(line string, c Cmd) Result { switch { case strings.Contains(line, "scan on"): return ok("Discovery started\n[NEW] Device AA:BB:CC:DD:EE:01 Speaker\n") case line == "bluetoothctl devices": return ok("Device 80:99:E7:C2:29:DA WH-1000XM4\nDevice AA:BB:CC:DD:EE:01 Speaker\nDevice AA:BB:CC:DD:EE:02 Phone\nDevice AA:BB:CC:DD:EE:03 Gone\n") case strings.HasSuffix(line, "EE:01"): return ok("Device AA:BB:CC:DD:EE:01\n\tName: Speaker\n\tPaired: no\n\tRSSI: 0xffffffc4 (-60)\n") case strings.HasSuffix(line, "EE:02"): return ok("Device AA:BB:CC:DD:EE:02\n\tName: Phone\n\tPaired: no\n\tRSSI: -40\n") case strings.HasSuffix(line, "EE:03"): return Result{Status: 1, Stdout: "Device AA:BB:CC:DD:EE:03 not available\n"} } return ok(headphones(false)) }) got, err := Scan(8) found := got["found"].([]Device) if err != nil || len(found) != 2 || found[0].Name != "Phone" || *found[1].RSSI != -60 { t.Fatalf("%+v %v", got, err) } if f.lines()[0] != "bluetoothctl --timeout 8 scan on" || f.asked[0].Timeout.Seconds() != 12 { t.Errorf("%v %v", f.lines()[0], f.asked[0].Timeout) } } func TestPairUsesAnAgentThatConfirmsNothingAndThenTrusts(t *testing.T) { f := using(t, func(line string, c Cmd) Result { if strings.Contains(line, " pair ") { return ok("Pairing successful\n") } return ok(headphones(false)) }) if _, err := Pair("80:99:e7:c2:29:da"); err != nil { t.Fatal(err) } if l := f.lines(); l[0] != "bluetoothctl --agent NoInputNoOutput --timeout 15 pair 80:99:e7:c2:29:da" || l[1] != "bluetoothctl trust 80:99:e7:c2:29:da" { t.Errorf("%v", l) } } func TestAnAddressIsSixHexPairs(t *testing.T) { for _, bad := range []string{"", "80:99:E7:C2:29", "80:99:E7:C2:29:DA; rm", "--help", "GG:99:E7:C2:29:DA"} { if _, err := addressOf(map[string]any{"address": bad}); err == nil { t.Errorf("%q accepted", bad) } } if a, err := addressOf(map[string]any{"address": "80:99:e7:c2:29:da"}); err != nil || a != "80:99:E7:C2:29:DA" { t.Errorf("%s %v", a, err) } }