-
Notifications
You must be signed in to change notification settings - Fork 133
feat(contact): add support for customer contacts #813
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
Open
bclerc
wants to merge
6
commits into
fastly:main
Choose a base branch
from
bclerc:feat/customer-contacts
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
5271b9b
feat(contact): add support for customer contacts
bclerc ade7d68
chore(changelog): link PR #813
bclerc 924004f
refactor(customer/contacts): move to dedicated sub-package
bclerc-wbd 3c28bf8
Merge branch 'main' into feat/customer-contacts
kpfleming 6302425
refactor(customer/contacts): use fastly.ToValue instead of local dere…
bclerc-wbd 0e7b397
test(customer/contacts): source customer ID from a constant
bclerc-wbd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| 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 | ||
|
|
||
| // The customer ID is an opaque identifier that is only replayed against the | ||
| // recorded fixtures; its exact value does not matter for the test. | ||
| customerID := "7i6ZbMEdjnTUNyk75XgWO0" | ||
|
|
||
| // 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) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| 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: fastly.ToValue(i.UserID), | ||
| ContactType: fastly.ToValue(i.ContactType), | ||
| Name: fastly.ToValue(i.Name), | ||
| FirstName: fastly.ToValue(i.FirstName), | ||
| LastName: fastly.ToValue(i.LastName), | ||
| Email: fastly.ToValue(i.Email), | ||
| Phone: fastly.ToValue(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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"` | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably use the
GetCurrentCustomerAPI endpoint (https://www.fastly.com/documentation/reference/api/account/customer/#get-logged-in-customer), but without recording it into the fixtures. This can be done by usingDefaultClient()to get a regular Fastly client, avoiding the recording process.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay, busy week. GetCurrentCustomer isn't implemented in go-fastly today, so i'm not sure i follow what you mean here. I hardcoded the customer id because that's how it's done in the other tests (token_test.go, account_event_test.go), so i followed the same pattern.
If you'd rather fetch it at runtime, the closest thing i see is to grab a regular client with DefaultClient() and call GetCurrentUser, the returned User have a CustomerID field, something like:
Let me know which you prefer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The underlying issue is that hardcoding a CID won't work, because it will have to match the CID linked to the token that is being used to authenticate to the API or the tests will fail. If we followed the existing pattern there would have to be additions to
fastly_test_utils.goto hold the hardcoded CID, and every developer trying to run this test would have to provide the CID which matches the token they are using.However, since these are the only API calls/tests which require a CID, I would be fine with determining that CID at runtime so that running the tests doesn't require additional setup. While
GetCurrentUsercan be used for that, we've also seen a number of cases where that API operation returns an error because the API token is service-limited or has some other limits on it, butGetCurrentCustomershould not fail in those cases. That's why I suggested usingGetCurrentCustomer.We can implement
GetCurrentCustomerif that's something you'd prefer us to do. it would be implemented in thefastlypackage since it's a client-related operation and does not need to be in a subpackage.