From 5df18618c767d351eda020df4834ffae9e129d0d Mon Sep 17 00:00:00 2001 From: Raj Nakarja Date: Mon, 17 Aug 2026 13:41:28 +0200 Subject: [PATCH] Prompt for the device button during device claim The server now holds the claim request until the button is pressed or a minute passes, so the command prompts for the press and waits on the one request with its own longer client timeout. --- internal/commands/device_claim.go | 7 +- internal/commands/device_claim_test.go | 129 ++++++++++++++----------- 2 files changed, 79 insertions(+), 57 deletions(-) diff --git a/internal/commands/device_claim.go b/internal/commands/device_claim.go index 79d0d7f..27f82bc 100644 --- a/internal/commands/device_claim.go +++ b/internal/commands/device_claim.go @@ -9,8 +9,11 @@ import ( "net/http" "strconv" "strings" + "time" ) +var claimClient = &http.Client{Timeout: 90 * time.Second} + func DeviceClaim(arguments []string) error { if len(arguments) != 2 && len(arguments) != 3 { return errors.New("device claim takes an IMEI, a fleet id, and an optional name") @@ -46,6 +49,8 @@ func DeviceClaim(arguments []string) error { return errors.New("the fleet id is the number shown by fleet list") } + fmt.Println("Press the button on the device to finish claiming it.") + payload := map[string]string{"imei": imei} if len(arguments) == 3 { @@ -67,7 +72,7 @@ func DeviceClaim(arguments []string) error { request.Header.Set("Content-Type", "application/json") - response, err := apiClient.Do(request) + response, err := claimClient.Do(request) if err != nil { return fmt.Errorf("the server could not be reached: %w", err) diff --git a/internal/commands/device_claim_test.go b/internal/commands/device_claim_test.go index 748d51a..52dc69c 100644 --- a/internal/commands/device_claim_test.go +++ b/internal/commands/device_claim_test.go @@ -9,46 +9,81 @@ import ( ) func TestDeviceClaim(t *testing.T) { - claimedImei := "" - claimedName := "" - - 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) - claimedImei = body.Imei - claimedName = body.Name - - if r.Header.Get("Content-Type") != "application/json" { - t.Errorf("Content-Type = %q, want application/json", r.Header.Get("Content-Type")) - } - - w.WriteHeader(http.StatusNoContent) - }) - - loggedInTestServer(t, mux) - - printed, err := captureStdout(t, func() error { - return DeviceClaim([]string{"354820091234567", "3", "roof sensor"}) - }) - - if err != nil { - t.Fatal(err) - } - - if claimedImei != "354820091234567" || claimedName != "roof sensor" { - t.Errorf("the server received IMEI %q and name %q", claimedImei, claimedName) + tests := []struct { + name string + statusCode int + message string + wantOutput string + wantError string + }{ + { + name: "button pressed", + statusCode: http.StatusNoContent, + wantOutput: "Press the button on the device to finish claiming it.\nClaimed the device into \"pilot\".\n", + }, + { + name: "button not pressed", + statusCode: http.StatusRequestTimeout, + message: "the button was not pressed in time", + wantOutput: "Press the button on the device to finish claiming it.\n", + wantError: "the server said: the button was not pressed in time", + }, } - if printed != "Claimed the device into \"pilot\".\n" { - t.Errorf("output = %q", printed) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + claimedImei := "" + claimedName := "" + + 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) + claimedImei = body.Imei + claimedName = 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) + }) + + loggedInTestServer(t, mux) + + printed, err := captureStdout(t, func() error { + return DeviceClaim([]string{"354820091234567", "3", "roof sensor"}) + }) + + 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 claimedImei != "354820091234567" || claimedName != "roof sensor" { + t.Errorf("the server received IMEI %q and name %q", claimedImei, claimedName) + } + + if printed != test.wantOutput { + t.Errorf("output = %q, want %q", printed, test.wantOutput) + } + }) } } @@ -79,24 +114,6 @@ func TestDeviceClaimOmitsAnAbsentName(t *testing.T) { } } -func TestDeviceClaimServerError(t *testing.T) { - 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) { - http.Error(w, "no unclaimed device with that IMEI", http.StatusNotFound) - }) - - loggedInTestServer(t, mux) - - err := DeviceClaim([]string{"354820091234567", "3"}) - - if err == nil || err.Error() != "the server said: no unclaimed device with that IMEI" { - t.Fatalf("error = %v", err) - } -} - func TestDeviceClaimArguments(t *testing.T) { tests := []struct { name string