using System.Reflection; using FluentAssertions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using ProposalSystem.Api.Controllers; using Xunit; namespace ProposalSystem.Tests.Controllers; /// /// QA-C3: Authorization attribute tests. /// Verifies that controllers and actions have correct [Authorize] and role requirements. /// These are static metadata tests — they verify the security annotations exist /// and are correct without needing to spin up an HTTP server. /// public class AuthorizationAttributeTests { #region Controller-Level Authorization [Fact(DisplayName = "QA-C3: ProposalsController requires authentication")] public void ProposalsController_HasAuthorizeAttribute() { typeof(ProposalsController) .GetCustomAttribute() .Should().NotBeNull("ProposalsController should require authentication"); } [Fact(DisplayName = "QA-C3: AdminController requires admins or sysadmins role")] public void AdminController_RequiresAdminOrSysAdminRole() { var attr = typeof(AdminController).GetCustomAttribute(); attr.Should().NotBeNull("AdminController should require authentication"); attr!.Roles.Should().NotBeNull(); attr.Roles!.Split(',').Select(r => r.Trim()).Should() .Contain("admins").And.Contain("sysadmins"); } [Fact(DisplayName = "QA-C3: UsersController requires authentication")] public void UsersController_HasAuthorizeAttribute() { typeof(UsersController) .GetCustomAttribute() .Should().NotBeNull("UsersController should require authentication"); } #endregion #region Action-Level Role Requirements [Theory(DisplayName = "QA-C3: Admin-only proposal actions require admins/sysadmins role")] [InlineData("Update")] [InlineData("Approve")] [InlineData("MarkSent")] [InlineData("Revise")] [InlineData("GetAudit")] [InlineData("GetSimilar")] [InlineData("GenerateSuggestions")] [InlineData("Regenerate")] [InlineData("AddSimilarReference")] public void ProposalsController_AdminActions_RequireAdminRole(string methodName) { var method = typeof(ProposalsController).GetMethod(methodName); method.Should().NotBeNull($"ProposalsController should have a {methodName} method"); var attr = method!.GetCustomAttribute(); attr.Should().NotBeNull($"{methodName} should have [Authorize] attribute"); attr!.Roles.Should().NotBeNull($"{methodName} should specify roles"); attr.Roles!.Split(',').Select(r => r.Trim()).Should() .Contain("admins", $"{methodName} should allow admins") .And.Contain("sysadmins", $"{methodName} should allow sysadmins"); } [Theory(DisplayName = "QA-C3: Dispatcher-accessible proposal actions do NOT have role restrictions")] [InlineData("Create")] [InlineData("GetAll")] [InlineData("GetById")] [InlineData("GetHistory")] [InlineData("GetStats")] public void ProposalsController_DispatcherActions_NoRoleRestriction(string methodName) { var method = typeof(ProposalsController).GetMethod(methodName); method.Should().NotBeNull($"ProposalsController should have a {methodName} method"); var attr = method!.GetCustomAttribute(); // These methods rely on controller-level [Authorize] but have no role restriction if (attr != null) { attr.Roles.Should().BeNullOrEmpty($"{methodName} should be accessible to all authenticated users"); } } [Theory(DisplayName = "QA-C3: SysAdmin-only user management actions require sysadmins role")] [InlineData("GetAll")] [InlineData("UpdateRole")] public void UsersController_SysAdminActions_RequireSysAdminRole(string methodName) { var method = typeof(UsersController).GetMethod(methodName); method.Should().NotBeNull($"UsersController should have a {methodName} method"); var attr = method!.GetCustomAttribute(); attr.Should().NotBeNull($"{methodName} should have [Authorize] attribute"); attr!.Roles.Should().NotBeNull($"{methodName} should specify roles"); attr.Roles!.Split(',').Select(r => r.Trim()).Should() .Contain("sysadmins", $"{methodName} should require sysadmins role"); } [Fact(DisplayName = "QA-C3: UsersController.GetMe is accessible to all authenticated users")] public void UsersController_GetMe_NoRoleRestriction() { var method = typeof(UsersController).GetMethod("GetMe"); method.Should().NotBeNull(); var attr = method!.GetCustomAttribute(); // GetMe should rely on controller-level [Authorize] without role restriction if (attr != null) { attr.Roles.Should().BeNullOrEmpty("GetMe should be accessible to all authenticated users"); } } #endregion #region Role Hierarchy Verification [Fact(DisplayName = "QA-C3: Dispatchers cannot access admin dashboard")] public void AdminController_NotAccessibleToDispatchers() { var attr = typeof(AdminController).GetCustomAttribute(); attr.Should().NotBeNull(); attr!.Roles.Should().NotBeNull(); var roles = attr.Roles!.Split(',').Select(r => r.Trim()).ToList(); roles.Should().NotContain("dispatchers", "dispatchers must not have access to admin controller"); } [Fact(DisplayName = "QA-C3: Dispatchers cannot access user management")] public void UsersController_GetAll_NotAccessibleToDispatchers() { var method = typeof(UsersController).GetMethod("GetAll"); var attr = method!.GetCustomAttribute(); attr.Should().NotBeNull(); attr!.Roles.Should().NotBeNull(); var roles = attr.Roles!.Split(',').Select(r => r.Trim()).ToList(); roles.Should().NotContain("dispatchers", "dispatchers must not have access to user management"); } [Fact(DisplayName = "QA-C3: Admins cannot access sysadmin-only user management")] public void UsersController_UpdateRole_NotAccessibleToAdmins() { var method = typeof(UsersController).GetMethod("UpdateRole"); var attr = method!.GetCustomAttribute(); attr.Should().NotBeNull(); var roles = attr!.Roles!.Split(',').Select(r => r.Trim()).ToList(); roles.Should().NotContain("admins", "admins must not have access to role management (sysadmins only)"); } #endregion #region All Controllers Must Have [Authorize] [Theory(DisplayName = "QA-C3: All API controllers (except AuthController) require authentication")] [InlineData(typeof(ProposalsController))] [InlineData(typeof(AdminController))] [InlineData(typeof(UsersController))] [InlineData(typeof(CustomersController))] [InlineData(typeof(LineItemsController))] [InlineData(typeof(GeneratedPdfsController))] [InlineData(typeof(FilesController))] [InlineData(typeof(VendorProposalsController))] public void AllControllers_HaveAuthorizeAttribute(Type controllerType) { var attr = controllerType.GetCustomAttribute(); attr.Should().NotBeNull( $"{controllerType.Name} must have [Authorize] attribute to prevent unauthenticated access"); } #endregion }