Update outdated documentation (#255)

This commit is contained in:
Aaron Po
2026-08-29 23:50:05 -04:00
committed by GitHub
parent be672b6ca9
commit e5b354e322
7 changed files with 130 additions and 39 deletions

View File

@@ -50,6 +50,7 @@ Active areas in the repository:
- React 19 website (React Router 7 + Vite) - React 19 website (React Router 7 + Vite)
- Shared Biergarten theme system + Storybook coverage - Shared Biergarten theme system + Storybook coverage
- Auth flows and account/email integration (local Mailpit in dev compose) - Auth flows and account/email integration (local Mailpit in dev compose)
- Photo upload to S3-compatible object storage (local SeaweedFS in dev compose)
- Data generation pipeline with C++ and Llama.cpp - Data generation pipeline with C++ and Llama.cpp
Archived/reference areas: Archived/reference areas:
@@ -98,6 +99,7 @@ Backend access:
- API Swagger: http://localhost:8080/swagger - API Swagger: http://localhost:8080/swagger
- Health Check: http://localhost:8080/health - Health Check: http://localhost:8080/health
- Mailpit UI (dev SMTP): http://localhost:8025 - Mailpit UI (dev SMTP): http://localhost:8025
- SeaweedFS S3 API (dev object storage): http://localhost:8333
### Frontend (Node) ### Frontend (Node)

View File

@@ -35,10 +35,11 @@ For visual representations, see:
The backend organizes business capabilities as feature slices instead of The backend organizes business capabilities as feature slices instead of
technical layers. Each feature (`Features.Auth`, `Features.Breweries`, technical layers. Each feature (`Features.Auth`, `Features.Breweries`,
`Features.UserManagement`, `Features.Emails`, `Features.Locations`) is a single `Features.UserManagement`, `Features.Emails`, `Features.Locations`,
project that owns its own MediatR commands/queries/handlers, validators, and `Features.PhotoUpload`) is a single project that owns its own MediatR
repository end to end. `Features.Emails` and `Features.Locations` have no commands/queries/handlers, validators, and repository end to end.
`Controllers/` folder — both are invoked internally by other slices via `Features.Emails`, `Features.Locations`, and `Features.PhotoUpload` have no
`Controllers/` folder — all three are invoked internally by other slices via
MediatR/DI, never over HTTP: MediatR/DI, never over HTTP:
``` ```
@@ -48,15 +49,15 @@ MediatR/DI, never over HTTP:
│ - Swagger/OpenAPI, JWT auth middleware, global exception filter │ │ - Swagger/OpenAPI, JWT auth middleware, global exception filter │
└───────────────────────────────────────────────────────────────────────┘ └───────────────────────────────────────────────────────────────────────┘
↓ discovers controllers via AddApplicationPart ↓ discovers controllers via AddApplicationPart
┌───────────────┬───────────────┬───────────────────┬──────────────────┬──────────────────┐ ┌───────────────┬───────────────┬───────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ Features.Auth │Features. │ Features. │ Features.Emails │ Features. │ │ Features.Auth │Features. │ Features. │ Features.Emails │ Features. │ Features. │
│ │Breweries │ UserManagement │ (no controller, │ Locations │ │ │Breweries │ UserManagement │ (no controller, │ Locations │ PhotoUpload │
│ Controller │ Controller │ Controller │ internal only) │ (no controller, │ │ Controller │ Controller │ Controller │ internal only) │ (no controller, │ (no controller, │
│ Commands/ │ Commands/ │ Commands/ │ Commands/ │ internal only) │ │ Commands/ │ Commands/ │ Commands/ │ Commands/ │ internal only) │ internal only) │
│ Queries + │ Queries + │ Queries + │ Handlers │ Repository │ │ Queries + │ Queries + │ Queries + │ Handlers │ Repository │ Commands/ │
│ Handlers │ Handlers │ Handlers │ │ │ │ Handlers │ Handlers │ Handlers │ │ │ Handlers │
│ Repository │ Repository │ Repository │ EmailDispatcher │ │ │ Repository │ Repository │ Repository │ EmailDispatcher │ │ Repository │
└───────────────┴───────────────┴───────────────────┴──────────────────┴──────────────────┘ └───────────────┴───────────────┴───────────────────┴──────────────────┴──────────────────┴──────────────────┘
↓ each slice depends only on shared/domain/infra, never on another slice ↓ each slice depends only on shared/domain/infra, never on another slice
┌─────────────────────────┬─────────────────────────┬────────────────────┐ ┌─────────────────────────┬─────────────────────────┬────────────────────┐
│ Shared.Contracts │ Shared.Application │ Domain.Entities / │ │ Shared.Contracts │ Shared.Application │ Domain.Entities / │
@@ -109,7 +110,7 @@ own
- No controllers, no business logic, no feature-specific contracts - No controllers, no business logic, no feature-specific contracts
- Exists purely to host and wire up the feature slices - Exists purely to host and wire up the feature slices
#### Feature slices (`Features.Auth`, `Features.Breweries`, `Features.UserManagement`, `Features.Emails`, `Features.Locations`) #### Feature slices (`Features.Auth`, `Features.Breweries`, `Features.UserManagement`, `Features.Emails`, `Features.Locations`, `Features.PhotoUpload`)
**Purpose**: Each slice is the complete vertical for one business capability **Purpose**: Each slice is the complete vertical for one business capability
@@ -132,7 +133,12 @@ no `Controllers/` folder and no MediatR handlers — it exposes
`ILocationRepository` directly as a plain service, currently consumed only by `ILocationRepository` directly as a plain service, currently consumed only by
`Database.Seed` to look up cities while seeding brewery locations. Other slices `Database.Seed` to look up cities while seeding brewery locations. Other slices
(for example, `Features.Breweries`) do not yet depend on it; they run their own (for example, `Features.Breweries`) do not yet depend on it; they run their own
`CityID` existence checks inline. `CityID` existence checks inline. `Features.PhotoUpload` also has no
`Controllers/` folder — its `UploadPhotoCommand` is sent by other features'
handlers (for example, a future brewery photo upload command) rather than
bound to an HTTP route directly. The one exception today is the `/upload-test`
minimal API endpoint wired directly in `API.Core/Program.cs`, a temporary route
for exercising the upload path end to end.
**Dependencies**: **Dependencies**:
@@ -140,7 +146,8 @@ no `Controllers/` folder and no MediatR handlers — it exposes
- `Infrastructure.Sql` (generic ADO.NET plumbing) plus whichever infrastructure - `Infrastructure.Sql` (generic ADO.NET plumbing) plus whichever infrastructure
project the slice needs (`Infrastructure.Jwt`/ project the slice needs (`Infrastructure.Jwt`/
`Infrastructure.PasswordHashing` for Auth, `Infrastructure.Email`/ `Infrastructure.PasswordHashing` for Auth, `Infrastructure.Email`/
`Infrastructure.Email.Templates` for Emails) `Infrastructure.Email.Templates` for Emails, `Infrastructure.FileUpload` for
PhotoUpload)
- `Shared.Contracts`, `Shared.Application` - `Shared.Contracts`, `Shared.Application`
- **Never** another `Features.*` project - **Never** another `Features.*` project
@@ -184,11 +191,16 @@ whichever slices need them
- **Infrastructure.Email**: Email sending capabilities (SMTP/MailKit) - **Infrastructure.Email**: Email sending capabilities (SMTP/MailKit)
- **Infrastructure.Email.Templates**: Email template rendering (Razor - **Infrastructure.Email.Templates**: Email template rendering (Razor
components) components)
- **Infrastructure.FileUpload**: `IFileStorageProvider` abstraction over
S3-compatible object storage, implemented by `S3FileStorageProvider`
(targets the SeaweedFS container in dev; any S3-compatible endpoint in
other environments) using the AWS SDK's `AmazonS3Client` with
`ForcePathStyle = true`
**Dependencies**: **Dependencies**:
- Domain entities - Domain entities
- External libraries (ADO.NET, JWT, Argon2, MailKit, and so on) - External libraries (ADO.NET, JWT, Argon2, MailKit, AWSSDK.S3, and so on)
**Rules**: **Rules**:
@@ -212,6 +224,8 @@ whichever slices need them
- `BeerStyle` - A beer style/category (schema table exists; no repository yet) - `BeerStyle` - A beer style/category (schema table exists; no repository yet)
- `BeerPost` - A beer listing tied to a brewery and style (schema table - `BeerPost` - A beer listing tied to a brewery and style (schema table
exists; no repository yet) exists; no repository yet)
- `Photo` - A photo uploaded by a user account, written via
`Features.PhotoUpload`'s `IPhotoUploadRepository`
**Dependencies**: **Dependencies**:
@@ -266,8 +280,14 @@ only:
- `Features.Auth/Repository/IAuthRepository.cs` - `Features.Auth/Repository/IAuthRepository.cs`
- `Features.Breweries/Repository/IBreweryRepository.cs` - `Features.Breweries/Repository/IBreweryRepository.cs`
- `Features.UserManagement/Repository/IUserAccountRepository.cs` - `Features.UserManagement/Repository/IUserAccountRepository.cs`
- `Features.PhotoUpload/Repository/IPhotoUploadRepository.cs`
- `Database.Connection/DefaultSqlConnectionFactory.cs`: the generic connection - `Database.Connection/DefaultSqlConnectionFactory.cs`: the generic connection
factory every slice's repository builds on factory every slice's repository builds on
- `Database.Connection/DapperRepository.cs`: shared abstract base class every
repository extends, providing `CreateConnection()` and a
`RollbackQuietlyAsync()` helper that rolls back a transaction while
swallowing any exception the rollback itself raises, so the exception that
triggered the rollback is what propagates
**Benefits**: **Benefits**:
@@ -524,9 +544,11 @@ pipeline under `tooling/pipeline/` (see [Pipeline README](pipeline/README.md))
- User accounts (via `Features.Auth`'s repository) - User accounts (via `Features.Auth`'s repository)
- Brewery posts with locations (via `Features.Breweries`' repository) - Brewery posts with locations (via `Features.Breweries`' repository)
Tables that exist in the schema but have no repository yet (`Photo`, `Photo` rows are written via `Features.PhotoUpload`'s `IPhotoUploadRepository`,
`UserAvatar`, `UserFollow`, `BeerStyle`, `BeerPost`, `BeerPostPhoto`, but `Database.Seed` doesn't call it, so photos aren't part of the seed data.
`BeerPostComment`, `BreweryPostPhoto`) are not seeded. Tables that exist in the schema but have no repository yet (`UserAvatar`,
`UserFollow`, `BeerStyle`, `BeerPost`, `BeerPostPhoto`, `BeerPostComment`,
`BreweryPostPhoto`) are not seeded either.
## Deployment architecture ## Deployment architecture
@@ -539,6 +561,7 @@ Tables that exist in the schema but have no repository yet (`Photo`,
- `database.seed` - Data seeder - `database.seed` - Data seeder
- `api.core` - ASP.NET Core Web API - `api.core` - ASP.NET Core Web API
- `mailpit` - Local dev/test SMTP server + web UI (not used in production) - `mailpit` - Local dev/test SMTP server + web UI (not used in production)
- `seaweedfs` - S3-compatible object storage for uploaded photos (dev only)
**Environments**: **Environments**:
@@ -573,9 +596,9 @@ healthcheck:
│ Tests │ │ Tests │
├──────────────┤ ├──────────────┤
│ Unit Tests │ ← Features.Auth.Tests, Features.Breweries.Tests, │ Unit Tests │ ← Features.Auth.Tests, Features.Breweries.Tests,
│ (per slice, │ Features.UserManagement.Tests, Features.Emails.Tests │ (per slice, │ Features.UserManagement.Tests, Features.Emails.Tests,
│ handler │ (commands/queries/handlers, with repository/service │ handler │ Features.PhotoUpload.Tests (commands/queries/handlers,
│ tests) │ dependencies mocked via Moq) │ tests) │ with repository/service dependencies mocked via Moq)
└──────────────┘ └──────────────┘
``` ```

View File

@@ -33,6 +33,7 @@ updates (`UPDATE ... WHERE ... AND RowVersion = @RowVersion`).
| `City` | `Domain.Entities.City`, `Features.Locations` | Referenced by `BreweryPostLocation.CityID`; written via `ILocationRepository`, read via `ILocationRepository` and `IBreweryRepository`'s location joins, currently written only by `Database.Seed` | | `City` | `Domain.Entities.City`, `Features.Locations` | Referenced by `BreweryPostLocation.CityID`; written via `ILocationRepository`, read via `ILocationRepository` and `IBreweryRepository`'s location joins, currently written only by `Database.Seed` |
| `StateProvince` | `Domain.Entities.StateProvince`, `Features.Locations` | Referenced by `City.StateProvinceID`; created on demand by `ILocationRepository.GetOrCreateCityIdAsync`, read via `IBreweryRepository`'s location joins | | `StateProvince` | `Domain.Entities.StateProvince`, `Features.Locations` | Referenced by `City.StateProvinceID`; created on demand by `ILocationRepository.GetOrCreateCityIdAsync`, read via `IBreweryRepository`'s location joins |
| `Country` | `Domain.Entities.Country`, `Features.Locations` | Referenced by `StateProvince.CountryID`; created on demand by `ILocationRepository.GetOrCreateCityIdAsync`, read via `IBreweryRepository`'s location joins | | `Country` | `Domain.Entities.Country`, `Features.Locations` | Referenced by `StateProvince.CountryID`; created on demand by `ILocationRepository.GetOrCreateCityIdAsync`, read via `IBreweryRepository`'s location joins |
| `Photo` | `Domain.Entities.Photo`, `Features.PhotoUpload` | Written via `IPhotoUploadRepository.CreateAsync`, sent as an `UploadPhotoCommand` from other features' handlers; not yet seeded by `Database.Seed` |
### Tables defined in the schema but not yet wired to a feature slice ### Tables defined in the schema but not yet wired to a feature slice
@@ -41,7 +42,6 @@ repository or seed data yet:
| Table | Purpose (from schema) | | Table | Purpose (from schema) |
| ------------------ | -------------------------------------------------------------------------------------------------------- | | ------------------ | -------------------------------------------------------------------------------------------------------- |
| `Photo` | A photo uploaded by a user account |
| `UserAvatar` | Links a `UserAccount` to a `Photo` as its avatar | | `UserAvatar` | Links a `UserAccount` to a `Photo` as its avatar |
| `UserFollow` | One user following another (`CannotFollowOwnAccount` check constraint) | | `UserFollow` | One user following another (`CannotFollowOwnAccount` check constraint) |
| `BeerStyle` | A beer style/category; has a `Domain.Entities.BeerStyle` type, but no repository yet | | `BeerStyle` | A beer style/category; has a `Domain.Entities.BeerStyle` type, but no repository yet |
@@ -66,8 +66,9 @@ an already-applied script means it won't re-run against existing databases.
`Database.Seed` populates a target database from data produced by the C++ `Database.Seed` populates a target database from data produced by the C++
pipeline (see [Pipeline README](../pipeline/README.md)): countries, pipeline (see [Pipeline README](../pipeline/README.md)): countries,
state/provinces, and cities (created on demand while resolving each brewery's state/provinces, and cities (created on demand while resolving each brewery's
location), user accounts, and brewery posts with locations. It only seeds location), user accounts, and brewery posts with locations. It doesn't call
tables that have a feature slice to write through (see the table above). every feature slice that has a write path — `Photo`, for example, has a
repository (`Features.PhotoUpload`) but no seed data.
## Database error handling ## Database error handling

View File

@@ -29,6 +29,7 @@ representation.
- Swagger UI enabled - Swagger UI enabled
- Seed data included - Seed data included
- Local Mailpit SMTP server for dev email testing - Local Mailpit SMTP server for dev email testing
- Local SeaweedFS S3-compatible object storage for photo uploads
- `CLEAR_DATABASE=true` (drops and recreates schema) - `CLEAR_DATABASE=true` (drops and recreates schema)
**Services**: **Services**:
@@ -39,6 +40,7 @@ database.migrations # DbUp migrations
database.seed # Seed initial data database.seed # Seed initial data
api.core # Web API (ports 8080, 8081) api.core # Web API (ports 8080, 8081)
mailpit # Local dev SMTP server + UI (port 8025) mailpit # Local dev SMTP server + UI (port 8025)
seaweedfs # S3-compatible object storage (ports 8333, 8888, 9333, 23646)
``` ```
**Start Development Environment**: **Start Development Environment**:
@@ -53,6 +55,10 @@ docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up -d
- Health Check: http://localhost:8080/health - Health Check: http://localhost:8080/health
- Mailpit UI: http://localhost:8025 - Mailpit UI: http://localhost:8025
- SQL Server: localhost:1434 (sa credentials from .env.dev) - SQL Server: localhost:1434 (sa credentials from .env.dev)
- SeaweedFS S3 API: http://localhost:8333 (credentials from
`SEAWEEDFS_ACCESS_KEY_ID`/`SEAWEEDFS_SECRET_ACCESS_KEY` in .env.dev)
- SeaweedFS Filer UI: http://localhost:8888
- SeaweedFS Master UI: http://localhost:9333
**Stop Environment**: **Stop Environment**:
@@ -182,8 +188,12 @@ graph TD
A[sqlserver: health check passes] --> B[database.migrations: completes successfully] A[sqlserver: health check passes] --> B[database.migrations: completes successfully]
B --> C[database.seed: completes successfully] B --> C[database.seed: completes successfully]
C --> D[api.core / tests: start when ready] C --> D[api.core / tests: start when ready]
E[seaweedfs: container started] --> D
``` ```
In development, `api.core` also depends on `seaweedfs` (`condition:
service_started`, not a health check) alongside `database.seed`.
**Health Check Example** (SQL Server): **Health Check Example** (SQL Server):
```yaml ```yaml
@@ -216,6 +226,8 @@ api.core:
- `sqlserverdata-dev` - Database files persist between restarts - `sqlserverdata-dev` - Database files persist between restarts
- `nuget-cache-dev` - NuGet package cache (speeds up builds) - `nuget-cache-dev` - NuGet package cache (speeds up builds)
- `seaweedfsdata-dev` - SeaweedFS object storage data persists between
restarts
**Testing**: **Testing**:
@@ -262,6 +274,10 @@ environment:
ACCESS_TOKEN_SECRET: "${ACCESS_TOKEN_SECRET}" ACCESS_TOKEN_SECRET: "${ACCESS_TOKEN_SECRET}"
REFRESH_TOKEN_SECRET: "${REFRESH_TOKEN_SECRET}" REFRESH_TOKEN_SECRET: "${REFRESH_TOKEN_SECRET}"
CONFIRMATION_TOKEN_SECRET: "${CONFIRMATION_TOKEN_SECRET}" CONFIRMATION_TOKEN_SECRET: "${CONFIRMATION_TOKEN_SECRET}"
SEAWEEDFS_SERVICE_URL: "${SEAWEEDFS_SERVICE_URL}"
SEAWEEDFS_ACCESS_KEY_ID: "${SEAWEEDFS_ACCESS_KEY_ID}"
SEAWEEDFS_SECRET_ACCESS_KEY: "${SEAWEEDFS_SECRET_ACCESS_KEY}"
SEAWEEDFS_BUCKET: "${SEAWEEDFS_BUCKET}"
``` ```
For complete list, see [Environment Variables](environment-variables.md). For complete list, see [Environment Variables](environment-variables.md).

