8 Commits

23 changed files with 2113 additions and 67 deletions

View File

@@ -6,14 +6,50 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIHttpContextAccessor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffe1b239a13ce466b829e79538c654ff54e938_003Fa5_003F4093f165_003FIHttpContextAccessor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIHttpContextAccessor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffe1b239a13ce466b829e79538c654ff54e938_003Fa5_003F4093f165_003FIHttpContextAccessor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb7208b3f72528d22781d25fde9a55271bdf2b5aade4f03b1324579a25493cd8_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb7208b3f72528d22781d25fde9a55271bdf2b5aade4f03b1324579a25493cd8_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMvcCoreServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fe3d0c01ead7c1fbe35a3504e9fbf28f212ac59349851c852a28fa06d719e95_003FMvcCoreServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOpenIdConnectConfiguration_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2820ca73717f4b1ab2b3b74fd61961bd1d5b0_003F7e_003F8be6b109_003FOpenIdConnectConfiguration_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOpenIdConnectConfiguration_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2820ca73717f4b1ab2b3b74fd61961bd1d5b0_003F7e_003F8be6b109_003FOpenIdConnectConfiguration_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARedirectResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2261f0cc7e883c3c89b8e6c7ea509ac7c52d0629e68690ed216e1cc0c8ac_003FRedirectResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARedirectResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2261f0cc7e883c3c89b8e6c7ea509ac7c52d0629e68690ed216e1cc0c8ac_003FRedirectResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASwaggerGenServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F95fc571df74c88edcdd2fb9f2e804132aa893358f3e0d8bca643299aed8dbc_003FSwaggerGenServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2c8e7ca976f350cba9836d5565dac56b11e0b56656fa786460eb1395857a6fa_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2c8e7ca976f350cba9836d5565dac56b11e0b56656fa786460eb1395857a6fa_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValidationMessageStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Ffc81648e473bb3cc818f71427c286ecddc3604d2f4c69c565205bb89e8b4ef4_003FValidationMessageStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValidationMessageStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Ffc81648e473bb3cc818f71427c286ecddc3604d2f4c69c565205bb89e8b4ef4_003FValidationMessageStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD; <s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD;
&lt;Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" /&gt;&#xD; &lt;Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" /&gt;&#xD;
&lt;Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" /&gt;&#xD; &lt;Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" /&gt;&#xD;
&lt;/AssemblyExplorer&gt;</s:String> &lt;/AssemblyExplorer&gt;</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=027ac703_002Df1f3_002D42aa_002D9c67_002D7cbaeecdbead/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="Refresh_ShouldReturnConflict_WhenRefreshTokenNotValid" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;xUnit::25DE1510-47E5-46FF-89A4-B9F99542218E::net8.0::HopFrame.Tests.Api.Controllers.OpenIdControllerTests.Refresh_ShouldReturnConflict_WhenRefreshTokenNotValid&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD;
&lt;/SessionState&gt;</s:String>

View File

