Files
fm_be/internal/transport/http/handlers/role_handler_test.go
2026-07-16 22:16:45 +07:00

1103 lines
38 KiB
Go

package handlers
import (
"bytes"
"context"
"errors"
"net/http"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"wucher/internal/domain/auth"
"wucher/internal/service"
"wucher/internal/shared/pkg/uuidv7"
)
type memRoleRepo struct {
roles map[string]*auth.Role
permissions map[string]*auth.Permission
listRoles []auth.Role
listTotal int64
listErr error
listPermissions []auth.Permission
listPermissionsTotal int64
listPermissionsErr error
createErr error
updateErr error
deleteErr error
getByIDErr error
getPermissionErr error
permissionNotFound bool
assignPermissionErr error
removePermissionErr error
}
func newMemRoleRepo() *memRoleRepo {
return &memRoleRepo{
roles: map[string]*auth.Role{},
permissions: map[string]*auth.Permission{},
}
}
func (r *memRoleRepo) CreateRole(_ context.Context, role *auth.Role) error {
if len(role.ID) == 0 {
role.ID = uuidv7.MustBytes()
}
r.roles[string(role.ID)] = role
return r.createErr
}
func (r *memRoleRepo) UpdateRole(_ context.Context, role *auth.Role) error {
r.roles[string(role.ID)] = role
return r.updateErr
}
func (r *memRoleRepo) DeleteRole(_ context.Context, id []byte) error {
delete(r.roles, string(id))
return r.deleteErr
}
func (r *memRoleRepo) GetRoleByID(_ context.Context, id []byte) (*auth.Role, error) {
return r.roles[string(id)], r.getByIDErr
}
func (r *memRoleRepo) GetRoleByName(_ context.Context, name string) (*auth.Role, error) {
for _, role := range r.roles {
if role.Name == name {
return role, nil
}
}
return nil, nil
}
func (r *memRoleRepo) ListRoles(_ context.Context, _ string, _ string, _ int, _ int) ([]auth.Role, int64, error) {
return r.listRoles, r.listTotal, r.listErr
}
func (r *memRoleRepo) ListPermissions(_ context.Context, _ string, _ string, _ int, _ int) ([]auth.Permission, int64, error) {
return r.listPermissions, r.listPermissionsTotal, r.listPermissionsErr
}
func (r *memRoleRepo) ListPermissionsByRoleID(_ context.Context, _ []byte) ([]auth.Permission, error) {
return []auth.Permission{}, nil
}
func (r *memRoleRepo) HasRolePermission(_ context.Context, _ []byte, _ string) (bool, error) {
return true, nil
}
func (r *memRoleRepo) GetPermissionByID(_ context.Context, id []byte) (*auth.Permission, error) {
if r.permissionNotFound {
return nil, r.getPermissionErr
}
if permission, ok := r.permissions[string(id)]; ok {
return permission, r.getPermissionErr
}
return &auth.Permission{}, r.getPermissionErr
}
func (r *memRoleRepo) GetPermissionByKey(_ context.Context, key string) (*auth.Permission, error) {
if r.permissionNotFound {
return nil, r.getPermissionErr
}
for _, permission := range r.permissions {
if permission.Key == key {
return permission, r.getPermissionErr
}
}
return &auth.Permission{}, r.getPermissionErr
}
func (r *memRoleRepo) UpdatePermission(_ context.Context, permission *auth.Permission) error {
if permission != nil {
r.permissions[string(permission.ID)] = permission
}
return r.updateErr
}
func (r *memRoleRepo) AssignPermission(_ context.Context, roleID, permissionID []byte) (*auth.RolePermission, error) {
return &auth.RolePermission{ID: uuidv7.MustBytes(), RoleID: roleID, PermissionID: permissionID}, r.assignPermissionErr
}
func (r *memRoleRepo) RemovePermission(_ context.Context, _ []byte, _ []byte) error {
return r.removePermissionErr
}
func TestRoleHandler_Update_MissingBodyID(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Patch("/api/v1/roles/update/:uuid", h.Update)
payload := []byte(`{"data":{"type":"role","attributes":{"name":"test_role"}}}`)
idStr, _ := uuidv7.BytesToString(roleID)
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/update/"+idStr, 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.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
}
func TestRoleHandler_Update_IDMismatch(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Patch("/api/v1/roles/update/:uuid", h.Update)
pathID, _ := uuidv7.BytesToString(roleID)
otherID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
payload := []byte(`{"data":{"type":"role","id":"` + otherID + `","attributes":{"name":"test_role"}}}`)
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/update/"+pathID, 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.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
}
func TestRoleHandler_Update_Success(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Patch("/api/v1/roles/update/:uuid", h.Update)
idStr, _ := uuidv7.BytesToString(roleID)
payload := []byte(`{"data":{"type":"role","id":"` + idStr + `","attributes":{"name":"test_role"}}}`)
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/update/"+idStr, 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.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if repo.roles[string(roleID)].Name != "test_role" {
t.Fatalf("expected role name updated")
}
}
func TestRoleHandler_Create_Success(t *testing.T) {
repo := newMemRoleRepo()
svc := service.NewRoleService(repo)
h := NewRoleHandler(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/roles/create", h.Create)
payload := []byte(`{"data":{"type":"role","attributes":{"name":"admin","description":"full"}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/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.roles {
if string(created.CreatedBy) != string(actor) {
t.Fatalf("expected created_by from actor")
}
}
}
func TestRoleHandler_Create_InvalidJSON(t *testing.T) {
repo := newMemRoleRepo()
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/create", h.Create)
payload := []byte(`{"data":`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/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.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
}
func TestRoleHandler_Create_ValidationError(t *testing.T) {
repo := newMemRoleRepo()
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/create", h.Create)
payload := []byte(`{"data":{"type":"role","attributes":{"name":""}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/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.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
}
func TestRoleHandler_Create_ServiceError(t *testing.T) {
repo := newMemRoleRepo()
repo.createErr = errors.New("db error")
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/create", h.Create)
payload := []byte(`{"data":{"type":"role","attributes":{"name":"admin"}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/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.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
}
func TestRoleHandler_Delete_Success(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Delete("/api/v1/roles/delete/:uuid", h.Delete)
idStr, _ := uuidv7.BytesToString(roleID)
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/roles/delete/"+idStr, nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
}
func TestRoleHandler_Delete_InvalidID(t *testing.T) {
repo := newMemRoleRepo()
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Delete("/api/v1/roles/delete/:uuid", h.Delete)
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/roles/delete/not-a-uuid", nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
}
func TestRoleHandler_Delete_ServiceError(t *testing.T) {
repo := newMemRoleRepo()
repo.deleteErr = errors.New("db error")
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Delete("/api/v1/roles/delete/:uuid", h.Delete)
idStr, _ := uuidv7.BytesToString(roleID)
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/roles/delete/"+idStr, nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
}
func TestRoleHandler_GetByID_Success(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Get("/api/v1/roles/get/:uuid", h.GetByID)
idStr, _ := uuidv7.BytesToString(roleID)
req, _ := http.NewRequest(http.MethodGet, "/api/v1/roles/get/"+idStr, nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
}
func TestRoleHandler_GetByID_NotFound(t *testing.T) {
repo := newMemRoleRepo()
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Get("/api/v1/roles/get/:uuid", h.GetByID)
idStr, _ := uuidv7.BytesToString(uuidv7.MustBytes())
req, _ := http.NewRequest(http.MethodGet, "/api/v1/roles/get/"+idStr, nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
if resp.StatusCode != http.StatusNotFound {
t.Fatalf("expected 404, got %d", resp.StatusCode)
}
}
func TestRoleHandler_List_Success(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.listRoles = []auth.Role{{ID: roleID, Name: "admin"}}
repo.listTotal = 1
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Get("/api/v1/roles/get-all", h.List)
req, _ := http.NewRequest(http.MethodGet, "/api/v1/roles/get-all?page[number]=1&page[size]=10&filter[name]=ad", nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
}
func TestRoleHandler_ListDatatable_Success(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.listRoles = []auth.Role{{ID: roleID, Name: "admin"}}
repo.listTotal = 1
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Get("/api/v1/roles/get-all/dt", h.ListDatatable)
req, _ := http.NewRequest(http.MethodGet, "/api/v1/roles/get-all/dt?page=1&size=10&search=ad", nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
}
func TestParseIntDefault(t *testing.T) {
if got := parseIntDefault("", 7); got != 7 {
t.Fatalf("expected default value")
}
if got := parseIntDefault("abc", 7); got != 7 {
t.Fatalf("expected default on invalid")
}
if got := parseIntDefault("9", 7); got != 9 {
t.Fatalf("expected parsed value")
}
}
func TestRoleHandler_Update_InvalidID(t *testing.T) {
repo := newMemRoleRepo()
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Patch("/api/v1/roles/update/:uuid", h.Update)
payload := []byte(`{"data":{"type":"role","id":"bad","attributes":{"name":"test_role"}}}`)
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/update/not-a-uuid", 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.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
}
func TestRoleHandler_Update_NoAttributes(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Patch("/api/v1/roles/update/:uuid", h.Update)
idStr, _ := uuidv7.BytesToString(roleID)
payload := []byte(`{"data":{"type":"role","id":"` + idStr + `","attributes":{}}}`)
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/update/"+idStr, 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.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
}
func TestRoleHandler_Update_ServiceError(t *testing.T) {
repo := newMemRoleRepo()
repo.updateErr = errors.New("db error")
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Patch("/api/v1/roles/update/:uuid", h.Update)
idStr, _ := uuidv7.BytesToString(roleID)
payload := []byte(`{"data":{"type":"role","id":"` + idStr + `","attributes":{"name":"test_role"}}}`)
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/update/"+idStr, 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.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
}
func TestRoleHandler_AssignPermission_Success(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions", h.AssignPermission)
idStr, _ := uuidv7.BytesToString(roleID)
permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
payload := []byte(`{"data":{"type":"role_permission","attributes":{"permission_id":"` + permID + `"}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions", 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.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
}
func TestRoleHandler_RemovePermission_Success(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Delete("/api/v1/roles/:uuid/permissions/:permission_uuid", h.RemovePermission)
idStr, _ := uuidv7.BytesToString(roleID)
permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/roles/"+idStr+"/permissions/"+permID, nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
}
func TestRoleHandler_AssignPermissionsBulk_Success(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions/assign-bulk", h.AssignPermissionsBulk)
idStr, _ := uuidv7.BytesToString(roleID)
permID1, _ := uuidv7.BytesToString(uuidv7.MustBytes())
permID2, _ := uuidv7.BytesToString(uuidv7.MustBytes())
payload := []byte(`{"data":{"type":"role_assign_permission_bulk","attributes":{"permission_ids":["` + permID1 + `","` + permID2 + `"]}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/assign-bulk", 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.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
}
func TestRoleHandler_RemovePermissionsBulk_Success(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions/remove-bulk", h.RemovePermissionsBulk)
idStr, _ := uuidv7.BytesToString(roleID)
permID1, _ := uuidv7.BytesToString(uuidv7.MustBytes())
permID2, _ := uuidv7.BytesToString(uuidv7.MustBytes())
payload := []byte(`{"data":{"type":"role_remove_permission_bulk","attributes":{"permission_ids":["` + permID1 + `","` + permID2 + `"]}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/remove-bulk", 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.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
}
func TestRoleHandler_AssignPermission_InvalidAndErrorPaths(t *testing.T) {
t.Run("invalid role id", func(t *testing.T) {
repo := newMemRoleRepo()
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions", h.AssignPermission)
payload := []byte(`{"data":{"type":"role_permission","attributes":{"permission_id":"bad"}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/not-uuid/permissions", 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("permission not found", func(t *testing.T) {
repo := newMemRoleRepo()
repo.permissionNotFound = true
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions", h.AssignPermission)
idStr, _ := uuidv7.BytesToString(roleID)
permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
payload := []byte(`{"data":{"type":"role_permission","attributes":{"permission_id":"` + permID + `"}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions", 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)
}
})
t.Run("assign service error", func(t *testing.T) {
repo := newMemRoleRepo()
repo.assignPermissionErr = errors.New("db error")
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions", h.AssignPermission)
idStr, _ := uuidv7.BytesToString(roleID)
permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
payload := []byte(`{"data":{"type":"role_permission","attributes":{"permission_id":"` + permID + `"}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions", 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 TestRoleHandler_RemovePermission_ErrorPaths(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
repo.removePermissionErr = errors.New("db error")
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Delete("/api/v1/roles/:uuid/permissions/:permission_uuid", h.RemovePermission)
idStr, _ := uuidv7.BytesToString(roleID)
permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/roles/"+idStr+"/permissions/"+permID, nil)
resp, _ := app.Test(req)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
}
func TestRoleHandler_RemovePermission_InvalidPermissionID(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Delete("/api/v1/roles/:uuid/permissions/:permission_uuid", h.RemovePermission)
idStr, _ := uuidv7.BytesToString(roleID)
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/roles/"+idStr+"/permissions/not-uuid", nil)
resp, _ := app.Test(req)
if resp.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp.StatusCode)
}
}
func TestRoleHandler_AssignPermissionsBulk_ErrorPaths(t *testing.T) {
t.Run("empty list", func(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions/assign-bulk", h.AssignPermissionsBulk)
idStr, _ := uuidv7.BytesToString(roleID)
payload := []byte(`{"data":{"type":"role_assign_permission_bulk","attributes":{"permission_ids":[]}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/assign-bulk", 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 permission id", func(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions/assign-bulk", h.AssignPermissionsBulk)
idStr, _ := uuidv7.BytesToString(roleID)
payload := []byte(`{"data":{"type":"role_assign_permission_bulk","attributes":{"permission_ids":["bad"]}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/assign-bulk", 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("assign error", func(t *testing.T) {
repo := newMemRoleRepo()
repo.assignPermissionErr = errors.New("db error")
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions/assign-bulk", h.AssignPermissionsBulk)
idStr, _ := uuidv7.BytesToString(roleID)
permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
payload := []byte(`{"data":{"type":"role_assign_permission_bulk","attributes":{"permission_ids":["` + permID + `"]}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/assign-bulk", 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 type", func(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions/assign-bulk", h.AssignPermissionsBulk)
idStr, _ := uuidv7.BytesToString(roleID)
permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
payload := []byte(`{"data":{"type":"role_remove_permission_bulk","attributes":{"permission_ids":["` + permID + `"]}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/assign-bulk", 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)
}
})
}
func TestRoleHandler_RemovePermissionsBulk_ErrorPaths(t *testing.T) {
t.Run("remove error", func(t *testing.T) {
repo := newMemRoleRepo()
repo.removePermissionErr = errors.New("db error")
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions/remove-bulk", h.RemovePermissionsBulk)
idStr, _ := uuidv7.BytesToString(roleID)
permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
payload := []byte(`{"data":{"type":"role_remove_permission_bulk","attributes":{"permission_ids":["` + permID + `"]}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/remove-bulk", 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 type", func(t *testing.T) {
repo := newMemRoleRepo()
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions/remove-bulk", h.RemovePermissionsBulk)
idStr, _ := uuidv7.BytesToString(roleID)
permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
payload := []byte(`{"data":{"type":"role_assign_permission_bulk","attributes":{"permission_ids":["` + permID + `"]}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/remove-bulk", 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)
}
})
}
func TestRoleHandler_AssignPermissionsBulk_InvalidIDAndRoleNotFound(t *testing.T) {
repo := newMemRoleRepo()
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions/assign-bulk", h.AssignPermissionsBulk)
req1, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/not-uuid/permissions/assign-bulk", bytes.NewReader([]byte(`{}`)))
req1.Header.Set("Content-Type", "application/json")
resp1, _ := app.Test(req1)
if resp1.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp1.StatusCode)
}
roleID := uuidv7.MustBytes()
idStr, _ := uuidv7.BytesToString(roleID)
permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
payload := []byte(`{"data":{"type":"role_assign_permission_bulk","attributes":{"permission_ids":["` + permID + `"]}}}`)
req2, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/assign-bulk", bytes.NewReader(payload))
req2.Header.Set("Content-Type", "application/json")
resp2, _ := app.Test(req2)
if resp2.StatusCode != http.StatusNotFound {
t.Fatalf("expected 404, got %d", resp2.StatusCode)
}
}
func TestRoleHandler_AssignPermissionsBulk_PermissionNotFound(t *testing.T) {
repo := newMemRoleRepo()
repo.permissionNotFound = true
roleID := uuidv7.MustBytes()
repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions/assign-bulk", h.AssignPermissionsBulk)
idStr, _ := uuidv7.BytesToString(roleID)
permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
payload := []byte(`{"data":{"type":"role_assign_permission_bulk","attributes":{"permission_ids":["` + permID + `"]}}}`)
req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/assign-bulk", 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 TestRoleHandler_RemovePermissionsBulk_ValidationAndNotFoundPaths(t *testing.T) {
repo := newMemRoleRepo()
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Post("/api/v1/roles/:uuid/permissions/remove-bulk", h.RemovePermissionsBulk)
req1, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/not-uuid/permissions/remove-bulk", bytes.NewReader([]byte(`{}`)))
req1.Header.Set("Content-Type", "application/json")
resp1, _ := app.Test(req1)
if resp1.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp1.StatusCode)
}
roleID := uuidv7.MustBytes()
idStr, _ := uuidv7.BytesToString(roleID)
payloadEmpty := []byte(`{"data":{"type":"role_remove_permission_bulk","attributes":{"permission_ids":[]}}}`)
req2, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/remove-bulk", bytes.NewReader(payloadEmpty))
req2.Header.Set("Content-Type", "application/json")
resp2, _ := app.Test(req2)
if resp2.StatusCode != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", resp2.StatusCode)
}
permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
payload := []byte(`{"data":{"type":"role_remove_permission_bulk","attributes":{"permission_ids":["` + permID + `"]}}}`)
req3, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/remove-bulk", bytes.NewReader(payload))
req3.Header.Set("Content-Type", "application/json")
resp3, _ := app.Test(req3)
if resp3.StatusCode != http.StatusNotFound {
t.Fatalf("expected 404, got %d", resp3.StatusCode)
}
}
func TestRoleHandler_ListPermissions_Success(t *testing.T) {
repo := newMemRoleRepo()
repo.listPermissions = []auth.Permission{
{
ID: uuidv7.MustBytes(),
Name: "Read User",
Key: "user.read",
Description: "Read users",
},
}
repo.listPermissionsTotal = 1
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Get("/api/v1/roles/permissions/get-all", h.ListPermissions)
req, _ := http.NewRequest(http.MethodGet, "/api/v1/roles/permissions/get-all?page[number]=1&page[size]=10&filter[name]=read&sort=name", nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
}
func TestRoleHandler_ListPermissions_Error(t *testing.T) {
repo := newMemRoleRepo()
repo.listPermissionsErr = errors.New("db error")
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Get("/api/v1/roles/permissions/get-all", h.ListPermissions)
req, _ := http.NewRequest(http.MethodGet, "/api/v1/roles/permissions/get-all", nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
}
func TestRoleHandler_UpdatePermission_Success(t *testing.T) {
repo := newMemRoleRepo()
permissionID := uuidv7.MustBytes()
repo.permissions[string(permissionID)] = &auth.Permission{
ID: permissionID,
Name: "Delete User",
Key: "user.delete",
Description: "Delete users",
RequiresPIN: false,
}
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Patch("/api/v1/roles/permissions/:permission_uuid", h.UpdatePermission)
permissionIDStr, _ := uuidv7.BytesToString(permissionID)
payload := []byte(`{"data":{"type":"permission_update","id":"` + permissionIDStr + `","attributes":{"requires_pin":true}}}`)
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/"+permissionIDStr, 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.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
if !repo.permissions[string(permissionID)].RequiresPIN {
t.Fatalf("expected permission requires_pin updated")
}
}
func TestRoleHandler_UpdatePermission_NotFound(t *testing.T) {
repo := newMemRoleRepo()
repo.permissionNotFound = true
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Patch("/api/v1/roles/permissions/:permission_uuid", h.UpdatePermission)
permissionIDStr, _ := uuidv7.BytesToString(uuidv7.MustBytes())
payload := []byte(`{"data":{"type":"permission_update","id":"` + permissionIDStr + `","attributes":{"requires_pin":true}}}`)
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/"+permissionIDStr, 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.StatusNotFound {
t.Fatalf("expected 404, got %d", resp.StatusCode)
}
}
func TestRoleHandler_UpdatePermission_ErrorBranches(t *testing.T) {
repo := newMemRoleRepo()
svc := service.NewRoleService(repo)
h := NewRoleHandler(svc)
app := fiber.New()
app.Patch("/api/v1/roles/permissions/:permission_uuid", h.UpdatePermission)
t.Run("invalid path id", func(t *testing.T) {
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/not-a-uuid", bytes.NewReader([]byte(`{"data":{"type":"permission_update","id":"x","attributes":{"requires_pin":true}}}`)))
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(uuidv7.MustBytes())
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/"+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("missing data id", func(t *testing.T) {
idStr, _ := uuidv7.BytesToString(uuidv7.MustBytes())
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/"+idStr, bytes.NewReader([]byte(`{"data":{"type":"permission_update","attributes":{"requires_pin":true}}}`)))
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 mismatch", func(t *testing.T) {
pathID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
otherID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/"+pathID, bytes.NewReader([]byte(`{"data":{"type":"permission_update","id":"`+otherID+`","attributes":{"requires_pin":true}}}`)))
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("missing requires_pin", func(t *testing.T) {
pathID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/"+pathID, bytes.NewReader([]byte(`{"data":{"type":"permission_update","id":"`+pathID+`","attributes":{}}}`)))
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 service error", func(t *testing.T) {
id := uuidv7.MustBytes()
idStr, _ := uuidv7.BytesToString(id)
repo.permissions[string(id)] = &auth.Permission{ID: id, Key: "x", Name: "x"}
repo.updateErr = errors.New("update failed")
defer func() { repo.updateErr = nil }()
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/"+idStr, bytes.NewReader([]byte(`{"data":{"type":"permission_update","id":"`+idStr+`","attributes":{"requires_pin":true}}}`)))
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 TestParseIntToString(t *testing.T) {
if got := parseIntToString(0); got != "0" {
t.Fatalf("expected 0, got %s", got)
}
if got := parseIntToString(123); got != "123" {
t.Fatalf("expected 123, got %s", got)
}
}
func TestRoleResource(t *testing.T) {
now := time.Now().UTC()
role := &auth.Role{
ID: uuidv7.MustBytes(),
Name: "admin",
Description: "full access",
CreatedBy: uuidv7.MustBytes(),
CreatedAt: now,
UpdatedAt: now,
}
res := roleResource(role)
if res.Type != "role" || res.ID == "" {
t.Fatalf("expected role resource type/id")
}
if res.Attributes.CreatedBy == "" {
t.Fatalf("expected created_by in role resource")
}
}