# Stevef92 Fantasycritic Fantasycritic

> Add an Integration Test

- Skill: `tomevault-io/stevef92-fantasycritic-fantasycritic` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/stevef92-fantasycritic-fantasycritic`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/stevef92-fantasycritic-fantasycritic/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/stevef92-fantasycritic-fantasycritic

---


# Add an Integration Test

## Project layout

```
src/FantasyCritic.IntegrationTests/
  IntegrationTestBase.cs          ← base class, always inherit this
  FantasyCriticWebApplicationFactory.cs
  NullEmailSender.cs
  Helpers/
    ApiSession.cs                 ← HTTP client wrapper + typed client properties
    AntiForgeryHelper.cs
    LeagueFixture.cs              ← LeagueFixture + LeagueFixtureBuilder for league scenarios
    LeagueScenario.cs             ← league configuration presets
  Tests/
    Auth/AuthTests.cs             ← existing example
    Royale/RoyaleTests.cs         ← existing example (typed client pattern)
    League/
      Setup/                      ← league creation + member management
      Draft/                      ← draft scenarios (LeagueDraftTestBase)
      Actions/                    ← post-draft bid/drop processing
    <FeatureArea>/                ← add other areas here
src/FantasyCritic.ApiClient/
  Generated/FantasyCriticClients.cs  ← NSwag-generated typed clients + DTOs (do not edit)
  nswag.json                     ← NSwag config
```

## How the typed clients work

`ApiSession` exposes one typed client property per controller, all generated by NSwag:

| Property | Client class | Controller |
|----------|-------------|------------|
| `session.Royale` | `RoyaleClient` | `RoyaleController` |
| `session.RoyaleGroup` | `RoyaleGroupClient` | `RoyaleGroupController` |
| `session.League` | `LeagueClient` | `LeagueController` |
| `session.LeagueManager` | `LeagueManagerClient` | `LeagueManagerController` |
| `session.Account` | `AccountClient` | `AccountController` |
| `session.Game` | `GameClient` | `GameController` |
| `session.Admin` | `AdminClient` | `AdminController` |
| `session.General` | `GeneralClient` | `GeneralController` |
| `session.CombinedData` | `CombinedDataClient` | `CombinedDataController` |
| `session.Conference` | `ConferenceClient` | `ConferenceController` |
| `session.FactChecker` | `FactCheckerClient` | `FactCheckerController` |
| `session.ActionRunner` | `ActionRunnerClient` | `ActionRunnerController` |

All request and response types used by these clients come from the `FantasyCritic.ApiClient` namespace (the generated file). Do **not** reference `FantasyCritic.Web.Models.Requests.*` or `FantasyCritic.Lib.SharedSerialization.API.*` in test files.

## Step 1 – Check (and fix) the controller's response type annotations

Before writing any test code, check that every action you want to call has an explicit `[ProducesResponseType]` attribute for its success response. NSwag only generates a typed `Task<T>` return when it can see the response type in the OpenAPI spec — without the annotation it falls back to `Task` (void).

**If the generated method returns `Task` but you need a value back:**

Add `[ProducesResponseType<YourViewModel>(StatusCodes.Status200OK)]` (or `Status201Created`, etc.) to the controller action, then regenerate.

```csharp
// Generates Task (void) — can't use the return value in tests
[HttpPost]
public async Task<IActionResult> CreateThing([FromBody] CreateThingRequest request) { ... }

// Generates Task<ThingViewModel> — tests can assert on result
[HttpPost]
[ProducesResponseType<ThingViewModel>(StatusCodes.Status200OK)]
public async Task<IActionResult> CreateThing([FromBody] CreateThingRequest request) { ... }
```

If fixing the annotation would require more than just adding the attribute (restructuring the return path, etc.), **flag it and move on** — decide together how to handle it.

## Step 2 – Find the right typed client method

Look at the controller you want to test (e.g. `GameController.cs`), then check the corresponding generated client in `Generated/FantasyCriticClients.cs` for the method names. They follow the pattern `<ActionName>Async(...)`.

## Step 3 – Create the fixture

For simple API smoke tests, inherit `IntegrationTestBase` directly.

For league scenarios (draft, bids, drops), use `LeagueFixtureBuilder`:

```csharp
private LeagueFixture _league = null!;

