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>
91 lines
2.4 KiB
Go
91 lines
2.4 KiB
Go
package services
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"veza-backend-api/internal/models"
|
|
)
|
|
|
|
// Tests focus on the pure helpers — the DB transactions are exercised
|
|
// in the integration test (test_dmca_workflow.go later) ; here we lock
|
|
// the audit-log append + the JSON shape of the audit entries since
|
|
// they're part of our compliance trail.
|
|
|
|
func TestAppendAudit_EmptyStart(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
actorID := uuid.New()
|
|
out, err := appendAudit(json.RawMessage(`[]`), models.DmcaAuditEntry{
|
|
Timestamp: now,
|
|
ActorID: &actorID,
|
|
Action: "takedown",
|
|
Note: "verified — match found",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("appendAudit: %v", err)
|
|
}
|
|
|
|
var entries []models.DmcaAuditEntry
|
|
if err := json.Unmarshal(out, &entries); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if len(entries) != 1 {
|
|
t.Fatalf("expected 1 entry, got %d", len(entries))
|
|
}
|
|
got := entries[0]
|
|
if got.Action != "takedown" {
|
|
t.Errorf("action = %q, want takedown", got.Action)
|
|
}
|
|
if got.ActorID == nil || *got.ActorID != actorID {
|
|
t.Errorf("actor_user_id mismatch")
|
|
}
|
|
if got.Note != "verified — match found" {
|
|
t.Errorf("note mismatch: %q", got.Note)
|
|
}
|
|
}
|
|
|
|
func TestAppendAudit_PreservesExisting(t *testing.T) {
|
|
existing := []models.DmcaAuditEntry{
|
|
{Timestamp: time.Now().Add(-time.Hour).UTC(), Action: "submitted", Note: "from 1.2.3.4"},
|
|
}
|
|
encoded, err := json.Marshal(existing)
|
|
if err != nil {
|
|
t.Fatalf("marshal existing: %v", err)
|
|
}
|
|
|
|
out, err := appendAudit(encoded, models.DmcaAuditEntry{
|
|
Timestamp: time.Now().UTC(),
|
|
Action: "dismissed",
|
|
Note: "fraudulent claim",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("appendAudit: %v", err)
|
|
}
|
|
|
|
var entries []models.DmcaAuditEntry
|
|
if err := json.Unmarshal(out, &entries); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if len(entries) != 2 {
|
|
t.Fatalf("expected 2 entries (1 existing + 1 new), got %d", len(entries))
|
|
}
|
|
if entries[0].Action != "submitted" {
|
|
t.Errorf("first entry action = %q, want submitted (preserved order)", entries[0].Action)
|
|
}
|
|
if entries[1].Action != "dismissed" {
|
|
t.Errorf("second entry action = %q, want dismissed", entries[1].Action)
|
|
}
|
|
}
|
|
|
|
func TestAppendAudit_RejectsCorruptedJSON(t *testing.T) {
|
|
_, err := appendAudit(json.RawMessage(`not-json`), models.DmcaAuditEntry{
|
|
Timestamp: time.Now(),
|
|
Action: "takedown",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for malformed audit_log JSON, got nil")
|
|
}
|
|
}
|