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
23 changes: 16 additions & 7 deletions internal/cmd/resource.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"encoding/json"
"fmt"
"os"
"strings"
Expand All @@ -9,6 +10,7 @@ import (
"github.com/spf13/cobra"

"github.com/jhuiting/chargebee-cli/internal/api"
"github.com/jhuiting/chargebee-cli/internal/output"
"github.com/jhuiting/chargebee-cli/internal/timeutil"
)

Expand Down Expand Up @@ -112,8 +114,9 @@ func buildOpCmd(res ResourceDef, op OpDef, factory ClientFactory) *cobra.Command
cmd.Flags().String(f.Flag, "", f.Help)
}
if res.TimestampField != "" {
cmd.Flags().String("after", "", "only return results after this time (ISO date, unix, or relative like 7d, 24h)")
cmd.Flags().String("before", "", "only return results before this time (ISO date, unix, or relative like 7d, 24h)")
cmd.Flags().String("after", "", "filter results after this time (e.g. 2024-01-01, 7d, 24h, 1700000000)")
cmd.Flags().String("before", "", "filter results before this time (e.g. 2024-01-01, 7d, 24h, 1700000000)")
cmd.Example = fmt.Sprintf(" cb %s list --after 7d\n cb %s list --before 2024-01-01\n cb %s list -d \"%s[after]=2024-01-01\" --raw", res.Name, res.Name, res.Name, res.TimestampField)
}
}

Expand Down Expand Up @@ -142,11 +145,7 @@ func opLongHelp(res ResourceDef, op OpDef) string {
fmt.Fprintf(&b, " cb %s list --%s %s\n", res.Name, res.Filters[0].Flag, filterExampleValue(res.Filters[0]))
}
fmt.Fprintf(&b, " cb %s list -l 10\n", res.Name)
if res.TimestampField != "" {
fmt.Fprintf(&b, " cb %s list --after 2024-01-01\n", res.Name)
fmt.Fprintf(&b, " cb %s list --after 7d\n", res.Name)
fmt.Fprintf(&b, " cb %s list -d \"%s[after]=2024-01-01\" --raw", res.Name, res.TimestampField)
} else {
if res.TimestampField == "" {
fmt.Fprintf(&b, " cb %s list -d \"created_at[after]=1700000000\" --raw", res.Name)
}

Expand Down Expand Up @@ -222,6 +221,16 @@ func makeRunOp(res ResourceDef, op OpDef, factory ClientFactory) func(*cobra.Com
printResponseHeaders(os.Stderr, result)
}

if op.Name == "list" && !raw {
var envelope struct {
List []json.RawMessage `json:"list"`
}
if json.Unmarshal(result.JSON(), &envelope) == nil && envelope.List != nil && len(envelope.List) == 0 {
output.Default.Dim("No results.")
return nil
}
}

return printJSON(os.Stdout, result.JSON(), raw)
}
}
Expand Down
47 changes: 47 additions & 0 deletions internal/cmd/resource_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cmd_test

import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
Expand All @@ -12,6 +14,7 @@ import (

"github.com/jhuiting/chargebee-cli/internal/api"
"github.com/jhuiting/chargebee-cli/internal/cmd"
"github.com/jhuiting/chargebee-cli/internal/output"
)

func newResourceTestCmd(baseURL string) *cobra.Command {
Expand Down Expand Up @@ -200,3 +203,47 @@ func TestResourceRetrieveHasNoFilterFlags(t *testing.T) {
assert.Nil(t, customersCmd.Flags().Lookup("company"), "retrieve should not have filter flags")
assert.Nil(t, customersCmd.Flags().Lookup("email"), "retrieve should not have filter flags")
}

func TestResourceListEmptyShowsNoResults(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{"list": []any{}})
}))
defer srv.Close()

var stderrBuf bytes.Buffer
out := output.New(&stderrBuf, io.Discard)

origDefault := output.Default
output.Default = out
defer func() { output.Default = origDefault }()

root := newResourceTestCmd(srv.URL)
root.SetArgs([]string{"customers", "list"})
err := root.Execute()

require.NoError(t, err)
assert.Contains(t, stderrBuf.String(), "No results.")
}

