Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions internal/device/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func Claim(session api.Session, arguments []string) error {
return api.ServerError(response)
}

fmt.Fprintf(session.Out, "Claimed the device into %q.\n", fleetName)
fmt.Fprintf(session.Out, "Claimed device %s into fleet %q.\n", imei, fleetName)

return nil
}
Expand Down Expand Up @@ -297,7 +297,7 @@ func Rename(session api.Session, arguments []string) error {
return api.ServerError(response)
}

fmt.Fprintf(session.Out, "Renamed the device to %q.\n", name)
fmt.Fprintf(session.Out, "Renamed device %s to %q.\n", imei, name)

return nil
}
Expand All @@ -320,10 +320,18 @@ func Release(session api.Session, arguments []string) error {
}

fleetId := int64(0)
label := ""

for _, device := range devices {
if device.Imei == imei {
fleetId = device.FleetId
if device.Imei != imei {
continue
}

fleetId = device.FleetId
label = imei

if device.Name != nil && *device.Name != "" {
label = *device.Name
}
}

Expand All @@ -349,7 +357,7 @@ func Release(session api.Session, arguments []string) error {
return errors.New("no such device, device list shows yours")
}

fmt.Fprintf(session.Out, "Release the device from %q? It erases everything on the device, and claiming it again means pressing its pairing button in person. [y/N] ", fleetName)
fmt.Fprintf(session.Out, "Release device %q from fleet %q? It wipes the device's files and restarts its code, and claiming it again means pressing its pairing button in person. [y/N] ", label, fleetName)

answer, _ := bufio.NewReader(session.In).ReadString('\n')

Expand Down Expand Up @@ -378,7 +386,7 @@ func Release(session api.Session, arguments []string) error {
return api.ServerError(response)
}

fmt.Fprintln(session.Out, "Released the device.")
fmt.Fprintf(session.Out, "Released device %q from fleet %q.\n", label, fleetName)

return nil
}
Expand Down
19 changes: 13 additions & 6 deletions internal/device/device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestDeviceClaim(t *testing.T) {
{
name: "button pressed",
statusCode: http.StatusNoContent,
wantOutput: "Press the pairing button on the device to finish claiming it.\nClaimed the device into \"pilot\".\n",
wantOutput: "Press the pairing button on the device to finish claiming it.\nClaimed device 354820091234567 into fleet \"pilot\".\n",
},
{
name: "button not pressed",
Expand Down Expand Up @@ -117,7 +117,7 @@ func TestDeviceClaimOmitsAnAbsentName(t *testing.T) {
t.Error("the request included a name although none was given")
}

if out.String() != "Press the pairing button on the device to finish claiming it.\nClaimed the device into \"pilot\".\n" {
if out.String() != "Press the pairing button on the device to finish claiming it.\nClaimed device 354820091234567 into fleet \"pilot\".\n" {
t.Errorf("output = %q", out.String())
}
}
Expand Down Expand Up @@ -264,7 +264,7 @@ func TestDeviceRename(t *testing.T) {
wantOutput string
wantError string
}{
{name: "renamed", wantOutput: "Renamed the device to \"pilot\".\n"},
{name: "renamed", wantOutput: "Renamed device 354820091234567 to \"pilot\".\n"},
{name: "server refusal", refusal: "no such device", wantError: "no such device"},
}

Expand Down Expand Up @@ -341,14 +341,16 @@ func TestDeviceRelease(t *testing.T) {
tests := []struct {
name string
answer string
devices string
fleets string
refusal string
wantReleased bool
wantOutput string
wantError string
}{
{name: "confirmed", answer: "yes\n", wantReleased: true, wantOutput: "Release the device from \"pilot\"? It erases everything on the device, and claiming it again means pressing its pairing button in person. [y/N] Released the device.\n"},
{name: "declined", answer: "n\n", wantOutput: "Release the device from \"pilot\"? It erases everything on the device, and claiming it again means pressing its pairing button in person. [y/N] Nothing released.\n"},
{name: "confirmed", answer: "yes\n", wantReleased: true, wantOutput: "Release device \"354820091234567\" from fleet \"pilot\"? It wipes the device's files and restarts its code, and claiming it again means pressing its pairing button in person. [y/N] Released device \"354820091234567\" from fleet \"pilot\".\n"},
{name: "declined", answer: "n\n", wantOutput: "Release device \"354820091234567\" from fleet \"pilot\"? It wipes the device's files and restarts its code, and claiming it again means pressing its pairing button in person. [y/N] Nothing released.\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: "Release device \"rooftop\" from fleet \"pilot\"? It wipes the device's files and restarts its code, and claiming it again means pressing its pairing button in person. [y/N] Nothing released.\n"},
{name: "server refuses", answer: "y\n", refusal: "no such device", wantReleased: true, wantError: "no such device"},
{name: "device belongs to an inaccessible fleet", fleets: `[]`, wantError: "no such device, device list shows yours"},
}
Expand All @@ -357,14 +359,19 @@ func TestDeviceRelease(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
releasedPath := ""
fleets := test.fleets
devices := test.devices

if fleets == "" {
fleets = `[{"id":3,"name":"pilot","owner":true}]`
}

if devices == "" {
devices = `[{"imei":"354820091234567","name":null,"fleet_id":3,"last_seen_at":null}]`
}

mux := http.NewServeMux()
mux.HandleFunc("GET /devices", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `[{"imei":"354820091234567","name":null,"fleet_id":3,"last_seen_at":null}]`)
fmt.Fprint(w, devices)
})
mux.HandleFunc("GET /fleets", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, fleets)
Expand Down
45 changes: 38 additions & 7 deletions internal/fleet/fleet.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ func Rename(session api.Session, arguments []string) error {
return api.ServerError(response)
}

fmt.Fprintf(session.Out, "Renamed the fleet to %q.\n", name)
fmt.Fprintf(session.Out, "Renamed fleet %d to %q.\n", fleetId, name)

return nil
}
Expand All @@ -167,6 +167,37 @@ func Transfer(session api.Session, arguments []string) error {

email := arguments[1]

fleets, err := api.FetchFleets(session)

if err != nil {
return err
}

name := ""
found := false

for _, fleet := range fleets {
if fleet.Id == fleetId {
name = fleet.Name
found = true
}
}

if !found {
return errors.New("no such fleet")
}

fmt.Fprintf(session.Out, "Hand fleet %q to %s? They become the owner, and you lose access to the fleet, its devices and its credit. [y/N] ", name, email)

answer, _ := bufio.NewReader(session.In).ReadString('\n')

answer = strings.ToLower(strings.TrimSpace(answer))

if answer != "y" && answer != "yes" {
fmt.Fprintln(session.Out, "Nothing transferred.")
return nil
}

body, err := json.Marshal(map[string]string{"email": email})

if err != nil {
Expand Down Expand Up @@ -194,7 +225,7 @@ func Transfer(session api.Session, arguments []string) error {
return api.ServerError(response)
}

fmt.Fprintf(session.Out, "Transferred the fleet to %s.\n", email)
fmt.Fprintf(session.Out, "Transferred fleet %q to %s.\n", name, email)

return nil
}
Expand Down Expand Up @@ -256,14 +287,14 @@ func Delete(session api.Session, arguments []string) error {
}
}

consequence := "It erases them all, and claiming one again means pressing its pairing button in person."
consequence := "It wipes their files and restarts their code, and claiming one again means pressing its pairing button in person."

if forfeitUnknown {
fmt.Fprintf(session.Out, "Delete %q, release its devices, and forfeit its remaining credit? %s [y/N] ", name, consequence)
fmt.Fprintf(session.Out, "Delete fleet %q, release its devices, and forfeit its remaining credit? %s [y/N] ", name, consequence)
} else if forfeited == "" {
fmt.Fprintf(session.Out, "Delete %q and release its devices? %s [y/N] ", name, consequence)
fmt.Fprintf(session.Out, "Delete fleet %q and release its devices? %s [y/N] ", name, consequence)
} else {
fmt.Fprintf(session.Out, "Delete %q, release its devices, and forfeit its remaining %s of credit? %s [y/N] ", name, forfeited, consequence)
fmt.Fprintf(session.Out, "Delete fleet %q, release its devices, and forfeit its remaining %s of credit? %s [y/N] ", name, forfeited, consequence)
}

answer, _ := bufio.NewReader(session.In).ReadString('\n')
Expand Down Expand Up @@ -294,7 +325,7 @@ func Delete(session api.Session, arguments []string) error {
return api.ServerError(response)
}

fmt.Fprintf(session.Out, "Deleted %q.\n", name)
fmt.Fprintf(session.Out, "Deleted fleet %q.\n", name)

return nil
}
84 changes: 69 additions & 15 deletions internal/fleet/fleet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func TestFleetRename(t *testing.T) {
wantOutput string
wantError string
}{
{name: "renamed", wantOutput: "Renamed the fleet to \"pilot\".\n"},
{name: "renamed", wantOutput: "Renamed fleet 9 to \"pilot\".\n"},
{name: "server refusal", refusal: "no such fleet", wantError: "no such fleet"},
}

