From 83cb51d945cc691bfee45bf8ecb1b0a012c1d073 Mon Sep 17 00:00:00 2001 From: Michal Gosek Date: Wed, 4 Dec 2024 15:05:04 +0100 Subject: [PATCH] refactor(SPV-1230): admin endpoints group - Contacts API. (#23) --- admin_api.go | 82 ++++++++++- commands/contacts.go | 12 +- examples/README.md | 20 ++- examples/Taskfile.yml | 42 ++++++ .../accept_invitation_as_admin.go | 23 +++ .../create_xpub_as_admin.go | 29 ++++ .../delete_contact_as_admin.go | 23 +++ .../fetch_contacts_as_admin.go | 24 +++ .../fetch_xpubs_as_admin.go | 28 ++++ .../reject_invitation_as_admin.go | 23 +++ .../update_contact_as_admin.go | 33 +++++ .../api/v1/admin/contacts/contacts_api.go | 96 ++++++++++++ .../v1/admin/contacts/contacts_api_test.go | 138 ++++++++++++++++++ .../contactstest/contacts_api_fixtures.go | 88 +++++++++++ .../contactstest/get_contacts_200.json | 34 +++++ .../contactstest/put_contact_update_200.json | 13 ++ .../v1/admin/invitations/invitations_api.go | 55 +++++++ .../admin/invitations/invitations_api_test.go | 88 +++++++++++ .../invitations_api_fixtures.go | 23 +++ .../{users => xpubs}/xpub_filter_builder.go | 0 .../xpub_filter_builder_test.go | 20 +-- .../v1/admin/{users => xpubs}/xpubs_api.go | 0 .../admin/{users => xpubs}/xpubs_api_test.go | 38 +++-- .../xpubstest}/get_xpubs_200.json | 0 .../xpubstest}/post_xpub_201.json | 0 .../xpubstest}/xpub_api_fixtures.go | 2 +- .../contacts/contact_filter_query_builder.go | 20 +-- .../contact_filter_query_builder_test.go | 9 +- internal/api/v1/user/contacts/contacts_api.go | 6 +- user_api.go | 2 +- 30 files changed, 918 insertions(+), 53 deletions(-) create mode 100644 examples/accept_invitation_as_admin/accept_invitation_as_admin.go create mode 100644 examples/create_xpub_as_admin/create_xpub_as_admin.go create mode 100644 examples/delete_contact_as_admin/delete_contact_as_admin.go create mode 100644 examples/fetch_contacts_as_admin/fetch_contacts_as_admin.go create mode 100644 examples/fetch_xpubs_as_admin/fetch_xpubs_as_admin.go create mode 100644 examples/reject_invitation_as_admin/reject_invitation_as_admin.go create mode 100644 examples/update_contact_as_admin/update_contact_as_admin.go create mode 100644 internal/api/v1/admin/contacts/contacts_api.go create mode 100644 internal/api/v1/admin/contacts/contacts_api_test.go create mode 100644 internal/api/v1/admin/contacts/contactstest/contacts_api_fixtures.go create mode 100644 internal/api/v1/admin/contacts/contactstest/get_contacts_200.json create mode 100644 internal/api/v1/admin/contacts/contactstest/put_contact_update_200.json create mode 100644 internal/api/v1/admin/invitations/invitations_api.go create mode 100644 internal/api/v1/admin/invitations/invitations_api_test.go create mode 100644 internal/api/v1/admin/invitations/invitationstest/invitations_api_fixtures.go rename internal/api/v1/admin/{users => xpubs}/xpub_filter_builder.go (100%) rename internal/api/v1/admin/{users => xpubs}/xpub_filter_builder_test.go (78%) rename internal/api/v1/admin/{users => xpubs}/xpubs_api.go (100%) rename internal/api/v1/admin/{users => xpubs}/xpubs_api_test.go (65%) rename internal/api/v1/admin/{users/userstest => xpubs/xpubstest}/get_xpubs_200.json (100%) rename internal/api/v1/admin/{users/userstest => xpubs/xpubstest}/post_xpub_201.json (100%) rename internal/api/v1/admin/{users/userstest => xpubs/xpubstest}/xpub_api_fixtures.go (99%) diff --git a/admin_api.go b/admin_api.go index d998a81..fefe903 100644 --- a/admin_api.go +++ b/admin_api.go @@ -8,7 +8,9 @@ import ( bip32 "github.com/bitcoin-sv/go-sdk/compat/bip32" "github.com/bitcoin-sv/spv-wallet-go-client/commands" "github.com/bitcoin-sv/spv-wallet-go-client/config" - xpubs "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/users" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/contacts" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/invitations" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/xpubs" "github.com/bitcoin-sv/spv-wallet-go-client/internal/auth" "github.com/bitcoin-sv/spv-wallet-go-client/internal/restyutil" "github.com/bitcoin-sv/spv-wallet-go-client/queries" @@ -25,7 +27,9 @@ import ( // Methods may return wrapped errors, including models.SPVError or // ErrUnrecognizedAPIResponse, depending on the behavior of the SPV Wallet API. type AdminAPI struct { - xpubsAPI *xpubs.API // Internal API for managing operations related to XPubs. + xpubsAPI *xpubs.API // Internal API for managing operations related to XPubs. + contactsAPI *contacts.API + invitationsAPI *invitations.API } // CreateXPub creates a new XPub record via the Admin XPubs API. @@ -60,6 +64,74 @@ func (a *AdminAPI) XPubs(ctx context.Context, opts ...queries.XPubQueryOption) ( return res, nil } +// Contacts retrieves a paginated list of user contacts from the admin contacts API. +// +// The response includes contact data along with pagination details, such as the +// current page, sort order, and sortBy field. Optional query parameters can be +// provided using query options. The result is unmarshaled into a *queries.UserContactsPage. +// Returns an error if the API request fails or the response cannot be decoded. +func (a *AdminAPI) Contacts(ctx context.Context, opts ...queries.ContactQueryOption) (*queries.UserContactsPage, error) { + res, err := a.contactsAPI.Contacts(ctx, opts...) + if err != nil { + return nil, contacts.HTTPErrorFormatter("retrieve user contacts page", err).FormatGetErr() + } + + return res, nil +} + +// ContactUpdate updates a user's contact information through the admin contacts API. +// +// This method uses the `UpdateContact` command to specify the details of the contact to update. +// It sends the update request to the API, unmarshals the response into a `*response.Contact`, +// and returns the updated contact. If the API request fails or the response cannot be decoded, +// an error is returned. +func (a *AdminAPI) ContactUpdate(ctx context.Context, cmd *commands.UpdateContact) (*response.Contact, error) { + res, err := a.contactsAPI.UpdateContact(ctx, cmd) + if err != nil { + msg := fmt.Sprintf("update contact with ID: %s", cmd.ID) + return nil, contacts.HTTPErrorFormatter(msg, err).FormatPutErr() + } + + return res, nil +} + +// DeleteContact deletes a user contact with the given ID via the admin contacts API. +// Returns an error if the API request fails or the response cannot be decoded. +// A nil error indicates the deleting contact was successful. +func (a *AdminAPI) DeleteContact(ctx context.Context, ID string) error { + err := a.contactsAPI.DeleteContact(ctx, ID) + if err != nil { + msg := fmt.Sprintf("delete contact with ID: %s", ID) + return contacts.HTTPErrorFormatter(msg, err).FormatDeleteErr() + } + + return nil +} + +// AcceptInvitation processes and accepts a user contact invitation using the given ID via the admin invitations API. +// Returns an error if the API request fails. A nil error indicates the invitation was successfully accepted. +func (a *AdminAPI) AcceptInvitation(ctx context.Context, ID string) error { + err := a.invitationsAPI.AcceptInvitation(ctx, ID) + if err != nil { + msg := fmt.Sprintf("accept invitation with ID: %s", ID) + return invitations.HTTPErrorFormatter(msg, err).FormatDeleteErr() + } + + return nil +} + +// RejectInvitation processes and rejects a user contact invitation using the given ID via the admin invitations API. +// Returns an error if the API request fails. A nil error indicates the invitation was successfully rejected. +func (a *AdminAPI) RejectInvitation(ctx context.Context, ID string) error { + err := a.invitationsAPI.RejectInvitation(ctx, ID) + if err != nil { + msg := fmt.Sprintf("delete invitation with ID: %s", ID) + return invitations.HTTPErrorFormatter(msg, err).FormatDeleteErr() + } + + return nil +} + // NewAdminAPIWithXPriv initializes a new AdminAPI instance using an extended private key (xPriv). // This function configures the API client with the provided configuration and uses the xPriv key for authentication. // If any step fails, an appropriate error is returned. @@ -106,5 +178,9 @@ func initAdminAPI(cfg config.Config, auth authenticator) (*AdminAPI, error) { } httpClient := restyutil.NewHTTPClient(cfg, auth) - return &AdminAPI{xpubsAPI: xpubs.NewAPI(url, httpClient)}, nil + return &AdminAPI{ + xpubsAPI: xpubs.NewAPI(url, httpClient), + contactsAPI: contacts.NewAPI(url, httpClient), + invitationsAPI: invitations.NewAPI(url, httpClient), + }, nil } diff --git a/commands/contacts.go b/commands/contacts.go index 76d7edf..9adc3ca 100644 --- a/commands/contacts.go +++ b/commands/contacts.go @@ -3,6 +3,16 @@ package commands // UpsertContact holds the necessary arguments for adding or updating a user's contact information. type UpsertContact struct { FullName string `json:"fullName"` // The full name of the user. - Metadata map[string]any `json:"metadata"` // Metadata associated with the transaction. + Metadata map[string]any `json:"metadata"` // Metadata associated with the contact. Paymail string `json:"requesterPaymail"` // Paymail address of the user, which is used for secure and simplified payment transfers. } + +// UpdateContact represents the arguments defined for updating a user's contact information. +// +// Note: The `ID` field is not included in the request body sent to the SPV Wallet API. +// Instead, it is used as part of the endpoint path (e.g., /api/v1/admin/contacts/{ID}). +type UpdateContact struct { + ID string `json:"-"` // Unique identifier of the contact to be updated. + FullName string `json:"fullName"` // The full name of the contact. + Metadata map[string]any `json:"metadata"` // Metadata associated with the contact. +} diff --git a/examples/README.md b/examples/README.md index d928290..6532ddc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -21,7 +21,11 @@ the wallet client package during interaction wit the SPV Wallet API. ``` task: [default] task --list task: Available tasks for this project: +* accept-invitation-as-admin: Accept invitation with a given ID as Admin. +* create-xpub-as-admin: Create xPub as Admin. * default: Display all available tasks. +* delete-contact-as-admin: Delete contact with a given ID as Admin. +* fetch-contacts-as-admin: Fetch contacts page as Admin. * fetch-user-contact-by-paymail: Fetch user contact by given paymail. * fetch-user-contacts: Fetch user contacts page. * fetch-user-merkleroots: Fetch user Merkle roots page. @@ -30,7 +34,10 @@ task: Available tasks for this project: * fetch-user-transactions: Fetch user transactions page. * fetch-user-utxos: Fetch user UTXOs page. * fetch-user-xpub: Fetch current authorized user's xpub info. +* fetch-xpubs-as-admin: Fetch xPubs page as Admin. * generate-keys: Generate keys for SPV Wallet API access. +* reject-invitation-as-admin: Reject invitation with a given ID as Admin. +* update-contact-as-admin: Update contact with a given ID as Admin. * user-contact-confirmation: Confirm user contact with a given paymail address. * user-contact-remove: Remove user contact with a given paymail address. * user-contact-unconfirm: Unconfirm user contact with a given paymail address. @@ -53,7 +60,7 @@ task: Available tasks for this project: Before interacting with the SPV Wallet API, you must complete the authorization process. -To begin, generate a pair of keys using the `task generate-keys command`, which is included in the dedicated Taskfile. +To begin, generate a pair of keys using the `task generate-keys` command, which is included in the dedicated Taskfile. **Example output:** ``` @@ -89,12 +96,12 @@ import ( func main() { xPriv := "121d2f43-4261-42ab-813e-3d3fa4d87313" cfg := wallet.NewDefaultConfig("http://localhost:3003") - spv, err := wallet.NewWithXPriv(cfg, xPriv) + userAPI, err := wallet.NewUserAPIWithXPriv(cfg, xPriv) if err != nil { log.Fatal(err) } - xPub, err := spv.XPub(context.Background()) + xPub, err := userAPI.XPub(context.Background()) if err != nil { log.Fatal(err) } @@ -111,8 +118,11 @@ func Print(s string, a any) { } fmt.Println(string(res)) } - ``` + +> [!TIP] +> The same principle applies when creating an AdminAPI client instance using one of the available constructors. + **Example output:** ``` @@ -144,5 +154,5 @@ cd examples task name_of_the_example ``` - > [!TIP] +> [!TIP] > To verify Taskfile installation run: `task` command in the terminal. diff --git a/examples/Taskfile.yml b/examples/Taskfile.yml index c74fd13..4f3e10f 100644 --- a/examples/Taskfile.yml +++ b/examples/Taskfile.yml @@ -14,6 +14,48 @@ tasks: - go run ../walletkeys/cmd/main.go - echo "==================================================================" + accept-invitation-as-admin: + desc: "Accept invitation with a given ID as Admin." + silent: true + cmds: + - go run ./accept_invitation_as_admin/accept_invitation_as_admin.go + + reject-invitation-as-admin: + desc: "Reject invitation with a given ID as Admin." + silent: true + cmds: + - go run ./reject_invitation_as_admin/reject_invitation_as_admin.go + + fetch-contacts-as-admin: + desc: "Fetch contacts page as Admin." + silent: true + cmds: + - go run ./fetch_contacts_as_admin/fetch_contacts_as_admin.go + + update-contact-as-admin: + desc: "Update contact with a given ID as Admin." + silent: true + cmds: + - go run ./update_contact_as_admin/update_contact_as_admin.go + + delete-contact-as-admin: + desc: "Delete contact with a given ID as Admin." + silent: true + cmds: + - go run ./delete_contact_as_admin/delete_contact_as_admin.go + + fetch-xpubs-as-admin: + desc: "Fetch xPubs page as Admin." + silent: true + cmds: + - go run ./fetch_xpubs_as_admin/fetch_xpubs_as_admin.go + + create-xpub-as-admin: + desc: "Create xPub as Admin." + silent: true + cmds: + - go run ./create_xpub_as_admin/create_xpub_as_admin.go + fetch-user-shared-config: desc: "Fetch user shared configuration." silent: true diff --git a/examples/accept_invitation_as_admin/accept_invitation_as_admin.go b/examples/accept_invitation_as_admin/accept_invitation_as_admin.go new file mode 100644 index 0000000..6ba959a --- /dev/null +++ b/examples/accept_invitation_as_admin/accept_invitation_as_admin.go @@ -0,0 +1,23 @@ +package main + +import ( + "context" + "log" + + wallet "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" + "github.com/bitcoin-sv/spv-wallet-go-client/examples/exampleutil" +) + +func main() { + adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv) + if err != nil { + log.Fatal(err) + } + + id := "cd401f8f-a9ae-4efe-a2a1-de3e6832593d" + err = adminAPI.AcceptInvitation(context.Background(), id) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/create_xpub_as_admin/create_xpub_as_admin.go b/examples/create_xpub_as_admin/create_xpub_as_admin.go new file mode 100644 index 0000000..dd5c4d8 --- /dev/null +++ b/examples/create_xpub_as_admin/create_xpub_as_admin.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "log" + + wallet "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/commands" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" + "github.com/bitcoin-sv/spv-wallet-go-client/examples/exampleutil" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" +) + +func main() { + adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv) + if err != nil { + log.Fatal(err) + } + + xPub, err := adminAPI.CreateXPub(context.Background(), &commands.CreateUserXpub{ + XPub: "1c318ad8-5ee4-42d3-9cf5-5b0babec9156", + Metadata: querybuilders.Metadata{"key": "value"}, + }) + if err != nil { + log.Fatal(err) + } + + exampleutil.Print("[HTTP POST] Create XPub - api/v1/admin/users", xPub) +} diff --git a/examples/delete_contact_as_admin/delete_contact_as_admin.go b/examples/delete_contact_as_admin/delete_contact_as_admin.go new file mode 100644 index 0000000..bf3d250 --- /dev/null +++ b/examples/delete_contact_as_admin/delete_contact_as_admin.go @@ -0,0 +1,23 @@ +package main + +import ( + "context" + "log" + + wallet "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" + "github.com/bitcoin-sv/spv-wallet-go-client/examples/exampleutil" +) + +func main() { + adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv) + if err != nil { + log.Fatal(err) + } + + id := "88db6027-e38a-43b7-97a0-45f08d535256" + err = adminAPI.DeleteContact(context.Background(), id) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/fetch_contacts_as_admin/fetch_contacts_as_admin.go b/examples/fetch_contacts_as_admin/fetch_contacts_as_admin.go new file mode 100644 index 0000000..8a1335f --- /dev/null +++ b/examples/fetch_contacts_as_admin/fetch_contacts_as_admin.go @@ -0,0 +1,24 @@ +package main + +import ( + "context" + "log" + + wallet "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" + "github.com/bitcoin-sv/spv-wallet-go-client/examples/exampleutil" +) + +func main() { + adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv) + if err != nil { + log.Fatal(err) + } + + page, err := adminAPI.Contacts(context.Background()) + if err != nil { + log.Fatal(err) + } + + exampleutil.Print("[HTTP GET] Contacts - api/v1/admin/contacts", page) +} diff --git a/examples/fetch_xpubs_as_admin/fetch_xpubs_as_admin.go b/examples/fetch_xpubs_as_admin/fetch_xpubs_as_admin.go new file mode 100644 index 0000000..c95415a --- /dev/null +++ b/examples/fetch_xpubs_as_admin/fetch_xpubs_as_admin.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "log" + + wallet "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" + "github.com/bitcoin-sv/spv-wallet-go-client/examples/exampleutil" + "github.com/bitcoin-sv/spv-wallet-go-client/queries" + "github.com/bitcoin-sv/spv-wallet/models/filter" +) + +func main() { + adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv) + if err != nil { + log.Fatal(err) + } + + page, err := adminAPI.XPubs(context.Background(), queries.XPubQueryWithPageFilter(filter.Page{ + Size: 1, + })) + if err != nil { + log.Fatal(err) + } + + exampleutil.Print("[HTTP GET] XPubs - api/v1/admin/users", page) +} diff --git a/examples/reject_invitation_as_admin/reject_invitation_as_admin.go b/examples/reject_invitation_as_admin/reject_invitation_as_admin.go new file mode 100644 index 0000000..b5b1151 --- /dev/null +++ b/examples/reject_invitation_as_admin/reject_invitation_as_admin.go @@ -0,0 +1,23 @@ +package main + +import ( + "context" + "log" + + wallet "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" + "github.com/bitcoin-sv/spv-wallet-go-client/examples/exampleutil" +) + +func main() { + adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv) + if err != nil { + log.Fatal(err) + } + + id := "bc84cb22-c9aa-415f-93e1-2ed0af27a6ef" + err = adminAPI.RejectInvitation(context.Background(), id) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/update_contact_as_admin/update_contact_as_admin.go b/examples/update_contact_as_admin/update_contact_as_admin.go new file mode 100644 index 0000000..b4b7140 --- /dev/null +++ b/examples/update_contact_as_admin/update_contact_as_admin.go @@ -0,0 +1,33 @@ +package main + +import ( + "context" + "fmt" + "log" + + wallet "github.com/bitcoin-sv/spv-wallet-go-client" + "github.com/bitcoin-sv/spv-wallet-go-client/commands" + "github.com/bitcoin-sv/spv-wallet-go-client/examples" + "github.com/bitcoin-sv/spv-wallet-go-client/examples/exampleutil" +) + +func main() { + adminAPI, err := wallet.NewAdminAPIWithXPriv(exampleutil.ExampleConfig, examples.XPriv) + if err != nil { + log.Fatal(err) + } + + id := "88db6027-e38a-43b7-97a0-45f08d535256" + contact, err := adminAPI.ContactUpdate(context.Background(), &commands.UpdateContact{ + ID: id, + FullName: "John Doe", + Metadata: map[string]any{ + "phoneNumber": "123456789", + }, + }) + if err != nil { + log.Fatal(err) + } + + exampleutil.Print(fmt.Sprintf("[HTTP PUT] Update contact - api/v1/admin/contacts/%s", id), contact) +} diff --git a/internal/api/v1/admin/contacts/contacts_api.go b/internal/api/v1/admin/contacts/contacts_api.go new file mode 100644 index 0000000..fbaf9e2 --- /dev/null +++ b/internal/api/v1/admin/contacts/contacts_api.go @@ -0,0 +1,96 @@ +package contacts + +import ( + "context" + "fmt" + "net/url" + + "github.com/bitcoin-sv/spv-wallet-go-client/commands" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/errutil" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/user/contacts" + "github.com/bitcoin-sv/spv-wallet-go-client/queries" + "github.com/bitcoin-sv/spv-wallet/models/response" + "github.com/go-resty/resty/v2" +) + +const route = "api/v1/admin/contacts" + +type API struct { + httpClient *resty.Client + url *url.URL +} + +func (a *API) Contacts(ctx context.Context, opts ...queries.ContactQueryOption) (*queries.UserContactsPage, error) { + var query queries.ContactQuery + for _, o := range opts { + o(&query) + } + + queryBuilder := querybuilders.NewQueryBuilder( + querybuilders.WithMetadataFilter(query.Metadata), + querybuilders.WithPageFilter(query.PageFilter), + querybuilders.WithFilterQueryBuilder(&contacts.ContactFilterQueryBuilder{ + ContactFilter: query.ContactFilter, + ModelFilterBuilder: querybuilders.ModelFilterBuilder{ + ModelFilter: query.ContactFilter.ModelFilter, + }, + }), + ) + params, err := queryBuilder.Build() + if err != nil { + return nil, fmt.Errorf("failed to build admin contacts query params: %w", err) + } + + var result queries.UserContactsPage + _, err = a.httpClient. + R(). + SetContext(ctx). + SetResult(&result). + SetQueryParams(params.ParseToMap()). + Get(a.url.String()) + if err != nil { + return nil, fmt.Errorf("HTTP response failure: %w", err) + } + + return &result, nil +} + +func (a *API) UpdateContact(ctx context.Context, cmd *commands.UpdateContact) (*response.Contact, error) { + var result response.Contact + _, err := a.httpClient. + R(). + SetContext(ctx). + SetResult(&result). + SetBody(cmd). + Put(a.url.JoinPath(cmd.ID).String()) + if err != nil { + return nil, fmt.Errorf("HTTP response failure: %w", err) + } + + return &result, nil +} + +func (a *API) DeleteContact(ctx context.Context, ID string) error { + _, err := a.httpClient. + R(). + SetContext(ctx). + Delete(a.url.JoinPath(ID).String()) + if err != nil { + return fmt.Errorf("HTTP response failure: %w", err) + } + + return nil +} + +func NewAPI(url *url.URL, httpClient *resty.Client) *API { + return &API{url: url.JoinPath(route), httpClient: httpClient} +} + +func HTTPErrorFormatter(action string, err error) *errutil.HTTPErrorFormatter { + return &errutil.HTTPErrorFormatter{ + Action: action, + API: "Admin Contacts API", + Err: err, + } +} diff --git a/internal/api/v1/admin/contacts/contacts_api_test.go b/internal/api/v1/admin/contacts/contacts_api_test.go new file mode 100644 index 0000000..c148853 --- /dev/null +++ b/internal/api/v1/admin/contacts/contacts_api_test.go @@ -0,0 +1,138 @@ +package contacts_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/bitcoin-sv/spv-wallet-go-client/commands" + "github.com/bitcoin-sv/spv-wallet-go-client/errors" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/contacts/contactstest" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/spvwallettest" + "github.com/bitcoin-sv/spv-wallet-go-client/queries" + "github.com/bitcoin-sv/spv-wallet/models/filter" + "github.com/bitcoin-sv/spv-wallet/models/response" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/require" +) + +func TestContactsAPI_Contacts(t *testing.T) { + tests := map[string]struct { + responder httpmock.Responder + expectedResponse *queries.UserContactsPage + expectedErr error + }{ + "HTTP GET /api/v1/admin/contacts response: 200": { + expectedResponse: contactstest.ExpectedUserContactsPage(t), + responder: httpmock.NewJsonResponderOrPanic(http.StatusOK, httpmock.File("contactstest/get_contacts_200.json")), + }, + "HTTP GET /api/v1/admin/contacts response: 400": { + expectedErr: contactstest.NewBadRequestSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, contactstest.NewBadRequestSPVError()), + }, + "HTTP GET /api/v1/admin/contacts response: 500": { + expectedErr: contactstest.NewInternalServerSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, contactstest.NewInternalServerSPVError()), + }, + "HTTP GET /api/v1/admin/contacts str response: 500": { + expectedErr: errors.ErrUnrecognizedAPIResponse, + responder: httpmock.NewStringResponder(http.StatusInternalServerError, "unexpected internal server failure"), + }, + } + + url := spvwallettest.TestAPIAddr + "/api/v1/admin/contacts" + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // when: + wallet, transport := spvwallettest.GivenSPVAdminAPI(t) + transport.RegisterResponder(http.MethodGet, url, tc.responder) + + // then: + got, err := wallet.Contacts(context.Background(), queries.ContactQueryWithPageFilter(filter.Page{Size: 1})) + require.ErrorIs(t, err, tc.expectedErr) + require.EqualValues(t, tc.expectedResponse, got) + }) + } +} + +func TestContactsAPI_ContactUpdate(t *testing.T) { + id := "4d570959-dd85-4f53-bad1-18d0671761e9" + tests := map[string]struct { + responder httpmock.Responder + expectedResponse *response.Contact + expectedErr error + }{ + fmt.Sprintf("HTTP PUT /api/v1/admin/contacts/%s response: 200", id): { + expectedResponse: contactstest.ExpectedUpdatedUserContact(t), + responder: httpmock.NewJsonResponderOrPanic(http.StatusOK, httpmock.File("contactstest/put_contact_update_200.json")), + }, + fmt.Sprintf("HTTP PUT /api/v1/admin/contacts/%s response: 400", id): { + expectedErr: contactstest.NewBadRequestSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, contactstest.NewBadRequestSPVError()), + }, + fmt.Sprintf("HTTP PUT /api/v1/admin/contacts/%s response: 500", id): { + expectedErr: contactstest.NewInternalServerSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, contactstest.NewInternalServerSPVError()), + }, + fmt.Sprintf("HTTP PUT /api/v1/admin/contacts/%s str response: 500", id): { + expectedErr: errors.ErrUnrecognizedAPIResponse, + responder: httpmock.NewStringResponder(http.StatusInternalServerError, "unexpected internal server failure"), + }, + } + + url := spvwallettest.TestAPIAddr + "/api/v1/admin/contacts/" + id + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // when: + wallet, transport := spvwallettest.GivenSPVAdminAPI(t) + transport.RegisterResponder(http.MethodPut, url, tc.responder) + + // then: + got, err := wallet.ContactUpdate(context.Background(), &commands.UpdateContact{ + ID: "4d570959-dd85-4f53-bad1-18d0671761e9", + FullName: "John Doe Williams", + Metadata: map[string]any{"phoneNumber": "123456789"}, + }) + require.ErrorIs(t, err, tc.expectedErr) + require.EqualValues(t, tc.expectedResponse, got) + }) + } +} + +func TestContactsAPI_DeleteContact(t *testing.T) { + id := "4d570959-dd85-4f53-bad1-18d0671761e9" + tests := map[string]struct { + responder httpmock.Responder + expectedErr error + }{ + fmt.Sprintf("HTTP DELETE /api/v1/admin/contacts/%s response: 200", id): { + responder: httpmock.NewStringResponder(http.StatusOK, http.StatusText(http.StatusOK)), + }, + fmt.Sprintf("HTTP DELETE/api/v1/admin/contacts/%s response: 400", id): { + expectedErr: contactstest.NewBadRequestSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, contactstest.NewBadRequestSPVError()), + }, + fmt.Sprintf("HTTP DELETE /api/v1/admin/contacts/%s response: 500", id): { + expectedErr: contactstest.NewInternalServerSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, contactstest.NewInternalServerSPVError()), + }, + fmt.Sprintf("HTTP DELETE /api/v1/admin/contacts/%s str response: 500", id): { + expectedErr: errors.ErrUnrecognizedAPIResponse, + responder: httpmock.NewStringResponder(http.StatusInternalServerError, "unexpected internal server failure"), + }, + } + + url := spvwallettest.TestAPIAddr + "/api/v1/admin/contacts/" + id + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // when: + wallet, transport := spvwallettest.GivenSPVAdminAPI(t) + transport.RegisterResponder(http.MethodDelete, url, tc.responder) + + // then: + err := wallet.DeleteContact(context.Background(), id) + require.ErrorIs(t, err, tc.expectedErr) + }) + } +} diff --git a/internal/api/v1/admin/contacts/contactstest/contacts_api_fixtures.go b/internal/api/v1/admin/contacts/contactstest/contacts_api_fixtures.go new file mode 100644 index 0000000..192a952 --- /dev/null +++ b/internal/api/v1/admin/contacts/contactstest/contacts_api_fixtures.go @@ -0,0 +1,88 @@ +package contactstest + +import ( + "net/http" + "testing" + "time" + + "github.com/bitcoin-sv/spv-wallet-go-client/queries" + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/bitcoin-sv/spv-wallet/models/response" +) + +func ExpectedUpdatedUserContact(t *testing.T) *response.Contact { + return &response.Contact{ + Model: response.Model{ + CreatedAt: parseTime(t, "2024-11-28T13:34:52.11722Z"), + UpdatedAt: parseTime(t, "2024-11-29T08:23:19.66093Z"), + Metadata: map[string]any{"phoneNumber": "123456789"}, + }, + ID: "4d570959-dd85-4f53-bad1-18d0671761e9", + FullName: "John Doe Williams", + Paymail: "john.doe.test@john.doe.test.4chain.space", + PubKey: "96843af4-fc9c-4778-945d-2131ac5b1a8a", + Status: "awaiting", + } +} + +func ExpectedUserContactsPage(t *testing.T) *queries.UserContactsPage { + return &queries.UserContactsPage{ + Content: []*response.Contact{ + { + Model: response.Model{ + CreatedAt: parseTime(t, "2024-11-28T14:58:13.262238Z"), + UpdatedAt: parseTime(t, "2024-11-28T16:18:43.842434Z"), + }, + ID: "7a5625ac-8256-454a-84a3-7f03f50cd7dc", + FullName: "John Doe", + Paymail: "john.doe.test@john.doe.4chain.space", + PubKey: "bbbb7a4e-a3f4-4ca4-800a-fdd8029eda37", + Status: "confirmed", + }, + { + Model: response.Model{ + CreatedAt: parseTime(t, "2024-11-28T14:58:13.029966Z"), + UpdatedAt: parseTime(t, "2024-11-28T14:58:13.03002Z"), + Metadata: map[string]any{ + "phoneNumber": "123456789", + }, + }, + ID: "d05d2388-3c16-426d-98f1-ced9d9c5f4e1", + FullName: "Jane Doe", + Paymail: "jane.doe.jane@john.doe.4chain.space", + PubKey: "ee191d63-1619-4fd3-ae3d-2202cfab751d", + Status: "unconfirmed", + }, + }, + Page: response.PageDescription{ + Size: 50, + Number: 1, + TotalElements: 2, + TotalPages: 1, + }, + } +} + +func NewBadRequestSPVError() models.SPVError { + return models.SPVError{ + Message: http.StatusText(http.StatusBadRequest), + StatusCode: http.StatusBadRequest, + Code: "invalid-data-format", + } +} + +func NewInternalServerSPVError() models.SPVError { + return models.SPVError{ + Message: http.StatusText(http.StatusInternalServerError), + StatusCode: http.StatusInternalServerError, + Code: models.UnknownErrorCode, + } +} + +func parseTime(t *testing.T, s string) time.Time { + ts, err := time.Parse(time.RFC3339Nano, s) + if err != nil { + t.Fatalf("test helper - time parse: %s", err) + } + return ts +} diff --git a/internal/api/v1/admin/contacts/contactstest/get_contacts_200.json b/internal/api/v1/admin/contacts/contactstest/get_contacts_200.json new file mode 100644 index 0000000..cf1ee68 --- /dev/null +++ b/internal/api/v1/admin/contacts/contactstest/get_contacts_200.json @@ -0,0 +1,34 @@ +{ + "content": [ + { + "createdAt": "2024-11-28T14:58:13.262238Z", + "updatedAt": "2024-11-28T16:18:43.842434Z", + "deletedAt": null, + "metadata": null, + "id": "7a5625ac-8256-454a-84a3-7f03f50cd7dc", + "fullName": "John Doe", + "paymail": "john.doe.test@john.doe.4chain.space", + "pubKey": "bbbb7a4e-a3f4-4ca4-800a-fdd8029eda37", + "status": "confirmed" + }, + { + "createdAt": "2024-11-28T14:58:13.029966Z", + "updatedAt": "2024-11-28T14:58:13.03002Z", + "deletedAt": null, + "metadata": { + "phoneNumber": "123456789" + }, + "id": "d05d2388-3c16-426d-98f1-ced9d9c5f4e1", + "fullName": "Jane Doe", + "paymail": "jane.doe.jane@john.doe.4chain.space", + "pubKey": "ee191d63-1619-4fd3-ae3d-2202cfab751d", + "status": "unconfirmed" + } + ], + "page": { + "size": 50, + "number": 1, + "totalElements": 2, + "totalPages": 1 + } +} diff --git a/internal/api/v1/admin/contacts/contactstest/put_contact_update_200.json b/internal/api/v1/admin/contacts/contactstest/put_contact_update_200.json new file mode 100644 index 0000000..c7d884e --- /dev/null +++ b/internal/api/v1/admin/contacts/contactstest/put_contact_update_200.json @@ -0,0 +1,13 @@ +{ + "createdAt": "2024-11-28T13:34:52.11722Z", + "updatedAt": "2024-11-29T08:23:19.66093Z", + "deletedAt": null, + "metadata": { + "phoneNumber": "123456789" + }, + "id": "4d570959-dd85-4f53-bad1-18d0671761e9", + "fullName": "John Doe Williams", + "paymail": "john.doe.test@john.doe.test.4chain.space", + "pubKey": "96843af4-fc9c-4778-945d-2131ac5b1a8a", + "status": "awaiting" +} diff --git a/internal/api/v1/admin/invitations/invitations_api.go b/internal/api/v1/admin/invitations/invitations_api.go new file mode 100644 index 0000000..0bf9dae --- /dev/null +++ b/internal/api/v1/admin/invitations/invitations_api.go @@ -0,0 +1,55 @@ +package invitations + +import ( + "context" + "fmt" + "net/url" + + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/errutil" + "github.com/go-resty/resty/v2" +) + +const route = "api/v1/admin/invitations" + +type API struct { + httpClient *resty.Client + url *url.URL +} + +func (a *API) AcceptInvitation(ctx context.Context, ID string) error { + URL := a.url.JoinPath(ID).String() + _, err := a.httpClient. + R(). + SetContext(ctx). + Post(URL) + if err != nil { + return fmt.Errorf("HTTP response failure: %w", err) + } + + return nil +} + +func (a *API) RejectInvitation(ctx context.Context, ID string) error { + URL := a.url.JoinPath(ID).String() + _, err := a.httpClient. + R(). + SetContext(ctx). + Delete(URL) + if err != nil { + return fmt.Errorf("HTTP response failure: %w", err) + } + + return nil +} + +func NewAPI(url *url.URL, httpClient *resty.Client) *API { + return &API{url: url.JoinPath(route), httpClient: httpClient} +} + +func HTTPErrorFormatter(action string, err error) *errutil.HTTPErrorFormatter { + return &errutil.HTTPErrorFormatter{ + Action: action, + API: "Admin Invitations API", + Err: err, + } +} diff --git a/internal/api/v1/admin/invitations/invitations_api_test.go b/internal/api/v1/admin/invitations/invitations_api_test.go new file mode 100644 index 0000000..e6c36d8 --- /dev/null +++ b/internal/api/v1/admin/invitations/invitations_api_test.go @@ -0,0 +1,88 @@ +package invitations_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/bitcoin-sv/spv-wallet-go-client/errors" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/invitations/invitationstest" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/spvwallettest" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/require" +) + +func TestInvitationsAPI_AcceptInvitation(t *testing.T) { + id := "34d0b1f9-6d00-4bdb-ba2e-146a3cbadd35" + tests := map[string]struct { + responder httpmock.Responder + expectedErr error + }{ + fmt.Sprintf("HTTP POST /api/v1/admin/invitations/%s response: 200", id): { + responder: httpmock.NewStringResponder(http.StatusOK, http.StatusText(http.StatusOK)), + }, + fmt.Sprintf("HTTP POST /api/v1/admin/invitations/%s response: 400", id): { + expectedErr: invitationstest.NewBadRequestSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, invitationstest.NewBadRequestSPVError()), + }, + fmt.Sprintf("HTTP POST /api/v1/admin/invitations/%s response: 500", id): { + expectedErr: invitationstest.NewInternalServerSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, invitationstest.NewInternalServerSPVError()), + }, + fmt.Sprintf("HTTP POST /api/v1/admin/invitations/%s str response: 500", id): { + expectedErr: errors.ErrUnrecognizedAPIResponse, + responder: httpmock.NewStringResponder(http.StatusInternalServerError, "unexpected internal server failure"), + }, + } + + url := spvwallettest.TestAPIAddr + "/api/v1/admin/invitations/" + id + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // when: + wallet, transport := spvwallettest.GivenSPVAdminAPI(t) + transport.RegisterResponder(http.MethodPost, url, tc.responder) + + // then: + err := wallet.AcceptInvitation(context.Background(), id) + require.ErrorIs(t, err, tc.expectedErr) + }) + } +} + +func TestInvitationsAPI_RejectInvitation(t *testing.T) { + id := "34d0b1f9-6d00-4bdb-ba2e-146a3cbadd35" + tests := map[string]struct { + responder httpmock.Responder + expectedErr error + }{ + fmt.Sprintf("HTTP DELETE /api/v1/admin/invitations/%s response: 200", id): { + responder: httpmock.NewStringResponder(http.StatusOK, http.StatusText(http.StatusOK)), + }, + fmt.Sprintf("HTTP DELETE /api/v1/admin/invitations/%s response: 400", id): { + expectedErr: invitationstest.NewBadRequestSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, invitationstest.NewBadRequestSPVError()), + }, + fmt.Sprintf("HTTP DELETE /api/v1/admin/invitations/%s response: 500", id): { + expectedErr: invitationstest.NewInternalServerSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, invitationstest.NewInternalServerSPVError()), + }, + fmt.Sprintf("HTTP DELETE /api/v1/admin/invitations/%s str response: 500", id): { + expectedErr: errors.ErrUnrecognizedAPIResponse, + responder: httpmock.NewStringResponder(http.StatusInternalServerError, "unexpected internal server failure"), + }, + } + + url := spvwallettest.TestAPIAddr + "/api/v1/admin/invitations/" + id + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // when: + wallet, transport := spvwallettest.GivenSPVAdminAPI(t) + transport.RegisterResponder(http.MethodDelete, url, tc.responder) + + // then: + err := wallet.RejectInvitation(context.Background(), id) + require.ErrorIs(t, err, tc.expectedErr) + }) + } +} diff --git a/internal/api/v1/admin/invitations/invitationstest/invitations_api_fixtures.go b/internal/api/v1/admin/invitations/invitationstest/invitations_api_fixtures.go new file mode 100644 index 0000000..a3340b8 --- /dev/null +++ b/internal/api/v1/admin/invitations/invitationstest/invitations_api_fixtures.go @@ -0,0 +1,23 @@ +package invitationstest + +import ( + "net/http" + + "github.com/bitcoin-sv/spv-wallet/models" +) + +func NewBadRequestSPVError() models.SPVError { + return models.SPVError{ + Message: http.StatusText(http.StatusBadRequest), + StatusCode: http.StatusBadRequest, + Code: "invalid-data-format", + } +} + +func NewInternalServerSPVError() models.SPVError { + return models.SPVError{ + Message: http.StatusText(http.StatusInternalServerError), + StatusCode: http.StatusInternalServerError, + Code: models.UnknownErrorCode, + } +} diff --git a/internal/api/v1/admin/users/xpub_filter_builder.go b/internal/api/v1/admin/xpubs/xpub_filter_builder.go similarity index 100% rename from internal/api/v1/admin/users/xpub_filter_builder.go rename to internal/api/v1/admin/xpubs/xpub_filter_builder.go diff --git a/internal/api/v1/admin/users/xpub_filter_builder_test.go b/internal/api/v1/admin/xpubs/xpub_filter_builder_test.go similarity index 78% rename from internal/api/v1/admin/users/xpub_filter_builder_test.go rename to internal/api/v1/admin/xpubs/xpub_filter_builder_test.go index 4c6477c..8c06156 100644 --- a/internal/api/v1/admin/users/xpub_filter_builder_test.go +++ b/internal/api/v1/admin/xpubs/xpub_filter_builder_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/users/userstest" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/xpubs/xpubstest" "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" "github.com/bitcoin-sv/spv-wallet/models/filter" "github.com/stretchr/testify/require" @@ -22,7 +22,7 @@ func TestXPubFilterBuilder_Build(t *testing.T) { }, "xpub filter: filter with only 'id' field set": { filter: filter.XpubFilter{ - ID: userstest.Ptr("5505cbc3-b38f-40d4-885f-c53efd84828f"), + ID: xpubstest.Ptr("5505cbc3-b38f-40d4-885f-c53efd84828f"), }, expectedParams: url.Values{ "id": []string{"5505cbc3-b38f-40d4-885f-c53efd84828f"}, @@ -30,7 +30,7 @@ func TestXPubFilterBuilder_Build(t *testing.T) { }, "xpub filter: filter with only 'current balance' field set": { filter: filter.XpubFilter{ - CurrentBalance: userstest.Ptr(uint64(24)), + CurrentBalance: xpubstest.Ptr(uint64(24)), }, expectedParams: url.Values{ "currentBalance": []string{"24"}, @@ -38,17 +38,17 @@ func TestXPubFilterBuilder_Build(t *testing.T) { }, "xpub filter: all fields set": { filter: filter.XpubFilter{ - ID: userstest.Ptr("5505cbc3-b38f-40d4-885f-c53efd84828f"), - CurrentBalance: userstest.Ptr(uint64(24)), + ID: xpubstest.Ptr("5505cbc3-b38f-40d4-885f-c53efd84828f"), + CurrentBalance: xpubstest.Ptr(uint64(24)), ModelFilter: filter.ModelFilter{ - IncludeDeleted: userstest.Ptr(true), + IncludeDeleted: xpubstest.Ptr(true), CreatedRange: &filter.TimeRange{ - From: userstest.Ptr(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)), - To: userstest.Ptr(time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)), + From: xpubstest.Ptr(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)), + To: xpubstest.Ptr(time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)), }, UpdatedRange: &filter.TimeRange{ - From: userstest.Ptr(time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC)), - To: userstest.Ptr(time.Date(2021, 2, 2, 0, 0, 0, 0, time.UTC)), + From: xpubstest.Ptr(time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC)), + To: xpubstest.Ptr(time.Date(2021, 2, 2, 0, 0, 0, 0, time.UTC)), }, }, }, diff --git a/internal/api/v1/admin/users/xpubs_api.go b/internal/api/v1/admin/xpubs/xpubs_api.go similarity index 100% rename from internal/api/v1/admin/users/xpubs_api.go rename to internal/api/v1/admin/xpubs/xpubs_api.go diff --git a/internal/api/v1/admin/users/xpubs_api_test.go b/internal/api/v1/admin/xpubs/xpubs_api_test.go similarity index 65% rename from internal/api/v1/admin/users/xpubs_api_test.go rename to internal/api/v1/admin/xpubs/xpubs_api_test.go index a92e607..4c5d214 100644 --- a/internal/api/v1/admin/users/xpubs_api_test.go +++ b/internal/api/v1/admin/xpubs/xpubs_api_test.go @@ -7,7 +7,7 @@ import ( "github.com/bitcoin-sv/spv-wallet-go-client/commands" "github.com/bitcoin-sv/spv-wallet-go-client/errors" - "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/users/userstest" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/admin/xpubs/xpubstest" "github.com/bitcoin-sv/spv-wallet-go-client/internal/spvwallettest" "github.com/bitcoin-sv/spv-wallet-go-client/queries" "github.com/bitcoin-sv/spv-wallet/models/response" @@ -22,12 +22,16 @@ func TestXPubsAPI_CreateXPub(t *testing.T) { expectedErr error }{ "HTTP POST /api/v1/admin/users response: 201": { - expectedResponse: userstest.ExpectedXPub(t), - responder: httpmock.NewJsonResponderOrPanic(http.StatusCreated, httpmock.File("userstest/post_xpub_201.json")), + expectedResponse: xpubstest.ExpectedXPub(t), + responder: httpmock.NewJsonResponderOrPanic(http.StatusCreated, httpmock.File("xpubstest/post_xpub_201.json")), }, "HTTP POST /api/v1/admin/users response: 400": { - expectedErr: userstest.NewBadRequestSPVError(), - responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, userstest.NewBadRequestSPVError()), + expectedErr: xpubstest.NewBadRequestSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, xpubstest.NewBadRequestSPVError()), + }, + "HTTP POST /api/v1/admin/users response: 500": { + expectedErr: xpubstest.NewInternalServerSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, xpubstest.NewInternalServerSPVError()), }, "HTTP POST /api/v1/admin/users str response: 500": { expectedErr: errors.ErrUnrecognizedAPIResponse, @@ -35,12 +39,12 @@ func TestXPubsAPI_CreateXPub(t *testing.T) { }, } - URL := spvwallettest.TestAPIAddr + "/api/v1/admin/users" + url := spvwallettest.TestAPIAddr + "/api/v1/admin/users" for name, tc := range tests { t.Run(name, func(t *testing.T) { // given: wallet, transport := spvwallettest.GivenSPVAdminAPI(t) - transport.RegisterResponder(http.MethodPost, URL, tc.responder) + transport.RegisterResponder(http.MethodPost, url, tc.responder) // when: got, err := wallet.CreateXPub(context.Background(), &commands.CreateUserXpub{ @@ -62,25 +66,29 @@ func TestXPubsAPI_XPubs(t *testing.T) { expectedErr error }{ "HTTP GET /api/v1/admin/users response: 200": { - expectedResponse: userstest.ExpectedXPubsPage(t), - responder: httpmock.NewJsonResponderOrPanic(http.StatusOK, httpmock.File("userstest/get_xpubs_200.json")), + expectedResponse: xpubstest.ExpectedXPubsPage(t), + responder: httpmock.NewJsonResponderOrPanic(http.StatusOK, httpmock.File("xpubstest/get_xpubs_200.json")), }, "HTTP GET /api/v1/admin/users response: 400": { - expectedErr: userstest.NewBadRequestSPVError(), - responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, userstest.NewBadRequestSPVError()), + expectedErr: xpubstest.NewBadRequestSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusBadRequest, xpubstest.NewBadRequestSPVError()), + }, + "HTTP GET /api/v1/admin/users response: 500": { + expectedErr: xpubstest.NewInternalServerSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, xpubstest.NewInternalServerSPVError()), }, "HTTP GET /api/v1/admin/users str response: 500": { - expectedErr: userstest.NewInternalServerSPVError(), - responder: httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, userstest.NewInternalServerSPVError()), + expectedErr: xpubstest.NewInternalServerSPVError(), + responder: httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, xpubstest.NewInternalServerSPVError()), }, } - URL := spvwallettest.TestAPIAddr + "/api/v1/admin/users" + url := spvwallettest.TestAPIAddr + "/api/v1/admin/users" for name, tc := range tests { t.Run(name, func(t *testing.T) { // given: wallet, transport := spvwallettest.GivenSPVAdminAPI(t) - transport.RegisterResponder(http.MethodGet, URL, tc.responder) + transport.RegisterResponder(http.MethodGet, url, tc.responder) // when: got, err := wallet.XPubs(context.Background()) diff --git a/internal/api/v1/admin/users/userstest/get_xpubs_200.json b/internal/api/v1/admin/xpubs/xpubstest/get_xpubs_200.json similarity index 100% rename from internal/api/v1/admin/users/userstest/get_xpubs_200.json rename to internal/api/v1/admin/xpubs/xpubstest/get_xpubs_200.json diff --git a/internal/api/v1/admin/users/userstest/post_xpub_201.json b/internal/api/v1/admin/xpubs/xpubstest/post_xpub_201.json similarity index 100% rename from internal/api/v1/admin/users/userstest/post_xpub_201.json rename to internal/api/v1/admin/xpubs/xpubstest/post_xpub_201.json diff --git a/internal/api/v1/admin/users/userstest/xpub_api_fixtures.go b/internal/api/v1/admin/xpubs/xpubstest/xpub_api_fixtures.go similarity index 99% rename from internal/api/v1/admin/users/userstest/xpub_api_fixtures.go rename to internal/api/v1/admin/xpubs/xpubstest/xpub_api_fixtures.go index af20319..a3d6a32 100644 --- a/internal/api/v1/admin/users/userstest/xpub_api_fixtures.go +++ b/internal/api/v1/admin/xpubs/xpubstest/xpub_api_fixtures.go @@ -1,4 +1,4 @@ -package userstest +package xpubstest import ( "net/http" diff --git a/internal/api/v1/user/contacts/contact_filter_query_builder.go b/internal/api/v1/user/contacts/contact_filter_query_builder.go index c4d50d7..4972109 100644 --- a/internal/api/v1/user/contacts/contact_filter_query_builder.go +++ b/internal/api/v1/user/contacts/contact_filter_query_builder.go @@ -8,13 +8,13 @@ import ( "github.com/bitcoin-sv/spv-wallet/models/filter" ) -type contactFilterQueryBuilder struct { - modelFilterBuilder querybuilders.ModelFilterBuilder - contactFilter filter.ContactFilter +type ContactFilterQueryBuilder struct { + ModelFilterBuilder querybuilders.ModelFilterBuilder + ContactFilter filter.ContactFilter } -func (c *contactFilterQueryBuilder) Build() (url.Values, error) { - modelFilterBuilder, err := c.modelFilterBuilder.Build() +func (c *ContactFilterQueryBuilder) Build() (url.Values, error) { + modelFilterBuilder, err := c.ModelFilterBuilder.Build() if err != nil { return nil, fmt.Errorf("failed to build model filter query params: %w", err) } @@ -24,10 +24,10 @@ func (c *contactFilterQueryBuilder) Build() (url.Values, error) { params.Append(modelFilterBuilder) } - params.AddPair("id", c.contactFilter.ID) - params.AddPair("fullName", c.contactFilter.FullName) - params.AddPair("paymail", c.contactFilter.Paymail) - params.AddPair("pubKey", c.contactFilter.PubKey) - params.AddPair("status", c.contactFilter.Status) + params.AddPair("id", c.ContactFilter.ID) + params.AddPair("fullName", c.ContactFilter.FullName) + params.AddPair("paymail", c.ContactFilter.Paymail) + params.AddPair("pubKey", c.ContactFilter.PubKey) + params.AddPair("status", c.ContactFilter.Status) return params.Values, nil } diff --git a/internal/api/v1/user/contacts/contact_filter_query_builder_test.go b/internal/api/v1/user/contacts/contact_filter_query_builder_test.go index a39e023..5ebe4f0 100644 --- a/internal/api/v1/user/contacts/contact_filter_query_builder_test.go +++ b/internal/api/v1/user/contacts/contact_filter_query_builder_test.go @@ -1,10 +1,11 @@ -package contacts +package contacts_test import ( "net/url" "testing" "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders" + "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/user/contacts" "github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/user/contacts/contactstest" "github.com/bitcoin-sv/spv-wallet/models/filter" "github.com/stretchr/testify/require" @@ -70,9 +71,9 @@ func TestContactFilterQueryBuilder_Build(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { // when: - queryBuilder := contactFilterQueryBuilder{ - contactFilter: tc.filter, - modelFilterBuilder: querybuilders.ModelFilterBuilder{ModelFilter: tc.filter.ModelFilter}, + queryBuilder := contacts.ContactFilterQueryBuilder{ + ContactFilter: tc.filter, + ModelFilterBuilder: querybuilders.ModelFilterBuilder{ModelFilter: tc.filter.ModelFilter}, } // then: diff --git a/internal/api/v1/user/contacts/contacts_api.go b/internal/api/v1/user/contacts/contacts_api.go index a38c86e..e50ae85 100644 --- a/internal/api/v1/user/contacts/contacts_api.go +++ b/internal/api/v1/user/contacts/contacts_api.go @@ -29,9 +29,9 @@ func (a *API) Contacts(ctx context.Context, opts ...queries.ContactQueryOption) queryBuilder := querybuilders.NewQueryBuilder( querybuilders.WithMetadataFilter(query.Metadata), querybuilders.WithPageFilter(query.PageFilter), - querybuilders.WithFilterQueryBuilder(&contactFilterQueryBuilder{ - contactFilter: query.ContactFilter, - modelFilterBuilder: querybuilders.ModelFilterBuilder{ + querybuilders.WithFilterQueryBuilder(&ContactFilterQueryBuilder{ + ContactFilter: query.ContactFilter, + ModelFilterBuilder: querybuilders.ModelFilterBuilder{ ModelFilter: query.ContactFilter.ModelFilter, }, }), diff --git a/user_api.go b/user_api.go index a2a7a6a..ed00a46 100644 --- a/user_api.go +++ b/user_api.go @@ -57,7 +57,7 @@ type UserAPI struct { func (u *UserAPI) Contacts(ctx context.Context, contactOpts ...queries.ContactQueryOption) (*queries.UserContactsPage, error) { res, err := u.contactsAPI.Contacts(ctx, contactOpts...) if err != nil { - return nil, contacts.HTTPErrorFormatter("retrieve contact", err).FormatGetErr() + return nil, contacts.HTTPErrorFormatter("retrieve user contacts page", err).FormatGetErr() } return res, nil