[OneTimeSetUp]
public async Task SetUp()
{
    _league = await LeagueFixtureBuilder.CreateAndStartDraftAsync(
        Factory, LeagueScenarios.Standard, NewUser);
    await _league.DraftToCompletionAsync();
}

[OneTimeTearDown]
public async Task TearDown() => await _league.DisposeAsync();
```

Use `_league.Manager` for the league manager's session (e.g. `LeagueManager`, draft pause). Index `_league.Publishers` by draft order when you need a specific player's publisher — do not assume a fixed list index is the manager.

For full-draft completion with shared assertions, inherit `LeagueDraftTestBase`
in `Tests/League/Draft/` instead.

```csharp
using System;
using System.Threading.Tasks;
using FantasyCritic.ApiClient;          // ALL request/response types come from here
using FantasyCritic.IntegrationTests.Helpers;
using NUnit.Framework;

namespace FantasyCritic.IntegrationTests.Tests.<Area>;

[TestFixture]
public class <Area>Tests : IntegrationTestBase
{
    [Test]
    public async Task <Scenario>()
    {
        var (email, password, displayName) = NewUser();
        using var session = new ApiSession(Factory);
        await session.RegisterAsync(email, password, displayName);  // auto-signs-in

        // Call typed client methods — no URL strings, no anonymous objects
        var result = await session.<Area>.Create<X>Async(new Create<X>Request
        {
            // ...
        });

        Assert.That(result, Is.Not.EqualTo(Guid.Empty));

        var vm = await session.<Area>.Get<X>Async(result);
        Assert.That(vm.Name, Is.EqualTo("expected name"));
    }
}
```

### Real example (from RoyaleTests.cs)

```csharp
var activeQuarter = await session.Royale.ActiveRoyaleQuarterAsync();
var publisherID = await session.Royale.CreateRoyalePublisherAsync(
    new CreateRoyalePublisherRequest
    {
        Year = activeQuarter.Year,
        Quarter = activeQuarter.Quarter,
        PublisherName = $"Pub-{Guid.NewGuid():N}"[..20],
    });
var publisher = await session.Royale.GetRoyalePublisherAsync(publisherID);
Assert.That(publisher.PublisherName, Is.Not.Null);
```

## Step 4 – Regenerate the client (if you added/changed an endpoint)

```powershell
# From repo root
dotnet build src/FantasyCritic.Web/FantasyCritic.Web.csproj
scripts/Regenerate-ApiClient.ps1
dotnet build
```

## Step 5 – Run

```powershell
# From repo root — confirm Docker is running first
docker compose -f infrastructure/docker-compose-mysql.yaml up -d

# -c Release avoids Debug DLL lock conflicts with the dev web server
dotnet test src/FantasyCritic.IntegrationTests/FantasyCritic.IntegrationTests.csproj -c Release
```

Expected: all tests pass, no 5xx responses.

## ApiSession helpers reference

| Member | Use for |
|--------|---------|
| `RegisterAsync(email, pw, name)` | Create a user and sign in (session is authenticated after this) |
| `LoginAsync(email, pw)` | Sign in on a fresh session; returns `true` on success |
| `session.Royale.*Async(...)` | All Royale API calls via typed client |
| `session.Game.*Async(...)` | All Game/MasterGame API calls via typed client |
| `session.RoyaleGroup.*Async(...)` | All RoyaleGroup API calls via typed client |
| `session.FactChecker.*Async(...)` | All FactChecker admin-role API calls via typed client |
| *(other typed clients)* | Same pattern for every controller |
| `GetAsync(path)` | Raw GET fallback — only use if no typed client method exists |
| `PostJsonAsync<T>(path, body)` | Raw POST fallback — only use if no typed client method exists |

## Rules

- Always use `NewUser()` — never hardcode credentials
- Always use the typed client properties (`session.Royale.*`, `session.Game.*`, etc.) — no raw URL strings
- All request/response types come from `FantasyCritic.ApiClient` — no anonymous objects or `JObject`
- Always build state through the API — no direct DB inserts
- No teardown needed — GUID-based names never collide

---
> Source: [SteveF92/FantasyCritic](https://github.com/SteveF92/FantasyCritic) — distributed by [TomeVault](https://tomevault.io).
<!-- tomevault:4.0:skill_md:2026-06-29 -->