Expand Down Expand Up @@ -247,21 +247,59 @@ func TestFleetRenameArguments(t *testing.T) {

func TestFleetTransfer(t *testing.T) {
tests := []struct {
name string
refusal string
wantOutput string
wantError string
name string
answer string
fleets string
refusal string
wantTransferred bool
wantOutput string
wantError string
}{
{name: "transferred", wantOutput: "Transferred the fleet to successor@example.com.\n"},
{name: "server refusal", refusal: "the new owner has no account", wantError: "the new owner has no account"},
{
name: "confirmed with y",
answer: "y\n",
wantTransferred: true,
wantOutput: "Hand fleet \"pilot\" to successor@example.com? They become the owner, and you lose access to the fleet, its devices and its credit. [y/N] Transferred fleet \"pilot\" to successor@example.com.\n",
},
{name: "confirmed with yes", answer: "YES\n", wantTransferred: true},
{
name: "declined with n",
answer: "n\n",
wantOutput: "Hand fleet \"pilot\" to successor@example.com? They become the owner, and you lose access to the fleet, its devices and its credit. [y/N] Nothing transferred.\n",
},
{name: "declined by default", answer: "\n"},
{name: "closed input", answer: ""},
{
name: "the server refuses after the confirmation",
answer: "y\n",
refusal: "no one with that email address has logged in yet",
wantTransferred: true,
wantError: "no one with that email address has logged in yet",
},
{
name: "a fleet that is not yours",
answer: "y\n",
fleets: `[]`,
wantError: "no such fleet",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
transferredPath := ""
transferredTo := ""
fleets := test.fleets

if fleets == "" {
fleets = `[{"id":3,"name":"pilot","owner":true}]`
}

mux := http.NewServeMux()

mux.HandleFunc("GET /fleets", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, fleets)
})

mux.HandleFunc("POST /fleets/{id}/owner", func(w http.ResponseWriter, r *http.Request) {
body := struct {
Email string `json:"email"`
Expand All @@ -281,23 +319,35 @@ func TestFleetTransfer(t *testing.T) {

session, out := apitest.LoggedInSession(t, mux)

session.In = strings.NewReader(test.answer)

err := Transfer(session, []string{"3", "successor@example.com"})

printed := out.String()

if test.wantError != "" {
if err == nil || err.Error() != test.wantError {
t.Fatalf("error = %v, want %q", err, test.wantError)
}

if strings.Contains(printed, "Transferred") {
t.Errorf("the output %q says the fleet was handed over although it was not", printed)
}
} else if err != nil {
t.Fatal(err)
}

if transferredPath != "/fleets/3/owner" || transferredTo != "successor@example.com" {
if test.wantOutput != "" && printed != test.wantOutput {
t.Errorf("output = %q, want %q", printed, test.wantOutput)
}

if test.wantTransferred && (transferredPath != "/fleets/3/owner" || transferredTo != "successor@example.com") {
t.Errorf("the server saw %q handed to %q, want %q handed to %q",
transferredPath, transferredTo, "/fleets/3/owner", "successor@example.com")
}

if out.String() != test.wantOutput {
t.Errorf("output = %q, want %q", out.String(), test.wantOutput)
if !test.wantTransferred && transferredPath != "" {
t.Errorf("the server saw %q handed over although the confirmation was declined", transferredPath)
}
})
}
Expand Down Expand Up @@ -397,6 +447,10 @@ func TestFleetDelete(t *testing.T) {
t.Errorf("the server saw %q deleted, want %q", deletedPath, "/fleets/3")
}

if test.wantDeleted && test.wantError == "" && !strings.Contains(printed, "Deleted fleet \"pilot\".") {
t.Errorf("the output %q does not name the fleet it deleted", printed)
}

if !test.wantDeleted && deletedPath != "" {
t.Errorf("the server saw %q deleted although the confirmation was declined", deletedPath)
}
Expand All @@ -414,18 +468,18 @@ func TestFleetDeletePromptStatesForfeitedCredit(t *testing.T) {
{
name: "remaining credit is stated",
balance: `[{"fleet":3,"balance":"12.340000","currency":"eur"}]`,
wantOutput: "Delete \"pilot\", release its devices, and forfeit its remaining €12.34 of credit? It erases them all, and claiming one again means pressing its pairing button in person. [y/N] Nothing deleted.\n",
wantOutput: "Delete fleet \"pilot\", release its devices, and forfeit its remaining €12.34 of credit? It wipes their files and restarts their code, and claiming one again means pressing its pairing button in person. [y/N] Nothing deleted.\n",
},
{
name: "an empty balance stays quiet",
balance: `[{"fleet":3,"balance":"0","currency":"eur"}]`,
wantOutput: "Delete \"pilot\" and release its devices? It erases them all, and claiming one again means pressing its pairing button in person. [y/N] Nothing deleted.\n",
wantOutput: "Delete fleet \"pilot\" and release its devices? It wipes their files and restarts their code, and claiming one again means pressing its pairing button in person. [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 \"pilot\", release its devices, and forfeit its remaining credit? It erases them all, and claiming one again means pressing its pairing button in person. [y/N] Nothing deleted.\n",
wantOutput: "Delete fleet \"pilot\", release its devices, and forfeit its remaining credit? It wipes their files and restarts their code, and claiming one again means pressing its pairing button in person. [y/N] Nothing deleted.\n",
wantAbsent: "€",
},
}
Expand Down Expand Up @@ -458,8 +512,8 @@ func TestFleetDeletePromptStatesForfeitedCredit(t *testing.T) {
t.Errorf("output = %q, want %q", printed, test.wantOutput)
}

if !strings.Contains(printed, "It erases them all, and claiming one again means pressing its pairing button in person.") {
t.Errorf("the prompt %q does not say the devices are erased", printed)
if !strings.Contains(printed, "It wipes their files and restarts their code, and claiming one again means pressing its pairing button in person.") {
t.Errorf("the prompt %q does not say what releasing the devices does to them", printed)
}

if test.wantAbsent != "" && strings.Contains(printed, test.wantAbsent) {
Expand Down
Loading