-
Notifications
You must be signed in to change notification settings - Fork 56
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
Fix: Include quest completion timestamp in get_quest_participants response #359
Changes from 3 commits
b591c97
b59b489
9d19ead
6f335fc
c0e781c
c020ba4
b35c529
bcd040a
e1dd67b
a9cf7cd
75b1b16
be7664e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,4 +32,4 @@ regex = "1.10.0" | |
ctor = "0.2.6" | ||
axum-client-ip = "0.4.0" | ||
jsonwebtoken = "9" | ||
tower = "0.4.13" | ||
tower = "0.4.13" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -229,8 +229,10 @@ pub fn load() -> Config { | |
let args: Vec<String> = env::args().collect(); | ||
let config_path = if args.len() <= 1 { | ||
"config.toml" | ||
} else { | ||
} else if (args.len() > 1) && (args.get(1).unwrap().contains("toml")) { | ||
args.get(1).unwrap() | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to have these changes, you can revert |
||
"config.toml" | ||
}; | ||
let file_contents = fs::read_to_string(config_path); | ||
if file_contents.is_err() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,40 +52,59 @@ pub async fn handler( | |
} | ||
} | ||
}, | ||
// First group by address to get the max timestamp for each participant | ||
doc! { | ||
"$group": { | ||
"_id": "$address", | ||
"count" : { "$sum": 1 } | ||
"count": { "$sum": 1 }, | ||
"last_completion": { "$max": "$timestamp" } | ||
} | ||
}, | ||
// Add a field to indicate if all tasks are completed | ||
doc! { | ||
"$match": { | ||
"count": tasks_count as i64 | ||
"$addFields": { | ||
"completed_all": { | ||
"$eq": ["$count", tasks_count as i64] | ||
} | ||
} | ||
}, | ||
// Conditionally set quest_completion_time based on completed_all | ||
doc! { | ||
"$facet": { | ||
"count": [ | ||
{ | ||
"$count": "count" | ||
"$addFields": { | ||
"quest_completion_time": { | ||
"$cond": { | ||
"if": "$completed_all", | ||
"then": "$last_completion", | ||
"else": null | ||
} | ||
} | ||
} | ||
}, | ||
// Sort by completion time (null values will be at the end) | ||
doc! { | ||
"$sort": { | ||
"quest_completion_time": 1 | ||
} | ||
}, | ||
doc! { | ||
"$facet": { | ||
"total": [ | ||
{ "$count": "count" } | ||
], | ||
"firstParticipants": [ | ||
{ | ||
"$limit": 3 | ||
} | ||
"participants": [ | ||
{ "$project": { | ||
"address": "$_id", | ||
"tasks_completed": "$count", | ||
"quest_completion_time": 1, | ||
"_id": 0 | ||
}} | ||
] | ||
} | ||
}, | ||
doc! { | ||
"$project": { | ||
"count": { | ||
"$arrayElemAt": [ | ||
"$count.count", | ||
0 | ||
] | ||
}, | ||
"firstParticipants": "$firstParticipants._id" | ||
"total": { "$ifNull": [{ "$arrayElemAt": ["$total.count", 0] }, 0] }, | ||
"first_participants": "$participants" | ||
} | ||
}, | ||
]; | ||
|
@@ -106,5 +125,167 @@ pub async fn handler( | |
} | ||
} | ||
|
||
return (StatusCode::OK, Json(res)).into_response(); | ||
(StatusCode::OK, Json(res)).into_response() | ||
} | ||
#[cfg(test)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to have the tests, but it was a good idea to make sure everything was working. The problem is that if we run tests in production, it'll reset the db |
||
mod tests { | ||
use crate::{ | ||
config::{self, Config}, | ||
logger, | ||
}; | ||
|
||
use super::*; | ||
use axum::body::HttpBody; | ||
use axum::{body::Bytes, http::StatusCode}; | ||
use mongodb::{bson::doc, Client, Database}; | ||
use reqwest::Url; | ||
use serde_json::Value; | ||
use starknet::providers::{jsonrpc::HttpTransport, JsonRpcClient}; | ||
use std::sync::Arc; | ||
use tokio::sync::Mutex; | ||
|
||
async fn setup_test_db() -> Database { | ||
let client = Client::with_uri_str("mongodb://localhost:27017") | ||
.await | ||
.expect("Failed to create MongoDB client"); | ||
let db = client.database("test_db"); | ||
|
||
// Clear collections before each test | ||
db.collection::<Document>("tasks").drop(None).await.ok(); | ||
db.collection::<Document>("completed_tasks") | ||
.drop(None) | ||
.await | ||
.ok(); | ||
|
||
db | ||
} | ||
|
||
async fn insert_test_data(db: Database, quest_id: i64, num_tasks: i64, num_participants: i64) { | ||
let tasks_collection = db.collection::<Document>("tasks"); | ||
let completed_tasks_collection = db.collection::<Document>("completed_tasks"); | ||
|
||
// Insert tasks | ||
for task_id in 1..=num_tasks { | ||
tasks_collection | ||
.insert_one( | ||
doc! { | ||
"id": task_id, | ||
"quest_id": quest_id, | ||
}, | ||
None, | ||
) | ||
.await | ||
.unwrap(); | ||
} | ||
|
||
// Insert completed tasks for participants | ||
// Each participant will have a different timestamp for each task | ||
// timestamp will be 1000 - (participant * 10) + task_id | ||
// This way, the last task for each participant will have the highest timestamp | ||
// and the last participant will be the one who completed the quest first | ||
|
||
// 2..=num_participants: skip the first participant | ||
// The first participant haven't completed all the tasks | ||
for participant in 1..=num_participants { | ||
let address = format!("participant_{}", participant); | ||
let base_timestamp = 1000 - (participant * 10); | ||
|
||
// First participant only do one task => not completed the quest yet | ||
if participant == 1 { | ||
completed_tasks_collection.insert_one( | ||
doc! { | ||
"address": address.clone(), | ||
"task_id": 1, | ||
"timestamp": base_timestamp + 1 | ||
}, | ||
None, | ||
).await.unwrap(); | ||
} else { | ||
for task_id in 1..=num_tasks { | ||
completed_tasks_collection | ||
.insert_one( | ||
doc! { | ||
"address": address.clone(), | ||
"task_id": task_id, | ||
// Last task for each participant will have the highest timestamp | ||
"timestamp": base_timestamp + task_id | ||
}, | ||
None, | ||
) | ||
.await | ||
.unwrap(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_get_quest_participants() { | ||
// Setup | ||
let db = setup_test_db().await; | ||
let conf = config::load(); | ||
let logger = logger::Logger::new(&conf.watchtower); | ||
let provider = JsonRpcClient::new(HttpTransport::new( | ||
Url::parse(&conf.variables.rpc_url).unwrap(), | ||
)); | ||
|
||
let app_state = Arc::new(AppState { | ||
db: db.clone(), | ||
last_task_id: Mutex::new(0), | ||
last_question_id: Mutex::new(0), | ||
conf, | ||
logger, | ||
provider, | ||
}); | ||
|
||
// Test data | ||
let quest_id = 1; | ||
let num_tasks = 3; | ||
let num_participants = 5; | ||
|
||
insert_test_data(db.clone(), quest_id, num_tasks, num_participants).await; | ||
|
||
// Create request | ||
let query = GetQuestParticipantsQuery { | ||
quest_id: quest_id as u32, | ||
}; | ||
|
||
// Execute request | ||
let response = handler(State(app_state), Query(query)) | ||
.await | ||
.into_response(); | ||
|
||
// Verify response | ||
assert_eq!(response.status(), StatusCode::OK); | ||
|
||
// Get the response body as bytes | ||
let body_bytes = match response.into_body().data().await { | ||
Some(Ok(bytes)) => bytes, | ||
_ => panic!("Failed to get response body"), | ||
}; | ||
|
||
// Parse the body | ||
let body: Value = serde_json::from_slice(&body_bytes).unwrap(); | ||
|
||
// We has excluded the first participant from the test data | ||
assert_eq!(body["total"], num_participants); | ||
|
||
// Verify first participants | ||
let first_participants = body["first_participants"].as_array().unwrap(); | ||
|
||
// Verify quest completion timestamp | ||
let quest_completion_timestamp = body["first_participants"][1]["quest_completion_time"] | ||
.as_i64() | ||
.unwrap(); | ||
assert_eq!(quest_completion_timestamp, 953); | ||
|
||
// Verify participant not completed the quest | ||
let participant_not_completed = first_participants.iter().find(|participant| { | ||
participant["address"].as_str().unwrap() == "participant_1" | ||
}).unwrap(); | ||
|
||
assert_eq!(participant_not_completed["tasks_completed"].as_i64().unwrap(), 1); | ||
assert_eq!(participant_not_completed["quest_completion_time"].as_i64(), None); // Not completed | ||
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,4 +26,8 @@ pub mod tests { | |
let response = client.get(endpoint).send().await.unwrap(); | ||
assert_eq!(response.status(), StatusCode::OK); | ||
} | ||
|
||
// #[tokio::test] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to have these comments |
||
// pub async fn test_get_quest_participants() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can remove this |
||
// let endpoint = format!("http:// | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
mod utils; | ||
mod endpoints; | ||
mod utils; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why ?