Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new: Add support for Parent/Child account switching #513

Merged
merged 3 commits into from
Jun 3, 2024
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -77,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
280 changes: 3 additions & 277 deletions go.work.sum

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,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)
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
Loading