Seed Attachments

On this page

Status

Step Description Status

1

Generate minimal test fixture files (PDF, PNG)

Done (pre-ADR-030)

2

Extend seed process to upload fixtures to object store

Done (pre-ADR-030)

3

Insert corresponding database records during seeding

Done (via API upload)

4

E2E test integration for upload/download verification

Not started

Epic: &TBD
Issues: #TBD
Branch: feature/seed-attachments

Context

CRAIG’s seed data (cargo xtask seed) populates the database with sample records for all services but does not include any file attachments in the object store. craig-cases supports contact attachments and craig-exchange supports ICPC document uploads, but there are no pre-seeded files for developers to interact with or for E2E tests to verify download functionality.

Without seed attachments:

  • Developers must manually upload files to test attachment features

  • E2E tests cannot verify the full upload/download cycle without first performing an upload

  • The seed data gives an incomplete picture of system functionality

This plan adds minimal test fixtures (a 1-page PDF and a 100x100 PNG) to the repository, uploads them during seeding, and creates corresponding database records so attachment features work immediately after cargo xtask seed.

Scope

In scope:

  • Generate or commit minimal test fixtures: 1-page PDF, 100x100 PNG

  • Upload fixtures to object store during cargo xtask seed

  • Insert contact_attachments records for seeded cases/contacts

  • E2E tests that verify download of seeded attachments

Out of scope:

  • Large file testing (stress tests for big uploads)

  • Virus scanning or content validation of seed files

  • Generating fixtures dynamically at test time (committed static files are simpler)

Design

Test Fixtures

Two minimal files committed to the repository:

File Description Size Format

tests/fixtures/sample.pdf

Single-page PDF with "CRAIG Test Document" text

~1 KB

PDF 1.4

tests/fixtures/sample.png

