diff --git a/account.go b/account.go index ef8588ef1..d7abb697a 100644 --- a/account.go +++ b/account.go @@ -31,22 +31,56 @@ type Account struct { ActiveSince *time.Time `json:"active_since"` } +// AccountUpdateOptions fields are those accepted by UpdateAccount +type AccountUpdateOptions struct { + Address1 string `json:"address_1,omitempty"` + Address2 string `json:"address_2,omitempty"` + City string `json:"city,omitempty"` + Company string `json:"company,omitempty"` + Country string `json:"country,omitempty"` + Email string `json:"email,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Phone string `json:"phone,omitempty"` + State string `json:"state,omitempty"` + TaxID string `json:"tax_id,omitempty"` + Zip string `json:"zip,omitempty"` +} + +// GetUpdateOptions converts an Account to AccountUpdateOptions for use in UpdateAccount +func (i Account) GetUpdateOptions() (o AccountUpdateOptions) { + o.Address1 = i.Address1 + o.Address2 = i.Address2 + o.City = i.City + o.Company = i.Company + o.Country = i.Country + o.Email = i.Email + o.FirstName = i.FirstName + o.LastName = i.LastName + o.Phone = i.Phone + o.State = i.State + o.TaxID = i.TaxID + o.Zip = i.Zip + + return +} + // UnmarshalJSON implements the json.Unmarshaler interface -func (account *Account) UnmarshalJSON(b []byte) error { +func (i *Account) UnmarshalJSON(b []byte) error { type Mask Account p := struct { *Mask ActiveSince *parseabletime.ParseableTime `json:"active_since"` }{ - Mask: (*Mask)(account), + Mask: (*Mask)(i), } if err := json.Unmarshal(b, &p); err != nil { return err } - account.ActiveSince = (*time.Time)(p.ActiveSince) + i.ActiveSince = (*time.Time)(p.ActiveSince) return nil } @@ -59,11 +93,10 @@ type CreditCard struct { // GetAccount gets the contact and billing information related to the Account. func (c *Client) GetAccount(ctx context.Context) (*Account, error) { - e := "account" - response, err := doGETRequest[Account](ctx, c, e) - if err != nil { - return nil, err - } + return doGETRequest[Account](ctx, c, "account") +} - return response, nil +// UpdateAccount updates the Account +func (c *Client) UpdateAccount(ctx context.Context, opts AccountUpdateOptions) (*Account, error) { + return doPUTRequest[Account](ctx, c, "account", opts) } diff --git a/account_agreements.go b/account_agreements.go new file mode 100644 index 000000000..18150652d --- /dev/null +++ b/account_agreements.go @@ -0,0 +1,37 @@ +package linodego + +import "context" + +// AccountAgreements represents the agreements and their acceptance status for an Account +type AccountAgreements struct { + EUModel bool `json:"eu_model"` + MasterServiceAgreement bool `json:"master_service_agreement"` + PrivacyPolicy bool `json:"privacy_policy"` +} + +// AccountAgreementsUpdateOptions fields are those accepted by UpdateAccountAgreements +type AccountAgreementsUpdateOptions struct { + EUModel bool `json:"eu_model,omitempty"` + MasterServiceAgreement bool `json:"master_service_agreement,omitempty"` + PrivacyPolicy bool `json:"privacy_policy,omitempty"` +} + +// GetUpdateOptions converts an AccountAgreements to AccountAgreementsUpdateOptions for use in UpdateAccountAgreements +func (i AccountAgreements) GetUpdateOptions() (o AccountAgreementsUpdateOptions) { + o.EUModel = i.EUModel + o.MasterServiceAgreement = i.MasterServiceAgreement + o.PrivacyPolicy = i.PrivacyPolicy + + return +} + +// GetAccountAgreements gets all agreements and their acceptance status for the Account. +func (c *Client) GetAccountAgreements(ctx context.Context) (*AccountAgreements, error) { + return doGETRequest[AccountAgreements](ctx, c, "account/agreements") +} + +// AcknowledgeAccountAgreements acknowledges account agreements for the Account +func (c *Client) AcknowledgeAccountAgreements(ctx context.Context, opts AccountAgreementsUpdateOptions) error { + _, err := doPOSTRequest[AccountAgreements](ctx, c, "account/agreements", opts) + return err +} diff --git a/account_maintenance.go b/account_maintenance.go new file mode 100644 index 000000000..c1047164d --- /dev/null +++ b/account_maintenance.go @@ -0,0 +1,51 @@ +package linodego + +import ( + "context" + "encoding/json" + "time" + + "github.com/linode/linodego/internal/parseabletime" +) + +// AccountMaintenance represents a Maintenance object for any entity a user has permissions to view +type AccountMaintenance struct { + Entity *Entity `json:"entity"` + Reason string `json:"reason"` + Status string `json:"status"` + Type string `json:"type"` + When *time.Time `json:"when"` +} + +// The entity being affected by maintenance +type Entity struct { + ID int `json:"id"` + Label string `json:"label"` + Type string `json:"type"` + URL string `json:"url"` +} + +// UnmarshalJSON implements the json.Unmarshaler interface +func (accountMaintenance *AccountMaintenance) UnmarshalJSON(b []byte) error { + type Mask AccountMaintenance + + p := struct { + *Mask + When *parseabletime.ParseableTime `json:"when"` + }{ + Mask: (*Mask)(accountMaintenance), + } + + if err := json.Unmarshal(b, &p); err != nil { + return err + } + + accountMaintenance.When = (*time.Time)(p.When) + + return nil +} + +// ListMaintenances lists Account Maintenance objects for any entity a user has permissions to view +func (c *Client) ListMaintenances(ctx context.Context, opts *ListOptions) ([]AccountMaintenance, error) { + return getPaginatedResults[AccountMaintenance](ctx, c, "account/maintenance", opts) +} diff --git a/go.work.sum b/go.work.sum index f39fc3e03..d1c29ab5a 100644 --- a/go.work.sum +++ b/go.work.sum @@ -29,6 +29,7 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= diff --git a/test/integration/account_agreements_test.go b/test/integration/account_agreements_test.go new file mode 100644 index 000000000..bb5268a94 --- /dev/null +++ b/test/integration/account_agreements_test.go @@ -0,0 +1,16 @@ +package integration + +import ( + "context" + "testing" +) + +func TestAccountAgreements_Get(t *testing.T) { + client, fixtureTeardown := createTestClient(t, "fixtures/TestAccountAgreements_List") + defer fixtureTeardown() + + _, err := client.GetAccountAgreements(context.Background()) + if err != nil { + t.Errorf("Error getting agreements, expected struct, got error %v", err) + } +} diff --git a/test/integration/account_maintenance_test.go b/test/integration/account_maintenance_test.go new file mode 100644 index 000000000..d3b49c574 --- /dev/null +++ b/test/integration/account_maintenance_test.go @@ -0,0 +1,19 @@ +package integration + +import ( + "context" + "testing" + + "github.com/linode/linodego" +) + +func TestAccountMaintenances_List(t *testing.T) { + client, fixtureTeardown := createTestClient(t, "fixtures/TestAccountMaintenances_List") + defer fixtureTeardown() + + listOpts := linodego.NewListOptions(0, "") + _, err := client.ListMaintenances(context.Background(), listOpts) + if err != nil { + t.Errorf("Error listing maintenances, expected array, got error %v", err) + } +} diff --git a/test/integration/fixtures/TestAccountAgreements_List.yaml b/test/integration/fixtures/TestAccountAgreements_List.yaml new file mode 100644 index 000000000..28af004f8 --- /dev/null +++ b/test/integration/fixtures/TestAccountAgreements_List.yaml @@ -0,0 +1,66 @@ +--- +version: 1 +interactions: +- request: + body: "" + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - linodego/dev https://github.com/linode/linodego + url: https://api.linode.com/v4beta/account/agreements + method: GET + response: + body: '{"privacy_policy": true, "eu_model": false, "master_service_agreement": + false}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS, POST, PUT, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status + Akamai-Internal-Account: + - '*' + Cache-Control: + - max-age=0, no-cache, no-store + Connection: + - keep-alive + Content-Length: + - "78" + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Expires: + - Wed, 30 Oct 2024 14:07:33 GMT + Pragma: + - no-cache + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization, X-Filter + - Authorization, X-Filter + X-Accepted-Oauth-Scopes: + - account:read_only + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + - DENY + X-Oauth-Scopes: + - '*' + X-Ratelimit-Limit: + - "800" + X-Xss-Protection: + - 1; mode=block + status: 200 OK + code: 200 + duration: "" diff --git a/test/integration/fixtures/TestAccountMaintenances_List.yaml b/test/integration/fixtures/TestAccountMaintenances_List.yaml new file mode 100644 index 000000000..2ea086062 --- /dev/null +++ b/test/integration/fixtures/TestAccountMaintenances_List.yaml @@ -0,0 +1,65 @@ +--- +version: 1 +interactions: +- request: + body: "" + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - linodego/dev https://github.com/linode/linodego + url: https://api.linode.com/v4beta/account/maintenance?page=1 + method: GET + response: + body: '{"data": [], "page": 1, "pages": 1, "results": 0}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS, POST, PUT, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status + Akamai-Internal-Account: + - '*' + Cache-Control: + - max-age=0, no-cache, no-store + Connection: + - keep-alive + Content-Length: + - "49" + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Expires: + - Wed, 30 Oct 2024 14:06:55 GMT + Pragma: + - no-cache + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization, X-Filter + - Authorization, X-Filter + X-Accepted-Oauth-Scopes: + - '*' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + - DENY + X-Oauth-Scopes: + - unknown + X-Ratelimit-Limit: + - "800" + X-Xss-Protection: + - 1; mode=block + status: 200 OK + code: 200 + duration: "" diff --git a/test/unit/account_agreements_test.go b/test/unit/account_agreements_test.go new file mode 100644 index 000000000..c409359e3 --- /dev/null +++ b/test/unit/account_agreements_test.go @@ -0,0 +1,44 @@ +package unit + +import ( + "context" + "github.com/jarcoal/httpmock" + "github.com/linode/linodego" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestAccountAgreements_Get(t *testing.T) { + fixtureData, err := fixtures.GetFixture("account_agreements_get") + assert.NoError(t, err) + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockGet("account/agreements", fixtureData) + + agreements, err := base.Client.GetAccountAgreements(context.Background()) + assert.NoError(t, err) + + assert.Equal(t, true, agreements.EUModel) + assert.Equal(t, true, agreements.PrivacyPolicy) + assert.Equal(t, true, agreements.MasterServiceAgreement) +} + +func TestAccountAgreements_Acknowledge(t *testing.T) { + client := createMockClient(t) + + requestData := linodego.AccountAgreementsUpdateOptions{ + EUModel: true, + MasterServiceAgreement: true, + PrivacyPolicy: true, + } + + httpmock.RegisterRegexpResponder("POST", mockRequestURL(t, "account/agreements"), + mockRequestBodyValidate(t, requestData, nil)) + + if err := client.AcknowledgeAccountAgreements(context.Background(), requestData); err != nil { + t.Fatal(err) + } +} diff --git a/test/unit/account_maintenance_test.go b/test/unit/account_maintenance_test.go new file mode 100644 index 000000000..5cee4d66a --- /dev/null +++ b/test/unit/account_maintenance_test.go @@ -0,0 +1,34 @@ +package unit + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAccountMaintenances_List(t *testing.T) { + fixtureData, err := fixtures.GetFixture("account_maintenance_list") + assert.NoError(t, err) + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockGet("account/maintenance", fixtureData) + + maintenances, err := base.Client.ListMaintenances(context.Background(), nil) + if err != nil { + t.Fatalf("Error listing maintenances: %v", err) + } + + assert.Equal(t, 1, len(maintenances)) + maintenance := maintenances[0] + assert.Equal(t, 1234, maintenance.Entity.ID) + assert.Equal(t, "demo-linode", maintenance.Entity.Label) + assert.Equal(t, "Linode", maintenance.Entity.Type) + assert.Equal(t, "https://api.linode.com/v4/linode/instances/{linodeId}", maintenance.Entity.URL) + assert.Equal(t, "This maintenance will allow us to update the BIOS on the host's motherboard.", maintenance.Reason) + assert.Equal(t, "started", maintenance.Status) + assert.Equal(t, "reboot", maintenance.Type) +} diff --git a/test/unit/account_test.go b/test/unit/account_test.go new file mode 100644 index 000000000..03f002b67 --- /dev/null +++ b/test/unit/account_test.go @@ -0,0 +1,50 @@ +package unit + +import ( + "context" + "github.com/linode/linodego" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestAccount_Update(t *testing.T) { + fixtureData, err := fixtures.GetFixture("account_update") + assert.NoError(t, err) + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + requestData := linodego.AccountUpdateOptions{ + City: "Cambridge", + State: "MA", + } + + base.MockPut("account", fixtureData) + + accountInfo, err := base.Client.UpdateAccount(context.Background(), requestData) + assert.NoError(t, err) + + assert.Equal(t, "John", accountInfo.FirstName) + assert.Equal(t, "Smith", accountInfo.LastName) + assert.Equal(t, "john.smith@linode.com", accountInfo.Email) + assert.Equal(t, "Linode LLC", accountInfo.Company) + assert.Equal(t, "123 Main Street", accountInfo.Address1) + assert.Equal(t, "Suite A", accountInfo.Address2) + assert.Equal(t, float32(200), accountInfo.Balance) + assert.Equal(t, float32(145), accountInfo.BalanceUninvoiced) + assert.Equal(t, "19102-1234", accountInfo.Zip) + assert.Equal(t, "US", accountInfo.Country) + assert.Equal(t, "ATU99999999", accountInfo.TaxID) + assert.Equal(t, "215-555-1212", accountInfo.Phone) + if accountInfo.CreditCard != nil { + assert.Equal(t, "11/2022", accountInfo.CreditCard.Expiry) + assert.Equal(t, "1111", accountInfo.CreditCard.LastFour) + } + assert.Equal(t, "E1AF5EEC-526F-487D-B317EBEB34C87D71", accountInfo.EUUID) + assert.Equal(t, "akamai", accountInfo.BillingSource) + assert.Equal(t, []string{"Linodes", "NodeBalancers", "Block Storage", "Object Storage", "Placement Groups", "Block Storage Encryption"}, accountInfo.Capabilities) + + assert.Equal(t, "Cambridge", accountInfo.City) + assert.Equal(t, "MA", accountInfo.State) +} diff --git a/test/unit/fixtures/account_agreements_get.json b/test/unit/fixtures/account_agreements_get.json new file mode 100644 index 000000000..dd2b8d966 --- /dev/null +++ b/test/unit/fixtures/account_agreements_get.json @@ -0,0 +1,5 @@ +{ + "eu_model": true, + "master_service_agreement": true, + "privacy_policy": true +} \ No newline at end of file diff --git a/test/unit/fixtures/account_maintenance_list.json b/test/unit/fixtures/account_maintenance_list.json new file mode 100644 index 000000000..71a6462b1 --- /dev/null +++ b/test/unit/fixtures/account_maintenance_list.json @@ -0,0 +1,19 @@ +{ + "data": [ + { + "entity": { + "id": 1234, + "label": "demo-linode", + "type": "Linode", + "url": "https://api.linode.com/v4/linode/instances/{linodeId}" + }, + "reason": "This maintenance will allow us to update the BIOS on the host's motherboard.", + "status": "started", + "type": "reboot", + "when": "2020-07-09T00:01:01" + } + ], + "page": 1, + "pages": 1, + "results": 1 +} \ No newline at end of file diff --git a/test/unit/fixtures/account_update.json b/test/unit/fixtures/account_update.json new file mode 100644 index 000000000..d89a4c817 --- /dev/null +++ b/test/unit/fixtures/account_update.json @@ -0,0 +1,43 @@ +{ + "active_promotions": [ + { + "credit_monthly_cap": "10.00", + "credit_remaining": "50.00", + "description": "Receive up to $10 off your services every month for 6 months! Unused credits will expire once this promotion period ends.", + "expire_dt": "2018-01-31T23:59:59", + "image_url": "https://linode.com/10_a_month_promotion.svg", + "service_type": "all", + "summary": "$10 off your Linode a month!", + "this_month_credit_remaining": "10.00" + } + ], + "active_since": "2018-01-01T00:01:01", + "address_1": "123 Main Street", + "address_2": "Suite A", + "balance": 200, + "balance_uninvoiced": 145, + "billing_source": "akamai", + "capabilities": [ + "Linodes", + "NodeBalancers", + "Block Storage", + "Object Storage", + "Placement Groups", + "Block Storage Encryption" + ], + "city": "Cambridge", + "company": "Linode LLC", + "country": "US", + "credit_card": { + "expiry": "11/2022", + "last_four": "1111" + }, + "email": "john.smith@linode.com", + "euuid": "E1AF5EEC-526F-487D-B317EBEB34C87D71", + "first_name": "John", + "last_name": "Smith", + "phone": "215-555-1212", + "state": "MA", + "tax_id": "ATU99999999", + "zip": "19102-1234" +} \ No newline at end of file