2026-03-04 18:03:17 -05:00
|
|
|
|
use std::sync::Arc;
|
2026-03-28 18:40:34 +00:00
|
|
|
|
use std::time::Duration;
|
2026-03-04 18:03:17 -05:00
|
|
|
|
|
2026-04-26 11:55:05 +02:00
|
|
|
|
use bytes::Bytes;
|
|
|
|
|
|
|
2026-03-04 18:03:17 -05:00
|
|
|
|
use super::thumbnail_service::{ThumbnailService, ThumbnailSize};
|
|
|
|
|
|
|
|
|
|
|
|
/// Minimal valid 1x1 red PNG (68 bytes).
|
|
|
|
|
|
fn tiny_png() -> Vec<u8> {
|
|
|
|
|
|
// Generated from a real 1×1 PNG — smallest valid RGBA image.
|
|
|
|
|
|
let mut img = image::RgbaImage::new(1, 1);
|
|
|
|
|
|
img.put_pixel(0, 0, image::Rgba([255, 0, 0, 255]));
|
|
|
|
|
|
let mut buf = Vec::new();
|
2026-03-05 21:28:51 +01:00
|
|
|
|
img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
|
|
|
|
|
|
.expect("encode test PNG");
|
2026-03-04 18:03:17 -05:00
|
|
|
|
buf
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Regression test: thumbnail generation must work when the source file lives
|
|
|
|
|
|
/// at a blob-style path (`.blobs/ab/ab1234…`) rather than a logical path
|
|
|
|
|
|
/// (`folder/image.png`). This broke after the blob storage migration
|
|
|
|
|
|
/// (commit 3c7c16f) because the handler passed the logical path — which
|
|
|
|
|
|
/// doesn't exist on disk — to the thumbnail service.
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
|
async fn generate_thumbnail_from_blob_path() {
|
|
|
|
|
|
let tmp = tempfile::tempdir().expect("create temp dir");
|
|
|
|
|
|
let storage_root = tmp.path();
|
|
|
|
|
|
|
|
|
|
|
|
// Simulate a blob-store layout: .blobs/ab/<hash>.blob
|
|
|
|
|
|
let blob_dir = storage_root.join(".blobs").join("ab");
|
|
|
|
|
|
std::fs::create_dir_all(&blob_dir).expect("create blob dir");
|
|
|
|
|
|
let blob_path = blob_dir.join("ab1234567890.blob");
|
|
|
|
|
|
std::fs::write(&blob_path, tiny_png()).expect("write test blob");
|
|
|
|
|
|
|
2026-03-28 18:40:34 +00:00
|
|
|
|
let svc = Arc::new(ThumbnailService::new(
|
|
|
|
|
|
storage_root,
|
|
|
|
|
|
100,
|
|
|
|
|
|
10 * 1024 * 1024,
|
|
|
|
|
|
Some(Duration::from_secs(30)),
|
|
|
|
|
|
));
|
2026-03-04 18:03:17 -05:00
|
|
|
|
svc.initialize().await.expect("init thumbnail dirs");
|
|
|
|
|
|
|
|
|
|
|
|
// The key assertion: the service can read from a blob path (not a logical path)
|
|
|
|
|
|
let result = svc
|
2026-04-14 10:51:48 +02:00
|
|
|
|
.get_thumbnail(
|
|
|
|
|
|
"test-file-id",
|
|
|
|
|
|
"ab1234567890",
|
|
|
|
|
|
ThumbnailSize::Icon,
|
|
|
|
|
|
&blob_path,
|
|
|
|
|
|
)
|
2026-03-04 18:03:17 -05:00
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
|
|
let thumb_bytes = result.expect("thumbnail generation should succeed from blob path");
|
|
|
|
|
|
assert!(!thumb_bytes.is_empty(), "thumbnail bytes must not be empty");
|
|
|
|
|
|
|
2026-03-07 20:37:19 +01:00
|
|
|
|
// Verify it's valid JPEG (starts with SOI marker 0xFF 0xD8)
|
2026-03-04 18:03:17 -05:00
|
|
|
|
assert!(
|
2026-03-07 20:37:19 +01:00
|
|
|
|
thumb_bytes.len() > 2 && thumb_bytes[0] == 0xFF && thumb_bytes[1] == 0xD8,
|
|
|
|
|
|
"output should be JPEG format"
|
2026-03-04 18:03:17 -05:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Verify that a non-existent path produces an error, not a panic.
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
|
async fn generate_thumbnail_nonexistent_path_returns_error() {
|
|
|
|
|
|
let tmp = tempfile::tempdir().expect("create temp dir");
|
2026-03-28 18:40:34 +00:00
|
|
|
|
let svc = Arc::new(ThumbnailService::new(
|
|
|
|
|
|
tmp.path(),
|
|
|
|
|
|
100,
|
|
|
|
|
|
10 * 1024 * 1024,
|
|
|
|
|
|
Some(Duration::from_secs(30)),
|
|
|
|
|
|
));
|
2026-03-04 18:03:17 -05:00
|
|
|
|
svc.initialize().await.expect("init thumbnail dirs");
|
|
|
|
|
|
|
|
|
|
|
|
let bad_path = tmp.path().join("does-not-exist.png");
|
|
|
|
|
|
let result = svc
|
2026-04-14 10:51:48 +02:00
|
|
|
|
.get_thumbnail(
|
|
|
|
|
|
"missing-id",
|
|
|
|
|
|
"nonexistent-hash",
|
|
|
|
|
|
ThumbnailSize::Icon,
|
|
|
|
|
|
&bad_path,
|
|
|
|
|
|
)
|
2026-03-04 18:03:17 -05:00
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
|
|
assert!(result.is_err(), "should fail for nonexistent file");
|
|
|
|
|
|
}
|
2026-04-26 11:55:05 +02:00
|
|
|
|
|
|
|
|
|
|
/// Regression test: thumbnail generation must also work when the image source
|
|
|
|
|
|
/// is reconstructed from blob storage bytes instead of a single local file.
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
|
async fn generate_thumbnail_from_blob_bytes() {
|
|
|
|
|
|
let tmp = tempfile::tempdir().expect("create temp dir");
|
|
|
|
|
|
let storage_root = tmp.path();
|
|
|
|
|
|
|
|
|
|
|
|
let svc = Arc::new(ThumbnailService::new(
|
|
|
|
|
|
storage_root,
|
|
|
|
|
|
100,
|
|
|
|
|
|
10 * 1024 * 1024,
|
|
|
|
|
|
Some(Duration::from_secs(30)),
|
|
|
|
|
|
));
|
|
|
|
|
|
svc.initialize().await.expect("init thumbnail dirs");
|
|
|
|
|
|
|
|
|
|
|
|
let result = svc
|
|
|
|
|
|
.get_thumbnail_from_bytes(
|
|
|
|
|
|
"bytes-file-id",
|
|
|
|
|
|
"bytes-hash-123",
|
|
|
|
|
|
ThumbnailSize::Preview,
|
|
|
|
|
|
Bytes::from(tiny_png()),
|
|
|
|
|
|
)
|
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
|
|
let thumb_bytes = result.expect("thumbnail generation should succeed from raw bytes");
|
|
|
|
|
|
assert!(!thumb_bytes.is_empty(), "thumbnail bytes must not be empty");
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
thumb_bytes.len() > 2 && thumb_bytes[0] == 0xFF && thumb_bytes[1] == 0xD8,
|
|
|
|
|
|
"output should be JPEG format"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|