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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ Each option needs a published release.
./superstack
```

[Nix](https://nixos.org) users need no Go install: `nix develop` enters the
dev shell, and with [direnv](https://direnv.net) hooked into your shell,
`direnv allow` run once in the checkout loads it automatically from then on.
[Nix](https://nixos.org) users: `nix develop` enters the dev shell, and with
[direnv](https://direnv.net) hooked into your shell, `direnv allow` run once
in the checkout loads it automatically from then on.

## Release setup

Expand Down Expand Up @@ -116,7 +116,7 @@ Do everything below once.
git push -u origin version-0.1.0
```

1. Merging creates a new commit, and the tag has to point at that one:
1. Tag the new commit that merging created:

```sh
git checkout main && git pull
Expand Down
98 changes: 98 additions & 0 deletions internal/commands/account_balance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package commands

import (
"encoding/json"
"errors"
"fmt"
"os"
"strconv"
)

func AccountBalance(arguments []string) error {

jsonOutput := false

positionals := []string{}

for _, argument := range arguments {
if argument == "--json" {
jsonOutput = true
continue
}

positionals = append(positionals, argument)
}

if len(positionals) > 1 {
return errors.New("account balance takes at most one fleet id")
}

chosenFleetId := int64(0)

if len(positionals) == 1 {
parsed, err := strconv.ParseInt(positionals[0], 10, 64)

if err != nil || parsed < 1 {
return errors.New("the fleet id is the number shown by fleet list")
}

chosenFleetId = parsed
}

fleets, err := fetchFleets()

if err != nil {
return err
}

fleetNames := map[int64]string{}

for _, fleet := range fleets {
fleetNames[fleet.Id] = fleet.Name
}

if chosenFleetId != 0 {
if _, found := fleetNames[chosenFleetId]; !found {
return errors.New("no such fleet")
}
}

fetched, err := fetchBalances()

if err != nil {
return err
}

balances := []balanceEntry{}

for _, balance := range fetched {
if chosenFleetId == 0 || balance.Fleet == chosenFleetId {
balances = append(balances, balance)
}
}

if jsonOutput {
return json.NewEncoder(os.Stdout).Encode(balances)
}

if len(balances) == 0 {
fmt.Println("No fleets yet. Create one with fleet create.")
return nil
}

idWidth := len("ID")
nameWidth := len("NAME")

for _, balance := range balances {
idWidth = max(idWidth, len(strconv.FormatInt(balance.Fleet, 10)))
nameWidth = max(nameWidth, len(fleetNames[balance.Fleet]))
}

fmt.Printf("%-*s %-*s %s\n", idWidth, "ID", nameWidth, "NAME", "BALANCE")

for _, balance := range balances {
fmt.Printf("%-*d %-*s %s\n", idWidth, balance.Fleet, nameWidth, fleetNames[balance.Fleet], formatBalance(balance))
}

return nil
}
130 changes: 130 additions & 0 deletions internal/commands/account_balance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package commands

import (
"fmt"
"net/http"
"strings"
"testing"
)

func TestAccountBalance(t *testing.T) {
tests := []struct {
name string
arguments []string
fleets string
balances string
wantLines []string
wantAbsent []string
wantExact string
wantError string
}{
{
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"}]`,
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"}]`,
wantLines: []string{"pilot", "€0.00"},
wantAbsent: []string{"crew"},
},
{
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",
},
{
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",
},
{
name: "machine readable with no fleets",
arguments: []string{"--json"},
fleets: `[]`,
balances: `[]`,
wantExact: "[]\n",
},
{
name: "no fleets",
arguments: []string{},
fleets: `[]`,
balances: `[]`,
wantLines: []string{"No fleets yet"},
},
{
name: "an unknown fleet",
arguments: []string{"9"},
fleets: `[{"id":1,"name":"crew","owner":true}]`,
balances: `[]`,
wantError: "no such fleet",
},
{
name: "a wordy id",
arguments: []string{"crew"},
wantError: "shown by fleet list",
},
{
name: "too many arguments",
arguments: []string{"1", "2"},
wantError: "at most one fleet id",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
mux := http.NewServeMux()

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

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

loggedInTestServer(t, mux)

printed, err := captureStdout(t, func() error {
return AccountBalance(test.arguments)
})

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

return
}

if err != nil {
t.Fatal(err)
}

if test.wantExact != "" && printed != test.wantExact {
t.Errorf("the output is %q, want exactly %q", printed, test.wantExact)
}

for _, want := range test.wantLines {
if !strings.Contains(printed, want) {
t.Errorf("the output %q does not show %q", printed, want)
}
}

for _, absent := range test.wantAbsent {
if strings.Contains(printed, absent) {
t.Errorf("the output %q shows %q although it was filtered out", printed, absent)
}
}
})
}
}
69 changes: 69 additions & 0 deletions internal/commands/account_topup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package commands

import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
)

func AccountTopup(arguments []string) error {

if len(arguments) != 1 {
return errors.New("account topup takes a fleet id")
}

fleetId, err := strconv.ParseInt(arguments[0], 10, 64)

if err != nil || fleetId < 1 {
return errors.New("the fleet id is the number shown by fleet list")
}

request, err := authenticatedRequest(http.MethodPost,
"/fleets/"+strconv.FormatInt(fleetId, 10)+"/topup", nil)

if err != nil {
return err
}

response, err := apiClient.Do(request)

if err != nil {
return fmt.Errorf("the server could not be reached: %w", err)
}

defer response.Body.Close()

if response.StatusCode != http.StatusOK {
message, _ := io.ReadAll(io.LimitReader(response.Body, 4096))

return fmt.Errorf("the server said: %s", strings.TrimSpace(string(message)))
}

opened := struct {
Url string `json:"url"`
}{}

err = json.NewDecoder(response.Body).Decode(&opened)

if err != nil || opened.Url == "" {
return errors.New("the payment page could not be opened, try again")
}

fmt.Printf("Open this link to choose an amount and pay:\n\n %s\n\nThe credit appears on the balance once the payment completes.\nPress enter to open the browser.\n", opened.Url)

_, err = bufio.NewReader(os.Stdin).ReadString('\n')

if err != nil {
return nil
}

openBrowser(opened.Url)

return nil
}
Loading