feat: add image upload for avatar and banner
This commit is contained in:
@@ -5,6 +5,7 @@ use domain::{
|
||||
testing::TestStore,
|
||||
value_objects::{Email, PasswordHash, UserId, Username},
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
fn make_user() -> User {
|
||||
User::new_local(
|
||||
@@ -64,3 +65,219 @@ async fn get_user_by_username_returns_correct_user() {
|
||||
let found = get_user_by_username(&store, "alice").await.unwrap();
|
||||
assert_eq!(found.id, user.id);
|
||||
}
|
||||
|
||||
// ── upload tests ──────────────────────────────────────────────────────────────
|
||||
|
||||
use bytes::Bytes;
|
||||
use domain::ports::{DataStream, MediaStore};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct MockMedia {
|
||||
store: Arc<Mutex<HashMap<String, Bytes>>>,
|
||||
deleted: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MediaStore for MockMedia {
|
||||
async fn put(&self, key: &str, mut data: DataStream) -> Result<(), DomainError> {
|
||||
use futures::stream::StreamExt;
|
||||
let mut buf = Vec::new();
|
||||
while let Some(chunk) = data.next().await {
|
||||
buf.extend_from_slice(&chunk?);
|
||||
}
|
||||
self.store
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(key.to_string(), Bytes::from(buf));
|
||||
Ok(())
|
||||
}
|
||||
async fn get(&self, key: &str) -> Result<DataStream, DomainError> {
|
||||
let bytes = self
|
||||
.store
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(key)
|
||||
.cloned()
|
||||
.ok_or(DomainError::NotFound)?;
|
||||
Ok(Box::pin(futures::stream::once(async move { Ok(bytes) })))
|
||||
}
|
||||
async fn delete(&self, key: &str) -> Result<(), DomainError> {
|
||||
self.store.lock().unwrap().remove(key);
|
||||
self.deleted.lock().unwrap().push(key.to_string());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_cfg() -> UploadConfig {
|
||||
UploadConfig::default()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upload_avatar_rejects_unsupported_mime() {
|
||||
let store = TestStore::default();
|
||||
let media = MockMedia::default();
|
||||
let user = make_user();
|
||||
store.users.lock().unwrap().push(user.clone());
|
||||
let err = upload_avatar(
|
||||
&store,
|
||||
&media,
|
||||
&store,
|
||||
&user.id,
|
||||
"http://localhost",
|
||||
&default_cfg(),
|
||||
"text/plain",
|
||||
Bytes::from("hi"),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, DomainError::InvalidInput(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upload_avatar_rejects_oversized_data() {
|
||||
let store = TestStore::default();
|
||||
let media = MockMedia::default();
|
||||
let user = make_user();
|
||||
store.users.lock().unwrap().push(user.clone());
|
||||
let big = Bytes::from(vec![0u8; 6 * 1024 * 1024]);
|
||||
let err = upload_avatar(
|
||||
&store,
|
||||
&media,
|
||||
&store,
|
||||
&user.id,
|
||||
"http://localhost",
|
||||
&default_cfg(),
|
||||
"image/jpeg",
|
||||
big,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, DomainError::InvalidInput(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upload_avatar_stores_file_and_updates_url() {
|
||||
let store = TestStore::default();
|
||||
let media = MockMedia::default();
|
||||
let user = make_user();
|
||||
store.users.lock().unwrap().push(user.clone());
|
||||
upload_avatar(
|
||||
&store,
|
||||
&media,
|
||||
&store,
|
||||
&user.id,
|
||||
"http://localhost",
|
||||
&default_cfg(),
|
||||
"image/jpeg",
|
||||
Bytes::from("img"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let key = format!("users/{}/avatar.jpg", user.id.as_uuid());
|
||||
assert!(media.store.lock().unwrap().contains_key(&key));
|
||||
let saved = store
|
||||
.users
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|u| u.id == user.id)
|
||||
.unwrap()
|
||||
.clone();
|
||||
assert_eq!(
|
||||
saved.avatar_url,
|
||||
Some(format!("http://localhost/media/{key}"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upload_avatar_deletes_old_file_on_reupload() {
|
||||
let store = TestStore::default();
|
||||
let media = MockMedia::default();
|
||||
let mut user = make_user();
|
||||
let old_key = format!("users/{}/avatar.png", user.id.as_uuid());
|
||||
user.avatar_url = Some(format!("http://localhost/media/{old_key}"));
|
||||
store.users.lock().unwrap().push(user.clone());
|
||||
media
|
||||
.store
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(old_key.clone(), Bytes::from("old"));
|
||||
upload_avatar(
|
||||
&store,
|
||||
&media,
|
||||
&store,
|
||||
&user.id,
|
||||
"http://localhost",
|
||||
&default_cfg(),
|
||||
"image/jpeg",
|
||||
Bytes::from("new"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!media.store.lock().unwrap().contains_key(&old_key));
|
||||
assert!(media.deleted.lock().unwrap().contains(&old_key));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upload_banner_stores_file_and_updates_header_url() {
|
||||
let store = TestStore::default();
|
||||
let media = MockMedia::default();
|
||||
let user = make_user();
|
||||
store.users.lock().unwrap().push(user.clone());
|
||||
upload_banner(
|
||||
&store,
|
||||
&media,
|
||||
&store,
|
||||
&user.id,
|
||||
"http://localhost",
|
||||
&default_cfg(),
|
||||
"image/png",
|
||||
Bytes::from("banner"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let key = format!("users/{}/banner.png", user.id.as_uuid());
|
||||
assert!(media.store.lock().unwrap().contains_key(&key));
|
||||
let saved = store
|
||||
.users
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|u| u.id == user.id)
|
||||
.unwrap()
|
||||
.clone();
|
||||
assert_eq!(
|
||||
saved.header_url,
|
||||
Some(format!("http://localhost/media/{key}"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upload_banner_deletes_old_file_on_reupload() {
|
||||
let store = TestStore::default();
|
||||
let media = MockMedia::default();
|
||||
let mut user = make_user();
|
||||
let old_key = format!("users/{}/banner.jpg", user.id.as_uuid());
|
||||
user.header_url = Some(format!("http://localhost/media/{old_key}"));
|
||||
store.users.lock().unwrap().push(user.clone());
|
||||
media
|
||||
.store
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(old_key.clone(), Bytes::from("old"));
|
||||
upload_banner(
|
||||
&store,
|
||||
&media,
|
||||
&store,
|
||||
&user.id,
|
||||
"http://localhost",
|
||||
&default_cfg(),
|
||||
"image/png",
|
||||
Bytes::from("new"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!media.store.lock().unwrap().contains_key(&old_key));
|
||||
assert!(media.deleted.lock().unwrap().contains(&old_key));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user