From 8e74e6a6656d3296231a4a1d42c2e208e2f7031e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Gr=C3=A4ff?= Date: Thu, 14 Jan 2021 23:40:48 +0100 Subject: [PATCH] Convert integration tests into examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Examples are still executed during `cargo test`, because test = true has been set in Cargo.toml for each example binary. Signed-off-by: David Gräff --- Cargo.toml | 17 ++ .../create_read_write_document.rs | 205 ++++++++++++------ tests/users.rs => examples/firebase_user.rs | 11 +- .../{own_auth/src/main.rs => own_auth.rs} | 47 +++- examples/own_auth/Cargo.toml | 13 -- examples/own_auth/readme.md | 6 - examples/readme.md | 35 +++ ...main.rs => rocket_http_protected_route.rs} | 11 +- .../rocket_http_protected_route/Cargo.toml | 15 -- .../rocket_http_protected_route/readme.md | 12 - .../rust-toolchain | 1 - {tests => examples}/test_user_id.txt | 0 12 files changed, 244 insertions(+), 129 deletions(-) rename tests/documents.rs => examples/create_read_write_document.rs (55%) rename tests/users.rs => examples/firebase_user.rs (62%) rename examples/{own_auth/src/main.rs => own_auth.rs} (50%) delete mode 100644 examples/own_auth/Cargo.toml delete mode 100644 examples/own_auth/readme.md create mode 100644 examples/readme.md rename examples/{rocket_http_protected_route/src/main.rs => rocket_http_protected_route.rs} (77%) delete mode 100644 examples/rocket_http_protected_route/Cargo.toml delete mode 100644 examples/rocket_http_protected_route/readme.md delete mode 100644 examples/rocket_http_protected_route/rust-toolchain rename {tests => examples}/test_user_id.txt (100%) diff --git a/Cargo.toml b/Cargo.toml index 4b9ddf6..5d01f4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,3 +38,20 @@ native-tls = ["reqwest/native-tls"] native-tls-vendored = ["reqwest/native-tls-vendored"] unstable = [] external_doc = [] + +[[example]] +name = "create_read_write_document" +test = true + +[[example]] +name = "firebase_user" +test = true + +[[example]] +name = "own_auth" +test = true + +[[example]] +name = "rocket_http_protected_route" +test = true +required-features = ["rustls-tls","rocket_support"] diff --git a/tests/documents.rs b/examples/create_read_write_document.rs similarity index 55% rename from tests/documents.rs rename to examples/create_read_write_document.rs index 501446e..7506214 100644 --- a/tests/documents.rs +++ b/examples/create_read_write_document.rs @@ -1,7 +1,10 @@ -use serde::{Deserialize, Serialize}; +use firestore_db_and_auth::{ + documents, dto, errors, sessions, Credentials, FirebaseAuthBearer, JWKSet, ServiceSession, +}; -use firestore_db_and_auth::errors::FirebaseError; -use firestore_db_and_auth::*; +use firestore_db_and_auth::documents::WriteResult; +use firestore_db_and_auth::jwt::download_google_jwks; +use serde::{Deserialize, Serialize}; const TEST_USER_ID: &str = include_str!("test_user_id.txt"); @@ -21,21 +24,7 @@ struct DemoDTOPartial { an_int: u32, } -#[test] -fn service_account_session() -> errors::Result<()> { - use std::path::PathBuf; - let mut credential_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - credential_file.push("firebase-service-account.json"); - - let cred = credentials::Credentials::from_file(credential_file.to_str().unwrap()).expect("Read credentials file"); - cred.verify()?; - - let mut session = ServiceSession::new(cred).unwrap(); - let b = session.access_token().to_owned(); - - // Check if cached value is used - assert_eq!(session.access_token(), b); - +fn write_document(session: &mut ServiceSession, doc_id: &str) -> errors::Result { println!("Write document"); let obj = DemoDTO { @@ -44,20 +33,10 @@ fn service_account_session() -> errors::Result<()> { a_timestamp: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Nanos, true), }; - documents::write( - &mut session, - "tests", - Some("service_test"), - &obj, - documents::WriteOptions::default(), - )?; - - println!("Read and compare document"); - let read: DemoDTO = documents::read(&mut session, "tests", "service_test")?; - - assert_eq!(read.a_string, "abcd"); - assert_eq!(read.an_int, 14); + documents::write(session, "tests", Some(doc_id), &obj, documents::WriteOptions::default()) +} +fn write_partial_document(session: &mut ServiceSession, doc_id: &str) -> errors::Result { println!("Partial write document"); let obj = DemoDTOPartial { @@ -66,15 +45,45 @@ fn service_account_session() -> errors::Result<()> { }; documents::write( - &mut session, + session, "tests", - Some("service_test"), + Some(doc_id), &obj, documents::WriteOptions { merge: true }, - )?; + ) +} +fn check_write(result: WriteResult, doc_id: &str) { + assert_eq!(result.document_id, doc_id); + let duration = chrono::Utc::now().signed_duration_since(result.update_time.unwrap()); + assert!( + duration.num_seconds() < 60, + "now = {}, updated: {}, created: {}", + chrono::Utc::now(), + result.update_time.unwrap(), + result.create_time.unwrap() + ); +} + +fn service_account_session(cred: Credentials) -> errors::Result<()> { + let mut session = ServiceSession::new(cred).unwrap(); + let b = session.access_token().to_owned(); + + let doc_id = "service_test"; + check_write(write_document(&mut session, doc_id)?, doc_id); + + // Check if cached value is used + assert_eq!(session.access_token(), b); + + println!("Read and compare document"); + let read: DemoDTO = documents::read(&mut session, "tests", doc_id)?; + + assert_eq!(read.a_string, "abcd"); + assert_eq!(read.an_int, 14); + + check_write(write_partial_document(&mut session, doc_id)?, doc_id); println!("Read and compare document"); - let read: DemoDTOPartial = documents::read(&mut session, "tests", "service_test")?; + let read: DemoDTOPartial = documents::read(&mut session, "tests", doc_id)?; // Should be updated assert_eq!(read.an_int, 16); @@ -84,14 +93,7 @@ fn service_account_session() -> errors::Result<()> { Ok(()) } -#[test] -fn user_account_session() -> errors::Result<()> { - use std::path::PathBuf; - let mut credential_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - credential_file.push("firebase-service-account.json"); - - let cred = credentials::Credentials::from_file(credential_file.to_str().unwrap()).expect("Read credentials file"); - +fn user_session_with_cached_refresh_token(cred: &Credentials) -> errors::Result { println!("Refresh token from file"); // Read refresh token from file if possible instead of generating a new refresh token each time let refresh_token: String = match std::fs::read_to_string("refresh-token-for-tests.txt") { @@ -115,6 +117,12 @@ fn user_account_session() -> errors::Result<()> { sessions::user::Session::by_refresh_token(&cred, &refresh_token)? }; + Ok(user_session) +} + +fn user_account_session(cred: Credentials) -> errors::Result<()> { + let user_session = user_session_with_cached_refresh_token(&cred)?; + assert_eq!(user_session.user_id, TEST_USER_ID); assert_eq!(user_session.project_id(), cred.project_id); @@ -131,26 +139,21 @@ fn user_account_session() -> errors::Result<()> { // Test writing println!("user::Session documents::write"); - let result = documents::write( - &user_session, - "tests", - Some("test"), - &obj, - documents::WriteOptions::default(), - )?; - assert_eq!(result.document_id, "test"); - let duration = chrono::Utc::now().signed_duration_since(result.update_time.unwrap()); - assert!( - duration.num_seconds() < 60, - "now = {}, updated: {}, created: {}", - chrono::Utc::now(), - result.update_time.unwrap(), - result.create_time.unwrap() + let doc_id = "user_doc"; + check_write( + documents::write( + &user_session, + "tests", + Some(doc_id), + &obj, + documents::WriteOptions::default(), + )?, + doc_id, ); // Test reading println!("user::Session documents::read"); - let read: DemoDTO = documents::read(&user_session, "tests", "test")?; + let read: DemoDTO = documents::read(&user_session, "tests", doc_id)?; assert_eq!(read.a_string, "abc"); assert_eq!(read.an_int, 12); @@ -180,7 +183,7 @@ fn user_account_session() -> errors::Result<()> { let r = documents::delete(&user_session, "tests/non_existing", true); assert!(r.is_err()); match r.err().unwrap() { - FirebaseError::APIError(code, message, context) => { + errors::FirebaseError::APIError(code, message, context) => { assert_eq!(code, 404); assert!(message.contains("No document to update")); assert_eq!(context, "tests/non_existing"); @@ -188,7 +191,7 @@ fn user_account_session() -> errors::Result<()> { _ => panic!("Expected an APIError"), }; - documents::delete(&user_session, "tests/test", false)?; + documents::delete(&user_session, &("tests/".to_owned() + doc_id), false)?; // Check if document is indeed removed println!("user::Session documents::query"); @@ -209,3 +212,83 @@ fn user_account_session() -> errors::Result<()> { Ok(()) } + +/// Download the two public key JWKS files if necessary and cache the content at the given file path. +/// Only use this option in cloud functions if the given file path is persistent. +/// You can use [`Credentials::add_jwks_public_keys`] to manually add more public keys later on. +pub fn from_cache_file(cache_file: &std::path::Path, c: &Credentials) -> errors::Result { + use std::fs::File; + use std::io::BufReader; + + Ok(if cache_file.exists() { + let f = BufReader::new(File::open(cache_file)?); + let jwks_set: JWKSet = serde_json::from_reader(f)?; + jwks_set + } else { + // If not present, download the two jwks (specific service account + google system account), + // merge them into one set of keys and store them in the cache file. + let mut jwks = JWKSet::new(&download_google_jwks(&c.client_email)?)?; + jwks.keys + .append(&mut JWKSet::new(&download_google_jwks("securetoken@system.gserviceaccount.com")?)?.keys); + let f = File::create(cache_file)?; + serde_json::to_writer_pretty(f, &jwks)?; + jwks + }) +} + +fn main() -> errors::Result<()> { + // Search for a credentials file in the root directory + use std::path::PathBuf; + let mut credential_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + credential_file.push("firebase-service-account.json"); + let mut cred = Credentials::from_file(credential_file.to_str().unwrap())?; + + // Only download the public keys once, and cache them. + let jwkset = from_cache_file(credential_file.with_file_name("cached_jwks.jwks").as_path(), &cred)?; + cred.add_jwks_public_keys(&jwkset); + cred.verify()?; + + // Perform some db operations via a service account session + service_account_session(cred.clone())?; + + // Perform some db operations via a firebase user session + user_account_session(cred)?; + + Ok(()) +} + +/// For integration tests and doc code snippets: Create a Credentials instance. +/// Necessary public jwk sets are downloaded or re-used if already present. +#[cfg(test)] +fn valid_test_credentials() -> errors::Result { + use std::path::PathBuf; + let mut jwks_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + jwks_path.push("firebase-service-account.jwks"); + + let mut cred: Credentials = Credentials::new(include_str!("../firebase-service-account.json"))?; + + // Only download the public keys once, and cache them. + let jwkset = from_cache_file(jwks_path.as_path(), &cred)?; + cred.add_jwks_public_keys(&jwkset); + cred.verify()?; + + Ok(cred) +} + +#[test] +fn valid_test_credentials_test() -> errors::Result<()> { + valid_test_credentials()?; + Ok(()) +} + +#[test] +fn service_account_session_test() -> errors::Result<()> { + service_account_session(valid_test_credentials()?)?; + Ok(()) +} + +#[test] +fn user_account_session_test() -> errors::Result<()> { + user_account_session(valid_test_credentials()?)?; + Ok(()) +} diff --git a/tests/users.rs b/examples/firebase_user.rs similarity index 62% rename from tests/users.rs rename to examples/firebase_user.rs index 62ee386..65e7ccb 100644 --- a/tests/users.rs +++ b/examples/firebase_user.rs @@ -1,10 +1,10 @@ +use firestore_db_and_auth::Credentials; use firestore_db_and_auth::*; const TEST_USER_ID: &str = include_str!("test_user_id.txt"); -#[test] -fn user_info() -> errors::Result<()> { - let cred = credentials::Credentials::from_file("firebase-service-account.json").expect("Read credentials file"); +fn main() -> errors::Result<()> { + let cred = Credentials::from_file("firebase-service-account.json").expect("Read credentials file"); let user_session = UserSession::by_user_id(&cred, TEST_USER_ID, false)?; @@ -14,3 +14,8 @@ fn user_info() -> errors::Result<()> { Ok(()) } + +#[test] +fn firebase_user_test() { + main().unwrap(); +} diff --git a/examples/own_auth/src/main.rs b/examples/own_auth.rs similarity index 50% rename from examples/own_auth/src/main.rs rename to examples/own_auth.rs index 3b84018..7099f88 100644 --- a/examples/own_auth/src/main.rs +++ b/examples/own_auth.rs @@ -1,9 +1,11 @@ -use firestore_db_and_auth::{Credentials, FirebaseAuthBearer, documents}; +use firestore_db_and_auth::errors::FirebaseError::APIError; +use firestore_db_and_auth::{documents, errors, Credentials, FirebaseAuthBearer}; /// Define your own structure that will implement the FirebaseAuthBearer trait struct MyOwnSession { /// The google credentials pub credentials: Credentials, + pub blocking_client: reqwest::blocking::Client, pub client: reqwest::Client, access_token: String, } @@ -23,27 +25,48 @@ impl FirebaseAuthBearer for MyOwnSession { } /// The reqwest http client. /// The `Client` holds a connection pool internally, so it is advised that it is reused for multiple, successive connections. - fn client(&self) -> &reqwest::Client { + fn client(&self) -> &reqwest::blocking::Client { + &self.blocking_client + } + + fn client_async(&self) -> &reqwest::Client { &self.client } } -fn main() { - let credentials = Credentials::from_file("firebase-service-account.json").unwrap(); +fn main() -> errors::Result<()> { + let credentials = Credentials::from_file("firebase-service-account.json")?; #[derive(serde::Serialize)] struct TestData { - an_int: u32 - }; - let t = TestData { - an_int: 12 - }; - + an_int: u32, + } + let t = TestData { an_int: 12 }; + let session = MyOwnSession { credentials, + blocking_client: reqwest::blocking::Client::new(), client: reqwest::Client::new(), - access_token: "The access token".to_owned() + access_token: "The access token".to_owned(), }; // Use any of the document functions with your own session object - documents::write(&session, "tests", Some("test_doc"), &t, documents::WriteOptions::default()).unwrap(); + documents::write( + &session, + "tests", + Some("test_doc"), + &t, + documents::WriteOptions::default(), + )?; + Ok(()) +} + +#[test] +fn own_auth_test() { + if let Err(APIError(code, str_code, context)) = main() { + assert_eq!(str_code, "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project."); + assert_eq!(context, "test_doc"); + assert_eq!(code, 401); + return; + } + panic!("Expected a failure with invalid access token"); } diff --git a/examples/own_auth/Cargo.toml b/examples/own_auth/Cargo.toml deleted file mode 100644 index 21e0ac0..0000000 --- a/examples/own_auth/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "own_auth" -publish = false -version = "0.0.0" -authors = ["David Gräff "] -edition = "2018" -license = "MIT" -readme = "readme.md" - -[dependencies] -firestore-db-and-auth = { path = "../../", version = "^0", features=["rustls-tls"], default-features=false } -reqwest = { version ="^0.9", default-features = false, features=["rustls-tls"] } -serde = "^1.0" \ No newline at end of file diff --git a/examples/own_auth/readme.md b/examples/own_auth/readme.md deleted file mode 100644 index 3034ad9..0000000 --- a/examples/own_auth/readme.md +++ /dev/null @@ -1,6 +0,0 @@ -# Example - Own Auth - -This example expects a "firebase-service-account.json" file in the working directory. - -It shows how to use your own implementation of FirebaseAuthBearer and avoid using any of the `session` -types. \ No newline at end of file diff --git a/examples/readme.md b/examples/readme.md new file mode 100644 index 0000000..c13867e --- /dev/null +++ b/examples/readme.md @@ -0,0 +1,35 @@ +# Examples + +All examples expects a "firebase-service-account.json" file in the working directory. + +## Create/Read/Write document example + +A document is created / read / and writen to in full and partially in this example. +A service account session is used as well as a firebase impersonated user session. + +* Build and run with `cargo run --example create_read_write_document`. + +## Own authentication mechanism example + +This example shows how to use your own implementation of FirebaseAuthBearer and avoid using any of the `session` +types. + +* Build and run with `cargo run --example own_auth`. + +## Firebase user interaction example + +This example shows how to print all available information about a firebase user, +identified by the firebase user id. + +* Build and run with `cargo run --example firebase_user`. + +## Rocket Protected Route example + +[Rocket](https://rocket.rs) is a an easy to use web-framework for Rust. + +This example shows how to protect an http route by only allowing logged in users. + +* Build and run with `cargo run --example rocket_http_protected_route`. +* Surf to http://127.0.0.1:8000/create_test_user. A firebase user "_test" will be created and an access token is printed. +* Surf to http://127.0.0.1:8000/hello?auth=A_FIREBASE_ACCESS_TOKEN and to http://127.0.0.1:8000/hello +* Surf to http://127.0.0.1:8000/remove_test_user to remove the created test user again. diff --git a/examples/rocket_http_protected_route/src/main.rs b/examples/rocket_http_protected_route.rs similarity index 77% rename from examples/rocket_http_protected_route/src/main.rs rename to examples/rocket_http_protected_route.rs index 5a0e4b9..46e4ed4 100644 --- a/examples/rocket_http_protected_route/src/main.rs +++ b/examples/rocket_http_protected_route.rs @@ -1,14 +1,13 @@ #![feature(proc_macro_hygiene, decl_macro)] -use firestore_db_and_auth::{Credentials, rocket::FirestoreAuthSessionGuard}; -use rocket::{get, routes}; +use firestore_db_and_auth::{rocket::FirestoreAuthSessionGuard, Credentials}; +use rocket::config::Environment; +use rocket::{get, routes, Config}; fn main() { let credentials = Credentials::from_file("firebase-service-account.json").unwrap(); - - let config = Config::build(Environment::Staging) - .port(8000) - .finalize()?; + + let config = Config::build(Environment::Staging).port(8000).finalize()?; rocket::custom(config) .manage(credentials) diff --git a/examples/rocket_http_protected_route/Cargo.toml b/examples/rocket_http_protected_route/Cargo.toml deleted file mode 100644 index 5ae283d..0000000 --- a/examples/rocket_http_protected_route/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "rocket_http_protected_route" -publish = false -version = "0.0.0" -authors = ["David Gräff "] -edition = "2018" -license = "MIT" -readme = "readme.md" - -[dependencies] -firestore-db-and-auth = { path = "../../", version = "^0", features=["rustls-tls","rocket_support"], default-features=false } - -[dependencies.rocket] -version = "^0.4" -default-features = false diff --git a/examples/rocket_http_protected_route/readme.md b/examples/rocket_http_protected_route/readme.md deleted file mode 100644 index a92137f..0000000 --- a/examples/rocket_http_protected_route/readme.md +++ /dev/null @@ -1,12 +0,0 @@ -# Rocket Protected Route Example - -[Rocket](https://rocket.rs) is a an easy to use web-framework for Rust. -This example only compiles with Rust nightly (because Rocket requires nightly) -and expects a "firebase-service-account.json" file in the working directory. - -It shows how to protect an http route by only allowing logged in users. - -* Build and run with `cargo run`. -* Surf to http://127.0.0.1:8000/create_test_user. A firebase user "_test" will be created and an access token is printed. -* Surf to http://127.0.0.1:8000/hello?auth=A_FIREBASE_ACCESS_TOKEN and to http://127.0.0.1:8000/hello -* Surf to http://127.0.0.1:8000/remove_test_user to remove the created test user again. diff --git a/examples/rocket_http_protected_route/rust-toolchain b/examples/rocket_http_protected_route/rust-toolchain deleted file mode 100644 index 07ade69..0000000 --- a/examples/rocket_http_protected_route/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -nightly \ No newline at end of file diff --git a/tests/test_user_id.txt b/examples/test_user_id.txt similarity index 100% rename from tests/test_user_id.txt rename to examples/test_user_id.txt