bluez and bluez-utils, and bluetooth.service running and enabled. On both workstations bluez is installed only as a dependency; declaring it keeps a clean-up from taking it. Nine Go tools over bluetoothctl: controller, power, devices with battery where reported, a bounded scan, connect, disconnect, trust, pair (an agent that confirms nothing, for headphones) and remove. An act whose output says it failed is an error whatever the exit status, and one bluez refuses the account is repeated through sudo -n.
164 lines
6.3 KiB
Go
164 lines
6.3 KiB
Go
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)
|
|
}
|
|
}
|