From e03e2476b83ac6d44562ece8f30f395b850e6538 Mon Sep 17 00:00:00 2001 From: Wilson Lin <125257432+wilson-dnsimple@users.noreply.github.com> Date: Thu, 24 Aug 2023 16:07:20 +1000 Subject: [PATCH] Add support for registrant APIs (#146) --- CHANGELOG.md | 6 + dnsimple/dnsimple.go | 10 +- dnsimple/registrar_registrant_changes.go | 161 ++++++++++++++++++ dnsimple/registrar_registrant_changes_test.go | 158 +++++++++++++++++ .../error-contactnotfound.http | 14 ++ .../error-domainnotfound.http | 15 ++ .../api/checkRegistrantChange/success.http | 15 ++ .../api/createRegistrantChange/success.http | 14 ++ .../api/deleteRegistrantChange/success.http | 13 ++ .../api/getRegistrantChange/success.http | 15 ++ .../api/listRegistrantChanges/success.http | 15 ++ 11 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 dnsimple/registrar_registrant_changes.go create mode 100644 dnsimple/registrar_registrant_changes_test.go create mode 100644 fixtures.http/api/checkRegistrantChange/error-contactnotfound.http create mode 100644 fixtures.http/api/checkRegistrantChange/error-domainnotfound.http create mode 100644 fixtures.http/api/checkRegistrantChange/success.http create mode 100644 fixtures.http/api/createRegistrantChange/success.http create mode 100644 fixtures.http/api/deleteRegistrantChange/success.http create mode 100644 fixtures.http/api/getRegistrantChange/success.http create mode 100644 fixtures.http/api/listRegistrantChanges/success.http diff --git a/CHANGELOG.md b/CHANGELOG.md index dc80292..0f9d71c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## main +## 1.3.0 (Unreleased) + +FEATURES: + +- NEW: Added `ListRegistrantChanges`, `CreateRegistrantChange`, `CheckRegistrantChange`, `GetRegistrantChange`, and `DeleteRegistrantChange` APIs to manage registrant changes. (dnsimple/dnsimple-go#146) + ## 1.2.1 FEATURES: diff --git a/dnsimple/dnsimple.go b/dnsimple/dnsimple.go index 39b82fa..02bb48d 100644 --- a/dnsimple/dnsimple.go +++ b/dnsimple/dnsimple.go @@ -262,7 +262,15 @@ func (c *Client) request(ctx context.Context, req *http.Request, obj interface{} if w, ok := obj.(io.Writer); ok { _, err = io.Copy(w, resp.Body) } else { - err = json.NewDecoder(resp.Body).Decode(obj) + var raw []byte + raw, err = io.ReadAll(resp.Body) + if err == nil { + if len(raw) == 0 { + // TODO Ignore empty body as temporary workaround for server sending Content-Type: application/json with an empty body. + } else { + err = json.Unmarshal(raw, obj) + } + } } } diff --git a/dnsimple/registrar_registrant_changes.go b/dnsimple/registrar_registrant_changes.go new file mode 100644 index 0000000..b8858fb --- /dev/null +++ b/dnsimple/registrar_registrant_changes.go @@ -0,0 +1,161 @@ +package dnsimple + +import ( + "context" + "fmt" +) + +type CreateRegistrantChangeInput struct { + DomainId string `json:"domain_id"` + ContactId string `json:"contact_id"` + ExtendedAttributes map[string]string `json:"extended_attributes"` +} + +type RegistrantChange struct { + Id int `json:"id"` + AccountId int `json:"account_id"` + ContactId int `json:"contact_id"` + DomainId int `json:"domain_id"` + // One of: "new", "pending", "cancelling", "cancelled", "completed". + State string `json:"state"` + ExtendedAttributes map[string]string `json:"extended_attributes"` + RegistryOwnerChange bool `json:"registry_owner_change"` + IrtLockLiftedBy string `json:"irt_lock_lifted_by"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type RegistrantChangeResponse struct { + Response + Data *RegistrantChange `json:"data"` +} + +type RegistrantChangesListResponse struct { + Response + Data []RegistrantChange `json:"data"` +} + +type RegistrantChangeListOptions struct { + // Only include results with a state field exactly matching the given string + State *string `url:"state,omitempty"` + // Only include results with a domain_id field exactly matching the given string + DomainId *string `url:"domain_id,omitempty"` + // Only include results with a contact_id field exactly matching the given string + ContactId *string `url:"contact_id,omitempty"` + + ListOptions +} + +type CheckRegistrantChangeInput struct { + DomainId string `json:"domain_id"` + ContactId string `json:"contact_id"` +} + +type ExtendedAttribute struct { + Name string `json:"name"` + Description string `json:"description"` + Required bool `json:"required"` + Options []ExtendedAttributeOption `json:"options"` +} + +type ExtendedAttributeOption struct { + Title string `json:"title"` + Value string `json:"value"` + Description string `json:"description"` +} + +type RegistrantChangeCheck struct { + DomainId int `json:"domain_id"` + ContactId int `json:"contact_id"` + ExtendedAttributes []ExtendedAttribute `json:"extended_attributes"` + RegistryOwnerChange bool `json:"registry_owner_change"` +} + +type RegistrantChangeCheckResponse struct { + Response + Data *RegistrantChangeCheck `json:"data"` +} + +type RegistrantChangeDeleteResponse struct { + Response +} + +// ListRegistrantChange lists registrant changes in the account. +// +// See https://developer.dnsimple.com/v2/registrar/#listRegistrantChanges +func (s *RegistrarService) ListRegistrantChange(ctx context.Context, accountID string, options *RegistrantChangeListOptions) (*RegistrantChangesListResponse, error) { + path := versioned(fmt.Sprintf("/%v/registrar/registrant_changes", accountID)) + changeResponse := &RegistrantChangesListResponse{} + + resp, err := s.client.get(ctx, path, changeResponse) + if err != nil { + return nil, err + } + + changeResponse.HTTPResponse = resp + return changeResponse, nil +} + +// CreateRegistrantChange starts a registrant change. +// +// See https://developer.dnsimple.com/v2/registrar/#createRegistrantChange +func (s *RegistrarService) CreateRegistrantChange(ctx context.Context, accountID string, input *CreateRegistrantChangeInput) (*RegistrantChangeResponse, error) { + path := versioned(fmt.Sprintf("/%v/registrar/registrant_changes", accountID)) + changeResponse := &RegistrantChangeResponse{} + + resp, err := s.client.post(ctx, path, input, changeResponse) + if err != nil { + return nil, err + } + + changeResponse.HTTPResponse = resp + return changeResponse, nil +} + +// CheckRegistrantChange retrieves the requirements of a registrant change. +// +// See https://developer.dnsimple.com/v2/registrar/#checkRegistrantChange +func (s *RegistrarService) CheckRegistrantChange(ctx context.Context, accountID string, input *CheckRegistrantChangeInput) (*RegistrantChangeCheckResponse, error) { + path := versioned(fmt.Sprintf("/%v/registrar/registrant_changes/check", accountID)) + checkResponse := &RegistrantChangeCheckResponse{} + + resp, err := s.client.post(ctx, path, input, checkResponse) + if err != nil { + return nil, err + } + + checkResponse.HTTPResponse = resp + return checkResponse, nil +} + +// GetRegistrantChange retrieves the details of an existing registrant change. +// +// See https://developer.dnsimple.com/v2/registrar/#getRegistrantChange +func (s *RegistrarService) GetRegistrantChange(ctx context.Context, accountID string, registrantChange int) (*RegistrantChangeResponse, error) { + path := versioned(fmt.Sprintf("/%v/registrar/registrant_changes/%v", accountID, registrantChange)) + checkResponse := &RegistrantChangeResponse{} + + resp, err := s.client.get(ctx, path, checkResponse) + if err != nil { + return nil, err + } + + checkResponse.HTTPResponse = resp + return checkResponse, nil +} + +// DeleteRegistrantChange cancels an ongoing registrant change from the account. +// +// See https://developer.dnsimple.com/v2/registrar/#deleteRegistrantChange +func (s *RegistrarService) DeleteRegistrantChange(ctx context.Context, accountID string, registrantChange int) (*RegistrantChangeDeleteResponse, error) { + path := versioned(fmt.Sprintf("/%v/registrar/registrant_changes/%v", accountID, registrantChange)) + deleteResponse := &RegistrantChangeDeleteResponse{} + + resp, err := s.client.delete(ctx, path, nil, deleteResponse) + if err != nil { + return nil, err + } + + deleteResponse.HTTPResponse = resp + return deleteResponse, nil +} diff --git a/dnsimple/registrar_registrant_changes_test.go b/dnsimple/registrar_registrant_changes_test.go new file mode 100644 index 0000000..92e548f --- /dev/null +++ b/dnsimple/registrar_registrant_changes_test.go @@ -0,0 +1,158 @@ +package dnsimple + +import ( + "context" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRegistrarService_ListRegistrantChanges(t *testing.T) { + setupMockServer() + defer teardownMockServer() + + mux.HandleFunc("/v2/1010/registrar/registrant_changes", func(w http.ResponseWriter, r *http.Request) { + httpResponse := httpResponseFixture(t, "/api/listRegistrantChanges/success.http") + + testMethod(t, r, "GET") + testHeaders(t, r) + + w.WriteHeader(httpResponse.StatusCode) + _, _ = io.Copy(w, httpResponse.Body) + }) + + res, err := client.Registrar.ListRegistrantChange(context.Background(), "1010", &RegistrantChangeListOptions{}) + + assert.NoError(t, err) + changes := res.Data + assert.Equal(t, changes[0], RegistrantChange{ + Id: 101, + AccountId: 101, + DomainId: 101, + ContactId: 101, + State: "new", + ExtendedAttributes: map[string]string{}, + RegistryOwnerChange: true, + IrtLockLiftedBy: "", + CreatedAt: "2017-02-03T17:43:22Z", + UpdatedAt: "2017-02-03T17:43:22Z", + }) +} + +func TestRegistrarService_CreateRegistrantChange(t *testing.T) { + setupMockServer() + defer teardownMockServer() + + mux.HandleFunc("/v2/1010/registrar/registrant_changes", func(w http.ResponseWriter, r *http.Request) { + httpResponse := httpResponseFixture(t, "/api/createRegistrantChange/success.http") + + testMethod(t, r, "POST") + testHeaders(t, r) + + w.WriteHeader(httpResponse.StatusCode) + _, _ = io.Copy(w, httpResponse.Body) + }) + + res, err := client.Registrar.CreateRegistrantChange(context.Background(), "1010", &CreateRegistrantChangeInput{ + DomainId: "example.com", + ContactId: "101", + ExtendedAttributes: map[string]string{}, + }) + + assert.NoError(t, err) + change := res.Data + assert.Equal(t, change, &RegistrantChange{ + Id: 101, + AccountId: 101, + DomainId: 101, + ContactId: 101, + State: "new", + ExtendedAttributes: map[string]string{}, + RegistryOwnerChange: true, + IrtLockLiftedBy: "", + CreatedAt: "2017-02-03T17:43:22Z", + UpdatedAt: "2017-02-03T17:43:22Z", + }) +} + +func TestRegistrarService_CheckRegistrantChange(t *testing.T) { + setupMockServer() + defer teardownMockServer() + + mux.HandleFunc("/v2/1010/registrar/registrant_changes/check", func(w http.ResponseWriter, r *http.Request) { + httpResponse := httpResponseFixture(t, "/api/checkRegistrantChange/success.http") + + testMethod(t, r, "POST") + testHeaders(t, r) + + w.WriteHeader(httpResponse.StatusCode) + _, _ = io.Copy(w, httpResponse.Body) + }) + + res, err := client.Registrar.CheckRegistrantChange(context.Background(), "1010", &CheckRegistrantChangeInput{ + DomainId: "example.com", + ContactId: "101", + }) + + assert.NoError(t, err) + change := res.Data + assert.Equal(t, change, &RegistrantChangeCheck{ + DomainId: 101, + ContactId: 101, + ExtendedAttributes: make([]ExtendedAttribute, 0), + RegistryOwnerChange: true, + }) +} + +func TestRegistrarService_GetRegistrantChange(t *testing.T) { + setupMockServer() + defer teardownMockServer() + + mux.HandleFunc("/v2/1010/registrar/registrant_changes/101", func(w http.ResponseWriter, r *http.Request) { + httpResponse := httpResponseFixture(t, "/api/getRegistrantChange/success.http") + + testMethod(t, r, "GET") + testHeaders(t, r) + + w.WriteHeader(httpResponse.StatusCode) + _, _ = io.Copy(w, httpResponse.Body) + }) + + res, err := client.Registrar.GetRegistrantChange(context.Background(), "1010", 101) + + assert.NoError(t, err) + change := res.Data + assert.Equal(t, change, &RegistrantChange{ + Id: 101, + AccountId: 101, + DomainId: 101, + ContactId: 101, + State: "new", + ExtendedAttributes: map[string]string{}, + RegistryOwnerChange: true, + IrtLockLiftedBy: "", + CreatedAt: "2017-02-03T17:43:22Z", + UpdatedAt: "2017-02-03T17:43:22Z", + }) +} + +func TestRegistrarService_DeleteRegistrantChange(t *testing.T) { + setupMockServer() + defer teardownMockServer() + + mux.HandleFunc("/v2/1010/registrar/registrant_changes/101", func(w http.ResponseWriter, r *http.Request) { + httpResponse := httpResponseFixture(t, "/api/deleteRegistrantChange/success.http") + + testMethod(t, r, "DELETE") + testHeaders(t, r) + + w.WriteHeader(httpResponse.StatusCode) + _, _ = io.Copy(w, httpResponse.Body) + }) + + _, err := client.Registrar.DeleteRegistrantChange(context.Background(), "1010", 101) + + assert.NoError(t, err) +} diff --git a/fixtures.http/api/checkRegistrantChange/error-contactnotfound.http b/fixtures.http/api/checkRegistrantChange/error-contactnotfound.http new file mode 100644 index 0000000..4d82ae2 --- /dev/null +++ b/fixtures.http/api/checkRegistrantChange/error-contactnotfound.http @@ -0,0 +1,14 @@ +HTTP/1.1 404 +server: nginx +date: Tue, 22 Aug 2023 13:59:02 GMT +content-type: application/json; charset=utf-8 +x-ratelimit-limit: 2400 +x-ratelimit-remaining: 2398 +x-ratelimit-reset: 1692716201 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +cache-control: no-cache +x-request-id: b1dd3f42-ebb9-42fd-a121-d595de96f667 +x-runtime: 0.019122 +strict-transport-security: max-age=63072000 + +{"message":"Contact `21` not found"} \ No newline at end of file diff --git a/fixtures.http/api/checkRegistrantChange/error-domainnotfound.http b/fixtures.http/api/checkRegistrantChange/error-domainnotfound.http new file mode 100644 index 0000000..6dcc238 --- /dev/null +++ b/fixtures.http/api/checkRegistrantChange/error-domainnotfound.http @@ -0,0 +1,15 @@ +HTTP/1.1 404 +server: nginx +date: Tue, 22 Aug 2023 11:09:40 GMT +content-type: application/json; charset=utf-8 +x-ratelimit-limit: 2400 +x-ratelimit-remaining: 2395 +x-ratelimit-reset: 1692705338 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +etag: W/"cef1e7d85d0b9bfd25e81b812891d34f" +cache-control: max-age=0, private, must-revalidate +x-request-id: 5b0d8bfb-7b6a-40b5-a079-b640fd817e34 +x-runtime: 3.066249 +strict-transport-security: max-age=63072000 + +{"message":"Domain `dnsimple-rraform.bio` not found"} \ No newline at end of file diff --git a/fixtures.http/api/checkRegistrantChange/success.http b/fixtures.http/api/checkRegistrantChange/success.http new file mode 100644 index 0000000..0520f6c --- /dev/null +++ b/fixtures.http/api/checkRegistrantChange/success.http @@ -0,0 +1,15 @@ +HTTP/1.1 200 +server: nginx +date: Tue, 22 Aug 2023 11:09:40 GMT +content-type: application/json; charset=utf-8 +x-ratelimit-limit: 2400 +x-ratelimit-remaining: 2395 +x-ratelimit-reset: 1692705338 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +etag: W/"cef1e7d85d0b9bfd25e81b812891d34f" +cache-control: max-age=0, private, must-revalidate +x-request-id: 5b0d8bfb-7b6a-40b5-a079-b640fd817e34 +x-runtime: 3.066249 +strict-transport-security: max-age=63072000 + +{"data":{"domain_id":101,"contact_id":101,"extended_attributes":[],"registry_owner_change":true}} \ No newline at end of file diff --git a/fixtures.http/api/createRegistrantChange/success.http b/fixtures.http/api/createRegistrantChange/success.http new file mode 100644 index 0000000..79048ee --- /dev/null +++ b/fixtures.http/api/createRegistrantChange/success.http @@ -0,0 +1,14 @@ +HTTP/1.1 202 +server: nginx +date: Tue, 22 Aug 2023 11:11:00 GMT +content-type: application/json; charset=utf-8 +x-ratelimit-limit: 2400 +x-ratelimit-remaining: 2394 +x-ratelimit-reset: 1692705339 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +cache-control: no-cache +x-request-id: 26bf7ff9-2075-42b0-9431-1778c825b6b0 +x-runtime: 3.408950 +strict-transport-security: max-age=63072000 + +{"data":{"id":101,"account_id":101,"domain_id":101,"contact_id":101,"state":"new","extended_attributes":{},"registry_owner_change":true,"irt_lock_lifted_by":null,"created_at":"2017-02-03T17:43:22Z","updated_at":"2017-02-03T17:43:22Z"}} \ No newline at end of file diff --git a/fixtures.http/api/deleteRegistrantChange/success.http b/fixtures.http/api/deleteRegistrantChange/success.http new file mode 100644 index 0000000..569ad51 --- /dev/null +++ b/fixtures.http/api/deleteRegistrantChange/success.http @@ -0,0 +1,13 @@ +HTTP/1.1 201 +server: nginx +date: Tue, 22 Aug 2023 11:14:44 GMT +content-type: application/json; charset=utf-8 +x-ratelimit-limit: 2400 +x-ratelimit-remaining: 2391 +x-ratelimit-reset: 1692705338 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +cache-control: no-cache +x-request-id: b123e1f0-aa70-4abb-95cf-34f377c83ef4 +x-runtime: 0.114839 +strict-transport-security: max-age=63072000 + diff --git a/fixtures.http/api/getRegistrantChange/success.http b/fixtures.http/api/getRegistrantChange/success.http new file mode 100644 index 0000000..9bf625c --- /dev/null +++ b/fixtures.http/api/getRegistrantChange/success.http @@ -0,0 +1,15 @@ +HTTP/1.1 200 +server: nginx +date: Tue, 22 Aug 2023 11:13:58 GMT +content-type: application/json; charset=utf-8 +x-ratelimit-limit: 2400 +x-ratelimit-remaining: 2392 +x-ratelimit-reset: 1692705338 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +etag: W/"76c5d4c7579b754b94a42ac7fa37a901" +cache-control: max-age=0, private, must-revalidate +x-request-id: e910cd08-3f9c-4da4-9986-50dbe9c3bc55 +x-runtime: 0.022006 +strict-transport-security: max-age=63072000 + +{"data":{"id":101,"account_id":101,"domain_id":101,"contact_id":101,"state":"new","extended_attributes":{},"registry_owner_change":true,"irt_lock_lifted_by":null,"created_at":"2017-02-03T17:43:22Z","updated_at":"2017-02-03T17:43:22Z"}} \ No newline at end of file diff --git a/fixtures.http/api/listRegistrantChanges/success.http b/fixtures.http/api/listRegistrantChanges/success.http new file mode 100644 index 0000000..91f1519 --- /dev/null +++ b/fixtures.http/api/listRegistrantChanges/success.http @@ -0,0 +1,15 @@ +HTTP/1.1 200 +server: nginx +date: Tue, 22 Aug 2023 11:12:49 GMT +content-type: application/json; charset=utf-8 +x-ratelimit-limit: 2400 +x-ratelimit-remaining: 2393 +x-ratelimit-reset: 1692705338 +x-work-with-us: Love automation? So do we! https://dnsimple.com/jobs +etag: W/"0049703ea058b06346df4c0e169eac29" +cache-control: max-age=0, private, must-revalidate +x-request-id: fd0334ce-414a-4872-8889-e548e0b1410c +x-runtime: 0.030759 +strict-transport-security: max-age=63072000 + +{"data":[{"id":101,"account_id":101,"domain_id":101,"contact_id":101,"state":"new","extended_attributes":{},"registry_owner_change":true,"irt_lock_lifted_by":null,"created_at":"2017-02-03T17:43:22Z","updated_at":"2017-02-03T17:43:22Z"}],"pagination":{"current_page":1,"per_page":30,"total_entries":1,"total_pages":1}} \ No newline at end of file