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_attachmentsrecords 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 |
|---|---|---|---|
|
Single-page PDF with "CRAIG Test Document" text |
~1 KB |
PDF 1.4 |
|
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:
-
Read fixture files from
tests/fixtures/ -
Upload to the object store via HTTP (using the craig-cases attachment API)
-
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.pdfas a contact attachment on the first seeded contact -
sample.pngas 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) andimage(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)
-
Create
tests/fixtures/directory if it does not exist -
Generate a minimal 1-page PDF (~1 KB) with the text "CRAIG Test Document"
-
Generate a minimal 100x100 solid blue PNG (~200 bytes)
-
Commit both files to the repository
-
Add
tests/fixtures/to.gitattributesas 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:
-
Query the contact attachments list endpoint to confirm both files appear
-
Print the attachment IDs for reference
-
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 |
|---|---|
|
NEW: Minimal 1-page test PDF |
|
NEW: 100x100 solid blue test PNG |
|
Add binary attribute for fixture files |
|
Add attachment upload after SQL seeding |
|
NEW: E2E tests for seeded attachment download |
Verification
-
cargo xtask dev start— devstack running -
cargo xtask seed— seed completes, prints attachment upload confirmation -
curl -H "Authorization: Bearer $TOKEN" http://localhost:8002/v1/cases/cases/{id}/contacts/{id}/attachments— returns 2 attachments -
Download each attachment via curl — verify file content matches original fixtures
-
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