From 495cb297885163bd7b96b70c6dc11641e0fb2e33 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea <28300158+sergiught@users.noreply.github.com> Date: Mon, 19 Dec 2022 18:47:00 +0100 Subject: [PATCH] Add ability to retrieve job errors (#141) --- management/job.go | 23 ++++ management/job_test.go | 41 +++++++ management/management.gen.go | 10 ++ management/management.gen_test.go | 16 +++ .../recordings/TestJobManager_ReadErrors.yaml | 102 ++++++++++++++++++ 5 files changed, 192 insertions(+) create mode 100644 management/testdata/recordings/TestJobManager_ReadErrors.yaml diff --git a/management/job.go b/management/job.go index 10a094cd..58a5cf38 100644 --- a/management/job.go +++ b/management/job.go @@ -69,6 +69,21 @@ type JobSummary struct { Total *int `json:"total,omitempty"` } +// JobError is used to check for errors during jobs. +// +// See: https://auth0.com/docs/manage-users/user-migration/bulk-user-imports#retrieve-failed-entries +type JobError struct { + User map[string]interface{} `json:"user,omitempty"` + Errors []JobUserErrors `json:"errors,omitempty"` +} + +// JobUserErrors holds errors for the specific user during a job. +type JobUserErrors struct { + Code string `json:"code,omitempty"` + Message string `json:"message,omitempty"` + Path string `json:"path,omitempty"` +} + // JobManager manages Auth0 Job resources. type JobManager struct { *Management @@ -86,6 +101,14 @@ func (m *JobManager) Read(id string, opts ...RequestOption) (j *Job, err error) return } +// ReadErrors retrieves error details of a failed job. +// +// See: https://auth0.com/docs/api/management/v2#!/Jobs/get_errors +func (m *JobManager) ReadErrors(id string, opts ...RequestOption) (jobErrors []JobError, err error) { + err = m.Request("GET", m.URI("jobs", id, "errors"), &jobErrors, opts...) + return +} + // VerifyEmail sends an email to the specified user that asks them to // click a link to verify their email address. func (m *JobManager) VerifyEmail(j *Job, opts ...RequestOption) error { diff --git a/management/job_test.go b/management/job_test.go index 02623464..a23102c9 100644 --- a/management/job_test.go +++ b/management/job_test.go @@ -86,3 +86,44 @@ func TestJobManager_ImportUsers(t *testing.T) { cleanupUser(t, users[0].GetID()) }) } + +func TestJobManager_ReadErrors(t *testing.T) { + setupHTTPRecordings(t) + + alreadyExistingUser := givenAUser(t) + conn, err := m.Connection.ReadByName("Username-Password-Authentication") + require.NoError(t, err) + + job := &Job{ + ConnectionID: conn.ID, + Users: []map[string]interface{}{ + { + "email": alreadyExistingUser.GetEmail(), + "email_verified": true, + }, + }, + } + err = m.Job.ImportUsers(job) + assert.NoError(t, err) + + // Let's give the ImportUsers job enough time to complete. + time.Sleep(time.Second * 2) + + expectedJobErrors := JobError{ + User: map[string]interface{}{ + "email": alreadyExistingUser.GetEmail(), + "email_verified": true, + }, + Errors: []JobUserErrors{ + { + Code: "DUPLICATED_USER", + Message: "The user already exist and upsert parameter is false", + }, + }, + } + + actualJobErrors, err := m.Job.ReadErrors(job.GetID()) + assert.NoError(t, err) + assert.Len(t, actualJobErrors, 1) + assert.Equal(t, expectedJobErrors, actualJobErrors[0]) +} diff --git a/management/management.gen.go b/management/management.gen.go index fc502a5e..0ce4f7e2 100644 --- a/management/management.gen.go +++ b/management/management.gen.go @@ -4965,6 +4965,11 @@ func (j *Job) String() string { return Stringify(j) } +// String returns a string representation of JobError. +func (j *JobError) String() string { + return Stringify(j) +} + // GetFailed returns the Failed field if it's non-nil, zero value otherwise. func (j *JobSummary) GetFailed() int { if j == nil || j.Failed == nil { @@ -5002,6 +5007,11 @@ func (j *JobSummary) String() string { return Stringify(j) } +// String returns a string representation of JobUserErrors. +func (j *JobUserErrors) String() string { + return Stringify(j) +} + // String returns a string representation of List. func (l *List) String() string { return Stringify(l) diff --git a/management/management.gen_test.go b/management/management.gen_test.go index 9d4917c1..a51f3207 100644 --- a/management/management.gen_test.go +++ b/management/management.gen_test.go @@ -6290,6 +6290,14 @@ func TestJob_String(t *testing.T) { } } +func TestJobError_String(t *testing.T) { + var rawJSON json.RawMessage + v := &JobError{} + if err := json.Unmarshal([]byte(v.String()), &rawJSON); err != nil { + t.Errorf("failed to produce a valid json") + } +} + func TestJobSummary_GetFailed(tt *testing.T) { var zeroValue int j := &JobSummary{Failed: &zeroValue} @@ -6338,6 +6346,14 @@ func TestJobSummary_String(t *testing.T) { } } +func TestJobUserErrors_String(t *testing.T) { + var rawJSON json.RawMessage + v := &JobUserErrors{} + if err := json.Unmarshal([]byte(v.String()), &rawJSON); err != nil { + t.Errorf("failed to produce a valid json") + } +} + func TestList_String(t *testing.T) { var rawJSON json.RawMessage v := &List{} diff --git a/management/testdata/recordings/TestJobManager_ReadErrors.yaml b/management/testdata/recordings/TestJobManager_ReadErrors.yaml new file mode 100644 index 00000000..914d8c14 --- /dev/null +++ b/management/testdata/recordings/TestJobManager_ReadErrors.yaml @@ -0,0 +1,102 @@ +--- +version: 1 +interactions: + - request: + body: | + {"connection":"Username-Password-Authentication","email":"chuck878@example.com","given_name":"Chuck","family_name":"Sanchez","username":"test-user636","nickname":"Chucky","password":"Passwords hide their chuck","user_metadata":{"favourite_attack":"roundhouse_kick"},"verify_email":false,"app_metadata":{"facts":["count_to_infinity_twice","kill_two_stones_with_one_bird","can_hear_sign_language"]},"picture":"https://example-picture-url.jpg","blocked":false,"email_verified":true} + form: { } + headers: + Content-Type: + - application/json + User-Agent: + - Go-Auth0-SDK/latest + url: https://go-auth0-dev.eu.auth0.com/api/v2/users + method: POST + response: + body: '{"blocked":false,"created_at":"2022-12-19T12:51:38.614Z","email":"chuck878@example.com","email_verified":true,"family_name":"Sanchez","given_name":"Chuck","identities":[{"connection":"Username-Password-Authentication","user_id":"63a05e5a6ea7634fae1893af","provider":"auth0","isSocial":false}],"name":"chuck878@example.com","nickname":"Chucky","picture":"https://example-picture-url.jpg","updated_at":"2022-12-19T12:51:38.614Z","user_id":"auth0|63a05e5a6ea7634fae1893af","user_metadata":{"favourite_attack":"roundhouse_kick"},"username":"test-user636","app_metadata":{"facts":["count_to_infinity_twice","kill_two_stones_with_one_bird","can_hear_sign_language"]}}' + headers: + Content-Length: + - "661" + Content-Type: + - application/json; charset=utf-8 + status: 201 Created + code: 201 + duration: 1ms + - request: + body: | + null + form: { } + headers: + Content-Type: + - application/json + User-Agent: + - Go-Auth0-SDK/latest + url: https://go-auth0-dev.eu.auth0.com/api/v2/connections?include_totals=true&name=Username-Password-Authentication&per_page=50 + method: GET + response: + body: '{"total":1,"start":0,"limit":50,"connections":[{"id":"con_ftSSLHb0O7rcUGmF","options":{"mfa":{"active":true,"return_enroll_settings":true},"validation":{"username":{"max":15,"min":1}},"import_mode":false,"configuration":{},"passwordPolicy":"good","password_history":{"size":5,"enable":false},"strategy_version":2,"requires_username":true,"password_dictionary":{"enable":false,"dictionary":[]},"brute_force_protection":true,"password_no_personal_info":{"enable":false},"password_complexity_options":{"min_length":8},"enabledDatabaseCustomization":false},"strategy":"auth0","name":"Username-Password-Authentication","is_domain_connection":false,"realms":["Username-Password-Authentication"],"enabled_clients":[]}]}' + headers: + Access-Control-Expose-Headers: + - WWW-Authenticate,Server-Authorization + Content-Type: + - application/json; charset=utf-8 + status: 200 OK + code: 200 + duration: 1ms + - request: + body: "--86d8458dcd43f0d49b30ae345c6469ce927f3429a5c0c6b0a201643d51d8\r\nContent-Disposition: form-data; name=\"connection_id\"\r\n\r\ncon_ftSSLHb0O7rcUGmF\r\n--86d8458dcd43f0d49b30ae345c6469ce927f3429a5c0c6b0a201643d51d8\r\nContent-Disposition: form-data; name=\"upsert\"\r\n\r\nfalse\r\n--86d8458dcd43f0d49b30ae345c6469ce927f3429a5c0c6b0a201643d51d8\r\nContent-Disposition: form-data; name=\"external_id\"\r\n\r\n\r\n--86d8458dcd43f0d49b30ae345c6469ce927f3429a5c0c6b0a201643d51d8\r\nContent-Disposition: form-data; name=\"send_completion_email\"\r\n\r\nfalse\r\n--86d8458dcd43f0d49b30ae345c6469ce927f3429a5c0c6b0a201643d51d8\r\nContent-Disposition: form-data; name=\"users\"; filename=\"users.json\"\r\nContent-Type: application/json\r\n\r\n[{\"email\":\"chuck878@example.com\",\"email_verified\":true}]\r\n--86d8458dcd43f0d49b30ae345c6469ce927f3429a5c0c6b0a201643d51d8--\r\n" + form: { } + headers: + Content-Type: + - multipart/form-data; boundary=86d8458dcd43f0d49b30ae345c6469ce927f3429a5c0c6b0a201643d51d8 + User-Agent: + - Go-Auth0-SDK/latest + url: https://go-auth0-dev.eu.auth0.com/api/v2/jobs/users-imports + method: POST + response: + body: '{"type":"users_import","status":"pending","connection_id":"con_ftSSLHb0O7rcUGmF","connection":"Username-Password-Authentication","created_at":"2022-12-19T12:51:39.606Z","id":"job_Atb40xBugG7DP1bz"}' + headers: + Content-Length: + - "197" + Content-Type: + - application/json; charset=utf-8 + status: 202 Accepted + code: 202 + duration: 1ms + - request: + body: | + null + form: { } + headers: + Content-Type: + - application/json + User-Agent: + - Go-Auth0-SDK/latest + url: https://go-auth0-dev.eu.auth0.com/api/v2/jobs/job_Atb40xBugG7DP1bz/errors + method: GET + response: + body: '[{"user":{"email":"chuck878@example.com","email_verified":true},"errors":[{"code":"DUPLICATED_USER","message":"The user already exist and upsert parameter is false"}]}]' + headers: + Content-Type: + - application/json + status: 200 OK + code: 200 + duration: 1ms + - request: + body: "" + form: { } + headers: + Content-Type: + - application/json + User-Agent: + - Go-Auth0-SDK/latest + url: https://go-auth0-dev.eu.auth0.com/api/v2/users/auth0%7C63a05e5a6ea7634fae1893af + method: DELETE + response: + body: "" + headers: + Content-Type: + - application/json; charset=utf-8 + status: 204 No Content + code: 204 + duration: 1ms