Files
HIPCTF2/docs/database/seed-sample-challenges-migration.md
T

7.1 KiB

type, title, description, tags, timestamp
type title description tags timestamp
database Seed Sample Challenges Migration (1700000000600) Forward-only migration that inserts a representative sample set of challenges on a fresh database so the /challenges board renders cards without manual admin action.
database
migration
challenge
seed
sample
2026-07-23T02:23:34Z

Purpose

A brand-new HIPCTF database (default path ./data/db.sqlite) has no challenge rows until an admin imports or creates them. The SeedSampleChallenges1700000000600 migration bridges that gap by inserting a small, representative sample set so the authenticated /challenges board (see Challenges Board) already has cards on the very first visit and the public landing can demonstrate the platform with real content.

The migration runs at the end of the MIGRATIONS array in DatabaseModule so every other category / challenge schema migration has already landed and the category rows it joins against are present.

File

backend/src/database/migrations/1700000000600-SeedSampleChallenges.ts

Registered last in backend/src/database/database.module.ts so it runs after UpgradeChallengeAdminSchema1700000000500.

Sample set

Eight challenges, one or more per canonical system category. Source of truth is the typed SAMPLE_CHALLENGES array exported by the migration:

Name system_key Difficulty initial_points minimum_points decay_solves Flag
Alpha Cipher CRY LOW 200 100 5 flag{alpha}
Beta Cipher CRY MEDIUM 300 100 10 flag{beta}
Zeta Key CRY HIGH 500 100 10 flag{zeta}
Web Welcome WEB LOW 100 50 10 flag{web1}
Mobile Mayhem PWN MEDIUM 300 100 10 flag{mob1}
Hardware Hello HW LOW 100 50 10 flag{hw1}
Markdown Mystery MSC LOW 100 50 10 flag{md}
Reverse Ranger REV MEDIUM 300 100 10 flag{rev1}

All rows are inserted with protocol='WEB', port=NULL, ip_address='', and enabled=1 so they appear on the board the moment the event window opens (see Event Window). Categories are resolved by system_key rather than UUID so the seed works regardless of the UUIDs the category migration assigned at install time. Sample flags follow the flag{<token>} convention and are exposed only inside admin views — the public challenge DTO continues to strip them (see Challenges Endpoints).

What the up() does

  1. Idempotency guardSELECT COUNT(*) FROM "challenge". If the table already contains rows the migration returns immediately; an admin that imported a custom challenge set first is never overwritten.
  2. System-category lookupSELECT id, system_key FROM "category" WHERE system_key IS NOT NULL and builds an in-memory Map<system_key, id>. If fewer than six canonical keys are present (the seed cannot bind) the migration aborts as a no-op rather than inserting challenges into the wrong category.
  3. Insert sample rows — for each entry in SAMPLE_CHALLENGES, an INSERT INTO "challenge" with a fresh UUID v4, ISO 8601 created_at / updated_at via strftime('%Y-%m-%dT%H:%M:%fZ','now'), and enabled=1. Categories without a matching system_key are silently skipped (defensive — only happens if the canonical six are missing).

Idempotency

  • Running the migration twice (or restarting after it has been recorded) leaves the same eight rows. The early-return count guard skips the second run.
  • Re-running the migration after a custom challenge row exists also no-ops, so manually-imported content from the Admin — Challenges Full-Replace Import flow is preserved.

down()

Scoped to the seeded set: DELETE FROM "challenge" WHERE "name" IN (...) using the eight fixed names from SAMPLE_CHALLENGES. It deliberately matches only the sample names so unrelated admin rows are not removed.

Tests

File What it asserts
tests/backend/migrations.spec.ts"seeds sample challenges on a fresh database (Job 906)" After all migrations run on an in-memory SQLite DB, the eight expected sample names are present.
tests/backend/migrations.spec.ts"seeded sample challenges are enabled, schema-compatible, and point at the right categories (Job 906)" Every seeded row has enabled=true, a valid difficulty, initial_points >= minimum_points, a non-empty flag, and a category_id that resolves to one of the six canonical system categories.
tests/backend/migrations.spec.ts"SeedSampleChallenges down() scope (Job 906)" down() removes only rows whose name is in the seed list and leaves manually-inserted rows of similar names (e.g. Other) intact.
tests/backend/challenges-board.spec.ts Board-listing test now allows extra seeded cards inside the CRY column (Alpha Cipher, Beta Cipher, Zeta Key) while still asserting that the hand-inserted alphacipher LOW and Zeta-key HIGH rows occupy the bookended slots and that the disabled hidden row is excluded.

Operational notes

  • On a fresh database the seed fires before the first app boot, so GET /api/v1/challenges/board returns the sample columns immediately and the player-facing Challenges Board shows eight cards. An admin deleting every challenge is the only way to reach an empty board.
  • To confirm on the command line after npm run setup:
    sqlite3 ./data/db.sqlite 'SELECT COUNT(*) FROM challenge;'
    
    Expected count: 8. Re-running setup on a populated DB leaves the count unchanged (idempotent guard).
  • Played flags are usable for end-to-end demos: e.g. submitting flag{alpha} against the Alpha Cipher card immediately awards points and broadcasts the solve on the Scoreboard Stream.

See also