init push
This commit is contained in:
856
internal/transport/http/handlers/user_handler_test.go
Normal file
856
internal/transport/http/handlers/user_handler_test.go
Normal file
@@ -0,0 +1,856 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
filemanager "wucher/internal/domain/file_manager"
|
||||
"wucher/internal/domain/user"
|
||||
filemanagerrealtime "wucher/internal/realtime/filemanager"
|
||||
"wucher/internal/service"
|
||||
"wucher/internal/shared/pkg/userctx"
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type memUserRepo struct {
|
||||
users map[string]*user.User
|
||||
|
||||
createErr error
|
||||
updateErr error
|
||||
deleteErr error
|
||||
getErr error
|
||||
listErr error
|
||||
|
||||
listUsers []user.User
|
||||
listTotal int64
|
||||
}
|
||||
|
||||
func newMemUserRepo() *memUserRepo {
|
||||
return &memUserRepo{users: map[string]*user.User{}}
|
||||
}
|
||||
|
||||
func (r *memUserRepo) Create(_ context.Context, u *user.User) error {
|
||||
if r.createErr != nil {
|
||||
return r.createErr
|
||||
}
|
||||
if len(u.ID) == 0 {
|
||||
u.ID = uuidv7.MustBytes()
|
||||
}
|
||||
r.users[string(u.ID)] = u
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *memUserRepo) Update(_ context.Context, u *user.User) error {
|
||||
if r.updateErr != nil {
|
||||
return r.updateErr
|
||||
}
|
||||
r.users[string(u.ID)] = u
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *memUserRepo) Delete(_ context.Context, id []byte) error {
|
||||
if r.deleteErr != nil {
|
||||
return r.deleteErr
|
||||
}
|
||||
delete(r.users, string(id))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *memUserRepo) GetByID(_ context.Context, id []byte) (*user.User, error) {
|
||||
if r.getErr != nil {
|
||||
return nil, r.getErr
|
||||
}
|
||||
return r.users[string(id)], nil
|
||||
}
|
||||
|
||||
func (r *memUserRepo) GetByEmail(_ context.Context, _ string) (*user.User, error) { return nil, nil }
|
||||
func (r *memUserRepo) GetByUsername(_ context.Context, _ string) (*user.User, error) { return nil, nil }
|
||||
|
||||
func (r *memUserRepo) List(_ context.Context, _ string, _ string, _ int, _ int) ([]user.User, int64, error) {
|
||||
if r.listErr != nil {
|
||||
return nil, 0, r.listErr
|
||||
}
|
||||
return r.listUsers, r.listTotal, nil
|
||||
}
|
||||
|
||||
func (r *memUserRepo) GetLastNameByID(_ context.Context, _ []byte) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func TestNewUserHandler(t *testing.T) {
|
||||
h := NewUserHandler(service.NewUserService(newMemUserRepo()))
|
||||
if h == nil || h.validate == nil {
|
||||
t.Fatalf("expected handler and validator initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserHandler_Create(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
repo := newMemUserRepo()
|
||||
svc := service.NewUserService(repo)
|
||||
h := NewUserHandler(svc)
|
||||
app := fiber.New()
|
||||
actor := uuidv7.MustBytes()
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
c.Locals("user_id", actor)
|
||||
return c.Next()
|
||||
})
|
||||
app.Post("/api/v1/users/create", h.Create)
|
||||
|
||||
roleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
payload := []byte(`{"data":{"type":"user","attributes":{"email":"u@e.com","username":"john.doe","first_name":"John","last_name":"Doe","role_id":"` + roleID + `"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if !bytes.Contains(body, []byte(`"username":"john.doe"`)) {
|
||||
t.Fatalf("expected response to include username, got %s", string(body))
|
||||
}
|
||||
if len(repo.users) != 1 {
|
||||
t.Fatalf("expected 1 created user, got %d", len(repo.users))
|
||||
}
|
||||
for _, created := range repo.users {
|
||||
if created.Username != "john.doe" {
|
||||
t.Fatalf("expected username persisted, got %q", created.Username)
|
||||
}
|
||||
if created.Timezone != "UTC" {
|
||||
t.Fatalf("expected default timezone UTC, got %q", created.Timezone)
|
||||
}
|
||||
if string(created.CreatedBy) != string(actor) {
|
||||
t.Fatalf("expected created_by from actor")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success with role_ids", func(t *testing.T) {
|
||||
repo := newMemUserRepo()
|
||||
svc := service.NewUserService(repo)
|
||||
h := NewUserHandler(svc)
|
||||
app := fiber.New()
|
||||
app.Post("/api/v1/users/create", h.Create)
|
||||
|
||||
roleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
extraRoleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
payload := []byte(`{"data":{"type":"user","attributes":{"email":"roles@e.com","username":"roles.user","first_name":"Roles","last_name":"User","role_id":"` + roleID + `","role_ids":["` + roleID + `","` + extraRoleID + `"]}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
||||
}
|
||||
for _, created := range repo.users {
|
||||
if len(created.RoleIDs) != 2 {
|
||||
t.Fatalf("expected 2 role ids persisted, got %d", len(created.RoleIDs))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid json", func(t *testing.T) {
|
||||
repo := newMemUserRepo()
|
||||
svc := service.NewUserService(repo)
|
||||
h := NewUserHandler(svc)
|
||||
app := fiber.New()
|
||||
app.Post("/api/v1/users/create", h.Create)
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader([]byte(`{"data":`)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid role id", func(t *testing.T) {
|
||||
repo := newMemUserRepo()
|
||||
svc := service.NewUserService(repo)
|
||||
h := NewUserHandler(svc)
|
||||
app := fiber.New()
|
||||
app.Post("/api/v1/users/create", h.Create)
|
||||
payload := []byte(`{"data":{"type":"user","attributes":{"email":"u@e.com","username":"john.doe","first_name":"John","last_name":"Doe","role_id":"bad"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid image_uuid requires file manager and returns bad request", func(t *testing.T) {
|
||||
repo := newMemUserRepo()
|
||||
svc := service.NewUserService(repo)
|
||||
h := NewUserHandler(svc)
|
||||
app := fiber.New()
|
||||
app.Post("/api/v1/users/create", h.Create)
|
||||
roleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
payload := []byte(`{"data":{"type":"user","attributes":{"email":"u@e.com","username":"john.doe","first_name":"John","last_name":"Doe","role_id":"` + roleID + `","image_uuid":"bad"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image_uuid requires file manager service", func(t *testing.T) {
|
||||
repo := newMemUserRepo()
|
||||
svc := service.NewUserService(repo)
|
||||
h := NewUserHandler(svc)
|
||||
app := fiber.New()
|
||||
app.Post("/api/v1/users/create", h.Create)
|
||||
roleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
fileUUID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
payload := []byte(`{"data":{"type":"user","attributes":{"email":"u@e.com","username":"john.doe","first_name":"John","last_name":"Doe","role_id":"` + roleID + `","image_uuid":"` + fileUUID + `"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid role_ids", func(t *testing.T) {
|
||||
repo := newMemUserRepo()
|
||||
svc := service.NewUserService(repo)
|
||||
h := NewUserHandler(svc)
|
||||
app := fiber.New()
|
||||
app.Post("/api/v1/users/create", h.Create)
|
||||
roleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
payload := []byte(`{"data":{"type":"user","attributes":{"email":"u@e.com","username":"john.doe","first_name":"John","last_name":"Doe","role_id":"` + roleID + `","role_ids":["bad"]}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid timezone", func(t *testing.T) {
|
||||
repo := newMemUserRepo()
|
||||
svc := service.NewUserService(repo)
|
||||
h := NewUserHandler(svc)
|
||||
app := fiber.New()
|
||||
app.Post("/api/v1/users/create", h.Create)
|
||||
roleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
payload := []byte(`{"data":{"type":"user","attributes":{"email":"u@e.com","username":"john.doe","first_name":"John","last_name":"Doe","timezone":"UTC+7","role_id":"` + roleID + `"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create service error", func(t *testing.T) {
|
||||
repo := newMemUserRepo()
|
||||
repo.createErr = errors.New("db error")
|
||||
svc := service.NewUserService(repo)
|
||||
h := NewUserHandler(svc)
|
||||
app := fiber.New()
|
||||
app.Post("/api/v1/users/create", h.Create)
|
||||
roleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
payload := []byte(`{"data":{"type":"user","attributes":{"email":"u@e.com","username":"john.doe","first_name":"John","last_name":"Doe","role_id":"` + roleID + `"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserEventsStream(t *testing.T) {
|
||||
hub := filemanagerrealtime.NewHub(1, nil)
|
||||
actor := uuidv7.MustBytes()
|
||||
userUUID, _ := uuidv7.BytesToString(actor)
|
||||
|
||||
app := fiber.New()
|
||||
fctx := &fasthttp.RequestCtx{}
|
||||
fctx.Request.SetRequestURI("/api/v1/users/events")
|
||||
c := app.AcquireCtx(fctx)
|
||||
c.Locals("user_id", actor)
|
||||
if err := NewFileManagerEventsHandler(hub).Stream(c); err != nil {
|
||||
t.Fatalf("unexpected stream setup error: %v", err)
|
||||
}
|
||||
|
||||
hub.PublishToUsers([]string{userUUID}, filemanagerrealtime.FileStatusEvent{
|
||||
FileUUID: mustUUIDStringBase(t, uuidv7.MustBytes()),
|
||||
Status: "ready",
|
||||
Name: "avatar.png",
|
||||
MimeType: "image/png",
|
||||
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
go func() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
closeFirstHubSubscriberForUser(hub, userUUID)
|
||||
}()
|
||||
|
||||
bodyStr := c.Context().Response.String()
|
||||
app.ReleaseCtx(c)
|
||||
if !strings.Contains(bodyStr, ": connected") {
|
||||
t.Fatalf("expected connected comment in body: %q", bodyStr)
|
||||
}
|
||||
if !strings.Contains(bodyStr, "event: file.updated") {
|
||||
t.Fatalf("expected file.updated event in body: %q", bodyStr)
|
||||
}
|
||||
if !strings.Contains(bodyStr, "\"file_id\"") || !strings.Contains(bodyStr, "\"status\":\"ready\"") {
|
||||
t.Fatalf("expected file.updated payload in body: %q", bodyStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserHandler_Update(t *testing.T) {
|
||||
repo := newMemUserRepo()
|
||||
userID := uuidv7.MustBytes()
|
||||
roleID := uuidv7.MustBytes()
|
||||
repo.users[string(userID)] = &user.User{ID: userID, Email: "u@e.com", FirstName: "John", LastName: "Doe", RoleID: roleID}
|
||||
|
||||
svc := service.NewUserService(repo)
|
||||
h := NewUserHandler(svc)
|
||||
app := fiber.New()
|
||||
app.Patch("/api/v1/users/update/:uuid", h.Update)
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"first_name":"Jane"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty image_uuid clears existing attachment", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
imageID := uuidv7.MustBytes()
|
||||
repo.users[string(userID)].ProfileAttachmentID = append([]byte(nil), imageID...)
|
||||
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"first_name":"Jane","image_uuid":""}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if repo.users[string(userID)].ProfileAttachmentID != nil {
|
||||
t.Fatalf("expected profile attachment to be cleared")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("null image_uuid clears existing attachment", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
imageID := uuidv7.MustBytes()
|
||||
repo.users[string(userID)].ProfileAttachmentID = append([]byte(nil), imageID...)
|
||||
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"first_name":"Jane","image_uuid":null}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if repo.users[string(userID)].ProfileAttachmentID != nil {
|
||||
t.Fatalf("expected profile attachment to be cleared on null image_uuid")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid id", func(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/not-uuid", bytes.NewReader([]byte(`{}`)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid json", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader([]byte(`{"data":`)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("id mismatch", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
other, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + other + `","attributes":{"first_name":"Jane"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("id required", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
payload := []byte(`{"data":{"type":"user","id":"","attributes":{"first_name":"Jane"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no attrs", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid image_uuid requires file manager and returns bad request", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"image_uuid":"bad"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image_uuid requires file manager service", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
fileUUID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"image_uuid":"` + fileUUID + `"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image_uuid accepts existing attachment id", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
attachmentID := uuidv7.MustBytes()
|
||||
attachmentIDStr, _ := uuidv7.BytesToString(attachmentID)
|
||||
h.WithFileManagerService(&fileManagerAttachmentHandlerServiceMock{
|
||||
getAttachmentFn: func(_ context.Context, id []byte) (*filemanager.Attachment, error) {
|
||||
if string(id) == string(attachmentID) {
|
||||
return &filemanager.Attachment{ID: attachmentID, FileID: uuidv7.MustBytes()}, nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
})
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"image_uuid":"` + attachmentIDStr + `"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if string(repo.users[string(userID)].ProfileAttachmentID) != string(attachmentID) {
|
||||
t.Fatalf("expected profile_attachment_id updated with attachment id")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image_uuid file uuid replaces existing profile attachment", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
oldAttachmentID := uuidv7.MustBytes()
|
||||
newAttachmentID := uuidv7.MustBytes()
|
||||
newFileID := uuidv7.MustBytes()
|
||||
newFileIDStr, _ := uuidv7.BytesToString(newFileID)
|
||||
repo.users[string(userID)].ProfileAttachmentID = append([]byte(nil), oldAttachmentID...)
|
||||
|
||||
h.WithFileManagerService(&fileManagerAttachmentHandlerServiceMock{
|
||||
getAttachmentFn: func(_ context.Context, id []byte) (*filemanager.Attachment, error) {
|
||||
// Incoming image_uuid is file UUID, not attachment UUID.
|
||||
if string(id) == string(newFileID) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
listAttachmentsFn: func(_ context.Context, _ filemanager.ListAttachmentsParams) ([]filemanager.Attachment, int64, error) {
|
||||
return []filemanager.Attachment{{ID: oldAttachmentID, FileID: uuidv7.MustBytes()}}, 1, nil
|
||||
},
|
||||
deleteAttachmentFn: func(_ context.Context, id []byte) error {
|
||||
if string(id) != string(oldAttachmentID) {
|
||||
t.Fatalf("unexpected deleted attachment id")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
createAttachmentFn: func(_ context.Context, p filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
|
||||
if string(p.FileID) != string(newFileID) {
|
||||
t.Fatalf("expected create attachment from new file id")
|
||||
}
|
||||
return &filemanager.Attachment{ID: newAttachmentID, FileID: newFileID}, nil
|
||||
},
|
||||
})
|
||||
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"image_uuid":"` + newFileIDStr + `"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(body))
|
||||
}
|
||||
if string(repo.users[string(userID)].ProfileAttachmentID) != string(newAttachmentID) {
|
||||
t.Fatalf("expected profile_attachment_id replaced with new attachment id")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("email empty", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"email":" "}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("first_name empty", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"first_name":" "}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("last_name empty", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"last_name":" "}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("update error", func(t *testing.T) {
|
||||
repo.updateErr = errors.New("db error")
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"first_name":"X"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
repo.updateErr = nil
|
||||
})
|
||||
|
||||
t.Run("invalid timezone", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"timezone":"UTC+7"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("update timezone success", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"timezone":"Asia/Jakarta"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if got := repo.users[string(userID)].Timezone; got != "Asia/Jakarta" {
|
||||
t.Fatalf("expected timezone updated, got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"first_name":"Jane"}}}`)
|
||||
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserHandler_DeleteGetListDatatable(t *testing.T) {
|
||||
repo := newMemUserRepo()
|
||||
roleID := uuidv7.MustBytes()
|
||||
userID := uuidv7.MustBytes()
|
||||
repo.users[string(userID)] = &user.User{ID: userID, Email: "u@e.com", FirstName: "John", LastName: "Doe", RoleID: roleID}
|
||||
repo.listUsers = []user.User{{ID: userID, Email: "u@e.com", FirstName: "John", LastName: "Doe", RoleID: roleID}}
|
||||
repo.listTotal = 1
|
||||
|
||||
svc := service.NewUserService(repo)
|
||||
h := NewUserHandler(svc)
|
||||
app := fiber.New()
|
||||
app.Delete("/api/v1/users/delete/:uuid", h.Delete)
|
||||
app.Get("/api/v1/users/get/:uuid", h.Get)
|
||||
app.Get("/api/v1/users/get-all", h.List)
|
||||
app.Get("/api/v1/users/get-all/dt", h.ListDatatable)
|
||||
|
||||
t.Run("delete invalid id", func(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/users/delete/not-uuid", nil)
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete success", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(userID)
|
||||
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/users/delete/"+idStr, nil)
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete service error", func(t *testing.T) {
|
||||
failingID := uuidv7.MustBytes()
|
||||
repo.users[string(failingID)] = &user.User{ID: failingID, Email: "fail@e.com", RoleID: roleID}
|
||||
repo.deleteErr = errors.New("delete failed")
|
||||
defer func() { repo.deleteErr = nil }()
|
||||
|
||||
idStr, _ := uuidv7.BytesToString(failingID)
|
||||
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/users/delete/"+idStr, nil)
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get not found", func(t *testing.T) {
|
||||
idStr, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/users/get/"+idStr, nil)
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get success", func(t *testing.T) {
|
||||
id := uuidv7.MustBytes()
|
||||
repo.users[string(id)] = &user.User{ID: id, Email: "a@b.com", FirstName: "A", LastName: "B", RoleID: roleID}
|
||||
idStr, _ := uuidv7.BytesToString(id)
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/users/get/"+idStr, nil)
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list success", func(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/users/get-all?page[number]=1&page[size]=10&filter[search]=u", nil)
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list error", func(t *testing.T) {
|
||||
repo.listErr = errors.New("db error")
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/users/get-all", nil)
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
repo.listErr = nil
|
||||
})
|
||||
|
||||
t.Run("datatable success", func(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/users/get-all/dt?page=1&size=10&search=u", nil)
|
||||
resp, _ := app.Test(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserResource(t *testing.T) {
|
||||
now := time.Now()
|
||||
primaryRoleID := uuidv7.MustBytes()
|
||||
extraRoleID := uuidv7.MustBytes()
|
||||
u := &user.User{
|
||||
ID: uuidv7.MustBytes(),
|
||||
Email: "u@e.com",
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
MobilePhone: "08123",
|
||||
ProfileAttachmentID: uuidv7.MustBytes(),
|
||||
RoleID: primaryRoleID,
|
||||
RoleName: "staff",
|
||||
RoleDescription: "Staff role",
|
||||
RoleIDs: [][]byte{primaryRoleID, extraRoleID},
|
||||
Roles: []user.RoleSummary{
|
||||
{ID: primaryRoleID, Name: "staff", Description: "Staff role"},
|
||||
{ID: extraRoleID, Name: "chief_pilot", Description: "Chief Pilot role"},
|
||||
},
|
||||
CreatedBy: uuidv7.MustBytes(),
|
||||
EmailVerifiedAt: &now,
|
||||
}
|
||||
h := NewUserHandler(service.NewUserService(newMemUserRepo()))
|
||||
r := h.userResource(context.Background(), u)
|
||||
if r.Type != "user" || r.Attributes.Role.Name != "staff" || r.Attributes.EmailVerifiedAt == "" {
|
||||
t.Fatalf("unexpected user resource: %#v", r)
|
||||
}
|
||||
if len(r.Attributes.RoleIDs) == 0 || len(r.Attributes.Roles) == 0 {
|
||||
t.Fatalf("expected role_ids and roles in response: %#v", r.Attributes)
|
||||
}
|
||||
if r.Attributes.Timezone != "UTC" {
|
||||
t.Fatalf("expected default timezone UTC, got %q", r.Attributes.Timezone)
|
||||
}
|
||||
if r.Attributes.CreatedBy == "" {
|
||||
t.Fatalf("expected created_by in response")
|
||||
}
|
||||
if r.Attributes.ProfilePicture == nil || r.Attributes.ProfilePicture.UUID == "" {
|
||||
t.Fatalf("expected profile_picture in response")
|
||||
}
|
||||
}
|
||||
|
||||
type staticUserNameGetter struct {
|
||||
lastName string
|
||||
}
|
||||
|
||||
func (g staticUserNameGetter) GetLastName(context.Context, []byte) (string, error) {
|
||||
return g.lastName, nil
|
||||
}
|
||||
|
||||
func TestUserHandler_HelperBranches(t *testing.T) {
|
||||
t.Run("with setters return same handler", func(t *testing.T) {
|
||||
h := NewUserHandler(service.NewUserService(newMemUserRepo()))
|
||||
if h.WithFileManagerService(nil) != h {
|
||||
t.Fatalf("expected WithFileManagerService to return same handler")
|
||||
}
|
||||
if h.WithFileStorage(nil) != h {
|
||||
t.Fatalf("expected WithFileStorage to return same handler")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resolve profile image with nil file uuid is ignored", func(t *testing.T) {
|
||||
h := NewUserHandler(service.NewUserService(newMemUserRepo()))
|
||||
app := fiber.New()
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
got, handled := h.resolveProfileImageAttachmentID(c, uuidv7.MustBytes(), nil)
|
||||
if handled {
|
||||
t.Fatalf("expected nil file uuid to be ignored")
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("expected nil attachment id")
|
||||
}
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
})
|
||||
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
||||
if _, err := app.Test(req); err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parse optional image_uuid from raw body", func(t *testing.T) {
|
||||
state := parseOptionalStringFieldFromJSONBody([]byte(`{"data":{"attributes":{"image_uuid":null}}}`), "image_uuid")
|
||||
if !state.Set || state.Valid {
|
||||
t.Fatalf("expected image_uuid null to be set+invalid, got %+v", state)
|
||||
}
|
||||
|
||||
state = parseOptionalStringFieldFromJSONBody([]byte(`{"data":{"attributes":{"image_uuid":"abc"}}}`), "image_uuid")
|
||||
if !state.Set || !state.Valid || state.Value != "abc" {
|
||||
t.Fatalf("expected image_uuid string to be set+valid, got %+v", state)
|
||||
}
|
||||
|
||||
state = parseOptionalStringFieldFromJSONBody([]byte(`{"data":{"attributes":{"other":"x"}}}`), "image_uuid")
|
||||
if state.Set {
|
||||
t.Fatalf("expected image_uuid absent to be unset, got %+v", state)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("contains role id matches only valid uuid bytes", func(t *testing.T) {
|
||||
roleID := uuidv7.MustBytes()
|
||||
if !containsRoleID([][]byte{roleID}, roleID) {
|
||||
t.Fatalf("expected containsRoleID to match")
|
||||
}
|
||||
if containsRoleID([][]byte{roleID}, []byte("short")) {
|
||||
t.Fatalf("expected invalid role id bytes to fail")
|
||||
}
|
||||
if containsRoleID([][]byte{roleID}, uuidv7.MustBytes()) {
|
||||
t.Fatalf("expected non-member role id to fail")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wire user name getter forwards to userctx", func(t *testing.T) {
|
||||
prev := userctx.GlobalUserNameGetter
|
||||
defer func() { userctx.GlobalUserNameGetter = prev }()
|
||||
|
||||
getter := staticUserNameGetter{lastName: "Doe"}
|
||||
WireUserNameGetter(getter)
|
||||
if userctx.GlobalUserNameGetter == nil {
|
||||
t.Fatalf("expected global getter configured")
|
||||
}
|
||||
got, err := userctx.GlobalUserNameGetter.GetLastName(context.Background(), uuidv7.MustBytes())
|
||||
if err != nil {
|
||||
t.Fatalf("GetLastName: %v", err)
|
||||
}
|
||||
if got != "Doe" {
|
||||
t.Fatalf("expected wired getter result, got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("profile picture download url resolves when storage exists", func(t *testing.T) {
|
||||
fileID := uuidv7.MustBytes()
|
||||
attachmentID := uuidv7.MustBytes()
|
||||
thumbnailKey := "avatars/avatar.thumb.png"
|
||||
u := &user.User{
|
||||
ID: uuidv7.MustBytes(),
|
||||
Email: "u@e.com",
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
ProfileAttachmentID: attachmentID,
|
||||
ProfileAttachment: &filemanager.Attachment{
|
||||
ID: attachmentID,
|
||||
File: &filemanager.File{
|
||||
ID: fileID,
|
||||
Name: "avatar.png",
|
||||
ObjectKey: "avatars/avatar.png",
|
||||
ThumbnailObjectKey: &thumbnailKey,
|
||||
SizeBytes: 123,
|
||||
},
|
||||
},
|
||||
}
|
||||
h := NewUserHandler(service.NewUserService(newMemUserRepo())).WithFileStorage(&fileManagerDownloadURLStorageMock{presignedURL: "https://signed/avatar.png"})
|
||||
res := h.userResource(context.Background(), u)
|
||||
if res.Attributes.ProfilePicture == nil || res.Attributes.ProfilePicture.DownloadURL != "https://signed/avatar.png" {
|
||||
t.Fatalf("expected signed profile picture download url, got %#v", res.Attributes.ProfilePicture)
|
||||
}
|
||||
if res.Attributes.ProfilePicture.ThumbnailDownloadURL != "https://signed/avatar.png" {
|
||||
t.Fatalf("expected signed profile picture thumbnail url, got %#v", res.Attributes.ProfilePicture)
|
||||
}
|
||||
if res.Attributes.ProfilePicture.OriginalSizeBytes == nil || *res.Attributes.ProfilePicture.OriginalSizeBytes != 123 {
|
||||
t.Fatalf("expected original size bytes 123, got %#v", res.Attributes.ProfilePicture)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user