Files
thoughts/crates/application/src/use_cases/profile/tests.rs
Gabriel Kaszewski 9c99f7a7a8
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
feat: add image upload for avatar and banner
2026-05-24 02:06:47 +02:00

284 lines
7.7 KiB
Rust

use super::*;
use domain::{
errors::DomainError,
models::user::User,
testing::TestStore,
value_objects::{Email, PasswordHash, UserId, Username},
};
use std::sync::{Arc, Mutex};
fn make_user() -> User {
User::new_local(
UserId::new(),
Username::new("alice").unwrap(),
Email::new("alice@ex.com").unwrap(),
PasswordHash("h".into()),
)
}
#[tokio::test]
async fn set_top_friends_rejects_more_than_eight() {
let store = TestStore::default();
let uid = UserId::new();
let friends: Vec<UserId> = (0..9).map(|_| UserId::new()).collect();
let err = set_top_friends(&store, &uid, friends).await.unwrap_err();
assert!(matches!(err, DomainError::InvalidInput(_)));
}
#[tokio::test]
async fn set_top_friends_assigns_sequential_positions() {
let store = TestStore::default();
let uid = UserId::new();
let f1 = UserId::new();
let f2 = UserId::new();
let f3 = UserId::new();
set_top_friends(&store, &uid, vec![f1.clone(), f2.clone(), f3.clone()])
.await
.unwrap();
let tf = store.top_friends.lock().unwrap();
assert_eq!(tf.len(), 3);
let pos_f1 = tf
.iter()
.find(|t| t.friend_id == f1)
.map(|t| t.position)
.unwrap();
let pos_f2 = tf
.iter()
.find(|t| t.friend_id == f2)
.map(|t| t.position)
.unwrap();
assert!(pos_f1 < pos_f2, "f1 should come before f2");
}
#[tokio::test]
async fn get_user_by_username_returns_not_found_for_missing_user() {
let store = TestStore::default();
let err = get_user_by_username(&store, "nobody").await.unwrap_err();
assert!(matches!(err, DomainError::NotFound));
}
#[tokio::test]
async fn get_user_by_username_returns_correct_user() {
let store = TestStore::default();
let user = make_user();
store.users.lock().unwrap().push(user.clone());
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));
}