488 lines
18 KiB
Go
488 lines
18 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/api/handlers/auth"
|
|
userhandler "RenewCMS/api/handlers/user"
|
|
"RenewCMS/api/middleware"
|
|
"RenewCMS/api/pkg/apperr"
|
|
validator "RenewCMS/api/pkg/validator"
|
|
"RenewCMS/internal/domain/user"
|
|
"RenewCMS/internal/infrastructure/useCases"
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/jwtauth/v5"
|
|
)
|
|
|
|
var displayNameResponseKeys = []string{"id", "username", "email", "is_verified", "created_at", "updated_at", "display_name", "role"}
|
|
|
|
func newDisplayNameRouter(userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
|
hasher := &stubHasher{}
|
|
getUser := useCases.NewGetUserUseCase(userRepo)
|
|
update := useCases.NewUpdateUserProfileUseCase(userRepo, discardRecorder)
|
|
handler := userhandler.NewHandler(update)
|
|
meHandler := auth.NewMeHandler(getUser, useCases.NewGetMyAuthorUseCase(plainAuthors()))
|
|
loginHandler := auth.NewLoginHandler(
|
|
token,
|
|
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com", discardRecorder),
|
|
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
|
|
true,
|
|
)
|
|
|
|
r := chi.NewRouter()
|
|
r.Route("/v1/user", func(r chi.Router) {
|
|
r.Use(jwtauth.Verifier(token))
|
|
r.Use(jwtauth.Authenticator(token))
|
|
r.Use(middleware.RequireCurrentToken(getUser))
|
|
r.Use(middleware.RequireVerifiedUser(getUser))
|
|
r.Patch("/me", handler.UpdateMe)
|
|
})
|
|
r.Route("/v1/auth", func(r chi.Router) {
|
|
r.Post("/login", loginHandler.Login)
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(jwtauth.Verifier(token))
|
|
r.Use(jwtauth.Authenticator(token))
|
|
r.Use(middleware.RequireCurrentToken(getUser))
|
|
r.Get("/me", meHandler.Me)
|
|
})
|
|
})
|
|
return r
|
|
}
|
|
|
|
func displayNameCookie(t *testing.T, token *jwtauth.JWTAuth, userId uint32, tokenVersion uint32) *http.Cookie {
|
|
t.Helper()
|
|
_, tokenString, err := token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
|
|
if err != nil {
|
|
t.Fatalf("failed to encode jwt: %v", err)
|
|
}
|
|
return &http.Cookie{Name: "jwt", Value: tokenString}
|
|
}
|
|
|
|
func sendDisplayNameRequest(router *chi.Mux, method, target, body string, cookie *http.Cookie) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(method, target, bytes.NewBufferString(body))
|
|
if cookie != nil {
|
|
req.AddCookie(cookie)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func displayNameBody(t *testing.T, value string) string {
|
|
t.Helper()
|
|
encoded, err := json.Marshal(map[string]string{"display_name": value})
|
|
if err != nil {
|
|
t.Fatalf("failed to encode body: %v", err)
|
|
}
|
|
return string(encoded)
|
|
}
|
|
|
|
func seededDisplayNameRepo(displayName string) *mockUserRepo {
|
|
repo := newMockUserRepo()
|
|
repo.users[7] = user.User{
|
|
ID: 7,
|
|
Username: "marta",
|
|
Email: "marta@renew.example",
|
|
Password: "hashed:correct-horse",
|
|
IsVerified: true,
|
|
DisplayName: displayName,
|
|
}
|
|
return repo
|
|
}
|
|
|
|
func decodeKeys(t *testing.T, body []byte) map[string]json.RawMessage {
|
|
t.Helper()
|
|
var decoded map[string]json.RawMessage
|
|
if err := json.Unmarshal(body, &decoded); err != nil {
|
|
t.Fatalf("failed to decode body %q: %v", string(body), err)
|
|
}
|
|
return decoded
|
|
}
|
|
|
|
func assertSevenKeys(t *testing.T, body []byte) map[string]json.RawMessage {
|
|
t.Helper()
|
|
decoded := decodeKeys(t, body)
|
|
if len(decoded) != len(displayNameResponseKeys) {
|
|
t.Errorf("expected exactly %d keys, got %d: %s", len(displayNameResponseKeys), len(decoded), string(body))
|
|
}
|
|
for _, key := range displayNameResponseKeys {
|
|
if _, present := decoded[key]; !present {
|
|
t.Errorf("expected key %q in body %s", key, string(body))
|
|
}
|
|
}
|
|
assertNoForbiddenKeys(t, body)
|
|
return decoded
|
|
}
|
|
|
|
func assertMeKeys(t *testing.T, body []byte) map[string]json.RawMessage {
|
|
t.Helper()
|
|
decoded := decodeKeys(t, body)
|
|
expected := append(append([]string{}, displayNameResponseKeys...), "author")
|
|
if len(decoded) != len(expected) {
|
|
t.Errorf("expected exactly %d keys, got %d: %s", len(expected), len(decoded), string(body))
|
|
}
|
|
for _, key := range expected {
|
|
if _, present := decoded[key]; !present {
|
|
t.Errorf("expected key %q in body %s", key, string(body))
|
|
}
|
|
}
|
|
assertNoForbiddenKeys(t, body)
|
|
return decoded
|
|
}
|
|
|
|
func decodedString(t *testing.T, decoded map[string]json.RawMessage, key string) string {
|
|
t.Helper()
|
|
var value string
|
|
if err := json.Unmarshal(decoded[key], &value); err != nil {
|
|
t.Fatalf("failed to decode %s: %v", key, err)
|
|
}
|
|
return value
|
|
}
|
|
|
|
func TestUpdateUserProfileUseCase(t *testing.T) {
|
|
newRepo := func() *mockUserRepo {
|
|
repo := newMockUserRepo()
|
|
repo.users[7] = user.User{ID: 7, DisplayName: "Old"}
|
|
return repo
|
|
}
|
|
|
|
successCases := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
}{
|
|
{"trims surrounding whitespace", " Marta ", "Marta"},
|
|
{"empty clears", "", ""},
|
|
{"whitespace only clears", " ", ""},
|
|
{"60 emoji runes", strings.Repeat("😀", 60), strings.Repeat("😀", 60)},
|
|
}
|
|
for _, tc := range successCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
repo := newRepo()
|
|
got, err := useCases.NewUpdateUserProfileUseCase(repo, discardRecorder).UpdateDisplayName(testCtx, 7, tc.input)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got.DisplayName != tc.want || repo.users[7].DisplayName != tc.want {
|
|
t.Errorf("expected %q, got %q stored %q", tc.want, got.DisplayName, repo.users[7].DisplayName)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("61 emoji runes are refused without calling the repository", func(t *testing.T) {
|
|
repo := newRepo()
|
|
_, err := useCases.NewUpdateUserProfileUseCase(repo, discardRecorder).UpdateDisplayName(testCtx, 7, strings.Repeat("😀", 61))
|
|
if !errors.Is(err, user.ErrInvalidDisplayName) {
|
|
t.Fatalf("expected ErrInvalidDisplayName, got %v", err)
|
|
}
|
|
if repo.updateDisplayNameCalls != 0 || repo.users[7].DisplayName != "Old" {
|
|
t.Errorf("repository must be untouched")
|
|
}
|
|
})
|
|
|
|
t.Run("unknown user", func(t *testing.T) {
|
|
_, err := useCases.NewUpdateUserProfileUseCase(newRepo(), discardRecorder).UpdateDisplayName(testCtx, 99, "Marta")
|
|
if !errors.Is(err, user.ErrUserNotFound) {
|
|
t.Fatalf("expected ErrUserNotFound, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("repository failure is returned unchanged", func(t *testing.T) {
|
|
repo := newRepo()
|
|
repo.fail.failNext("UpdateDisplayName")
|
|
_, err := useCases.NewUpdateUserProfileUseCase(repo, discardRecorder).UpdateDisplayName(testCtx, 7, "Marta")
|
|
if !errors.Is(err, errRepositoryFailure) {
|
|
t.Fatalf("expected repository failure, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUserDisplayNameHttp(t *testing.T) {
|
|
validator.InitValidator()
|
|
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
|
|
|
patch := func(t *testing.T, repo *mockUserRepo, body string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
router := newDisplayNameRouter(repo, token)
|
|
return sendDisplayNameRequest(router, http.MethodPatch, "/v1/user/me", body, displayNameCookie(t, token, 7, 0))
|
|
}
|
|
|
|
t.Run("A verified user sets a display name", func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("")
|
|
router := newDisplayNameRouter(repo, token)
|
|
cookie := displayNameCookie(t, token, 7, 0)
|
|
|
|
rec := sendDisplayNameRequest(router, http.MethodPatch, "/v1/user/me", `{"display_name": "Marta Alves"}`, cookie)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
decoded := assertSevenKeys(t, rec.Body.Bytes())
|
|
var id uint32
|
|
_ = json.Unmarshal(decoded["id"], &id)
|
|
if id != 7 || decodedString(t, decoded, "username") != "marta" || decodedString(t, decoded, "display_name") != "Marta Alves" {
|
|
t.Errorf("unexpected body: %s", rec.Body.String())
|
|
}
|
|
if repo.users[7].DisplayName != "Marta Alves" || repo.users[7].Username != "marta" {
|
|
t.Errorf("unexpected stored user: %+v", repo.users[7])
|
|
}
|
|
})
|
|
|
|
trimCases := []struct{ name, sent, returned string }{
|
|
{"spaces", " Marta Alves ", "Marta Alves"},
|
|
{"tab and newline", "\tMarta Alves\n", "Marta Alves"},
|
|
{"interior spaces kept", "Marta Alves", "Marta Alves"},
|
|
}
|
|
for _, tc := range trimCases {
|
|
t.Run("The value is trimmed before it is stored and returned: "+tc.name, func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("")
|
|
rec := patch(t, repo, displayNameBody(t, tc.sent))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if got := decodedString(t, decodeKeys(t, rec.Body.Bytes()), "display_name"); got != tc.returned {
|
|
t.Errorf("expected %q, got %q", tc.returned, got)
|
|
}
|
|
if repo.users[7].DisplayName != tc.returned {
|
|
t.Errorf("expected stored %q, got %q", tc.returned, repo.users[7].DisplayName)
|
|
}
|
|
})
|
|
}
|
|
|
|
for _, sent := range []string{"", " ", "\t \n"} {
|
|
t.Run("An empty or whitespace-only value clears the display name: "+displayNameBody(t, sent), func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("Marta Alves")
|
|
rec := patch(t, repo, displayNameBody(t, sent))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
decoded := decodeKeys(t, rec.Body.Bytes())
|
|
if decodedString(t, decoded, "display_name") != "" || decodedString(t, decoded, "username") != "marta" {
|
|
t.Errorf("unexpected body: %s", rec.Body.String())
|
|
}
|
|
if repo.users[7].DisplayName != "" {
|
|
t.Errorf("expected cleared, got %q", repo.users[7].DisplayName)
|
|
}
|
|
})
|
|
}
|
|
|
|
limitCases := []struct {
|
|
name string
|
|
count int
|
|
unit string
|
|
status int
|
|
}{
|
|
{"60 a", 60, "a", http.StatusOK},
|
|
{"61 a", 61, "a", http.StatusBadRequest},
|
|
{"60 emoji", 60, "😀", http.StatusOK},
|
|
{"61 emoji", 61, "😀", http.StatusBadRequest},
|
|
{"60 e-acute", 60, "é", http.StatusOK},
|
|
}
|
|
for _, tc := range limitCases {
|
|
t.Run("The limit is 60 Unicode characters, counted as runes: "+tc.name, func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("")
|
|
value := strings.Repeat(tc.unit, tc.count)
|
|
rec := patch(t, repo, displayNameBody(t, value))
|
|
if rec.Code != tc.status {
|
|
t.Fatalf("expected %d, got %d: %s", tc.status, rec.Code, rec.Body.String())
|
|
}
|
|
if tc.status == http.StatusOK {
|
|
if repo.users[7].DisplayName != value {
|
|
t.Errorf("value not stored")
|
|
}
|
|
return
|
|
}
|
|
if rec.Body.String() != apperr.DisplayNameErrorMessage+"\n" {
|
|
t.Errorf("unexpected body: %q", rec.Body.String())
|
|
}
|
|
if repo.users[7].DisplayName != "" || repo.updateDisplayNameCalls != 0 {
|
|
t.Errorf("repository must be untouched")
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("The limit applies after trimming, not before", func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("")
|
|
core := strings.Repeat("a", 60)
|
|
rec := patch(t, repo, displayNameBody(t, strings.Repeat(" ", 5)+core+strings.Repeat(" ", 5)))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if got := decodedString(t, decodeKeys(t, rec.Body.Bytes()), "display_name"); got != core {
|
|
t.Errorf("unexpected display_name %q", got)
|
|
}
|
|
})
|
|
|
|
t.Run("Control characters inside the name are kept", func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("")
|
|
rec := patch(t, repo, `{"display_name": "Ma\nrta"}`)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if got := decodedString(t, decodeKeys(t, rec.Body.Bytes()), "display_name"); got != "Ma\nrta" {
|
|
t.Errorf("unexpected display_name %q", got)
|
|
}
|
|
if repo.users[7].DisplayName != "Ma\nrta" {
|
|
t.Errorf("unexpected stored %q", repo.users[7].DisplayName)
|
|
}
|
|
})
|
|
|
|
t.Run("A rejected value leaves the existing display name untouched", func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("Marta Alves")
|
|
rec := patch(t, repo, displayNameBody(t, strings.Repeat("a", 61)))
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", rec.Code)
|
|
}
|
|
if repo.updateDisplayNameCalls != 0 || repo.users[7].DisplayName != "Marta Alves" {
|
|
t.Errorf("repository must be untouched")
|
|
}
|
|
})
|
|
|
|
malformedBodies := []string{"not json", `{}`, `{"display_name": null}`, `{"display_name": 42}`, `{"displayName": "Marta"}`}
|
|
for _, body := range malformedBodies {
|
|
t.Run("A malformed body is refused with 400: "+body, func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("Marta Alves")
|
|
rec := patch(t, repo, body)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if repo.updateDisplayNameCalls != 0 || repo.users[7].DisplayName != "Marta Alves" {
|
|
t.Errorf("repository must be untouched")
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("The password, token version and login username are unaffected by a display name", func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("")
|
|
router := newDisplayNameRouter(repo, token)
|
|
cookie := displayNameCookie(t, token, 7, 0)
|
|
|
|
rec := sendDisplayNameRequest(router, http.MethodPatch, "/v1/user/me", `{"display_name": "Marta Alves"}`, cookie)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if repo.users[7].Password != "hashed:correct-horse" || repo.users[7].TokenVersion != 0 {
|
|
t.Errorf("password or token version changed: %+v", repo.users[7])
|
|
}
|
|
me := sendDisplayNameRequest(router, http.MethodGet, "/v1/auth/me", "", cookie)
|
|
if me.Code != http.StatusOK {
|
|
t.Errorf("expected 200 from /me, got %d", me.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("GET /v1/auth/me returns the display name once set", func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("Marta Alves")
|
|
router := newDisplayNameRouter(repo, token)
|
|
rec := sendDisplayNameRequest(router, http.MethodGet, "/v1/auth/me", "", displayNameCookie(t, token, 7, 0))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
decoded := assertMeKeys(t, rec.Body.Bytes())
|
|
if decodedString(t, decoded, "display_name") != "Marta Alves" || decodedString(t, decoded, "username") != "marta" {
|
|
t.Errorf("unexpected body: %s", rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("GET /v1/auth/me returns an empty display_name, not a missing key, when none is set", func(t *testing.T) {
|
|
repo := newMockUserRepo()
|
|
repo.users[8] = user.User{ID: 8, Username: "bob", IsVerified: false}
|
|
router := newDisplayNameRouter(repo, token)
|
|
rec := sendDisplayNameRequest(router, http.MethodGet, "/v1/auth/me", "", displayNameCookie(t, token, 8, 0))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
decoded := assertMeKeys(t, rec.Body.Bytes())
|
|
if decodedString(t, decoded, "display_name") != "" {
|
|
t.Errorf("expected empty display_name")
|
|
}
|
|
})
|
|
|
|
t.Run("Logging in still uses the username only", func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("Ines Duarte")
|
|
router := newDisplayNameRouter(repo, token)
|
|
|
|
rec := sendDisplayNameRequest(router, http.MethodPost, "/v1/auth/login", `{"username":"Ines Duarte","password":"correct-horse"}`, nil)
|
|
if rec.Code != http.StatusForbidden || rec.Body.String() != apperr.LogsErrorMessage+"\n" {
|
|
t.Errorf("expected 403 logs error, got %d: %q", rec.Code, rec.Body.String())
|
|
}
|
|
rec = sendDisplayNameRequest(router, http.MethodPost, "/v1/auth/login", `{"username":"marta","password":"correct-horse"}`, nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("An unverified user is refused", func(t *testing.T) {
|
|
repo := newMockUserRepo()
|
|
repo.users[8] = user.User{ID: 8, Username: "bob", IsVerified: false}
|
|
router := newDisplayNameRouter(repo, token)
|
|
rec := sendDisplayNameRequest(router, http.MethodPatch, "/v1/user/me", `{"display_name": "Bob"}`, displayNameCookie(t, token, 8, 0))
|
|
if rec.Code != http.StatusForbidden || rec.Body.String() != apperr.VerificationRequiredErrorMessage+"\n" {
|
|
t.Errorf("expected 403 verification required, got %d: %q", rec.Code, rec.Body.String())
|
|
}
|
|
if repo.updateDisplayNameCalls != 0 || repo.users[8].DisplayName != "" {
|
|
t.Errorf("repository must be untouched")
|
|
}
|
|
})
|
|
|
|
t.Run("No jwt cookie is refused", func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("")
|
|
router := newDisplayNameRouter(repo, token)
|
|
rec := sendDisplayNameRequest(router, http.MethodPatch, "/v1/user/me", `{"display_name": "Marta"}`, nil)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", rec.Code)
|
|
}
|
|
if repo.updateDisplayNameCalls != 0 {
|
|
t.Errorf("repository must be untouched")
|
|
}
|
|
})
|
|
|
|
t.Run("A stale jwt is refused", func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("")
|
|
u := repo.users[7]
|
|
u.TokenVersion = 1
|
|
repo.users[7] = u
|
|
router := newDisplayNameRouter(repo, token)
|
|
rec := sendDisplayNameRequest(router, http.MethodPatch, "/v1/user/me", `{"display_name": "Marta"}`, displayNameCookie(t, token, 7, 0))
|
|
if rec.Code != http.StatusUnauthorized || rec.Body.String() != apperr.UnauthorizedErrorMessage+"\n" {
|
|
t.Errorf("expected 401 unauthorized, got %d: %q", rec.Code, rec.Body.String())
|
|
}
|
|
if repo.updateDisplayNameCalls != 0 {
|
|
t.Errorf("repository must be untouched")
|
|
}
|
|
})
|
|
|
|
t.Run("A jwt minted for a since-deleted account is refused", func(t *testing.T) {
|
|
repo := newMockUserRepo()
|
|
router := newDisplayNameRouter(repo, token)
|
|
rec := sendDisplayNameRequest(router, http.MethodPatch, "/v1/user/me", `{"display_name": "Marta"}`, displayNameCookie(t, token, 999, 0))
|
|
if rec.Code != http.StatusUnauthorized || rec.Body.String() != apperr.UnauthorizedErrorMessage+"\n" {
|
|
t.Errorf("expected 401 unauthorized, got %d: %q", rec.Code, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("A repository failure while saving answers 500", func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("")
|
|
repo.fail.failNext("UpdateDisplayName")
|
|
rec := patch(t, repo, `{"display_name": "Marta"}`)
|
|
if rec.Code != http.StatusInternalServerError || rec.Body.String() != "Internal server error\n" {
|
|
t.Errorf("expected 500, got %d: %q", rec.Code, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("The user is deleted between authentication and the save", func(t *testing.T) {
|
|
repo := seededDisplayNameRepo("")
|
|
repo.fail.failNextWith("UpdateDisplayName", user.ErrUserNotFound)
|
|
rec := patch(t, repo, `{"display_name": "Marta"}`)
|
|
if rec.Code != http.StatusUnauthorized || rec.Body.String() != apperr.UnauthorizedErrorMessage+"\n" {
|
|
t.Errorf("expected 401 unauthorized, got %d: %q", rec.Code, rec.Body.String())
|
|
}
|
|
})
|
|
}
|