diff --git a/README.md b/README.md index 8ca2de2..7cf9c6b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Superstack CLI `superstack` is the command line interface to Superstack: log in, create -fleets, pair devices, and manage who can reach them. It is a single static +fleets, manage devices, and control who can reach them. It is a single static binary for managing Superstack from a terminal. Uploading Lua code and streaming logs are not available yet. diff --git a/internal/account/account.go b/internal/account/account.go index bed3d1f..3da20f6 100644 --- a/internal/account/account.go +++ b/internal/account/account.go @@ -60,7 +60,7 @@ func Balance(invocation api.Invocation, arguments []string) error { balances := []api.BalanceEntry{} for _, balance := range fetched { - if chosenFleetId == 0 || balance.Fleet == chosenFleetId { + if chosenFleetId == 0 || balance.FleetId == chosenFleetId { balances = append(balances, balance) } } @@ -87,7 +87,7 @@ func Balance(invocation api.Invocation, arguments []string) error { amountValues := make([]string, len(balances)) for index, balance := range balances { - name, known := fleetNames[balance.Fleet] + name, known := fleetNames[balance.FleetId] if !known { name = "-" @@ -97,14 +97,14 @@ func Balance(invocation api.Invocation, arguments []string) error { nameValues[index] = api.Printable(name) amountValues[index] = api.Printable(formatted) - idWidth = max(idWidth, len(strconv.FormatInt(balance.Fleet, 10))) + idWidth = max(idWidth, len(strconv.FormatInt(balance.FleetId, 10))) nameWidth = max(nameWidth, len(nameValues[index])) } fmt.Fprintf(invocation.Out, "%-*s %-*s %s\n", idWidth, "ID", nameWidth, "NAME", "BALANCE") for index, balance := range balances { - fmt.Fprintf(invocation.Out, "%-*d %-*s %s\n", idWidth, balance.Fleet, nameWidth, nameValues[index], amountValues[index]) + fmt.Fprintf(invocation.Out, "%-*d %-*s %s\n", idWidth, balance.FleetId, nameWidth, nameValues[index], amountValues[index]) } return nil @@ -122,7 +122,7 @@ func TopUp(invocation api.Invocation, arguments []string) error { } request, err := api.AuthenticatedRequest(invocation, http.MethodPost, - "/fleets/"+strconv.FormatInt(fleetId, 10)+"/topup", nil) + "/fleets/"+strconv.FormatInt(fleetId, 10)+"/top-ups", nil) if err != nil { return err diff --git a/internal/account/account_test.go b/internal/account/account_test.go index 92c48ca..460ed71 100644 --- a/internal/account/account_test.go +++ b/internal/account/account_test.go @@ -28,14 +28,14 @@ func TestAccountBalance(t *testing.T) { name: "every fleet", arguments: []string{}, fleets: `[{"id":1,"name":"crew","owner":true},{"id":2,"name":"pilot","owner":false}]`, - balances: `[{"fleet":1,"balance":"15.000000","currency":"eur"},{"fleet":2,"balance":"0","currency":"eur"}]`, + balances: `[{"fleet_id":1,"balance":"15.000000","currency":"eur"},{"fleet_id":2,"balance":"0","currency":"eur"}]`, wantLines: []string{"ID", "NAME", "BALANCE", "crew", "€15.00", "pilot", "€0.00"}, }, { name: "one fleet", arguments: []string{"2"}, fleets: `[{"id":1,"name":"crew","owner":true},{"id":2,"name":"pilot","owner":false}]`, - balances: `[{"fleet":1,"balance":"15.000000","currency":"eur"},{"fleet":2,"balance":"0","currency":"eur"}]`, + balances: `[{"fleet_id":1,"balance":"15.000000","currency":"eur"},{"fleet_id":2,"balance":"0","currency":"eur"}]`, wantLines: []string{"pilot", "€0.00"}, wantAbsent: []string{"crew"}, }, @@ -43,7 +43,7 @@ func TestAccountBalance(t *testing.T) { name: "a fleet name with control characters is escaped", arguments: []string{}, fleets: `[{"id":1,"name":"\u001b[2Kquiet","owner":true}]`, - balances: `[{"fleet":1,"balance":"15.000000","currency":"eur"}]`, + balances: `[{"fleet_id":1,"balance":"15.000000","currency":"eur"}]`, wantLines: []string{`\x1b[2Kquiet`}, wantAbsent: []string{"\x1b"}, }, @@ -51,15 +51,15 @@ func TestAccountBalance(t *testing.T) { name: "machine readable", arguments: []string{"--json"}, fleets: `[{"id":1,"name":"crew","owner":true}]`, - balances: `[{"fleet":1,"balance":"15.000000","currency":"eur"}]`, - wantExact: `[{"fleet":1,"balance":"15.000000","currency":"eur"}]` + "\n", + balances: `[{"fleet_id":1,"balance":"15.000000","currency":"eur"}]`, + wantExact: `[{"fleet_id":1,"balance":"15.000000","currency":"eur"}]` + "\n", }, { name: "machine readable for one fleet", arguments: []string{"2", "--json"}, fleets: `[{"id":1,"name":"crew","owner":true},{"id":2,"name":"pilot","owner":false}]`, - balances: `[{"fleet":1,"balance":"15.000000","currency":"eur"},{"fleet":2,"balance":"0","currency":"eur"}]`, - wantExact: `[{"fleet":2,"balance":"0","currency":"eur"}]` + "\n", + balances: `[{"fleet_id":1,"balance":"15.000000","currency":"eur"},{"fleet_id":2,"balance":"0","currency":"eur"}]`, + wantExact: `[{"fleet_id":2,"balance":"0","currency":"eur"}]` + "\n", }, { name: "machine readable with no fleets", @@ -86,7 +86,7 @@ func TestAccountBalance(t *testing.T) { name: "a fleet the list does not name", arguments: []string{}, fleets: `[{"id":1,"name":"crew","owner":true}]`, - balances: `[{"fleet":99,"balance":"15.000000","currency":"eur"}]`, + balances: `[{"fleet_id":99,"balance":"15.000000","currency":"eur"}]`, wantLines: []string{"99 -"}, }, { @@ -184,13 +184,13 @@ func TestAccountTopUp(t *testing.T) { name: "the top-up page opened on enter", arguments: []string{"3"}, stdin: "\n", - wantPath: "/fleets/3/topup", + wantPath: "/fleets/3/top-ups", wantBrowser: true, }, { name: "the top-up page left alone", arguments: []string{"3"}, - wantPath: "/fleets/3/topup", + wantPath: "/fleets/3/top-ups", }, { name: "no fleet id", @@ -210,14 +210,14 @@ func TestAccountTopUp(t *testing.T) { { name: "response has no url", arguments: []string{"3"}, - wantPath: "/fleets/3/topup", + wantPath: "/fleets/3/top-ups", emptyBody: true, wantError: "could not open the top-up page, try again", }, { name: "the server refuses", arguments: []string{"9"}, - wantPath: "/fleets/9/topup", + wantPath: "/fleets/9/top-ups", refusal: "no such fleet", wantError: "no such fleet", }, @@ -227,7 +227,7 @@ func TestAccountTopUp(t *testing.T) { t.Run(test.name, func(t *testing.T) { mux := http.NewServeMux() - mux.HandleFunc("POST /fleets/{id}/topup", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("POST /fleets/{id}/top-ups", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != test.wantPath { t.Errorf("the request went to %s, want %s", r.URL.Path, test.wantPath) } diff --git a/internal/api/api_test.go b/internal/api/api_test.go index 684a84b..42e99f4 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -210,7 +210,7 @@ func TestFetchFleetKeysFailures(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { mux := http.NewServeMux() - mux.HandleFunc("GET /keys", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("GET /fleet-keys", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(test.status) fmt.Fprint(w, test.body) }) diff --git a/internal/api/balances.go b/internal/api/balances.go index 24479d2..b533b78 100644 --- a/internal/api/balances.go +++ b/internal/api/balances.go @@ -8,7 +8,7 @@ import ( ) type BalanceEntry struct { - Fleet int64 `json:"fleet"` + FleetId int64 `json:"fleet_id"` Balance string `json:"balance"` Currency string `json:"currency"` } diff --git a/internal/api/devices.go b/internal/api/devices.go index 82804d4..a6f849b 100644 --- a/internal/api/devices.go +++ b/internal/api/devices.go @@ -6,13 +6,10 @@ import ( ) type DeviceEntry struct { - Imei string `json:"imei"` - Name *string `json:"name"` - FleetId int64 `json:"fleet_id"` - LastSeenAt *string `json:"last_seen_at"` - RunState *int `json:"run_state"` - StorageUsed *int64 `json:"storage_used"` - StorageTotal *int64 `json:"storage_total"` + Imei string `json:"imei"` + Name *string `json:"name"` + FleetId int64 `json:"fleet_id"` + LastSeenAt *string `json:"last_seen_at"` } func FetchDevices(invocation Invocation) ([]DeviceEntry, error) { @@ -34,35 +31,13 @@ func FetchDevices(invocation Invocation) ([]DeviceEntry, error) { return nil, ServerError(response) } - serverDevices := []struct { - Imei string `json:"imei"` - Name *string `json:"name"` - FleetId int64 `json:"fleet_id"` - LastSeenAt *string `json:"last_seen_at"` - RunState *int `json:"reported_state"` - StorageUsed *int64 `json:"storage_used"` - StorageTotal *int64 `json:"storage_total"` - }{} + devices := []DeviceEntry{} - err = Decode(response, &serverDevices) + err = Decode(response, &devices) if err != nil { return nil, err } - devices := make([]DeviceEntry, len(serverDevices)) - - for index, device := range serverDevices { - devices[index] = DeviceEntry{ - Imei: device.Imei, - Name: device.Name, - FleetId: device.FleetId, - LastSeenAt: device.LastSeenAt, - RunState: device.RunState, - StorageUsed: device.StorageUsed, - StorageTotal: device.StorageTotal, - } - } - return devices, nil } diff --git a/internal/api/fleet_keys.go b/internal/api/fleet_keys.go index c18e6d7..dc5ce4e 100644 --- a/internal/api/fleet_keys.go +++ b/internal/api/fleet_keys.go @@ -6,14 +6,14 @@ import ( ) type FleetKeyEntry struct { - Id int64 `json:"id"` - Fleet int64 `json:"fleet"` - Label string `json:"label"` - Suffix string `json:"suffix"` + Id int64 `json:"id"` + FleetId int64 `json:"fleet_id"` + Label string `json:"label"` + FleetKeySuffix string `json:"fleet_key_suffix"` } func FetchFleetKeys(invocation Invocation) ([]FleetKeyEntry, error) { - request, err := AuthenticatedRequest(invocation, http.MethodGet, "/keys", nil) + request, err := AuthenticatedRequest(invocation, http.MethodGet, "/fleet-keys", nil) if err != nil { return nil, err diff --git a/internal/device/device.go b/internal/device/device.go index d89fe28..9d1e8cf 100644 --- a/internal/device/device.go +++ b/internal/device/device.go @@ -14,83 +14,6 @@ import ( "github.com/siliconwitchery/superstack-cli/internal/api" ) -func Pair(invocation api.Invocation, arguments []string) error { - if len(arguments) != 2 && len(arguments) != 3 { - return errors.New("device pair takes an IMEI, a fleet id, and an optional name") - } - - imei := arguments[0] - - if !validImei(imei) { - return errors.New("the IMEI is the 15-digit number printed on the device") - } - - fleetId, err := strconv.ParseInt(arguments[1], 10, 64) - - if err != nil || fleetId < 1 { - return errors.New("the fleet id is the number shown by fleet list") - } - - fleets, err := api.FetchFleets(invocation) - - if err != nil { - return err - } - - fleetName := "" - - for _, fleet := range fleets { - if fleet.Id == fleetId { - fleetName = fleet.Name - } - } - - if fleetName == "" { - return errors.New("no such fleet") - } - - fmt.Fprintln(invocation.Out, "Press the pairing button on the device to finish pairing it.") - - payload := map[string]string{"imei": imei} - - if len(arguments) == 3 { - payload["name"] = arguments[2] - } - - body, err := json.Marshal(payload) - - if err != nil { - return err - } - - request, err := api.AuthenticatedRequest(invocation, http.MethodPost, - "/fleets/"+strconv.FormatInt(fleetId, 10)+"/devices", bytes.NewReader(body)) - - if err != nil { - return err - } - - request.Header.Set("Content-Type", "application/json") - - pairingClient := &http.Client{Timeout: 90 * time.Second} - - response, err := pairingClient.Do(request) - - if err != nil { - return errors.New("the server could not be reached, check your internet access") - } - - defer response.Body.Close() - - if response.StatusCode != http.StatusNoContent { - return api.ServerError(response) - } - - fmt.Fprintf(invocation.Out, "Paired device %s with fleet %q.\n", imei, fleetName) - - return nil -} - func List(invocation api.Invocation, arguments []string) error { positionals, jsonOutput := api.TakeJsonFlag(arguments) @@ -150,7 +73,7 @@ func List(invocation api.Invocation, arguments []string) error { if len(filtered) == 0 { if chosenFleetId == 0 { - fmt.Fprintln(invocation.Out, "No devices yet. Pair one with device pair.") + fmt.Fprintln(invocation.Out, "No devices yet.") } else { fmt.Fprintln(invocation.Out, "No devices in that fleet.") } @@ -161,13 +84,9 @@ func List(invocation api.Invocation, arguments []string) error { imeiWidth := len("IMEI") nameWidth := len("NAME") fleetWidth := len("FLEET") - runStateWidth := len("RUN STATE") - storageWidth := len("STORAGE") imeiValues := make([]string, len(filtered)) nameValues := make([]string, len(filtered)) fleetValues := make([]string, len(filtered)) - runStateValues := make([]string, len(filtered)) - storageValues := make([]string, len(filtered)) lastSeenValues := make([]string, len(filtered)) for index, device := range filtered { @@ -200,36 +119,6 @@ func List(invocation api.Invocation, arguments []string) error { } } - runState := "unknown" - - if device.RunState != nil { - switch *device.RunState { - case 2: - runState = "running" - case 3: - runState = "stopped" - case 4: - runState = "crashed" - } - } - - storage := "-" - - if device.StorageUsed != nil && device.StorageTotal != nil { - formatBytes := func(bytes int64) string { - switch { - case bytes < 1000: - return fmt.Sprintf("%d B", bytes) - case bytes < 1000*1000: - return fmt.Sprintf("%.1f kB", float64(bytes)/1000) - default: - return fmt.Sprintf("%.1f MB", float64(bytes)/(1000*1000)) - } - } - - storage = fmt.Sprintf("%s of %s", formatBytes(*device.StorageUsed), formatBytes(*device.StorageTotal)) - } - fleetName, known := fleetNames[device.FleetId] if !known { @@ -239,24 +128,19 @@ func List(invocation api.Invocation, arguments []string) error { imeiValues[index] = api.Printable(device.Imei) nameValues[index] = api.Printable(name) fleetValues[index] = api.Printable(fleetName) - runStateValues[index] = runState - storageValues[index] = storage lastSeenValues[index] = lastSeen imeiWidth = max(imeiWidth, len(imeiValues[index])) nameWidth = max(nameWidth, len(nameValues[index])) fleetWidth = max(fleetWidth, len(fleetValues[index])) - runStateWidth = max(runStateWidth, len(runStateValues[index])) - storageWidth = max(storageWidth, len(storageValues[index])) } - fmt.Fprintf(invocation.Out, "%-*s %-*s %-*s %-*s %-*s %s\n", - imeiWidth, "IMEI", nameWidth, "NAME", fleetWidth, "FLEET", - runStateWidth, "RUN STATE", storageWidth, "STORAGE", "LAST SEEN") + fmt.Fprintf(invocation.Out, "%-*s %-*s %-*s %s\n", + imeiWidth, "IMEI", nameWidth, "NAME", fleetWidth, "FLEET", "LAST SEEN") for index := range filtered { - fmt.Fprintf(invocation.Out, "%-*s %-*s %-*s %-*s %-*s %s\n", + fmt.Fprintf(invocation.Out, "%-*s %-*s %-*s %s\n", imeiWidth, imeiValues[index], nameWidth, nameValues[index], fleetWidth, fleetValues[index], - runStateWidth, runStateValues[index], storageWidth, storageValues[index], lastSeenValues[index]) + lastSeenValues[index]) } return nil @@ -365,7 +249,7 @@ func Unpair(invocation api.Invocation, arguments []string) error { return errors.New("no such device, device list shows yours") } - fmt.Fprintf(invocation.Out, "Unpair device %q from fleet %q? It wipes the device's user files and restarts Lua, and pairing it again means pressing its pairing button in person. [y/N] ", label, fleetName) + fmt.Fprintf(invocation.Out, "Unpair device %q from fleet %q? It will no longer appear in the fleet. [y/N] ", label, fleetName) answer, _ := bufio.NewReader(invocation.In).ReadString('\n') diff --git a/internal/device/device_test.go b/internal/device/device_test.go index e00fa32..d88b319 100644 --- a/internal/device/device_test.go +++ b/internal/device/device_test.go @@ -12,159 +12,11 @@ import ( "github.com/siliconwitchery/superstack-cli/internal/api/apitest" ) -func TestDevicePair(t *testing.T) { - tests := []struct { - name string - statusCode int - message string - wantOutput string - wantError string - }{ - { - name: "button pressed", - statusCode: http.StatusNoContent, - wantOutput: "Press the pairing button on the device to finish pairing it.\nPaired device 354820091234567 with fleet \"pilot\".\n", - }, - { - name: "button not pressed", - statusCode: http.StatusRequestTimeout, - message: "the button was not pressed in time", - wantOutput: "Press the pairing button on the device to finish pairing it.\n", - wantError: "the button was not pressed in time", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - pairedImei := "" - pairedName := "" - - mux := http.NewServeMux() - mux.HandleFunc("GET /fleets", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, `[{"id":3,"name":"pilot","owner":true}]`) - }) - mux.HandleFunc("POST /fleets/{id}/devices", func(w http.ResponseWriter, r *http.Request) { - body := struct { - Imei string `json:"imei"` - Name string `json:"name"` - }{} - - json.NewDecoder(r.Body).Decode(&body) - pairedImei = body.Imei - pairedName = body.Name - - if r.Header.Get("Content-Type") != "application/json" { - t.Errorf("Content-Type = %q, want application/json", r.Header.Get("Content-Type")) - } - - if test.message != "" { - http.Error(w, test.message, test.statusCode) - - return - } - - w.WriteHeader(test.statusCode) - }) - - invocation, out := apitest.LoggedInInvocation(t, mux) - - err := Pair(invocation, []string{"354820091234567", "3", "roof sensor"}) - - printed := out.String() - - if test.wantError == "" && err != nil { - t.Fatal(err) - } - - if test.wantError != "" && (err == nil || err.Error() != test.wantError) { - t.Fatalf("error = %v, want %q", err, test.wantError) - } - - if pairedImei != "354820091234567" || pairedName != "roof sensor" { - t.Errorf("the server received IMEI %q and name %q", pairedImei, pairedName) - } - - if printed != test.wantOutput { - t.Errorf("output = %q, want %q", printed, test.wantOutput) - } - }) - } -} - -func TestDevicePairOmitsAnAbsentName(t *testing.T) { - nameWasPresent := false - - mux := http.NewServeMux() - mux.HandleFunc("GET /fleets", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, `[{"id":3,"name":"pilot","owner":true}]`) - }) - mux.HandleFunc("POST /fleets/{id}/devices", func(w http.ResponseWriter, r *http.Request) { - body := map[string]string{} - json.NewDecoder(r.Body).Decode(&body) - _, nameWasPresent = body["name"] - w.WriteHeader(http.StatusNoContent) - }) - - invocation, out := apitest.LoggedInInvocation(t, mux) - - err := Pair(invocation, []string{"354820091234567", "3"}) - - if err != nil { - t.Fatal(err) - } - - if nameWasPresent { - t.Error("the request included a name although none was given") - } - - if out.String() != "Press the pairing button on the device to finish pairing it.\nPaired device 354820091234567 with fleet \"pilot\".\n" { - t.Errorf("output = %q", out.String()) - } -} - -func TestDevicePairArguments(t *testing.T) { - tests := []struct { - name string - arguments []string - wantError string - }{ - {"no arguments", nil, "takes an IMEI"}, - {"too many arguments", []string{"354820091234567", "3", "one", "two"}, "takes an IMEI"}, - {"short IMEI", []string{"123", "3"}, "15-digit"}, - {"non-digit IMEI", []string{"35482009123456x", "3"}, "15-digit"}, - {"wordy fleet", []string{"354820091234567", "pilot"}, "shown by fleet list"}, - {"zero fleet", []string{"354820091234567", "0"}, "shown by fleet list"}, - } - - for _, test := range tests { - err := Pair(api.Invocation{}, test.arguments) - - if err == nil || !strings.Contains(err.Error(), test.wantError) { - t.Errorf("%s: error = %v, want it to mention %q", test.name, err, test.wantError) - } - } -} - -func TestDevicePairUnknownFleet(t *testing.T) { - mux := http.NewServeMux() - mux.HandleFunc("GET /fleets", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, `[]`) - }) - - invocation, _ := apitest.LoggedInInvocation(t, mux) - - err := Pair(invocation, []string{"354820091234567", "9"}) - - if err == nil || err.Error() != "no such fleet" { - t.Fatalf("error = %v", err) - } -} - func TestDeviceList(t *testing.T) { now := time.Now() - devices := fmt.Sprintf(`[{"imei":"111111111111111","name":"roof","fleet_id":3,"last_seen_at":%q,"reported_state":2,"storage_used":1240,"storage_total":57344},`+ - `{"imei":"222222222222222","name":null,"fleet_id":4,"last_seen_at":%q,"reported_state":4,"storage_used":2500000,"storage_total":8000000},`+ - `{"imei":"333333333333333","name":"shed","fleet_id":3,"last_seen_at":null,"reported_state":null,"storage_used":null,"storage_total":null}]`, + devices := fmt.Sprintf(`[{"imei":"111111111111111","name":"roof","fleet_id":3,"last_seen_at":%q},`+ + `{"imei":"222222222222222","name":null,"fleet_id":4,"last_seen_at":%q},`+ + `{"imei":"333333333333333","name":"shed","fleet_id":3,"last_seen_at":null}]`, now.Add(-time.Minute).Format(time.RFC3339), now.Add(-3*time.Hour).Format(time.RFC3339)) fleets := `[{"id":3,"name":"pilot","owner":true},{"id":4,"name":"workshop","owner":true},{"id":5,"name":"empty","owner":true}]` @@ -179,24 +31,20 @@ func TestDeviceList(t *testing.T) { fleets string refusal string }{ - {name: "table", wantShown: []string{"IMEI NAME FLEET RUN STATE STORAGE LAST SEEN", "roof", "pilot", "running", "1.2 kB of 57.3 kB", "just now", "-", "workshop", "crashed", "2.5 MB of 8.0 MB", "3 h ago", "unknown", "never"}}, + {name: "table", wantShown: []string{"IMEI NAME FLEET LAST SEEN", "roof", "pilot", "just now", "-", "workshop", "3 h ago", "never"}}, {name: "filtered", arguments: []string{"3"}, wantShown: []string{"111111111111111", "333333333333333"}, wantHidden: []string{"222222222222222", "workshop"}}, - {name: "json flag anywhere", arguments: []string{"3", "--json"}, wantShown: []string{`"imei":"111111111111111"`, `"fleet_id":3`, `"run_state":2`}, wantHidden: []string{"LAST SEEN", "222222222222222", `"reported_state"`}}, + {name: "json flag anywhere", arguments: []string{"3", "--json"}, wantShown: []string{`"imei":"111111111111111"`, `"fleet_id":3`, `"last_seen_at":`}, wantHidden: []string{"LAST SEEN", "222222222222222", `"reported_state"`, `"run_state"`, `"storage_used"`}}, {name: "empty fleet", arguments: []string{"5"}, wantExact: "No devices in that fleet.\n"}, - {name: "no devices", devices: `[]`, fleets: `[]`, wantExact: "No devices yet. Pair one with device pair.\n"}, + {name: "no devices", devices: `[]`, fleets: `[]`, wantExact: "No devices yet.\n"}, {name: "server refusal", refusal: "devices unavailable", wantError: "devices unavailable"}, {name: "unknown fleet", arguments: []string{"9"}, wantError: "no such fleet"}, {name: "two ids", arguments: []string{"3", "4"}, wantError: "takes at most one fleet id"}, {name: "wordy id", arguments: []string{"pilot"}, wantError: "shown by fleet list"}, - {name: "an unreadable last seen time leaves the rest of the table", devices: `[{"imei":"111111111111111","name":"roof","fleet_id":3,"last_seen_at":"yesterday"}]`, wantShown: []string{"111111111111111 roof pilot unknown - unknown"}}, - {name: "a fleet the list does not name", devices: `[{"imei":"888888888888888","name":"orphan","fleet_id":99}]`, wantShown: []string{"888888888888888 orphan - unknown - never"}}, + {name: "an unreadable last seen time leaves the rest of the table", devices: `[{"imei":"111111111111111","name":"roof","fleet_id":3,"last_seen_at":"yesterday"}]`, wantShown: []string{"111111111111111 roof pilot unknown"}}, + {name: "a fleet the list does not name", devices: `[{"imei":"888888888888888","name":"orphan","fleet_id":99}]`, wantShown: []string{"888888888888888 orphan - never"}}, {name: "a name with control characters is escaped", devices: `[{"imei":"111111111111111","name":"\u001b[2K\rhidden","fleet_id":3}]`, wantShown: []string{`\x1b[2K\rhidden`}, wantHidden: []string{"\x1b"}}, {name: "minutes ago", devices: fmt.Sprintf(`[{"imei":"111111111111111","name":"roof","fleet_id":3,"last_seen_at":%q}]`, now.Add(-12*time.Minute).Format(time.RFC3339)), wantShown: []string{"12 min ago"}}, {name: "days ago", devices: fmt.Sprintf(`[{"imei":"111111111111111","name":"roof","fleet_id":3,"last_seen_at":%q}]`, now.Add(-49*time.Hour).Format(time.RFC3339)), wantShown: []string{"2 d ago"}}, - {name: "stopped and undefined run states", devices: `[{"imei":"444444444444444","name":"halted","fleet_id":3,"reported_state":3},{"imei":"555555555555555","name":"odd","fleet_id":3,"reported_state":1}]`, wantShown: []string{"stopped", "unknown"}}, - {name: "byte storage", devices: `[{"imei":"666666666666666","name":"bytes","fleet_id":3,"storage_used":999,"storage_total":999}]`, wantShown: []string{"999 B of 999 B"}}, - {name: "missing used storage", devices: `[{"imei":"777777777777777","name":"nil-used","fleet_id":3,"storage_used":null,"storage_total":57344}]`, wantShown: []string{"777777777777777 nil-used pilot unknown - never"}}, - {name: "missing total storage", devices: `[{"imei":"888888888888888","name":"nil-total","fleet_id":3,"storage_used":1240,"storage_total":null}]`, wantShown: []string{"888888888888888 nil-total pilot unknown - never"}}, } for _, test := range tests { @@ -350,9 +198,9 @@ func TestDeviceUnpair(t *testing.T) { wantOutput string wantError string }{ - {name: "confirmed", answer: "yes\n", wantUnpaired: true, wantOutput: "Unpair device \"354820091234567\" from fleet \"pilot\"? It wipes the device's user files and restarts Lua, and pairing it again means pressing its pairing button in person. [y/N] Unpaired device \"354820091234567\" from fleet \"pilot\".\n"}, - {name: "declined", answer: "n\n", wantOutput: "Unpair device \"354820091234567\" from fleet \"pilot\"? It wipes the device's user files and restarts Lua, and pairing it again means pressing its pairing button in person. [y/N] Nothing unpaired.\n"}, - {name: "a named device is named back, not its IMEI", answer: "n\n", devices: `[{"imei":"354820091234567","name":"rooftop","fleet_id":3,"last_seen_at":null}]`, wantOutput: "Unpair device \"rooftop\" from fleet \"pilot\"? It wipes the device's user files and restarts Lua, and pairing it again means pressing its pairing button in person. [y/N] Nothing unpaired.\n"}, + {name: "confirmed", answer: "yes\n", wantUnpaired: true, wantOutput: "Unpair device \"354820091234567\" from fleet \"pilot\"? It will no longer appear in the fleet. [y/N] Unpaired device \"354820091234567\" from fleet \"pilot\".\n"}, + {name: "declined", answer: "n\n", wantOutput: "Unpair device \"354820091234567\" from fleet \"pilot\"? It will no longer appear in the fleet. [y/N] Nothing unpaired.\n"}, + {name: "a named device is named back, not its IMEI", answer: "n\n", devices: `[{"imei":"354820091234567","name":"rooftop","fleet_id":3,"last_seen_at":null}]`, wantOutput: "Unpair device \"rooftop\" from fleet \"pilot\"? It will no longer appear in the fleet. [y/N] Nothing unpaired.\n"}, {name: "server refuses", answer: "y\n", refusal: "no such device", wantUnpaired: true, wantError: "no such device"}, {name: "device belongs to an inaccessible fleet", fleets: `[]`, wantError: "no such device, device list shows yours"}, } diff --git a/internal/dispatch/dispatch.go b/internal/dispatch/dispatch.go index 40b518c..ca7f505 100644 --- a/internal/dispatch/dispatch.go +++ b/internal/dispatch/dispatch.go @@ -178,7 +178,5 @@ func Dispatch(sections []Section, version string, arguments []string, in io.Read return fmt.Errorf("%s is not available yet", entry.Name) } - err := entry.Run(invocation, rest) - - return err + return entry.Run(invocation, rest) } diff --git a/internal/dispatch/dispatch_test.go b/internal/dispatch/dispatch_test.go index 1037f1b..781670d 100644 --- a/internal/dispatch/dispatch_test.go +++ b/internal/dispatch/dispatch_test.go @@ -144,10 +144,9 @@ func TestResolve(t *testing.T) { sections := []Section{{Commands: []Command{ {Name: "login"}, {Name: "device list"}, - {Name: "device pair"}, {Name: "fleet create"}, {Name: "member add"}, - {Name: "fleet key create"}, + {Name: "key create"}, {Name: "account balance"}, {Name: "account topup"}, {Name: "upload"}, @@ -161,17 +160,16 @@ func TestResolve(t *testing.T) { }{ {arguments: []string{"login"}, name: "login", rest: []string{}, found: true}, {arguments: []string{"device", "list"}, name: "device list", rest: []string{}, found: true}, - {arguments: []string{"device", "pair", "354820091234567", "sensor-01"}, name: "device pair", rest: []string{"354820091234567", "sensor-01"}, found: true}, {arguments: []string{"fleet", "create", "thermostats"}, name: "fleet create", rest: []string{"thermostats"}, found: true}, {arguments: []string{"member", "add", "member@example.com"}, name: "member add", rest: []string{"member@example.com"}, found: true}, - {arguments: []string{"fleet", "key", "create", "42", "production"}, name: "fleet key create", rest: []string{"42", "production"}, found: true}, + {arguments: []string{"key", "create", "42", "production"}, name: "key create", rest: []string{"42", "production"}, found: true}, {arguments: []string{"account", "balance"}, name: "account balance", rest: []string{}, found: true}, {arguments: []string{"account", "topup", "42"}, name: "account topup", rest: []string{"42"}, found: true}, {arguments: []string{"upload", "./main.lua", "--device", "sensor-01"}, name: "upload", rest: []string{"./main.lua", "--device", "sensor-01"}, found: true}, {arguments: []string{"fleet"}, found: false}, {arguments: []string{"member"}, found: false}, {arguments: []string{"device"}, found: false}, - {arguments: []string{"fleet", "key"}, found: false}, + {arguments: []string{"key"}, found: false}, {arguments: []string{"account"}, found: false}, {arguments: []string{"deploy"}, found: false}, {arguments: []string{}, found: false}, @@ -261,30 +259,3 @@ func TestDispatch(t *testing.T) { }) } } - -func TestHelpListsEveryCommand(t *testing.T) { - sections := []Section{ - {Title: "Things", Commands: []Command{{Name: "thing list", Arguments: "[--json]", Summary: "List things"}}}, - {Title: "Account", Commands: []Command{{Name: "account delete", Summary: "Delete the account"}}}, - } - out := &bytes.Buffer{} - invocation := api.NewInvocation(api.DefaultBase, "1.2.3", strings.NewReader(""), out) - - printHelp(invocation, sections) - - for _, section := range sections { - if !strings.Contains(out.String(), section.Title) { - t.Errorf("help is missing the section %q", section.Title) - } - - for _, entry := range section.Commands { - if !strings.Contains(out.String(), entry.Name) { - t.Errorf("help is missing the command %q", entry.Name) - } - - if !strings.Contains(out.String(), entry.Summary) { - t.Errorf("help is missing the summary for %q", entry.Name) - } - } - } -} diff --git a/internal/fleet/fleet.go b/internal/fleet/fleet.go index 226ced5..387b211 100644 --- a/internal/fleet/fleet.go +++ b/internal/fleet/fleet.go @@ -275,7 +275,7 @@ func Delete(invocation api.Invocation, arguments []string) error { forfeitUnknown := false for _, balance := range balances { - if balance.Fleet != fleetId { + if balance.FleetId != fleetId { continue } @@ -291,14 +291,12 @@ func Delete(invocation api.Invocation, arguments []string) error { } } - consequence := "It wipes their user files and restarts Lua, and pairing one again means pressing its pairing button in person." - if forfeitUnknown { - fmt.Fprintf(invocation.Out, "Delete fleet %q, unpair its devices, and forfeit its remaining credit? %s [y/N] ", name, consequence) + fmt.Fprintf(invocation.Out, "Delete fleet %q, unpair its devices, and forfeit its remaining credit? [y/N] ", name) } else if forfeited == "" { - fmt.Fprintf(invocation.Out, "Delete fleet %q and unpair its devices? %s [y/N] ", name, consequence) + fmt.Fprintf(invocation.Out, "Delete fleet %q and unpair its devices? [y/N] ", name) } else { - fmt.Fprintf(invocation.Out, "Delete fleet %q, unpair its devices, and forfeit its remaining %s of credit? %s [y/N] ", name, forfeited, consequence) + fmt.Fprintf(invocation.Out, "Delete fleet %q, unpair its devices, and forfeit its remaining %s of credit? [y/N] ", name, forfeited) } answer, _ := bufio.NewReader(invocation.In).ReadString('\n') diff --git a/internal/fleet/fleet_test.go b/internal/fleet/fleet_test.go index 7f8c7d3..aa38f0e 100644 --- a/internal/fleet/fleet_test.go +++ b/internal/fleet/fleet_test.go @@ -424,7 +424,7 @@ func TestFleetDelete(t *testing.T) { }) mux.HandleFunc("GET /balance", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, `[{"fleet":3,"balance":"0","currency":"eur"}]`) + fmt.Fprint(w, `[{"fleet_id":3,"balance":"0","currency":"eur"}]`) }) mux.HandleFunc("DELETE /fleets/{id}", func(w http.ResponseWriter, r *http.Request) { @@ -484,19 +484,19 @@ func TestFleetDeletePromptStatesForfeitedCredit(t *testing.T) { }{ { name: "remaining credit is stated", - balance: `[{"fleet":3,"balance":"12.340000","currency":"eur"}]`, - wantOutput: "Delete fleet \"pilot\", unpair its devices, and forfeit its remaining €12.34 of credit? It wipes their user files and restarts Lua, and pairing one again means pressing its pairing button in person. [y/N] Nothing deleted.\n", + balance: `[{"fleet_id":3,"balance":"12.340000","currency":"eur"}]`, + wantOutput: "Delete fleet \"pilot\", unpair its devices, and forfeit its remaining €12.34 of credit? [y/N] Nothing deleted.\n", }, { name: "an empty balance stays quiet", - balance: `[{"fleet":3,"balance":"0","currency":"eur"}]`, - wantOutput: "Delete fleet \"pilot\" and unpair its devices? It wipes their user files and restarts Lua, and pairing one again means pressing its pairing button in person. [y/N] Nothing deleted.\n", + balance: `[{"fleet_id":3,"balance":"0","currency":"eur"}]`, + wantOutput: "Delete fleet \"pilot\" and unpair its devices? [y/N] Nothing deleted.\n", wantAbsent: "forfeit", }, { name: "an unparseable balance warns without an amount", - balance: `[{"fleet":3,"balance":"15,00","currency":"eur"}]`, - wantOutput: "Delete fleet \"pilot\", unpair its devices, and forfeit its remaining credit? It wipes their user files and restarts Lua, and pairing one again means pressing its pairing button in person. [y/N] Nothing deleted.\n", + balance: `[{"fleet_id":3,"balance":"15,00","currency":"eur"}]`, + wantOutput: "Delete fleet \"pilot\", unpair its devices, and forfeit its remaining credit? [y/N] Nothing deleted.\n", wantAbsent: "€", }, } @@ -529,10 +529,6 @@ func TestFleetDeletePromptStatesForfeitedCredit(t *testing.T) { t.Errorf("output = %q, want %q", printed, test.wantOutput) } - if !strings.Contains(printed, "It wipes their user files and restarts Lua, and pairing one again means pressing its pairing button in person.") { - t.Errorf("the prompt %q does not say what unpairing the devices does to them", printed) - } - if test.wantAbsent != "" && strings.Contains(printed, test.wantAbsent) { t.Errorf("the prompt %q mentions %q although nothing is forfeited", printed, test.wantAbsent) } diff --git a/internal/fleetkey/fleetkey.go b/internal/fleetkey/fleetkey.go index f27d3f8..df04dc7 100644 --- a/internal/fleetkey/fleetkey.go +++ b/internal/fleetkey/fleetkey.go @@ -15,7 +15,7 @@ import ( func Create(invocation api.Invocation, arguments []string) error { if len(arguments) != 2 || arguments[1] == "" { - return errors.New("fleet key create takes a fleet id and a label, quoted if it has spaces") + return errors.New("key create takes a fleet id and a label, quoted if it has spaces") } fleetId, err := strconv.ParseInt(arguments[0], 10, 64) @@ -31,7 +31,7 @@ func Create(invocation api.Invocation, arguments []string) error { } request, err := api.AuthenticatedRequest(invocation, http.MethodPost, - "/fleets/"+strconv.FormatInt(fleetId, 10)+"/keys", bytes.NewReader(body)) + "/fleets/"+strconv.FormatInt(fleetId, 10)+"/fleet-keys", bytes.NewReader(body)) if err != nil { return err @@ -53,7 +53,7 @@ func Create(invocation api.Invocation, arguments []string) error { created := struct { Id int64 `json:"id"` - FleetKey string `json:"key"` + FleetKey string `json:"fleet_key"` }{} err = api.Decode(response, &created) @@ -63,10 +63,10 @@ func Create(invocation api.Invocation, arguments []string) error { } if created.FleetKey == "" { - return errors.New("the fleet key was not created, try again") + return errors.New("the key was not created, try again") } - fmt.Fprintf(invocation.Out, "Created fleet key %d.\n\n %s\n\nAnyone holding it can send data to the fleet, and you will not see it again.\n", created.Id, api.Printable(created.FleetKey)) + fmt.Fprintf(invocation.Out, "Created key %d.\n\n %s\n\nAnyone holding it can send data to the fleet, and you will not see it again.\n", created.Id, api.Printable(created.FleetKey)) return nil } @@ -75,7 +75,7 @@ func List(invocation api.Invocation, arguments []string) error { positionals, jsonOutput := api.TakeJsonFlag(arguments) if len(positionals) > 1 { - return errors.New("fleet key list takes at most one fleet id") + return errors.New("key list takes at most one fleet id") } chosenFleetId := int64(0) @@ -117,7 +117,7 @@ func List(invocation api.Invocation, arguments []string) error { fleetKeys := []api.FleetKeyEntry{} for _, fleetKey := range fetched { - if chosenFleetId == 0 || fleetKey.Fleet == chosenFleetId { + if chosenFleetId == 0 || fleetKey.FleetId == chosenFleetId { fleetKeys = append(fleetKeys, fleetKey) } } @@ -130,9 +130,9 @@ func List(invocation api.Invocation, arguments []string) error { if len(fleetKeys) == 0 { if chosenFleetId == 0 { - fmt.Fprintln(invocation.Out, "No fleet keys yet. Create one with fleet key create.") + fmt.Fprintln(invocation.Out, "No keys yet. Create one with key create.") } else { - fmt.Fprintln(invocation.Out, "No fleet keys on that fleet yet.") + fmt.Fprintln(invocation.Out, "No keys on that fleet yet.") } return nil @@ -141,32 +141,32 @@ func List(invocation api.Invocation, arguments []string) error { idWidth := len("ID") fleetIdWidth := len("FLEET") fleetNameWidth := len("FLEET NAME") - fleetKeyWidth := len("FLEET KEY") + fleetKeyWidth := len("KEY") fleetNameValues := make([]string, len(fleetKeys)) suffixValues := make([]string, len(fleetKeys)) labelValues := make([]string, len(fleetKeys)) for index, fleetKey := range fleetKeys { - fleetName, known := fleetNames[fleetKey.Fleet] + fleetName, known := fleetNames[fleetKey.FleetId] if !known { fleetName = "-" } fleetNameValues[index] = api.Printable(fleetName) - suffixValues[index] = api.Printable(fleetKey.Suffix) + suffixValues[index] = api.Printable(fleetKey.FleetKeySuffix) labelValues[index] = api.Printable(fleetKey.Label) idWidth = max(idWidth, len(strconv.FormatInt(fleetKey.Id, 10))) - fleetIdWidth = max(fleetIdWidth, len(strconv.FormatInt(fleetKey.Fleet, 10))) + fleetIdWidth = max(fleetIdWidth, len(strconv.FormatInt(fleetKey.FleetId, 10))) fleetNameWidth = max(fleetNameWidth, len(fleetNameValues[index])) } fmt.Fprintf(invocation.Out, "%-*s %-*s %-*s %-*s %s\n", - idWidth, "ID", fleetIdWidth, "FLEET", fleetNameWidth, "FLEET NAME", fleetKeyWidth, "FLEET KEY", "LABEL") + idWidth, "ID", fleetIdWidth, "FLEET", fleetNameWidth, "FLEET NAME", fleetKeyWidth, "KEY", "LABEL") for index, fleetKey := range fleetKeys { fmt.Fprintf(invocation.Out, "%-*d %-*d %-*s %-*s %s\n", - idWidth, fleetKey.Id, fleetIdWidth, fleetKey.Fleet, fleetNameWidth, fleetNameValues[index], + idWidth, fleetKey.Id, fleetIdWidth, fleetKey.FleetId, fleetNameWidth, fleetNameValues[index], fleetKeyWidth, "..."+suffixValues[index], labelValues[index]) } @@ -175,13 +175,13 @@ func List(invocation api.Invocation, arguments []string) error { func Revoke(invocation api.Invocation, arguments []string) error { if len(arguments) != 1 { - return errors.New("fleet key revoke takes a fleet key id") + return errors.New("key revoke takes a key id") } fleetKeyId, err := strconv.ParseInt(arguments[0], 10, 64) if err != nil || fleetKeyId < 1 { - return errors.New("the fleet key id is the number shown by fleet key list") + return errors.New("the key id is the number shown by key list") } fleetKeys, err := api.FetchFleetKeys(invocation) @@ -201,10 +201,10 @@ func Revoke(invocation api.Invocation, arguments []string) error { } if !found { - return errors.New("no such fleet key") + return errors.New("no such key") } - fmt.Fprintf(invocation.Out, "Revoke fleet key %q? Anything still using it stops reaching the fleet. [y/N] ", label) + fmt.Fprintf(invocation.Out, "Revoke key %q? Anything still using it stops reaching the fleet. [y/N] ", label) answer, _ := bufio.NewReader(invocation.In).ReadString('\n') @@ -216,7 +216,7 @@ func Revoke(invocation api.Invocation, arguments []string) error { } request, err := api.AuthenticatedRequest(invocation, http.MethodDelete, - "/keys/"+strconv.FormatInt(fleetKeyId, 10), nil) + "/fleet-keys/"+strconv.FormatInt(fleetKeyId, 10), nil) if err != nil { return err @@ -234,7 +234,7 @@ func Revoke(invocation api.Invocation, arguments []string) error { return api.ServerError(response) } - fmt.Fprintf(invocation.Out, "Revoked fleet key %q.\n", label) + fmt.Fprintf(invocation.Out, "Revoked key %q.\n", label) return nil } diff --git a/internal/fleetkey/fleetkey_test.go b/internal/fleetkey/fleetkey_test.go index 6378a7a..c93a26f 100644 --- a/internal/fleetkey/fleetkey_test.go +++ b/internal/fleetkey/fleetkey_test.go @@ -23,7 +23,7 @@ func TestFleetKeyCreate(t *testing.T) { { name: "the server answers without a fleet key", arguments: []string{"3", "deploy server"}, - wantPath: "/fleets/3/keys", + wantPath: "/fleets/3/fleet-keys", wantLabel: "deploy server", answer: `{"id":1}`, wantError: "was not created", @@ -31,7 +31,7 @@ func TestFleetKeyCreate(t *testing.T) { { name: "a labelled fleet key", arguments: []string{"3", "deploy server"}, - wantPath: "/fleets/3/keys", + wantPath: "/fleets/3/fleet-keys", wantLabel: "deploy server", }, { @@ -62,7 +62,7 @@ func TestFleetKeyCreate(t *testing.T) { { name: "the server refuses", arguments: []string{"9", "doomed"}, - wantPath: "/fleets/9/keys", + wantPath: "/fleets/9/fleet-keys", refusal: "no such fleet", wantError: "no such fleet", }, @@ -72,7 +72,7 @@ func TestFleetKeyCreate(t *testing.T) { t.Run(test.name, func(t *testing.T) { mux := http.NewServeMux() - mux.HandleFunc("POST /fleets/{id}/keys", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("POST /fleets/{id}/fleet-keys", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != test.wantPath { t.Errorf("the request went to %s, want %s", r.URL.Path, test.wantPath) } @@ -99,7 +99,7 @@ func TestFleetKeyCreate(t *testing.T) { answer := test.answer if answer == "" { - answer = `{"id":1,"key":"ssf_testtesttestab2de"}` + answer = `{"id":1,"fleet_key":"ssf_testtesttestab2de"}` } fmt.Fprint(w, answer) @@ -131,7 +131,7 @@ func TestFleetKeyCreate(t *testing.T) { t.Errorf("the output %q does not warn that the fleet key cannot be shown again", printed) } - if !strings.Contains(printed, "Created fleet key 1.") { + if !strings.Contains(printed, "Created key 1.") { t.Errorf("the output %q does not name the fleet key it created", printed) } }) @@ -143,8 +143,8 @@ func TestFleetKeyList(t *testing.T) { `{"id":4,"name":"skunkworks","owner":false},` + `{"id":5,"name":"spares","owner":true}]` - fleetKeys := `[{"id":1,"fleet":3,"label":"deploy server","suffix":"ab2de"},` + - `{"id":2,"fleet":4,"label":"lab sensor","suffix":"f9hjk"}]` + fleetKeys := `[{"id":1,"fleet_id":3,"label":"deploy server","fleet_key_suffix":"ab2de"},` + + `{"id":2,"fleet_id":4,"label":"lab sensor","fleet_key_suffix":"f9hjk"}]` tests := []struct { name string @@ -164,7 +164,7 @@ func TestFleetKeyList(t *testing.T) { { name: "every fleet's fleet keys", arguments: []string{}, - wantShown: []string{"ID FLEET FLEET NAME FLEET KEY", "crew", "skunkworks", "...ab2de", "...f9hjk", "deploy server", "lab sensor"}, + wantShown: []string{"ID FLEET FLEET NAME KEY", "crew", "skunkworks", "...ab2de", "...f9hjk", "deploy server", "lab sensor"}, }, { name: "one fleet's fleet keys", @@ -175,26 +175,26 @@ func TestFleetKeyList(t *testing.T) { { name: "a label with control characters is escaped", arguments: []string{}, - fleetKeys: `[{"id":1,"fleet":3,"label":"\u001b[2Kquiet","suffix":"ab2de"}]`, + fleetKeys: `[{"id":1,"fleet_id":3,"label":"\u001b[2Kquiet","fleet_key_suffix":"ab2de"}]`, wantShown: []string{`\x1b[2Kquiet`}, wantHidden: []string{"\x1b"}, }, { name: "no fleet keys", arguments: []string{}, - wantShown: []string{"No fleet keys yet. Create one with fleet key create."}, + wantShown: []string{"No keys yet. Create one with key create."}, fleetKeys: `[]`, }, { name: "a fleet without fleet keys", arguments: []string{"5"}, - wantShown: []string{"No fleet keys on that fleet yet."}, + wantShown: []string{"No keys on that fleet yet."}, wantHidden: []string{"ID FLEET"}, }, { name: "machine-readable output", arguments: []string{"--json"}, - wantShown: []string{`"suffix":"ab2de"`, `"fleet":4`}, + wantShown: []string{`"fleet_key_suffix":"ab2de"`, `"fleet_id":4`}, wantHidden: []string{"ID FLEET"}, }, { @@ -206,7 +206,7 @@ func TestFleetKeyList(t *testing.T) { { name: "a fleet the list does not name", arguments: []string{}, - fleetKeys: `[{"id":1,"fleet":99,"label":"orphan","suffix":"ab2de"}]`, + fleetKeys: `[{"id":1,"fleet_id":99,"label":"orphan","fleet_key_suffix":"ab2de"}]`, wantShown: []string{"99 -"}, }, { @@ -240,7 +240,7 @@ func TestFleetKeyList(t *testing.T) { fmt.Fprint(w, fleets) }) - mux.HandleFunc("GET /keys", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("GET /fleet-keys", func(w http.ResponseWriter, r *http.Request) { if test.refusal != "" { http.Error(w, test.refusal, http.StatusServiceUnavailable) return @@ -296,8 +296,8 @@ func TestFleetKeyRevoke(t *testing.T) { name: "revoke a fleet key", arguments: []string{"3"}, answer: "y\n", - wantRevoked: "/keys/3", - wantShown: "Revoked fleet key \"production\".", + wantRevoked: "/fleet-keys/3", + wantShown: "Revoked key \"production\".", }, { name: "declined by default", @@ -321,29 +321,29 @@ func TestFleetKeyRevoke(t *testing.T) { arguments: []string{"3"}, answer: "y\n", refusal: "no such fleet key", - wantRevoked: "/keys/3", + wantRevoked: "/fleet-keys/3", wantError: "no such fleet key", }, { name: "a fleet key that is not yours", arguments: []string{"9"}, answer: "y\n", - wantError: "no such fleet key", + wantError: "no such key", }, { name: "no fleet key id", arguments: []string{}, - wantError: "takes a fleet key id", + wantError: "takes a key id", }, { name: "two fleet key ids", arguments: []string{"3", "4"}, - wantError: "takes a fleet key id", + wantError: "takes a key id", }, { name: "a wordy id", arguments: []string{"pilot"}, - wantError: "shown by fleet key list", + wantError: "shown by key list", }, } @@ -353,11 +353,11 @@ func TestFleetKeyRevoke(t *testing.T) { mux := http.NewServeMux() - mux.HandleFunc("GET /keys", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, `[{"id":3,"fleet":1,"label":"production","suffix":"a1b2c"}]`) + mux.HandleFunc("GET /fleet-keys", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `[{"id":3,"fleet_id":1,"label":"production","fleet_key_suffix":"a1b2c"}]`) }) - mux.HandleFunc("DELETE /keys/{id}", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("DELETE /fleet-keys/{id}", func(w http.ResponseWriter, r *http.Request) { revokedPath = r.URL.Path if test.refusal != "" { diff --git a/internal/login/login.go b/internal/login/login.go index d19e8df..f8dc424 100644 --- a/internal/login/login.go +++ b/internal/login/login.go @@ -242,7 +242,7 @@ func Login(invocation api.Invocation, arguments []string) error { } login := struct { - LoginKey string `json:"key"` + LoginKey string `json:"login_key"` Email string `json:"email"` }{} diff --git a/internal/login/login_test.go b/internal/login/login_test.go index ce45dbd..b222d88 100644 --- a/internal/login/login_test.go +++ b/internal/login/login_test.go @@ -150,7 +150,7 @@ func fakeSuperstack(t *testing.T, providersRefusal string, loginAnswer string) ( return } - fmt.Fprint(w, `{"key": "ssk_test", "email": "someone@example.com"}`) + fmt.Fprint(w, `{"login_key": "ssk_test", "email": "someone@example.com"}`) }) server := httptest.NewServer(mux) @@ -210,7 +210,7 @@ func TestLogin(t *testing.T) { name: "superstack returns an empty login key", provider: "github", pollAnswers: []string{`{"access_token":"gho_test"}`}, - loginAnswer: `{"key":"","email":"someone@example.com"}`, + loginAnswer: `{"login_key":"","email":"someone@example.com"}`, wantError: "the login did not complete", }, { diff --git a/main.go b/main.go index 4b0b398..7fc9800 100644 --- a/main.go +++ b/main.go @@ -36,10 +36,9 @@ var sections = []dispatch.Section{ { Title: "Devices", Commands: []dispatch.Command{ - {Name: "device pair", Arguments: " [name]", Summary: "Pair a device with a fleet using its pairing button", Run: device.Pair}, - {Name: "device list", Arguments: "[fleet_id] [--json]", Summary: "List devices, their run state, and when they were last seen", Run: device.List}, + {Name: "device list", Arguments: "[fleet_id] [--json]", Summary: "List devices and when they were last seen", Run: device.List}, {Name: "device rename", Arguments: " ", Summary: "Rename a device", Run: device.Rename}, - {Name: "device unpair", Arguments: "", Summary: "Unpair a device, wipe its user files, and restart Lua", Run: device.Unpair}, + {Name: "device unpair", Arguments: "", Summary: "Remove a device from its fleet", Run: device.Unpair}, {Name: "device start", Arguments: "", Summary: "Start the code on a device"}, {Name: "device stop", Arguments: "", Summary: "Stop the code on a device"}, {Name: "device restart", Arguments: "", Summary: "Restart the code on a device"}, @@ -68,11 +67,11 @@ var sections = []dispatch.Section{ }, }, { - Title: "Fleet keys", + Title: "Keys", Commands: []dispatch.Command{ - {Name: "fleet key create", Arguments: "