Skip to content

Commit

Permalink
Improve test coverage, add comments and improve data structures
Browse files Browse the repository at this point in the history
  • Loading branch information
szaydel committed Jan 2, 2024
1 parent 17d2256 commit 138ee22
Show file tree
Hide file tree
Showing 15 changed files with 804 additions and 57 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
run: make build

- name: Test
run: make unit-test
run: make unittest

- name: Generate coverage report
run: make cover
Expand Down
13 changes: 11 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ module github.com/szaydel/lyvecloud

go 1.20

require github.com/steinfletcher/apitest v1.5.15
require (
github.com/k0kubun/pp/v3 v3.2.0
github.com/steinfletcher/apitest v1.5.15
)

require github.com/davecgh/go-spew v1.1.1 // indirect
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
golang.org/x/text v0.3.7 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/k0kubun/pp/v3 v3.2.0 h1:h33hNTZ9nVFNP3u2Fsgz8JXiF5JINoZfFq4SvKJwNcs=
github.com/k0kubun/pp/v3 v3.2.0/go.mod h1:ODtJQbQcIRfAD3N+theGCV1m/CBxweERz2dapdz1EwA=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/steinfletcher/apitest v1.5.15 h1:AAdTN0yMbf0VMH/PMt9uB2I7jljepO6i+5uhm1PjH3c=
github.com/steinfletcher/apitest v1.5.15/go.mod h1:mF+KnYaIkuHM0C4JgGzkIIOJAEjo+EA5tTjJ+bHXnQc=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
8 changes: 5 additions & 3 deletions lyveapi/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,11 @@ func (client *Client) GetServiceAccount(svcAcctId string) (*ServiceAcct, error)
return acctInfo, nil
}

