Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b51100ab6 | ||
|
|
f920a6d0a5 | ||
|
|
75595970fb | ||
|
|
27497e31a9 | ||
|
|
bdc85a1f3a |
@@ -61,7 +61,6 @@ jobs:
|
|||||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
|
JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
|
||||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
|
||||||
QA_ADMIN_PORT: ${{ vars.QA_ADMIN_PORT }}
|
QA_ADMIN_PORT: ${{ vars.QA_ADMIN_PORT }}
|
||||||
steps:
|
steps:
|
||||||
# actions/checkout@v4 is a Node-based action; this runner has no node
|
# actions/checkout@v4 is a Node-based action; this runner has no node
|
||||||
@@ -82,7 +81,6 @@ jobs:
|
|||||||
runs-on: [self-hosted, qa]
|
runs-on: [self-hosted, qa]
|
||||||
env:
|
env:
|
||||||
QA_ADMIN_PORT: ${{ vars.QA_ADMIN_PORT }}
|
QA_ADMIN_PORT: ${{ vars.QA_ADMIN_PORT }}
|
||||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
|
||||||
steps:
|
steps:
|
||||||
# actions/checkout@v4 is a Node-based action; this runner has no node
|
# actions/checkout@v4 is a Node-based action; this runner has no node
|
||||||
# in PATH, so checkout plain git instead of via marketplace action.
|
# in PATH, so checkout plain git instead of via marketplace action.
|
||||||
|
|||||||
+1
-2
@@ -3,6 +3,7 @@
|
|||||||
<File Path=".editorconfig" />
|
<File Path=".editorconfig" />
|
||||||
<File Path=".gitignore" />
|
<File Path=".gitignore" />
|
||||||
<File Path="CLAUDE.md" />
|
<File Path="CLAUDE.md" />
|
||||||
|
<File Path="docker-compose.yml" />
|
||||||
<File Path="readme.md" />
|
<File Path="readme.md" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/src/">
|
<Folder Name="/src/">
|
||||||
@@ -12,12 +13,10 @@
|
|||||||
</Project>
|
</Project>
|
||||||
<Project Path="src/api/MicCheck.Api/MicCheck.Api.csproj" />
|
<Project Path="src/api/MicCheck.Api/MicCheck.Api.csproj" />
|
||||||
<Project Path="src/MicCheck.AppHost/MicCheck.AppHost.csproj" />
|
<Project Path="src/MicCheck.AppHost/MicCheck.AppHost.csproj" />
|
||||||
<Project Path="src/MicCheck.Client/MicCheck.Client.csproj" />
|
|
||||||
<Project Path="src/MicCheck.ServiceDefaults/MicCheck.ServiceDefaults.csproj" />
|
<Project Path="src/MicCheck.ServiceDefaults/MicCheck.ServiceDefaults.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/api/MicCheck.Api.Tests.Unit/MicCheck.Api.Tests.Unit.csproj" />
|
<Project Path="tests/api/MicCheck.Api.Tests.Unit/MicCheck.Api.Tests.Unit.csproj" />
|
||||||
<Project Path="tests/api/MicCheck.Api.Tests.Integration/MicCheck.Api.Tests.Integration.csproj" />
|
<Project Path="tests/api/MicCheck.Api.Tests.Integration/MicCheck.Api.Tests.Integration.csproj" />
|
||||||
<Project Path="tests/client/MicCheck.Client.Tests.Unit/MicCheck.Client.Tests.Unit.csproj" />
|
|
||||||
</Folder>
|
</Folder>
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
+1
-1
@@ -123,7 +123,7 @@
|
|||||||
|
|
||||||
<text x="53" y="15" fill="#010101" fill-opacity=".3">Coverage</text>
|
<text x="53" y="15" fill="#010101" fill-opacity=".3">Coverage</text>
|
||||||
<text x="53" y="14" fill="#fff">Coverage</text>
|
<text x="53" y="14" fill="#fff">Coverage</text>
|
||||||
<text class="" x="132.5" y="15" fill="#010101" fill-opacity=".3">68.9%</text><text class="" x="132.5" y="14">68.9%</text>
|
<text class="" x="132.5" y="15" fill="#010101" fill-opacity=".3">69.2%</text><text class="" x="132.5" y="14">69.2%</text>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
@@ -9,5 +9,4 @@ REGISTRY_TOKEN=changeme
|
|||||||
|
|
||||||
# QA environment
|
# QA environment
|
||||||
JWT_SECRET_KEY=change-this-to-a-random-32-plus-char-secret
|
JWT_SECRET_KEY=change-this-to-a-random-32-plus-char-secret
|
||||||
POSTGRES_PASSWORD=change-this-to-a-random-password
|
|
||||||
QA_ADMIN_PORT=3001
|
QA_ADMIN_PORT=3001
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: miccheck
|
POSTGRES_DB: miccheck
|
||||||
POSTGRES_USER: miccheck
|
POSTGRES_USER: miccheck
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
|
POSTGRES_PASSWORD: password
|
||||||
ports:
|
ports:
|
||||||
- "${DB_BIND_HOST:-127.0.0.1}:55432:5432"
|
- "${DB_BIND_HOST:-127.0.0.1}:55432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -31,7 +31,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
ASPNETCORE_ENVIRONMENT: Development
|
ASPNETCORE_ENVIRONMENT: Development
|
||||||
ASPNETCORE_URLS: http://+:8080
|
ASPNETCORE_URLS: http://+:8080
|
||||||
ConnectionStrings__miccheck: "Host=db;Database=miccheck;Username=miccheck;Password=${POSTGRES_PASSWORD}"
|
ConnectionStrings__miccheck: "Host=db;Database=miccheck;Username=miccheck;Password=password"
|
||||||
Jwt__SecretKey: ${JWT_SECRET_KEY}
|
Jwt__SecretKey: ${JWT_SECRET_KEY}
|
||||||
Jwt__Issuer: MicCheck
|
Jwt__Issuer: MicCheck
|
||||||
Jwt__Audience: MicCheck
|
Jwt__Audience: MicCheck
|
||||||
|
|||||||
+22
-14
@@ -3,19 +3,27 @@ set -e
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
echo "Building solution..."
|
build_api() {
|
||||||
dotnet build "$SCRIPT_DIR/MicCheck.slnx"
|
echo "Building API..."
|
||||||
|
dotnet publish "$SCRIPT_DIR/src/api/MicCheck.Api/MicCheck.Api.csproj" -c Release -o "$SCRIPT_DIR/src/api/MicCheck.Api/publish"
|
||||||
|
docker compose -f "$SCRIPT_DIR/docker-compose.yml" restart api
|
||||||
|
echo "API done."
|
||||||
|
}
|
||||||
|
|
||||||
echo "Installing admin dependencies..."
|
build_admin() {
|
||||||
npm --prefix "$SCRIPT_DIR/src/admin" ci --silent
|
echo "Building admin..."
|
||||||
|
npm --prefix "$SCRIPT_DIR/src/admin" ci --silent
|
||||||
|
npm --prefix "$SCRIPT_DIR/src/admin" run build
|
||||||
|
docker compose -f "$SCRIPT_DIR/docker-compose.yml" restart admin
|
||||||
|
echo "Admin done."
|
||||||
|
}
|
||||||
|
|
||||||
echo "Building admin..."
|
case "${1:-all}" in
|
||||||
npm --prefix "$SCRIPT_DIR/src/admin" run build
|
api) build_api ;;
|
||||||
|
admin) build_admin ;;
|
||||||
echo "Running .NET unit tests..."
|
all) build_api && build_admin ;;
|
||||||
dotnet test "$SCRIPT_DIR/tests/api/MicCheck.Api.Tests.Unit/MicCheck.Api.Tests.Unit.csproj"
|
*)
|
||||||
|
echo "Usage: $0 [api|admin|all]"
|
||||||
echo "Running Jest tests..."
|
exit 1
|
||||||
npm --prefix "$SCRIPT_DIR/src/admin" test
|
;;
|
||||||
|
esac
|
||||||
echo "Build and tests complete."
|
|
||||||
|
|||||||
Executable
+60
@@ -0,0 +1,60 @@
|
|||||||
|
services:
|
||||||
|
|
||||||
|
# ── PostgreSQL ───────────────────────────────────────────────────────────────
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: miccheck
|
||||||
|
POSTGRES_USER: miccheck
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U miccheck -d miccheck"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
# ── .NET API ─────────────────────────────────────────────────────────────────
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: src/api/MicCheck.Api/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./src/api/MicCheck.Api/publish:/app
|
||||||
|
environment:
|
||||||
|
ASPNETCORE_ENVIRONMENT: Development
|
||||||
|
ASPNETCORE_URLS: http://+:8080
|
||||||
|
ConnectionStrings__DefaultConnection: "Host=db;Database=miccheck;Username=miccheck;Password=password"
|
||||||
|
Jwt__SecretKey: "miccheck-dev-secret-key-change-in-production!!"
|
||||||
|
Jwt__Issuer: MicCheck
|
||||||
|
Jwt__Audience: MicCheck
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/api/v1/health 2>/dev/null || exit 0"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
|
# ── Vue Admin Site (nginx) ───────────────────────────────────────────────────
|
||||||
|
admin:
|
||||||
|
build:
|
||||||
|
context: src/admin
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
volumes:
|
||||||
|
- ./src/admin/dist:/usr/share/nginx/html
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_started
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
# System Design — QA Physical Architecture
|
|
||||||
|
|
||||||
Physical deployment topology for the QA environment. Source: `deploy/qa/docker-compose.qa.yml`, `scripts/ci/*.sh`, `.github/workflows/ci.yml`.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
subgraph CI["Self-hosted CI runner (qa)"]
|
|
||||||
Pipeline["deploy-qa.sh / smoke-qa.sh"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Registry["Gitea Container Registry"]
|
|
||||||
ApiImage["miccheck-api:qa"]
|
|
||||||
AdminImage["miccheck-admin:qa"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph QAHost["QA Docker host — network: miccheck-qa-net (bridge)"]
|
|
||||||
subgraph AdminC["admin container<br/>nginx:1.27-alpine"]
|
|
||||||
Nginx["nginx<br/>serves Vue SPA<br/>proxies /api/, /health"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph ApiC["api container<br/>aspnet:10.0"]
|
|
||||||
Api["MicCheck.Api<br/>ASPNETCORE_URLS=http://+:8080"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph DbC["db container<br/>postgres:16-alpine"]
|
|
||||||
Db[("miccheck DB")]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Browser["Browser / QA tester"]
|
|
||||||
|
|
||||||
Pipeline -->|docker build/push| Registry
|
|
||||||
Registry -->|pull :qa| AdminC
|
|
||||||
Registry -->|pull :qa| ApiC
|
|
||||||
|
|
||||||
Browser -->|":3001 (QA_ADMIN_PORT)"| Nginx
|
|
||||||
Nginx -->|"http://api:8080 (internal)"| Api
|
|
||||||
Api -->|"Host=db;5432 (internal)"| Db
|
|
||||||
|
|
||||||
Pipeline -.->|"127.0.0.1:55432 (bridge-gateway bound)"| Db
|
|
||||||
```
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
| Component | Image | Exposure | Notes |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `admin` | `nginx:1.27-alpine` (built from `src/admin/Dockerfile.ci`, `node:22-alpine` build stage) | `${QA_ADMIN_PORT:-3001}:80` published on host | Serves Vue/Vite SPA; nginx (`src/admin/nginx.conf`) reverse-proxies `/api/` and `/health` to `api:8080` |
|
|
||||||
| `api` | `mcr.microsoft.com/dotnet/aspnet:10.0` (built from `src/api/MicCheck.Api/Dockerfile.ci`, `sdk:10.0` build stage) | no published host port — internal only, reached via `admin`'s nginx proxy | `ASPNETCORE_URLS=http://+:8080`; JWT config (`Jwt__SecretKey`/`Issuer=MicCheck`/`Audience=MicCheck`) from `JWT_SECRET_KEY` secret; healthcheck `curl localhost:8080/health` |
|
|
||||||
| `db` | `postgres:16-alpine` | `${DB_BIND_HOST:-127.0.0.1}:55432 → 5432`, bound to docker bridge gateway IP (not `0.0.0.0`) — reachable only from sibling CI containers, not off-box | DB `miccheck`, user `miccheck`, password from `POSTGRES_PASSWORD` secret; volume `miccheck-qa-pgdata` |
|
|
||||||
|
|
||||||
All three services run on an isolated bridge network `miccheck-qa-net` (project `miccheck-qa`), separate from the local dev Aspire stack.
|
|
||||||
|
|
||||||
## Deploy flow
|
|
||||||
|
|
||||||
1. `build-and-push` job builds `api` and `admin` images, tags with git SHA and `qa`, pushes to Gitea registry as `$REGISTRY/$REGISTRY_OWNER/miccheck-api` / `miccheck-admin`.
|
|
||||||
2. `deploy-qa` job (self-hosted runner, `main` branch only) pulls `:qa` images and runs `docker-compose.qa.yml` via `deploy-qa.sh`.
|
|
||||||
3. `smoke-qa` job hits the deployed stack (`smoke-qa.sh`) to verify health.
|
|
||||||
|
|
||||||
Secrets used: `REGISTRY`, `REGISTRY_OWNER`, `REGISTRY_USER`, `REGISTRY_TOKEN`, `JWT_SECRET_KEY`, `POSTGRES_PASSWORD`. Vars: `QA_ADMIN_PORT`.
|
|
||||||
@@ -29,11 +29,10 @@ Code in the API is organized by feature area (e.g. `Features`, `Segments`, `Iden
|
|||||||
|
|
||||||
## Running locally
|
## Running locally
|
||||||
|
|
||||||
`MicCheck.AppHost` (.NET Aspire) orchestrates the API, admin app, and supporting services for local development.
|
`docker-compose.yml` provides supporting services. `MicCheck.AppHost` (.NET Aspire) orchestrates the API and admin app for local development.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./dev-build.sh
|
./dev-build.sh
|
||||||
aspire run --project src/MicCheck.AppHost
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ registry_login
|
|||||||
|
|
||||||
export API_IMAGE ADMIN_IMAGE
|
export API_IMAGE ADMIN_IMAGE
|
||||||
export JWT_SECRET_KEY="${JWT_SECRET_KEY:?JWT_SECRET_KEY env var is required}"
|
export JWT_SECRET_KEY="${JWT_SECRET_KEY:?JWT_SECRET_KEY env var is required}"
|
||||||
export POSTGRES_PASSWORD="${POSTGRES_PASSWORD:?POSTGRES_PASSWORD env var is required}"
|
|
||||||
export QA_ADMIN_PORT="${QA_ADMIN_PORT:-3001}"
|
export QA_ADMIN_PORT="${QA_ADMIN_PORT:-3001}"
|
||||||
|
|
||||||
# Bind narrowly to docker's bridge gateway IP rather than 0.0.0.0: reachable
|
# Bind narrowly to docker's bridge gateway IP rather than 0.0.0.0: reachable
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ BASE_URL="http://${CI_HOST}:${QA_ADMIN_PORT}"
|
|||||||
log "Resolved docker host as $CI_HOST for reaching the QA stack's published ports"
|
log "Resolved docker host as $CI_HOST for reaching the QA stack's published ports"
|
||||||
|
|
||||||
export MICCHECK_API_BASE_URL="$BASE_URL"
|
export MICCHECK_API_BASE_URL="$BASE_URL"
|
||||||
export MICCHECK_DB_CONNECTION_STRING="${MICCHECK_DB_CONNECTION_STRING:-Host=$CI_HOST;Port=55432;Database=miccheck;Username=miccheck;Password=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD env var is required}}"
|
export MICCHECK_DB_CONNECTION_STRING="${MICCHECK_DB_CONNECTION_STRING:-Host=$CI_HOST;Port=55432;Database=miccheck;Username=miccheck;Password=password}"
|
||||||
|
|
||||||
log "Running API integration suite against $BASE_URL"
|
log "Running API integration suite against $BASE_URL"
|
||||||
dotnet test tests/api/MicCheck.Api.Tests.Integration/MicCheck.Api.Tests.Integration.csproj -c Release --logger trx
|
dotnet test tests/api/MicCheck.Api.Tests.Integration/MicCheck.Api.Tests.Integration.csproj -c Release --logger trx
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
namespace MicCheck.Common;
|
|
||||||
|
|
||||||
internal static class Guard
|
|
||||||
{
|
|
||||||
public static void Null<T>(T t, string parameterName) where T : class
|
|
||||||
{
|
|
||||||
if (t is null)
|
|
||||||
throw new ArgumentNullException(parameterName, $"{nameof(parameterName)} can not be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Empty(string value, string parameterName)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
throw new ArgumentException($"{parameterName} can not be empty", parameterName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Empty<T>(IEnumerable<T> collection, string parameterName)
|
|
||||||
{
|
|
||||||
if (collection == null || !collection.Any())
|
|
||||||
throw new ArgumentException($"{parameterName} can not be empty", parameterName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Negative(int value, string parameterName)
|
|
||||||
{
|
|
||||||
if (value < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(parameterName, $"{parameterName} must be a positive number or zero");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void NegativeOrZero(int value, string parameterName)
|
|
||||||
{
|
|
||||||
if (value <= 0)
|
|
||||||
throw new ArgumentOutOfRangeException(parameterName, $"{nameof(parameterName)} must be a positive number greater then zero");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Default<T>(T value, string parameterName)
|
|
||||||
{
|
|
||||||
if (EqualityComparer<T>.Default.Equals(value, default))
|
|
||||||
throw new ArgumentException($"{parameterName} can not be a default value", parameterName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using System.Net.Http.Json;
|
|
||||||
using MicCheck.Common;
|
|
||||||
|
|
||||||
namespace MicCheck;
|
|
||||||
|
|
||||||
public class FeatureClient(HttpClient httpClient) : IFeatureClient
|
|
||||||
{
|
|
||||||
public async Task<bool> IsEnabledAsync(string featureName, bool defaultValue = false, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
Guard.Empty(featureName, nameof(featureName));
|
|
||||||
|
|
||||||
var flags = await httpClient.GetFromJsonAsync<List<FlagResult>>("api/v1/flags", cancellationToken);
|
|
||||||
var match = flags?.FirstOrDefault(flag => string.Equals(flag.Feature.Name, featureName, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
return match?.Enabled ?? defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IFeatureClient
|
|
||||||
{
|
|
||||||
Task<bool> IsEnabledAsync(string featureName, bool defaultValue = false, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
namespace MicCheck;
|
|
||||||
|
|
||||||
public record FlagResult(int Id, FeatureSummary Feature, bool Enabled, string? FeatureStateValue);
|
|
||||||
|
|
||||||
public record FeatureSummary(int Id, string Name, string Type);
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
<RootNamespace>MicCheck</RootNamespace>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
using MicCheck.Api.Common.Security.Authorization;
|
using MicCheck.Api.Common.Security.Authorization;
|
||||||
using MicCheck.Api.Common;
|
using MicCheck.Api.Common;
|
||||||
using MicCheck.Api.Features;
|
|
||||||
using MicCheck.Api.Identities;
|
|
||||||
using MicCheck.Api.Segments;
|
|
||||||
using MicCheck.Api.Webhooks;
|
using MicCheck.Api.Webhooks;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -149,9 +146,9 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("api/v1/environment/{apiKey}/identities")]
|
[HttpGet("api/v1/environment/{apiKey}/identities")]
|
||||||
public async Task<ActionResult<PaginatedResponse<AdminIdentityResponse>>> ListIdentities(
|
public async Task<ActionResult<PaginatedResponse<Identities.AdminIdentityResponse>>> ListIdentities(
|
||||||
string apiKey,
|
string apiKey,
|
||||||
[FromServices] AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
[FromQuery] int page = 1,
|
[FromQuery] int page = 1,
|
||||||
[FromQuery] int pageSize = 20,
|
[FromQuery] int pageSize = 20,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
@@ -161,15 +158,15 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
if (environment is null) return NotFound();
|
if (environment is null) return NotFound();
|
||||||
|
|
||||||
var result = await adminIdentityService.ListAsync(environment.Id, page, pageSize, ct);
|
var result = await adminIdentityService.ListAsync(environment.Id, page, pageSize, ct);
|
||||||
var results = result.Items.Select(AdminIdentityResponse.From).ToList();
|
var results = result.Items.Select(Identities.AdminIdentityResponse.From).ToList();
|
||||||
return Ok(new PaginatedResponse<AdminIdentityResponse>(result.Total, null, null, results));
|
return Ok(new PaginatedResponse<Identities.AdminIdentityResponse>(result.Total, null, null, results));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("api/v1/environment/{apiKey}/identities")]
|
[HttpPost("api/v1/environment/{apiKey}/identities")]
|
||||||
public async Task<ActionResult<AdminIdentityResponse>> CreateIdentity(
|
public async Task<ActionResult<Identities.AdminIdentityResponse>> CreateIdentity(
|
||||||
string apiKey,
|
string apiKey,
|
||||||
[FromBody] CreateIdentityRequest request,
|
[FromBody] Identities.CreateIdentityRequest request,
|
||||||
[FromServices] AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -180,13 +177,13 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
return Conflict(new { error = "An identity with this identifier already exists in the environment." });
|
return Conflict(new { error = "An identity with this identifier already exists in the environment." });
|
||||||
|
|
||||||
return CreatedAtAction(nameof(GetIdentity), new { apiKey, id = identity.Id },
|
return CreatedAtAction(nameof(GetIdentity), new { apiKey, id = identity.Id },
|
||||||
AdminIdentityResponse.From(identity));
|
Identities.AdminIdentityResponse.From(identity));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("api/v1/environment/{apiKey}/identity/{id}")]
|
[HttpGet("api/v1/environment/{apiKey}/identity/{id}")]
|
||||||
public async Task<ActionResult<AdminIdentityResponse>> GetIdentity(
|
public async Task<ActionResult<Identities.AdminIdentityResponse>> GetIdentity(
|
||||||
string apiKey, int id,
|
string apiKey, int id,
|
||||||
[FromServices] AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -194,13 +191,13 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
|
|
||||||
var identity = await adminIdentityService.FindByIdAsync(id, environment.Id, ct);
|
var identity = await adminIdentityService.FindByIdAsync(id, environment.Id, ct);
|
||||||
if (identity is null) return NotFound();
|
if (identity is null) return NotFound();
|
||||||
return Ok(AdminIdentityResponse.From(identity));
|
return Ok(Identities.AdminIdentityResponse.From(identity));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("api/v1/environment/{apiKey}/identity/{id}")]
|
[HttpDelete("api/v1/environment/{apiKey}/identity/{id}")]
|
||||||
public async Task<IActionResult> DeleteIdentity(
|
public async Task<IActionResult> DeleteIdentity(
|
||||||
string apiKey, int id,
|
string apiKey, int id,
|
||||||
[FromServices] AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -214,10 +211,10 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("api/v1/environment/{apiKey}/identity/{id}/trait/{key}")]
|
[HttpPut("api/v1/environment/{apiKey}/identity/{id}/trait/{key}")]
|
||||||
public async Task<ActionResult<TraitResponse>> UpsertIdentityTrait(
|
public async Task<ActionResult<Identities.TraitResponse>> UpsertIdentityTrait(
|
||||||
string apiKey, int id, string key,
|
string apiKey, int id, string key,
|
||||||
[FromBody] UpsertTraitRequest request,
|
[FromBody] Identities.UpsertTraitRequest request,
|
||||||
[FromServices] AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -231,7 +228,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
[HttpDelete("api/v1/environment/{apiKey}/identity/{id}/trait/{key}")]
|
[HttpDelete("api/v1/environment/{apiKey}/identity/{id}/trait/{key}")]
|
||||||
public async Task<IActionResult> DeleteIdentityTrait(
|
public async Task<IActionResult> DeleteIdentityTrait(
|
||||||
string apiKey, int id, string key,
|
string apiKey, int id, string key,
|
||||||
[FromServices] AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -243,9 +240,9 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("api/v1/environment/{apiKey}/identity/{id}/featurestates")]
|
[HttpGet("api/v1/environment/{apiKey}/identity/{id}/featurestates")]
|
||||||
public async Task<ActionResult<IReadOnlyList<FeatureStateResponse>>> GetIdentityFeatureStates(
|
public async Task<ActionResult<IReadOnlyList<Features.FeatureStateResponse>>> GetIdentityFeatureStates(
|
||||||
string apiKey, int id,
|
string apiKey, int id,
|
||||||
[FromServices] AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -255,14 +252,14 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
if (identity is null) return NotFound();
|
if (identity is null) return NotFound();
|
||||||
|
|
||||||
var states = await adminIdentityService.GetFeatureStatesAsync(id, environment.Id, ct);
|
var states = await adminIdentityService.GetFeatureStatesAsync(id, environment.Id, ct);
|
||||||
return Ok(states.Select(FeatureStateResponse.From).ToList());
|
return Ok(states.Select(Features.FeatureStateResponse.From).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("api/v1/environment/{apiKey}/identity/{id}/featurestate/{featureId}")]
|
[HttpPut("api/v1/environment/{apiKey}/identity/{id}/featurestate/{featureId}")]
|
||||||
public async Task<ActionResult<FeatureStateResponse>> SetIdentityFeatureState(
|
public async Task<ActionResult<Features.FeatureStateResponse>> SetIdentityFeatureState(
|
||||||
string apiKey, int id, int featureId,
|
string apiKey, int id, int featureId,
|
||||||
UpdateFeatureStateRequest request,
|
Features.UpdateFeatureStateRequest request,
|
||||||
[FromServices] AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -272,13 +269,13 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
if (identity is null) return NotFound();
|
if (identity is null) return NotFound();
|
||||||
|
|
||||||
var state = await adminIdentityService.SetFeatureStateAsync(id, environment.Id, featureId, request.Enabled, request.Value, ct);
|
var state = await adminIdentityService.SetFeatureStateAsync(id, environment.Id, featureId, request.Enabled, request.Value, ct);
|
||||||
return Ok(FeatureStateResponse.From(state));
|
return Ok(Features.FeatureStateResponse.From(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("api/v1/environment/{apiKey}/identity/{id}/featurestate/{featureId}")]
|
[HttpDelete("api/v1/environment/{apiKey}/identity/{id}/featurestate/{featureId}")]
|
||||||
public async Task<IActionResult> DeleteIdentityFeatureState(
|
public async Task<IActionResult> DeleteIdentityFeatureState(
|
||||||
string apiKey, int id, int featureId,
|
string apiKey, int id, int featureId,
|
||||||
[FromServices] AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -292,22 +289,22 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("api/v1/environment/{apiKey}/featurestates")]
|
[HttpGet("api/v1/environment/{apiKey}/featurestates")]
|
||||||
public async Task<ActionResult<IReadOnlyList<FeatureStateResponse>>> ListFeatureStates(
|
public async Task<ActionResult<IReadOnlyList<Features.FeatureStateResponse>>> ListFeatureStates(
|
||||||
string apiKey,
|
string apiKey,
|
||||||
[FromServices] FeatureStateService featureStateService,
|
[FromServices] Features.FeatureStateService featureStateService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
if (environment is null) return NotFound();
|
if (environment is null) return NotFound();
|
||||||
|
|
||||||
var states = await featureStateService.ListByEnvironmentAsync(environment.Id, ct);
|
var states = await featureStateService.ListByEnvironmentAsync(environment.Id, ct);
|
||||||
return Ok(states.Select(FeatureStateResponse.From).ToList());
|
return Ok(states.Select(Features.FeatureStateResponse.From).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("api/v1/environment/{apiKey}/featurestate/{id}")]
|
[HttpGet("api/v1/environment/{apiKey}/featurestate/{id}")]
|
||||||
public async Task<ActionResult<FeatureStateResponse>> GetFeatureState(
|
public async Task<ActionResult<Features.FeatureStateResponse>> GetFeatureState(
|
||||||
string apiKey, int id,
|
string apiKey, int id,
|
||||||
[FromServices] FeatureStateService featureStateService,
|
[FromServices] Features.FeatureStateService featureStateService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -315,14 +312,14 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
|
|
||||||
var state = await featureStateService.FindByIdAsync(id, environment.Id, ct);
|
var state = await featureStateService.FindByIdAsync(id, environment.Id, ct);
|
||||||
if (state is null) return NotFound();
|
if (state is null) return NotFound();
|
||||||
return Ok(FeatureStateResponse.From(state));
|
return Ok(Features.FeatureStateResponse.From(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("api/v1/environment/{apiKey}/featurestate/{id}")]
|
[HttpPut("api/v1/environment/{apiKey}/featurestate/{id}")]
|
||||||
public async Task<ActionResult<FeatureStateResponse>> UpdateFeatureState(
|
public async Task<ActionResult<Features.FeatureStateResponse>> UpdateFeatureState(
|
||||||
string apiKey, int id,
|
string apiKey, int id,
|
||||||
UpdateFeatureStateRequest request,
|
Features.UpdateFeatureStateRequest request,
|
||||||
[FromServices] FeatureStateService featureStateService,
|
[FromServices] Features.FeatureStateService featureStateService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -332,14 +329,14 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
if (state is null) return NotFound();
|
if (state is null) return NotFound();
|
||||||
|
|
||||||
var updated = await featureStateService.UpdateAsync(id, request.Enabled, request.Value, ct);
|
var updated = await featureStateService.UpdateAsync(id, request.Enabled, request.Value, ct);
|
||||||
return Ok(FeatureStateResponse.From(updated));
|
return Ok(Features.FeatureStateResponse.From(updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("api/v1/environment/{apiKey}/featurestate/{id}")]
|
[HttpPatch("api/v1/environment/{apiKey}/featurestate/{id}")]
|
||||||
public async Task<ActionResult<FeatureStateResponse>> PatchFeatureState(
|
public async Task<ActionResult<Features.FeatureStateResponse>> PatchFeatureState(
|
||||||
string apiKey, int id,
|
string apiKey, int id,
|
||||||
PatchFeatureStateRequest request,
|
Features.PatchFeatureStateRequest request,
|
||||||
[FromServices] FeatureStateService featureStateService,
|
[FromServices] Features.FeatureStateService featureStateService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -349,15 +346,15 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
if (state is null) return NotFound();
|
if (state is null) return NotFound();
|
||||||
|
|
||||||
var updated = await featureStateService.PatchAsync(id, request.Enabled, request.Value, ct);
|
var updated = await featureStateService.PatchAsync(id, request.Enabled, request.Value, ct);
|
||||||
return Ok(FeatureStateResponse.From(updated));
|
return Ok(Features.FeatureStateResponse.From(updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Feature Segments ────────────────────────────────────────────────────
|
// ─── Feature Segments ────────────────────────────────────────────────────
|
||||||
|
|
||||||
[HttpGet("api/v1/environment/{apiKey}/feature/{featureId}/segments")]
|
[HttpGet("api/v1/environment/{apiKey}/feature/{featureId}/segments")]
|
||||||
public async Task<ActionResult<IReadOnlyList<FeatureSegmentResponse>>> ListFeatureSegments(
|
public async Task<ActionResult<IReadOnlyList<Features.FeatureSegmentResponse>>> ListFeatureSegments(
|
||||||
string apiKey, int featureId,
|
string apiKey, int featureId,
|
||||||
[FromServices] FeatureSegmentService featureSegmentService,
|
[FromServices] Features.FeatureSegmentService featureSegmentService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -368,10 +365,10 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("api/v1/environment/{apiKey}/feature/{featureId}/segments")]
|
[HttpPost("api/v1/environment/{apiKey}/feature/{featureId}/segments")]
|
||||||
public async Task<ActionResult<FeatureSegmentResponse>> CreateFeatureSegment(
|
public async Task<ActionResult<Features.FeatureSegmentResponse>> CreateFeatureSegment(
|
||||||
string apiKey, int featureId,
|
string apiKey, int featureId,
|
||||||
CreateFeatureSegmentRequest request,
|
Features.CreateFeatureSegmentRequest request,
|
||||||
[FromServices] FeatureSegmentService featureSegmentService,
|
[FromServices] Features.FeatureSegmentService featureSegmentService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -391,10 +388,10 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("api/v1/environment/{apiKey}/feature/{featureId}/segment/{id}")]
|
[HttpPut("api/v1/environment/{apiKey}/feature/{featureId}/segment/{id}")]
|
||||||
public async Task<ActionResult<FeatureSegmentResponse>> UpdateFeatureSegment(
|
public async Task<ActionResult<Features.FeatureSegmentResponse>> UpdateFeatureSegment(
|
||||||
string apiKey, int featureId, int id,
|
string apiKey, int featureId, int id,
|
||||||
UpdateFeatureSegmentRequest request,
|
Features.UpdateFeatureSegmentRequest request,
|
||||||
[FromServices] FeatureSegmentService featureSegmentService,
|
[FromServices] Features.FeatureSegmentService featureSegmentService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -408,7 +405,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
[HttpDelete("api/v1/environment/{apiKey}/feature/{featureId}/segment/{id}")]
|
[HttpDelete("api/v1/environment/{apiKey}/feature/{featureId}/segment/{id}")]
|
||||||
public async Task<IActionResult> DeleteFeatureSegment(
|
public async Task<IActionResult> DeleteFeatureSegment(
|
||||||
string apiKey, int featureId, int id,
|
string apiKey, int featureId, int id,
|
||||||
[FromServices] FeatureSegmentService featureSegmentService,
|
[FromServices] Features.FeatureSegmentService featureSegmentService,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -421,11 +418,11 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
// ─── Identity Segments ───────────────────────────────────────────────────
|
// ─── Identity Segments ───────────────────────────────────────────────────
|
||||||
|
|
||||||
[HttpGet("api/v1/environment/{apiKey}/identity/{id}/segments")]
|
[HttpGet("api/v1/environment/{apiKey}/identity/{id}/segments")]
|
||||||
public async Task<ActionResult<IReadOnlyList<SegmentSummaryResponse>>> GetIdentitySegments(
|
public async Task<ActionResult<IReadOnlyList<Segments.SegmentSummaryResponse>>> GetIdentitySegments(
|
||||||
string apiKey, int id,
|
string apiKey, int id,
|
||||||
[FromServices] AdminIdentityService adminIdentityService,
|
[FromServices] Identities.AdminIdentityService adminIdentityService,
|
||||||
[FromServices] SegmentService segmentService,
|
[FromServices] Segments.SegmentService segmentService,
|
||||||
[FromServices] SegmentEvaluator segmentEvaluator,
|
[FromServices] Segments.SegmentEvaluator segmentEvaluator,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
var environment = await environmentService.FindByApiKeyAsync(apiKey, ct);
|
||||||
@@ -437,7 +434,7 @@ public class EnvironmentsController(EnvironmentService environmentService, Webho
|
|||||||
var segments = await segmentService.ListByProjectAsync(environment.ProjectId, ct);
|
var segments = await segmentService.ListByProjectAsync(environment.ProjectId, ct);
|
||||||
var matching = segments
|
var matching = segments
|
||||||
.Where(s => segmentEvaluator.Evaluate(s, identity.Traits.ToList(), identity.Identifier))
|
.Where(s => segmentEvaluator.Evaluate(s, identity.Traits.ToList(), identity.Identifier))
|
||||||
.Select(s => new SegmentSummaryResponse(s.Id, s.Name))
|
.Select(s => new Segments.SegmentSummaryResponse(s.Id, s.Name))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return Ok(matching);
|
return Ok(matching);
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Text;
|
|
||||||
using Moq;
|
|
||||||
using Moq.Protected;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace MicCheck.Client.Tests.Unit;
|
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
public class FeatureClientTests
|
|
||||||
{
|
|
||||||
[Test]
|
|
||||||
public async Task WhenFeatureIsEnabled_ThenIsEnabledAsyncReturnsTrue()
|
|
||||||
{
|
|
||||||
var client = CreateClient(HttpStatusCode.OK, """
|
|
||||||
[{"id":1,"feature":{"id":1,"name":"my-feature","type":"STANDARD"},"enabled":true,"featureStateValue":null}]
|
|
||||||
""");
|
|
||||||
|
|
||||||
var result = await client.IsEnabledAsync("my-feature");
|
|
||||||
|
|
||||||
Assert.That(result, Is.True);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task WhenFeatureIsDisabled_ThenIsEnabledAsyncReturnsFalse()
|
|
||||||
{
|
|
||||||
var client = CreateClient(HttpStatusCode.OK, """
|
|
||||||
[{"id":1,"feature":{"id":1,"name":"my-feature","type":"STANDARD"},"enabled":false,"featureStateValue":null}]
|
|
||||||
""");
|
|
||||||
|
|
||||||
var result = await client.IsEnabledAsync("my-feature");
|
|
||||||
|
|
||||||
Assert.That(result, Is.False);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task WhenFeatureNameDiffersOnlyByCase_ThenIsEnabledAsyncStillMatches()
|
|
||||||
{
|
|
||||||
var client = CreateClient(HttpStatusCode.OK, """
|
|
||||||
[{"id":1,"feature":{"id":1,"name":"My-Feature","type":"STANDARD"},"enabled":true,"featureStateValue":null}]
|
|
||||||
""");
|
|
||||||
|
|
||||||
var result = await client.IsEnabledAsync("my-feature");
|
|
||||||
|
|
||||||
Assert.That(result, Is.True);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task WhenFeatureIsNotFound_ThenIsEnabledAsyncReturnsProvidedDefaultValue()
|
|
||||||
{
|
|
||||||
var client = CreateClient(HttpStatusCode.OK, "[]");
|
|
||||||
|
|
||||||
var result = await client.IsEnabledAsync("missing-feature", defaultValue: true);
|
|
||||||
|
|
||||||
Assert.That(result, Is.True);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void WhenFeatureNameIsEmpty_ThenIsEnabledAsyncThrowsArgumentException()
|
|
||||||
{
|
|
||||||
var client = CreateClient(HttpStatusCode.OK, "[]");
|
|
||||||
|
|
||||||
Assert.That(async () => await client.IsEnabledAsync(" "), Throws.ArgumentException);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static FeatureClient CreateClient(HttpStatusCode responseStatus, string responseBody)
|
|
||||||
{
|
|
||||||
var handler = new Mock<HttpMessageHandler>();
|
|
||||||
handler.Protected()
|
|
||||||
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
|
||||||
.ReturnsAsync(new HttpResponseMessage(responseStatus)
|
|
||||||
{
|
|
||||||
Content = new StringContent(responseBody, Encoding.UTF8, "application/json")
|
|
||||||
});
|
|
||||||
|
|
||||||
var httpClient = new HttpClient(handler.Object) { BaseAddress = new Uri("https://example.com") };
|
|
||||||
return new FeatureClient(httpClient);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
|
||||||
<PackageReference Include="NUnit" Version="4.5.1" />
|
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="../../../src/MicCheck.Client/MicCheck.Client.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
Reference in New Issue
Block a user