857 lines
32 KiB
Go
857 lines
32 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|