Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Breaking:

### Enhancements:
- feat(customer/contacts): add support for customer contacts (list/create/delete) ([#813](https://github.com/fastly/go-fastly/pull/813))

### Dependencies:
- build(deps): `golang.org/x/crypto` from 0.50.0 to 0.51.0 ([#812](https://github.com/fastly/go-fastly/pull/812))
Expand Down
93 changes: 93 additions & 0 deletions fastly/customer/contacts/api_contacts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package contacts

import (
"context"
"testing"

"github.com/stretchr/testify/require"

"github.com/fastly/go-fastly/v15/fastly"
)

func TestClient_Contacts(t *testing.T) {
ctx := context.TODO()

var (
err error
customerID string
)
fastly.Record(t, "get_current_user", func(c *fastly.Client) {
var u *fastly.User
u, err = c.GetCurrentUser(ctx)
if err == nil && u != nil && u.CustomerID != nil {
customerID = *u.CustomerID
}
})
require.NoError(t, err)
require.NotEmpty(t, customerID)

// Fastly refuses to delete the last contact of a given type, so we
// create a guard contact first that we leave in place for the duration
// of the test (and best-effort delete during cleanup).
//
// NOTE: When recreating the fixtures, update both emails.
guardEmail := "go-fastly-test+contact-guard+20260522@example.com"
email := "go-fastly-test+contact+20260522@example.com"

var guard *Contact
fastly.Record(t, "create_guard", func(c *fastly.Client) {
guard, err = Create(ctx, c, &CreateInput{
CustomerID: fastly.ToPointer(customerID),
ContactType: fastly.ToPointer("emergency"),
Name: fastly.ToPointer("guard contact"),
Email: fastly.ToPointer(guardEmail),
})
})
require.NoError(t, err)
require.NotNil(t, guard)

var co *Contact
fastly.Record(t, "create", func(c *fastly.Client) {
co, err = Create(ctx, c, &CreateInput{
CustomerID: fastly.ToPointer(customerID),
ContactType: fastly.ToPointer("emergency"),
Name: fastly.ToPointer("test contact"),
Email: fastly.ToPointer(email),
})
})
require.NoError(t, err)
require.NotEmpty(t, co.ContactID)
require.Equal(t, email, co.Email)

// Best-effort cleanup: guard deletion may fail if it is the last
// emergency contact on the account, which is fine.
defer func() {
fastly.Record(t, "cleanup", func(c *fastly.Client) {
_ = Delete(ctx, c, &DeleteInput{
CustomerID: fastly.ToPointer(customerID),
ContactID: fastly.ToPointer(co.ContactID),
})
_ = Delete(ctx, c, &DeleteInput{
CustomerID: fastly.ToPointer(customerID),
ContactID: fastly.ToPointer(guard.ContactID),
})
})
}()

var cs []*Contact
fastly.Record(t, "list", func(c *fastly.Client) {
cs, err = List(ctx, c, &ListInput{
CustomerID: fastly.ToPointer(customerID),
})
})
require.NoError(t, err)
require.GreaterOrEqual(t, len(cs), 1)

fastly.Record(t, "delete", func(c *fastly.Client) {
err = Delete(ctx, c, &DeleteInput{
CustomerID: fastly.ToPointer(customerID),
ContactID: fastly.ToPointer(co.ContactID),
})
})
require.NoError(t, err)
}
88 changes: 88 additions & 0 deletions fastly/customer/contacts/api_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package contacts

import (
"context"

"github.com/google/jsonapi"

"github.com/fastly/go-fastly/v15/fastly"
)

// CreateInput specifies the information needed for the Create() function to
// perform the operation.
type CreateInput struct {
// CustomerID is the alphanumeric identifier of the customer (required).
CustomerID *string
// UserID is the alphanumeric identifier of an existing user. Required when
// not providing Email and Name.
UserID *string
// ContactType is the type of contact. One of: primary, billing, technical,
// security, emergency.
ContactType *string
// Name is the name of this contact, when not referencing an existing user.
Name *string
// FirstName is the first name of this contact, when not referencing an
// existing user.
FirstName *string
// LastName is the last name of this contact, when not referencing an
// existing user.
LastName *string
// Email is the email of this contact, when not referencing an existing user.
Email *string
// Phone is the contact's phone number. Required for the primary, technical,
// and security contact types.
Phone *string
}

// createPayload is the JSON:API marshaling shape for Create.
//
// It mirrors Contact but omits the fields that must not be sent in the body
// (CustomerID lives in the URL).
type createPayload struct {
ContactID string `jsonapi:"primary,customer_contact"`
UserID string `jsonapi:"attr,user_id,omitempty"`
ContactType string `jsonapi:"attr,contact_type,omitempty"`
Name string `jsonapi:"attr,name,omitempty"`
FirstName string `jsonapi:"attr,first_name,omitempty"`
LastName string `jsonapi:"attr,last_name,omitempty"`
Email string `jsonapi:"attr,email,omitempty"`
Phone string `jsonapi:"attr,phone,omitempty"`
}

// Create creates a new contact for the given customer.
func Create(ctx context.Context, c *fastly.Client, i *CreateInput) (*Contact, error) {
if i.CustomerID == nil {
return nil, fastly.ErrMissingCustomerID
}

path := fastly.ToSafeURL("customer", *i.CustomerID, "contacts")

payload := &createPayload{
UserID: deref(i.UserID),
ContactType: deref(i.ContactType),
Name: deref(i.Name),
FirstName: deref(i.FirstName),
LastName: deref(i.LastName),
Email: deref(i.Email),
Phone: deref(i.Phone),
}

resp, err := c.PostJSONAPI(ctx, path, payload, fastly.CreateRequestOptions())
if err != nil {
return nil, err
}
defer resp.Body.Close()

var co Contact
if err := jsonapi.UnmarshalPayload(resp.Body, &co); err != nil {
return nil, err
}
return &co, nil
}

func deref(s *string) string {
if s == nil {
return ""
}
return *s
}
40 changes: 40 additions & 0 deletions fastly/customer/contacts/api_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package contacts

import (
"context"
"net/http"

"github.com/fastly/go-fastly/v15/fastly"
)

// DeleteInput specifies the information needed for the Delete() function to
// perform the operation.
type DeleteInput struct {
// CustomerID is the alphanumeric identifier of the customer (required).
CustomerID *string
// ContactID is the alphanumeric identifier of the contact (required).
ContactID *string
}

// Delete deletes the specified contact.
func Delete(ctx context.Context, c *fastly.Client, i *DeleteInput) error {
if i.CustomerID == nil {
return fastly.ErrMissingCustomerID
}
if i.ContactID == nil {
return fastly.ErrMissingContactID
}

path := fastly.ToSafeURL("customer", *i.CustomerID, "contacts", *i.ContactID)

resp, err := c.Delete(ctx, path, fastly.CreateRequestOptions())
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusNoContent {
return fastly.NewHTTPError(resp)
}
return nil
}
52 changes: 52 additions & 0 deletions fastly/customer/contacts/api_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package contacts

import (
"context"
"fmt"
"reflect"

"github.com/google/jsonapi"

"github.com/fastly/go-fastly/v15/fastly"
)

// ListInput specifies the information needed for the List() function to
// perform the operation.
type ListInput struct {
// CustomerID is the alphanumeric identifier of the customer (required).
CustomerID *string
}

// List retrieves all contacts for the given customer.
func List(ctx context.Context, c *fastly.Client, i *ListInput) ([]*Contact, error) {
if i.CustomerID == nil {
return nil, fastly.ErrMissingCustomerID
}

path := fastly.ToSafeURL("customer", *i.CustomerID, "contacts")

opts := fastly.CreateRequestOptions()
opts.Headers["Accept"] = jsonapi.MediaType

resp, err := c.Get(ctx, path, opts)
if err != nil {
return nil, err
}
defer resp.Body.Close()

data, err := jsonapi.UnmarshalManyPayload(resp.Body, reflect.TypeOf(new(Contact)))
if err != nil {
return nil, err
}

cs := make([]*Contact, len(data))
for idx := range data {
typed, ok := data[idx].(*Contact)
if !ok {
return nil, fmt.Errorf("unexpected response type: %T", data[idx])
}
cs[idx] = typed
}

return cs, nil
}
18 changes: 18 additions & 0 deletions fastly/customer/contacts/api_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package contacts

import "time"

// Contact represents a customer contact.
type Contact struct {
ContactID string `jsonapi:"primary,customer_contact"`
UserID string `jsonapi:"attr,user_id,omitempty"`
ContactType string `jsonapi:"attr,contact_type,omitempty"`
Name string `jsonapi:"attr,name,omitempty"`
FirstName string `jsonapi:"attr,first_name,omitempty"`
LastName string `jsonapi:"attr,last_name,omitempty"`
Email string `jsonapi:"attr,email,omitempty"`
Phone string `jsonapi:"attr,phone,omitempty"`
CreatedAt *time.Time `jsonapi:"attr,created_at,iso8601"`
UpdatedAt *time.Time `jsonapi:"attr,updated_at,iso8601"`
DeletedAt *time.Time `jsonapi:"attr,deleted_at,iso8601"`
}
38 changes: 38 additions & 0 deletions fastly/customer/contacts/api_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package contacts

import (
"context"
"testing"

"github.com/stretchr/testify/require"

"github.com/fastly/go-fastly/v15/fastly"
)

func TestClient_List_validation(t *testing.T) {
_, err := List(context.TODO(), fastly.TestClient, &ListInput{
CustomerID: nil,
})
require.ErrorIs(t, err, fastly.ErrMissingCustomerID)
}

func TestClient_Create_validation(t *testing.T) {
_, err := Create(context.TODO(), fastly.TestClient, &CreateInput{
CustomerID: nil,
})
require.ErrorIs(t, err, fastly.ErrMissingCustomerID)
}

func TestClient_Delete_validation(t *testing.T) {
err := Delete(context.TODO(), fastly.TestClient, &DeleteInput{
CustomerID: nil,
ContactID: fastly.ToPointer("abc"),
})
require.ErrorIs(t, err, fastly.ErrMissingCustomerID)

err = Delete(context.TODO(), fastly.TestClient, &DeleteInput{
CustomerID: fastly.ToPointer("abc"),
ContactID: nil,
})
require.ErrorIs(t, err, fastly.ErrMissingContactID)
}
3 changes: 3 additions & 0 deletions fastly/customer/contacts/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package contacts contains functions for managing customer contacts
// (list, create, delete) on a Fastly account.
package contacts
Loading