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
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>
150 lines
6 KiB
TypeScript
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);
|
|
});
|
|
});
|