diff options
-rw-r--r-- | src/bin/main.rs (renamed from src/main.rs) | 9 | ||||
-rw-r--r-- | src/custom_filters.rs | 18 | ||||
-rw-r--r-- | src/handlers.rs | 128 | ||||
-rw-r--r-- | src/lib.rs | 34 | ||||
-rw-r--r-- | src/routes.rs | 380 | ||||
-rw-r--r-- | src/schema.rs | 68 | ||||
-rwxr-xr-x | tester.sh | 2 | ||||
-rw-r--r-- | tests/route_tests.rs | 371 |
8 files changed, 529 insertions, 481 deletions
diff --git a/src/main.rs b/src/bin/main.rs index 5683aea..8d88286 100644 --- a/src/main.rs +++ b/src/bin/main.rs | |||
@@ -1,10 +1,9 @@ | |||
1 | use gradecoin::schema::create_database; | ||
2 | use std::env; | 1 | use std::env; |
3 | use warp::Filter; | 2 | use warp::Filter; |
4 | 3 | ||
5 | mod custom_filters; | 4 | use gradecoin::routes::consensus_routes; |
6 | mod handlers; | 5 | use gradecoin::schema::create_database; |
7 | mod routes; | 6 | |
8 | // mod validators; | 7 | // mod validators; |
9 | 8 | ||
10 | #[tokio::main] | 9 | #[tokio::main] |
@@ -17,7 +16,7 @@ async fn main() { | |||
17 | 16 | ||
18 | let db = create_database(); | 17 | let db = create_database(); |
19 | 18 | ||
20 | let api = routes::consensus_routes(db); | 19 | let api = consensus_routes(db); |
21 | 20 | ||
22 | let routes = api.with(warp::log("gradecoin")); | 21 | let routes = api.with(warp::log("gradecoin")); |
23 | 22 | ||
diff --git a/src/custom_filters.rs b/src/custom_filters.rs index f93f572..dfdae04 100644 --- a/src/custom_filters.rs +++ b/src/custom_filters.rs | |||
@@ -1,30 +1,38 @@ | |||
1 | use gradecoin::schema::{AuthRequest, Block, Db, Transaction}; | 1 | /// Functions that extracts Structs to be used in warp routines |
2 | use crate::schema::{AuthRequest, Block, Db, Transaction}; | ||
2 | use std::convert::Infallible; | 3 | use std::convert::Infallible; |
3 | use warp::{Filter, Rejection}; | 4 | use warp::{Filter, Rejection}; |
4 | 5 | ||
5 | // Database context for routes | 6 | /// Wraps the database to be used in warp routes |
6 | pub fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = Infallible> + Clone { | 7 | pub fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = Infallible> + Clone { |
7 | warp::any().map(move || db.clone()) | 8 | warp::any().map(move || db.clone()) |
8 | } | 9 | } |
9 | 10 | ||
10 | // Accept only json encoded User body and reject big payloads | 11 | /// Extracts an `AuthRequest` JSON body from the request |
12 | /// Accepts only JSON encoded `AuthRequest` body and rejects big payloads | ||
13 | /// | ||
11 | // TODO: find a good limit for this, (=e2482057; 8 char String + rsa pem) <11-04-21, yigit> // | 14 | // TODO: find a good limit for this, (=e2482057; 8 char String + rsa pem) <11-04-21, yigit> // |
12 | pub fn auth_request_json_body() -> impl Filter<Extract = (AuthRequest,), Error = Rejection> + Clone | 15 | pub fn auth_request_json_body() -> impl Filter<Extract = (AuthRequest,), Error = Rejection> + Clone |
13 | { | 16 | { |
14 | warp::body::content_length_limit(1024 * 32).and(warp::body::json()) | 17 | warp::body::content_length_limit(1024 * 32).and(warp::body::json()) |
15 | } | 18 | } |
16 | 19 | ||
17 | // Accept only json encoded Transaction body and reject big payloads | 20 | /// Extracts an `Transaction` JSON body from the request |
21 | /// Accepts only JSON encoded `Transaction` body and rejects big payloads | ||
18 | // TODO: find a good limit for this <11-04-21, yigit> // | 22 | // TODO: find a good limit for this <11-04-21, yigit> // |
19 | pub fn transaction_json_body() -> impl Filter<Extract = (Transaction,), Error = Rejection> + Clone { | 23 | pub fn transaction_json_body() -> impl Filter<Extract = (Transaction,), Error = Rejection> + Clone { |
20 | warp::body::content_length_limit(1024 * 32).and(warp::body::json()) | 24 | warp::body::content_length_limit(1024 * 32).and(warp::body::json()) |
21 | } | 25 | } |
22 | 26 | ||
27 | /// Extracts the value of the `Authorization` header field, hopefully a valid JWT | ||
28 | /// Used in Authorization for `Block` and `Transaction` proposals | ||
29 | /// Rejects the request if the Authorization header does not exist | ||
23 | pub fn auth_header() -> impl Filter<Extract = (String,), Error = Rejection> + Clone { | 30 | pub fn auth_header() -> impl Filter<Extract = (String,), Error = Rejection> + Clone { |
24 | warp::header::header::<String>("Authorization") | 31 | warp::header::header::<String>("Authorization") |
25 | } | 32 | } |
26 | 33 | ||
27 | // Accept only json encoded Block body and reject big payloads | 34 | /// Extracts an `Block` JSON body from the request |
35 | /// Accepts only JSON encoded `Block` body and rejects big payloads | ||
28 | // TODO: find a good limit for this <11-04-21, yigit> // | 36 | // TODO: find a good limit for this <11-04-21, yigit> // |
29 | pub fn block_json_body() -> impl Filter<Extract = (Block,), Error = Rejection> + Clone { | 37 | pub fn block_json_body() -> impl Filter<Extract = (Block,), Error = Rejection> + Clone { |
30 | warp::body::content_length_limit(1024 * 32).and(warp::body::json()) | 38 | warp::body::content_length_limit(1024 * 32).and(warp::body::json()) |
diff --git a/src/handlers.rs b/src/handlers.rs index 07986f5..80ed1f7 100644 --- a/src/handlers.rs +++ b/src/handlers.rs | |||
@@ -1,37 +1,28 @@ | |||
1 | /// API handlers, the ends of each filter chain | ||
1 | use blake2::{Blake2s, Digest}; | 2 | use blake2::{Blake2s, Digest}; |
2 | use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; | 3 | use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; |
3 | /// API handlers, the ends of each filter chain | ||
4 | use log::debug; | 4 | use log::debug; |
5 | use md5::Md5; | 5 | use md5::Md5; |
6 | use parking_lot::RwLockUpgradableReadGuard; | 6 | use parking_lot::RwLockUpgradableReadGuard; |
7 | use serde::{Deserialize, Serialize}; | ||
8 | use serde_json; | 7 | use serde_json; |
9 | use serde_json::json; | ||
10 | use std::convert::Infallible; | 8 | use std::convert::Infallible; |
11 | use std::fs; | 9 | use std::fs; |
12 | use warp::{http::Response, http::StatusCode, reject, reply}; | 10 | use warp::{http::Response, http::StatusCode, reply}; |
13 | 11 | ||
14 | use gradecoin::schema::{ | 12 | use crate::schema::{AuthRequest, Block, Claims, Db, MetuId, NakedBlock, Transaction, User}; |
15 | AuthRequest, Block, Db, MetuId, NakedBlock, PublicKeySignature, Transaction, User, | ||
16 | }; | ||
17 | 13 | ||
18 | const BEARER: &str = "Bearer "; | 14 | const BEARER: &str = "Bearer "; |
19 | 15 | ||
20 | /// tha: Transaction Hash, String | 16 | /// POST request to /register endpoint |
21 | /// iat: Issued At, Unix Time, epoch | 17 | /// |
22 | #[derive(Debug, Serialize, Deserialize)] | 18 | /// Lets a [`User`] (=student) to authenticate themselves to the system |
23 | pub struct Claims { | 19 | /// This `request` can be rejected if the payload is malformed (= not authenticated properly) or if |
24 | pub tha: String, | 20 | /// the [`AuthRequest.user_id`] of the `request` is not in the list of users that can hold a Gradecoin account |
25 | pub iat: usize, | ||
26 | } | ||
27 | |||
28 | /// POST /register | ||
29 | /// Enables a student to introduce themselves to the system | ||
30 | /// Can fail | ||
31 | pub async fn authenticate_user( | 21 | pub async fn authenticate_user( |
32 | request: AuthRequest, | 22 | request: AuthRequest, |
33 | db: Db, | 23 | db: Db, |
34 | ) -> Result<impl warp::Reply, warp::Rejection> { | 24 | ) -> Result<impl warp::Reply, warp::Rejection> { |
25 | debug!("POST request to /register, authenticate_user"); | ||
35 | let given_id = request.student_id.clone(); | 26 | let given_id = request.student_id.clone(); |
36 | 27 | ||
37 | if let Some(priv_student_id) = MetuId::new(request.student_id) { | 28 | if let Some(priv_student_id) = MetuId::new(request.student_id) { |
@@ -77,7 +68,7 @@ pub async fn authenticate_user( | |||
77 | /// Returns JSON array of transactions | 68 | /// Returns JSON array of transactions |
78 | /// Cannot fail | 69 | /// Cannot fail |
79 | pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> { | 70 | pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> { |
80 | debug!("list all transactions"); | 71 | debug!("GET request to /transaction, list_transactions"); |
81 | let mut result = Vec::new(); | 72 | let mut result = Vec::new(); |
82 | 73 | ||
83 | let transactions = db.pending_transactions.read(); | 74 | let transactions = db.pending_transactions.read(); |
@@ -95,31 +86,13 @@ pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> { | |||
95 | /// Cannot fail | 86 | /// Cannot fail |
96 | /// Mostly around for debug purposes | 87 | /// Mostly around for debug purposes |
97 | pub async fn list_blocks(db: Db) -> Result<impl warp::Reply, Infallible> { | 88 | pub async fn list_blocks(db: Db) -> Result<impl warp::Reply, Infallible> { |
98 | debug!("list all block"); | 89 | debug!("GET request to /block, list_blocks"); |
99 | 90 | ||
100 | let block = db.blockchain.read(); | 91 | let block = db.blockchain.read(); |
101 | 92 | ||
102 | Ok(reply::with_status(reply::json(&*block), StatusCode::OK)) | 93 | Ok(reply::with_status(reply::json(&*block), StatusCode::OK)) |
103 | } | 94 | } |
104 | 95 | ||
105 | /// POST /transaction | ||
106 | /// Pushes a new transaction for pending transaction pool | ||
107 | /// Can reject the transaction proposal | ||
108 | /// TODO: when is a new transaction rejected <07-04-21, yigit> // | ||
109 | pub async fn propose_transaction( | ||
110 | new_transaction: Transaction, | ||
111 | db: Db, | ||
112 | ) -> Result<impl warp::Reply, warp::Rejection> { | ||
113 | debug!("new transaction request {:?}", new_transaction); | ||
114 | |||
115 | // let mut transactions = db.lock().await; | ||
116 | let mut transactions = db.pending_transactions.write(); | ||
117 | |||
118 | transactions.insert(new_transaction.source.to_owned(), new_transaction); | ||
119 | |||
120 | Ok(StatusCode::CREATED) | ||
121 | } | ||
122 | |||
123 | /// POST /block | 96 | /// POST /block |
124 | /// Proposes a new block for the next round | 97 | /// Proposes a new block for the next round |
125 | /// Can reject the block | 98 | /// Can reject the block |
@@ -181,41 +154,64 @@ pub async fn propose_block(new_block: Block, db: Db) -> Result<impl warp::Reply, | |||
181 | } | 154 | } |
182 | } | 155 | } |
183 | 156 | ||
157 | /// POST /transaction | ||
158 | /// | ||
159 | /// Handles the new transaction requests | ||
160 | /// Can reject the block if; | ||
161 | /// # Arguments | ||
162 | /// * `new_transaction` - Valid JSON of a [`Transaction`] | ||
163 | /// * `token` - An Authorization header value such as `Bearer aaa.bbb.ccc` | ||
164 | /// * `db` - Global [`Db`] instance | ||
165 | /// | ||
166 | /// TODO This method should check if the user has enough balance for the transaction | ||
184 | pub async fn auth_propose_transaction( | 167 | pub async fn auth_propose_transaction( |
185 | new_transaction: Transaction, | 168 | new_transaction: Transaction, |
186 | token: String, | 169 | token: String, |
187 | db: Db, | 170 | db: Db, |
188 | ) -> Result<impl warp::Reply, warp::Rejection> { | 171 | ) -> Result<impl warp::Reply, warp::Rejection> { |
189 | debug!("new transaction request {:?}", new_transaction); | 172 | debug!("POST request to /transaction, propose_transaction"); |
190 | let raw_jwt = token.trim_start_matches(BEARER).to_owned(); | 173 | debug!("The transaction request: {:?}", new_transaction); |
191 | 174 | ||
192 | let decoded = jsonwebtoken::decode::<Claims>( | 175 | let raw_jwt = token.trim_start_matches(BEARER).to_owned(); |
193 | &token, | 176 | debug!("raw_jwt: {:?}", raw_jwt); |
194 | &DecodingKey::from_rsa_pem( | 177 | |
195 | db.users | 178 | if let Some(user) = db.users.read().get(&new_transaction.by) { |
196 | .read() | 179 | // This public key was already written to the database, we can panic if it's not valid at |
197 | .get(&new_transaction.by) | 180 | // *this* point |
198 | .unwrap() | 181 | let by_public_key = &user.public_key; |
199 | .public_key | 182 | |
200 | .as_bytes(), | 183 | if let Ok(decoded) = decode::<Claims>( |
201 | ) | 184 | &raw_jwt, |
202 | .unwrap(), | 185 | &DecodingKey::from_rsa_pem(by_public_key.as_bytes()).unwrap(), |
203 | // todo@keles: If user is not found return user not found error | 186 | &Validation::new(Algorithm::RS256), |
204 | &Validation::new(Algorithm::PS256), | 187 | ) { |
205 | ) | 188 | // this transaction was already checked for correctness at custom_filters, we can panic |
206 | .unwrap(); | 189 | // here if it has been changed since |
207 | // todo: If user is found but header is not validated, return header not valid | 190 | |
208 | 191 | let hashed_transaction = Md5::digest(&serde_json::to_vec(&new_transaction).unwrap()); | |
209 | let hashed_transaction = Md5::digest(&serde_json::to_vec(&new_transaction).unwrap()); | 192 | |
210 | 193 | if decoded.claims.tha == format!("{:x}", hashed_transaction) { | |
211 | // let mut transactions = db.lock().await; | 194 | let mut transactions = db.pending_transactions.write(); |
212 | if decoded.claims.tha == format!("{:x}", hashed_transaction) { | 195 | |
213 | let mut transactions = db.pending_transactions.write(); | 196 | transactions.insert(new_transaction.source.to_owned(), new_transaction); |
214 | 197 | ||
215 | transactions.insert(new_transaction.source.to_owned(), new_transaction); | 198 | Ok(StatusCode::CREATED) |
216 | 199 | } else { | |
217 | Ok(StatusCode::CREATED) | 200 | debug!( |
201 | "the hash of the request {:x} did not match with the hash given in jwt {:?}", | ||
202 | hashed_transaction, decoded.claims.tha | ||
203 | ); | ||
204 | Ok(StatusCode::BAD_REQUEST) | ||
205 | } | ||
206 | } else { | ||
207 | debug!("raw_jwt was malformed {:?}", raw_jwt); | ||
208 | Ok(StatusCode::BAD_REQUEST) | ||
209 | } | ||
218 | } else { | 210 | } else { |
211 | debug!( | ||
212 | "A user with public key signature {:?} is not found in the database", | ||
213 | new_transaction.by | ||
214 | ); | ||
219 | Ok(StatusCode::BAD_REQUEST) | 215 | Ok(StatusCode::BAD_REQUEST) |
220 | } | 216 | } |
221 | } | 217 | } |
@@ -1,9 +1,27 @@ | |||
1 | pub mod schema; | 1 | //! # Gradecoin |
2 | //! | ||
3 | //! ## Services | ||
4 | //! ### /register | ||
5 | //! - Student creates their own 2048 bit RSA `keypair` | ||
6 | //! - Downloads `Gradecoin`'s Public Key from Moodle | ||
7 | //! - Encrypts their JSON wrapped `Public Key` and `Student ID` using Gradecoin's Public Key | ||
8 | //! - Their public key is now in our Db under [`schema::User::public_key`] and can be used to sign their JWT's during requests | ||
9 | //! | ||
10 | //! ### /transaction | ||
11 | //! - offer a [`schema::Transaction`] - POST request | ||
12 | //! - The request should have `Authorization` | ||
13 | //! - The request header should be signed by the Public Key of the `by` field in the transaction | ||
14 | //! - fetch the list of `Transaction`s - GET request | ||
15 | //! | ||
16 | //! ### /block | ||
17 | //! - offer a [`schema::Block`] - POST request | ||
18 | //! - The request should have `Authorization` | ||
19 | //! - The [`schema::Block::transaction_list`] of the block should be a subset of [`schema::Db::pending_transactions`] | ||
20 | //! - fetch the last accepted [`schema::Block`] - GET request | ||
21 | //! | ||
22 | //! `Authorization`: The request header should have Bearer JWT.Token signed with Student Public Key | ||
2 | 23 | ||
3 | pub use schema::create_database; | 24 | pub mod custom_filters; |
4 | pub use schema::AuthRequest; | 25 | pub mod handlers; |
5 | pub use schema::Block; | 26 | pub mod routes; |
6 | pub use schema::Db; | 27 | pub mod schema; |
7 | pub use schema::MetuId; | ||
8 | pub use schema::Transaction; | ||
9 | pub use schema::User; | ||
diff --git a/src/routes.rs b/src/routes.rs index ed2acad..e4bdee4 100644 --- a/src/routes.rs +++ b/src/routes.rs | |||
@@ -1,10 +1,11 @@ | |||
1 | use gradecoin::schema::Db; | 1 | /// Endpoints and their construction |
2 | use warp::{Filter, Rejection, Reply}; | 2 | use warp::{Filter, Rejection, Reply}; |
3 | 3 | ||
4 | use crate::custom_filters; | 4 | use crate::custom_filters; |
5 | use crate::handlers; | 5 | use crate::handlers; |
6 | use crate::schema::Db; | ||
6 | 7 | ||
7 | /// Root, all routes combined | 8 | /// Every route combined |
8 | pub fn consensus_routes(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | 9 | pub fn consensus_routes(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { |
9 | transaction_list(db.clone()) | 10 | transaction_list(db.clone()) |
10 | .or(register_user(db.clone())) | 11 | .or(register_user(db.clone())) |
@@ -39,15 +40,6 @@ pub fn block_list(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection | |||
39 | } | 40 | } |
40 | 41 | ||
41 | /// POST /transaction warp route | 42 | /// POST /transaction warp route |
42 | pub fn transaction_propose(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | ||
43 | warp::path!("transaction") | ||
44 | .and(warp::post()) | ||
45 | .and(custom_filters::transaction_json_body()) | ||
46 | .and(custom_filters::with_db(db)) | ||
47 | .and_then(handlers::propose_transaction) | ||
48 | } | ||
49 | |||
50 | /// POST /transaction warp route | ||
51 | pub fn auth_transaction_propose( | 43 | pub fn auth_transaction_propose( |
52 | db: Db, | 44 | db: Db, |
53 | ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | 45 | ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { |
@@ -68,369 +60,3 @@ pub fn block_propose(db: Db) -> impl Filter<Extract = impl Reply, Error = Reject | |||
68 | .and_then(handlers::propose_block) | 60 | .and_then(handlers::propose_block) |
69 | } | 61 | } |
70 | 62 | ||
71 | #[cfg(test)] | ||
72 | mod tests { | ||
73 | use gradecoin::schema::{create_database, AuthRequest, Block, MetuId, Transaction, User}; | ||
74 | use handlers::Claims; | ||
75 | // use chrono::prelude::*; | ||
76 | // use parking_lot::RwLock; | ||
77 | // use std::sync::Arc; | ||
78 | use warp::http::StatusCode; | ||
79 | |||
80 | use super::*; | ||
81 | |||
82 | use jsonwebtoken::{Header, encode, EncodingKey, Algorithm}; | ||
83 | const private_key_pem: &str = "-----BEGIN RSA PRIVATE KEY----- | ||
84 | MIIEpAIBAAKCAQEA4nU0G4WjkmcQUx0hq6LQuV5Q+ACmUFL/OjoYMDwC/O/6pCd1 | ||
85 | UZgCfgHN2xEffDPznzcTn8OiFRxr4oWyBinyrUpnY4mhy0SQUwoeCw7YkcHAyhCj | ||
86 | NT74aR/ohX0MCj0qRRdbt5ZQXM/GC3HJuXE1ptSuhFgQxziItamn8maoJ6JUSVEX | ||
87 | VO1NOrrjoM3r7Q+BK2B+sX4/bLZ+VG5g1q2nEbFdTHS6pHqtZNHQndTmEKwRfh0R | ||
88 | YtzEzOXuO6e1gQY42Tujkof40dhGCIU7TeIGGHwdFxy1niLkXwtHNjV7lnIOkTbx | ||
89 | 6+sSPamRfQAlZqUWM2Lf5o+7h3qWP3ENB138sQIDAQABAoIBAD23nYTmrganag6M | ||
90 | wPFrBSGP79c3Lhx0EjUHQjJbGKFgsdltG48qM3ut+DF9ACy0Z+/7bbC7+39vaIOq | ||
91 | 1jLR2d6aiYTaLKseO4s2FawD1sgamvU3BZPsXn0gAhnnU5Gyy8Nas1dccvhoc9wI | ||
92 | neaZUPrvucQ90AzLfo6r9yacDbYHB1lOyomApUvpJxOgHISGEtc9qGPDrdH19aF0 | ||
93 | 8fCv2bbQRh+TChgN3IB0o5w0wXaI7YAyAouAv/AzHCoEMpt7OGjFTkjh/ujlPL9O | ||
94 | +FLuJNsQRHDN0gJo2pcvwGwDCsioMixQ9bZ7ZrUu2BNpEQygyeSbj9ZI1iRvhosO | ||
95 | JU3rwEECgYEA9MppTYA6A9WQbCCwPH1QMpUAmPNVSWVhUVag4lGOEhdCDRcz9ook | ||
96 | DohQMKctiEB1luKuvDokxo0uMOfMO9/YwjsRB7qjQip7Th1zMJIjD+A+juLzHK4r | ||
97 | /RiRtWYGAnF8mptDvE+93JsPb3C/lQLvIhio5GQYWBqPJu6SpeosIskCgYEA7NPi | ||
98 | Gbffzr2UQhW8BNKmctEEh8yFRVojFo3wwwWxSNUVXGSmSm31CL+Q8h817R+2OkPV | ||
99 | 1ZMUOBU4UJiqFt28kIvTDFqbAJlJQGCpY2mY7OLQiD2A+TVLcFrHmoCaPfCAK1Qd | ||
100 | hQ0PmFK7Mf8qClpA3E5chop/WfKQfiu46sZv1qkCgYAhGdXPcw1lQ1W6KVlrdI6J | ||
101 | qHhiNlVMDXdxZkNvFxQdAiQeXQrbxaZGiMw/J/wSNpUwCAsUzM/4QVMDrfSCDCzl | ||
102 | ZtNQtj4pTlFKKNVQthIjrXEIJUw2jp7IJLBfVSJu5iWxSlmId0f3MsiNizN81N69 | ||
103 | P5Rm/doE3+KHoy8VXGsHcQKBgQCkNh62enqjHWypjex6450qS6f6iWN3PRLLVsw0 | ||
104 | TcQpniZblCaBwVCAKmRUnjOEIdL2/4ZLutnwMTaFG/YEOOfAylMiY8jKV38lNmD9 | ||
105 | X4D78CFr9klxgvS2CRwSE03f2NzmLkLxuKaxldvaxPTfjMkgeO1LFMlNExYBhkuH | ||
106 | 7uQpUQKBgQCKX6qMNh2gSdgG7qyxfTFZ4y5EGOBoKe/dE+IcVF3Vnh6DZVbCAbBL | ||
107 | 5EdFWZSrCnDjA4xiKW55mwp95Ud9EZsZAb13L8V9t82eK+UDBoWlb7VRNYpda/x1 | ||
108 | 5/i4qQJ28x2UNJDStpYFpnp4Ba1lvXjKngIbDPkjU+hbBJ+BNGAIeg== | ||
109 | -----END RSA PRIVATE KEY-----"; | ||
110 | /// Create a mock database to be used in tests | ||
111 | fn mocked_db() -> Db { | ||
112 | let db = create_database(); | ||
113 | |||
114 | db.users.write().insert( | ||
115 | "mock_transaction_source".to_owned(), | ||
116 | User { | ||
117 | user_id: MetuId::new("e254275".to_owned()).unwrap(), | ||
118 | public_key: | ||
119 | "-----BEGIN PUBLIC KEY----- | ||
120 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4nU0G4WjkmcQUx0hq6LQ | ||
121 | uV5Q+ACmUFL/OjoYMDwC/O/6pCd1UZgCfgHN2xEffDPznzcTn8OiFRxr4oWyBiny | ||
122 | rUpnY4mhy0SQUwoeCw7YkcHAyhCjNT74aR/ohX0MCj0qRRdbt5ZQXM/GC3HJuXE1 | ||
123 | ptSuhFgQxziItamn8maoJ6JUSVEXVO1NOrrjoM3r7Q+BK2B+sX4/bLZ+VG5g1q2n | ||
124 | EbFdTHS6pHqtZNHQndTmEKwRfh0RYtzEzOXuO6e1gQY42Tujkof40dhGCIU7TeIG | ||
125 | GHwdFxy1niLkXwtHNjV7lnIOkTbx6+sSPamRfQAlZqUWM2Lf5o+7h3qWP3ENB138 | ||
126 | sQIDAQAB | ||
127 | -----END PUBLIC KEY-----" | ||
128 | .to_owned(), | ||
129 | balance: 0, | ||
130 | }, | ||
131 | ); | ||
132 | db.pending_transactions.write().insert( | ||
133 | "hash_value".to_owned(), | ||
134 | Transaction { | ||
135 | by: "source_account".to_owned(), | ||
136 | source: "source_account".to_owned(), | ||
137 | target: "target_account".to_owned(), | ||
138 | amount: 20, | ||
139 | timestamp: chrono::NaiveDate::from_ymd(2021, 04, 09).and_hms(1, 30, 30), | ||
140 | }, | ||
141 | ); | ||
142 | |||
143 | *db.blockchain.write() = Block { | ||
144 | transaction_list: vec![ | ||
145 | "old_transaction_hash_1".to_owned(), | ||
146 | "old_transaction_hash_2".to_owned(), | ||
147 | "old_transaction_hash_3".to_owned(), | ||
148 | ], | ||
149 | nonce: 0, | ||
150 | timestamp: chrono::NaiveDate::from_ymd(2021, 04, 08).and_hms(12, 30, 30), | ||
151 | hash: "not_a_thing_yet".to_owned(), | ||
152 | }; | ||
153 | |||
154 | db | ||
155 | } | ||
156 | |||
157 | fn mocked_jwt() -> String { | ||
158 | |||
159 | let claims = Claims { | ||
160 | tha: "6692e774eba7fb92dc0fe6cf7347591e".to_owned(), | ||
161 | iat: 1516239022, | ||
162 | }; | ||
163 | let header = Header::new(Algorithm::RS256); | ||
164 | encode(&header, &claims, &EncodingKey::from_rsa_pem(private_key_pem.as_bytes()).unwrap()).unwrap() | ||
165 | } | ||
166 | /// Create a mock user that is allowed to be in gradecoin to be used in tests | ||
167 | fn priviliged_mocked_user() -> AuthRequest { | ||
168 | AuthRequest { | ||
169 | student_id: String::from("e254275"), | ||
170 | public_key: "NOT IMPLEMENTED".to_owned(), | ||
171 | } | ||
172 | } | ||
173 | |||
174 | /// Create a mock user that is NOT allowed to be in gradecoin to be used in tests | ||
175 | fn unpriviliged_mocked_user() -> AuthRequest { | ||
176 | AuthRequest { | ||
177 | student_id: String::from("foobarbaz"), | ||
178 | public_key: "NOT IMPLEMENTED".to_owned(), | ||
179 | } | ||
180 | } | ||
181 | |||
182 | /// Create a mock transaction to be used in tests | ||
183 | fn mocked_transaction() -> Transaction { | ||
184 | Transaction { | ||
185 | by: "mock_transaction_source".to_owned(), | ||
186 | source: "mock_transaction_source".to_owned(), | ||
187 | target: "mock_transaction_target".to_owned(), | ||
188 | amount: 25, | ||
189 | timestamp: chrono::NaiveDate::from_ymd(2021, 04, 09).and_hms(14, 30, 00), | ||
190 | } | ||
191 | } | ||
192 | |||
193 | /// Create a mock block with a correct mined hash to be used in tests | ||
194 | fn mocked_block() -> Block { | ||
195 | Block { | ||
196 | transaction_list: vec!["hash_value".to_owned()], | ||
197 | nonce: 3831993, | ||
198 | timestamp: chrono::NaiveDate::from_ymd(2021, 04, 08).and_hms(12, 30, 30), | ||
199 | hash: "2b648ffab5d9af1d5d5fc052fc9e51b882fc4fb0c998608c99232f9282000000".to_owned(), | ||
200 | } | ||
201 | } | ||
202 | |||
203 | /// Create a mock block with a wrong hash and nonce | ||
204 | fn mocked_wrong_block() -> Block { | ||
205 | Block { | ||
206 | transaction_list: vec!["foobarbaz".to_owned(), "dazsaz".to_owned()], | ||
207 | nonce: 1000, // can you imagine | ||
208 | timestamp: chrono::NaiveDate::from_ymd(2021, 04, 12).and_hms(05, 29, 30), | ||
209 | hash: "tnarstnarsuthnarsthlarjstk".to_owned(), | ||
210 | } | ||
211 | } | ||
212 | |||
213 | /// Test simple GET request to /transaction, resource that exists | ||
214 | /// https://tools.ietf.org/html/rfc7231#section-6.3.1 | ||
215 | /// We should get the only pending transaction available in the database as json | ||
216 | #[tokio::test] | ||
217 | async fn get_pending_transactions() { | ||
218 | let db = mocked_db(); | ||
219 | |||
220 | let reply = consensus_routes(db); | ||
221 | |||
222 | let res = warp::test::request() | ||
223 | .method("GET") | ||
224 | .path("/transaction") | ||
225 | .reply(&reply) | ||
226 | .await; | ||
227 | |||
228 | assert_eq!(res.status(), StatusCode::OK); | ||
229 | |||
230 | let expected_json_body = r#"[{"by":"source_account","source":"source_account","target":"target_account","amount":20,"timestamp":"2021-04-09T01:30:30"}]"#; | ||
231 | |||
232 | assert_eq!(res.body(), expected_json_body); | ||
233 | } | ||
234 | |||
235 | /// Test simple GET request to /block, resource that exists | ||
236 | /// https://tools.ietf.org/html/rfc7231#section-6.3.1 | ||
237 | /// Should return the single block available in the database as json | ||
238 | #[tokio::test] | ||
239 | async fn get_blockchain() { | ||
240 | let db = mocked_db(); | ||
241 | let filter = consensus_routes(db); | ||
242 | |||
243 | let res = warp::test::request() | ||
244 | .method("GET") | ||
245 | .path("/block") | ||
246 | .reply(&filter) | ||
247 | .await; | ||
248 | |||
249 | assert_eq!(res.status(), StatusCode::OK); | ||
250 | |||
251 | let expected_json_body = r#"{"transaction_list":["old_transaction_hash_1","old_transaction_hash_2","old_transaction_hash_3"],"nonce":0,"timestamp":"2021-04-08T12:30:30","hash":"not_a_thing_yet"}"#; | ||
252 | assert_eq!(res.body(), expected_json_body); | ||
253 | } | ||
254 | |||
255 | /// Test a simple GET request to a nonexisting path | ||
256 | /// https://tools.ietf.org/html/rfc7231#section-6.5.4 | ||
257 | /// Should respond with 404 and stop | ||
258 | #[tokio::test] | ||
259 | async fn get_nonexisting_path_404() { | ||
260 | let db = mocked_db(); | ||
261 | let filter = consensus_routes(db); | ||
262 | |||
263 | let res = warp::test::request() | ||
264 | .method("GET") | ||
265 | .path("/this_path_does_not_exist") | ||
266 | .reply(&filter) | ||
267 | .await; | ||
268 | |||
269 | assert_eq!(res.status(), StatusCode::NOT_FOUND); | ||
270 | } | ||
271 | |||
272 | /// Test a POST request to /transaction, a resource that exists | ||
273 | /// https://tools.ietf.org/html/rfc7231#section-6.3.2 | ||
274 | /// Should accept the json request, create | ||
275 | /// the transaction and add it to pending transactions in the db | ||
276 | #[tokio::test] | ||
277 | async fn post_json_201() { | ||
278 | let db = mocked_db(); | ||
279 | let filter = consensus_routes(db.clone()); | ||
280 | let res = warp::test::request() | ||
281 | .method("POST") | ||
282 | .json(&mocked_transaction()) | ||
283 | .path("/transaction") | ||
284 | .reply(&filter) | ||
285 | .await; | ||
286 | |||
287 | assert_eq!(res.status(), StatusCode::CREATED); | ||
288 | assert_eq!(db.pending_transactions.read().len(), 2); | ||
289 | } | ||
290 | |||
291 | /// Test a POST request to /transaction, a resource that exists | ||
292 | /// https://tools.ietf.org/html/rfc7231#section-6.3.2 | ||
293 | /// Should accept the json request, create | ||
294 | /// the transaction and add it to pending transactions in the db | ||
295 | #[tokio::test] | ||
296 | async fn post_auth_json_201() { | ||
297 | let db = mocked_db(); | ||
298 | let filter = consensus_routes(db.clone()); | ||
299 | |||
300 | let res = warp::test::request() | ||
301 | .method("POST") | ||
302 | .json(&mocked_transaction()) | ||
303 | .header("Authorization", format!("Bearer {}", &mocked_jwt())) | ||
304 | .path("/transaction") | ||
305 | .reply(&filter) | ||
306 | .await; | ||
307 | |||
308 | assert_eq!(res.status(), StatusCode::CREATED); | ||
309 | assert_eq!(db.pending_transactions.read().len(), 2); | ||
310 | } | ||
311 | |||
312 | /// Test a POST request to /transaction, a resource that exists | ||
313 | /// https://tools.ietf.org/html/rfc7231#section-6.3.2 | ||
314 | /// Should accept the json request, create | ||
315 | /// the transaction and add it to pending transactions in the db | ||
316 | #[tokio::test] | ||
317 | async fn post_auth_json_400() { | ||
318 | let db = mocked_db(); | ||
319 | let filter = consensus_routes(db.clone()); | ||
320 | |||
321 | let res = warp::test::request() | ||
322 | .method("POST") | ||
323 | .json(&mocked_transaction()) | ||
324 | .header("Authorization", "Bearer aaaaaaaasdlkjaldkasljdaskjlaaaaaaaaaaaaaa") | ||
325 | .path("/transaction") | ||
326 | .reply(&filter) | ||
327 | .await; | ||
328 | |||
329 | assert_eq!(res.status(), StatusCode::BAD_REQUEST); | ||
330 | assert_eq!(db.pending_transactions.read().len(), 1); | ||
331 | } | ||
332 | |||
333 | /// Test a POST request to /block, a resource that exists | ||
334 | /// https://tools.ietf.org/html/rfc7231#section-6.3.2 | ||
335 | /// Should accept the json request, create | ||
336 | /// the block | ||
337 | #[tokio::test] | ||
338 | async fn post_block_201() { | ||
339 | let db = mocked_db(); | ||
340 | let filter = consensus_routes(db.clone()); | ||
341 | |||
342 | let res = warp::test::request() | ||
343 | .method("POST") | ||
344 | .json(&mocked_block()) | ||
345 | .path("/block") | ||
346 | .reply(&filter) | ||
347 | .await; | ||
348 | |||
349 | assert_eq!(res.status(), StatusCode::CREATED); | ||
350 | assert_eq!( | ||
351 | *db.blockchain.read().hash, | ||
352 | "2b648ffab5d9af1d5d5fc052fc9e51b882fc4fb0c998608c99232f9282000000".to_owned() | ||
353 | ); | ||
354 | } | ||
355 | |||
356 | /// Test a POST request to /block, a resource that exists | ||
357 | /// https://tools.ietf.org/html/rfc7231#section-6.3.2 | ||
358 | /// Should reject the block because of the wrong hash | ||
359 | #[tokio::test] | ||
360 | async fn post_block_wrong_hash() { | ||
361 | let db = mocked_db(); | ||
362 | let filter = consensus_routes(db.clone()); | ||
363 | |||
364 | let res = warp::test::request() | ||
365 | .method("POST") | ||
366 | .json(&mocked_wrong_block()) | ||
367 | .path("/block") | ||
368 | .reply(&filter) | ||
369 | .await; | ||
370 | |||
371 | assert_eq!(res.status(), StatusCode::BAD_REQUEST); | ||
372 | } | ||
373 | |||
374 | /// Test a POST request to /register, an endpoint that exists | ||
375 | /// https://tools.ietf.org/html/rfc7231#section-6.3.2 | ||
376 | /// Should accept the json request, create a new user and | ||
377 | /// add it to the user hashmap in the db | ||
378 | #[tokio::test] | ||
379 | async fn post_register_priviliged_user() { | ||
380 | let db = mocked_db(); | ||
381 | let filter = consensus_routes(db.clone()); | ||
382 | |||
383 | let res = warp::test::request() | ||
384 | .method("POST") | ||
385 | .json(&priviliged_mocked_user()) | ||
386 | .path("/register") | ||
387 | .reply(&filter) | ||
388 | .await; | ||
389 | |||
390 | println!("{:?}", res.body()); | ||
391 | assert_eq!(res.status(), StatusCode::CREATED); | ||
392 | assert_eq!(db.users.read().len(), 2); | ||
393 | } | ||
394 | |||
395 | /// Test a POST request to /transaction, an endpoint that exists | ||
396 | /// https://tools.ietf.org/html/rfc7231#section-6.3.2 | ||
397 | /// Should NOT accept the json request as the user is unpriviliged | ||
398 | #[tokio::test] | ||
399 | async fn post_register_unpriviliged_user() { | ||
400 | let db = mocked_db(); | ||
401 | let filter = consensus_routes(db.clone()); | ||
402 | |||
403 | let res = warp::test::request() | ||
404 | .method("POST") | ||
405 | .json(&unpriviliged_mocked_user()) | ||
406 | .path("/register") | ||
407 | .reply(&filter) | ||
408 | .await; | ||
409 | |||
410 | println!("{:?}", res.body()); | ||
411 | assert_eq!(res.status(), StatusCode::BAD_REQUEST); | ||
412 | assert_eq!(db.users.read().len(), 1); | ||
413 | } | ||
414 | |||
415 | /// Test a POST request to /transaction, a resource that exists with a longer than expected | ||
416 | /// payload | ||
417 | /// https://tools.ietf.org/html/rfc7231#section-6.5.11 | ||
418 | /// Should return 413 to user | ||
419 | #[tokio::test] | ||
420 | async fn post_too_long_content_413() { | ||
421 | let db = mocked_db(); | ||
422 | let filter = consensus_routes(db); | ||
423 | |||
424 | let res = warp::test::request() | ||
425 | .method("POST") | ||
426 | .header("content-length", 1024 * 36) | ||
427 | .path("/transaction") | ||
428 | .reply(&filter) | ||
429 | .await; | ||
430 | |||
431 | assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE); | ||
432 | } | ||
433 | } | ||
434 | |||
435 | // TODO: POST block without correct transactions test <09-04-21, yigit> // | ||
436 | // TODO: POST transaction while that source has pending transaction test <09-04-21, yigit> // | ||
diff --git a/src/schema.rs b/src/schema.rs index 39921b8..4eb2f0a 100644 --- a/src/schema.rs +++ b/src/schema.rs | |||
@@ -1,3 +1,12 @@ | |||
1 | //! # Data Representations | ||
2 | //! | ||
3 | //! We need persistence for [`Block`]s and [`User`]s, not so much for [`Transaction`]s | ||
4 | //! | ||
5 | //! There are around 30 students, a full fledged database would be an overkill (for next year?) | ||
6 | //! | ||
7 | //! Pending transactions are held in memory, these are cleared with every new block | ||
8 | //! Only the last block is held in memory, every block is written to a file | ||
9 | //! Users are held in memory and they're also backed up to text files | ||
1 | use chrono::{NaiveDate, NaiveDateTime}; | 10 | use chrono::{NaiveDate, NaiveDateTime}; |
2 | use lazy_static::lazy_static; | 11 | use lazy_static::lazy_static; |
3 | use parking_lot::RwLock; | 12 | use parking_lot::RwLock; |
@@ -9,24 +18,45 @@ use std::sync::Arc; | |||
9 | 18 | ||
10 | // use crate::validators; | 19 | // use crate::validators; |
11 | 20 | ||
12 | /// We need persistence for blocks and users, not so much for transactions | 21 | pub type PublicKeySignature = String; |
13 | /// There are around 30 students, a full fledged database would be an overkill (for next year?) | ||
14 | /// Pending transactions are held in memory, these are cleared with every new block | ||
15 | /// Only the last block is held in memory, every block is written to a file | ||
16 | /// Users are held in memory and they're also backed up to text files | ||
17 | 22 | ||
18 | /// Creates a new database connection | 23 | /// Creates a new database |
19 | pub fn create_database() -> Db { | 24 | pub fn create_database() -> Db { |
20 | fs::create_dir_all("blocks").unwrap(); | 25 | fs::create_dir_all("blocks").unwrap(); |
21 | fs::create_dir_all("users").unwrap(); | 26 | fs::create_dir_all("users").unwrap(); |
22 | Db::new() | 27 | Db::new() |
23 | } | 28 | } |
24 | 29 | ||
30 | /// A JWT Payload/Claims representation | ||
31 | /// | ||
32 | /// https://tools.ietf.org/html/rfc7519#section-4.1 | ||
33 | /// | ||
34 | /// - `tha`: Transaction Hash, String (custom field) | ||
35 | /// - `iat`: Issued At, Unix Time, epoch | ||
36 | /// - `exp`: Expiration Time, epoch | ||
37 | #[derive(Debug, Serialize, Deserialize)] | ||
38 | pub struct Claims { | ||
39 | pub tha: String, | ||
40 | pub iat: usize, | ||
41 | pub exp: usize, | ||
42 | } | ||
43 | |||
44 | /// Global Database representation | ||
45 | /// | ||
46 | /// [`blockchain`] is just the last block that was mined. All the blocks are written to disk as text | ||
47 | /// files whenever they are accepted. | ||
48 | /// | ||
49 | /// [`pending_transactions`] is the in memory representation of the waiting transactions. Every | ||
50 | /// user can have only one outstanding transaction at any given time. | ||
51 | /// | ||
52 | /// [`users`] is the in memory representation of the users, with their public keys, metu_ids and | ||
53 | /// gradecoin balances. | ||
54 | /// | ||
55 | /// TODO: Replace the pending_transactions HashMap<String, Transaction> with | ||
56 | /// HashMap<PublicKeySignature, Transaction> | ||
25 | #[derive(Debug, Clone)] | 57 | #[derive(Debug, Clone)] |
26 | pub struct Db { | 58 | pub struct Db { |
27 | // heh. also https://doc.rust-lang.org/std/collections/struct.LinkedList.html says Vec is generally faster | ||
28 | pub blockchain: Arc<RwLock<Block>>, | 59 | pub blockchain: Arc<RwLock<Block>>, |
29 | // every proposer can have _one_ pending transaction, a way to enforce this, String is proposer identifier | ||
30 | pub pending_transactions: Arc<RwLock<HashMap<String, Transaction>>>, | 60 | pub pending_transactions: Arc<RwLock<HashMap<String, Transaction>>>, |
31 | pub users: Arc<RwLock<HashMap<String, User>>>, | 61 | pub users: Arc<RwLock<HashMap<String, User>>>, |
32 | } | 62 | } |
@@ -41,8 +71,6 @@ impl Db { | |||
41 | } | 71 | } |
42 | } | 72 | } |
43 | 73 | ||
44 | pub type PublicKeySignature = String; | ||
45 | |||
46 | /// A transaction between `source` and `target` that moves `amount` | 74 | /// A transaction between `source` and `target` that moves `amount` |
47 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] | 75 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] |
48 | pub struct Transaction { | 76 | pub struct Transaction { |
@@ -53,20 +81,21 @@ pub struct Transaction { | |||
53 | pub timestamp: NaiveDateTime, | 81 | pub timestamp: NaiveDateTime, |
54 | } | 82 | } |
55 | 83 | ||
56 | /// A block that was proposed with `transaction_list` and `nonce` that made `hash` valid | 84 | /// A block that was proposed with `transaction_list` and `nonce` that made `hash` valid, 6 zeroes |
85 | /// at the right hand side of the hash (24 bytes) | ||
86 | /// | ||
87 | /// We are mining using blake2s algorithm, which produces 256 bit hashes. Hash/second is roughly | ||
88 | /// 20x10^3. | ||
89 | /// | ||
57 | /// https://serde.rs/container-attrs.html might be valuable to normalize the serialize/deserialize | 90 | /// https://serde.rs/container-attrs.html might be valuable to normalize the serialize/deserialize |
58 | /// conventions as these will be hashed | 91 | /// conventions as these will be hashed |
92 | /// | ||
59 | #[derive(Serialize, Deserialize, Debug)] | 93 | #[derive(Serialize, Deserialize, Debug)] |
60 | pub struct Block { | 94 | pub struct Block { |
61 | // TODO: transaction list should hold transaction hash values <09-04-21, yigit> // | 95 | pub transaction_list: Vec<PublicKeySignature>, |
62 | // but do we link them somehow? (like a log of old transactions?) | ||
63 | // we can leave this as is and whenever we have a new block we _could_ just log it to file | ||
64 | // somewhere | ||
65 | // I want to keep this as a String vector because it makes things easier elsewhere | ||
66 | pub transaction_list: Vec<PublicKeySignature>, // hashes of the transactions (or just "source" for now) | ||
67 | pub nonce: u32, | 96 | pub nonce: u32, |
68 | pub timestamp: NaiveDateTime, | 97 | pub timestamp: NaiveDateTime, |
69 | pub hash: String, // future proof'd baby | 98 | pub hash: String, |
70 | } | 99 | } |
71 | 100 | ||
72 | /// For prototyping and letting serde handle everything json | 101 | /// For prototyping and letting serde handle everything json |
@@ -89,7 +118,7 @@ impl Block { | |||
89 | } | 118 | } |
90 | } | 119 | } |
91 | 120 | ||
92 | /// Or simply a Student | 121 | /// Simply a Student |
93 | #[derive(Serialize, Deserialize, Debug)] | 122 | #[derive(Serialize, Deserialize, Debug)] |
94 | pub struct User { | 123 | pub struct User { |
95 | pub user_id: MetuId, | 124 | pub user_id: MetuId, |
@@ -103,6 +132,7 @@ pub struct MetuId { | |||
103 | id: String, | 132 | id: String, |
104 | } | 133 | } |
105 | 134 | ||
135 | // TODO: this will arrive encrypted <13-04-21, yigit> // | ||
106 | #[derive(Serialize, Deserialize, Debug)] | 136 | #[derive(Serialize, Deserialize, Debug)] |
107 | pub struct AuthRequest { | 137 | pub struct AuthRequest { |
108 | pub student_id: String, | 138 | pub student_id: String, |
@@ -13,7 +13,7 @@ curl --request POST \ | |||
13 | http://localhost:8080/register | 13 | http://localhost:8080/register |
14 | 14 | ||
15 | ## new registration request | 15 | ## new registration request |
16 | ## should fail because foobar is not a student | 16 | ## this student can hold a gradecoin account |
17 | curl --request POST \ | 17 | curl --request POST \ |
18 | --header 'Content-Type: application/json' \ | 18 | --header 'Content-Type: application/json' \ |
19 | --data '{ | 19 | --data '{ |
diff --git a/tests/route_tests.rs b/tests/route_tests.rs new file mode 100644 index 0000000..ba3ecf3 --- /dev/null +++ b/tests/route_tests.rs | |||
@@ -0,0 +1,371 @@ | |||
1 | #[cfg(test)] | ||
2 | mod tests { | ||
3 | use gradecoin::schema::{ | ||
4 | create_database, AuthRequest, Block, Claims, Db, MetuId, Transaction, User, | ||
5 | }; | ||
6 | |||
7 | use gradecoin::routes::consensus_routes; | ||
8 | use warp::http::StatusCode; | ||
9 | |||
10 | use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; | ||
11 | const PRIVATE_KEY_PEM: &str = "-----BEGIN RSA PRIVATE KEY----- | ||
12 | MIIEpAIBAAKCAQEA4nU0G4WjkmcQUx0hq6LQuV5Q+ACmUFL/OjoYMDwC/O/6pCd1 | ||
13 | UZgCfgHN2xEffDPznzcTn8OiFRxr4oWyBinyrUpnY4mhy0SQUwoeCw7YkcHAyhCj | ||
14 | NT74aR/ohX0MCj0qRRdbt5ZQXM/GC3HJuXE1ptSuhFgQxziItamn8maoJ6JUSVEX | ||
15 | VO1NOrrjoM3r7Q+BK2B+sX4/bLZ+VG5g1q2nEbFdTHS6pHqtZNHQndTmEKwRfh0R | ||
16 | YtzEzOXuO6e1gQY42Tujkof40dhGCIU7TeIGGHwdFxy1niLkXwtHNjV7lnIOkTbx | ||
17 | 6+sSPamRfQAlZqUWM2Lf5o+7h3qWP3ENB138sQIDAQABAoIBAD23nYTmrganag6M | ||
18 | wPFrBSGP79c3Lhx0EjUHQjJbGKFgsdltG48qM3ut+DF9ACy0Z+/7bbC7+39vaIOq | ||
19 | 1jLR2d6aiYTaLKseO4s2FawD1sgamvU3BZPsXn0gAhnnU5Gyy8Nas1dccvhoc9wI | ||
20 | neaZUPrvucQ90AzLfo6r9yacDbYHB1lOyomApUvpJxOgHISGEtc9qGPDrdH19aF0 | ||
21 | 8fCv2bbQRh+TChgN3IB0o5w0wXaI7YAyAouAv/AzHCoEMpt7OGjFTkjh/ujlPL9O | ||
22 | +FLuJNsQRHDN0gJo2pcvwGwDCsioMixQ9bZ7ZrUu2BNpEQygyeSbj9ZI1iRvhosO | ||
23 | JU3rwEECgYEA9MppTYA6A9WQbCCwPH1QMpUAmPNVSWVhUVag4lGOEhdCDRcz9ook | ||
24 | DohQMKctiEB1luKuvDokxo0uMOfMO9/YwjsRB7qjQip7Th1zMJIjD+A+juLzHK4r | ||
25 | /RiRtWYGAnF8mptDvE+93JsPb3C/lQLvIhio5GQYWBqPJu6SpeosIskCgYEA7NPi | ||
26 | Gbffzr2UQhW8BNKmctEEh8yFRVojFo3wwwWxSNUVXGSmSm31CL+Q8h817R+2OkPV | ||
27 | 1ZMUOBU4UJiqFt28kIvTDFqbAJlJQGCpY2mY7OLQiD2A+TVLcFrHmoCaPfCAK1Qd | ||
28 | hQ0PmFK7Mf8qClpA3E5chop/WfKQfiu46sZv1qkCgYAhGdXPcw1lQ1W6KVlrdI6J | ||
29 | qHhiNlVMDXdxZkNvFxQdAiQeXQrbxaZGiMw/J/wSNpUwCAsUzM/4QVMDrfSCDCzl | ||
30 | ZtNQtj4pTlFKKNVQthIjrXEIJUw2jp7IJLBfVSJu5iWxSlmId0f3MsiNizN81N69 | ||
31 | P5Rm/doE3+KHoy8VXGsHcQKBgQCkNh62enqjHWypjex6450qS6f6iWN3PRLLVsw0 | ||
32 | TcQpniZblCaBwVCAKmRUnjOEIdL2/4ZLutnwMTaFG/YEOOfAylMiY8jKV38lNmD9 | ||
33 | X4D78CFr9klxgvS2CRwSE03f2NzmLkLxuKaxldvaxPTfjMkgeO1LFMlNExYBhkuH | ||
34 | 7uQpUQKBgQCKX6qMNh2gSdgG7qyxfTFZ4y5EGOBoKe/dE+IcVF3Vnh6DZVbCAbBL | ||
35 | 5EdFWZSrCnDjA4xiKW55mwp95Ud9EZsZAb13L8V9t82eK+UDBoWlb7VRNYpda/x1 | ||
36 | 5/i4qQJ28x2UNJDStpYFpnp4Ba1lvXjKngIbDPkjU+hbBJ+BNGAIeg== | ||
37 | -----END RSA PRIVATE KEY-----"; | ||
38 | |||
39 | /// Create a mock database to be used in tests | ||
40 | fn mocked_db() -> Db { | ||
41 | let db = create_database(); | ||
42 | |||
43 | db.users.write().insert( | ||
44 | "mock_transaction_source".to_owned(), | ||
45 | User { | ||
46 | user_id: MetuId::new("e254275".to_owned()).unwrap(), | ||
47 | public_key: "-----BEGIN PUBLIC KEY----- | ||
48 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4nU0G4WjkmcQUx0hq6LQ | ||
49 | uV5Q+ACmUFL/OjoYMDwC/O/6pCd1UZgCfgHN2xEffDPznzcTn8OiFRxr4oWyBiny | ||
50 | rUpnY4mhy0SQUwoeCw7YkcHAyhCjNT74aR/ohX0MCj0qRRdbt5ZQXM/GC3HJuXE1 | ||
51 | ptSuhFgQxziItamn8maoJ6JUSVEXVO1NOrrjoM3r7Q+BK2B+sX4/bLZ+VG5g1q2n | ||
52 | EbFdTHS6pHqtZNHQndTmEKwRfh0RYtzEzOXuO6e1gQY42Tujkof40dhGCIU7TeIG | ||
53 | GHwdFxy1niLkXwtHNjV7lnIOkTbx6+sSPamRfQAlZqUWM2Lf5o+7h3qWP3ENB138 | ||
54 | sQIDAQAB | ||
55 | -----END PUBLIC KEY-----" | ||
56 | .to_owned(), | ||
57 | balance: 0, | ||
58 | }, | ||
59 | ); | ||
60 | |||
61 | db.pending_transactions.write().insert( | ||
62 | "hash_value".to_owned(), | ||
63 | Transaction { | ||
64 | by: "source_account".to_owned(), | ||
65 | source: "source_account".to_owned(), | ||
66 | target: "target_account".to_owned(), | ||
67 | amount: 20, | ||
68 | timestamp: chrono::NaiveDate::from_ymd(2021, 04, 09).and_hms(1, 30, 30), | ||
69 | }, | ||
70 | ); | ||
71 | |||
72 | *db.blockchain.write() = Block { | ||
73 | transaction_list: vec![ | ||
74 | "old_transaction_hash_1".to_owned(), | ||
75 | "old_transaction_hash_2".to_owned(), | ||
76 | "old_transaction_hash_3".to_owned(), | ||
77 | ], | ||
78 | nonce: 0, | ||
79 | timestamp: chrono::NaiveDate::from_ymd(2021, 04, 08).and_hms(12, 30, 30), | ||
80 | hash: "not_a_thing_yet".to_owned(), | ||
81 | }; | ||
82 | |||
83 | db | ||
84 | } | ||
85 | |||
86 | fn mocked_jwt() -> String { | ||
87 | let claims = Claims { | ||
88 | tha: "6692e774eba7fb92dc0fe6cf7347591e".to_owned(), | ||
89 | iat: 1618275851, | ||
90 | exp: 1648275851, | ||
91 | }; | ||
92 | let header = Header::new(Algorithm::RS256); | ||
93 | encode( | ||
94 | &header, | ||
95 | &claims, | ||
96 | &EncodingKey::from_rsa_pem(PRIVATE_KEY_PEM.as_bytes()).unwrap(), | ||
97 | ) | ||
98 | .unwrap() | ||
99 | } | ||
100 | |||
101 | /// Create a mock user that is allowed to be in gradecoin to be used in tests | ||
102 | fn priviliged_mocked_user() -> AuthRequest { | ||
103 | AuthRequest { | ||
104 | student_id: String::from("e254275"), | ||
105 | public_key: "NOT IMPLEMENTED".to_owned(), | ||
106 | } | ||
107 | } | ||
108 | |||
109 | /// Create a mock user that is NOT allowed to be in gradecoin to be used in tests | ||
110 | fn unpriviliged_mocked_user() -> AuthRequest { | ||
111 | AuthRequest { | ||
112 | student_id: String::from("foobarbaz"), | ||
113 | public_key: "NOT IMPLEMENTED".to_owned(), | ||
114 | } | ||
115 | } | ||
116 | |||
117 | /// Create a mock transaction to be used in tests | ||
118 | fn mocked_transaction() -> Transaction { | ||
119 | Transaction { | ||
120 | by: "mock_transaction_source".to_owned(), | ||
121 | source: "mock_transaction_source".to_owned(), | ||
122 | target: "mock_transaction_target".to_owned(), | ||
123 | amount: 25, | ||
124 | timestamp: chrono::NaiveDate::from_ymd(2021, 04, 09).and_hms(14, 30, 00), | ||
125 | } | ||
126 | } | ||
127 | |||
128 | /// Create a mock block with a correct mined hash to be used in tests | ||
129 | fn mocked_block() -> Block { | ||
130 | Block { | ||
131 | transaction_list: vec!["hash_value".to_owned()], | ||
132 | nonce: 3831993, | ||
133 | timestamp: chrono::NaiveDate::from_ymd(2021, 04, 08).and_hms(12, 30, 30), | ||
134 | hash: "2b648ffab5d9af1d5d5fc052fc9e51b882fc4fb0c998608c99232f9282000000".to_owned(), | ||
135 | } | ||
136 | } | ||
137 | |||
138 | /// Create a mock block with a wrong hash and nonce | ||
139 | fn mocked_wrong_block() -> Block { | ||
140 | Block { | ||
141 | transaction_list: vec!["foobarbaz".to_owned(), "dazsaz".to_owned()], | ||
142 | nonce: 1000, // can you imagine | ||
143 | timestamp: chrono::NaiveDate::from_ymd(2021, 04, 12).and_hms(05, 29, 30), | ||
144 | hash: "tnarstnarsuthnarsthlarjstk".to_owned(), | ||
145 | } | ||
146 | } | ||
147 | |||
148 | /// Test simple GET request to /transaction, an endpoint that exists | ||
149 | /// https://tools.ietf.org/html/rfc7231#section-6.3.1 | ||
150 | /// We should get the only pending transaction available in the database as json | ||
151 | #[tokio::test] | ||
152 | async fn get_pending_transactions() { | ||
153 | let db = mocked_db(); | ||
154 | |||
155 | let reply = consensus_routes(db); | ||
156 | |||
157 | let res = warp::test::request() | ||
158 | .method("GET") | ||
159 | .path("/transaction") | ||
160 | .reply(&reply) | ||
161 | .await; | ||
162 | |||
163 | assert_eq!(res.status(), StatusCode::OK); | ||
164 | |||
165 | let expected_json_body = r#"[{"by":"source_account","source":"source_account","target":"target_account","amount":20,"timestamp":"2021-04-09T01:30:30"}]"#; | ||
166 | |||
167 | assert_eq!(res.body(), expected_json_body); | ||
168 | } | ||
169 | |||
170 | /// Test simple GET request to /block, an enpoint that exists | ||
171 | /// | ||
172 | /// https://tools.ietf.org/html/rfc7231#section-6.3.1 | ||
173 | /// | ||
174 | /// Should return the single block available in the database as json | ||
175 | #[tokio::test] | ||
176 | async fn get_blockchain() { | ||
177 | let db = mocked_db(); | ||
178 | let filter = consensus_routes(db); | ||
179 | |||
180 | let res = warp::test::request() | ||
181 | .method("GET") | ||
182 | .path("/block") | ||
183 | .reply(&filter) | ||
184 | .await; | ||
185 | |||
186 | assert_eq!(res.status(), StatusCode::OK); | ||
187 | |||
188 | let expected_json_body = r#"{"transaction_list":["old_transaction_hash_1","old_transaction_hash_2","old_transaction_hash_3"],"nonce":0,"timestamp":"2021-04-08T12:30:30","hash":"not_a_thing_yet"}"#; | ||
189 | assert_eq!(res.body(), expected_json_body); | ||
190 | } | ||
191 | |||
192 | /// Test a simple GET request to a nonexisting path | ||
193 | /// https://tools.ietf.org/html/rfc7231#section-6.5.4 | ||
194 | /// Should respond with 404 and stop | ||
195 | #[tokio::test] | ||
196 | async fn get_nonexisting_path_404() { | ||
197 | let db = mocked_db(); | ||
198 | let filter = consensus_routes(db); | ||
199 | |||
200 | let res = warp::test::request() | ||
201 | .method("GET") | ||
202 | .path("/this_path_does_not_exist") | ||
203 | .reply(&filter) | ||
204 | .await; | ||
205 | |||
206 | assert_eq!(res.status(), StatusCode::NOT_FOUND); | ||
207 | } | ||
208 | |||
209 | /// Test a POST request to /transaction, an endpoint that exists | ||
210 | /// | ||
211 | /// https://tools.ietf.org/html/rfc7231#section-6.3.2 | ||
212 | /// | ||
213 | /// Should accept the json request, create | ||
214 | /// the transaction and add it to pending transactions in the db | ||
215 | #[tokio::test] | ||
216 | async fn post_auth_json_201() { | ||
217 | let db = mocked_db(); | ||
218 | let filter = consensus_routes(db.clone()); | ||
219 | |||
220 | let res = warp::test::request() | ||
221 | .method("POST") | ||
222 | .json(&mocked_transaction()) | ||
223 | .header("Authorization", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aGEiOiI2NjkyZTc3NGViYTdmYjkyZGMwZmU2Y2Y3MzQ3NTkxZSIsImlhdCI6MTYxODI2MDY0MSwiZXhwIjoxNzE4MjYwNjQxfQ.M_FVVE5F_aYcDsprkcqV8n2DAhnM6jImAUEXChI9qYn55meE_0Pmp6AaJlTzclYUT1ZUQfFuehYTYu5UkigQ_AimDhqM5VWxPdnyfTQscV916arbNn4qXW6-3oHGUR93xK7-mX6mxeXyDZLxr1SD_JEvVzGWTU4Xo9SMYSIcaHjROAg_ChxJdD4WLe5T4He7O443jpXdAeeVVYfKoJyBfINx_bxiF58-ni1vur9q6-nrjnMw6sMMbtWD3qvzKZHN7HzfwNXM-90D-9VX1KiaJN05jIxLzCYacLeBUH595I4--XfgpLmqrV_P3Sucmny0yvagbZtjYjswmf0DjR99ug") | ||
224 | .path("/transaction") | ||
225 | .reply(&filter) | ||
226 | .await; | ||
227 | |||
228 | println!("{:?}", res.body()); | ||
229 | assert_eq!(res.status(), StatusCode::CREATED); | ||
230 | assert_eq!(db.pending_transactions.read().len(), 2); | ||
231 | } | ||
232 | |||
233 | /// Test a POST request to /transaction, an endpoint that exists with an incorrect JWT in the | ||
234 | /// Authorization header | ||
235 | /// | ||
236 | /// https://tools.ietf.org/html/rfc7231#section-6.3.2 | ||
237 | /// | ||
238 | /// Should reject the request | ||
239 | #[tokio::test] | ||
240 | async fn post_auth_json_400() { | ||
241 | let db = mocked_db(); | ||
242 | let filter = consensus_routes(db.clone()); | ||
243 | |||
244 | let res = warp::test::request() | ||
245 | .method("POST") | ||
246 | .json(&mocked_transaction()) | ||
247 | .header( | ||
248 | "Authorization", | ||
249 | "Bearer aaaaaaaasdlkjaldkasljdaskjlaaaaaaaaaaaaaa", | ||
250 | ) | ||
251 | .path("/transaction") | ||
252 | .reply(&filter) | ||
253 | .await; | ||
254 | |||
255 | assert_eq!(res.status(), StatusCode::BAD_REQUEST); | ||
256 | assert_eq!(db.pending_transactions.read().len(), 1); | ||
257 | } | ||
258 | |||
259 | /// Test a POST request to /block, an endpoint that exists | ||
260 | /// | ||
261 | /// https://tools.ietf.org/html/rfc7231#section-6.3.2 | ||
262 | /// | ||
263 | /// Should accept the json request, create | ||
264 | /// the block | ||
265 | #[tokio::test] | ||
266 | async fn post_block_201() { | ||
267 | let db = mocked_db(); | ||
268 | let filter = consensus_routes(db.clone()); | ||
269 | |||
270 | let res = warp::test::request() | ||
271 | .method("POST") | ||
272 | .json(&mocked_block()) | ||
273 | .path("/block") | ||
274 | .reply(&filter) | ||
275 | .await; | ||
276 | |||
277 | assert_eq!(res.status(), StatusCode::CREATED); | ||
278 | assert_eq!( | ||
279 | *db.blockchain.read().hash, | ||
280 | "2b648ffab5d9af1d5d5fc052fc9e51b882fc4fb0c998608c99232f9282000000".to_owned() | ||
281 | ); | ||
282 | } | ||
283 | |||
284 | /// Test a POST request to /block, an endpoint that exists | ||
285 | /// | ||
286 | /// https://tools.ietf.org/html/rfc7231#section-6.3.2 | ||
287 | /// | ||
288 | /// Should reject the block because of the wrong hash/nonce | ||
289 | /// // TODO: split this into two tests | ||
290 | #[tokio::test] | ||
291 | async fn post_block_wrong_hash() { | ||
292 | let db = mocked_db(); | ||
293 | let filter = consensus_routes(db.clone()); | ||
294 | |||
295 | let res = warp::test::request() | ||
296 | .method("POST") | ||
297 | .json(&mocked_wrong_block()) | ||
298 | .path("/block") | ||
299 | .reply(&filter) | ||
300 | .await; | ||
301 | |||
302 | assert_eq!(res.status(), StatusCode::BAD_REQUEST); | ||
303 | } | ||
304 | |||
305 | /// Test a POST request to /register, an endpoint that exists | ||
306 | /// | ||
307 | /// https://tools.ietf.org/html/rfc7231#section-6.3.2 | ||
308 | /// | ||
309 | /// Should accept the json request, create a new user and | ||
310 | /// add it to the user hashmap in the db | ||
311 | #[tokio::test] | ||
312 | async fn post_register_priviliged_user() { | ||
313 | let db = mocked_db(); | ||
314 | let filter = consensus_routes(db.clone()); | ||
315 | |||
316 | let res = warp::test::request() | ||
317 | .method("POST") | ||
318 | .json(&priviliged_mocked_user()) | ||
319 | .path("/register") | ||
320 | .reply(&filter) | ||
321 | .await; | ||
322 | |||
323 | println!("{:?}", res.body()); | ||
324 | assert_eq!(res.status(), StatusCode::CREATED); | ||
325 | assert_eq!(db.users.read().len(), 2); | ||
326 | } | ||
327 | |||
328 | /// Test a POST request to /transaction, an endpoint that exists | ||
329 | /// https://tools.ietf.org/html/rfc7231#section-6.3.2 | ||
330 | /// Should NOT accept the json request as the user is unpriviliged | ||
331 | #[tokio::test] | ||
332 | async fn post_register_unpriviliged_user() { | ||
333 | let db = mocked_db(); | ||
334 | let filter = consensus_routes(db.clone()); | ||
335 | |||
336 | let res = warp::test::request() | ||
337 | .method("POST") | ||
338 | .json(&unpriviliged_mocked_user()) | ||
339 | .path("/register") | ||
340 | .reply(&filter) | ||
341 | .await; | ||
342 | |||
343 | println!("{:?}", res.body()); | ||
344 | assert_eq!(res.status(), StatusCode::BAD_REQUEST); | ||
345 | assert_eq!(db.users.read().len(), 1); | ||
346 | } | ||
347 | |||
348 | /// Test a POST request to /transaction, an endpoint that exists with a longer than expected | ||
349 | /// payload | ||
350 | /// | ||
351 | /// https://tools.ietf.org/html/rfc7231#section-6.5.11 | ||
352 | /// | ||
353 | /// Should return 413 to user | ||
354 | #[tokio::test] | ||
355 | async fn post_too_long_content_413() { | ||
356 | let db = mocked_db(); | ||
357 | let filter = consensus_routes(db); | ||
358 | |||
359 | let res = warp::test::request() | ||
360 | .method("POST") | ||
361 | .header("content-length", 1024 * 36) | ||
362 | .path("/transaction") | ||
363 | .reply(&filter) | ||
364 | .await; | ||
365 | |||
366 | assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE); | ||
367 | } | ||
368 | } | ||
369 | |||
370 | // TODO: POST block without correct transactions test <09-04-21, yigit> // | ||
371 | // TODO: POST transaction while that source has pending transaction test <09-04-21, yigit> // | ||