|
1 | | -import { Pool } from 'pg'; |
2 | | -import { IdDomain, generateId } from '../../src/services/db/id_gen'; |
| 1 | +import { Page } from '@playwright/test'; |
| 2 | +import { buildMapZip } from '../../src/services/maps/tests/map_generator'; |
3 | 3 |
|
4 | | -// Direct Postgres access to the running Supabase DB (connection comes from .env.e2e, layered in by |
5 | | -// tools/e2e.sh). Used to seed many maps quickly: uploading 20+ through the real submit flow would |
6 | | -// be far too slow just to populate a list for the pagination test. |
7 | | -let pool: Pool | undefined; |
8 | | -function getPool(): Pool { |
9 | | - if (pool == null) { |
10 | | - pool = new Pool({ |
11 | | - host: process.env.PGHOST, |
12 | | - port: Number(process.env.PGPORT), |
13 | | - user: process.env.PGUSER, |
14 | | - database: process.env.PGDATABASE, |
15 | | - password: process.env.PGPASSWORD, |
16 | | - }); |
17 | | - } |
18 | | - return pool; |
19 | | -} |
| 4 | +// Seeds maps through the real submit flow over HTTP (presigned PUT to S3 + server-side validation + |
| 5 | +// publish), rather than writing rows directly: it's slower than a raw insert but goes through the |
| 6 | +// exact code path production does, so it can't drift from the schema the way hand-written SQL can. |
| 7 | +// Uses `page.request` so it shares the (authenticated) browser context's cookies; the caller must |
| 8 | +// have logged the page in first. |
20 | 9 |
|
21 | 10 | export type SeededMap = { id: string; title: string }; |
22 | 11 |
|
23 | 12 | /** |
24 | | - * Inserts `count` public, valid maps that all share one (caller-supplied, run-unique) artist, so a |
25 | | - * search for that artist isolates exactly this run's maps from anything else in the persisted DB. |
| 13 | + * Uploads `count` valid maps, all under one (caller-supplied, run-unique) artist so a search for it |
| 14 | + * isolates exactly this run's maps. Returns the published map ids. |
26 | 15 | */ |
27 | | -export async function seedPublicMaps(opts: { |
28 | | - artist: string; |
29 | | - count: number; |
30 | | -}): Promise<SeededMap[]> { |
| 16 | +export async function seedPublicMaps( |
| 17 | + page: Page, |
| 18 | + opts: { artist: string; count: number } |
| 19 | +): Promise<SeededMap[]> { |
31 | 20 | const { artist, count } = opts; |
32 | 21 | const maps: SeededMap[] = []; |
33 | 22 | for (let i = 0; i < count; i++) { |
34 | | - const title = `Infinite Scroll Map ${String(i).padStart(3, '0')}`; |
35 | | - const id = await generateId( |
36 | | - IdDomain.MAPS, |
37 | | - async (candidate) => |
38 | | - ((await getPool().query('SELECT 1 FROM maps WHERE id = $1', [candidate])).rowCount ?? 0) > 0 |
39 | | - ); |
40 | | - if (id == null) { |
41 | | - throw new Error('Could not generate a unique map id for seeding'); |
| 23 | + const n = String(i).padStart(3, '0'); |
| 24 | + const title = `Infinite Scroll Map ${n}`; |
| 25 | + const zip = buildMapZip({ folder: `IScroll${n}`, title, artist }); |
| 26 | + |
| 27 | + // 1. Reserve a map id + presigned S3 upload URL. |
| 28 | + const submit = await page.request.post('/api/maps/submit', { data: { title: `${title}.zip` } }); |
| 29 | + const submitBody = await submit.json(); |
| 30 | + if (!submitBody.success) { |
| 31 | + throw new Error(`submit failed for "${title}": ${submitBody.errorMessage}`); |
| 32 | + } |
| 33 | + const { id, url } = submitBody as { id: string; url: string }; |
| 34 | + |
| 35 | + // 2. Upload the archive to the presigned URL (real S3 / Minio). |
| 36 | + const put = await page.request.put(url, { |
| 37 | + data: zip, |
| 38 | + headers: { 'Content-Type': 'application/zip' }, |
| 39 | + }); |
| 40 | + if (!put.ok()) { |
| 41 | + throw new Error(`S3 upload failed for "${title}": ${put.status()}`); |
| 42 | + } |
| 43 | + |
| 44 | + // 3. Validate + publish. |
| 45 | + const complete = await page.request.post('/api/maps/submit/complete', { |
| 46 | + data: { id, isReupload: false }, |
| 47 | + }); |
| 48 | + const completeBody = await complete.json(); |
| 49 | + if (!completeBody.success) { |
| 50 | + throw new Error(`complete failed for "${title}": ${completeBody.errorMessage}`); |
42 | 51 | } |
43 | | - await getPool().query( |
44 | | - `INSERT INTO maps |
45 | | - (id, visibility, validity, submission_date, title, artist, uploader, download_count, complexity) |
46 | | - VALUES ($1, 'public', 'valid', $2, $3, $4, 'e2e', 0, 1)`, |
47 | | - [id, new Date(Date.now() + i * 1000).toISOString(), title, artist] |
48 | | - ); |
49 | 52 | maps.push({ id, title }); |
50 | 53 | } |
51 | 54 | return maps; |
52 | 55 | } |
53 | 56 |
|
54 | | -export async function deleteSeededMaps(artist: string): Promise<void> { |
55 | | - await getPool().query('DELETE FROM maps WHERE artist = $1', [artist]); |
56 | | -} |
57 | | - |
58 | | -export async function closeSeedPool(): Promise<void> { |
59 | | - if (pool != null) { |
60 | | - await pool.end(); |
61 | | - pool = undefined; |
| 57 | +/** Deletes the seeded maps via the real delete endpoint (cascades difficulties/favorites + S3). */ |
| 58 | +export async function deleteSeededMaps(page: Page, ids: string[]): Promise<void> { |
| 59 | + for (const id of ids) { |
| 60 | + await page.request.post(`/api/maps/${id}/delete`); |
62 | 61 | } |
63 | 62 | } |
0 commit comments