mod common; use axum::http::StatusCode; use tower::ServiceExt; /// Write a blob straight into the harness storage root at `key`, bypassing the /// upload handlers — the only way to land a key whose extension resolves to the /// `application/octet-stream` fallback (uploads always mint image extensions). fn write_blob(h: &common::Harness, key: &str, bytes: &[u8]) { let path = h._storage_dir.path().join(key); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write(path, bytes).unwrap(); } #[sqlx::test(migrations = "./migrations")] async fn octet_stream_blobs_are_served_as_attachment(pool: sqlx::PgPool) { // A blob with an unknown extension serves as application/octet-stream. Such a // body could be crafted HTML/JS, so it must never render inline: force a // download with Content-Disposition: attachment (nosniff is already set). let h = common::harness(pool); write_blob(&h, "misc/blob.bin", b"\x00\x01not-an-image"); let resp = h .app .oneshot(common::get("/api/v1/files/misc/blob.bin")) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get("content-type").unwrap(), "application/octet-stream" ); assert_eq!( resp.headers().get("content-disposition").unwrap(), "attachment" ); } #[sqlx::test(migrations = "./migrations")] async fn image_blobs_are_served_inline(pool: sqlx::PgPool) { // Regression guard: known image types keep rendering inline (no attachment // disposition), so covers/pages still display in the reader. let h = common::harness(pool); write_blob(&h, "misc/pic.png", &common::fake_png_bytes()); let resp = h .app .oneshot(common::get("/api/v1/files/misc/pic.png")) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.headers().get("content-type").unwrap(), "image/png"); assert!( resp.headers().get("content-disposition").is_none(), "images must render inline, not download" ); }