Compare commits

..
5 Commits
Author SHA1 Message Date
Rafał Tyński 24c9e582c9 ci: update commented CI format and test steps 2026-10-04 01:50:01 +02:00
Rafał Tyński 40e07bc1a6 docs(readme): document layered backend and API endpoints 2026-10-04 01:50:01 +02:00
Rafał Tyński a9815b54e0 fix(infra): fix Dockerfile restore and build script solution name
Copy the layered project files before restore so the Docker build resolves project references, and build the correct solution file (TarninaHub.slnx) in scripts/build.mjs.
2026-10-04 01:50:01 +02:00
Rafał Tyński 1cbf965657 test(backend): add xUnit tests for the API skeleton
Add tests/TarninaHub.Tests with health controller and SurrealDB endpoint builder tests, and register the test and layer projects in the solution.
2026-10-04 01:49:56 +02:00
Rafał Tyński b3756238a3 feat(backend): restructure API into layers with OpenAPI, versioning and error handling
Split the backend into API host, Domain, Application and Infrastructure projects. Add Swagger/OpenAPI per API version, Asp.Versioning (query/header), ProblemDetails with a global exception handler and SurrealQL migrations applied at startup (namespace/database ensured). Infrastructure owns the SurrealDB client, settings and DI.
2026-10-04 01:49:51 +02:00
34 changed files with 546 additions and 67 deletions
+3 -2
View File
@@ -24,8 +24,9 @@
# - name: Build # - name: Build
# run: dotnet build backend/TarninaHub.csproj --configuration Release --no-restore # run: dotnet build backend/TarninaHub.csproj --configuration Release --no-restore
# - name: Format check # - name: Format check
# run: dotnet format backend/TarninaHub.csproj --verify-no-changes # run: dotnet format whitespace --verify-no-changes TarninaHub.slnx
# # Tests: enabled once test projects are added (`dotnet test`). # - name: Test
# run: dotnet test TarninaHub.slnx --configuration Release --no-build
# #
# frontend: # frontend:
# runs-on: ubuntu-latest # runs-on: ubuntu-latest
+3
View File
@@ -12,6 +12,9 @@ RUN npm run build
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src WORKDIR /src
COPY backend/TarninaHub.csproj ./backend/ COPY backend/TarninaHub.csproj ./backend/
COPY backend/Domain/TarninaHub.Domain.csproj ./backend/Domain/
COPY backend/Application/TarninaHub.Application.csproj ./backend/Application/
COPY backend/Infrastructure/TarninaHub.Infrastructure.csproj ./backend/Infrastructure/
RUN dotnet restore ./backend/TarninaHub.csproj RUN dotnet restore ./backend/TarninaHub.csproj
COPY backend/ ./backend/ COPY backend/ ./backend/
COPY --from=client /src/client/dist ./backend/wwwroot/ COPY --from=client /src/client/dist ./backend/wwwroot/
+17 -1
View File
@@ -113,8 +113,16 @@ Konfiguracja przez zmienne środowiskowe (`SurrealDb__*`, `LLM__ApiKey`, `OTEL_*
``` ```
TarninaHub/ TarninaHub/
├─ backend/ # ASP.NET Core Web API + wwwroot (build z client) ├─ backend/ # ASP.NET Core Web API (API host) + wwwroot + migrations
│ ├─ Controllers/ # warstwa API (kontrolery, DTO wejścia/wyjścia)
│ ├─ Errors/ # globalna obsługa błędów (ProblemDetails)
│ ├─ Configuration/ # konfiguracja Swagger/OpenAPI
│ ├─ Domain/ # warstwa domeny (enumy, reguły)
│ ├─ Application/ # DTO i abstrakcje (interfejsy)
│ ├─ Infrastructure/ # SurrealDB, migracje, DI
│ └─ migrations/ # *.surql (aplikowane przy starcie)
├─ client/ # Vue 3 + TS + Vite ├─ client/ # Vue 3 + TS + Vite
├─ tests/TarninaHub.Tests/ # testy jednostkowe (xUnit)
├─ scripts/ ├─ scripts/
│ ├─ build.mjs # build całości │ ├─ build.mjs # build całości
│ └─ copy-client.mjs # client/dist -> backend/wwwroot │ └─ copy-client.mjs # client/dist -> backend/wwwroot
@@ -124,3 +132,11 @@ TarninaHub/
├─ TarninaHub.slnx ├─ TarninaHub.slnx
└─ README.md └─ README.md
``` ```
## API
- Swagger UI: `GET /swagger` (dokument per wersja, np. `/swagger/v1/swagger.json`).
- Wersjonowanie: `?api-version=1.0` lub nagłówek `X-Api-Version` (domyślnie `1.0`).
- Błędy: `ProblemDetails` (RFC 7807), globalny handler wyjątków.
- Healthcheck: `GET /api/health` (bez wersji, dla infrastruktury).
- Migracje SurrealQL są aplikowane automatycznie przy starcie (katalog `migrations/`).
+6
View File
@@ -1,5 +1,11 @@
<Solution> <Solution>
<Folder Name="/backend/"> <Folder Name="/backend/">
<Project Path="backend/TarninaHub.csproj" /> <Project Path="backend/TarninaHub.csproj" />
<Project Path="backend/Application/TarninaHub.Application.csproj" />
<Project Path="backend/Domain/TarninaHub.Domain.csproj" />
<Project Path="backend/Infrastructure/TarninaHub.Infrastructure.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/TarninaHub.Tests/TarninaHub.Tests.csproj" />
</Folder> </Folder>
</Solution> </Solution>
@@ -0,0 +1,6 @@
namespace TarninaHub.Application.Abstractions;
public interface IMigrationRunner
{
Task ApplyAsync(CancellationToken cancellationToken);
}
@@ -1,6 +1,6 @@
using TarninaHub.Models; using TarninaHub.Application.Dtos;
namespace TarninaHub.Services; namespace TarninaHub.Application.Abstractions;
public interface IStorageService public interface IStorageService
{ {
@@ -1,4 +1,4 @@
namespace TarninaHub.Models; namespace TarninaHub.Application.Dtos;
public sealed class StorageProbe public sealed class StorageProbe
{ {
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\TarninaHub.Domain.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,29 @@
using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace TarninaHub.Configuration;
public sealed class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider _provider;
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
{
_provider = provider;
}
public void Configure(SwaggerGenOptions options)
{
foreach (var description in _provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, new OpenApiInfo
{
Title = "TarninaHub API",
Version = description.ApiVersion.ToString(),
Description = "Backend API (BFF) for TarninaHub — Małopolski Hub Innowacji Społecznych.",
});
}
}
}
+4 -2
View File
@@ -1,10 +1,12 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using TarninaHub.Models; using TarninaHub.Application.Abstractions;
using TarninaHub.Services; using TarninaHub.Application.Dtos;
namespace TarninaHub.Controllers; namespace TarninaHub.Controllers;
[ApiController] [ApiController]
[ApiVersion("1.0")]
[Route("api/system")] [Route("api/system")]
public class SystemController : ControllerBase public class SystemController : ControllerBase
{ {
@@ -0,0 +1,9 @@
namespace TarninaHub.Domain.Enums;
public enum ApplicationStatus
{
Draft,
Submitted,
Accepted,
Rejected,
}
+9
View File
@@ -0,0 +1,9 @@
namespace TarninaHub.Domain.Enums;
public enum DocumentScope
{
Local,
Partial,
Full,
Anonymized,
}
+8
View File
@@ -0,0 +1,8 @@
namespace TarninaHub.Domain.Enums;
public enum GrantCallStatus
{
Planned,
Open,
Closed,
}
+10
View File
@@ -0,0 +1,10 @@
namespace TarninaHub.Domain.Enums;
public enum IdeaStatus
{
Draft,
Submitted,
InReview,
Accepted,
Rejected,
}
+9
View File
@@ -0,0 +1,9 @@
namespace TarninaHub.Domain.Enums;
public enum InnovationStage
{
Idea,
Pilot,
Scaling,
Service,
}
+8
View File
@@ -0,0 +1,8 @@
namespace TarninaHub.Domain.Enums;
public enum LocationKind
{
Gmina,
Powiat,
Wojewodztwo,
}
@@ -0,0 +1,8 @@
namespace TarninaHub.Domain.Enums;
public enum PartnershipStatus
{
Proposed,
Active,
Closed,
}
+11
View File
@@ -0,0 +1,11 @@
namespace TarninaHub.Domain.Enums;
public enum SubmissionStatus
{
New,
Triaged,
Matched,
InProgress,
Closed,
Rejected,
}
+10
View File
@@ -0,0 +1,10 @@
namespace TarninaHub.Domain.Enums;
public enum TagGroup
{
Audience,
Topic,
Stage,
Sector,
Cross,
}
+10
View File
@@ -0,0 +1,10 @@
namespace TarninaHub.Domain.Enums;
public enum UserRole
{
User,
Jst,
Operator,
Manager,
Admin,
}
+9
View File
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+38
View File
@@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
namespace TarninaHub.Errors;
public sealed class GlobalExceptionHandler : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
_logger = logger;
}
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
_logger.LogError(exception, "Unhandled exception for {Method} {Path}",
httpContext.Request.Method,
httpContext.Request.Path);
var problem = new ProblemDetails
{
Type = "https://httpstatuses.io/500",
Title = "An unexpected error occurred.",
Status = StatusCodes.Status500InternalServerError,
Instance = httpContext.Request.Path,
};
httpContext.Response.StatusCode = problem.Status.Value;
await httpContext.Response.WriteAsJsonAsync(problem, cancellationToken);
return true;
}
}
@@ -0,0 +1,52 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using SurrealDb.Net;
using TarninaHub.Application.Abstractions;
using TarninaHub.Infrastructure.Persistence;
using TarninaHub.Infrastructure.Settings;
namespace TarninaHub.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
services.Configure<SurrealDbSettings>(configuration.GetSection("SurrealDb"));
var settings = configuration.GetSection("SurrealDb").Get<SurrealDbSettings>()
?? new SurrealDbSettings();
var options = new SurrealDbOptions
{
Endpoint = SurrealDbEndpoint.Build(settings),
Namespace = settings.Namespace,
Database = settings.Database,
};
if (string.IsNullOrWhiteSpace(settings.RemoteUrl))
{
// Embedded RocksDB, persisted next to the running app (./data), never in memory.
var dataDirectory = Path.Combine(AppContext.BaseDirectory, settings.DataDirectory);
Directory.CreateDirectory(dataDirectory);
services
.AddSurreal(options, ServiceLifetime.Singleton)
.AddRocksDbProvider();
}
else
{
// Remote SurrealDB server on its own dedicated port (used for sync with the frontend).
options.Username = settings.Username;
options.Password = settings.Password;
services.AddSurreal(options, ServiceLifetime.Singleton);
}
services.AddSingleton<IStorageService, StorageService>();
services.AddSingleton<IMigrationRunner, MigrationRunner>();
return services;
}
}
@@ -0,0 +1,63 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SurrealDb.Net;
using TarninaHub.Application.Abstractions;
using TarninaHub.Infrastructure.Settings;
namespace TarninaHub.Infrastructure.Persistence;
public sealed class MigrationRunner : IMigrationRunner
{
private readonly ISurrealDbClient _client;
private readonly SurrealDbSettings _settings;
private readonly ILogger<MigrationRunner> _logger;
public MigrationRunner(
ISurrealDbClient client,
IOptions<SurrealDbSettings> settings,
ILogger<MigrationRunner> logger)
{
_client = client;
_settings = settings.Value;
_logger = logger;
}
public async Task ApplyAsync(CancellationToken cancellationToken)
{
await EnsureNamespaceAsync(cancellationToken);
var migrationsPath = Path.Combine(AppContext.BaseDirectory, _settings.MigrationsDirectory);
if (!Directory.Exists(migrationsPath))
{
_logger.LogWarning(
"Migrations directory '{MigrationsPath}' was not found; skipping migrations.",
migrationsPath);
return;
}
var files = Directory.GetFiles(migrationsPath, "*.surql")
.OrderBy(file => file, StringComparer.Ordinal);
foreach (var file in files)
{
var sql = await File.ReadAllTextAsync(file, cancellationToken);
await _client.RawQuery(sql, cancellationToken: cancellationToken);
_logger.LogInformation("Applied migration {Migration}.", Path.GetFileName(file));
}
}
private async Task EnsureNamespaceAsync(CancellationToken cancellationToken)
{
var sql =
$"DEFINE NAMESPACE IF NOT EXISTS {_settings.Namespace}; " +
$"USE NS {_settings.Namespace}; " +
$"DEFINE DATABASE IF NOT EXISTS {_settings.Database}; " +
$"USE DB {_settings.Database};";
await _client.RawQuery(sql, cancellationToken: cancellationToken);
}
}
@@ -1,9 +1,10 @@
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using SurrealDb.Net; using SurrealDb.Net;
using TarninaHub.Models; using TarninaHub.Application.Abstractions;
using TarninaHub.Settings; using TarninaHub.Application.Dtos;
using TarninaHub.Infrastructure.Settings;
namespace TarninaHub.Services; namespace TarninaHub.Infrastructure.Persistence;
public sealed class StorageService : IStorageService public sealed class StorageService : IStorageService
{ {
@@ -0,0 +1,21 @@
namespace TarninaHub.Infrastructure.Settings;
public static class SurrealDbEndpoint
{
/// <summary>
/// Builds the SurrealDB endpoint. Remote mode uses the configured URL;
/// embedded mode uses the RocksDB provider persisted to the data directory
/// (forward slashes, so the same endpoint works on Windows and Linux).
/// </summary>
public static string Build(SurrealDbSettings settings)
{
if (!string.IsNullOrWhiteSpace(settings.RemoteUrl))
{
return settings.RemoteUrl;
}
var dataDirectory = Path.Combine(AppContext.BaseDirectory, settings.DataDirectory);
return $"rocksdb://{dataDirectory.Replace('\\', '/')}";
}
}
@@ -1,4 +1,4 @@
namespace TarninaHub.Settings; namespace TarninaHub.Infrastructure.Settings;
public sealed class SurrealDbSettings public sealed class SurrealDbSettings
{ {
@@ -8,6 +8,12 @@ public sealed class SurrealDbSettings
public string DataDirectory { get; set; } = "data"; public string DataDirectory { get; set; } = "data";
/// <summary>
/// Directory (relative to the app base directory) with ordered *.surql
/// migration files applied at startup. Copied from backend/migrations.
/// </summary>
public string MigrationsDirectory { get; set; } = "migrations";
/// <summary> /// <summary>
/// When set (e.g. ws://127.0.0.1:8010/rpc), the backend connects to a /// When set (e.g. ws://127.0.0.1:8010/rpc), the backend connects to a
/// dedicated SurrealDB server instead of the embedded RocksDB engine. /// dedicated SurrealDB server instead of the embedded RocksDB engine.
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.7" />
<PackageReference Include="SurrealDb.Embedded.RocksDb" Version="1.0.0" />
<PackageReference Include="SurrealDb.Net" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Application\TarninaHub.Application.csproj" />
<ProjectReference Include="..\Domain\TarninaHub.Domain.csproj" />
</ItemGroup>
</Project>
+52 -51
View File
@@ -1,61 +1,74 @@
using TarninaHub.Services; using Asp.Versioning;
using TarninaHub.Settings; using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerGen;
using TarninaHub.Application.Abstractions;
using TarninaHub.Configuration;
using TarninaHub.Errors;
using TarninaHub.Infrastructure;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
var surrealSettings = builder.Configuration.GetSection("SurrealDb").Get<SurrealDbSettings>() // Layered infrastructure: SurrealDB client, settings and persistence services.
?? new SurrealDbSettings(); builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.Configure<SurrealDbSettings>(builder.Configuration.GetSection("SurrealDb"));
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddSingleton<IStorageService, StorageService>(); // Uniform error responses (RFC 7807) and a global exception handler.
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
var surrealOptions = new SurrealDbOptions // API versioning: `?api-version=1.0` or header `X-Api-Version`; default 1.0.
{ builder.Services
Endpoint = BuildEndpoint(surrealSettings), .AddApiVersioning(options =>
Namespace = surrealSettings.Namespace, {
Database = surrealSettings.Database, options.DefaultApiVersion = new ApiVersion(1, 0);
}; options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("api-version"),
new HeaderApiVersionReader("X-Api-Version"));
options.ReportApiVersions = true;
})
.AddMvc()
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
if (string.IsNullOrWhiteSpace(surrealSettings.RemoteUrl)) // OpenAPI document + Swagger UI (Swashbuckle), one document per API version.
{ builder.Services.AddEndpointsApiExplorer();
// Embedded RocksDB, persisted next to the running app (./data), never in memory. builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
var dataDirectory = Path.Combine(AppContext.BaseDirectory, surrealSettings.DataDirectory); builder.Services.AddSwaggerGen();
Directory.CreateDirectory(dataDirectory);
builder.Services
.AddSurreal(surrealOptions, ServiceLifetime.Singleton)
.AddRocksDbProvider();
}
else
{
// Remote SurrealDB server on its own dedicated port (used for sync with the frontend).
surrealOptions.Username = surrealSettings.Username;
surrealOptions.Password = surrealSettings.Password;
builder.Services.AddSurreal(surrealOptions, ServiceLifetime.Singleton);
}
var app = builder.Build(); var app = builder.Build();
using (var scope = app.Services.CreateScope()) using (var scope = app.Services.CreateScope())
{ {
var migrationRunner = scope.ServiceProvider.GetRequiredService<IMigrationRunner>();
var storageService = scope.ServiceProvider.GetRequiredService<IStorageService>(); var storageService = scope.ServiceProvider.GetRequiredService<IStorageService>();
await migrationRunner.ApplyAsync(CancellationToken.None);
await storageService.InitializeAsync(CancellationToken.None); await storageService.InitializeAsync(CancellationToken.None);
} }
// Configure the HTTP request pipeline. app.UseExceptionHandler();
if (app.Environment.IsDevelopment()) app.UseStatusCodePages();
app.UseSwagger();
app.UseSwaggerUI(options =>
{ {
app.MapOpenApi(); var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
}
else foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint(
$"/swagger/{description.GroupName}/swagger.json",
$"TarninaHub API {description.GroupName.ToUpperInvariant()}");
}
});
if (!app.Environment.IsDevelopment())
{ {
app.UseHttpsRedirection(); app.UseHttpsRedirection();
} }
@@ -68,15 +81,3 @@ app.MapControllers();
app.MapFallbackToFile("index.html"); app.MapFallbackToFile("index.html");
app.Run(); app.Run();
static string BuildEndpoint(SurrealDbSettings settings)
{
if (!string.IsNullOrWhiteSpace(settings.RemoteUrl))
{
return settings.RemoteUrl;
}
var dataDirectory = Path.Combine(AppContext.BaseDirectory, settings.DataDirectory);
return $"rocksdb://{dataDirectory.Replace('\\', '/')}";
}
+24 -3
View File
@@ -9,9 +9,30 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" /> <!-- Layered projects are nested under backend/ for deployment; exclude their
<PackageReference Include="SurrealDb.Embedded.RocksDb" Version="1.0.0" /> sources from the API host compile (they are referenced as projects). -->
<PackageReference Include="SurrealDb.Net" Version="1.0.0" /> <Compile Remove="Application/**/*.cs" />
<Compile Remove="Domain/**/*.cs" />
<Compile Remove="Infrastructure/**/*.cs" />
<Content Remove="Application/**" />
<Content Remove="Domain/**" />
<Content Remove="Infrastructure/**" />
</ItemGroup>
<ItemGroup>
<Content Include="migrations/**/*.surql" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc" Version="10.2.1" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.2.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="Application/TarninaHub.Application.csproj" />
<ProjectReference Include="Domain/TarninaHub.Domain.csproj" />
<ProjectReference Include="Infrastructure/TarninaHub.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+1 -1
View File
@@ -15,6 +15,6 @@ function run(command) {
run('npm --prefix client run build') run('npm --prefix client run build')
run('node scripts/copy-client.mjs') run('node scripts/copy-client.mjs')
run('dotnet build HubMi.slnx --configuration Release') run('dotnet build TarninaHub.slnx --configuration Release')
console.log('\n[build] Done.') console.log('\n[build] Done.')
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc;
using TarninaHub.Controllers;
namespace TarninaHub.Tests.Controllers;
public class HealthControllerTests
{
[Fact]
public void Get_ReturnsOkWithHealthyStatus()
{
var controller = new HealthController();
var result = controller.Get();
var ok = Assert.IsType<OkObjectResult>(result.Result);
var payload = Assert.IsType<HealthResponse>(ok.Value);
Assert.Equal("ok", payload.Status);
}
}
@@ -0,0 +1,34 @@
using TarninaHub.Infrastructure.Settings;
namespace TarninaHub.Tests.Infrastructure;
public class SurrealDbEndpointTests
{
[Fact]
public void Build_ReturnsRemoteUrl_WhenConfigured()
{
var settings = new SurrealDbSettings
{
RemoteUrl = "ws://127.0.0.1:8010/rpc",
};
var endpoint = SurrealDbEndpoint.Build(settings);
Assert.Equal("ws://127.0.0.1:8010/rpc", endpoint);
}
[Fact]
public void Build_ReturnsRocksDbEndpoint_WithForwardSlashes_WhenEmbedded()
{
var settings = new SurrealDbSettings
{
DataDirectory = "data",
};
var endpoint = SurrealDbEndpoint.Build(settings);
Assert.StartsWith("rocksdb://", endpoint);
Assert.DoesNotContain('\\', endpoint);
Assert.EndsWith("/data", endpoint);
}
}
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\backend\TarninaHub.csproj" />
<ProjectReference Include="..\..\backend\Infrastructure\TarninaHub.Infrastructure.csproj" />
</ItemGroup>
</Project>