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") } }