Skip to content

Commit

Permalink
provide additional integration tests (#3)
Browse files Browse the repository at this point in the history
provide additional integration tests
  • Loading branch information
maxcountryman authored Sep 25, 2023
1 parent c10863b commit c78add1
Show file tree
Hide file tree
Showing 10 changed files with 442 additions and 239 deletions.
38 changes: 35 additions & 3 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
name: Rust

on: [ push, pull_request ]
on:
push:
branches:
- main
pull_request: {}

env:
CARGO_TERM_COLOR: always
REDIS_URL: redis://localhost:6379/1
POSTGRES_URL: postgres://postgres:postgres@localhost:5432
MYSQL_URL: mysql://root@localhost:3306/public

jobs:
check:
Expand Down Expand Up @@ -55,11 +62,10 @@ jobs:
rustup toolchain install nightly --profile minimal
cargo install cargo-tarpaulin
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: taiki-e/install-action@nextest
- name: Run tests
run: |
cargo nextest run --all --all-features --all-targets
cargo nextest run --test integration-tests
- name: Generate code coverage
run: |
cargo tarpaulin -olcov --output-dir ./coverage
Expand All @@ -82,3 +88,29 @@ jobs:
- name: Run doc tests
run: |
cargo test --all-features --doc
integration-tests:
needs: check
runs-on: ubuntu-latest

strategy:
matrix:
session_store:
- redis-store
- postgres-store
- mysql-store
- sqlite-store

steps:
- uses: actions/checkout@v4
- run: |
rustup toolchain install stable --profile minimal
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@nextest
- name: Start session store
if: matrix.session_store != 'sqlite-store'
run: |
docker compose -f tests/docker-compose.yml up ${{ matrix.session_store }} -d
- name: Run integration tests
run: |
cargo nextest run --test ${{ matrix.session_store}}-integration-tests --features ${{ matrix.session_store }}
31 changes: 22 additions & 9 deletions src/sqlx_store/postgres_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,26 @@ impl PostgresStore {
let mut tx = self.pool.begin().await?;

let create_schema_query = format!(
"create schema if not exists {schema_name}",
r#"create schema if not exists "{schema_name}""#,
schema_name = self.schema_name,
);
sqlx::query(&create_schema_query).execute(&mut *tx).await?;
// Concurrent create schema may fail due to duplicate key violations.
//
// This works around that by assuming the schema must exist on such an error.
if let Err(err) = sqlx::query(&create_schema_query).execute(&mut *tx).await {
if !err
.to_string()
.contains("duplicate key value violates unique constraint")
{
return Err(err);
}

return Ok(());
}

let create_table_query = format!(
r#"
create table if not exists {schema_name}.{table_name}
create table if not exists "{schema_name}"."{table_name}"
(
id text primary key not null,
expiration_time timestamptz null,
Expand Down Expand Up @@ -126,7 +138,7 @@ impl PostgresStore {
async fn delete_expired(&self) -> sqlx::Result<()> {
let query = format!(
r#"
delete from {schema_name}.{table_name}
delete from "{schema_name}"."{table_name}"
where expiration_time < (now() at time zone 'utc')
"#,
schema_name = self.schema_name,
Expand All @@ -144,9 +156,10 @@ impl SessionStore for PostgresStore {
async fn save(&self, session_record: &SessionRecord) -> Result<(), Self::Error> {
let query = format!(
r#"
insert into {schema_name}.{table_name}
(id, expiration_time, data) values ($1, $2, $3)
on conflict(id) do update set
insert into "{schema_name}"."{table_name}" (id, expiration_time, data)
values ($1, $2, $3)
on conflict (id) do update
set
expiration_time = excluded.expiration_time,
data = excluded.data
"#,
Expand All @@ -166,7 +179,7 @@ impl SessionStore for PostgresStore {
async fn load(&self, session_id: &SessionId) -> Result<Option<Session>, Self::Error> {
let query = format!(
r#"
select * from {schema_name}.{table_name}
select * from "{schema_name}"."{table_name}"
where id = $1
and (expiration_time is null or expiration_time > $2)
"#,
Expand All @@ -192,7 +205,7 @@ impl SessionStore for PostgresStore {

async fn delete(&self, session_id: &SessionId) -> Result<(), Self::Error> {
let query = format!(
r#"delete from {schema_name}.{table_name} where id = $1"#,
r#"delete from "{schema_name}"."{table_name}" where id = $1"#,
schema_name = self.schema_name,
table_name = self.table_name
);
Expand Down
257 changes: 257 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
use axum::{error_handling::HandleErrorLayer, routing::get, Router};
use axum_core::{body::BoxBody, BoxError};
use http::{header, HeaderMap, StatusCode};
use time::Duration;
use tower::ServiceBuilder;
use tower_cookies::{cookie, Cookie};
use tower_sessions::{Session, SessionManagerLayer, SessionStore};

fn routes() -> Router {
Router::new()
.route("/", get(|_: Session| async move { "Hello, world!" }))
.route(
"/insert",
get(|session: Session| async move {
session.insert("foo", 42).unwrap();
}),
)
.route(
"/get",
get(|session: Session| async move {
format!("{}", session.get::<usize>("foo").unwrap().unwrap())
}),
)
.route(
"/remove",
get(|session: Session| async move {
session.remove::<usize>("foo").unwrap();
}),
)
.route(
"/cycle_id",
get(|session: Session| async move {
session.cycle_id();
}),
)
.route(
"/delete",
get(|session: Session| async move {
session.delete();
}),
)
}

pub fn build_app<Store: SessionStore>(
mut session_manager: SessionManagerLayer<Store>,
max_age: Option<Duration>,
) -> Router {
if let Some(max_age) = max_age {
session_manager = session_manager.with_max_age(max_age);
}

let session_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::BAD_REQUEST
}))
.layer(session_manager);

routes().layer(session_service)
}

pub async fn body_string(body: BoxBody) -> String {
let bytes = hyper::body::to_bytes(body).await.unwrap();
String::from_utf8_lossy(&bytes).into()
}

pub fn get_session_cookie(headers: &HeaderMap) -> Result<Cookie<'_>, cookie::ParseError> {
headers
.get_all(header::SET_COOKIE)
.iter()
.flat_map(|header| header.to_str())
.next()
.ok_or(cookie::ParseError::MissingPair)
.and_then(Cookie::parse_encoded)
}

#[macro_export]
macro_rules! route_tests {
($create_app:expr) => {
use axum::body::Body;
use common::{body_string, get_session_cookie};
use http::{header, Request, StatusCode};
use time::Duration;
use tower::ServiceExt;
use tower_cookies::{cookie::SameSite, Cookie};

#[tokio::test]
async fn no_session_set() {
let req = Request::builder().uri("/").body(Body::empty()).unwrap();
let res = $create_app(Some(Duration::hours(1)))
.await
.oneshot(req)
.await
.unwrap();

assert!(res
.headers()
.get_all(header::SET_COOKIE)
.iter()
.next()
.is_none());
}

#[tokio::test]
async fn bogus_session_cookie() {
let session_cookie = Cookie::new("tower.sid", "00000000-0000-0000-0000-000000000000");
let req = Request::builder()
.uri("/insert")
.header(header::COOKIE, session_cookie.encoded().to_string())
.body(Body::empty())
.unwrap();
let res = $create_app(Some(Duration::hours(1)))
.await
.oneshot(req)
.await
.unwrap();
let session_cookie = get_session_cookie(res.headers()).unwrap();

assert_eq!(res.status(), StatusCode::OK);
assert_ne!(
session_cookie.value(),
"00000000-0000-0000-0000-000000000000"
);
}

#[tokio::test]
async fn malformed_session_cookie() {
let session_cookie = Cookie::new("tower.sid", "malformed");
let req = Request::builder()
.uri("/")
.header(header::COOKIE, session_cookie.encoded().to_string())
.body(Body::empty())
.unwrap();
let res = $create_app(Some(Duration::hours(1)))
.await
.oneshot(req)
.await
.unwrap();

assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}

#[tokio::test]
async fn insert_session() {
let req = Request::builder()
.uri("/insert")
.body(Body::empty())
.unwrap();
let res = $create_app(Some(Duration::hours(1)))
.await
.oneshot(req)
.await
.unwrap();
let session_cookie = get_session_cookie(res.headers()).unwrap();

assert_eq!(session_cookie.name(), "tower.sid");
assert_eq!(session_cookie.http_only(), Some(true));
assert_eq!(session_cookie.same_site(), Some(SameSite::Strict));
assert!(session_cookie
.max_age()
.is_some_and(|dt| dt <= Duration::hours(1)));
assert_eq!(session_cookie.secure(), Some(true));
assert_eq!(session_cookie.path(), Some("/"));
}

#[tokio::test]
async fn session_expiration() {
let req = Request::builder()
.uri("/insert")
.body(Body::empty())
.unwrap();
let res = $create_app(None).await.oneshot(req).await.unwrap();
let session_cookie = get_session_cookie(res.headers()).unwrap();

assert_eq!(session_cookie.name(), "tower.sid");
assert_eq!(session_cookie.http_only(), Some(true));
assert_eq!(session_cookie.same_site(), Some(SameSite::Strict));
assert!(session_cookie.max_age().is_none());
assert_eq!(session_cookie.secure(), Some(true));
assert_eq!(session_cookie.path(), Some("/"));
}

#[tokio::test]
async fn get_session() {
let app = $create_app(Some(Duration::hours(1))).await;

let req = Request::builder()
.uri("/insert")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
let session_cookie = get_session_cookie(res.headers()).unwrap();

let req = Request::builder()
.uri("/get")
.header(header::COOKIE, session_cookie.encoded().to_string())
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();

assert_eq!(body_string(res.into_body()).await, "42");
}

#[tokio::test]
async fn cycle_session_id() {
let app = $create_app(Some(Duration::hours(1))).await;

let req = Request::builder()
.uri("/insert")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
let first_session_cookie = get_session_cookie(res.headers()).unwrap();

let req = Request::builder()
.uri("/cycle_id")
.header(header::COOKIE, first_session_cookie.encoded().to_string())
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
let second_session_cookie = get_session_cookie(res.headers()).unwrap();

let req = Request::builder()
.uri("/get")
.header(header::COOKIE, second_session_cookie.encoded().to_string())
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();

assert_ne!(first_session_cookie.value(), second_session_cookie.value());
assert_eq!(body_string(res.into_body()).await, "42");
}

#[tokio::test]
async fn delete_session() {
let app = $create_app(Some(Duration::hours(1))).await;

let req = Request::builder()
.uri("/insert")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
let session_cookie = get_session_cookie(res.headers()).unwrap();

let req = Request::builder()
.uri("/delete")
.header(header::COOKIE, session_cookie.encoded().to_string())
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();

let session_cookie = get_session_cookie(res.headers()).unwrap();

assert_eq!(session_cookie.value(), "");
assert_eq!(session_cookie.max_age(), Some(Duration::ZERO));
}
};
}
Loading

0 comments on commit c78add1

Please sign in to comment.