veza/tests/e2e/29-dmca-notice.spec.ts
senke 49335322b5
Some checks failed
Veza CI / Notify on failure (push) Blocked by required conditions
Veza CI / Rust (Stream Server) (push) Successful in 5m33s
Security Scan / Secret Scanning (gitleaks) (push) Failing after 1m0s
Veza CI / Backend (Go) (push) Failing after 9m37s
Veza CI / Frontend (Web) (push) Has been cancelled
E2E Playwright / e2e (full) (push) Has been cancelled
feat(legal): DMCA notice handler + admin queue + 451 playback gate (W3 Day 14)
End-to-end DMCA workflow. Public submission, admin queue, takedown
flips track to is_public=false + dmca_blocked=true, playback paths
return 451 Unavailable For Legal Reasons.

Backend
- migrations/988_dmca_notices.sql + rollback : table dmca_notices
  (id, status, claimant_*, work_description, infringing_track_id FK,
  sworn_statement_at, takedown_at, counter_notice_at, restored_at,
  audit_log JSONB, created_at, updated_at). Adds tracks.dmca_blocked
  BOOLEAN. Partial indexes for the pending queue + per-track lookup.
  Status enum constrained via CHECK.
- internal/models/dmca_notice.go + DmcaBlocked field on Track.
- internal/services/dmca_service.go : CreateNotice + ListPending +
  Takedown + Dismiss. Takedown is a single transaction that flips the
  track's flags AND appends an audit_log entry — partial state can't
  happen if the track was deleted between fetch and update.
- internal/handlers/dmca_handler.go : POST /api/v1/dmca/notice (public),
  GET /api/v1/admin/dmca/notices (paginated), POST /:id/takedown,
  POST /:id/dismiss. sworn_statement=false → 400. Conflict → 409.
  Track gone after notice → 410.
- internal/api/routes_legal.go : route registration. Admin chain :
  RequireAuth + RequireAdmin + RequireMFA (same as moderation routes).
- internal/core/track/track_hls_handler.go : both StreamTrack +
  DownloadTrack now early-return 451 when track.DmcaBlocked. Owner
  cannot bypass — only an admin restoring the notice clears the gate.
- internal/services/dmca_service_test.go : audit_log append helpers,
  malformed-JSON rejection, ordering preservation.

Frontend
- apps/web/src/features/legal/pages/DmcaNoticePage.tsx : public form
  at /legal/dmca/notice. Validates sworn-statement checkbox client-side.
  Receipt panel shows the notice ID after submission.
- apps/web/src/services/api/dmca.ts : thin client (POST /dmca/notice).
- routeConfig + lazy registry updated for the new route.
- DmcaPage now links to /legal/dmca/notice instead of saying "form
  pending".

E2E
- tests/e2e/29-dmca-notice.spec.ts : 3 tests. (1) anonymous submit
  yields 201 + pending receipt. (2) sworn_statement=false rejected
  with 400. (3) admin takedown gates playback with 451 — gated behind
  E2E_DMCA_ADMIN=1 because admin path requires MFA-bearing seed.

Acceptance (Day 14) : public submission produces a pending notice,
admin takedown blocks playback at 451. Lab-side validation pending
admin MFA seed for the e2e admin pathway.

W3 progress : Redis Sentinel ✓ · MinIO distribué ✓ · CDN ✓ · DMCA ✓ ·
embed  Day 15.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:39:33 +02:00

150 lines
6 KiB
TypeScript

