From 396ed6fe811204f70ce7e2ea3e6b6eec485c50b2 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 9 Sep 2025 13:46:31 -0400 Subject: [PATCH] Add comprehensive unit tests and documentation for roles service - Enhanced doc strings for all functions and structs with detailed descriptions - Added complete test coverage for RoleService CRUD operations (Create, Get, GetAll, Delete, Import) - Created comprehensive test fixtures for success and error scenarios - Added struct validation tests for Role, RoleMethod, and RoleView types - All 15 test cases pass with proper error handling and edge case coverage --- pkg/services/roles.go | 36 +- pkg/services/roles_test.go | 328 ++++++++++++++++++ .../authorization/roles/create.success.json | 22 ++ .../authorization/roles/delete.success.json | 4 + .../authorization/roles/get.success.json | 30 ++ .../authorization/roles/getall.success.json | 45 +++ .../authorization/roles/import.success.json | 22 ++ 7 files changed, 477 insertions(+), 10 deletions(-) create mode 100644 pkg/services/roles_test.go create mode 100644 pkg/services/testdata/2023.2/authorization/roles/create.success.json create mode 100644 pkg/services/testdata/2023.2/authorization/roles/delete.success.json create mode 100644 pkg/services/testdata/2023.2/authorization/roles/get.success.json create mode 100644 pkg/services/testdata/2023.2/authorization/roles/getall.success.json create mode 100644 pkg/services/testdata/2023.2/authorization/roles/import.success.json diff --git a/pkg/services/roles.go b/pkg/services/roles.go index b1886f5b..347b49eb 100644 --- a/pkg/services/roles.go +++ b/pkg/services/roles.go @@ -14,19 +14,22 @@ import ( "github.com/itential/ipctl/pkg/logger" ) -// RoleMethod represents an allowed method for a role +// RoleMethod represents an allowed method for a role in the authorization system. +// It defines what operations a role can perform with its associated provenance. type RoleMethod struct { Name string `json:"name"` Provenance string `json:"provenance"` } -// RoleView represents an allowed view for a role +// RoleView represents an allowed view for a role in the authorization system. +// It defines what UI views or resources a role can access with its associated provenance and path. type RoleView struct { Provenance string `json:"provenance"` Path string `json:"path"` } -// Role represents a role in the authorization system +// Role represents a role in the authorization system. +// Roles define collections of allowed methods and views that can be assigned to users and groups. type Role struct { Id string `json:"_id,omitempty"` Provenance string `json:"provenance"` @@ -36,17 +39,21 @@ type Role struct { AllowedViews []RoleView `json:"allowedViews"` } -// RoleService provides methods for managing roles +// RoleService provides methods for managing roles in the Itential Platform. +// It handles CRUD operations and import functionality for authorization roles. type RoleService struct { client client.Client } -// NewRoleService creates a new RoleService with the given client +// NewRoleService creates a new RoleService instance with the provided HTTP client. +// The client is used to communicate with the Itential Platform authorization API. func NewRoleService(c client.Client) *RoleService { return &RoleService{client: c} } -// Create implements `http.MethodPost /authorization/roles` +// Create creates a new role in the authorization system. +// It sends a POST request to /authorization/roles with the role data. +// Returns the created role or an error if the operation fails. func (svc *RoleService) Create(in Role) (*Role, error) { logger.Trace() @@ -99,7 +106,9 @@ func (svc *RoleService) Create(in Role) (*Role, error) { return response.Data, nil } -// Delete implements `http.MethodDelete /authorization/roles/{id}` +// Delete removes a role from the authorization system by its ID. +// It sends a DELETE request to /authorization/roles/{id}. +// Returns an error if the operation fails or the role is not found. func (svc *RoleService) Delete(id string) error { logger.Trace() @@ -129,7 +138,9 @@ func (svc *RoleService) Delete(id string) error { return nil } -// GetAll implements `http.MethodGet /authorization/roles` +// GetAll retrieves all roles from the authorization system. +// It handles pagination automatically, fetching all pages of results. +// Returns a slice of all roles or an error if the operation fails. func (svc *RoleService) GetAll() ([]Role, error) { logger.Trace() @@ -176,7 +187,9 @@ func (svc *RoleService) GetAll() ([]Role, error) { return roles, nil } -// Get implements `http.MethodGet /authorization/roles/{id}` +// Get retrieves a specific role by its ID from the authorization system. +// It sends a GET request to /authorization/roles/{id}. +// Returns the role or an error if the operation fails or the role is not found. func (svc *RoleService) Get(id string) (*Role, error) { logger.Trace() @@ -208,7 +221,10 @@ func (svc *RoleService) Get(id string) (*Role, error) { } -// Import imports a role into the authorization system +// Import imports a role into the authorization system. +// This method creates a role using the complete role object, including all fields. +// It's typically used when restoring roles from exports or backups. +// Returns the imported role or an error if the operation fails. func (svc *RoleService) Import(in Role) (*Role, error) { logger.Trace() diff --git a/pkg/services/roles_test.go b/pkg/services/roles_test.go new file mode 100644 index 00000000..2db6bf16 --- /dev/null +++ b/pkg/services/roles_test.go @@ -0,0 +1,328 @@ +// Copyright 2024 Itential Inc. All Rights Reserved +// Unauthorized copying of this file, via any medium is strictly prohibited +// Proprietary and confidential + +package services + +import ( + "net/http" + "path/filepath" + "reflect" + "testing" + + "github.com/itential/ipctl/internal/testlib" + "github.com/stretchr/testify/assert" +) + +var ( + rolesGetSuccess = "authorization/roles/get.success.json" + rolesGetAllSuccess = "authorization/roles/getall.success.json" + rolesCreateSuccess = "authorization/roles/create.success.json" + rolesDeleteSuccess = "authorization/roles/delete.success.json" + rolesImportSuccess = "authorization/roles/import.success.json" +) + +func setupRoleService() *RoleService { + return NewRoleService( + testlib.Setup(), + ) +} + +// TestNewRoleService tests the constructor +func TestNewRoleService(t *testing.T) { + client := testlib.Setup() + defer testlib.Teardown() + + svc := NewRoleService(client) + + assert.NotNil(t, svc) + assert.Equal(t, client, svc.client) +} + +// TestRoleServiceGetAll tests retrieving all roles +func TestRoleServiceGetAll(t *testing.T) { + svc := setupRoleService() + defer testlib.Teardown() + + for _, ele := range fixtureSuites { + response := testlib.Fixture( + filepath.Join(fixtureRoot, ele, rolesGetAllSuccess), + ) + + testlib.AddGetResponseToMux("/authorization/roles", response, 0) + + res, err := svc.GetAll() + + assert.Nil(t, err) + assert.Equal(t, 2, len(res)) + assert.Equal(t, "admin_role", res[0].Name) + assert.Equal(t, "user_role", res[1].Name) + assert.Equal(t, "local_aaa", res[0].Provenance) + assert.NotEmpty(t, res[0].Id) + assert.NotEmpty(t, res[0].AllowedMethods) + assert.NotEmpty(t, res[0].AllowedViews) + } +} + +// TestRoleServiceGetAllError tests error handling for GetAll +func TestRoleServiceGetAllError(t *testing.T) { + svc := setupRoleService() + defer testlib.Teardown() + + testlib.AddGetErrorToMux("/authorization/roles", "", 0) + + res, err := svc.GetAll() + + assert.NotNil(t, err) + assert.Nil(t, res) +} + +// TestRoleServiceGet tests retrieving a specific role by ID +func TestRoleServiceGet(t *testing.T) { + svc := setupRoleService() + defer testlib.Teardown() + + for _, ele := range fixtureSuites { + response := testlib.Fixture( + filepath.Join(fixtureRoot, ele, rolesGetSuccess), + ) + testlib.AddGetResponseToMux("/authorization/roles/{id}", response, 0) + + res, err := svc.Get("678ec1ab6d849669c6dbe952") + + assert.Nil(t, err) + assert.NotNil(t, res) + assert.Equal(t, reflect.TypeOf((*Role)(nil)), reflect.TypeOf(res)) + assert.Equal(t, "admin_role", res.Name) + assert.Equal(t, "local_aaa", res.Provenance) + assert.Equal(t, "678ec1ab6d849669c6dbe952", res.Id) + assert.Equal(t, "Administrative role with full permissions", res.Description) + assert.Len(t, res.AllowedMethods, 3) + assert.Len(t, res.AllowedViews, 2) + assert.Equal(t, "createUser", res.AllowedMethods[0].Name) + assert.Equal(t, "/admin/users", res.AllowedViews[0].Path) + } +} + +// TestRoleServiceGetError tests error handling for Get +func TestRoleServiceGetError(t *testing.T) { + svc := setupRoleService() + defer testlib.Teardown() + + testlib.AddGetErrorToMux("/authorization/roles", "", 0) + + res, err := svc.Get("invalid-id") + + assert.NotNil(t, err) + assert.Nil(t, res) +} + +// TestRoleServiceCreate tests creating a new role +func TestRoleServiceCreate(t *testing.T) { + svc := setupRoleService() + defer testlib.Teardown() + + for _, ele := range fixtureSuites { + response := testlib.Fixture( + filepath.Join(fixtureRoot, ele, rolesCreateSuccess), + ) + testlib.AddPostResponseToMux("/authorization/roles", response, http.StatusOK) + + inputRole := Role{ + Name: "test_role", + Provenance: "local_aaa", + Description: "Test role for validation", + AllowedMethods: []RoleMethod{ + {Name: "testMethod", Provenance: "local_aaa"}, + }, + AllowedViews: []RoleView{ + {Provenance: "local_aaa", Path: "/test"}, + }, + } + + res, err := svc.Create(inputRole) + + assert.Nil(t, err) + assert.NotNil(t, res) + assert.Equal(t, "test_role", res.Name) + assert.Equal(t, "local_aaa", res.Provenance) + assert.NotEmpty(t, res.Id) + assert.Len(t, res.AllowedMethods, 1) + assert.Len(t, res.AllowedViews, 1) + } +} + +// TestRoleServiceCreateWithEmptyArrays tests creating a role with empty allowed methods and views +func TestRoleServiceCreateWithEmptyArrays(t *testing.T) { + svc := setupRoleService() + defer testlib.Teardown() + + for _, ele := range fixtureSuites { + response := testlib.Fixture( + filepath.Join(fixtureRoot, ele, rolesCreateSuccess), + ) + testlib.AddPostResponseToMux("/authorization/roles", response, http.StatusOK) + + inputRole := Role{ + Name: "test_role", + Provenance: "local_aaa", + Description: "Test role with empty arrays", + AllowedMethods: []RoleMethod{}, + AllowedViews: []RoleView{}, + } + + res, err := svc.Create(inputRole) + + assert.Nil(t, err) + assert.NotNil(t, res) + assert.Equal(t, "test_role", res.Name) + } +} + +// TestRoleServiceCreateError tests error handling for Create +func TestRoleServiceCreateError(t *testing.T) { + svc := setupRoleService() + defer testlib.Teardown() + + testlib.AddPostErrorToMux("/authorization/roles", "", http.StatusInternalServerError) + + inputRole := Role{ + Name: "test_role", + Provenance: "local_aaa", + } + + res, err := svc.Create(inputRole) + + assert.NotNil(t, err) + assert.Nil(t, res) +} + +// TestRoleServiceDelete tests deleting a role +func TestRoleServiceDelete(t *testing.T) { + svc := setupRoleService() + defer testlib.Teardown() + + for _, ele := range fixtureSuites { + response := testlib.Fixture( + filepath.Join(fixtureRoot, ele, rolesDeleteSuccess), + ) + testlib.AddDeleteResponseToMux("/authorization/roles/{id}", response, http.StatusOK) + + err := svc.Delete("678ec1ab6d849669c6dbe952") + + assert.Nil(t, err) + } +} + +// TestRoleServiceDeleteError tests error handling for Delete +func TestRoleServiceDeleteError(t *testing.T) { + svc := setupRoleService() + defer testlib.Teardown() + + testlib.AddDeleteErrorToMux("/authorization/roles/{id}", "", http.StatusNotFound) + + err := svc.Delete("invalid-id") + + assert.NotNil(t, err) +} + +// TestRoleServiceImport tests importing a role +func TestRoleServiceImport(t *testing.T) { + svc := setupRoleService() + defer testlib.Teardown() + + for _, ele := range fixtureSuites { + response := testlib.Fixture( + filepath.Join(fixtureRoot, ele, rolesImportSuccess), + ) + testlib.AddPostResponseToMux("/authorization/roles", response, http.StatusOK) + + inputRole := Role{ + Id: "678ec1ab6d849669c6dbe955", + Name: "imported_role", + Provenance: "local_aaa", + Description: "Role imported from external source", + AllowedMethods: []RoleMethod{ + {Name: "importMethod", Provenance: "local_aaa"}, + }, + AllowedViews: []RoleView{ + {Provenance: "local_aaa", Path: "/import"}, + }, + } + + res, err := svc.Import(inputRole) + + assert.Nil(t, err) + assert.NotNil(t, res) + assert.Equal(t, "imported_role", res.Name) + assert.Equal(t, "local_aaa", res.Provenance) + assert.Equal(t, "678ec1ab6d849669c6dbe955", res.Id) + assert.Len(t, res.AllowedMethods, 1) + assert.Len(t, res.AllowedViews, 1) + } +} + +// TestRoleServiceImportError tests error handling for Import +func TestRoleServiceImportError(t *testing.T) { + svc := setupRoleService() + defer testlib.Teardown() + + testlib.AddPostErrorToMux("/authorization/roles", "", http.StatusInternalServerError) + + inputRole := Role{ + Name: "imported_role", + Provenance: "local_aaa", + } + + res, err := svc.Import(inputRole) + + assert.NotNil(t, err) + assert.Nil(t, res) +} + +// TestRoleStructFields tests the Role struct field mappings +func TestRoleStructFields(t *testing.T) { + role := Role{ + Id: "test-id", + Name: "test-role", + Provenance: "local_aaa", + Description: "Test description", + AllowedMethods: []RoleMethod{ + {Name: "testMethod", Provenance: "local_aaa"}, + }, + AllowedViews: []RoleView{ + {Provenance: "local_aaa", Path: "/test"}, + }, + } + + assert.Equal(t, "test-id", role.Id) + assert.Equal(t, "test-role", role.Name) + assert.Equal(t, "local_aaa", role.Provenance) + assert.Equal(t, "Test description", role.Description) + assert.Len(t, role.AllowedMethods, 1) + assert.Len(t, role.AllowedViews, 1) + assert.Equal(t, "testMethod", role.AllowedMethods[0].Name) + assert.Equal(t, "/test", role.AllowedViews[0].Path) +} + +// TestRoleMethodStruct tests the RoleMethod struct +func TestRoleMethodStruct(t *testing.T) { + method := RoleMethod{ + Name: "testMethod", + Provenance: "local_aaa", + } + + assert.Equal(t, "testMethod", method.Name) + assert.Equal(t, "local_aaa", method.Provenance) +} + +// TestRoleViewStruct tests the RoleView struct +func TestRoleViewStruct(t *testing.T) { + view := RoleView{ + Provenance: "local_aaa", + Path: "/admin/test", + } + + assert.Equal(t, "local_aaa", view.Provenance) + assert.Equal(t, "/admin/test", view.Path) +} \ No newline at end of file diff --git a/pkg/services/testdata/2023.2/authorization/roles/create.success.json b/pkg/services/testdata/2023.2/authorization/roles/create.success.json new file mode 100644 index 00000000..9d32f0b0 --- /dev/null +++ b/pkg/services/testdata/2023.2/authorization/roles/create.success.json @@ -0,0 +1,22 @@ +{ + "status": "success", + "message": "Role created successfully", + "data": { + "_id": "678ec1ab6d849669c6dbe954", + "provenance": "local_aaa", + "name": "test_role", + "description": "Test role for validation", + "allowedMethods": [ + { + "name": "testMethod", + "provenance": "local_aaa" + } + ], + "allowedViews": [ + { + "provenance": "local_aaa", + "path": "/test" + } + ] + } +} \ No newline at end of file diff --git a/pkg/services/testdata/2023.2/authorization/roles/delete.success.json b/pkg/services/testdata/2023.2/authorization/roles/delete.success.json new file mode 100644 index 00000000..2b4c049a --- /dev/null +++ b/pkg/services/testdata/2023.2/authorization/roles/delete.success.json @@ -0,0 +1,4 @@ +{ + "status": "success", + "message": "Role deleted successfully" +} \ No newline at end of file diff --git a/pkg/services/testdata/2023.2/authorization/roles/get.success.json b/pkg/services/testdata/2023.2/authorization/roles/get.success.json new file mode 100644 index 00000000..6c8c2468 --- /dev/null +++ b/pkg/services/testdata/2023.2/authorization/roles/get.success.json @@ -0,0 +1,30 @@ +{ + "_id": "678ec1ab6d849669c6dbe952", + "provenance": "local_aaa", + "name": "admin_role", + "description": "Administrative role with full permissions", + "allowedMethods": [ + { + "name": "createUser", + "provenance": "local_aaa" + }, + { + "name": "deleteUser", + "provenance": "local_aaa" + }, + { + "name": "updateUser", + "provenance": "local_aaa" + } + ], + "allowedViews": [ + { + "provenance": "local_aaa", + "path": "/admin/users" + }, + { + "provenance": "local_aaa", + "path": "/admin/settings" + } + ] +} \ No newline at end of file diff --git a/pkg/services/testdata/2023.2/authorization/roles/getall.success.json b/pkg/services/testdata/2023.2/authorization/roles/getall.success.json new file mode 100644 index 00000000..108820bf --- /dev/null +++ b/pkg/services/testdata/2023.2/authorization/roles/getall.success.json @@ -0,0 +1,45 @@ +{ + "results": [ + { + "_id": "678ec1ab6d849669c6dbe952", + "provenance": "local_aaa", + "name": "admin_role", + "description": "Administrative role with full permissions", + "allowedMethods": [ + { + "name": "createUser", + "provenance": "local_aaa" + }, + { + "name": "deleteUser", + "provenance": "local_aaa" + } + ], + "allowedViews": [ + { + "provenance": "local_aaa", + "path": "/admin/users" + } + ] + }, + { + "_id": "678ec1ab6d849669c6dbe953", + "provenance": "local_aaa", + "name": "user_role", + "description": "Basic user role with limited permissions", + "allowedMethods": [ + { + "name": "viewProfile", + "provenance": "local_aaa" + } + ], + "allowedViews": [ + { + "provenance": "local_aaa", + "path": "/profile" + } + ] + } + ], + "total": 2 +} \ No newline at end of file diff --git a/pkg/services/testdata/2023.2/authorization/roles/import.success.json b/pkg/services/testdata/2023.2/authorization/roles/import.success.json new file mode 100644 index 00000000..7d8f758f --- /dev/null +++ b/pkg/services/testdata/2023.2/authorization/roles/import.success.json @@ -0,0 +1,22 @@ +{ + "status": "success", + "message": "Role imported successfully", + "data": { + "_id": "678ec1ab6d849669c6dbe955", + "provenance": "local_aaa", + "name": "imported_role", + "description": "Role imported from external source", + "allowedMethods": [ + { + "name": "importMethod", + "provenance": "local_aaa" + } + ], + "allowedViews": [ + { + "provenance": "local_aaa", + "path": "/import" + } + ] + } +} \ No newline at end of file