Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added GetSnapVotes endpoint #3

Merged
merged 1 commit into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions proto/ratings_features_user.proto
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ service User {
rpc Delete (google.protobuf.Empty) returns (google.protobuf.Empty) {}
rpc Vote (VoteRequest) returns (google.protobuf.Empty) {}
rpc ListMyVotes (ListMyVotesRequest) returns (ListMyVotesResponse) {}
rpc GetSnapVotes(GetSnapVotesRequest) returns (GetSnapVotesResponse) {}
}

message AuthenticateRequest {
Expand All @@ -29,6 +30,14 @@ message ListMyVotesResponse {
repeated Vote votes = 1;
}

message GetSnapVotesRequest {
string snap_id = 1;
}

message GetSnapVotesResponse {
repeated Vote votes = 1;
}

message Vote {
string snap_id = 1;
int32 snap_revision = 2;
Expand Down
2 changes: 2 additions & 0 deletions src/features/user/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pub enum UserError {
FailedToCreateUserRecord,
#[error("failed to delete user by instance id")]
FailedToDeleteUserRecord,
#[error("failed to get user vote")]
FailedToGetUserVote,
#[error("failed to cast vote")]
FailedToCastVote,
#[error("unknown user error")]
Expand Down
57 changes: 57 additions & 0 deletions src/features/user/infrastructure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,63 @@ pub(crate) async fn delete_user_by_client_hash(
Ok(rows.rows_affected())
}

pub(crate) async fn get_snap_votes_by_client_hash(
app_ctx: &AppContext,
snap_id: String,
client_hash: String,
) -> Result<Vec<Vote>, UserError> {
let mut pool = app_ctx
.infrastructure()
.repository()
.await
.map_err(|error| {
error!("{error:?}");
UserError::FailedToGetUserVote
})?;

let result = sqlx::query(
r#"
SELECT
votes.id,
votes.created,
votes.snap_id,
votes.snap_revision,
votes.vote_up
FROM
users
INNER JOIN
votes
ON
users.id = votes.user_id_fk
WHERE
users.client_hash = $1
AND
votes.snap_id = $2
"#,
)
.bind(client_hash.clone())
.bind(snap_id)
.fetch_all(&mut *pool)
.await
.map_err(|error| {
error!("{error:?}");
UserError::Unknown
})?;

let votes: Vec<Vote> = result
.into_iter()
.map(|row| Vote {
client_hash: client_hash.clone(),
snap_id: row.get("snap_id"),
snap_revision: row.get::<i32, _>("snap_revision") as u32,
vote_up: row.get("vote_up"),
timestamp: row.get("created"),
})
.collect();

Ok(votes)
}

pub(crate) async fn save_vote_to_db(app_ctx: &AppContext, vote: Vote) -> Result<u64, UserError> {
let mut pool = app_ctx
.infrastructure()
Expand Down
28 changes: 26 additions & 2 deletions src/features/user/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use super::service::UserService;
use super::use_cases;

use self::protobuf::{
AuthenticateRequest, AuthenticateResponse, ListMyVotesRequest, ListMyVotesResponse, User,
VoteRequest,
AuthenticateRequest, AuthenticateResponse, GetSnapVotesRequest, GetSnapVotesResponse,
ListMyVotesRequest, ListMyVotesResponse, User, VoteRequest,
};

pub mod protobuf {
Expand Down Expand Up @@ -113,6 +113,30 @@ impl User for UserService {
Err(_error) => Err(Status::unknown("Internal server error")),
}
}

#[tracing::instrument]
async fn get_snap_votes(
&self,
request: Request<GetSnapVotesRequest>,
) -> Result<Response<GetSnapVotesResponse>, Status> {
let app_ctx = request.extensions().get::<AppContext>().unwrap().clone();
let Claims {
sub: client_hash, ..
} = claims(&request);

let GetSnapVotesRequest { snap_id } = request.into_inner();

let result = use_cases::get_snap_votes(&app_ctx, snap_id, client_hash).await;

match result {
Ok(votes) => {
let votes = votes.into_iter().map(|vote| vote.into_dto()).collect();
let payload = GetSnapVotesResponse { votes };
Ok(Response::new(payload))
}
Err(_error) => Err(Status::unknown("Internal server error")),
}
}
}

fn claims<T>(request: &Request<T>) -> Claims {
Expand Down
12 changes: 11 additions & 1 deletion src/features/user/use_cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use crate::features::user::infrastructure::{find_user_votes, save_vote_to_db};

use super::entities::{User, Vote};
use super::errors::UserError;
use super::infrastructure::{create_or_seen_user, delete_user_by_client_hash};
use super::infrastructure::{
create_or_seen_user, delete_user_by_client_hash, get_snap_votes_by_client_hash,
};

pub async fn authenticate(app_ctx: &AppContext, id: &str) -> Result<User, UserError> {
let user = User::new(id);
Expand All @@ -22,6 +24,14 @@ pub async fn vote(app_ctx: &AppContext, vote: Vote) -> Result<(), UserError> {
Ok(())
}

pub async fn get_snap_votes(
app_ctx: &AppContext,
snap_id: String,
client_hash: String,
) -> Result<Vec<Vote>, UserError> {
get_snap_votes_by_client_hash(app_ctx, snap_id, client_hash).await
}

pub async fn list_my_votes(
app_ctx: &AppContext,
client_hash: String,
Expand Down
21 changes: 21 additions & 0 deletions tests/helpers/client_user.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use tonic::metadata::MetadataValue;
use tonic::transport::Endpoint;
use tonic::{Request, Response, Status};

use self::pb::GetSnapVotesResponse;
pub mod pb {
pub use self::user_client::UserClient;

Expand Down Expand Up @@ -45,6 +47,25 @@ impl UserClient {
client.vote(ballet).await
}

#[allow(dead_code)]
pub async fn get_snap_votes(
&self,
token: &str,
request: pb::GetSnapVotesRequest,
) -> Result<Response<GetSnapVotesResponse>, Status> {
let channel = Endpoint::from_shared(self.url.clone())
.unwrap()
.connect()
.await
.unwrap();
let mut client = pb::UserClient::with_interceptor(channel, move |mut req: Request<()>| {
let header: MetadataValue<_> = format!("Bearer {token}").parse().unwrap();
req.metadata_mut().insert("authorization", header);
Ok(req)
});
client.get_snap_votes(request).await
}

#[allow(dead_code)]
pub async fn delete(&self, token: &str) -> Result<Response<()>, Status> {
let channel = Endpoint::from_shared(self.url.clone())
Expand Down
1 change: 1 addition & 0 deletions tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod user_tests {
mod double_authenticate_test;
mod get_votes_lifecycle_test;
mod reject_invalid_register_test;
mod simple_lifecycle_test;
}
Expand Down
190 changes: 190 additions & 0 deletions tests/user_tests/get_votes_lifecycle_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
use crate::helpers;
use crate::helpers::client_user::pb::GetSnapVotesRequest;
use crate::helpers::test_data::TestData;

use super::super::helpers::client_user::pb::{AuthenticateResponse, VoteRequest};
use super::super::helpers::client_user::UserClient;
use super::super::helpers::with_lifecycle::with_lifecycle;
use futures::FutureExt;
use ratings::app::AppContext;
use ratings::utils::{self, Infrastructure};
use sqlx::Row;

use utils::Config;

#[tokio::test]
async fn get_votes_lifecycle_test() -> Result<(), Box<dyn std::error::Error>> {
let config = Config::load()?;
let infra = Infrastructure::new(&config).await?;
let app_ctx = AppContext::new(&config, infra);

let data = TestData {
user_client: Some(UserClient::new(&config.socket())),
app_ctx,
id: None,
token: None,
app_client: None,
snap_id: None,
};

with_lifecycle(async {
authenticate(data.clone())
.then(cast_vote)
.then(get_votes)
.await;
})
.await;
Ok(())
}

async fn authenticate(mut data: TestData) -> TestData {
let id: String = helpers::data_faker::rnd_sha_256();
data.id = Some(id.to_string());

let client = data.user_client.clone().unwrap();
let response: AuthenticateResponse = client
.authenticate(&id)
.await
.expect("authentication request should succeed")
.into_inner();

let token: String = response.token;
data.token = Some(token.to_string());
helpers::assert::assert_token_is_valid(&token, &data.app_ctx.config().jwt_secret);

let mut conn = data.repository().await.unwrap();

let rows = sqlx::query("SELECT * FROM users WHERE client_hash = $1")
.bind(&id)
.fetch_one(&mut *conn)
.await
.unwrap();

let actual: String = rows.get("client_hash");

assert_eq!(actual, id);

data
}

async fn cast_vote(data: TestData) -> TestData {
let id = data.id.clone().unwrap();
let token = data.token.clone().unwrap();
let client = data.user_client.clone().unwrap();

let expected_snap_id = "r4LxMVp7zWramXsJQAKdamxy6TAWlaDD";
let expected_snap_revision = 111;
let expected_vote_up = true;

let ballet = VoteRequest {
snap_id: expected_snap_id.to_string(),
snap_revision: expected_snap_revision,
vote_up: expected_vote_up,
};

client
.vote(&token, ballet)
.await
.expect("vote should succeed")
.into_inner();

let mut conn = data.repository().await.unwrap();

let result = sqlx::query(
r#"
SELECT votes.*
FROM votes
JOIN users ON votes.user_id_fk = users.id
WHERE users.client_hash = $1 AND votes.snap_id = $2 AND votes.snap_revision = $3;
"#,
)
.bind(&id)
.bind(expected_snap_id)
.bind(expected_snap_revision)
.fetch_one(&mut *conn)
.await
.unwrap();

let actual_snap_id: String = result.try_get("snap_id").unwrap();
let actual_snap_revision: i32 = result.try_get("snap_revision").unwrap();
let actual_vote_up: bool = result.try_get("vote_up").unwrap();

assert_eq!(actual_snap_id, expected_snap_id);
assert_eq!(actual_snap_revision, expected_snap_revision);
assert_eq!(actual_vote_up, expected_vote_up);

let expected_snap_id = "r4LxMVp7zWramXsJQAKdamxy6TAWlaDD";
let expected_snap_revision = 112;
let expected_vote_up = false;

let ballet = VoteRequest {
snap_id: expected_snap_id.to_string(),
snap_revision: expected_snap_revision,
vote_up: expected_vote_up,
};

client
.vote(&token, ballet)
.await
.expect("vote should succeed")
.into_inner();

let result = sqlx::query(
r#"
SELECT votes.*
FROM votes
JOIN users ON votes.user_id_fk = users.id
WHERE users.client_hash = $1 AND votes.snap_id = $2 AND votes.snap_revision = $3;
"#,
)
.bind(&id)
.bind(expected_snap_id)
.bind(expected_snap_revision)
.fetch_one(&mut *conn)
.await
.unwrap();

let actual_snap_id: String = result.try_get("snap_id").unwrap();
let actual_snap_revision: i32 = result.try_get("snap_revision").unwrap();
let actual_vote_up: bool = result.try_get("vote_up").unwrap();

assert_eq!(actual_snap_id, expected_snap_id);
assert_eq!(actual_snap_revision, expected_snap_revision);
assert_eq!(actual_vote_up, expected_vote_up);

data
}

async fn get_votes(data: TestData) -> TestData {
let token = data.token.clone().unwrap();
let client = data.user_client.clone().unwrap();

let expected_snap_id = "r4LxMVp7zWramXsJQAKdamxy6TAWlaDD".to_string();
let expected_first_revision = 111;
let expected_first_vote_up = true;
let expected_second_revision = 112;
let expected_second_vote_up = false;

let request = GetSnapVotesRequest {
snap_id: expected_snap_id.clone(),
};
let votes = client
.get_snap_votes(&token, request)
.await
.expect("get votes should succeed")
.into_inner()
.votes;

let actual_snap_id = &votes[0].snap_id;
let actual_first_revision = votes[0].snap_revision;
let actual_first_vote_up = votes[0].vote_up;
let actual_second_revision = votes[1].snap_revision;
let actual_second_vote_up = votes[1].vote_up;

assert_eq!(actual_snap_id, &expected_snap_id);
assert_eq!(actual_first_revision, expected_first_revision);
assert_eq!(actual_first_vote_up, expected_first_vote_up);
assert_eq!(actual_second_vote_up, expected_second_vote_up);
assert_eq!(actual_second_revision, expected_second_revision);
data
}
Loading