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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
account visible to the login (every sub-account for a main login,
just the login itself for a sub-account).

### Changed

- `internal/usage`: add a `(t Traffic) IsSummary() bool` helper so
callers no longer rely on the `Day == 0` magic number to distinguish
the monthly summary row from per-day entries; the table renderer is
switched over too. Document on `Space` that `UsedWebspace` is the sum
of the four sub-buckets so future readers do not double-count.

- `kasapi-cli usage traffic`: pre-validate `--year` (must be in
`[2000, currentYear+1]`) and `--month` (must be `1..12`) instead of
forwarding obvious typos to KAS. Closes #45.

### Fixed

- `internal/auth/source.go`: sharpen the `Heartbeat` doc comment.
Expand Down
20 changes: 20 additions & 0 deletions internal/cli/usage.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package cli

import (
"fmt"
"time"

"github.com/spf13/cobra"

"github.com/chmmou/kasapi-cli/internal/usage"
)

// minTrafficYear is the lower bound for --year. KAS predates this date
// but the CLI is the kasapi-cli era, so 2000 is generous enough to be
// indistinguishable from "no validation" for any realistic input while
// still rejecting obvious typos like 200 or 20256.
const minTrafficYear = 2000

// NewUsageCmd returns the "kasapi-cli usage" subcommand tree:
// space (get_space), space-detail (get_space_usage),
// traffic (get_traffic).
Expand Down Expand Up @@ -76,6 +85,17 @@ func newUsageTrafficCmd(opts *RootOptions) *cobra.Command {
Use: "traffic",
Short: "Show monthly HTTP/FTP traffic (get_traffic)",
Args: cobra.NoArgs,
PreRunE: func(_ *cobra.Command, _ []string) error {
maxYear := time.Now().Year() + 1
if year != 0 && (year < minTrafficYear || year > maxYear) {
return fmt.Errorf("--year must be between %d and %d, got %d",
minTrafficYear, maxYear, year)
}
if month != 0 && (month < 1 || month > 12) {
return fmt.Errorf("--month must be between 1 and 12, got %d", month)
}
return nil
},
RunE: func(cmd *cobra.Command, _ []string) error {
api, err := BuildAPIClient(opts)
if err != nil {
Expand Down
43 changes: 43 additions & 0 deletions internal/cli/usage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cli_test

import (
"bytes"
"strings"
"testing"

"github.com/chmmou/kasapi-cli/internal/cli"
)

func TestUsageTrafficRejectsInvalidFlags(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
want string
}{
{"month negative", []string{"usage", "traffic", "--month", "-1"}, "--month must be between 1 and 12"},
{"month too high", []string{"usage", "traffic", "--month", "13"}, "--month must be between 1 and 12"},
{"year too low", []string{"usage", "traffic", "--year", "1999"}, "--year must be between"},
{"year typo", []string{"usage", "traffic", "--year", "20256"}, "--year must be between"},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
root, opts := cli.NewRootCmd()
root.AddCommand(cli.NewUsageCmd(opts))

var buf bytes.Buffer
root.SetOut(&buf)
root.SetErr(&buf)
root.SetArgs(tc.args)
err := root.Execute()
if err == nil {
t.Fatalf("Execute returned nil, want error containing %q", tc.want)
}
if !strings.Contains(err.Error(), tc.want) {
t.Errorf("err = %v, want substring %q", err, tc.want)
}
})
}
}
11 changes: 10 additions & 1 deletion internal/usage/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ type Caller interface {
// resource type. KAS reports webspace and max_webspace as xsd:int and
// the per-resource breakdowns as xsd:string-encoded numbers; we parse
// both into int64 so the per-account totals add up without overflow.
//
// UsedWebspace is the sum of UsedHTDocsSpace, UsedChrootSpace,
// UsedDatabaseSpace, and UsedMailaccountSpace — do not add the
// sub-buckets to UsedWebspace when computing totals.
type Space struct {
AccountLogin string `json:"account_login" yaml:"account_login"`
LastCalculation int64 `json:"last_calculation" yaml:"last_calculation"`
Expand Down Expand Up @@ -68,6 +72,11 @@ type Traffic struct {
Comment string `json:"comment,omitempty" yaml:"comment,omitempty"`
}

// IsSummary reports whether t is the monthly summary row that KAS
// emits alongside the per-day entries. The summary is identified by a
// zero Day; literal day-zero never occurs in get_traffic responses.
func (t Traffic) IsSummary() bool { return t.Day == 0 }

// TrafficList is the typed payload of get_traffic; satisfies
// cli.Tabular.
type TrafficList []Traffic
Expand Down Expand Up @@ -292,7 +301,7 @@ func (l TrafficList) TableRows() [][]string {
rows := make([][]string, 0, len(l))
for _, t := range l {
day := "*"
if t.Day != 0 {
if !t.IsSummary() {
day = fmt.Sprintf("%02d", t.Day)
}
rows = append(rows, []string{
Expand Down
6 changes: 6 additions & 0 deletions internal/usage/usage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ func TestDecodeTraffic(t *testing.T) {
if day.Day != 1 {
t.Errorf("day.Day = %d, want 1", day.Day)
}
if !summary.IsSummary() {
t.Errorf("summary.IsSummary() = false, want true")
}
if day.IsSummary() {
t.Errorf("day.IsSummary() = true, want false")
}
}

func TestClientSpace(t *testing.T) {
Expand Down
Loading