func TestResourceListEmptyRawPassesThrough(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"list":[]}`))
}))
defer srv.Close()

var stderrBuf bytes.Buffer
out := output.New(&stderrBuf, io.Discard)

origDefault := output.Default
output.Default = out
defer func() { output.Default = origDefault }()

root := newResourceTestCmd(srv.URL)
root.SetArgs([]string{"customers", "list", "--raw"})
err := root.Execute()

require.NoError(t, err)
assert.NotContains(t, stderrBuf.String(), "No results.")
}
3 changes: 2 additions & 1 deletion internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,11 @@ func Execute() {

select {
case info := <-updateCh:
if info != nil && info.UpdateAvailable {
if info != nil && info.UpdateAvailable && !info.NotifiedRecently {
_, _ = fmt.Fprintln(os.Stderr)
output.Default.Warning("A new version of cb is available: %s → %s", info.CurrentVersion, info.LatestVersion)
output.Default.Dim("Update with: brew upgrade chargebee-cli")
update.MarkNotified()
}
default:
}
Expand Down
37 changes: 32 additions & 5 deletions internal/update/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ var releaseURL = "https://api.github.com/repos/jhuiting/chargebee-cli/releases/l

// Info holds the result of an update check.
type Info struct {
CurrentVersion string
LatestVersion string
UpdateAvailable bool
CurrentVersion string
LatestVersion string
UpdateAvailable bool
NotifiedRecently bool
}

type cache struct {
CheckedAt time.Time `json:"checked_at"`
LatestVersion string `json:"latest_version"`
NotifiedAt time.Time `json:"notified_at,omitempty"`
}

// CheckForUpdate checks if a newer CLI version is available.
Expand All @@ -48,20 +50,45 @@ func CheckForUpdate(ctx context.Context, currentVersion string) *Info {

cached, err := readCache(cachePath)
if err == nil && time.Since(cached.CheckedAt) < checkInterval {
return buildInfo(currentVersion, cached.LatestVersion)
info := buildInfo(currentVersion, cached.LatestVersion)
info.NotifiedRecently = !cached.NotifiedAt.IsZero() && time.Since(cached.NotifiedAt) < checkInterval
return info
}

latest, err := fetchLatestVersion(ctx)
if err != nil {
return nil
}

var notifiedAt time.Time
if cached != nil {
notifiedAt = cached.NotifiedAt
}

_ = writeCache(cachePath, &cache{
CheckedAt: time.Now(),
LatestVersion: latest,
NotifiedAt: notifiedAt,
})

return buildInfo(currentVersion, latest)
info := buildInfo(currentVersion, latest)
info.NotifiedRecently = !notifiedAt.IsZero() && time.Since(notifiedAt) < checkInterval
return info
}

// MarkNotified records that the update notification was shown, so it can be
// suppressed for the next 24 hours.
func MarkNotified() {
path := cacheFilePath()
if path == "" {
return
}
cached, err := readCache(path)
if err != nil {
return
}
cached.NotifiedAt = time.Now()
_ = writeCache(path, cached)
}

func cacheFilePath() string {
Expand Down
29 changes: 29 additions & 0 deletions internal/update/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,32 @@ func TestCheckForUpdate_UsesCache(t *testing.T) {
assert.True(t, info.UpdateAvailable)
assert.Equal(t, 1, callCount)
}

func TestCheckForUpdate_NotifiedRecently(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]string{"tag_name": "v2.0.0"})
}))
defer srv.Close()

origURL := releaseURL
releaseURL = srv.URL
defer func() { releaseURL = origURL }()

t.Setenv("CB_CONFIG_DIR", t.TempDir())
t.Setenv("CB_NO_UPDATE_CHECK", "")

// First check: not recently notified.
info := CheckForUpdate(context.Background(), "v1.0.0")
require.NotNil(t, info)
assert.True(t, info.UpdateAvailable)
assert.False(t, info.NotifiedRecently)

// Mark as notified.
MarkNotified()

// Second check: recently notified.
info = CheckForUpdate(context.Background(), "v1.0.0")
require.NotNil(t, info)
assert.True(t, info.UpdateAvailable)
assert.True(t, info.NotifiedRecently)
}
Loading