feat: implement full-text search functionality with API integration, add search router and persistence logic, and create related schemas and tests
This commit is contained in:
@@ -5,6 +5,7 @@ pub mod auth;
|
|||||||
pub mod feed;
|
pub mod feed;
|
||||||
pub mod friends;
|
pub mod friends;
|
||||||
pub mod root;
|
pub mod root;
|
||||||
|
pub mod search;
|
||||||
pub mod tag;
|
pub mod tag;
|
||||||
pub mod thought;
|
pub mod thought;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
@@ -30,6 +31,7 @@ pub fn create_router(state: AppState) -> Router {
|
|||||||
.nest("/feed", create_feed_router())
|
.nest("/feed", create_feed_router())
|
||||||
.nest("/tags", tag::create_tag_router())
|
.nest("/tags", tag::create_tag_router())
|
||||||
.nest("/friends", friends::create_friends_router())
|
.nest("/friends", friends::create_friends_router())
|
||||||
|
.nest("/search", search::create_search_router())
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
.layer(cors)
|
.layer(cors)
|
||||||
}
|
}
|
||||||
|
53
thoughts-backend/api/src/routers/search.rs
Normal file
53
thoughts-backend/api/src/routers/search.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
use crate::{error::ApiError, extractor::OptionalAuthUser};
|
||||||
|
use app::{persistence::search, state::AppState};
|
||||||
|
use axum::{
|
||||||
|
extract::{Query, State},
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::get,
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use models::schemas::{
|
||||||
|
search::SearchResultsSchema,
|
||||||
|
thought::{ThoughtListSchema, ThoughtSchema},
|
||||||
|
user::UserListSchema,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
#[derive(Deserialize, IntoParams)]
|
||||||
|
pub struct SearchQuery {
|
||||||
|
q: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "",
|
||||||
|
params(SearchQuery),
|
||||||
|
responses((status = 200, body = SearchResultsSchema))
|
||||||
|
)]
|
||||||
|
async fn search_all(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
viewer: OptionalAuthUser,
|
||||||
|
Query(query): Query<SearchQuery>,
|
||||||
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
|
let viewer_id = viewer.0.map(|u| u.id);
|
||||||
|
|
||||||
|
let (users, thoughts) = tokio::try_join!(
|
||||||
|
search::search_users(&state.conn, &query.q),
|
||||||
|
search::search_thoughts(&state.conn, &query.q, viewer_id)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let thought_schemas: Vec<ThoughtSchema> =
|
||||||
|
thoughts.into_iter().map(ThoughtSchema::from).collect();
|
||||||
|
|
||||||
|
let response = SearchResultsSchema {
|
||||||
|
users: UserListSchema::from(users),
|
||||||
|
thoughts: ThoughtListSchema::from(thought_schemas),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_search_router() -> Router<AppState> {
|
||||||
|
Router::new().route("/", get(search_all))
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
pub mod api_key;
|
pub mod api_key;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod follow;
|
pub mod follow;
|
||||||
|
pub mod search;
|
||||||
pub mod tag;
|
pub mod tag;
|
||||||
pub mod thought;
|
pub mod thought;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
65
thoughts-backend/app/src/persistence/search.rs
Normal file
65
thoughts-backend/app/src/persistence/search.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use models::{
|
||||||
|
domains::{thought, user},
|
||||||
|
schemas::thought::ThoughtWithAuthor,
|
||||||
|
};
|
||||||
|
use sea_orm::{
|
||||||
|
prelude::{Expr, Uuid},
|
||||||
|
DatabaseConnection, DbErr, EntityTrait, JoinType, QueryFilter, QuerySelect, RelationTrait,
|
||||||
|
Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::persistence::follow;
|
||||||
|
|
||||||
|
fn is_visible(
|
||||||
|
author_id: Uuid,
|
||||||
|
viewer_id: Option<Uuid>,
|
||||||
|
friend_ids: &[Uuid],
|
||||||
|
visibility: &thought::Visibility,
|
||||||
|
) -> bool {
|
||||||
|
match visibility {
|
||||||
|
thought::Visibility::Public => true,
|
||||||
|
thought::Visibility::Private => viewer_id.map_or(false, |v| v == author_id),
|
||||||
|
thought::Visibility::FriendsOnly => {
|
||||||
|
viewer_id.map_or(false, |v| v == author_id || friend_ids.contains(&author_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search_thoughts(
|
||||||
|
db: &DatabaseConnection,
|
||||||
|
query: &str,
|
||||||
|
viewer_id: Option<Uuid>,
|
||||||
|
) -> Result<Vec<ThoughtWithAuthor>, DbErr> {
|
||||||
|
let mut friend_ids = Vec::new();
|
||||||
|
if let Some(viewer) = viewer_id {
|
||||||
|
friend_ids = follow::get_friend_ids(db, viewer).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must join with the user table to get the author's username
|
||||||
|
let thoughts_with_authors = thought::Entity::find()
|
||||||
|
.column_as(user::Column::Username, "author_username")
|
||||||
|
.join(JoinType::InnerJoin, thought::Relation::User.def())
|
||||||
|
.filter(Expr::cust_with_values(
|
||||||
|
"thought.search_document @@ websearch_to_tsquery('english', $1)",
|
||||||
|
[Value::from(query)],
|
||||||
|
))
|
||||||
|
.into_model::<ThoughtWithAuthor>() // Convert directly in the query
|
||||||
|
.all(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Apply visibility filtering in Rust after the search
|
||||||
|
Ok(thoughts_with_authors
|
||||||
|
.into_iter()
|
||||||
|
.filter(|t| is_visible(t.author_id, viewer_id, &friend_ids, &t.visibility))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search_users(db: &DatabaseConnection, query: &str) -> Result<Vec<user::Model>, DbErr> {
|
||||||
|
user::Entity::find()
|
||||||
|
.filter(Expr::cust_with_values(
|
||||||
|
"\"user\".search_document @@ websearch_to_tsquery('english', $1)",
|
||||||
|
[Value::from(query)],
|
||||||
|
))
|
||||||
|
.all(db)
|
||||||
|
.await
|
||||||
|
}
|
@@ -201,7 +201,7 @@ pub async fn get_thoughts_by_tag_name(
|
|||||||
Ok(visible_thoughts)
|
Ok(visible_thoughts)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_visibility_filter(
|
pub fn apply_visibility_filter(
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
viewer_id: Option<Uuid>,
|
viewer_id: Option<Uuid>,
|
||||||
friend_ids: &[Uuid],
|
friend_ids: &[Uuid],
|
||||||
|
@@ -11,10 +11,10 @@ mod auth;
|
|||||||
mod feed;
|
mod feed;
|
||||||
mod friends;
|
mod friends;
|
||||||
mod root;
|
mod root;
|
||||||
|
mod search;
|
||||||
mod tag;
|
mod tag;
|
||||||
mod thought;
|
mod thought;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
#[openapi(
|
#[openapi(
|
||||||
nest(
|
nest(
|
||||||
@@ -26,6 +26,7 @@ mod user;
|
|||||||
(path = "/feed", api = feed::FeedApi),
|
(path = "/feed", api = feed::FeedApi),
|
||||||
(path = "/tags", api = tag::TagApi),
|
(path = "/tags", api = tag::TagApi),
|
||||||
(path = "/friends", api = friends::FriendsApi),
|
(path = "/friends", api = friends::FriendsApi),
|
||||||
|
(path = "/search", api = search::SearchApi),
|
||||||
),
|
),
|
||||||
tags(
|
tags(
|
||||||
(name = "root", description = "Root API"),
|
(name = "root", description = "Root API"),
|
||||||
@@ -35,6 +36,7 @@ mod user;
|
|||||||
(name = "feed", description = "Feed API"),
|
(name = "feed", description = "Feed API"),
|
||||||
(name = "tag", description = "Tag Discovery API"),
|
(name = "tag", description = "Tag Discovery API"),
|
||||||
(name = "friends", description = "Friends API"),
|
(name = "friends", description = "Friends API"),
|
||||||
|
(name = "search", description = "Search API"),
|
||||||
),
|
),
|
||||||
modifiers(&SecurityAddon),
|
modifiers(&SecurityAddon),
|
||||||
)]
|
)]
|
||||||
|
21
thoughts-backend/doc/src/search.rs
Normal file
21
thoughts-backend/doc/src/search.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use api::{models::ApiErrorResponse, routers::search::*};
|
||||||
|
use models::schemas::{
|
||||||
|
search::SearchResultsSchema,
|
||||||
|
thought::{ThoughtListSchema, ThoughtSchema},
|
||||||
|
user::{UserListSchema, UserSchema},
|
||||||
|
};
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(search_all),
|
||||||
|
components(schemas(
|
||||||
|
SearchResultsSchema,
|
||||||
|
ApiErrorResponse,
|
||||||
|
ThoughtSchema,
|
||||||
|
ThoughtListSchema,
|
||||||
|
UserSchema,
|
||||||
|
UserListSchema
|
||||||
|
))
|
||||||
|
)]
|
||||||
|
pub(super) struct SearchApi;
|
@@ -7,6 +7,7 @@ mod m20250906_130237_add_tags;
|
|||||||
mod m20250906_134056_add_api_keys;
|
mod m20250906_134056_add_api_keys;
|
||||||
mod m20250906_145148_add_reply_to_thoughts;
|
mod m20250906_145148_add_reply_to_thoughts;
|
||||||
mod m20250906_145755_add_visibility_to_thoughts;
|
mod m20250906_145755_add_visibility_to_thoughts;
|
||||||
|
mod m20250906_231359_add_full_text_search;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20250906_134056_add_api_keys::Migration),
|
Box::new(m20250906_134056_add_api_keys::Migration),
|
||||||
Box::new(m20250906_145148_add_reply_to_thoughts::Migration),
|
Box::new(m20250906_145148_add_reply_to_thoughts::Migration),
|
||||||
Box::new(m20250906_145755_add_visibility_to_thoughts::Migration),
|
Box::new(m20250906_145755_add_visibility_to_thoughts::Migration),
|
||||||
|
Box::new(m20250906_231359_add_full_text_search::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,48 @@
|
|||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
// --- Users Table ---
|
||||||
|
// Add the tsvector column for users
|
||||||
|
manager.get_connection().execute_unprepared(
|
||||||
|
"ALTER TABLE \"user\" ADD COLUMN \"search_document\" tsvector \
|
||||||
|
GENERATED ALWAYS AS (to_tsvector('english', username || ' ' || coalesce(display_name, ''))) STORED"
|
||||||
|
).await?;
|
||||||
|
// Add the GIN index for users
|
||||||
|
manager.get_connection().execute_unprepared(
|
||||||
|
"CREATE INDEX \"user_search_document_idx\" ON \"user\" USING GIN(\"search_document\")"
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// --- Thoughts Table ---
|
||||||
|
// Add the tsvector column for thoughts
|
||||||
|
manager
|
||||||
|
.get_connection()
|
||||||
|
.execute_unprepared(
|
||||||
|
"ALTER TABLE \"thought\" ADD COLUMN \"search_document\" tsvector \
|
||||||
|
GENERATED ALWAYS AS (to_tsvector('english', content)) STORED",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
// Add the GIN index for thoughts
|
||||||
|
manager.get_connection().execute_unprepared(
|
||||||
|
"CREATE INDEX \"thought_search_document_idx\" ON \"thought\" USING GIN(\"search_document\")"
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.get_connection()
|
||||||
|
.execute_unprepared("ALTER TABLE \"user\" DROP COLUMN \"search_document\"")
|
||||||
|
.await?;
|
||||||
|
manager
|
||||||
|
.get_connection()
|
||||||
|
.execute_unprepared("ALTER TABLE \"thought\" DROP COLUMN \"search_document\"")
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@@ -25,6 +25,8 @@ pub struct Model {
|
|||||||
pub reply_to_id: Option<Uuid>,
|
pub reply_to_id: Option<Uuid>,
|
||||||
pub visibility: Visibility,
|
pub visibility: Visibility,
|
||||||
pub created_at: DateTimeWithTimeZone,
|
pub created_at: DateTimeWithTimeZone,
|
||||||
|
#[sea_orm(column_type = "custom(\"tsvector\")", nullable, ignore)]
|
||||||
|
pub search_document: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
@@ -19,6 +19,8 @@ pub struct Model {
|
|||||||
pub custom_css: Option<String>,
|
pub custom_css: Option<String>,
|
||||||
pub created_at: DateTimeWithTimeZone,
|
pub created_at: DateTimeWithTimeZone,
|
||||||
pub updated_at: DateTimeWithTimeZone,
|
pub updated_at: DateTimeWithTimeZone,
|
||||||
|
#[sea_orm(column_type = "custom(\"tsvector\")", nullable, ignore)]
|
||||||
|
pub search_document: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
pub mod api_key;
|
pub mod api_key;
|
||||||
|
pub mod search;
|
||||||
pub mod thought;
|
pub mod thought;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
9
thoughts-backend/models/src/schemas/search.rs
Normal file
9
thoughts-backend/models/src/schemas/search.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use super::{thought::ThoughtListSchema, user::UserListSchema};
|
||||||
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct SearchResultsSchema {
|
||||||
|
pub users: UserListSchema,
|
||||||
|
pub thoughts: ThoughtListSchema,
|
||||||
|
}
|
@@ -4,6 +4,7 @@ mod auth;
|
|||||||
mod feed;
|
mod feed;
|
||||||
mod follow;
|
mod follow;
|
||||||
mod main;
|
mod main;
|
||||||
|
mod search;
|
||||||
mod tag;
|
mod tag;
|
||||||
mod thought;
|
mod thought;
|
||||||
mod user;
|
mod user;
|
||||||
|
198
thoughts-backend/tests/api/search.rs
Normal file
198
thoughts-backend/tests/api/search.rs
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
use crate::api::main::{create_user_with_password, login_user, setup};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use http_body_util::BodyExt;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use utils::testing::{make_get_request, make_jwt_request};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_search_all() {
|
||||||
|
let app = setup().await;
|
||||||
|
|
||||||
|
// 1. Setup users and data
|
||||||
|
let user1 =
|
||||||
|
create_user_with_password(&app.db, "search_user1", "password123", "s1@test.com").await;
|
||||||
|
let user2 =
|
||||||
|
create_user_with_password(&app.db, "search_user2", "password123", "s2@test.com").await;
|
||||||
|
let _user3 =
|
||||||
|
create_user_with_password(&app.db, "stranger_user", "password123", "s3@test.com").await;
|
||||||
|
|
||||||
|
// Make user1 and user2 friends
|
||||||
|
app::persistence::follow::follow_user(&app.db, user1.id, user2.id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
app::persistence::follow::follow_user(&app.db, user2.id, user1.id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let token1 = login_user(app.router.clone(), "search_user1", "password123").await;
|
||||||
|
let token2 = login_user(app.router.clone(), "search_user2", "password123").await;
|
||||||
|
let token3 = login_user(app.router.clone(), "stranger_user", "password123").await;
|
||||||
|
|
||||||
|
// User1 posts thoughts with different visibilities
|
||||||
|
let thought_public =
|
||||||
|
json!({ "content": "A very public thought about Rust.", "visibility": "Public" })
|
||||||
|
.to_string();
|
||||||
|
let thought_friends =
|
||||||
|
json!({ "content": "A friendly thought, just for pals.", "visibility": "FriendsOnly" })
|
||||||
|
.to_string();
|
||||||
|
let thought_private =
|
||||||
|
json!({ "content": "A private thought, for my eyes only.", "visibility": "Private" })
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
make_jwt_request(
|
||||||
|
app.router.clone(),
|
||||||
|
"/thoughts",
|
||||||
|
"POST",
|
||||||
|
Some(thought_public),
|
||||||
|
&token1,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
make_jwt_request(
|
||||||
|
app.router.clone(),
|
||||||
|
"/thoughts",
|
||||||
|
"POST",
|
||||||
|
Some(thought_friends),
|
||||||
|
&token1,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
make_jwt_request(
|
||||||
|
app.router.clone(),
|
||||||
|
"/thoughts",
|
||||||
|
"POST",
|
||||||
|
Some(thought_private),
|
||||||
|
&token1,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// 2. Run search tests
|
||||||
|
|
||||||
|
// -- User Search --
|
||||||
|
let response = make_get_request(app.router.clone(), "/search?q=search_user1", None).await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
|
let v: Value = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(v["users"]["users"].as_array().unwrap().len(), 1);
|
||||||
|
assert_eq!(v["users"]["users"][0]["username"], "search_user1");
|
||||||
|
|
||||||
|
// -- Thought Search (Public) --
|
||||||
|
let response = make_get_request(app.router.clone(), "/search?q=public", None).await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
|
let v: Value = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
v["thoughts"]["thoughts"].as_array().unwrap().len(),
|
||||||
|
1,
|
||||||
|
"Guest should find public thought"
|
||||||
|
);
|
||||||
|
assert!(v["thoughts"]["thoughts"][0]["content"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap()
|
||||||
|
.contains("public"));
|
||||||
|
|
||||||
|
// -- Thought Search (FriendsOnly) --
|
||||||
|
let response = make_jwt_request(
|
||||||
|
app.router.clone(),
|
||||||
|
"/search?q=friendly",
|
||||||
|
"GET",
|
||||||
|
None,
|
||||||
|
&token1,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
|
let v: Value = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
v["thoughts"]["thoughts"].as_array().unwrap().len(),
|
||||||
|
1,
|
||||||
|
"Author should find friends thought"
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = make_jwt_request(
|
||||||
|
app.router.clone(),
|
||||||
|
"/search?q=friendly",
|
||||||
|
"GET",
|
||||||
|
None,
|
||||||
|
&token2,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
|
let v: Value = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
v["thoughts"]["thoughts"].as_array().unwrap().len(),
|
||||||
|
1,
|
||||||
|
"Friend should find friends thought"
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = make_jwt_request(
|
||||||
|
app.router.clone(),
|
||||||
|
"/search?q=friendly",
|
||||||
|
"GET",
|
||||||
|
None,
|
||||||
|
&token3,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
|
let v: Value = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
v["thoughts"]["thoughts"].as_array().unwrap().len(),
|
||||||
|
0,
|
||||||
|
"Stranger should NOT find friends thought"
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = make_get_request(app.router.clone(), "/search?q=friendly", None).await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
|
let v: Value = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
v["thoughts"]["thoughts"].as_array().unwrap().len(),
|
||||||
|
0,
|
||||||
|
"Guest should NOT find friends thought"
|
||||||
|
);
|
||||||
|
|
||||||
|
// -- Thought Search (Private) --
|
||||||
|
let response = make_jwt_request(
|
||||||
|
app.router.clone(),
|
||||||
|
"/search?q=private",
|
||||||
|
"GET",
|
||||||
|
None,
|
||||||
|
&token1,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
|
let v: Value = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
v["thoughts"]["thoughts"].as_array().unwrap().len(),
|
||||||
|
1,
|
||||||
|
"Author should find private thought"
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = make_jwt_request(
|
||||||
|
app.router.clone(),
|
||||||
|
"/search?q=private",
|
||||||
|
"GET",
|
||||||
|
None,
|
||||||
|
&token2,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
|
let v: Value = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
v["thoughts"]["thoughts"].as_array().unwrap().len(),
|
||||||
|
0,
|
||||||
|
"Friend should NOT find private thought"
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = make_get_request(app.router.clone(), "/search?q=private", None).await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
|
let v: Value = serde_json::from_slice(&body).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
v["thoughts"]["thoughts"].as_array().unwrap().len(),
|
||||||
|
0,
|
||||||
|
"Guest should NOT find private thought"
|
||||||
|
);
|
||||||
|
}
|
Reference in New Issue
Block a user