import { test, expect } from '@chromatic-com/playwright';
import { CONFIG } from './helpers';
/**
* v1.0.9 W3 Day 14 — DMCA workflow E2E.
*
* Roundtrip :
* 1. Anonymous user submits notice via POST /api/v1/dmca/notice.
* 2. Admin logs in (MFA-bearing account from seed) and lists pending
* notices via GET /api/v1/admin/dmca/notices.
* 3. Admin honors the takedown via POST /api/v1/admin/dmca/notices/:id/takedown.
* 4. The previously-public track is now 451-gated on
* GET /api/v1/tracks/:id/stream.
*
* The test gates itself behind two conditions :
* - Admin requires MFA (RequireMFA is in the chain). If the seed
* admin doesn't carry an MFA-bypassing token in this environment,
* the admin-side calls 403 ; we skip rather than report a false
* red. Set E2E_DMCA_ADMIN=1 once MFA seeding is in CI.
* - The seed creator track must exist + be public. We pick the first
* track returned by /api/v1/tracks for the creator login.
*/
const ADMIN_AVAILABLE = process.env.E2E_DMCA_ADMIN === '1';
interface ApiEnvelope<T> {
success?: boolean;
data: T;
error?: { code?: string; message?: string };
}
async function login(
request: import('@playwright/test').APIRequestContext,
email: string,
password: string,
): Promise<{ accessToken: string }> {
const resp = await request.post(`${CONFIG.apiURL}/api/v1/auth/login`, {
data: { email, password, remember_me: false },
});
expect(resp.status()).toBe(200);
const body = (await resp.json()) as ApiEnvelope<{ token: { access_token: string } }>;
const data = body?.data ?? (body as unknown as { token: { access_token: string } });
const accessToken: string = data?.token?.access_token ?? '';
expect(accessToken.length).toBeGreaterThan(0);
return { accessToken };
}
test.describe('LEGAL — DMCA notice workflow (v1.0.9 W3 Day 14)', () => {
test('29. anonymous notice submission produces a pending receipt', async ({ request }) => {
test.setTimeout(30_000);
const resp = await request.post(`${CONFIG.apiURL}/api/v1/dmca/notice`, {
data: {
claimant_name: 'E2E Tester',
claimant_email: `e2e_dmca_${Date.now()}@example.com`,
claimant_address: '1 Test Lane, Testville, Testland',
work_description:
'E2E test work description — this is a DMCA test notice generated by the playwright suite. Not a real claim.',
sworn_statement: true,
},
});
expect(resp.status(), 'public DMCA submission must accept a well-formed notice').toBe(201);
const body = (await resp.json()) as ApiEnvelope<{ id: string; status: string }>;
const receipt = body?.data ?? (body as unknown as { id: string; status: string });
expect(receipt.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
expect(receipt.status).toBe('pending');
});
test('30. sworn_statement=false is rejected (DMCA § 512(c)(3)(A)(vi))', async ({ request }) => {
const resp = await request.post(`${CONFIG.apiURL}/api/v1/dmca/notice`, {
data: {
claimant_name: 'E2E Tester',
claimant_email: `e2e_dmca_${Date.now()}@example.com`,
claimant_address: '1 Test Lane',
work_description: 'A description that meets the min length requirement easily.',
sworn_statement: false,
},
});
// The binding=required on a boolean treats `false` as the zero
// value, so gin's validator returns 400 — same shape we'd get for
// any malformed payload.
expect(resp.status()).toBe(400);
});
test('31. admin takedown gates playback with 451', async ({ request }) => {
test.setTimeout(60_000);
if (!ADMIN_AVAILABLE) {
test.skip(
true,
'Set E2E_DMCA_ADMIN=1 once admin MFA bypass for tests is wired in the CI seed.',
);
}
// Step 1 : creator publishes a track. Reuse the first track from
// the seeded creator account ; we don't upload a fresh one to keep
// the test fast.
const { accessToken: creatorToken } = await login(
request,
CONFIG.users.creator.email,
CONFIG.users.creator.password,
);
const tracksResp = await request.get(`${CONFIG.apiURL}/api/v1/tracks`, {
headers: { Authorization: `Bearer ${creatorToken}` },
});
expect(tracksResp.status()).toBe(200);
const tracksBody = (await tracksResp.json()) as ApiEnvelope<{ tracks: { id: string }[] }>;
const tracks = tracksBody.data?.tracks ?? [];
expect(tracks.length, 'seed creator account must have at least one track').toBeGreaterThan(0);
const trackID = tracks[0].id;
// Step 2 : anonymous claimant files a notice naming that track.
const noticeResp = await request.post(`${CONFIG.apiURL}/api/v1/dmca/notice`, {
data: {
claimant_name: 'E2E Rights Holder',
claimant_email: `e2e_dmca_admin_${Date.now()}@example.com`,
claimant_address: '1 Holder St, Rights City',
work_description:
'E2E test — claimed work is the seed creator track ' + trackID + '.',
infringing_track_id: trackID,
sworn_statement: true,
},
});
expect(noticeResp.status()).toBe(201);
const noticeBody = (await noticeResp.json()) as ApiEnvelope<{ id: string }>;
const noticeID = noticeBody.data.id;
// Step 3 : admin approves takedown.
const { accessToken: adminToken } = await login(
request,
CONFIG.users.admin.email,
CONFIG.users.admin.password,
);
const takedownResp = await request.post(
`${CONFIG.apiURL}/api/v1/admin/dmca/notices/${noticeID}/takedown`,
{
headers: { Authorization: `Bearer ${adminToken}` },
data: { note: 'E2E test takedown' },
},
);
expect(takedownResp.status(), 'admin takedown must succeed').toBe(200);
// Step 4 : streaming the track now returns 451 (Unavailable For Legal Reasons).
const streamResp = await request.get(
`${CONFIG.apiURL}/api/v1/tracks/${trackID}/stream`,
{ headers: { Authorization: `Bearer ${creatorToken}` } },
);
expect(streamResp.status(), 'DMCA-blocked track must 451 even for the owner').toBe(451);
});
});