Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c599dfca8c | |||
| f7bb16b85c | |||
| e530cf735a | |||
| 166134c6d8 | |||
| e613fa66e3 | |||
| 99d39be9ac | |||
| b8b0d571ab | |||
| a323da829f | |||
| ef4f05f0b6 | |||
| 5c7e38aa40 | |||
| 11126e8080 | |||
| 0b9766f7db | |||
| 849ad649a8 | |||
| 3031dda710 | |||
| 73d89a241f | |||
| df68b6dbf8 | |||
| 20684ca40a |
@@ -6,14 +6,51 @@
|
||||
<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_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_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_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"><AssemblyExplorer>
|
||||
<Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" />
|
||||
<Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" />
|
||||
</AssemblyExplorer></s:String>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=027ac703_002Df1f3_002D42aa_002D9c67_002D7cbaeecdbead/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="Refresh_ShouldReturnConflict_WhenRefreshTokenNotValid" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>xUnit::25DE1510-47E5-46FF-89A4-B9F99542218E::net8.0::HopFrame.Tests.Api.Controllers.OpenIdControllerTests.Refresh_ShouldReturnConflict_WhenRefreshTokenNotValid</TestId>
|
||||
</TestAncestor>
|
||||
</SessionState></s:String>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
120
docs/api/endpoints/auth.md
Normal 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
237
docs/api/endpoints/group.md
Normal 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.
|
||||
82
docs/api/endpoints/openId.md
Normal file
82
docs/api/endpoints/openId.md
Normal 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
316
docs/api/endpoints/user.md
Normal 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.
|
||||
@@ -14,3 +14,66 @@ public sealed class UserPasswordValidation {
|
||||
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; }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```csharp
|
||||
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; }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Api.Controller;
|
||||
|
||||
[ApiController, Route("api/v1/openid")]
|
||||
public class OpenIdController(IOpenIdAccessor accessor, IOptions<OpenIdOptions> options) : ControllerBase {
|
||||
public class OpenIdController(IOpenIdAccessor accessor) : ControllerBase {
|
||||
public const string DefaultCallback = "api/v1/openid/callback";
|
||||
|
||||
[HttpGet("redirect")]
|
||||
public async Task<IActionResult> RedirectToProvider([FromQuery] string redirectAfter, [FromQuery] int performRedirect = 1) {
|
||||
var uri = await accessor.ConstructAuthUri(DefaultCallback, redirectAfter);
|
||||
var uri = await accessor.ConstructAuthUri(redirectAfter);
|
||||
|
||||
if (performRedirect == 1) {
|
||||
return Redirect(uri);
|
||||
@@ -29,22 +26,13 @@ public class OpenIdController(IOpenIdAccessor accessor, IOptions<OpenIdOptions>
|
||||
return BadRequest("Authorization code is missing");
|
||||
}
|
||||
|
||||
var token = await accessor.RequestToken(code, DefaultCallback);
|
||||
var token = await accessor.RequestToken(code);
|
||||
|
||||
if (token is null) {
|
||||
return Forbid("Authorization code is not valid");
|
||||
}
|
||||
|
||||
Response.Cookies.Append(ITokenContext.AccessTokenType, token.AccessToken, new CookieOptions {
|
||||
MaxAge = TimeSpan.FromSeconds(token.ExpiresIn),
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
Response.Cookies.Append(ITokenContext.RefreshTokenType, token.RefreshToken, new CookieOptions {
|
||||
MaxAge = options.Value.RefreshToken.ConstructTimeSpan,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
accessor.SetAuthenticationCookies(token);
|
||||
|
||||
if (string.IsNullOrEmpty(state)) {
|
||||
return Ok(new SingleValueResult<string>(token.AccessToken));
|
||||
@@ -63,21 +51,16 @@ public class OpenIdController(IOpenIdAccessor accessor, IOptions<OpenIdOptions>
|
||||
var token = await accessor.RefreshAccessToken(refreshToken);
|
||||
|
||||
if (token is null)
|
||||
return NotFound("Refresh token not valid");
|
||||
return Conflict("Refresh token not valid");
|
||||
|
||||
Response.Cookies.Append(ITokenContext.AccessTokenType, token.AccessToken, new CookieOptions {
|
||||
MaxAge = TimeSpan.FromSeconds(token.ExpiresIn),
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
accessor.SetAuthenticationCookies(token);
|
||||
|
||||
return Ok(new SingleValueResult<string>(token.AccessToken));
|
||||
}
|
||||
|
||||
[HttpDelete("logout")]
|
||||
public IActionResult Logout() {
|
||||
Response.Cookies.Delete(ITokenContext.RefreshTokenType);
|
||||
Response.Cookies.Delete(ITokenContext.AccessTokenType);
|
||||
accessor.Logout();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using HopFrame.Api.Controller;
|
||||
using HopFrame.Api.Logic;
|
||||
using HopFrame.Api.Logic.Implementation;
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -17,40 +19,52 @@ public static class ServiceCollectionExtensions {
|
||||
/// </summary>
|
||||
/// <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="config">Configuration for how the HopFrame services are set up</param>
|
||||
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
||||
public static void AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase {
|
||||
var controllers = new List<Type> { typeof(UserController), typeof(GroupController) };
|
||||
public static void AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration, HopFrameApiModuleConfig config = null) where TDbContext : HopDbContextBase {
|
||||
config ??= new();
|
||||
|
||||
var controllers = new List<Type>();
|
||||
|
||||
if (config.ExposeModelEndpoints)
|
||||
controllers.AddRange([typeof(UserController), typeof(GroupController)]);
|
||||
|
||||
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));
|
||||
|
||||
if (configuration.GetValue<bool>("HopFrame:Authentication:OpenID:Enabled"))
|
||||
if (configuration.GetValue<bool>("HopFrame:Authentication:OpenID:Enabled") && config.ExposeAuthEndpoints) {
|
||||
IOpenIdAccessor.DefaultCallback = OpenIdController.DefaultCallback;
|
||||
controllers.Add(typeof(OpenIdController));
|
||||
}
|
||||
|
||||
AddHopFrameNoEndpoints<TDbContext>(services, configuration);
|
||||
AddHopFrameNoEndpoints<TDbContext>(services, configuration, config);
|
||||
services.AddMvcCore().UseSpecificControllers(controllers.ToArray());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds all HopFrame services to the application
|
||||
/// </summary>
|
||||
/// <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="config">Configuration for how the HopFrame services are set up</param>
|
||||
/// <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 => {
|
||||
var endpoints = manager.ApplicationParts.SingleOrDefault(p => p.Name == typeof(ServiceCollectionExtensions).Namespace!.Replace(".Extensions", ""));
|
||||
manager.ApplicationParts.Remove(endpoints);
|
||||
});
|
||||
|
||||
|
||||
services.AddSingleton(config);
|
||||
services.AddHopFrameRepositories<TDbContext>();
|
||||
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddScoped<IAuthLogic, AuthLogic>();
|
||||
services.AddScoped<IUserLogic, UserLogic>();
|
||||
services.AddScoped<IGroupLogic, GroupLogic>();
|
||||
|
||||
services.AddHopFrameAuthentication(configuration);
|
||||
services.AddHopFrameAuthentication(configuration, config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
8
src/HopFrame.Api/Models/HopFrameApiModuleConfig.cs
Normal file
8
src/HopFrame.Api/Models/HopFrameApiModuleConfig.cs
Normal 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;
|
||||
}
|
||||
@@ -8,6 +8,8 @@ namespace HopFrame.Database;
|
||||
/// </summary>
|
||||
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<Permission> Permissions { get; set; }
|
||||
public virtual DbSet<Token> Tokens { get; set; }
|
||||
@@ -36,4 +38,36 @@ public abstract class HopDbContextBase : DbContext {
|
||||
.WithOne(t => t.Token)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,18 @@ public class Permission {
|
||||
[ForeignKey("UserId"), JsonIgnore]
|
||||
public virtual User User { get; set; }
|
||||
|
||||
public Guid? UserId { get; set; }
|
||||
|
||||
[ForeignKey("GroupName"), JsonIgnore]
|
||||
public virtual PermissionGroup Group { get; set; }
|
||||
|
||||
[MaxLength(255)]
|
||||
public string GroupName { get; set; }
|
||||
|
||||
[ForeignKey("TokenId"), JsonIgnore]
|
||||
public virtual Token Token { get; set; }
|
||||
|
||||
public Guid? TokenId { get; set; }
|
||||
}
|
||||
|
||||
public interface IPermissionOwner;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authentication.OpenID.Implementation;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Models;
|
||||
using HopFrame.Security.Options;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -16,25 +19,48 @@ public static class HopFrameAuthenticationExtensions {
|
||||
/// <summary>
|
||||
/// Configures the WebApplication to use the authentication and authorization of the HopFrame API
|
||||
/// </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="config">Configuration for how the HopFrame services are set up</param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service, ConfigurationManager configuration) {
|
||||
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
service.AddScoped<ITokenContext, TokenContextImplementor>();
|
||||
|
||||
service.AddHttpClient();
|
||||
service.AddMemoryCache();
|
||||
service.AddScoped<IOpenIdAccessor, OpenIdAccessor>();
|
||||
|
||||
service.AddOptionsFromConfiguration<HopFrameAuthenticationOptions>(configuration);
|
||||
service.AddOptionsFromConfiguration<AdminPermissionOptions>(configuration);
|
||||
service.AddOptionsFromConfiguration<OpenIdOptions>(configuration);
|
||||
|
||||
service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
|
||||
service.AddAuthorization();
|
||||
public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection services, ConfigurationManager configuration, HopFrameConfig config = null) {
|
||||
config ??= new HopFrameConfig();
|
||||
|
||||
return service;
|
||||
services.AddSingleton(config);
|
||||
services.AddScoped(typeof(ICacheProvider), config.CacheProvider);
|
||||
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddScoped<ITokenContext, TokenContextImplementor>();
|
||||
|
||||
if (config.CacheProvider == typeof(MemoryCacheProvider))
|
||||
services.AddMemoryCache();
|
||||
|
||||
services.AddHttpClient<OpenIdAccessor>();
|
||||
services.AddScoped<IOpenIdAccessor, OpenIdAccessor>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -3,9 +3,13 @@ using HopFrame.Security.Authentication.OpenID.Models;
|
||||
namespace HopFrame.Security.Authentication.OpenID;
|
||||
|
||||
public interface IOpenIdAccessor {
|
||||
public static string DefaultCallback;
|
||||
|
||||
Task<OpenIdConfiguration> LoadConfiguration();
|
||||
Task<OpenIdToken> RequestToken(string code, string defaultCallback);
|
||||
Task<string> ConstructAuthUri(string defaultCallback, string state = null);
|
||||
Task<OpenIdToken> RequestToken(string code);
|
||||
Task<string> ConstructAuthUri(string state = null);
|
||||
Task<OpenIdIntrospection> InspectToken(string token);
|
||||
Task<OpenIdToken> RefreshAccessToken(string refreshToken);
|
||||
void SetAuthenticationCookies(OpenIdToken token);
|
||||
void Logout();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,28 @@
|
||||
using System.Text.Json;
|
||||
using HopFrame.Security.Authentication.OpenID.Models;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
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 AuthCodeCacheKey = "HopFrame:OpenID:Code:";
|
||||
private const string TokenCacheKey = "HopFrame:OpenID:Token:";
|
||||
|
||||
public async Task<OpenIdConfiguration> LoadConfiguration() {
|
||||
if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled && cache.TryGetValue(ConfigurationCacheKey, out object cachedConfiguration)) {
|
||||
return cachedConfiguration as OpenIdConfiguration;
|
||||
public Task<OpenIdConfiguration> LoadConfiguration() {
|
||||
if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled) {
|
||||
return cache.GetOrCreate(ConfigurationCacheKey, LoadConfigurationInCache);
|
||||
}
|
||||
|
||||
return LoadConfigurationInCache();
|
||||
}
|
||||
|
||||
internal async Task<OpenIdConfiguration> LoadConfigurationInCache() {
|
||||
var client = clientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, Path.Combine(options.Value.Issuer, ".well-known/openid-configuration"));
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, Path.Combine(options.Value.Issuer, ".well-known/openid-configuration").Replace("\\", "/"));
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -27,18 +31,22 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdO
|
||||
var config = await JsonSerializer.DeserializeAsync<OpenIdConfiguration>(await response.Content.ReadAsStreamAsync());
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public async Task<OpenIdToken> RequestToken(string code, string defaultCallback) {
|
||||
if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled && cache.TryGetValue(AuthCodeCacheKey + code, out object cachedToken)) {
|
||||
return cachedToken as OpenIdToken;
|
||||
public Task<OpenIdToken> RequestToken(string code) {
|
||||
if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled) {
|
||||
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 callback = options.Value.Callback ?? $"{protocol}://{accessor.HttpContext!.Request.Host.Value}/{defaultCallback}";
|
||||
var callback = options.Value.Callback ?? Path.Combine($"{protocol}://{accessor.HttpContext!.Request.Host.Value}", IOpenIdAccessor.DefaultCallback).Replace("\\", "/");
|
||||
|
||||
var configuration = await LoadConfiguration();
|
||||
|
||||
@@ -60,24 +68,28 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdO
|
||||
var token = await JsonSerializer.DeserializeAsync<OpenIdToken>(await response.Content.ReadAsStreamAsync());
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public async Task<string> ConstructAuthUri(string defaultCallback, string state = null) {
|
||||
public async Task<string> ConstructAuthUri(string state = null) {
|
||||
var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http";
|
||||
var callback = options.Value.Callback ?? $"{protocol}://{accessor.HttpContext!.Request.Host.Value}/{defaultCallback}";
|
||||
var callback = options.Value.Callback ?? Path.Combine($"{protocol}://{accessor.HttpContext!.Request.Host.Value}", IOpenIdAccessor.DefaultCallback).Replace("\\", "/");
|
||||
|
||||
var configuration = await LoadConfiguration();
|
||||
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) {
|
||||
if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled && cache.TryGetValue(TokenCacheKey + token, out object cachedToken)) {
|
||||
return cachedToken as OpenIdIntrospection;
|
||||
public Task<OpenIdIntrospection> InspectToken(string token) {
|
||||
if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled) {
|
||||
return cache.GetOrCreate(TokenCacheKey + token, () => InspectTokenInCache(token));
|
||||
}
|
||||
|
||||
|
||||
return InspectTokenInCache(token);
|
||||
}
|
||||
|
||||
internal async Task<OpenIdIntrospection> InspectTokenInCache(string token) {
|
||||
var configuration = await LoadConfiguration();
|
||||
|
||||
var client = clientFactory.CreateClient();
|
||||
@@ -96,7 +108,7 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdO
|
||||
var introspection = await JsonSerializer.DeserializeAsync<OpenIdIntrospection>(await response.Content.ReadAsStreamAsync());
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -120,4 +132,25 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdO
|
||||
|
||||
return await JsonSerializer.DeserializeAsync<OpenIdToken>(await response.Content.ReadAsStreamAsync());
|
||||
}
|
||||
|
||||
public void SetAuthenticationCookies(OpenIdToken token) {
|
||||
if (token.AccessToken is not null)
|
||||
accessor.HttpContext!.Response.Cookies.Append(ITokenContext.AccessTokenType, token.AccessToken, new CookieOptions {
|
||||
MaxAge = TimeSpan.FromSeconds(token.ExpiresIn),
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
|
||||
if (token.RefreshToken is not null)
|
||||
accessor.HttpContext!.Response.Cookies.Append(ITokenContext.RefreshTokenType, token.RefreshToken, new CookieOptions {
|
||||
MaxAge = options.Value.RefreshToken.ConstructTimeSpan,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
}
|
||||
|
||||
public void Logout() {
|
||||
accessor.HttpContext!.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
|
||||
accessor.HttpContext!.Response.Cookies.Delete(ITokenContext.AccessTokenType);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ public sealed class OpenIdOptions : OptionsFromConfiguration {
|
||||
Configuration = new() {
|
||||
Enabled = true,
|
||||
TTL = new() {
|
||||
Minutes = 10
|
||||
Hours = 24
|
||||
}
|
||||
},
|
||||
Auth = new() {
|
||||
|
||||
7
src/HopFrame.Security/Models/HopFrameConfig.cs
Normal file
7
src/HopFrame.Security/Models/HopFrameConfig.cs
Normal 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);
|
||||
}
|
||||
7
src/HopFrame.Web/Models/HopFrameWebModuleConfig.cs
Normal file
7
src/HopFrame.Web/Models/HopFrameWebModuleConfig.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using HopFrame.Security.Models;
|
||||
|
||||
namespace HopFrame.Web.Models;
|
||||
|
||||
public class HopFrameWebModuleConfig : HopFrameConfig {
|
||||
public string AdminLoginPageUri { get; set; } = "/administration/login";
|
||||
}
|
||||
@@ -8,11 +8,12 @@
|
||||
@using HopFrame.Security.Authorization
|
||||
@using HopFrame.Web.Admin.Providers
|
||||
@using HopFrame.Web.Components
|
||||
@using HopFrame.Web.Models
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.Extensions.Options
|
||||
@layout AdminLayout
|
||||
|
||||
<AuthorizedView Permission="@Options.Value.Dashboard" RedirectIfUnauthorized="/administration/login" />
|
||||
<AuthorizedView Permission="@Options.Value.Dashboard" RedirectIfUnauthorized="@ConstructRedirectUri()" />
|
||||
|
||||
<PageTitle>Admin Dashboard</PageTitle>
|
||||
|
||||
@@ -38,11 +39,16 @@
|
||||
@inject NavigationManager Navigator
|
||||
@inject IAdminPagesProvider Pages
|
||||
@inject IOptions<AdminPermissionOptions> Options
|
||||
@inject HopFrameWebModuleConfig Config
|
||||
|
||||
@code {
|
||||
|
||||
public void NavigateTo(string url) {
|
||||
Navigator.NavigateTo("administration/" + url, true);
|
||||
Navigator.NavigateTo("/administration/" + url, true);
|
||||
}
|
||||
|
||||
public string ConstructRedirectUri() {
|
||||
return Config.AdminLoginPageUri + "?redirect=/administration";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -65,6 +65,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.NavigateTo(string.IsNullOrEmpty(RedirectAfter) ? DefaultRedirect : "/administration/" + RedirectAfter, true);
|
||||
Navigator.NavigateTo(string.IsNullOrEmpty(RedirectAfter) ? DefaultRedirect : RedirectAfter, true);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
@using HopFrame.Security.Claims
|
||||
@using HopFrame.Web.Admin
|
||||
@using HopFrame.Web.Components
|
||||
@using HopFrame.Web.Models
|
||||
|
||||
<PageTitle>@_pageData.Title</PageTitle>
|
||||
<AuthorizedView Permission="@_pageData.Permissions.Read" RedirectIfUnauthorized="@GenerateRedirectString()" />
|
||||
@@ -107,6 +108,7 @@
|
||||
@inject IPermissionRepository Permissions
|
||||
@inject SweetAlertService Alerts
|
||||
@inject NavigationManager Navigator
|
||||
@inject HopFrameWebModuleConfig Config
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
@@ -251,6 +253,6 @@
|
||||
}
|
||||
|
||||
private string GenerateRedirectString() {
|
||||
return "/administration/login?redirect=" + _pageData?.Url;
|
||||
return Config.AdminLoginPageUri + "?redirect=/administration/" + _pageData?.Url;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using CurrieTechnologies.Razor.SweetAlert2;
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Web.Admin;
|
||||
using HopFrame.Web.Models;
|
||||
using HopFrame.Web.Services;
|
||||
using HopFrame.Web.Services.Implementation;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
@@ -12,18 +13,19 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
namespace HopFrame.Web;
|
||||
|
||||
public static class ServiceCollectionExtensions {
|
||||
public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase {
|
||||
services.AddHttpClient();
|
||||
public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration, HopFrameWebModuleConfig config = null) where TDbContext : HopDbContextBase {
|
||||
config ??= new HopFrameWebModuleConfig();
|
||||
services.AddHopFrameRepositories<TDbContext>();
|
||||
services.AddScoped<IAuthService, AuthService>();
|
||||
services.AddTransient<AuthMiddleware>();
|
||||
services.AddAdminContext<HopAdminContext>();
|
||||
services.AddSingleton(config);
|
||||
|
||||
// Component library's
|
||||
services.AddSweetAlert2();
|
||||
services.AddBlazorStrap();
|
||||
|
||||
services.AddHopFrameAuthentication(configuration);
|
||||
services.AddHopFrameAuthentication(configuration, config);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -108,11 +108,7 @@ internal class AuthService(
|
||||
});
|
||||
}
|
||||
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, openIdToken.AccessToken, new CookieOptions {
|
||||
MaxAge = TimeSpan.FromSeconds(openIdToken.ExpiresIn),
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
accessor.SetAuthenticationCookies(openIdToken);
|
||||
return new() {
|
||||
Owner = user,
|
||||
CreatedAt = DateTime.Now,
|
||||
|
||||
@@ -2,6 +2,7 @@ using HopFrame.Api.Logic;
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Testing.Api.Models;
|
||||
@@ -68,9 +69,8 @@ public class TestController(ITokenContext userContext, DatabaseContext context,
|
||||
}
|
||||
|
||||
[HttpGet("url")]
|
||||
public async Task<ActionResult<SingleValueResult<string>>> GetUrl() {
|
||||
var protocol = Request.IsHttps ? "https" : "http";
|
||||
return Ok($"{protocol}://{Request.Host.Value}/auth/callback");
|
||||
public ActionResult<string> GetUrl() {
|
||||
return Ok(IOpenIdAccessor.DefaultCallback ?? "Not set");
|
||||
}
|
||||
|
||||
}
|
||||
369
tests/HopFrame.Tests.Api/Controllers/GroupControllerTests.cs
Normal file
369
tests/HopFrame.Tests.Api/Controllers/GroupControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
177
tests/HopFrame.Tests.Api/Controllers/OpenIdControllerTests.cs
Normal file
177
tests/HopFrame.Tests.Api/Controllers/OpenIdControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
514
tests/HopFrame.Tests.Api/Controllers/UserControllerTests.cs
Normal file
514
tests/HopFrame.Tests.Api/Controllers/UserControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ public class AdminLoginTests : TestContext {
|
||||
var password = component.Find("""input[type="password"]""");
|
||||
var submit = component.Find("button");
|
||||
|
||||
component.Instance.RedirectAfter = "testRedirect";
|
||||
component.Instance.RedirectAfter = "/administration/testRedirect";
|
||||
|
||||
// Act
|
||||
email.Change("test@example.com");
|
||||
|
||||
Reference in New Issue
Block a user