Add route for getting most popular posts

This commit is contained in:
Alex Selimov 2026-06-15 13:21:50 -04:00
parent 6979e73307
commit 3ee97b5d43
4 changed files with 84 additions and 8 deletions

View file

@ -18,8 +18,7 @@ impl Env {
.map(|s| s.trim().to_string())
.collect();
let port = env::var("PORT")
.unwrap_or_else(|_| "3000".to_string());
let port = env::var("PORT").unwrap_or_else(|_| "3000".to_string());
Env {
postgres_connection_string,

View file

@ -14,9 +14,9 @@ async fn main() {
let state = AppState::new().await;
let port = &state.env.port;
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await.unwrap();
tracing::info!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, upvoters::app(state))
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}"))
.await
.unwrap();
tracing::info!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, upvoters::app(state)).await.unwrap();
}

View file

@ -1,18 +1,19 @@
use axum::{
Json, Router,
extract::{Path, State},
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
routing::{delete, get, post},
};
use axum_extra::extract::{CookieJar, cookie::Cookie};
use serde::{Deserialize, Serialize};
use tracing::{error, info};
use uuid::Uuid;
use crate::{
state::AppState,
votes::{
repository::{delete_vote, insert_new_vote},
repository::{delete_vote, get_top_n_slugs, insert_new_vote},
service::get_votes_and_voted,
},
};
@ -22,6 +23,7 @@ pub fn router() -> Router<AppState> {
.route("/posts/{slug}/vote", post(upvote_handler))
.route("/posts/{slug}/vote", delete(delete_vote_handler))
.route("/posts/{slug}/votes", get(get_votes_and_voted_handler))
.route("/posts/best", get(get_best_handler))
}
fn get_or_init_voter_id(jar: CookieJar) -> (CookieJar, Uuid) {
@ -110,6 +112,30 @@ async fn get_votes_and_voted_handler(
}
}
#[derive(Serialize, Deserialize)]
struct BestVotesQuery {
top: usize,
}
async fn get_best_handler(
jar: CookieJar,
State(state): State<AppState>,
query: Query<BestVotesQuery>,
) -> impl IntoResponse {
match get_top_n_slugs(query.top as i64, &state.db).await {
Ok(top_slugs) => {
info!(n=%query.top, "fetched most popular slugs successfully");
(StatusCode::OK, jar, Json(top_slugs)).into_response()
}
Err(err) => {
error!(
error = %err,
"failed to fetch popular slugs"
);
(StatusCode::INTERNAL_SERVER_ERROR, jar, "Failed to upvote").into_response()
}
}
}
#[cfg(test)]
mod tests {
use axum::{
@ -333,4 +359,54 @@ mod tests {
delete_votes_for_slug(slug, &db).await.unwrap();
}
#[tokio::test]
async fn get_best_returns_top_slugs_limited_by_query() {
let db = test_pool().await;
let slugs = [
"get_best_handler_top_post_1",
"get_best_handler_top_post_2",
"get_best_handler_top_post_3",
];
for slug in slugs {
delete_votes_for_slug(slug, &db).await.unwrap();
}
for voter_id in [
Uuid::from_u128(0x100),
Uuid::from_u128(0x101),
Uuid::from_u128(0x102),
] {
insert_new_vote(slugs[0], &voter_id, &db).await.unwrap();
}
for voter_id in [Uuid::from_u128(0x200), Uuid::from_u128(0x201)] {
insert_new_vote(slugs[1], &voter_id, &db).await.unwrap();
}
insert_new_vote(slugs[2], &Uuid::from_u128(0x300), &db)
.await
.unwrap();
let app = crate::app(test_state(db.clone()));
let request = Request::builder()
.method("GET")
.uri("/posts/best?top=2")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
let json: Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json.as_array().unwrap().len(), 2);
assert_eq!(json[0]["slug"], slugs[0]);
assert_eq!(json[0]["vote_count"], 3);
assert_eq!(json[1]["slug"], slugs[1]);
assert_eq!(json[1]["vote_count"], 2);
for slug in slugs {
delete_votes_for_slug(slug, &db).await.unwrap();
}
}
}

View file

@ -1,4 +1,5 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::prelude::FromRow;
use uuid::Uuid;
@ -11,7 +12,7 @@ pub struct Vote {
}
/// Struct representing the best slug posts
#[derive(Debug, FromRow)]
#[derive(Debug, FromRow, Serialize, Deserialize)]
pub struct BestSlugs {
pub slug: String,
pub vote_count: i64,