feat: enhance user registration and follow functionality, add popular tags endpoint, and update tests

This commit is contained in:
2025-09-06 16:49:38 +02:00
parent 508f218fc0
commit 728bf0e231
23 changed files with 216 additions and 64 deletions

View File

@@ -357,6 +357,7 @@ name = "app"
version = "0.1.0"
dependencies = [
"bcrypt",
"chrono",
"models",
"rand 0.8.5",
"sea-orm",

View File

@@ -1,7 +1,7 @@
use axum::{extract::State, response::IntoResponse, routing::get, Json, Router};
use app::{
persistence::{follow::get_followed_ids, thought::get_feed_for_user},
persistence::{follow::get_following_ids, thought::get_feed_for_user},
state::AppState,
};
use models::schemas::thought::{ThoughtListSchema, ThoughtSchema};
@@ -23,8 +23,8 @@ async fn feed_get(
State(state): State<AppState>,
auth_user: AuthUser,
) -> Result<impl IntoResponse, ApiError> {
let followed_ids = get_followed_ids(&state.conn, auth_user.id).await?;
let mut thoughts_with_authors = get_feed_for_user(&state.conn, followed_ids).await?;
let following_ids = get_following_ids(&state.conn, auth_user.id).await?;
let mut thoughts_with_authors = get_feed_for_user(&state.conn, following_ids).await?;
let own_thoughts = get_feed_for_user(&state.conn, vec![auth_user.id]).await?;
thoughts_with_authors.extend(own_thoughts);

View File

@@ -1,5 +1,8 @@
use crate::error::ApiError;
use app::{persistence::thought::get_thoughts_by_tag_name, state::AppState};
use app::{
persistence::{tag, thought::get_thoughts_by_tag_name},
state::AppState,
};
use axum::{
extract::{Path, State},
response::IntoResponse,
@@ -27,6 +30,20 @@ async fn get_thoughts_by_tag(
Ok(Json(ThoughtListSchema::from(thoughts_schema)))
}
pub fn create_tag_router() -> Router<AppState> {
Router::new().route("/{tag_name}", get(get_thoughts_by_tag))
#[utoipa::path(
get,
path = "/popular",
responses((status = 200, description = "List of popular tags", body = Vec<String>))
)]
async fn get_popular_tags(State(state): State<AppState>) -> Result<impl IntoResponse, ApiError> {
let tags = tag::get_popular_tags(&state.conn).await;
println!("Fetched popular tags: {:?}", tags);
let tags = tags?;
Ok(Json(tags))
}
pub fn create_tag_router() -> Router<AppState> {
Router::new()
.route("/{tag_name}", get(get_thoughts_by_tag))
.route("/popular", get(get_popular_tags))
}

View File

@@ -248,7 +248,12 @@ async fn get_user_by_param(
}
} else {
match get_user_by_username(&state.conn, &username).await {
Ok(Some(user)) => Json(UserSchema::from(user)).into_response(),
Ok(Some(user)) => {
let top_friends = app::persistence::user::get_top_friends(&state.conn, user.id)
.await
.unwrap_or_default();
Json(UserSchema::from((user, top_friends))).into_response()
}
Ok(None) => ApiError::from(UserError::NotFound).into_response(),
Err(e) => ApiError::from(e).into_response(),
}
@@ -332,7 +337,9 @@ async fn get_me(
let user = get_user(&state.conn, auth_user.id)
.await?
.ok_or(UserError::NotFound)?;
Ok(axum::Json(UserSchema::from(user)))
let top_friends = app::persistence::user::get_top_friends(&state.conn, auth_user.id).await?;
Ok(axum::Json(UserSchema::from((user, top_friends))))
}
#[utoipa::path(

View File

@@ -14,3 +14,4 @@ models = { path = "../models" }
validator = "0.20"
rand = "0.8.5"
sea-orm = { version = "1.1.12" }
chrono = { workspace = true }

View File

@@ -13,7 +13,6 @@ fn hash_password(password: &str) -> Result<String, BcryptError> {
}
pub async fn register_user(db: &DbConn, params: RegisterParams) -> Result<user::Model, UserError> {
// Validate the parameters
params
.validate()
.map_err(|e| UserError::Validation(e.to_string()))?;
@@ -22,8 +21,10 @@ pub async fn register_user(db: &DbConn, params: RegisterParams) -> Result<user::
hash_password(&params.password).map_err(|e| UserError::Internal(e.to_string()))?;
let new_user = user::ActiveModel {
username: Set(params.username),
username: Set(params.username.clone()),
password_hash: Set(Some(hashed_password)),
email: Set(Some(params.email)),
display_name: Set(Some(params.username)),
..Default::default()
};

View File

@@ -7,7 +7,7 @@ use models::domains::follow;
pub async fn add_follower(
db: &DbConn,
followed_id: Uuid,
following_id: Uuid,
follower_actor_id: &str,
) -> Result<(), UserError> {
let follower_username = follower_actor_id
@@ -20,21 +20,21 @@ pub async fn add_follower(
.map_err(|e| UserError::Internal(e.to_string()))?
.ok_or(UserError::NotFound)?;
follow_user(db, follower.id, followed_id)
follow_user(db, follower.id, following_id)
.await
.map_err(|e| UserError::Internal(e.to_string()))?;
Ok(())
}
pub async fn follow_user(db: &DbConn, follower_id: Uuid, followed_id: Uuid) -> Result<(), DbErr> {
if follower_id == followed_id {
pub async fn follow_user(db: &DbConn, follower_id: Uuid, following_id: Uuid) -> Result<(), DbErr> {
if follower_id == following_id {
return Err(DbErr::Custom("Users cannot follow themselves".to_string()));
}
let follow = follow::ActiveModel {
follower_id: Set(follower_id),
followed_id: Set(followed_id),
following_id: Set(following_id),
};
follow.insert(db).await?;
@@ -44,11 +44,11 @@ pub async fn follow_user(db: &DbConn, follower_id: Uuid, followed_id: Uuid) -> R
pub async fn unfollow_user(
db: &DbConn,
follower_id: Uuid,
followed_id: Uuid,
following_id: Uuid,
) -> Result<(), UserError> {
let deleted_result = follow::Entity::delete_many()
.filter(follow::Column::FollowerId.eq(follower_id))
.filter(follow::Column::FollowedId.eq(followed_id))
.filter(follow::Column::FollowingId.eq(following_id))
.exec(db)
.await
.map_err(|e| UserError::Internal(e.to_string()))?;
@@ -60,18 +60,18 @@ pub async fn unfollow_user(
Ok(())
}
pub async fn get_followed_ids(db: &DbConn, user_id: Uuid) -> Result<Vec<Uuid>, DbErr> {
pub async fn get_following_ids(db: &DbConn, user_id: Uuid) -> Result<Vec<Uuid>, DbErr> {
let followed_users = follow::Entity::find()
.filter(follow::Column::FollowerId.eq(user_id))
.all(db)
.await?;
Ok(followed_users.into_iter().map(|f| f.followed_id).collect())
Ok(followed_users.into_iter().map(|f| f.following_id).collect())
}
pub async fn get_follower_ids(db: &DbConn, user_id: Uuid) -> Result<Vec<Uuid>, DbErr> {
let followers = follow::Entity::find()
.filter(follow::Column::FollowedId.eq(user_id))
.filter(follow::Column::FollowingId.eq(user_id))
.all(db)
.await?;
Ok(followers.into_iter().map(|f| f.follower_id).collect())

View File

@@ -1,6 +1,8 @@
use models::domains::{tag, thought_tag};
use chrono::{Duration, Utc};
use models::domains::{tag, thought, thought_tag};
use sea_orm::{
sqlx::types::uuid, ColumnTrait, ConnectionTrait, DbErr, EntityTrait, QueryFilter, Set,
prelude::Expr, sea_query::Alias, sqlx::types::uuid, ColumnTrait, ConnectionTrait, DbErr,
EntityTrait, QueryFilter, QueryOrder, QuerySelect, RelationTrait, Set,
};
use std::collections::HashSet;
@@ -84,3 +86,34 @@ where
thought_tag::Entity::insert_many(links).exec(db).await?;
Ok(())
}
pub async fn get_popular_tags<C>(db: &C) -> Result<Vec<String>, DbErr>
where
C: ConnectionTrait,
{
let seven_days_ago = Utc::now() - Duration::days(7);
let popular_tags = tag::Entity::find()
.select_only()
.column(tag::Column::Name)
.column_as(Expr::col((tag::Entity, tag::Column::Id)).count(), "count")
.join(
sea_orm::JoinType::InnerJoin,
tag::Relation::ThoughtTag.def(),
)
.join(
sea_orm::JoinType::InnerJoin,
thought_tag::Relation::Thought.def(),
)
.filter(thought::Column::CreatedAt.gte(seven_days_ago))
.group_by(tag::Column::Name)
.group_by(tag::Column::Id)
.order_by_desc(Expr::col(Alias::new("count")))
.order_by_asc(tag::Column::Name)
.limit(10)
.into_tuple::<(String, i64)>()
.all(db)
.await?;
Ok(popular_tags.into_iter().map(|(name, _)| name).collect())
}

View File

@@ -69,9 +69,9 @@ pub async fn get_thoughts_by_user(
pub async fn get_feed_for_user(
db: &DbConn,
followed_ids: Vec<Uuid>,
following_ids: Vec<Uuid>,
) -> Result<Vec<ThoughtWithAuthor>, UserError> {
if followed_ids.is_empty() {
if following_ids.is_empty() {
return Ok(vec![]);
}
@@ -83,7 +83,7 @@ pub async fn get_feed_for_user(
.column(thought::Column::AuthorId)
.column_as(user::Column::Username, "author_username")
.join(JoinType::InnerJoin, thought::Relation::User.def())
.filter(thought::Column::AuthorId.is_in(followed_ids))
.filter(thought::Column::AuthorId.is_in(following_ids))
.order_by_desc(thought::Column::CreatedAt)
.into_model::<ThoughtWithAuthor>()
.all(db)

View File

@@ -1,6 +1,7 @@
use sea_orm::prelude::Uuid;
use sea_orm::{
ActiveModelTrait, ColumnTrait, DbConn, DbErr, EntityTrait, QueryFilter, Set, TransactionTrait,
ActiveModelTrait, ColumnTrait, DbConn, DbErr, EntityTrait, JoinType, QueryFilter, QueryOrder,
QuerySelect, RelationTrait, Set, TransactionTrait,
};
use models::domains::{top_friends, user};
@@ -127,3 +128,12 @@ pub async fn update_user_profile(
.await
.map_err(|e| UserError::Internal(e.to_string()))
}
pub async fn get_top_friends(db: &DbConn, user_id: Uuid) -> Result<Vec<user::Model>, DbErr> {
user::Entity::find()
.join(JoinType::InnerJoin, top_friends::Relation::User.def().rev())
.filter(top_friends::Column::UserId.eq(user_id))
.order_by_asc(top_friends::Column::Position)
.all(db)
.await
}

View File

@@ -46,12 +46,12 @@ impl MigrationTrait for Migration {
.table(Follow::Table)
.if_not_exists()
.col(uuid(Follow::FollowerId).not_null())
.col(uuid(Follow::FollowedId).not_null())
.col(uuid(Follow::FollowingId).not_null())
// Composite Primary Key to ensure a user can only follow another once
.primary_key(
Index::create()
.col(Follow::FollowerId)
.col(Follow::FollowedId),
.col(Follow::FollowingId),
)
.foreign_key(
ForeignKey::create()
@@ -62,8 +62,8 @@ impl MigrationTrait for Migration {
)
.foreign_key(
ForeignKey::create()
.name("fk_follow_followed_id")
.from(Follow::Table, Follow::FollowedId)
.name("fk_follow_following_id")
.from(Follow::Table, Follow::FollowingId)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade),
)
@@ -97,5 +97,5 @@ pub enum Follow {
// The user who is initiating the follow
FollowerId,
// The user who is being followed
FollowedId,
FollowingId,
}

View File

@@ -6,7 +6,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub follower_id: Uuid,
#[sea_orm(primary_key, auto_increment = false)]
pub followed_id: Uuid,
pub following_id: Uuid,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -21,12 +21,12 @@ pub enum Relation {
Follower,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::FollowedId",
from = "Column::FollowingId",
to = "super::user::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
Followed,
Following,
}
impl Related<super::user::Entity> for Entity {

View File

@@ -6,6 +6,8 @@ use validator::Validate;
pub struct RegisterParams {
#[validate(length(min = 3))]
pub username: String,
#[validate(email)]
pub email: String,
#[validate(length(min = 6))]
pub password: String,
}

View File

@@ -14,12 +14,26 @@ pub struct UserSchema {
pub avatar_url: Option<String>,
pub header_url: Option<String>,
pub custom_css: Option<String>,
// In a real implementation, you'd fetch and return this data.
// For now, we'll omit it from the schema to keep it simple.
// pub top_friends: Vec<String>,
pub top_friends: Vec<String>,
pub joined_at: DateTimeWithTimeZoneWrapper,
}
impl From<(user::Model, Vec<user::Model>)> for UserSchema {
fn from((user, top_friends): (user::Model, Vec<user::Model>)) -> Self {
Self {
id: user.id,
username: user.username,
display_name: user.display_name,
bio: user.bio,
avatar_url: user.avatar_url,
header_url: user.header_url,
custom_css: user.custom_css,
top_friends: top_friends.into_iter().map(|u| u.username).collect(),
joined_at: user.created_at.into(),
}
}
}
impl From<user::Model> for UserSchema {
fn from(user: user::Model) -> Self {
Self {
@@ -30,6 +44,7 @@ impl From<user::Model> for UserSchema {
avatar_url: user.avatar_url,
header_url: user.header_url,
custom_css: user.custom_css,
top_friends: vec![], // Defaults to an empty list
joined_at: user.created_at.into(),
}
}

View File

@@ -9,7 +9,7 @@ use utils::testing::{
#[tokio::test]
async fn test_webfinger_discovery() {
let app = setup().await;
create_user_with_password(&app.db, "testuser", "password123").await;
create_user_with_password(&app.db, "testuser", "password123", "testuser@example.com").await;
// 1. Valid WebFinger lookup for existing user
let url = "/.well-known/webfinger?resource=acct:testuser@localhost:3000";
@@ -36,7 +36,7 @@ async fn test_webfinger_discovery() {
#[tokio::test]
async fn test_user_actor_endpoint() {
let app = setup().await;
create_user_with_password(&app.db, "testuser", "password123").await;
create_user_with_password(&app.db, "testuser", "password123", "testuser@example.com").await;
let response = make_request_with_headers(
app.router.clone(),
@@ -64,9 +64,11 @@ async fn test_user_actor_endpoint() {
async fn test_user_inbox_follow() {
let app = setup().await;
// user1 will be followed
let user1 = create_user_with_password(&app.db, "user1", "password123").await;
let user1 =
create_user_with_password(&app.db, "user1", "password123", "user1@example.com").await;
// user2 will be the follower
let user2 = create_user_with_password(&app.db, "user2", "password123").await;
let user2 =
create_user_with_password(&app.db, "user2", "password123", "user2@example.com").await;
// Construct a follow activity from user2, targeting user1
let follow_activity = json!({
@@ -90,7 +92,7 @@ async fn test_user_inbox_follow() {
assert_eq!(response.status(), StatusCode::ACCEPTED);
// Verify that user2 is now following user1 in the database
let followers = app::persistence::follow::get_followed_ids(&app.db, user2.id)
let followers = app::persistence::follow::get_following_ids(&app.db, user2.id)
.await
.unwrap();
assert!(
@@ -98,7 +100,7 @@ async fn test_user_inbox_follow() {
"User2 should be following user1"
);
let following = app::persistence::follow::get_followed_ids(&app.db, user1.id)
let following = app::persistence::follow::get_following_ids(&app.db, user1.id)
.await
.unwrap();
assert!(
@@ -111,7 +113,7 @@ async fn test_user_inbox_follow() {
#[tokio::test]
async fn test_user_outbox_get() {
let app = setup().await;
create_user_with_password(&app.db, "testuser", "password123").await;
create_user_with_password(&app.db, "testuser", "password123", "testuser@example.com").await;
let token = super::main::login_user(app.router.clone(), "testuser", "password123").await;
// Create a thought first

View File

@@ -7,7 +7,13 @@ use utils::testing::{make_jwt_request, make_request_with_headers};
#[tokio::test]
async fn test_api_key_flow() {
let app = setup().await;
let _ = create_user_with_password(&app.db, "apikey_user", "password123").await;
let _ = create_user_with_password(
&app.db,
"apikey_user",
"password123",
"apikey_user@example.com",
)
.await;
let jwt = login_user(app.router.clone(), "apikey_user", "password123").await;
// 1. Create a new API key using JWT auth

View File

@@ -11,6 +11,7 @@ async fn test_auth_flow() {
let register_body = json!({
"username": "testuser",
"email": "testuser@example.com",
"password": "password123"
})
.to_string();
@@ -26,6 +27,7 @@ async fn test_auth_flow() {
"/auth/register",
json!({
"username": "testuser",
"email": "testuser@example.com",
"password": "password456"
})
.to_string(),
@@ -48,6 +50,7 @@ async fn test_auth_flow() {
let bad_login_body = json!({
"username": "testuser",
"email": "testuser@example.com",
"password": "wrongpassword"
})
.to_string();

View File

@@ -7,9 +7,9 @@ use utils::testing::make_jwt_request;
#[tokio::test]
async fn test_feed_and_user_thoughts() {
let app = setup().await;
create_user_with_password(&app.db, "user1", "password1").await;
create_user_with_password(&app.db, "user2", "password2").await;
create_user_with_password(&app.db, "user3", "password3").await;
create_user_with_password(&app.db, "user1", "password1", "user1@example.com").await;
create_user_with_password(&app.db, "user2", "password2", "user2@example.com").await;
create_user_with_password(&app.db, "user3", "password3", "user3@example.com").await;
// As user1, post a thought
let token = super::main::login_user(app.router.clone(), "user1", "password1").await;

View File

@@ -7,8 +7,8 @@ async fn test_follow_endpoints() {
std::env::set_var("AUTH_SECRET", "test-secret");
let app = setup().await;
create_user_with_password(&app.db, "user1", "password1").await;
create_user_with_password(&app.db, "user2", "password2").await;
create_user_with_password(&app.db, "user1", "password1", "user1@example.com").await;
create_user_with_password(&app.db, "user2", "password2", "user2@example.com").await;
let token = super::main::login_user(app.router.clone(), "user1", "password1").await;

View File

@@ -38,10 +38,12 @@ pub async fn create_user_with_password(
db: &DatabaseConnection,
username: &str,
password: &str,
email: &str,
) -> user::Model {
let params = RegisterParams {
username: username.to_string(),
password: password.to_string(),
email: email.to_string(),
};
app::persistence::auth::register_user(db, params)
.await

View File

@@ -1,4 +1,4 @@
use crate::api::main::{create_user_with_password, login_user, setup};
use crate::api::main::{create_user_with_password, login_user, setup, TestApp};
use axum::http::StatusCode;
use http_body_util::BodyExt;
use serde_json::{json, Value};
@@ -7,7 +7,8 @@ use utils::testing::{make_get_request, make_jwt_request};
#[tokio::test]
async fn test_hashtag_flow() {
let app = setup().await;
let user = create_user_with_password(&app.db, "taguser", "password123").await;
let user =
create_user_with_password(&app.db, "taguser", "password123", "taguser@example.com").await;
let token = login_user(app.router.clone(), "taguser", "password123").await;
// 1. Post a thought with hashtags
@@ -48,3 +49,43 @@ async fn test_hashtag_flow() {
assert_eq!(thoughts.len(), 1);
assert_eq!(thoughts[0]["id"], thought_id);
}
#[tokio::test]
async fn test_popular_tags() {
let app = setup().await;
let _ = create_user_with_password(&app.db, "poptag_user", "password123", "poptag@example.com")
.await;
let token = login_user(app.router.clone(), "poptag_user", "password123").await;
// Helper async function to post a thought
async fn post_thought(app: &TestApp, token: &str, content: &str) {
let body = json!({ "content": content }).to_string();
let response =
make_jwt_request(app.router.clone(), "/thoughts", "POST", Some(body), token).await;
assert_eq!(response.status(), StatusCode::CREATED);
}
// 1. Post thoughts to create tag usage data
// Expected counts: rust (3), web (2), axum (2), testing (1)
post_thought(&app, &token, "My first post about #rust and the #web").await;
post_thought(&app, &token, "Another post about #rust and #axum").await;
post_thought(&app, &token, "I'm really enjoying #rust lately").await;
post_thought(&app, &token, "Let's talk about #axum and the #web").await;
post_thought(&app, &token, "Don't forget about #testing").await;
// 2. Fetch the popular tags
let response = make_get_request(app.router.clone(), "/tags/popular", None).await;
println!("Response: {:?}", response);
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let v: Vec<String> = serde_json::from_slice(&body).unwrap();
// 3. Assert the results
assert_eq!(v.len(), 4, "Should return the 4 unique tags used");
assert_eq!(
v,
vec!["rust", "axum", "web", "testing"],
"Tags should be ordered by popularity, then alphabetically"
);
}

View File

@@ -10,8 +10,10 @@ use utils::testing::{make_delete_request, make_post_request};
#[tokio::test]
async fn test_thought_endpoints() {
let app = setup().await;
let user1 = create_user_with_password(&app.db, "user1", "password123").await; // AuthUser is ID 1
let _user2 = create_user_with_password(&app.db, "user2", "password123").await; // Other user is ID 2
let user1 =
create_user_with_password(&app.db, "user1", "password123", "user1@example.com").await; // AuthUser is ID 1
let _user2 =
create_user_with_password(&app.db, "user2", "password123", "user2@example.com").await; // Other user is ID 2
// 1. Post a new thought as user 1
let body = json!({ "content": "My first thought!" }).to_string();

View File

@@ -12,7 +12,8 @@ use crate::api::main::{create_user_with_password, login_user, setup};
async fn test_post_users() {
let app = setup().await;
let body = r#"{"username": "test", "password": "password123"}"#.to_owned();
let body = r#"{"username": "test", "email": "test@example.com", "password": "password123"}"#
.to_owned();
let response = make_post_request(app.router, "/auth/register", body, None).await;
assert_eq!(response.status(), StatusCode::CREATED);
@@ -21,14 +22,15 @@ async fn test_post_users() {
let v: Value = serde_json::from_slice(&body).unwrap();
assert_eq!(v["username"], "test");
assert!(v["display_name"].is_null());
assert!(v["display_name"].is_string());
}
#[tokio::test]
pub(super) async fn test_post_users_error() {
let app = setup().await;
let body = r#"{"username": "1", "password": "password123"}"#.to_owned();
let body =
r#"{"username": "1", "email": "test@example.com", "password": "password123"}"#.to_owned();
let response = make_post_request(app.router, "/auth/register", body, None).await;
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
@@ -43,7 +45,8 @@ pub(super) async fn test_post_users_error() {
pub async fn test_get_users() {
let app = setup().await;
let body = r#"{"username": "test", "password": "password123"}"#.to_owned();
let body = r#"{"username": "test", "email": "test@example.com", "password": "password123"}"#
.to_owned();
make_post_request(app.router.clone(), "/auth/register", body, None).await;
let response = make_get_request(app.router, "/users", None).await;
@@ -65,6 +68,7 @@ async fn test_me_endpoints() {
// 1. Register a new user
let register_body = json!({
"username": "me_user",
"email": "me_user@example.com",
"password": "password123"
})
.to_string();
@@ -82,7 +86,7 @@ async fn test_me_endpoints() {
let v: Value = serde_json::from_slice(&body).unwrap();
assert_eq!(v["username"], "me_user");
assert!(v["bio"].is_null());
assert!(v["display_name"].is_null());
assert!(v["display_name"].is_string());
// 4. PUT /users/me to update the profile
let update_body = json!({
@@ -119,10 +123,14 @@ async fn test_update_me_top_friends() {
let app = setup().await;
// 1. Create users for the test
let user_me = create_user_with_password(&app.db, "me_user", "password123").await;
let friend1 = create_user_with_password(&app.db, "friend1", "password123").await;
let friend2 = create_user_with_password(&app.db, "friend2", "password123").await;
let _friend3 = create_user_with_password(&app.db, "friend3", "password123").await;
let user_me =
create_user_with_password(&app.db, "me_user", "password123", "me_user@example.com").await;
let friend1 =
create_user_with_password(&app.db, "friend1", "password123", "friend1@example.com").await;
let friend2 =
create_user_with_password(&app.db, "friend2", "password123", "friend2@example.com").await;
let _friend3 =
create_user_with_password(&app.db, "friend3", "password123", "friend3@example.com").await;
// 2. Log in as "me_user"
let token = login_user(app.router.clone(), "me_user", "password123").await;
@@ -189,7 +197,8 @@ async fn test_update_me_css_and_images() {
let app = setup().await;
// 1. Create and log in as a user
let _ = create_user_with_password(&app.db, "css_user", "password123").await;
let _ =
create_user_with_password(&app.db, "css_user", "password123", "css_user@example.com").await;
let token = login_user(app.router.clone(), "css_user", "password123").await;
// 2. Attempt to update with an invalid avatar URL