@@ -1,21 +0,0 @@
# HopFrame Endpoints
HopFrame currently only supports endpoints for authentication out of the box.
> **Hint:** with the help of the [repositories](../repositories.md) you can very easily create missing endpoints for HopFrame components yourself.
## All currently supported endpoints
> **Hint:** you can use the build-in [swagger](https://swagger.io/) ui to explore and test all endpoints of your application __including__ HopFrame endpoints.
### SecurityController
Base endpoint: `/api/v1/authentication`\
**Important:** All primitive data types (including `string`) are return as a [`SingleValueResult`](./models.md#SingleValueResult)
| Method | Endpoint | Payload | Returns |
|--------|---------------|--------------------------------------------------------------|-----------------------|
| PUT | /login | [UserLogin](../models.md#UserLogin) | access token (string) |
| POST | /register | [UserRegister](../models.md#UserRegister) | access token (string) |
| GET | /authenticate | | access token (string) |
| DELETE | /logout | | |
| DELETE | /delete | [UserPasswordValidation](./models.md#UserPasswordValidation) | |

120
docs/api/endpoints/auth.md Normal file
View File

@@ -0,0 +1,120 @@
# Auth Endpoints
## Used Models
- [UserLogin](../../models.md#userlogin)
- [UserRegister](../../models.md#userregister)
- [SingleValueResult](../../models.md#singlevalueresult)
- [UserPasswordValidation](../../models.md#userpasswordvalidation)
## API Endpoint: Login
**Endpoint:** `PUT /api/v1/auth/login`
**Description:** Authenticates a user and provides access and refresh tokens.
**Authorization Required:** No
**Parameters:**
- **UserLogin** (required): The login credentials of the user.
```json
{
"email": "string",
"password": "string"
}
```
**Response:**
- **200 OK:** Returns the access token.
```json
{
"value": "string"
}
```
- **400 Bad Request:** HopFrame authentication scheme is disabled.
- **404 Not Found:** The provided email address was not found.
- **403 Forbidden:** The provided password is not correct.
## API Endpoint: Register
**Endpoint:** `POST /api/v1/auth/register`
**Description:** Registers a new user and provides access and refresh tokens.
**Authorization Required:** No
**Parameters:**
- **UserRegister** (required): The registration details of the user.
```json
{
"username": "string",
"email": "string",
"password": "string"
}
```
**Response:**
- **200 OK:** Returns the access token.
```json
{
"value": "string"
}
```
- **400 Bad Request:** HopFrame authentication scheme is disabled or the password is too short.
- **409 Conflict:** Username or email is already registered.
## API Endpoint: Authenticate
**Endpoint:** `GET /api/v1/auth/authenticate`
**Description:** Authenticates the user using the refresh token and provides a new access token.
**Authorization Required:** Yes
**Parameters:**
- None
**Response:**
- **200 OK:** Returns the access token.
```json
{
"value": "string"
}
```
- **400 Bad Request:** HopFrame authentication scheme is disabled or refresh token not provided.
- **404 Not Found:** The refresh token is not valid.
- **403 Forbidden:** The refresh token is expired.
- **409 Conflict:** The provided token is not a refresh token.
## API Endpoint: Logout
**Endpoint:** `DELETE /api/v1/auth/logout`
**Description:** Logs out the user and deletes the access and refresh tokens.
**Authorization Required:** Yes
**Parameters:**
- None
**Response:**
- **200 OK:** User is logged out successfully.
## API Endpoint: Delete
**Endpoint:** `DELETE /api/v1/auth/delete`
**Description:** Deletes the user account.
**Authorization Required:** Yes
**Parameters:**
- **UserPasswordValidation** (required): The password validation for the user.
```json
{
"password": "string"
}
```
**Response:**
- **200 OK:** User account is deleted successfully.
- **403 Forbidden:** The provided password is not correct.

237
docs/api/endpoints/group.md Normal file
View File

@@ -0,0 +1,237 @@
# Group Endpoints
## Used Models
- [Group](../../models.md#permissiongroup)
## API Endpoint: GetGroups
**Endpoint:** `GET /api/v1/groups`
**Description:** Retrieves a list of all groups.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.groups.read`
**Parameters:** None
**Response:**
- **200 OK:** Returns a list of groups.
```json
[
{
"Name": "string",
"IsDefaultGroup": "boolean",
"Description": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
]
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
## API Endpoint: GetDefaultGroups
**Endpoint:** `GET /api/v1/groups/default`
**Description:** Retrieves a list of default groups.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.groups.read`
**Parameters:** None
**Response:**
- **200 OK:** Returns a list of default groups.
```json
[
{
"Name": "string",
"IsDefaultGroup": "boolean",
"Description": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
]
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
## API Endpoint: GetUserGroups
**Endpoint:** `GET /api/v1/groups/user/{userId}`
**Description:** Retrieves a list of groups for a specific user.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.groups.read`
**Parameters:**
- **userId:** `string` (path) - The ID of the user.
**Response:**
- **200 OK:** Returns a list of groups for the user.
```json
[
{
"Name": "string",
"IsDefaultGroup": "boolean",
"Description": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
]
```
- **400 Bad Request:** Invalid user ID.
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** User does not exist.
## API Endpoint: GetGroup
**Endpoint:** `GET /api/v1/groups/{name}`
**Description:** Retrieves details of a specific group.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.groups.read`
**Parameters:**
- **name:** `string` (path) - The name of the group.
**Response:**
- **200 OK:** Returns the details of the group.
```json
{
"Name": "string",
"IsDefaultGroup": "boolean",
"Description": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** Group does not exist.
## API Endpoint: CreateGroup
**Endpoint:** `POST /api/v1/groups`
**Description:** Creates a new group.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.groups.create`
**Parameters:**
- **group:** `PermissionGroup` (body) - The group to be created.
**Response:**
- **200 OK:** Returns the created group.
```json
{
"Name": "string",
"IsDefaultGroup": "boolean",
"Description": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **400 Bad Request:** Provide a group.
- **400 Bad Request:** Group names must start with 'group.'.
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **409 Conflict:** Group already exists.
## API Endpoint: UpdateGroup
**Endpoint:** `PUT /api/v1/groups`
**Description:** Updates an existing group.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.groups.update`
**Parameters:**
- **group:** `PermissionGroup` (body) - The group to be updated.
**Response:**
- **200 OK:** Returns the updated group.
```json
{
"Name": "string",
"IsDefaultGroup": "boolean",
"Description": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** Group does not exist.
## API Endpoint: DeleteGroup
**Endpoint:** `DELETE /api/v1/groups/{name}`
**Description:** Deletes a specific group.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.groups.delete`
**Parameters:**
- **name:** `string` (path) - The name of the group to be deleted.
**Response:**
- **200 OK:** Group deleted successfully.
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** Group does not exist.

View File

@@ -0,0 +1,82 @@
# OpenID Endpoints
## Used Models
- [SingleValueResult](../../models.md#singlevalueresult)
## API Endpoint: RedirectToProvider
**Endpoint:** `GET /api/v1/openid/redirect`
**Description:** Redirects the user to the OpenID provider's authorization endpoint.
**Authorization Required:** No
**Parameters:**
- **redirectAfter** (query, optional): The URL to redirect to after authentication.
- **performRedirect** (query, optional): A flag to indicate if the user should be redirected (default is 1).
**Response:**
- **302 Found:** Redirects the user to the OpenID provider's authorization endpoint.
- **200 OK:** Returns the constructed authorization URI.
```json
{
"value": "string"
}
```
## API Endpoint: Callback
**Endpoint:** `GET /api/v1/openid/callback`
**Description:** Handles the callback from the OpenID provider and exchanges the authorization code for tokens.
**Authorization Required:** No
**Parameters:**
- **code** (query, required): The authorization code received from the OpenID provider.
- **state** (query, optional): The state parameter to handle the redirect after authentication.
**Response:**
- **200 OK:** Returns the access token.
```json
{
"value": "string"
}
```
- **400 Bad Request:** Authorization code is missing.
- **403 Forbidden:** Authorization code is not valid.
## API Endpoint: Refresh
**Endpoint:** `GET /api/v1/openid/refresh`
**Description:** Refreshes the access token using the refresh token.
**Authorization Required:** Yes
**Parameters:**
- None
**Response:**
- **200 OK:** Returns the refreshed access token.
```json
{
"value": "string"
}
```
- **400 Bad Request:** Refresh token not provided.
- **409 Conflict**: Refresh token not valid.
## API Endpoint: Logout
**Endpoint:** `DELETE /api/v1/openid/logout`
**Description:** Logs out the user by deleting the authentication cookies.
**Authorization Required:** Yes
**Parameters:**
- None
**Response:**
- **200 OK:** User is logged out successfully.

316
docs/api/endpoints/user.md Normal file
View File

@@ -0,0 +1,316 @@
# User Endpoints
## Used Models
- [User](../../models.md#user)
- [UserCreator](../../models.md#usercreator)
- [Permission](../../models.md#permission)
## API Endpoint: GetUsers
**Endpoint:** `GET /api/v1/users`
**Description:** Retrieves a list of users.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.read`
**Parameters:** None
**Response:**
- **200 OK:** Returns a list of users.
```json
[
{
"Id": "guid",
"Username": "string",
"Email": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
]
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
## API Endpoint: GetUser
**Endpoint:** `GET /api/v1/users/{userId}`
**Description:** Retrieves a user by their ID.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.read`
**Parameters:**
- **userId:** `string` (Path) - The ID of the user to retrieve.
**Response:**
- **200 OK:** Returns the user details.
```json
{
"Id": "guid",
"Username": "string",
"Email": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **400 Bad Request:** Invalid user ID format.
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** User does not exist.
## API Endpoint: GetUserByUsername
**Endpoint:** `GET /api/v1/users/username/{username}`
**Description:** Retrieves a user by their username.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.read`
**Parameters:**
- **username:** `string` (Path) - The username of the user to retrieve.
**Response:**
- **200 OK:** Returns the user details.
```json
{
"Id": "guid",
"Username": "string",
"Email": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** User does not exist.
## API Endpoint: GetUserByEmail
**Endpoint:** `GET /api/v1/users/email/{email}`
**Description:** Retrieves a user by their email address.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.read`
**Parameters:**
- **email:** `string` (Path) - The email address of the user to retrieve.
**Response:**
- **200 OK:** Returns the user details.
```json
{
"Id": "guid",
"Username": "string",
"Email": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** User does not exist.
## API Endpoint: CreateUser
**Endpoint:** `POST /api/v1/users`
**Description:** Creates a new user.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.create`
**Parameters:**
- **UserCreator:** (Body) - The user creation details.
```json
{
"Username": "string",
"Email": "string",
"Password": "string",
"Permissions": [
"permission1",
"permission2"
]
}
```
**Response:**
- **200 OK:** Returns the created user.
```json
{
"Id": "guid",
"Username": "string",
"Email": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **409 Conflict:** The user already exists.
## API Endpoint: UpdateUser
**Endpoint:** `PUT /api/v1/users/{userId}`
**Description:** Updates an existing user by their ID.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.update`
**Parameters:**
- **userId:** `string` (Path) - The ID of the user to update.
- **User:** (Body) - The user details to update.
```json
{
"Id": "guid",
"Username": "string",
"Email": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
**Response:**
- **200 OK:** Returns the updated user.
```json
{
"Id": "guid",
"Username": "string",
"Email": "string",
"CreatedAt": "2023-12-23T00:00:00Z",
"Permissions": [
{
"Id": 1,
"PermissionName": "string",
"GrantedAt": "2023-12-23T00:00:00Z"
}
]
}
```
- **400 Bad Request:** Invalid user ID format.
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** User does not exist.
- **409 Conflict:** Cannot edit user with different user ID.
## API Endpoint: DeleteUser
**Endpoint:** `DELETE /api/v1/users/{userId}`
**Description:** Deletes a user by their ID.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.delete`
**Parameters:**
- **userId:** `string` (Path) - The ID of the user to delete.
**Response:**
- **200 OK:** User successfully deleted.
- **400 Bad Request:** Invalid user ID format.
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** User does not exist.
## API Endpoint: ChangePassword
**Endpoint:** `PUT /api/v1/users/{userId}/password`
**Description:** Updates the password for a user by their ID.
**Authorization Required:** Yes
**Required Permission:** `hopframe.admin.users.update` (if the userId is not the id of the requesting user)
**Parameters:**
- **userId:** `string` (Path) - The ID of the user whose password is being changed.
- **UserPasswordChange:** (Body) - The password change details (note, if you change someone else's password the old password doesn't need to be correct).
```json
{
"oldPassword": "string",
"newPassword": "string"
}
```
**Response:**
- **200 OK:** Password successfully updated.
- **400 Bad Request:** Invalid user ID format.
- **401 Unauthorized:** User is not authorized to access this endpoint.
- **404 Not Found:** User does not exist.
- **409 Conflict:** Old password is incorrect.

View File

@@ -14,3 +14,66 @@ public sealed class UserPasswordValidation {
public string Password { get; set; } public string Password { get; set; }
} }
``` ```
## OpenIdConfiguration
```csharp
public sealed class OpenIdConfiguration {
public string Issuer { get; set; }
public string AuthorizationEndpoint { get; set; }
public string TokenEndpoint { get; set; }
public string UserinfoEndpoint { get; set; }
public string EndSessionEndpoint { get; set; }
public string IntrospectionEndpoint { get; set; }
public string RevocationEndpoint { get; set; }
public string DeviceAuthorizationEndpoint { get; set; }
public List<string> ResponseTypesSupported { get; set; }
public List<string> ResponseModesSupported { get; set; }
public string JwksUri { get; set; }
public List<string> GrantTypesSupported { get; set; }
public List<string> IdTokenSigningAlgValuesSupported { get; set; }
public List<string> SubjectTypesSupported { get; set; }
public List<string> TokenEndpointAuthMethodsSupported { get; set; }
public List<string> AcrValuesSupported { get; set; }
public List<string> ScopesSupported { get; set; }
public bool RequestParameterSupported { get; set; }
public List<string> ClaimsSupported { get; set; }
public bool ClaimsParameterSupported { get; set; }
public List<string> CodeChallengeMethodsSupported { get; set; }
}
```
## OpenIdIntrospection
```csharp
public sealed class OpenIdIntrospection {
public string Issuer { get; set; }
public string Subject { get; set; }
public string Audience { get; set; }
public long Expiration { get; set; }
public long IssuedAt { get; set; }
public long AuthTime { get; set; }
public string Acr { get; set; }
public List<string> AuthenticationMethods { get; set; }
public string SessionId { get; set; }
public string Email { get; set; }
public bool EmailVerified { get; set; }
public string Name { get; set; }
public string GivenName { get; set; }
public string PreferredUsername { get; set; }
public string Nickname { get; set; }
public List<string> Groups { get; set; }
public bool Active { get; set; }
public string Scope { get; set; }
public string ClientId { get; set; }
}
```
## OpenIdToken
```csharp
public sealed class OpenIdToken {
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public string TokenType { get; set; }
public int ExpiresIn { get; set; }
public string IdToken { get; set; }
}
```

View File

@@ -67,7 +67,31 @@ public class UserRegister {
} }
``` ```
## UserCreator
```csharp
public class UserCreator {
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public virtual List<string> Permissions { get; set; }
}
```
## IPermissionOwner ## IPermissionOwner
```csharp ```csharp
public interface IPermissionOwner; public interface IPermissionOwner;
``` ```
## SingleValueResult
```csharp
public struct SingleValueResult<TValue>(TValue value) {
public TValue Value { get; set; } = value;
}
```
## UserPasswordValidation
```csharp
public sealed class UserPasswordValidation {
public string Password { get; set; }
}
```

View File

@@ -51,7 +51,7 @@ public class OpenIdController(IOpenIdAccessor accessor) : ControllerBase {
var token = await accessor.RefreshAccessToken(refreshToken); var token = await accessor.RefreshAccessToken(refreshToken);
if (token is null) if (token is null)
return NotFound("Refresh token not valid"); return Conflict("Refresh token not valid");
accessor.SetAuthenticationCookies(token); accessor.SetAuthenticationCookies(token);

View File

@@ -1,6 +1,7 @@
using HopFrame.Api.Controller; using HopFrame.Api.Controller;
using HopFrame.Api.Logic; using HopFrame.Api.Logic;
using HopFrame.Api.Logic.Implementation; using HopFrame.Api.Logic.Implementation;
using HopFrame.Api.Models;
using HopFrame.Database; using HopFrame.Database;
using HopFrame.Security.Authentication; using HopFrame.Security.Authentication;
using HopFrame.Security.Authentication.OpenID; using HopFrame.Security.Authentication.OpenID;
@@ -18,20 +19,26 @@ public static class ServiceCollectionExtensions {
/// </summary> /// </summary>
/// <param name="services">The service provider to add the services to</param> /// <param name="services">The service provider to add the services to</param>
/// <param name="configuration">The configuration used to configure HopFrame authentication</param> /// <param name="configuration">The configuration used to configure HopFrame authentication</param>
/// <param name="config">Configuration for how the HopFrame services are set up</param>
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam> /// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
public static void AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase { public static void AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration, HopFrameApiModuleConfig config = null) where TDbContext : HopDbContextBase {
var controllers = new List<Type> { typeof(UserController), typeof(GroupController) }; config ??= new();
var controllers = new List<Type>();
if (config.ExposeModelEndpoints)
controllers.AddRange([typeof(UserController), typeof(GroupController)]);
var defaultAuthenticationSection = configuration.GetSection("HopFrame:Authentication:DefaultAuthentication"); var defaultAuthenticationSection = configuration.GetSection("HopFrame:Authentication:DefaultAuthentication");
if (!defaultAuthenticationSection.Exists() || configuration.GetValue<bool>("HopFrame:Authentication:DefaultAuthentication")) if ((!defaultAuthenticationSection.Exists() || configuration.GetValue<bool>("HopFrame:Authentication:DefaultAuthentication")) && config.ExposeAuthEndpoints)
controllers.Add(typeof(AuthController)); controllers.Add(typeof(AuthController));
if (configuration.GetValue<bool>("HopFrame:Authentication:OpenID:Enabled")) { if (configuration.GetValue<bool>("HopFrame:Authentication:OpenID:Enabled") && config.ExposeAuthEndpoints) {
IOpenIdAccessor.DefaultCallback = OpenIdController.DefaultCallback; IOpenIdAccessor.DefaultCallback = OpenIdController.DefaultCallback;
controllers.Add(typeof(OpenIdController)); controllers.Add(typeof(OpenIdController));
} }
AddHopFrameNoEndpoints<TDbContext>(services, configuration); AddHopFrameNoEndpoints<TDbContext>(services, configuration, config);
services.AddMvcCore().UseSpecificControllers(controllers.ToArray()); services.AddMvcCore().UseSpecificControllers(controllers.ToArray());
} }
@@ -40,20 +47,24 @@ public static class ServiceCollectionExtensions {
/// </summary> /// </summary>
/// <param name="services">The service provider to add the services to</param> /// <param name="services">The service provider to add the services to</param>
/// <param name="configuration">The configuration used to configure HopFrame authentication</param> /// <param name="configuration">The configuration used to configure HopFrame authentication</param>
/// <param name="config">Configuration for how the HopFrame services are set up</param>
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam> /// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase { public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services, ConfigurationManager configuration, HopFrameApiModuleConfig config = null) where TDbContext : HopDbContextBase {
config ??= new();
services.AddMvcCore().ConfigureApplicationPartManager(manager => { services.AddMvcCore().ConfigureApplicationPartManager(manager => {
var endpoints = manager.ApplicationParts.SingleOrDefault(p => p.Name == typeof(ServiceCollectionExtensions).Namespace!.Replace(".Extensions", "")); var endpoints = manager.ApplicationParts.SingleOrDefault(p => p.Name == typeof(ServiceCollectionExtensions).Namespace!.Replace(".Extensions", ""));
manager.ApplicationParts.Remove(endpoints); manager.ApplicationParts.Remove(endpoints);
}); });
services.AddSingleton(config);
services.AddHopFrameRepositories<TDbContext>(); services.AddHopFrameRepositories<TDbContext>();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IAuthLogic, AuthLogic>(); services.AddScoped<IAuthLogic, AuthLogic>();
services.AddScoped<IUserLogic, UserLogic>(); services.AddScoped<IUserLogic, UserLogic>();
services.AddScoped<IGroupLogic, GroupLogic>(); services.AddScoped<IGroupLogic, GroupLogic>();
services.AddHopFrameAuthentication(configuration); services.AddHopFrameAuthentication(configuration, config);
} }
} }

View File

@@ -0,0 +1,8 @@
using HopFrame.Security.Models;
namespace HopFrame.Api.Models;
public class HopFrameApiModuleConfig : HopFrameConfig {
public bool ExposeModelEndpoints { get; set; } = true;
public bool ExposeAuthEndpoints { get; set; } = true;
}

View File

@@ -8,6 +8,8 @@ namespace HopFrame.Database;
/// </summary> /// </summary>
public abstract class HopDbContextBase : DbContext { public abstract class HopDbContextBase : DbContext {
public static IList<Action<HopDbContextBase>> SaveHandlers = new List<Action<HopDbContextBase>>();
public virtual DbSet<User> Users { get; set; } public virtual DbSet<User> Users { get; set; }
public virtual DbSet<Permission> Permissions { get; set; } public virtual DbSet<Permission> Permissions { get; set; }
public virtual DbSet<Token> Tokens { get; set; } public virtual DbSet<Token> Tokens { get; set; }
@@ -36,4 +38,36 @@ public abstract class HopDbContextBase : DbContext {
.WithOne(t => t.Token) .WithOne(t => t.Token)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
} }
private void OnSaving() {
var orphanedPermissions = Permissions
.Where(p => p.UserId == null && p.GroupName == null && p.TokenId == null)
.ToList();
foreach (var handler in SaveHandlers) {
handler.Invoke(this);
}
Permissions.RemoveRange(orphanedPermissions);
}
public override int SaveChanges() {
OnSaving();
return base.SaveChanges();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess) {
OnSaving();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) {
OnSaving();
return base.SaveChangesAsync(cancellationToken);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken()) {
OnSaving();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
} }

View File

@@ -18,12 +18,18 @@ public class Permission {
[ForeignKey("UserId"), JsonIgnore] [ForeignKey("UserId"), JsonIgnore]
public virtual User User { get; set; } public virtual User User { get; set; }
public Guid? UserId { get; set; }
[ForeignKey("GroupName"), JsonIgnore] [ForeignKey("GroupName"), JsonIgnore]
public virtual PermissionGroup Group { get; set; } public virtual PermissionGroup Group { get; set; }
[MaxLength(255)]
public string GroupName { get; set; }
[ForeignKey("TokenId"), JsonIgnore] [ForeignKey("TokenId"), JsonIgnore]
public virtual Token Token { get; set; } public virtual Token Token { get; set; }
public Guid? TokenId { get; set; }
} }
public interface IPermissionOwner; public interface IPermissionOwner;

View File

@@ -1,8 +1,11 @@
using HopFrame.Database;
using HopFrame.Database.Models;
using HopFrame.Security.Authentication.OpenID; using HopFrame.Security.Authentication.OpenID;
using HopFrame.Security.Authentication.OpenID.Implementation; using HopFrame.Security.Authentication.OpenID.Implementation;
using HopFrame.Security.Authentication.OpenID.Options; using HopFrame.Security.Authentication.OpenID.Options;
using HopFrame.Security.Authorization; using HopFrame.Security.Authorization;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Models;
using HopFrame.Security.Options; using HopFrame.Security.Options;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@@ -16,25 +19,48 @@ public static class HopFrameAuthenticationExtensions {
/// <summary> /// <summary>
/// Configures the WebApplication to use the authentication and authorization of the HopFrame API /// Configures the WebApplication to use the authentication and authorization of the HopFrame API
/// </summary> /// </summary>
/// <param name="service">The service provider to add the services to</param> /// <param name="services">The service provider to add the services to</param>
/// <param name="configuration">The configuration used to configure HopFrame authentication</param> /// <param name="configuration">The configuration used to configure HopFrame authentication</param>
/// <param name="config">Configuration for how the HopFrame services are set up</param>
/// <returns></returns> /// <returns></returns>
public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service, ConfigurationManager configuration) { public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection services, ConfigurationManager configuration, HopFrameConfig config = null) {
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); config ??= new HopFrameConfig();
service.AddScoped<ITokenContext, TokenContextImplementor>();
service.AddHttpClient(); services.AddSingleton(config);
service.AddMemoryCache(); services.AddScoped(typeof(ICacheProvider), config.CacheProvider);
service.AddScoped<IOpenIdAccessor, OpenIdAccessor>(); services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<ITokenContext, TokenContextImplementor>();
service.AddOptionsFromConfiguration<HopFrameAuthenticationOptions>(configuration); if (config.CacheProvider == typeof(MemoryCacheProvider))
service.AddOptionsFromConfiguration<AdminPermissionOptions>(configuration); services.AddMemoryCache();
service.AddOptionsFromConfiguration<OpenIdOptions>(configuration);
service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {}); services.AddHttpClient<OpenIdAccessor>();
service.AddAuthorization(); services.AddScoped<IOpenIdAccessor, OpenIdAccessor>();
return service; services.AddOptionsFromConfiguration<HopFrameAuthenticationOptions>(configuration);
services.AddOptionsFromConfiguration<AdminPermissionOptions>(configuration);
services.AddOptionsFromConfiguration<OpenIdOptions>(configuration);
services.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
services.AddAuthorization();
HopDbContextBase.SaveHandlers.Add(context => {
var section = configuration.GetSection("HopFrame:Authentication");
var accessToken = section?.GetSection("AccessToken")?.Get<HopFrameAuthenticationOptions.TokenTime>()?.ConstructTimeSpan ?? new HopFrameAuthenticationOptions().AccessTokenTime;
var refreshToken = section?.GetSection("RefreshToken")?.Get<HopFrameAuthenticationOptions.TokenTime>()?.ConstructTimeSpan ?? new HopFrameAuthenticationOptions().RefreshTokenTime;
var now = DateTime.Now;
var accessTokenExpiry = now - accessToken;
var refreshTokenExpiry = now - refreshToken;
var invalidTokens = context.Tokens
.Where(t =>
(t.Type == Token.AccessTokenType && t.CreatedAt < accessTokenExpiry) ||
(t.Type == Token.RefreshTokenType && t.CreatedAt < refreshTokenExpiry))
.ToList();
context.Tokens.RemoveRange(invalidTokens);
});
return services;
} }
} }

View File

@@ -0,0 +1,6 @@
namespace HopFrame.Security.Authentication.OpenID;
public interface ICacheProvider {
Task<TItem> GetOrCreate<TItem>(string key, Func<Task<TItem>> factory) where TItem : class;
Task Set<TItem>(string key, TItem value, TimeSpan ttl);
}

View File

@@ -0,0 +1,18 @@
using Microsoft.Extensions.Caching.Memory;
namespace HopFrame.Security.Authentication.OpenID.Implementation;
public class MemoryCacheProvider(IMemoryCache cache) : ICacheProvider {
public Task<TItem> GetOrCreate<TItem>(string key, Func<Task<TItem>> factory) where TItem : class {
if (cache.TryGetValue(key, out var value)) {
return Task.FromResult(value as TItem);
}
return factory.Invoke();
}
public Task Set<TItem>(string key, TItem value, TimeSpan ttl) {
cache.Set(key, value, ttl);
return Task.CompletedTask;
}
}

View File

@@ -3,21 +3,24 @@ using HopFrame.Security.Authentication.OpenID.Models;
using HopFrame.Security.Authentication.OpenID.Options; using HopFrame.Security.Authentication.OpenID.Options;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace HopFrame.Security.Authentication.OpenID.Implementation; namespace HopFrame.Security.Authentication.OpenID.Implementation;
internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdOptions> options, IHttpContextAccessor accessor, IMemoryCache cache) : IOpenIdAccessor { internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdOptions> options, IHttpContextAccessor accessor, ICacheProvider cache) : IOpenIdAccessor {
private const string ConfigurationCacheKey = "HopFrame:OpenID:Configuration"; private const string ConfigurationCacheKey = "HopFrame:OpenID:Configuration";
private const string AuthCodeCacheKey = "HopFrame:OpenID:Code:"; private const string AuthCodeCacheKey = "HopFrame:OpenID:Code:";
private const string TokenCacheKey = "HopFrame:OpenID:Token:"; private const string TokenCacheKey = "HopFrame:OpenID:Token:";
public async Task<OpenIdConfiguration> LoadConfiguration() { public Task<OpenIdConfiguration> LoadConfiguration() {
if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled && cache.TryGetValue(ConfigurationCacheKey, out object cachedConfiguration)) { if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled) {
return cachedConfiguration as OpenIdConfiguration; return cache.GetOrCreate(ConfigurationCacheKey, LoadConfigurationInCache);
} }
return LoadConfigurationInCache();
}
internal async Task<OpenIdConfiguration> LoadConfigurationInCache() {
var client = clientFactory.CreateClient(); var client = clientFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, Path.Combine(options.Value.Issuer, ".well-known/openid-configuration").Replace("\\", "/")); var request = new HttpRequestMessage(HttpMethod.Get, Path.Combine(options.Value.Issuer, ".well-known/openid-configuration").Replace("\\", "/"));
var response = await client.SendAsync(request); var response = await client.SendAsync(request);
@@ -28,16 +31,20 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdO
var config = await JsonSerializer.DeserializeAsync<OpenIdConfiguration>(await response.Content.ReadAsStreamAsync()); var config = await JsonSerializer.DeserializeAsync<OpenIdConfiguration>(await response.Content.ReadAsStreamAsync());
if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled) if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled)
cache.Set(ConfigurationCacheKey, config, options.Value.Cache.Configuration.TTL.ConstructTimeSpan); await cache.Set(ConfigurationCacheKey, config, options.Value.Cache.Configuration.TTL.ConstructTimeSpan);
return config; return config;
} }
public async Task<OpenIdToken> RequestToken(string code) { public Task<OpenIdToken> RequestToken(string code) {
if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled && cache.TryGetValue(AuthCodeCacheKey + code, out object cachedToken)) { if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled) {
return cachedToken as OpenIdToken; return cache.GetOrCreate(AuthCodeCacheKey + code, () => RequestTokenInCache(code));
} }
return RequestTokenInCache(code);
}
internal async Task<OpenIdToken> RequestTokenInCache(string code) {
var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http"; var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http";
var callback = options.Value.Callback ?? Path.Combine($"{protocol}://{accessor.HttpContext!.Request.Host.Value}", IOpenIdAccessor.DefaultCallback).Replace("\\", "/"); var callback = options.Value.Callback ?? Path.Combine($"{protocol}://{accessor.HttpContext!.Request.Host.Value}", IOpenIdAccessor.DefaultCallback).Replace("\\", "/");
@@ -61,7 +68,7 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdO
var token = await JsonSerializer.DeserializeAsync<OpenIdToken>(await response.Content.ReadAsStreamAsync()); var token = await JsonSerializer.DeserializeAsync<OpenIdToken>(await response.Content.ReadAsStreamAsync());
if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled) if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled)
cache.Set(AuthCodeCacheKey + code, token, options.Value.Cache.Auth.TTL.ConstructTimeSpan); await cache.Set(AuthCodeCacheKey + code, token, options.Value.Cache.Auth.TTL.ConstructTimeSpan);
return token; return token;
} }
@@ -74,11 +81,15 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdO
return $"{configuration.AuthorizationEndpoint}?response_type=code&client_id={options.Value.ClientId}&redirect_uri={callback}&scope=openid%20profile%20email%20offline_access&state={state}"; return $"{configuration.AuthorizationEndpoint}?response_type=code&client_id={options.Value.ClientId}&redirect_uri={callback}&scope=openid%20profile%20email%20offline_access&state={state}";
} }
public async Task<OpenIdIntrospection> InspectToken(string token) { public Task<OpenIdIntrospection> InspectToken(string token) {
if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled && cache.TryGetValue(TokenCacheKey + token, out object cachedToken)) { if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled) {
return cachedToken as OpenIdIntrospection; return cache.GetOrCreate(TokenCacheKey + token, () => InspectTokenInCache(token));
} }
return InspectTokenInCache(token);
}
internal async Task<OpenIdIntrospection> InspectTokenInCache(string token) {
var configuration = await LoadConfiguration(); var configuration = await LoadConfiguration();
var client = clientFactory.CreateClient(); var client = clientFactory.CreateClient();
@@ -97,7 +108,7 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdO
var introspection = await JsonSerializer.DeserializeAsync<OpenIdIntrospection>(await response.Content.ReadAsStreamAsync()); var introspection = await JsonSerializer.DeserializeAsync<OpenIdIntrospection>(await response.Content.ReadAsStreamAsync());
if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled) if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled)
cache.Set(TokenCacheKey + token, introspection, options.Value.Cache.Inspection.TTL.ConstructTimeSpan); await cache.Set(TokenCacheKey + token, introspection, options.Value.Cache.Inspection.TTL.ConstructTimeSpan);
return introspection; return introspection;
} }

View File

@@ -0,0 +1,7 @@
using HopFrame.Security.Authentication.OpenID.Implementation;
namespace HopFrame.Security.Models;
public class HopFrameConfig {
public Type CacheProvider { get; set; } = typeof(MemoryCacheProvider);
}

View File

@@ -1,5 +1,7 @@
using HopFrame.Security.Models;
namespace HopFrame.Web.Models; namespace HopFrame.Web.Models;
public class HopFrameWebModuleConfig { public class HopFrameWebModuleConfig : HopFrameConfig {
public string AdminLoginPageUri { get; set; } = "/administration/login"; public string AdminLoginPageUri { get; set; } = "/administration/login";
} }

View File

@@ -14,18 +14,18 @@ namespace HopFrame.Web;
public static class ServiceCollectionExtensions { public static class ServiceCollectionExtensions {
public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration, HopFrameWebModuleConfig config = null) where TDbContext : HopDbContextBase { public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration, HopFrameWebModuleConfig config = null) where TDbContext : HopDbContextBase {
services.AddHttpClient(); config ??= new HopFrameWebModuleConfig();
services.AddHopFrameRepositories<TDbContext>(); services.AddHopFrameRepositories<TDbContext>();
services.AddScoped<IAuthService, AuthService>(); services.AddScoped<IAuthService, AuthService>();
services.AddTransient<AuthMiddleware>(); services.AddTransient<AuthMiddleware>();
services.AddAdminContext<HopAdminContext>(); services.AddAdminContext<HopAdminContext>();
services.AddSingleton(config ?? new HopFrameWebModuleConfig()); services.AddSingleton(config);
// Component library's // Component library's
services.AddSweetAlert2(); services.AddSweetAlert2();
services.AddBlazorStrap(); services.AddBlazorStrap();
services.AddHopFrameAuthentication(configuration); services.AddHopFrameAuthentication(configuration, config);
return services; return services;
} }

View File

@@ -0,0 +1,369 @@
using System.Net;
using HopFrame.Api.Controller;
using HopFrame.Api.Logic;
using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Security.Authorization;
using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Moq;
namespace HopFrame.Tests.Api.Controllers;
public class GroupControllerTests {
private (Mock<IGroupLogic>, Mock<IOptions<AdminPermissionOptions>>, Mock<IPermissionRepository>, Mock<ITokenContext>
, GroupController) SetupEnvironment() {
var mockGroups = new Mock<IGroupLogic>();
var mockPermissions = new Mock<IOptions<AdminPermissionOptions>>();
var mockPerms = new Mock<IPermissionRepository>();
var mockContext = new Mock<ITokenContext>();
var options = new AdminPermissionOptions();
mockPermissions.Setup(o => o.Value).Returns(options);
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(true);
var controller = new GroupController(mockPermissions.Object, mockPerms.Object, mockContext.Object,
mockGroups.Object);
return (mockGroups, mockPermissions, mockPerms, mockContext, controller);
}
[Fact]
public async Task GetGroups_ShouldReturnUnauthorized_WhenUserIsNotAuthorized() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result = await controller.GetGroups();
// Assert
Assert.IsType<UnauthorizedResult>(result.Result);
}
[Fact]
public async Task GetGroups_ShouldReturnGroups_WhenUserIsAuthorized() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
var groups = new List<PermissionGroup> { new PermissionGroup { Name = "testgroup" } };
mockGroups.Setup(g => g.GetGroups()).ReturnsAsync(LogicResult<IList<PermissionGroup>>.Ok(groups));
// Act
var result = await controller.GetGroups();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var returnedGroups = Assert.IsAssignableFrom<IList<PermissionGroup>>(okResult.Value);
Assert.Single(returnedGroups);
Assert.Equal("testgroup", returnedGroups.First().Name);
}
[Fact]
public async Task GetDefaultGroups_ShouldReturnUnauthorized_WhenUserIsNotAuthorized() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result = await controller.GetDefaultGroups();
// Assert
Assert.IsType<UnauthorizedResult>(result.Result);
}
[Fact]
public async Task GetDefaultGroups_ShouldReturnGroups_WhenUserIsAuthorized() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
var groups = new List<PermissionGroup> { new PermissionGroup { Name = "defaultgroup" } };
mockGroups.Setup(g => g.GetDefaultGroups()).ReturnsAsync(LogicResult<IList<PermissionGroup>>.Ok(groups));
// Act
var result = await controller.GetDefaultGroups();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var returnedGroups = Assert.IsAssignableFrom<IList<PermissionGroup>>(okResult.Value);
Assert.Single(returnedGroups);
Assert.Equal("defaultgroup", returnedGroups.First().Name);
}
[Fact]
public async Task GetUserGroups_ShouldReturnUnauthorized_WhenUserIsNotAuthorized() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result = await controller.GetUserGroups("valid-user-id");
// Assert
Assert.IsType<UnauthorizedResult>(result.Result);
}
[Fact]
public async Task GetUserGroups_ShouldReturnGroups_WhenUserIsAuthorized() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(true);
var groups = new List<PermissionGroup> { new PermissionGroup { Name = "usergroup" } };
mockGroups.Setup(g => g.GetUserGroups(It.IsAny<string>()))
.ReturnsAsync(LogicResult<IList<PermissionGroup>>.Ok(groups));
// Act
var result = await controller.GetUserGroups("valid-user-id");
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var returnedGroups = Assert.IsAssignableFrom<IList<PermissionGroup>>(okResult.Value);
Assert.Single(returnedGroups);
Assert.Equal("usergroup", returnedGroups.First().Name);
}
[Fact]
public async Task GetGroup_ShouldReturnUnauthorized_WhenUserIsNotAuthorized() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result = await controller.GetGroup("group-name");
// Assert
Assert.IsType<UnauthorizedResult>(result.Result);
}
[Fact]
public async Task GetGroup_ShouldReturnGroup_WhenUserIsAuthorized() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(true);
var group = new PermissionGroup { Name = "group-name" };
mockGroups.Setup(g => g.GetGroup(It.IsAny<string>())).ReturnsAsync(LogicResult<PermissionGroup>.Ok(group));
// Act
var result = await controller.GetGroup("group-name");
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var returnedGroup = Assert.IsType<PermissionGroup>(okResult.Value);
Assert.Equal("group-name", returnedGroup.Name);
}
[Fact]
public async Task CreateGroup_ShouldReturnUnauthorized_WhenUserIsNotAuthorized() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result = await controller.CreateGroup(new PermissionGroup { Name = "newgroup" });
// Assert
Assert.IsType<UnauthorizedResult>(result.Result);
}
[Fact]
public async Task CreateGroup_ShouldReturnGroup_WhenUserIsAuthorized() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(true);
var group = new PermissionGroup { Name = "newgroup" };
mockGroups.Setup(g => g.CreateGroup(It.IsAny<PermissionGroup>()))
.ReturnsAsync(LogicResult<PermissionGroup>.Ok(group));
// Act
var result = await controller.CreateGroup(new PermissionGroup { Name = "newgroup" });
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var createdGroup = Assert.IsType<PermissionGroup>(okResult.Value);
Assert.Equal("newgroup", createdGroup.Name);
}
[Fact]
public async Task UpdateGroup_ShouldReturnUnauthorized_WhenUserIsNotAuthorized() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result = await controller.UpdateGroup(new PermissionGroup { Name = "updategroup" });
// Assert
Assert.IsType<UnauthorizedResult>(result.Result);
}
[Fact]
public async Task UpdateGroup_ShouldReturnGroup_WhenUserIsAuthorized() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(true);
var group = new PermissionGroup { Name = "updategroup" };
mockGroups.Setup(g => g.UpdateGroup(It.IsAny<PermissionGroup>()))
.ReturnsAsync(LogicResult<PermissionGroup>.Ok(group));
// Act
var result = await controller.UpdateGroup(new PermissionGroup { Name = "updategroup" });
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var updatedGroup = Assert.IsType<PermissionGroup>(okResult.Value);
Assert.Equal("updategroup", updatedGroup.Name);
}
[Fact]
public async Task DeleteGroup_ShouldReturnUnauthorized_WhenUserIsNotAuthorized() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result = await controller.DeleteGroup("group-name");
// Assert
Assert.IsType<UnauthorizedResult>(result);
}
[Fact]
public async Task DeleteGroup_ShouldReturnOk_WhenUserIsAuthorized() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(true);
mockGroups.Setup(g => g.DeleteGroup(It.IsAny<string>())).ReturnsAsync(LogicResult.Ok());
// Act
var result = await controller.DeleteGroup("group-name");
// Assert
Assert.IsType<OkResult>(result);
}
[Fact]
public async Task GetUserGroups_ShouldReturnBadRequest_WhenUserIdIsInvalid() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockGroups.Setup(g => g.GetUserGroups(It.IsAny<string>()))
.ReturnsAsync(LogicResult<IList<PermissionGroup>>.BadRequest("Invalid user id"));
// Act
var result = await controller.GetUserGroups("invalid-user-id");
// Assert
var badRequestResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.BadRequest, badRequestResult.StatusCode);
Assert.Equal("Invalid user id", badRequestResult.Value);
}
[Fact]
public async Task GetUserGroups_ShouldReturnNotFound_WhenUserDoesNotExist() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockGroups.Setup(g => g.GetUserGroups(It.IsAny<string>()))
.ReturnsAsync(LogicResult<IList<PermissionGroup>>.NotFound("That user does not exist"));
// Act
var result = await controller.GetUserGroups("nonexistent-user-id");
// Assert
var notFoundResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.NotFound, notFoundResult.StatusCode);
Assert.Equal("That user does not exist", notFoundResult.Value);
}
[Fact]
public async Task GetGroup_ShouldReturnNotFound_WhenGroupDoesNotExist() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockGroups.Setup(g => g.GetGroup(It.IsAny<string>()))
.ReturnsAsync(LogicResult<PermissionGroup>.NotFound("That group does not exist"));
// Act
var result = await controller.GetGroup("nonexistent-group-name");
// Assert
var notFoundResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.NotFound, notFoundResult.StatusCode);
Assert.Equal("That group does not exist", notFoundResult.Value);
}
[Fact]
public async Task CreateGroup_ShouldReturnBadRequest_WhenGroupIsNull() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockGroups.Setup(g => g.CreateGroup(It.IsAny<PermissionGroup>()))
.ReturnsAsync(LogicResult<PermissionGroup>.BadRequest("Provide a group"));
// Act
var result = await controller.CreateGroup(null);
// Assert
var badRequestResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.BadRequest, badRequestResult.StatusCode);
Assert.Equal("Provide a group", badRequestResult.Value);
}
[Fact]
public async Task CreateGroup_ShouldReturnBadRequest_WhenGroupNameIsInvalid() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockGroups.Setup(g => g.CreateGroup(It.IsAny<PermissionGroup>()))
.ReturnsAsync(LogicResult<PermissionGroup>.BadRequest("Group names must start with 'group.'"));
// Act
var result = await controller.CreateGroup(new PermissionGroup { Name = "invalidgroupname" });
// Assert
var badRequestResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.BadRequest, badRequestResult.StatusCode);
Assert.Equal("Group names must start with 'group.'", badRequestResult.Value);
}
[Fact]
public async Task CreateGroup_ShouldReturnConflict_WhenGroupAlreadyExists() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockGroups.Setup(g => g.CreateGroup(It.IsAny<PermissionGroup>()))
.ReturnsAsync(LogicResult<PermissionGroup>.Conflict("That group already exists"));
// Act
var result = await controller.CreateGroup(new PermissionGroup { Name = "group.exists" });
// Assert
var conflictResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.Conflict, conflictResult.StatusCode);
Assert.Equal("That group already exists", conflictResult.Value);
}
[Fact]
public async Task UpdateGroup_ShouldReturnNotFound_WhenGroupDoesNotExist() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockGroups.Setup(g => g.UpdateGroup(It.IsAny<PermissionGroup>())).ReturnsAsync(LogicResult<PermissionGroup>.NotFound("That group does not exist"));
// Act
var result = await controller.UpdateGroup(new PermissionGroup { Name = "nonexistent-group-name" });
// Assert
var notFoundResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.NotFound, notFoundResult.StatusCode);
Assert.Equal("That group does not exist", notFoundResult.Value);
}
[Fact]
public async Task DeleteGroup_ShouldReturnNotFound_WhenGroupDoesNotExist() {
// Arrange
var (mockGroups, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockGroups.Setup(g => g.DeleteGroup(It.IsAny<string>())).ReturnsAsync(LogicResult.NotFound("That group does not exist"));
// Act
var result = await controller.DeleteGroup("nonexistent-group-name");
// Assert
var notFoundResult = Assert.IsType<ObjectResult>(result);
Assert.Equal((int)HttpStatusCode.NotFound, notFoundResult.StatusCode);
Assert.Equal("That group does not exist", notFoundResult.Value);
}
}

View File

@@ -0,0 +1,177 @@
using HopFrame.Api.Controller;
using HopFrame.Api.Models;
using HopFrame.Security.Authentication.OpenID;
using HopFrame.Security.Authentication.OpenID.Models;
using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moq;
namespace HopFrame.Tests.Api.Controllers;
public class OpenIdControllerTests {
private (Mock<IOpenIdAccessor>, OpenIdController) SetupEnvironment(out HttpContext httpContext) {
var mockAccessor = new Mock<IOpenIdAccessor>();
var controller = new OpenIdController(mockAccessor.Object);
httpContext = new DefaultHttpContext();
controller.ControllerContext = new ControllerContext {
HttpContext = httpContext
};
return (mockAccessor, controller);
}
[Fact]
public async Task RedirectToProvider_ShouldRedirect_WhenPerformRedirectIsTrue() {
// Arrange
var (mockAccessor, controller) = SetupEnvironment(out _);
var uri = "https://example.com/auth";
mockAccessor.Setup(a => a.ConstructAuthUri(It.IsAny<string>())).ReturnsAsync(uri);
// Act
var result = await controller.RedirectToProvider("https://redirectafter.com", 1);
// Assert
var redirectResult = Assert.IsType<RedirectResult>(result);
Assert.Equal(uri, redirectResult.Url);
}
[Fact]
public async Task RedirectToProvider_ShouldReturnOk_WhenPerformRedirectIsFalse() {
// Arrange
var (mockAccessor, controller) = SetupEnvironment(out _);
var uri = "https://example.com/auth";
mockAccessor.Setup(a => a.ConstructAuthUri(It.IsAny<string>())).ReturnsAsync(uri);
// Act
var result = await controller.RedirectToProvider("https://redirectafter.com", 0);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var singleValueResult = Assert.IsType<SingleValueResult<string>>(okResult.Value);
Assert.Equal(uri, singleValueResult.Value);
}
[Fact]
public async Task Callback_ShouldReturnBadRequest_WhenAuthorizationCodeIsMissing() {
// Arrange
var (_, controller) = SetupEnvironment(out _);
// Act
var result = await controller.Callback(string.Empty, "state");
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.Equal("Authorization code is missing", badRequestResult.Value);
}
[Fact]
public async Task Callback_ShouldReturnForbidden_WhenAuthorizationCodeIsNotValid() {
// Arrange
var (mockAccessor, controller) = SetupEnvironment(out _);
mockAccessor.Setup(a => a.RequestToken(It.IsAny<string>())).ReturnsAsync((OpenIdToken)null);
// Act
var result = await controller.Callback("invalid_code", "state");
// Assert
var forbidResult = Assert.IsType<ForbidResult>(result);
Assert.Equal("Authorization code is not valid", forbidResult.AuthenticationSchemes.First());
}
[Fact]
public async Task Callback_ShouldReturnOk_WhenStateIsNull() {
// Arrange
var (mockAccessor, controller) = SetupEnvironment(out _);
var token = new OpenIdToken { AccessToken = "valid_token" };
mockAccessor.Setup(a => a.RequestToken(It.IsAny<string>())).ReturnsAsync(token);
// Act
var result = await controller.Callback("valid_code", null);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var singleValueResult = Assert.IsType<SingleValueResult<string>>(okResult.Value);
Assert.Equal("valid_token", singleValueResult.Value);
}
[Fact]
public async Task Callback_ShouldRedirect_WhenStateIsProvided() {
// Arrange
var (mockAccessor, controller) = SetupEnvironment(out _);
var token = new OpenIdToken { AccessToken = "valid_token" };
mockAccessor.Setup(a => a.RequestToken(It.IsAny<string>())).ReturnsAsync(token);
// Act
var result = await controller.Callback("valid_code", "https://redirect.com/{token}");
// Assert
var redirectResult = Assert.IsType<RedirectResult>(result);
Assert.Equal("https://redirect.com/valid_token", redirectResult.Url);
}
[Fact]
public async Task Refresh_ShouldReturnBadRequest_WhenRefreshTokenNotProvided() {
// Arrange
var (_, controller) = SetupEnvironment(out _);
// Act
var result = await controller.Refresh();
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.Equal("Refresh token not provided", badRequestResult.Value);
}
[Fact]
public async Task Refresh_ShouldReturnConflict_WhenRefreshTokenNotValid() {
// Arrange
var (mockAccessor, controller) = SetupEnvironment(out var httpContext);
var cookies = new Mock<IRequestCookieCollection>();
cookies
.SetupGet(c => c[ITokenContext.RefreshTokenType])
.Returns("invalid_token");
httpContext.Request.Cookies = cookies.Object;
mockAccessor.Setup(a => a.RefreshAccessToken(It.IsAny<string>())).ReturnsAsync((OpenIdToken)null);
// Act
var result = await controller.Refresh();
// Assert
var conflictResult = Assert.IsType<ConflictObjectResult>(result);
Assert.Equal("Refresh token not valid", conflictResult.Value);
}
[Fact]
public async Task Refresh_ShouldReturnOk_WhenRefreshTokenIsValid() {
// Arrange
var (mockAccessor, controller) = SetupEnvironment(out var httpContext);
var cookies = new Mock<IRequestCookieCollection>();
cookies
.SetupGet(c => c[ITokenContext.RefreshTokenType])
.Returns("valid_token");
httpContext.Request.Cookies = cookies.Object;
var token = new OpenIdToken { AccessToken = "new_access_token", RefreshToken = "new_refresh_token", ExpiresIn = 3600 };
mockAccessor.Setup(a => a.RefreshAccessToken(It.IsAny<string>())).ReturnsAsync(token);
// Act
var result = await controller.Refresh();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var singleValueResult = Assert.IsType<SingleValueResult<string>>(okResult.Value);
Assert.Equal("new_access_token", singleValueResult.Value);
}
[Fact]
public void Logout_ShouldReturnOk() {
// Arrange
var (_, controller) = SetupEnvironment(out _);
// Act
var result = controller.Logout();
// Assert
Assert.IsType<OkResult>(result);
}
}

View File

@@ -0,0 +1,514 @@
using System.Net;
using HopFrame.Api.Controller;
using HopFrame.Api.Logic;
using HopFrame.Api.Models;
using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using HopFrame.Security.Authorization;
using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Moq;
namespace HopFrame.Tests.Api.Controllers;
public class UserControllerTests {
private (Mock<IUserLogic>, Mock<IOptions<AdminPermissionOptions>>, Mock<IPermissionRepository>, Mock<ITokenContext>, UserController) SetupEnvironment() {
var mockLogic = new Mock<IUserLogic>();
var mockPermissions = new Mock<IOptions<AdminPermissionOptions>>();
var mockPerms = new Mock<IPermissionRepository>();
var mockContext = new Mock<ITokenContext>();
var options = new AdminPermissionOptions();
mockPermissions.Setup(o => o.Value).Returns(options);
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(true);
mockContext
.Setup(c => c.User)
.Returns(new User { Id = Guid.NewGuid(), Username = "user" });
var controller =
new UserController(mockPermissions.Object, mockPerms.Object, mockContext.Object, mockLogic.Object);
return (mockLogic, mockPermissions, mockPerms, mockContext, controller);
}
[Fact]
public async Task GetUsers_ShouldReturnUnauthorized_WhenUserIsNotAuthorized() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result = await controller.GetUsers();
// Assert
Assert.IsType<UnauthorizedResult>(result.Result);
}
[Fact]
public async Task GetUsers_ShouldReturnUsers_WhenUserIsAuthorized() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
var users = new List<User> { new User { Username = "testuser" } };
mockLogic.Setup(l => l.GetUsers()).ReturnsAsync(LogicResult<IList<User>>.Ok(users));
// Act
var result = await controller.GetUsers();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var returnedUsers = Assert.IsAssignableFrom<IList<User>>(okResult.Value);
Assert.Single(returnedUsers);
Assert.Equal("testuser", returnedUsers.First().Username);
}
[Fact]
public async Task GetUsers_Logic_ShouldReturnOk() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
var users = new List<User> { new User { Username = "testuser" } };
mockLogic.Setup(l => l.GetUsers()).ReturnsAsync(LogicResult<IList<User>>.Ok(users));
// Act
var result = await controller.GetUsers();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var returnedUsers = Assert.IsAssignableFrom<IList<User>>(okResult.Value);
Assert.Single(returnedUsers);
Assert.Equal("testuser", returnedUsers.First().Username);
}
[Fact]
public async Task GetUsers_Logic_ShouldReturnNotFound() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.GetUsers()).ReturnsAsync(LogicResult<IList<User>>.NotFound("No users found"));
// Act
var result = await controller.GetUsers();
// Assert
var notFoundResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.NotFound, notFoundResult.StatusCode);
Assert.Equal("No users found", notFoundResult.Value);
}
[Fact]
public async Task GetUser_Logic_ShouldReturnOk() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
var user = new User { Username = "testuser" };
mockLogic.Setup(l => l.GetUser(It.IsAny<string>())).ReturnsAsync(LogicResult<User>.Ok(user));
// Act
var result = await controller.GetUser("valid-user-id");
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var returnedUser = Assert.IsType<User>(okResult.Value);
Assert.Equal("testuser", returnedUser.Username);
}
[Fact]
public async Task GetUser_Logic_ShouldReturnBadRequest() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.GetUser(It.IsAny<string>()))
.ReturnsAsync(LogicResult<User>.BadRequest("Invalid user id"));
// Act
var result = await controller.GetUser("invalid-user-id");
// Assert
var badRequestResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.BadRequest, badRequestResult.StatusCode);
Assert.Equal("Invalid user id", badRequestResult.Value);
}
[Fact]
public async Task GetUser_Logic_ShouldReturnUnauthorized() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result = await controller.GetUser("valid-user-id");
// Assert
Assert.IsType<UnauthorizedResult>(result.Result);
}
[Fact]
public async Task GetUser_Logic_ShouldReturnNotFound() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.GetUser(It.IsAny<string>())).ReturnsAsync(LogicResult<User>.NotFound("User not found"));
// Act
var result = await controller.GetUser("invalid-user-id");
// Assert
var notFoundResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.NotFound, notFoundResult.StatusCode);
Assert.Equal("User not found", notFoundResult.Value);
}
[Fact]
public async Task GetUserByUsername_Logic_ShouldReturnOk() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
var user = new User { Username = "testuser" };
mockLogic.Setup(l => l.GetUserByUsername(It.IsAny<string>())).ReturnsAsync(LogicResult<User>.Ok(user));
// Act
var result = await controller.GetUserByUsername("valid-username");
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var returnedUser = Assert.IsType<User>(okResult.Value);
Assert.Equal("testuser", returnedUser.Username);
}
[Fact]
public async Task GetUserByUsername_Logic_ShouldReturnNotFound() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.GetUserByUsername(It.IsAny<string>()))
.ReturnsAsync(LogicResult<User>.NotFound("User not found"));
// Act
var result = await controller.GetUserByUsername("invalid-username");
// Assert
var notFoundResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.NotFound, notFoundResult.StatusCode);
Assert.Equal("User not found", notFoundResult.Value);
}
[Fact]
public async Task GetUserByEmail_Logic_ShouldReturnOk() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
var user = new User { Username = "testuser" };
mockLogic.Setup(l => l.GetUserByEmail(It.IsAny<string>())).ReturnsAsync(LogicResult<User>.Ok(user));
// Act
var result = await controller.GetUserByEmail("valid-email@example.com");
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var returnedUser = Assert.IsType<User>(okResult.Value);
Assert.Equal("testuser", returnedUser.Username);
}
[Fact]
public async Task GetUserByEmail_Logic_ShouldReturnNotFound() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.GetUserByEmail(It.IsAny<string>()))
.ReturnsAsync(LogicResult<User>.NotFound("User not found"));
// Act
var result = await controller.GetUserByEmail("invalid-email@example.com");
// Assert
var notFoundResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.NotFound, notFoundResult.StatusCode);
Assert.Equal("User not found", notFoundResult.Value);
}
[Fact]
public async Task CreateUser_Logic_ShouldReturnOk() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
var newUser = new User { Username = "newuser" };
mockLogic.Setup(l => l.CreateUser(It.IsAny<UserCreator>())).ReturnsAsync(LogicResult<User>.Ok(newUser));
// Act
var result = await controller.CreateUser(new UserCreator
{ Username = "newuser", Email = "newuser@example.com", Password = "password" });
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var createdUser = Assert.IsType<User>(okResult.Value);
Assert.Equal("newuser", createdUser.Username);
}
[Fact]
public async Task CreateUser_Logic_ShouldReturnConflict() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.CreateUser(It.IsAny<UserCreator>()))
.ReturnsAsync(LogicResult<User>.Conflict("User already exists"));
// Act
var result = await controller.CreateUser(new UserCreator
{ Username = "existinguser", Email = "existinguser@example.com", Password = "password" });
// Assert
var conflictResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.Conflict, conflictResult.StatusCode);
Assert.Equal("User already exists", conflictResult.Value);
}
[Fact]
public async Task UpdateUser_Logic_ShouldReturnOk() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
var updatedUser = new User { Username = "updateduser" };
mockLogic.Setup(l => l.UpdateUser(It.IsAny<string>(), It.IsAny<User>()))
.ReturnsAsync(LogicResult<User>.Ok(updatedUser));
// Act
var result =
await controller.UpdateUser("valid-user-id", new User { Id = Guid.NewGuid(), Username = "updateduser" });
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var returnedUser = Assert.IsType<User>(okResult.Value);
Assert.Equal("updateduser", returnedUser.Username);
}
[Fact]
public async Task UpdateUser_Logic_ShouldReturnBadRequest() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.UpdateUser(It.IsAny<string>(), It.IsAny<User>()))
.ReturnsAsync(LogicResult<User>.BadRequest("Invalid user id"));
// Act
var result = await controller.UpdateUser("invalid-user-id",
new User { Id = Guid.NewGuid(), Username = "updateduser" });
// Assert
var badRequestResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.BadRequest, badRequestResult.StatusCode);
Assert.Equal("Invalid user id", badRequestResult.Value);
}
[Fact]
public async Task UpdateUser_Logic_ShouldReturnNotFound() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.UpdateUser(It.IsAny<string>(), It.IsAny<User>()))
.ReturnsAsync(LogicResult<User>.NotFound("User not found"));
// Act
var result = await controller.UpdateUser("nonexistent-user-id",
new User { Id = Guid.NewGuid(), Username = "nonexistentuser" });
// Assert
var notFoundResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.NotFound, notFoundResult.StatusCode);
Assert.Equal("User not found", notFoundResult.Value);
}
[Fact]
public async Task UpdateUser_Logic_ShouldReturnConflict() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.UpdateUser(It.IsAny<string>(), It.IsAny<User>()))
.ReturnsAsync(LogicResult<User>.Conflict("Conflict in user update"));
// Act
var result = await controller.UpdateUser("conflict-user-id",
new User { Id = Guid.NewGuid(), Username = "conflictuser" });
// Assert
var conflictResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal((int)HttpStatusCode.Conflict, conflictResult.StatusCode);
Assert.Equal("Conflict in user update", conflictResult.Value);
}
[Fact]
public async Task DeleteUser_Logic_ShouldReturnOk() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.DeleteUser(It.IsAny<string>())).ReturnsAsync(LogicResult.Ok());
// Act
var result = await controller.DeleteUser("valid-user-id");
// Assert
var okResult = Assert.IsType<OkResult>(result);
}
[Fact]
public async Task DeleteUser_Logic_ShouldReturnNotFound() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.DeleteUser(It.IsAny<string>())).ReturnsAsync(LogicResult.NotFound("User not found"));
// Act
var result = await controller.DeleteUser("invalid-user-id");
// Assert
var notFoundResult = Assert.IsType<ObjectResult>(result);
Assert.Equal((int)HttpStatusCode.NotFound, notFoundResult.StatusCode);
Assert.Equal("User not found", notFoundResult.Value);
}
[Fact]
public async Task DeleteUser_Logic_ShouldReturnBadRequest() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.DeleteUser(It.IsAny<string>())).ReturnsAsync(LogicResult.BadRequest("Invalid user id"));
// Act
var result = await controller.DeleteUser("invalid-user-id");
// Assert
var badRequestResult = Assert.IsType<ObjectResult>(result);
Assert.Equal((int)HttpStatusCode.BadRequest, badRequestResult.StatusCode);
Assert.Equal("Invalid user id", badRequestResult.Value);
}
[Fact]
public async Task ChangePassword_Logic_ShouldReturnOk() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.UpdatePassword(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(LogicResult.Ok());
// Act
var result = await controller.ChangePassword("valid-user-id",
new UserPasswordChange { OldPassword = "oldPassword", NewPassword = "newPassword" });
// Assert
var okResult = Assert.IsType<OkResult>(result);
}
[Fact]
public async Task ChangePassword_Logic_ShouldReturnBadRequest() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.UpdatePassword(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(LogicResult.BadRequest("Invalid user id"));
// Act
var result = await controller.ChangePassword("invalid-user-id",
new UserPasswordChange { OldPassword = "oldPassword", NewPassword = "newPassword" });
// Assert
var badRequestResult = Assert.IsType<ObjectResult>(result);
Assert.Equal((int)HttpStatusCode.BadRequest, badRequestResult.StatusCode);
Assert.Equal("Invalid user id", badRequestResult.Value);
}
[Fact]
public async Task ChangePassword_Logic_ShouldReturnNotFound() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.UpdatePassword(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(LogicResult.NotFound("User not found"));
// Act
var result = await controller.ChangePassword("nonexistent-user-id",
new UserPasswordChange { OldPassword = "oldPassword", NewPassword = "newPassword" });
// Assert
var notFoundResult = Assert.IsType<ObjectResult>(result);
Assert.Equal((int)HttpStatusCode.NotFound, notFoundResult.StatusCode);
Assert.Equal("User not found", notFoundResult.Value);
}
[Fact]
public async Task ChangePassword_Logic_ShouldReturnConflict() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockLogic.Setup(l => l.UpdatePassword(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(LogicResult.Conflict("Old password is not correct"));
// Act
var result = await controller.ChangePassword("conflict-user-id",
new UserPasswordChange { OldPassword = "wrongOldPassword", NewPassword = "newPassword" });
// Assert
var conflictResult = Assert.IsType<ObjectResult>(result);
Assert.Equal((int)HttpStatusCode.Conflict, conflictResult.StatusCode);
Assert.Equal("Old password is not correct", conflictResult.Value);
}
[Fact]
public async Task GetUserByUsername_Logic_ShouldReturnUnauthorized() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result = await controller.GetUserByUsername("valid-username");
// Assert
Assert.IsType<UnauthorizedResult>(result.Result);
}
[Fact]
public async Task GetUserByEmail_Logic_ShouldReturnUnauthorized() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result = await controller.GetUserByEmail("valid-email@example.com");
// Assert
Assert.IsType<UnauthorizedResult>(result.Result);
}
[Fact]
public async Task CreateUser_Logic_ShouldReturnUnauthorized() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result = await controller.CreateUser(new UserCreator
{ Username = "newuser", Email = "newuser@example.com", Password = "password" });
// Assert
Assert.IsType<UnauthorizedResult>(result.Result);
}
[Fact]
public async Task UpdateUser_Logic_ShouldReturnUnauthorized() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result =
await controller.UpdateUser("valid-user-id", new User { Id = Guid.NewGuid(), Username = "updateduser" });
// Assert
Assert.IsType<UnauthorizedResult>(result.Result);
}
[Fact]
public async Task DeleteUser_Logic_ShouldReturnUnauthorized() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result = await controller.DeleteUser("valid-user-id");
// Assert
Assert.IsType<UnauthorizedResult>(result);
}
[Fact]
public async Task ChangePassword_Logic_ShouldReturnUnauthorized() {
// Arrange
var (mockLogic, mockPermissions, mockPerms, mockContext, controller) = SetupEnvironment();
mockPerms.Setup(p => p.HasPermission(It.IsAny<User>(), It.IsAny<string>())).ReturnsAsync(false);
// Act
var result = await controller.ChangePassword("valid-user-id",
new UserPasswordChange { OldPassword = "oldPassword", NewPassword = "newPassword" });
// Assert
Assert.IsType<UnauthorizedResult>(result);
}
}