View File

@@ -35,8 +35,9 @@ Environment-specific `.env` files loaded via `env_file:` in docker-compose.yaml:
`web/generate-env.sh` copies `.env.example` to a target file and replaces the `web/generate-env.sh` copies `.env.example` to a target file and replaces the
secret-bearing values (`DB_PASSWORD`, `ACCESS_TOKEN_SECRET`, secret-bearing values (`DB_PASSWORD`, `ACCESS_TOKEN_SECRET`,
`REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`, `SMTP_USERNAME`, `REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`, `SMTP_USERNAME`,
`SMTP_PASSWORD`) with freshly randomized ones via `openssl`. Non-secret values `SMTP_PASSWORD`, `SEAWEEDFS_ACCESS_KEY_ID`, `SEAWEEDFS_SECRET_ACCESS_KEY`)
are left untouched. The output file is written with `600` permissions. with freshly randomized ones via `openssl`. Non-secret values are left
untouched. The output file is written with `600` permissions.
```bash ```bash
cd web cd web
@@ -221,6 +222,25 @@ SMTP_FROM_NAME=The Biergarten App # Optional, defaults to "The Biergarten
- **Production**: point at a real provider (SendGrid, Mailgun, Amazon SES, and - **Production**: point at a real provider (SendGrid, Mailgun, Amazon SES, and
so on) so on)
### File storage configuration (backend)
Read by `Infrastructure.FileUpload/S3FileStorageProvider.cs` for uploading and
retrieving photos via an S3-compatible object store.
```bash
SEAWEEDFS_SERVICE_URL=http://seaweedfs:8333 # Required, no default
SEAWEEDFS_ACCESS_KEY_ID=admin # Required, no default
SEAWEEDFS_SECRET_ACCESS_KEY=secret # Required, no default
SEAWEEDFS_BUCKET=my-bucket # Required, no default
```
- **Implementation**: `S3FileStorageProvider` throws on construction if any of
these four values is missing
- **Local dev**: point at the `seaweedfs` Docker service (S3 API on port 8333)
- **Production**: point at any S3-compatible endpoint (AWS S3, a hosted
SeaweedFS cluster, and so on); `ForcePathStyle` is always enabled, which
SeaweedFS requires and AWS S3 also accepts
## Docker-specific variables ## Docker-specific variables
### SQL Server container ### SQL Server container
@@ -288,6 +308,10 @@ reference only and is not run as part of the active stack.
| `SMTP_USE_SSL` | ✓ | | ✓ | No | Defaults to `true` | | `SMTP_USE_SSL` | ✓ | | ✓ | No | Defaults to `true` |
| `SMTP_FROM_EMAIL` | ✓ | | ✓ | Yes | Email sender address | | `SMTP_FROM_EMAIL` | ✓ | | ✓ | Yes | Email sender address |
| `SMTP_FROM_NAME` | ✓ | | ✓ | No | Defaults to `The Biergarten` | | `SMTP_FROM_NAME` | ✓ | | ✓ | No | Defaults to `The Biergarten` |
| `SEAWEEDFS_SERVICE_URL` | ✓ | | ✓ | Yes | S3-compatible endpoint URL |
| `SEAWEEDFS_ACCESS_KEY_ID` | ✓ | | ✓ | Yes | S3 access key |
| `SEAWEEDFS_SECRET_ACCESS_KEY` | ✓ | | ✓ | Yes | S3 secret key |
| `SEAWEEDFS_BUCKET` | ✓ | | ✓ | Yes | S3 bucket name |
| `API_BASE_URL` | | ✓ | | Yes | Website-to-API base URL | | `API_BASE_URL` | | ✓ | | Yes | Website-to-API base URL |
| `SESSION_SECRET` | | ✓ | | Yes | Website session signing | | `SESSION_SECRET` | | ✓ | | Yes | Website session signing |
| `NODE_ENV` | | ✓ | | No | Runtime mode | | `NODE_ENV` | | ✓ | | No | Runtime mode |
@@ -309,8 +333,10 @@ reference only and is not run as part of the active stack.
Variables are validated at startup: Variables are validated at startup:
- Missing required variables (`ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, - Missing required variables (`ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`,
`CONFIRMATION_TOKEN_SECRET`, `SMTP_HOST`, `SMTP_FROM_EMAIL`, DB connection `CONFIRMATION_TOKEN_SECRET`, `SMTP_HOST`, `SMTP_FROM_EMAIL`,
values) cause the application to fail with an `InvalidOperationException` `SEAWEEDFS_SERVICE_URL`, `SEAWEEDFS_ACCESS_KEY_ID`,
`SEAWEEDFS_SECRET_ACCESS_KEY`, `SEAWEEDFS_BUCKET`, DB connection values)
cause the application to fail with an `InvalidOperationException`
- No minimum length is enforced on the token secrets in code; the "minimum 32 - No minimum length is enforced on the token secrets in code; the "minimum 32
characters" guidance above is a recommendation, not an enforced check characters" guidance above is a recommendation, not an enforced check
@@ -347,6 +373,12 @@ SMTP_USE_SSL=false
SMTP_FROM_EMAIL=noreply@thebiergarten.app SMTP_FROM_EMAIL=noreply@thebiergarten.app
SMTP_FROM_NAME=The Biergarten App SMTP_FROM_NAME=The Biergarten App
# SeaweedFS (S3-compatible storage)
SEAWEEDFS_SERVICE_URL=http://seaweedfs:8333
SEAWEEDFS_ACCESS_KEY_ID=<generated-with-openssl>
SEAWEEDFS_SECRET_ACCESS_KEY=<generated-with-openssl>
SEAWEEDFS_BUCKET=my-bucket
# Migration # Migration
CLEAR_DATABASE=true CLEAR_DATABASE=true

View File

@@ -58,14 +58,16 @@ section for details on `generate-env.sh`.
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up -d docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up -d
``` ```
This starts SQL Server, migrations, seeding, the API, and Mailpit (local dev This starts SQL Server, migrations, seeding, the API, Mailpit (local dev
SMTP). SMTP), and SeaweedFS (local dev S3-compatible object storage for photo
uploads).
Available endpoints: Available endpoints:
- API Swagger: http://localhost:8080/swagger - API Swagger: http://localhost:8080/swagger
- Health Check: http://localhost:8080/health - Health Check: http://localhost:8080/health
- Mailpit UI: http://localhost:8025 - Mailpit UI: http://localhost:8025
- SeaweedFS S3 API: http://localhost:8333
### 4. Start the active frontend ### 4. Start the active frontend
@@ -124,6 +126,10 @@ export ACCESS_TOKEN_SECRET="<generated>"
export REFRESH_TOKEN_SECRET="<generated>" export REFRESH_TOKEN_SECRET="<generated>"
export CONFIRMATION_TOKEN_SECRET="<generated>" export CONFIRMATION_TOKEN_SECRET="<generated>"
export WEBSITE_BASE_URL="http://localhost:3000" export WEBSITE_BASE_URL="http://localhost:3000"
export SEAWEEDFS_SERVICE_URL="http://localhost:8333"
export SEAWEEDFS_ACCESS_KEY_ID="<generated>"
export SEAWEEDFS_SECRET_ACCESS_KEY="<generated>"
export SEAWEEDFS_BUCKET="my-bucket"
``` ```
### 2. Run migrations and seed ### 2. Run migrations and seed

View File

@@ -11,9 +11,9 @@ The project uses a multi-layered testing approach across backend and frontend:
live, seeded database live, seeded database
- **Features.\*.Tests** - One unit test project per backend feature slice - **Features.\*.Tests** - One unit test project per backend feature slice
(`Features.Auth.Tests`, `Features.Breweries.Tests`, (`Features.Auth.Tests`, `Features.Breweries.Tests`,
`Features.UserManagement.Tests`, `Features.Emails.Tests`), covering that `Features.UserManagement.Tests`, `Features.Emails.Tests`,
slice's command/query handlers, with dependencies mocked via Moq (no real `Features.PhotoUpload.Tests`), covering that slice's command/query
database required) handlers, with dependencies mocked via Moq (no real database required)
- **Storybook Vitest project** - Browser-based interaction tests for shared - **Storybook Vitest project** - Browser-based interaction tests for shared
website stories website stories
- **Storybook Playwright suite** - Browser checks against Storybook-rendered - **Storybook Playwright suite** - Browser checks against Storybook-rendered
@@ -49,6 +49,7 @@ cat test-results/Features.Auth.Tests.trx
cat test-results/Features.Breweries.Tests.trx cat test-results/Features.Breweries.Tests.trx
cat test-results/Features.UserManagement.Tests.trx cat test-results/Features.UserManagement.Tests.trx
cat test-results/Features.Emails.Tests.trx cat test-results/Features.Emails.Tests.trx
cat test-results/Features.PhotoUpload.Tests.trx
``` ```
### Clean up ### Clean up
@@ -86,6 +87,7 @@ dotnet test Features/Features.Auth.Tests/Features.Auth.Tests.csproj
dotnet test Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj dotnet test Features/Features.Breweries.Tests/Features.Breweries.Tests.csproj
dotnet test Features/Features.UserManagement.Tests/Features.UserManagement.Tests.csproj dotnet test Features/Features.UserManagement.Tests/Features.UserManagement.Tests.csproj
dotnet test Features/Features.Emails.Tests/Features.Emails.Tests.csproj dotnet test Features/Features.Emails.Tests/Features.Emails.Tests.csproj
dotnet test Features/Features.PhotoUpload.Tests/Features.PhotoUpload.Tests.csproj
# Or run all of them at once via the solution: # Or run all of them at once via the solution:
for proj in Features/*.Tests; do dotnet test "$proj"; done for proj in Features/*.Tests; do dotnet test "$proj"; done
@@ -175,6 +177,14 @@ The default (`headless: true`) is set in `web/frontend/vite.config.ts`.
- Registration and resend-confirmation email dispatch handlers - Registration and resend-confirmation email dispatch handlers
**Features.PhotoUpload.Tests**:
- Upload command handler: storage key construction, content type passthrough,
and photo persistence via the mocked storage provider and repository
- Upload validator: min/max file size bounds, and PNG/JPEG/WebP signature
checks (including rejection of files with a mismatched or missing
signature)
**Frontend UI Coverage**: **Frontend UI Coverage**:
- Shared submit button states - Shared submit button states
@@ -188,7 +198,6 @@ The default (`headless: true`) is set in `web/frontend/vite.config.ts`.
- [ ] Password reset functionality - [ ] Password reset functionality
- [ ] Beer post operations - [ ] Beer post operations
- [ ] User follow/unfollow - [ ] User follow/unfollow
- [ ] Image upload service
- [ ] Frontend route integration coverage beyond Storybook stories - [ ] Frontend route integration coverage beyond Storybook stories
## Testing frameworks and tools ## Testing frameworks and tools
@@ -276,9 +285,11 @@ Features.Auth.Tests/
└── TokenServiceValidationTests.cs └── TokenServiceValidationTests.cs
``` ```
Each of the other three slices (`Features.Breweries.Tests`, Each of the other slices (`Features.Breweries.Tests`,
`Features.UserManagement.Tests`, `Features.Emails.Tests`) follows the same `Features.UserManagement.Tests`, `Features.Emails.Tests`,
shape: a `Commands/`/`Queries/` folder with one test file per handler. `Features.PhotoUpload.Tests`) follows the same shape: a `Commands/`/`Queries/`
folder with one test file per handler (`Features.PhotoUpload.Tests` also adds
a validator test file alongside its handler test).
## Writing tests ## Writing tests