Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

53 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tennis API

A REST API for tennis player statistics with a Blazor WebAssembly frontend, built as a technical test for L'Atelier.

Stack

Layer Technology
API ASP.NET Core 10, Vertical Slice, MediatR
Database PostgreSQL 16, EF Core 10, Npgsql
Frontend Blazor WebAssembly (hosted), Radzen UI
Auth JWT Bearer, role-based (Admin / Reader / Writer)
Tests xUnit, bUnit, ReqnRoll, Testcontainers
Observability Serilog, Swagger, health check
Deployment Docker Compose (local), Render (cloud)

Prerequisites


Run locally

# Clone
git clone https://github.com/vinvir/latelier.git
cd latelier

# Start API + database + Blazor frontend
docker compose up --build
URL Description
http://localhost:8080 Blazor UI
http://localhost:8080/swagger Swagger / OpenAPI
http://localhost:8080/health Health check

Reset and reseed the database

docker compose down -v && docker compose up --build

Development without Docker

Start PostgreSQL separately (or use docker compose up db -d), then:

# Apply migrations and run the API
cd src/Tennis.Api
dotnet run

The API reads appsettings.Development.json for the connection string and seed credentials. Create that file locally (it is gitignored):

{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Database=tennis;Username=tennis;Password=tennis"
  },
  "Jwt": {
    "Key": "dev-secret-key-must-be-at-least-32-chars!!",
    "Issuer": "tennis-api",
    "Audience": "tennis-client",
    "ExpiryMinutes": 60
  },
  "Seed": {
    "Users": [
      { "Username": "vviroleau",  "Password": "isagoodchoice", "Role": "Admin"  },
      { "Username": "readuser",   "Password": "letmeread",      "Role": "Reader" },
      { "Username": "writeuser",  "Password": "writeisright",   "Role": "Writer" }
    ]
  }
}

Test users

Username Password Role Permissions
vviroleau isagoodchoice Admin All endpoints
readuser letmeread Reader GET only
writeuser writeisright Writer GET + POST /players

API endpoints

All endpoints except /api/auth/login and /health require a Bearer token.

POST   /api/auth/login          Authenticate, receive JWT
GET    /api/players             List players (sorted, see below)
GET    /api/players/{id}        Get player by ID
POST   /api/players             Add a new player  [Writer, Admin]
GET    /api/stats               Aggregated statistics
POST   /api/admin/reset         Reset players to the original seed  [Admin]
GET    /health                  Health check

Sorting

GET /api/players accepts optional query parameters:

Parameter Values Default
sortBy Rank, Points, Age, Name Rank
order Asc, Desc Asc

Example: GET /api/players?sortBy=Points&order=Desc

Authentication

# 1. Login
curl -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"vviroleau","password":"isagoodchoice"}'

# 2. Use the returned token
curl http://localhost:8080/api/players \
  -H "Authorization: Bearer <token>"

Run tests

# All tests
dotnet test Tennis.sln

# Unit tests only (no Docker required)
dotnet test tests/Tennis.Api.Tests/Tennis.Api.Tests.csproj
dotnet test tests/Tennis.Client.Tests/Tennis.Client.Tests.csproj

# Integration tests (requires Docker for Testcontainers)
dotnet test tests/Tennis.IntegrationTests/Tennis.IntegrationTests.csproj
Project Type Count
tests/Tennis.Api.Tests xUnit unit tests — handlers, mappers, stats calculators, validators 29
tests/Tennis.Client.Tests bUnit Blazor component tests 5
tests/Tennis.IntegrationTests ReqnRoll BDD + Testcontainers (real PostgreSQL) 9

Coverage

Rider's dotCover reports 0% for Blazor WebAssembly components. Use the coverlet collector (via the provided runsettings) instead.

./coverage.sh           # runs all tests with coverage and renders coveragereport/index.html

Or manually:

dotnet test Tennis.sln --settings coverlet.runsettings --results-directory coverage-tmp
dotnet tool restore
dotnet reportgenerator -reports:"coverage-tmp/**/coverage.cobertura.xml" -targetdir:coveragereport

Architecture

latelier/
├── src/
│   ├── Tennis.Api/          ASP.NET Core — vertical slices
│   │   ├── Domain/          Exceptions (no anemic entities — slices read through EF models)
│   │   ├── Features/        One folder per use case (Query/Command + Handler [+ Validator])
│   │   ├── Infrastructure/  EF Core, mappers, seeder, security, caching,
│   │   │                    MediatR behaviors (logging / validation / cache invalidation),
│   │   │                    error handling (IExceptionHandler + ProblemDetails)
│   │   └── Extensions/      Composition-root helpers (auth, OpenAPI, migrate & seed)
│   ├── Tennis.Client/       Blazor WebAssembly (Radzen) — hosted by Tennis.Api
│   └── Tennis.Shared/       DTOs shared between API and client
└── tests/
    ├── Tennis.Api.Tests/        Unit tests
    ├── Tennis.Client.Tests/     Component tests
    └── Tennis.IntegrationTests/ BDD integration tests

Cross-cutting concerns (request logging with secret redaction, validation, and output-cache invalidation) are MediatR pipeline behaviors, so feature handlers stay focused on their use case. Key decisions are documented in docs/adr/.


Live demo

Deployed on Render: https://latelier-1btt.onrender.com

URL
/ Blazor UI
/health Health check

Sign in with any of the test users above — e.g. readuser / letmeread.

Note: The free tier sleeps after ~15 minutes of inactivity, so the first request may take ~30 seconds to wake.

About

tech test

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages