package apis_test import ( "net/http" "strings" "testing" "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/tests" ) func TestUsersAuthMethods(t *testing.T) { scenarios := []tests.ApiScenario{ { Method: http.MethodGet, Url: "/api/users/auth-methods", ExpectedStatus: 200, ExpectedContent: []string{ `"emailPassword":true`, `"authProviders":[{`, `"authProviders":[{`, `"name":"gitlab"`, `"state":`, `"codeVerifier":`, `"codeChallenge":`, `"codeChallengeMethod":`, `"authUrl":`, }, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestUserEmailAuth(t *testing.T) { scenarios := []tests.ApiScenario{ { Name: "authorized as user", Method: http.MethodPost, Url: "/api/users/auth-via-email", RequestHeaders: map[string]string{ "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, }, { Name: "authorized as admin", Method: http.MethodPost, Url: "/api/users/auth-via-email", RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, }, { Name: "invalid body format", Method: http.MethodPost, Url: "/api/users/auth-via-email", Body: strings.NewReader(`{"email`), ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, }, { Name: "invalid data", Method: http.MethodPost, Url: "/api/users/auth-via-email", Body: strings.NewReader(`{"email":"","password":""}`), ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"email":{`, `"password":{`, }, }, { Name: "disabled email/pass auth with valid data", Method: http.MethodPost, Url: "/api/users/auth-via-email", Body: strings.NewReader(`{"email":"test@example.com","password":"123456"}`), BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { app.Settings().EmailAuth.Enabled = false }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, }, { Name: "valid data", Method: http.MethodPost, Url: "/api/users/auth-via-email", Body: strings.NewReader(`{"email":"test2@example.com","password":"123456"}`), ExpectedStatus: 200, ExpectedContent: []string{ `"token"`, `"user"`, `"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`, `"email":"test2@example.com"`, `"verified":false`, // unverified user should be able to authenticate }, ExpectedEvents: map[string]int{"OnUserAuthRequest": 1}, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestUserRequestPasswordReset(t *testing.T) { scenarios := []tests.ApiScenario{ { Name: "empty data", Method: http.MethodPost, Url: "/api/users/request-password-reset", Body: strings.NewReader(``), ExpectedStatus: 400, ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."}}`}, }, { Name: "invalid data", Method: http.MethodPost, Url: "/api/users/request-password-reset", Body: strings.NewReader(`{"email`), ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, }, { Name: "missing user", Method: http.MethodPost, Url: "/api/users/request-password-reset", Body: strings.NewReader(`{"email":"missing@example.com"}`), ExpectedStatus: 204, }, { Name: "existing user", Method: http.MethodPost, Url: "/api/users/request-password-reset", Body: strings.NewReader(`{"email":"test@example.com"}`), ExpectedStatus: 204, // usually this events are fired but since the submit is // executed in a separate go routine they are fired async // ExpectedEvents: map[string]int{ // "OnModelBeforeUpdate": 1, // "OnModelAfterUpdate": 1, // "OnMailerBeforeUserResetPasswordSend": 1, // "OnMailerAfterUserResetPasswordSend": 1, // }, }, { Name: "existing user (after already sent)", Method: http.MethodPost, Url: "/api/users/request-password-reset", Body: strings.NewReader(`{"email":"test@example.com"}`), ExpectedStatus: 204, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestUserConfirmPasswordReset(t *testing.T) { scenarios := []tests.ApiScenario{ { Name: "empty data", Method: http.MethodPost, Url: "/api/users/confirm-password-reset", Body: strings.NewReader(``), ExpectedStatus: 400, ExpectedContent: []string{`"data":{"password":{"code":"validation_required","message":"Cannot be blank."},"passwordConfirm":{"code":"validation_required","message":"Cannot be blank."},"token":{"code":"validation_required","message":"Cannot be blank."}}`}, }, { Name: "invalid data format", Method: http.MethodPost, Url: "/api/users/confirm-password-reset", Body: strings.NewReader(`{"password`), ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, }, { Name: "expired token", Method: http.MethodPost, Url: "/api/users/confirm-password-reset", Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImlkIjoiNGQwMTk3Y2MtMmI0YS0zZjgzLWEyNmItZDc3YmM4NDIzZDNjIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxNjQxMDMxMjAwfQ.t2lVe0ny9XruQsSFQdXqBi0I85i6vIUAQjFXZY5HPxc","password":"123456789","passwordConfirm":"123456789"}`), ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"token":{`, `"code":"validation_invalid_token"`, }, }, { Name: "valid token and data", Method: http.MethodPost, Url: "/api/users/confirm-password-reset", Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImlkIjoiNGQwMTk3Y2MtMmI0YS0zZjgzLWEyNmItZDc3YmM4NDIzZDNjIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxODYxOTU2MDAwfQ.V1gEbY4caEIF6IhQAJ8KZD4RvOGvTCFuYg1fTRSvhe0","password":"123456789","passwordConfirm":"123456789"}`), ExpectedStatus: 200, ExpectedContent: []string{ `"token":`, `"user":`, `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, `"email":"test@example.com"`, }, ExpectedEvents: map[string]int{"OnUserAuthRequest": 1, "OnModelAfterUpdate": 1, "OnModelBeforeUpdate": 1}, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestUserRequestVerification(t *testing.T) { scenarios := []tests.ApiScenario{ // empty data { Method: http.MethodPost, Url: "/api/users/request-verification", Body: strings.NewReader(``), ExpectedStatus: 400, ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."}}`}, }, // invalid data { Method: http.MethodPost, Url: "/api/users/request-verification", Body: strings.NewReader(`{"email`), ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, }, // missing user { Method: http.MethodPost, Url: "/api/users/request-verification", Body: strings.NewReader(`{"email":"missing@example.com"}`), ExpectedStatus: 204, }, // existing already verified user { Method: http.MethodPost, Url: "/api/users/request-verification", Body: strings.NewReader(`{"email":"test@example.com"}`), ExpectedStatus: 204, }, // existing unverified user { Method: http.MethodPost, Url: "/api/users/request-verification", Body: strings.NewReader(`{"email":"test2@example.com"}`), ExpectedStatus: 204, // usually this events are fired but since the submit is // executed in a separate go routine they are fired async // ExpectedEvents: map[string]int{ // "OnModelBeforeUpdate": 1, // "OnModelAfterUpdate": 1, // "OnMailerBeforeUserVerificationSend": 1, // "OnMailerAfterUserVerificationSend": 1, // }, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestUserConfirmVerification(t *testing.T) { scenarios := []tests.ApiScenario{ // empty data { Method: http.MethodPost, Url: "/api/users/confirm-verification", Body: strings.NewReader(``), ExpectedStatus: 400, ExpectedContent: []string{ `"data":`, `"token":{"code":"validation_required"`, }, }, // invalid data { Method: http.MethodPost, Url: "/api/users/confirm-verification", Body: strings.NewReader(`{"token`), ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, }, // expired token { Method: http.MethodPost, Url: "/api/users/confirm-verification", Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImlkIjoiN2JjODRkMjctNmJhMi1iNDJhLTM4M2YtNDE5N2NjM2QzZDBjIiwiZW1haWwiOiJ0ZXN0MkBleGFtcGxlLmNvbSIsImV4cCI6MTY0MTAzMTIwMH0.YCqyREksfqn7cWu-innNNTbWQCr9DgYr7dduM2wxrtQ"}`), ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"token":{`, `"code":"validation_invalid_token"`, }, }, // valid token { Method: http.MethodPost, Url: "/api/users/confirm-verification", Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImlkIjoiN2JjODRkMjctNmJhMi1iNDJhLTM4M2YtNDE5N2NjM2QzZDBjIiwiZW1haWwiOiJ0ZXN0MkBleGFtcGxlLmNvbSIsImV4cCI6MTg2MTk1NjAwMH0.OsxRKuZrNTnwyVjvCwB4jY8TbT-NPZ-UFCpRhCvuv2U"}`), ExpectedStatus: 200, ExpectedContent: []string{ `"token":`, `"user":`, `"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`, `"email":"test2@example.com"`, `"verified":true`, }, ExpectedEvents: map[string]int{ "OnUserAuthRequest": 1, "OnModelAfterUpdate": 1, "OnModelBeforeUpdate": 1, }, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestUserRequestEmailChange(t *testing.T) { scenarios := []tests.ApiScenario{ // unauthorized { Method: http.MethodPost, Url: "/api/users/request-email-change", Body: strings.NewReader(`{"newEmail":"change@example.com"}`), ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, }, // authorized as admin { Method: http.MethodPost, Url: "/api/users/request-email-change", Body: strings.NewReader(`{"newEmail":"change@example.com"}`), RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, }, // invalid data { Method: http.MethodPost, Url: "/api/users/request-email-change", Body: strings.NewReader(`{"newEmail`), RequestHeaders: map[string]string{ "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, }, // empty data { Method: http.MethodPost, Url: "/api/users/request-email-change", Body: strings.NewReader(``), RequestHeaders: map[string]string{ "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":`, `"newEmail":{"code":"validation_required"`, }, }, // valid data (existing email) { Method: http.MethodPost, Url: "/api/users/request-email-change", Body: strings.NewReader(`{"newEmail":"test2@example.com"}`), RequestHeaders: map[string]string{ "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":`, `"newEmail":{"code":"validation_user_email_exists"`, }, }, // valid data (new email) { Method: http.MethodPost, Url: "/api/users/request-email-change", Body: strings.NewReader(`{"newEmail":"change@example.com"}`), RequestHeaders: map[string]string{ "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", }, ExpectedStatus: 204, ExpectedEvents: map[string]int{ "OnMailerBeforeUserChangeEmailSend": 1, "OnMailerAfterUserChangeEmailSend": 1, }, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestUserConfirmEmailChange(t *testing.T) { scenarios := []tests.ApiScenario{ // empty data { Method: http.MethodPost, Url: "/api/users/confirm-email-change", Body: strings.NewReader(``), ExpectedStatus: 400, ExpectedContent: []string{ `"data":`, `"token":{"code":"validation_required"`, `"password":{"code":"validation_required"`, }, }, // invalid data { Method: http.MethodPost, Url: "/api/users/confirm-email-change", Body: strings.NewReader(`{"token`), ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, }, // expired token and correct password { Method: http.MethodPost, Url: "/api/users/confirm-email-change", Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjdiYzg0ZDI3LTZiYTItYjQyYS0zODNmLTQxOTdjYzNkM2QwYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0MkBleGFtcGxlLmNvbSIsIm5ld0VtYWlsIjoiY2hhbmdlQGV4YW1wbGUuY29tIiwiZXhwIjoxNjQwOTkxNjAwfQ.DOqNtSDcXbWix8OsK13X-tjfWi6jZNlAzIZiwG_YDOs","password":"123456"}`), ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"token":{`, `"code":"validation_invalid_token"`, }, }, // valid token and incorrect password { Method: http.MethodPost, Url: "/api/users/confirm-email-change", Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjdiYzg0ZDI3LTZiYTItYjQyYS0zODNmLTQxOTdjYzNkM2QwYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0MkBleGFtcGxlLmNvbSIsIm5ld0VtYWlsIjoiY2hhbmdlQGV4YW1wbGUuY29tIiwiZXhwIjoxODkzNDUyNDAwfQ.aWMQJ_c49yFbzHO5TNhlkbKRokQ_isc2RbLGuSJx44c","password":"654321"}`), ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"password":{`, `"code":"validation_invalid_password"`, }, }, // valid token and correct password { Method: http.MethodPost, Url: "/api/users/confirm-email-change", Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjdiYzg0ZDI3LTZiYTItYjQyYS0zODNmLTQxOTdjYzNkM2QwYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0MkBleGFtcGxlLmNvbSIsIm5ld0VtYWlsIjoiY2hhbmdlQGV4YW1wbGUuY29tIiwiZXhwIjoxODkzNDUyNDAwfQ.aWMQJ_c49yFbzHO5TNhlkbKRokQ_isc2RbLGuSJx44c","password":"123456"}`), ExpectedStatus: 200, ExpectedContent: []string{ `"token":`, `"user":`, `"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`, `"email":"change@example.com"`, `"verified":true`, }, ExpectedEvents: map[string]int{"OnUserAuthRequest": 1, "OnModelAfterUpdate": 1, "OnModelBeforeUpdate": 1}, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestUserRefresh(t *testing.T) { scenarios := []tests.ApiScenario{ // unauthorized { Method: http.MethodPost, Url: "/api/users/refresh", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, }, // authorized as admin { Method: http.MethodPost, Url: "/api/users/refresh", RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, }, // authorized as user { Method: http.MethodPost, Url: "/api/users/refresh", RequestHeaders: map[string]string{ "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", }, ExpectedStatus: 200, ExpectedContent: []string{ `"token":`, `"user":`, `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, }, ExpectedEvents: map[string]int{"OnUserAuthRequest": 1}, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestUsersList(t *testing.T) { scenarios := []tests.ApiScenario{ // unauthorized { Method: http.MethodGet, Url: "/api/users", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, }, // authorized as user { Method: http.MethodGet, Url: "/api/users", RequestHeaders: map[string]string{ "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, }, // authorized as admin { Method: http.MethodGet, Url: "/api/users", RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 200, ExpectedContent: []string{ `"page":1`, `"perPage":30`, `"totalItems":3`, `"items":[{`, `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, `"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`, `"id":"97cc3d3d-6ba2-383f-b42a-7bc84d27410c"`, }, ExpectedEvents: map[string]int{"OnUsersListRequest": 1}, }, // authorized as admin + paging and sorting { Method: http.MethodGet, Url: "/api/users?page=2&perPage=2&sort=-created", RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 200, ExpectedContent: []string{ `"page":2`, `"perPage":2`, `"totalItems":3`, `"items":[{`, `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, }, ExpectedEvents: map[string]int{"OnUsersListRequest": 1}, }, // authorized as admin + invalid filter { Method: http.MethodGet, Url: "/api/users?filter=invalidfield~'test2'", RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, }, // authorized as admin + valid filter { Method: http.MethodGet, Url: "/api/users?filter=verified=true", RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 200, ExpectedContent: []string{ `"page":1`, `"perPage":30`, `"totalItems":2`, `"items":[{`, `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, `"id":"97cc3d3d-6ba2-383f-b42a-7bc84d27410c"`, }, ExpectedEvents: map[string]int{"OnUsersListRequest": 1}, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestUserView(t *testing.T) { scenarios := []tests.ApiScenario{ { Name: "unauthorized", Method: http.MethodGet, Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, }, { Name: "authorized as admin + nonexisting user id", Method: http.MethodGet, Url: "/api/users/00000000-0000-0000-0000-d77bc8423d3c", RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, }, { Name: "authorized as admin + existing user id", Method: http.MethodGet, Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 200, ExpectedContent: []string{ `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, }, ExpectedEvents: map[string]int{"OnUserViewRequest": 1}, }, { Name: "authorized as user - trying to view another user", Method: http.MethodGet, Url: "/api/users/7bc84d27-6ba2-b42a-383f-4197cc3d3d0c", RequestHeaders: map[string]string{ "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, }, { Name: "authorized as user - owner", Method: http.MethodGet, Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", RequestHeaders: map[string]string{ "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", }, ExpectedStatus: 200, ExpectedContent: []string{ `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, }, ExpectedEvents: map[string]int{"OnUserViewRequest": 1}, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestUserDelete(t *testing.T) { scenarios := []tests.ApiScenario{ { Name: "unauthorized", Method: http.MethodDelete, Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, }, { Name: "authorized as admin + nonexisting user id", Method: http.MethodDelete, Url: "/api/users/00000000-0000-0000-0000-d77bc8423d3c", RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, }, { Name: "authorized as admin + existing user id", Method: http.MethodDelete, Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 204, ExpectedEvents: map[string]int{ "OnUserBeforeDeleteRequest": 1, "OnUserAfterDeleteRequest": 1, "OnModelBeforeDelete": 2, // cascade delete to related Record model "OnModelAfterDelete": 2, // cascade delete to related Record model }, }, { Name: "authorized as user - trying to delete another user", Method: http.MethodDelete, Url: "/api/users/7bc84d27-6ba2-b42a-383f-4197cc3d3d0c", RequestHeaders: map[string]string{ "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", }, ExpectedStatus: 403, ExpectedContent: []string{`"data":{}`}, }, { Name: "authorized as user - owner", Method: http.MethodDelete, Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", RequestHeaders: map[string]string{ "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", }, ExpectedStatus: 204, ExpectedEvents: map[string]int{ "OnUserBeforeDeleteRequest": 1, "OnUserAfterDeleteRequest": 1, "OnModelBeforeDelete": 2, // cascade delete to related Record model "OnModelAfterDelete": 2, // cascade delete to related Record model }, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestUserCreate(t *testing.T) { scenarios := []tests.ApiScenario{ { Name: "empty data", Method: http.MethodPost, Url: "/api/users", Body: strings.NewReader(``), ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"email":{"code":"validation_required"`, `"password":{"code":"validation_required"`, }, ExpectedEvents: map[string]int{ "OnUserBeforeCreateRequest": 1, }, }, { Name: "invalid data", Method: http.MethodPost, Url: "/api/users", Body: strings.NewReader(`{"email":"test@example.com","password":"1234","passwordConfirm":"4321"}`), ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"email":{"code":"validation_user_email_exists"`, `"password":{"code":"validation_length_out_of_range"`, `"passwordConfirm":{"code":"validation_values_mismatch"`, }, ExpectedEvents: map[string]int{ "OnUserBeforeCreateRequest": 1, }, }, { Name: "valid data but with disabled email/pass auth", Method: http.MethodPost, Url: "/api/users", Body: strings.NewReader(`{"email":"newuser@example.com","password":"123456789","passwordConfirm":"123456789"}`), BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { app.Settings().EmailAuth.Enabled = false }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, }, { Name: "valid data", Method: http.MethodPost, Url: "/api/users", Body: strings.NewReader(`{"email":"newuser@example.com","password":"123456789","passwordConfirm":"123456789"}`), ExpectedStatus: 200, ExpectedContent: []string{ `"id":`, `"email":"newuser@example.com"`, }, ExpectedEvents: map[string]int{ "OnUserBeforeCreateRequest": 1, "OnUserAfterCreateRequest": 1, "OnModelBeforeCreate": 2, // +1 for the created profile record "OnModelAfterCreate": 2, // +1 for the created profile record }, }, } for _, scenario := range scenarios { scenario.Test(t) } } func TestUserUpdate(t *testing.T) { scenarios := []tests.ApiScenario{ { Name: "unauthorized", Method: http.MethodPatch, Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", Body: strings.NewReader(`{"email":"new@example.com"}`), ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, }, { Name: "authorized as user (owner)", Method: http.MethodPatch, Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", Body: strings.NewReader(`{"email":"new@example.com"}`), RequestHeaders: map[string]string{ "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", }, ExpectedStatus: 401, ExpectedContent: []string{`"data":{}`}, }, { Name: "authorized as admin - invalid/missing user id", Method: http.MethodPatch, Url: "/api/users/invalid", Body: strings.NewReader(``), RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, }, { Name: "authorized as admin - empty data", Method: http.MethodPatch, Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", Body: strings.NewReader(``), RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 200, ExpectedContent: []string{ `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, `"email":"test@example.com"`, }, ExpectedEvents: map[string]int{ "OnUserBeforeUpdateRequest": 1, "OnUserAfterUpdateRequest": 1, "OnModelBeforeUpdate": 1, "OnModelAfterUpdate": 1, }, }, { Name: "authorized as admin - invalid data", Method: http.MethodPatch, Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", Body: strings.NewReader(`{"email":"test2@example.com"}`), RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 400, ExpectedContent: []string{ `"data":{`, `"email":{"code":"validation_user_email_exists"`, }, ExpectedEvents: map[string]int{ "OnUserBeforeUpdateRequest": 1, }, }, { Name: "authorized as admin - valid data", Method: http.MethodPatch, Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", Body: strings.NewReader(`{"email":"new@example.com"}`), RequestHeaders: map[string]string{ "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", }, ExpectedStatus: 200, ExpectedContent: []string{ `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, `"email":"new@example.com"`, }, ExpectedEvents: map[string]int{ "OnUserBeforeUpdateRequest": 1, "OnUserAfterUpdateRequest": 1, "OnModelBeforeUpdate": 1, "OnModelAfterUpdate": 1, }, }, } for _, scenario := range scenarios { scenario.Test(t) } }