feat: enhance user registration and follow functionality, add popular tags endpoint, and update tests
This commit is contained in:
1
thoughts-backend/Cargo.lock
generated
1
thoughts-backend/Cargo.lock
generated
@@ -357,6 +357,7 @@ name = "app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bcrypt",
|
||||
"chrono",
|
||||
"models",
|
||||
"rand 0.8.5",
|
||||
"sea-orm",
|
||||
|
@@ -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);
|
||||
|
@@ -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))
|
||||
}
|
||||
|
@@ -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(
|
||||
|
@@ -14,3 +14,4 @@ models = { path = "../models" }
|
||||
validator = "0.20"
|
||||
rand = "0.8.5"
|
||||
sea-orm = { version = "1.1.12" }
|
||||
chrono = { workspace = true }
|
||||
|
@@ -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(¶ms.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()
|
||||
};
|
||||
|
||||
|
@@ -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())
|
||||
|
@@ -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())
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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,
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
}
|
||||
|
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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();
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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"
|
||||
);
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user