Skip to content

Commit

Permalink
new: Add support for Parent/Child account switching (#455)
Browse files Browse the repository at this point in the history
* Implement new endpoints

Add UserType enum

Add test case for user type

User fixtures

Add unit tests

Oops

* gofumpt

* Address feedback

* Consume new helpers

* test

* Add unit directory; move mock tests

* Add integration test

* Use variadic

* oops

* add test note

* oops

* Use testify in unit tests

* Make account tests opt-in

* Update README

* oops

* oops

* drop opt in tests

* Drop optInTest

* Add User.LastLogin

* SDF

* Support test tags in makefile
  • Loading branch information
lgarber-akamai committed Mar 6, 2024
1 parent 377f746 commit 1a81a0d
Show file tree
Hide file tree
Showing 28 changed files with 1,017 additions and 394 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ citest: lint test

testunit:
go test -v $(PACKAGES) $(ARGS)
cd test && make unit-test

testint:
cd test && make test
Expand Down Expand Up @@ -76,7 +77,7 @@ run_fixtures:
LINODE_API_VERSION="v4beta" \
LINODE_URL="$(LINODE_URL)" \
GO111MODULE="on" \
go test -timeout=$(TEST_TIMEOUT) -v $(ARGS)
go test --tags $(TEST_TAGS) -timeout=$(TEST_TIMEOUT) -v $(ARGS)

sanitize:
@echo "* Sanitizing fixtures"
Expand Down
31 changes: 30 additions & 1 deletion account.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package linodego

import "context"
import (
"context"
"encoding/json"
"time"

"github.com/linode/linodego/internal/parseabletime"
)

// Account associated with the token in use.
type Account struct {
Expand All @@ -20,6 +26,29 @@ type Account struct {
Phone string `json:"phone"`
CreditCard *CreditCard `json:"credit_card"`
EUUID string `json:"euuid"`
BillingSource string `json:"billing_source"`
Capabilities []string `json:"capabilities"`
ActiveSince *time.Time `json:"-"`
}

// UnmarshalJSON implements the json.Unmarshaler interface
func (account *Account) UnmarshalJSON(b []byte) error {
type Mask Account

p := struct {
*Mask
ActiveSince *parseabletime.ParseableTime `json:"active_since"`
}{
Mask: (*Mask)(account),
}

if err := json.Unmarshal(b, &p); err != nil {
return err
}

account.ActiveSince = (*time.Time)(p.ActiveSince)

return nil
}

// CreditCard information associated with the Account.
Expand Down
44 changes: 44 additions & 0 deletions account_child.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package linodego

import (
"context"
)

// ChildAccount represents an account under the current account.
// NOTE: This is an alias to prevent any future breaking changes.
type ChildAccount = Account

// ChildAccountToken represents a short-lived token created using
// the CreateChildAccountToken(...) function.
// NOTE: This is an alias to prevent any future breaking changes.
type ChildAccountToken = Token

// ListChildAccounts lists child accounts under the current account.
func (c *Client) ListChildAccounts(ctx context.Context, opts *ListOptions) ([]ChildAccount, error) {
return getPaginatedResults[ChildAccount](
ctx,
c,
"account/child-accounts",
opts,
)
}

// GetChildAccount gets a single child accounts under the current account.
func (c *Client) GetChildAccount(ctx context.Context, euuid string) (*ChildAccount, error) {
return doGETRequest[ChildAccount](
ctx,
c,
formatAPIPath("account/child-accounts/%s", euuid),
)
}

// CreateChildAccountToken creates a short-lived token that can be used to
// access the Linode API under a child account.
// The attributes of this token are not currently configurable.
func (c *Client) CreateChildAccountToken(ctx context.Context, euuid string) (*ChildAccountToken, error) {
return doPOSTRequest[ChildAccountToken, any](
ctx,
c,
formatAPIPath("account/child-accounts/%s/token", euuid),
)
}
10 changes: 10 additions & 0 deletions account_users.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,20 @@ import (
"github.com/linode/linodego/internal/parseabletime"
)

type UserType string

const (
UserTypeProxy UserType = "proxy"
UserTypeParent UserType = "parent"
UserTypeChild UserType = "child"
UserTypeDefault UserType = "default"
)

// User represents a User object
type User struct {
Username string `json:"username"`
Email string `json:"email"`
UserType UserType `json:"user_type"`
Restricted bool `json:"restricted"`
TFAEnabled bool `json:"tfa_enabled"`
SSHKeys []string `json:"ssh_keys"`
Expand Down
2 changes: 1 addition & 1 deletion go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,4 @@ golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c h1:GohjlNKauSai7gN4wsJkeZ3WAJx4Sh+oT/b5IYn5suA=
k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
7 changes: 6 additions & 1 deletion test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@ smoketest:
LINODE_TOKEN="awesometokenawesometokenawesometoken" \
LINODE_API_VERSION="v4beta" \
GO111MODULE="on" \
go test -v -run smoke ./integration/...
go test -v -run smoke ./integration/...


.PHONY: unit-test
unit-test:
go test -v ./unit $(ARGS)
2 changes: 2 additions & 0 deletions test/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/jarcoal/httpmock v1.3.1
github.com/linode/linodego v1.25.0
github.com/linode/linodego/k8s v0.0.0-00010101000000-000000000000
github.com/stretchr/testify v1.8.4
golang.org/x/net v0.21.0
golang.org/x/oauth2 v0.17.0
k8s.io/client-go v0.28.1
Expand All @@ -31,6 +32,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/term v0.17.0 // indirect
Expand Down
1 change: 1 addition & 0 deletions test/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
Expand Down
40 changes: 40 additions & 0 deletions test/integration/account_child_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//go:build parent_child

package integration

import (
"context"
"reflect"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
)

// NOTE: These fixtures are expected to be run under a parent account.
func TestAccountChild_basic(t *testing.T) {
client, teardown := createTestClient(t, "fixtures/TestAccountChild_basic")
defer teardown()

childAccounts, err := client.ListChildAccounts(context.Background(), nil)
require.NoError(t, err)
require.Greater(
t,
len(childAccounts),
0,
"number of child accounts should be > 0",
)

childAccount, err := client.GetChildAccount(context.Background(), childAccounts[0].EUUID)
require.NoError(t, err)
require.True(
t,
reflect.DeepEqual(*childAccount, childAccounts[0]),
"child accounts should be equal",
cmp.Diff(*childAccount, childAccounts[0]),
)

token, err := client.CreateChildAccountToken(context.Background(), childAccount.EUUID)
require.NoError(t, err)
require.Greater(t, len(token.Token), 0)
}
6 changes: 6 additions & 0 deletions test/integration/account_users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ func TestUser_Get_smoke(t *testing.T) {
if user.VerifiedPhoneNumber != nil {
t.Error("expected phone number is not set")
}
if user.UserType == "" {
t.Errorf("expected user type, got none")
}
}

func TestUser_Update(t *testing.T) {
Expand Down Expand Up @@ -150,6 +153,9 @@ func TestUsers_List(t *testing.T) {
if newUser.VerifiedPhoneNumber != nil {
t.Error("expected phone number is not set")
}
if newUser.UserType == "" {
t.Errorf("expected user type, got none")
}
}

func createUser(t *testing.T, client *linodego.Client, userModifiers ...userModifier) (*User, func()) {
Expand Down
12 changes: 6 additions & 6 deletions test/integration/fixtures/ExampleGetAccount.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ interactions:
url: https://api.linode.com/v4beta/account
method: GET
response:
body: '{"company": "Linode", "email": "lgarber@linode.com", "first_name": "Lena",
"last_name": "Garber", "address_1": "2228 Lenox Ridge Ct NE", "address_2": "NA",
"city": "Atlanta", "state": "GA", "zip": "30319", "country": "US", "phone":
"6787613864", "balance": 0.0, "tax_id": "", "billing_source": "linode", "credit_card":
{"last_four": "1488", "expiry": "02/2022"}, "balance_uninvoiced": 0.0, "active_since":
body: '{"company": "Linode", "email": "foo@linode.com", "first_name": "foo",
"last_name": "bar", "address_1": "123 Street Street", "address_2": "NA",
"city": "Philadelphia", "state": "PA", "zip": "30000", "country": "US", "phone":
"1234567891", "balance": 0.0, "tax_id": "", "billing_source": "linode", "credit_card":
{"last_four": "1234", "expiry": "02/2020"}, "balance_uninvoiced": 0.0, "active_since":
"2018-01-02T03:04:05", "capabilities": ["Linodes", "NodeBalancers", "Block Storage",
"Object Storage", "Kubernetes", "Cloud Firewall", "Vlans", "LKE HA Control Planes",
"Machine Images", "Managed Databases"], "active_promotions": [], "euuid": "590F6313-2E4D-47CE-90943D2F724A87CB"}'
"Machine Images", "Managed Databases"], "active_promotions": [], "euuid": "FFFFFFFF-2E4D-47CE-FFFFFFFFFFFFFFFFF"}'
headers:
Access-Control-Allow-Credentials:
- "true"
Expand Down
13 changes: 8 additions & 5 deletions test/integration/fixtures/TestAccountAvailability_Get.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,19 @@ interactions:
Access-Control-Expose-Headers:
- X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status
Cache-Control:
- private, max-age=0, s-maxage=0, no-cache, no-store
- private, max-age=60, s-maxage=60
- max-age=0, no-cache, no-store
Connection:
- keep-alive
Content-Length:
- "40"
Content-Security-Policy:
- default-src 'none'
Content-Type:
- application/json
Server:
- nginx
Expires:
- Tue, 13 Feb 2024 18:57:39 GMT
Pragma:
- no-cache
Strict-Transport-Security:
- max-age=31536000
Vary:
Expand All @@ -52,7 +55,7 @@ interactions:
X-Oauth-Scopes:
- '*'
X-Ratelimit-Limit:
- "1200"
- "400"
X-Xss-Protection:
- 1; mode=block
status: 200 OK
Expand Down
38 changes: 25 additions & 13 deletions test/integration/fixtures/TestAccountAvailability_List.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,22 @@ interactions:
url: https://api.linode.com/v4beta/account/availability
method: GET
response:
body: '{"data": [{"region": "ap-west", "unavailable": []}, {"region": "ca-central",
"unavailable": []}, {"region": "ap-southeast", "unavailable": []}, {"region":
"us-central", "unavailable": []}, {"region": "us-west", "unavailable": []},
{"region": "us-southeast", "unavailable": []}, {"region": "us-east", "unavailable":
[]}, {"region": "eu-west", "unavailable": []}, {"region": "ap-south", "unavailable":
[]}, {"region": "eu-central", "unavailable": []}, {"region": "ap-northeast",
"unavailable": []}], "page": 1, "pages": 1, "results": 11}'
body: '{"data": [{"region": "us-central", "unavailable": []}, {"region": "us-west",
"unavailable": []}, {"region": "us-southeast", "unavailable": []}, {"region":
"us-east", "unavailable": []}, {"region": "eu-west", "unavailable": []}, {"region":
"ap-south", "unavailable": []}, {"region": "eu-central", "unavailable": []},
{"region": "ap-northeast", "unavailable": ["Linodes", "NodeBalancers", "Block
Storage", "Kubernetes"]}, {"region": "ap-west", "unavailable": []}, {"region":
"ca-central", "unavailable": []}, {"region": "ap-southeast", "unavailable":
[]}, {"region": "us-iad", "unavailable": ["NodeBalancers", "Block Storage"]},
{"region": "us-ord", "unavailable": []}, {"region": "fr-par", "unavailable":
[]}, {"region": "us-sea", "unavailable": []}, {"region": "br-gru", "unavailable":
[]}, {"region": "nl-ams", "unavailable": []}, {"region": "se-sto", "unavailable":
[]}, {"region": "es-mad", "unavailable": []}, {"region": "in-maa", "unavailable":
[]}, {"region": "jp-osa", "unavailable": []}, {"region": "it-mil", "unavailable":
[]}, {"region": "us-mia", "unavailable": []}, {"region": "id-cgk", "unavailable":
[]}, {"region": "us-lax", "unavailable": []}], "page": 1, "pages": 1, "results":
25}'
headers:
Access-Control-Allow-Credentials:
- "true"
Expand All @@ -33,20 +42,23 @@ interactions:
Access-Control-Expose-Headers:
- X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status
Cache-Control:
- private, max-age=0, s-maxage=0, no-cache, no-store
- private, max-age=60, s-maxage=60
- max-age=0, no-cache, no-store
Connection:
- keep-alive
Content-Security-Policy:
- default-src 'none'
Content-Type:
- application/json
Server:
- nginx
Expires:
- Tue, 13 Feb 2024 18:57:39 GMT
Pragma:
- no-cache
Strict-Transport-Security:
- max-age=31536000
Vary:
- Accept-Encoding
- Authorization, X-Filter
- Authorization, X-Filter
- Accept-Encoding
X-Accepted-Oauth-Scopes:
- account:read_only
X-Content-Type-Options:
Expand All @@ -57,7 +69,7 @@ interactions:
X-Oauth-Scopes:
- '*'
X-Ratelimit-Limit:
- "1200"
- "400"
X-Xss-Protection:
- 1; mode=block
status: 200 OK
Expand Down
Loading

0 comments on commit 1a81a0d

Please sign in to comment.