100x100 solid blue (#3B82F6) PNG

~200 bytes

PNG

These files are intentionally tiny to keep the repository small and tests fast.

Seed Upload Flow

During cargo xtask seed, after SQL data is inserted:

  1. Read fixture files from tests/fixtures/

  2. Upload to the object store via HTTP (using the craig-cases attachment API)

  3. The upload API creates both the object store entry and the database record

Alternatively, upload directly to the object store (Garage/local FS) and insert DB records via SQL, bypassing the API. This is faster but requires coordinating object keys.

Recommended approach: Use the API — it validates the upload, creates the DB record, and ensures the object key format is consistent. This also exercises the upload path during seeding, catching regressions early.

Object Store Paths

Following the existing convention in services/craig-cases/src/api/contact_attachments.rs:

  • contacts/{contact_id}/{attachment_id}/{filename} — for contact attachments

The seed process uploads:

  • sample.pdf as a contact attachment on the first seeded contact

  • sample.png as a contact attachment on the first seeded contact

Fixture Generation

The PDF and PNG files can be:

  • Option A (recommended): Generated once using a script, then committed as binary files. This avoids runtime dependencies on PDF/image libraries.

  • Option B: Generated programmatically in the xtask seed command using printpdf (PDF) and image (PNG) crates. This adds dependencies but ensures fixtures are always fresh.

Option A is recommended for simplicity. The files are tiny and binary-stable.

To generate the PDF (one-time, using Python or any tool):

# Generate sample.pdf — run once, commit the output
from reportlab.pdfgen import canvas
c = canvas.Canvas("sample.pdf", pagesize=(200, 200))
c.drawString(30, 100, "CRAIG Test Document")
c.save()

To generate the PNG (one-time):

# Generate sample.png — run once, commit the output
from PIL import Image
img = Image.new('RGB', (100, 100), color=(59, 130, 246))
img.save('sample.png')

Or use any image editor / CLI tool. The exact method does not matter — only the output files are committed.

Steps

Step 1: Generate and Commit Test Fixtures

Files: tests/fixtures/sample.pdf (NEW), tests/fixtures/sample.png (NEW)

  1. Create tests/fixtures/ directory if it does not exist

  2. Generate a minimal 1-page PDF (~1 KB) with the text "CRAIG Test Document"

  3. Generate a minimal 100x100 solid blue PNG (~200 bytes)

  4. Commit both files to the repository

  5. Add tests/fixtures/ to .gitattributes as binary files to prevent line-ending issues:

tests/fixtures/*.pdf binary
tests/fixtures/*.png binary

Step 2: Extend Seed Process to Upload Fixtures

Files: xtask/src/seed.rs (or equivalent seed command module)

Add attachment upload to the seed workflow, after SQL data insertion:

async fn seed_attachments(config: &SeedConfig) -> Result<()> {
    let client = reqwest::Client::new();
    let token = get_admin_token(&config.oidc_internal_url).await?;

    // Find the first seeded case and contact
    let cases_url = format!("{}/v1/cases/cases", config.cases_url);
    let cases: Vec<serde_json::Value> = client
        .get(&cases_url)
        .bearer_auth(&token)
        .send().await?
        .json().await?;

    let case_id = cases.first()
        .and_then(|c| c["id"].as_str())
        .ok_or_else(|| anyhow::anyhow!("no seeded cases found"))?;

    // Get first contact for this case
    let contacts_url = format!("{}/v1/cases/cases/{case_id}/contacts", config.cases_url);
    let contacts: Vec<serde_json::Value> = client
        .get(&contacts_url)
        .bearer_auth(&token)
        .send().await?
        .json().await?;

    let contact_id = contacts.first()
        .and_then(|c| c["id"].as_str())
        .ok_or_else(|| anyhow::anyhow!("no seeded contacts found"))?;

    // Upload PDF
    let pdf_bytes = std::fs::read("tests/fixtures/sample.pdf")?;
    let pdf_part = reqwest::multipart::Part::bytes(pdf_bytes)
        .file_name("sample.pdf")
        .mime_str("application/pdf")?;
    let form = reqwest::multipart::Form::new()
        .part("file", pdf_part)
        .text("attachment_type", "document");

    let upload_url = format!(
        "{}/v1/cases/cases/{case_id}/contacts/{contact_id}/attachments",
        config.cases_url
    );
    client.post(&upload_url)
        .bearer_auth(&token)
        .multipart(form)
        .send().await?
        .error_for_status()?;

    println!("  uploaded sample.pdf as contact attachment");

    // Upload PNG
    let png_bytes = std::fs::read("tests/fixtures/sample.png")?;
    let png_part = reqwest::multipart::Part::bytes(png_bytes)
        .file_name("sample.png")
        .mime_str("image/png")?;
    let form = reqwest::multipart::Form::new()
        .part("file", png_part)
        .text("attachment_type", "photo");

    client.post(&upload_url)
        .bearer_auth(&token)
        .multipart(form)
        .send().await?
        .error_for_status()?;

    println!("  uploaded sample.png as contact attachment");
    Ok(())
}

Step 3: Database Record Verification

Files: xtask/src/seed.rs

After uploading via the API (Step 2), the database records are created automatically by the upload handler. No manual SQL inserts needed.

Add verification after upload:

  1. Query the contact attachments list endpoint to confirm both files appear

  2. Print the attachment IDs for reference

  3. If verification fails, print a warning (do not fail the entire seed — attachments are supplementary)

Step 4: E2E Test Integration

Files: tests/e2e/specs/attachments.spec.ts (NEW or extend existing)

Add Playwright E2E tests that verify seeded attachments:

// tests/e2e/specs/attachments.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Seeded Attachments', () => {
    test('can view attachment list on a contact', async ({ page }) => {
        // Navigate to a seeded case's contact
        await page.goto('/cases');
        await page.click('text=first seeded case');
        await page.click('text=Contacts');
        // Verify attachments are listed
        await expect(page.locator('text=sample.pdf')).toBeVisible();
        await expect(page.locator('text=sample.png')).toBeVisible();
    });

    test('can download seeded PDF attachment', async ({ page }) => {
        // Navigate to attachment
        // Click download link
        // Verify download starts (check response headers or download event)
        const [download] = await Promise.all([
            page.waitForEvent('download'),
            page.click('text=sample.pdf >> xpath=../.. >> text=Download'),
        ]);
        expect(download.suggestedFilename()).toBe('sample.pdf');
    });
});
Exact selectors depend on the current UI structure. Verify against the actual rendered HTML at implementation time.

Files Touched

File Change

tests/fixtures/sample.pdf

NEW: Minimal 1-page test PDF

tests/fixtures/sample.png

NEW: 100x100 solid blue test PNG

.gitattributes

Add binary attribute for fixture files

xtask/src/seed.rs

Add attachment upload after SQL seeding

tests/e2e/specs/attachments.spec.ts

NEW: E2E tests for seeded attachment download

Verification

  1. cargo xtask dev start — devstack running

  2. cargo xtask seed — seed completes, prints attachment upload confirmation

  3. curl -H "Authorization: Bearer $TOKEN" http://localhost:8002/v1/cases/cases/{id}/contacts/{id}/attachments — returns 2 attachments

  4. Download each attachment via curl — verify file content matches original fixtures

  5. cargo xtask e2e — attachment E2E tests pass

Documentation Updates

  • .claude/docs/testing.md — note seed attachments in seed data section

  • CHANGELOG.adoc — entry under == Unreleased

Edit this page · latest