// UpdateServiceAccount updates an existing service account with changed
// settings in updatesReq and returns a nil and an error if decoding of the
// response fails, otherwise a decoded object and nil error is returned.
func (client *Client) UpdateServiceAccount(
svcAcctId string, changes ServiceAcctUpdateReq) error {
svcAcctId string, updatesReq *ServiceAcct) error {
client.mtx.RLock()
endpoint := client.apiUrl + "/service-accounts"
url := endpoint + "/" + svcAcctId
Expand All @@ -111,8 +114,7 @@ func (client *Client) UpdateServiceAccount(
var rdr io.ReadCloser
var data []byte

if data, err = json.Marshal(changes); err != nil {

if data, err = json.Marshal(updatesReq); err != nil {
return err
}

Expand Down
18 changes: 18 additions & 0 deletions lyveapi/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import (
"net/http"
)

// decodeFailedApiResponse takes a response object from the API and converts it
// into a more user-friendly native representation. It returns an exported
// error type, which has methods for accessing the status code from the API and
// the message.
func decodeFailedApiResponse(resp *http.Response) error {
body := &bytes.Buffer{}
tRdr := io.TeeReader(resp.Body, body)
Expand Down Expand Up @@ -50,6 +54,9 @@ func decodeFailedApiResponse(resp *http.Response) error {
return errors.New("dubious response from the API: " + string(bodySlc))
}

// apiRequestAuthenticated packages up requests to the API without attempting
// to authenticate first. A valid token is required to complete requests
// successfully.
func apiRequestAuthenticated(
token, method, url string, payload []byte) (io.ReadCloser, error) {
headers := map[string][]string{
Expand Down Expand Up @@ -93,6 +100,13 @@ func apiRequestAuthenticated(
return nil, err
}

// DEBUGGING:
// buf, _ := io.ReadAll(resp.Body)
// body := bytes.NewBuffer(buf)
// resp.Body = io.NopCloser(body)

// log.Printf("DEBUG (response body): %s", body.String())

// Check response from the API and if resp.StatusCode != http.StatusOK, we
// are going to have access to the error object which we should return to
// the caller.
Expand Down Expand Up @@ -121,6 +135,10 @@ func apiRequestAuthenticated(
return resp.Body, nil
}

// Authenticate attempts to authenticate against the API and returns a token
// upon successful authentication. The API will expire this token after 24
// hours. This expiration period appears to be fixed but Lyve Cloud may change
// it at any time.
func Authenticate(credentials *Credentials, authEndpointUrl string) (*Token, error) {
var data *bytes.Buffer

Expand Down
1 change: 1 addition & 0 deletions lyveapi/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

func Test_decodeFailedApiResponse(t *testing.T) {
t.Parallel()

resp := &http.Response{}

type testCase struct {
Expand Down
2 changes: 2 additions & 0 deletions lyveapi/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
)

var (
InvalidPermissionsErrMsg = "permission IDs are not known to the API; fetch a list of permissions to obtain correct IDs"
InvalidTokenErrMsg = "token presented to the API is invalid"
ExpiredTokenErrMsg = "token presented to the API is already expired"
AuthenticationFailedErrMsg = "authentication was unsuccessful; check supplied credentials"
Expand All @@ -22,6 +23,7 @@ var (
var errorCodesToErrors = map[string]string{
"ExpiredToken": ExpiredTokenErrMsg,
"InvalidToken": InvalidTokenErrMsg,
"InvalidPermissions": InvalidPermissionsErrMsg,
"AuthenticationFailed": AuthenticationFailedErrMsg,
// We are seemingly getting a trailing space in auth failure responses.
"AuthenticationFailed ": AuthenticationFailedErrMsg,
Expand Down
1 change: 0 additions & 1 deletion lyveapi/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package lyveapi
import "testing"

func TestApiCallFailedErrorMsg(t *testing.T) {

t.Parallel()

const msg1 = "This is a test message"
Expand Down
14 changes: 14 additions & 0 deletions lyveapi/monotime/time.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package monotime

import (
"time"
_ "unsafe"
)

//go:noescape
//go:linkname nanotime runtime.nanotime
func nanotime() int64

func Monotonic() time.Duration {
return time.Duration(nanotime())
}
108 changes: 90 additions & 18 deletions lyveapi/types.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package lyveapi

import (
"strconv"
"time"

"github.com/szaydel/lyvecloud/lyveapi/monotime"
)

type Credentials struct {
AccountId string `json:"accountId"`
AccessKey string `json:"accessKey"`
Expand All @@ -21,19 +28,20 @@ type CreateServiceAcctResp struct {

// Depending on the API used, some of these fields may or may not be used.
type ServiceAcct struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Enabled bool `json:"enabled,omitempty"`
ReadyState bool `json:"readyState,omitempty"`
Permissions []string `json:"permissions,omitempty"`
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
ExpirationDate string `json:"expirationDate"`
ReadyState bool `json:"readyState"`
Permissions []string `json:"permissions,omitempty"`
}

type ServiceAcctUpdateReq struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Permissions []string `json:"permissions,omitempty"`
}
// type ServiceAcctUpdateReq struct {
// Name string `json:"name"`
// Description string `json:"description,omitempty"`
// Permissions []string `json:"permissions,omitempty"`
// }

type ServiceAcctList []ServiceAcct

Expand Down Expand Up @@ -78,15 +86,27 @@ type Token struct {
ExpirationSec string `json:"expirationSec,omitempty"`
}

func (t Token) ExpiresMonoNanos() (time.Duration, error) {
var err error
var secs int64

if secs, err = strconv.ParseInt(t.ExpirationSec, 10, 64); err != nil {
return 0, err
}

return time.Duration(secs*1e9) + monotime.Monotonic(), nil
}

// Bucket describes usage of a particular bucket.
type Bucket struct {
Name string `json:"name"` // Name is the name of the given bucket
UsageGB float64 `json:"usageGB"` // UsageGB reports GBs used by bucket
}

// UsageInBytes converts from the gigabytes reported by the API to bytes. A
// gigabyte (GB) is 1e9 bytes.
func (b Bucket) UsageInBytes() float64 {
// BytesUsed converts from the gigabytes reported by the API to bytes. The API
// uses base10 values for reporting usage, where a Gigabyte is 1000 Megabytes
// and 1 Megabyte is 1000 Kilobytes, etc.
func (b Bucket) BytesUsed() float64 {
return 1e9 * b.UsageGB
}

Expand All @@ -111,32 +131,72 @@ type SubAccount struct {
Trial int `json:"trial,omitempty"`
}

type Buckets []Bucket

// BytesUsedCombined returns a sum of usages across all buckets in the given
// list of buckets.
func (b Buckets) BytesUsedCombined() uint64 {
var tot float64
for _, b := range b {
tot += b.BytesUsed()
}

return uint64(tot)
}

func (b Buckets) BytesUsedByName(name string) uint64 {
for _, b := range b {
if b.Name == name {
return uint64(b.UsageGB)
}
}
return 0
}

type Usages []Usage

func (us Usages) MonthlyTotalUsageGB() map[MonthYearTuple]float64 {
m := make(map[MonthYearTuple]float64, len(us))
for _, u := range us {
my := MonthYearTuple{u.Month, u.Year}
m[my] = u.TotalUsageGB
}
return m
}

// Usage reports various bucket usage details and included fields will vary
// depending upon whether the query is for current usage or monthly usage.
type Usage struct {
// Year only used in monthly usage report
Year uint16 `json:"year,omitempty"`
Year Year `json:"year,omitempty"`
// Month only used in monthly usage report
Month Month `json:"month,omitempty"`
// NumBuckets only used in current usage report
NumBuckets int `json:"numBuckets,omitempty"`
// TotalUsageGB is the amount of space consumed in Gigabytes (hopefully)
TotalUsageGB float64 `json:"totalUsageGB"`
Buckets []Bucket `json:"buckets,omitempty"`
Buckets Buckets `json:"buckets,omitempty"`
SubAccounts []SubAccount `json:"subAccounts,omitempty"`
}

// BytesUsedCombined returns combined usage in bytes across all buckets in the
// given list of buckets. The API uses base10 values for reporting usage, where
// a Gigabyte is 1000 Megabytes and 1 Megabyte is 1000 Kilobytes, etc.
func (u Usage) BytesUsedCombined() uint64 {
return uint64(u.TotalUsageGB * 1e9)
}

// MonthlyUsageResp is the response object containing by month usage
// information by bucket. If the account performing the query is a sub-account,
// the SubAccounts field will not contain any data, because sub-accounts cannot
// contain sub-accounts.
type MonthlyUsageResp struct {
// UsageByBucket contains a slice of Usage structs for all buckets under the master account or the sub-account for each month in the range.
UsageByBucket []Usage `json:"usageByBucket,omitempty"`
UsageByBucket Usages `json:"usageByBucket,omitempty"`
// UsageBySubAccount will be an empty slice unless the data is requested
// with credentials belonging to a "master" account. In most instances data
// will be queried with credentials belonging to a sub-account.
UsageBySubAccount []Usage `json:"usageBySubAccount,omitempty"`
UsageBySubAccount Usages `json:"usageBySubAccount,omitempty"`
}

// CurrentUsageResp is the response object containing usage information by
Expand All @@ -153,4 +213,16 @@ type CurrentUsageResp struct {
TotalUsageGB float64 `json:"totalUsageGB"`
SubAccounts []SubAccount `json:"subAccounts,omitempty"`
} `json:"usageBySubAccount,omitempty"`
// UsageBySubAccount UsageBySubAccount `json:"usageBySubAccount,omitempty"`
}

// type UsageBySubAccount struct {
// TotalUsageGB float64 `json:"totalUsageGB"`
// SubAccounts []SubAccount `json:"subAccounts,omitempty"`
// }

// // BytesUsedCombined returns combined usage across all buckets for the given
// // sub-account as bytes.
// func (usages UsageBySubAccount) BytesUsedCombined() uint64 {
// return uint64(usages.TotalUsageGB * 1e9)
// }
36 changes: 34 additions & 2 deletions lyveapi/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package lyveapi
import (
"math"
"testing"
"time"

"github.com/szaydel/lyvecloud/lyveapi/monotime"
)

func TestBucketUsageInBytes(t *testing.T) {
func TestBucketBytesUsed(t *testing.T) {
t.Parallel()

const expected = 1.45656e+12
Expand All @@ -14,9 +17,38 @@ func TestBucketUsageInBytes(t *testing.T) {
UsageGB: 1456.56,
}

t.Log(almostEqual(expected, bucket.UsageInBytes(), 0.01))
if !almostEqual(expected, bucket.BytesUsed(), 0.01) {
t.Errorf("Usage expected to be ~ %v; got %v",
expected, bucket.BytesUsed())
}
}

func almostEqual(expected, actual, epsilon float64) bool {
return math.Abs(expected-actual) < epsilon
}

func TestTokenExpiresMonoNanos(t *testing.T) {
t.Parallel()

const low time.Duration = 12000000000000
const high time.Duration = 12000000100000

token := Token{
Token: "mock-token-string",
ExpirationSec: "12000",
}

nowMonotonic := monotime.Monotonic()
e, _ := token.ExpiresMonoNanos()
if !approx(e-nowMonotonic, low, high) {
t.Errorf("Value %v not in the range '%v - %v'",
e-nowMonotonic, low, high)
}
}

func approx(n, low, high time.Duration) bool {
if n < low || n > high {
return false
}
return true
}
Loading

0 comments on commit 138ee22

Please sign in to comment.