Skip to content

Commit fae9a17

Browse files
committed
add lots of docs and tests and fix some things
1 parent d1620ff commit fae9a17

File tree

2 files changed

+245
-9
lines changed

2 files changed

+245
-9
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,9 @@ async-session = "1.0.2"
1313
sqlx = { version = "0.3.5" }
1414
async-std = "1.6.2"
1515

16+
17+
[dev-dependencies]
18+
async-std = { version = "1.6.2", features = ["attributes"] }
19+
1620
[patch.crates-io]
1721
async-session = { git = "https://github.com/jbr/async-session", branch = "tide" }

src/sqlite.rs

Lines changed: 241 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,69 @@ pub struct SqliteStore {
2929
}
3030

3131
impl SqliteStore {
32+
/// constructs a new SqliteStore from an existing
33+
/// sqlx::SqlitePool. the default table name for this session
34+
/// store will be "async_sessions". To override this, chain this
35+
/// with [`with_table_name`](crate::SqliteStore::with_table_name).
36+
///
37+
/// ```rust
38+
/// # use async_sqlx_session::SqliteStore;
39+
/// # use async_session::Result;
40+
/// # fn main() -> Result { async_std::task::block_on(async {
41+
/// let pool = sqlx::SqlitePool::new("sqlite:%3Amemory:").await.unwrap();
42+
/// let store = SqliteStore::from_client(pool)
43+
/// .with_table_name("custom_table_name");
44+
/// store.migrate().await;
45+
/// # Ok(()) }) }
46+
/// ```
3247
pub fn from_client(client: SqlitePool) -> Self {
3348
Self {
3449
client,
3550
table_name: "async_sessions".into(),
3651
}
3752
}
3853

54+
/// constructs a new SqliteStore from a sqlite: database url. the
55+
/// default table name for this session store will be
56+
/// "async_sessions". To override this, either chain with
57+
/// [`with_table_name`](crate::SqliteStore::with_table_name) or
58+
/// use
59+
/// [`new_with_table_name`](crate::SqliteStore::new_with_table_name)
60+
///
61+
/// ```rust
62+
/// # use async_sqlx_session::SqliteStore;
63+
/// # use async_session::Result;
64+
/// # fn main() -> Result { async_std::task::block_on(async {
65+
/// let store = SqliteStore::new("sqlite:%3Amemory:").await?
66+
/// .with_table_name("custom_table_name");
67+
/// store.migrate().await;
68+
/// # Ok(()) }) }
69+
/// ```
3970
pub async fn new(database_url: &str) -> sqlx::Result<Self> {
4071
Ok(Self::from_client(SqlitePool::new(database_url).await?))
4172
}
4273

74+
/// constructs a new SqliteStore from a sqlite: database url. the
75+
/// default table name for this session store will be
76+
/// "async_sessions". To override this, either chain with
77+
/// [`with_table_name`](crate::SqliteStore::with_table_name) or
78+
/// use
79+
/// [`new_with_table_name`](crate::SqliteStore::new_with_table_name)
80+
///
81+
/// ```rust
82+
/// # use async_sqlx_session::SqliteStore;
83+
/// # use async_session::Result;
84+
/// # fn main() -> Result { async_std::task::block_on(async {
85+
/// let store = SqliteStore::new_with_table_name("sqlite:%3Amemory:", "custom_table_name").await?;
86+
/// store.migrate().await;
87+
/// # Ok(()) }) }
88+
/// ```
4389
pub async fn new_with_table_name(database_url: &str, table_name: &str) -> sqlx::Result<Self> {
4490
Ok(Self::new(database_url).await?.with_table_name(table_name))
4591
}
4692

93+
/// Chainable method to add a custom table name. This will panic
94+
/// if the table name is not `[a-zA-Z0-9_-]+`.
4795
pub fn with_table_name(mut self, table_name: impl AsRef<str>) -> Self {
4896
let table_name = table_name.as_ref();
4997
if table_name.is_empty()
@@ -61,6 +109,11 @@ impl SqliteStore {
61109
self
62110
}
63111

112+
/// Creates a session table if it does not already exist. If it
113+
/// does, this will noop, making it safe to call repeatedly on
114+
/// store initialization. In the future, this may make
115+
/// exactly-once modifications to the schema of the session table
116+
/// on breaking releases.
64117
pub async fn migrate(&self) -> sqlx::Result<()> {
65118
log::info!("migrating sessions on `{}`", self.table_name);
66119

@@ -87,6 +140,8 @@ impl SqliteStore {
87140
self.client.acquire().await
88141
}
89142

143+
/// Spawns an async_std::task that clears out stale (expired)
144+
/// sessions on a periodic basis.
90145
pub fn spawn_cleanup_task(&self, period: Duration) -> task::JoinHandle<()> {
91146
let store = self.clone();
92147
task::spawn(async move {
@@ -99,6 +154,8 @@ impl SqliteStore {
99154
})
100155
}
101156

157+
/// Performs a one-time cleanup task that clears out stale
158+
/// (expired) sessions. You may want to call this from cron.
102159
pub async fn cleanup(&self) -> sqlx::Result<()> {
103160
let mut connection = self.connection().await?;
104161
sqlx::query(&self.substitute_table_name(
@@ -113,6 +170,14 @@ impl SqliteStore {
113170

114171
Ok(())
115172
}
173+
174+
pub async fn count(&self) -> sqlx::Result<i32> {
175+
let (count,) = sqlx::query_as("select count(*) from async_sessions")
176+
.fetch_one(&mut self.connection().await?)
177+
.await?;
178+
179+
Ok(count)
180+
}
116181
}
117182

118183
#[async_trait]
@@ -124,7 +189,7 @@ impl SessionStore for SqliteStore {
124189
let (session,): (String,) = sqlx::query_as(&self.substitute_table_name(
125190
r#"
126191
SELECT session FROM %%TABLE_NAME%%
127-
WHERE id = ? AND (expires IS NULL || expires > ?)
192+
WHERE id = ? AND (expires IS NULL OR expires > ?)
128193
"#,
129194
))
130195
.bind(&id)
@@ -141,7 +206,7 @@ impl SessionStore for SqliteStore {
141206
let string = serde_json::to_string(&session).ok()?;
142207
let mut connection = self.connection().await.ok()?;
143208

144-
let result = sqlx::query(&self.substitute_table_name(
209+
sqlx::query(&self.substitute_table_name(
145210
r#"
146211
INSERT INTO %%TABLE_NAME%%
147212
(id, session, expires) VALUES (?, ?, ?)
@@ -154,14 +219,10 @@ impl SessionStore for SqliteStore {
154219
.bind(&string)
155220
.bind(&session.expiry().map(|expiry| expiry.timestamp()))
156221
.execute(&mut connection)
157-
.await;
222+
.await
223+
.ok()?;
158224

159-
if let Err(e) = result {
160-
dbg!(e);
161-
None
162-
} else {
163-
session.into_cookie_value()
164-
}
225+
session.into_cookie_value()
165226
}
166227

167228
async fn destroy_session(&self, session: Session) -> Result {
@@ -192,3 +253,174 @@ impl SessionStore for SqliteStore {
192253
Ok(())
193254
}
194255
}
256+
257+
#[cfg(test)]
258+
mod tests {
259+
use super::*;
260+
261+
async fn test_store() -> SqliteStore {
262+
let store = SqliteStore::new("sqlite:%3Amemory:")
263+
.await
264+
.expect("building a sqlite :memory: SqliteStore");
265+
store
266+
.migrate()
267+
.await
268+
.expect("migrating a brand new :memory: SqliteStore");
269+
store
270+
}
271+
272+
#[async_std::test]
273+
async fn creating_a_new_session_with_no_expiry() -> Result {
274+
let store = test_store().await;
275+
let session = Session::new();
276+
session.insert("key".into(), "value".into());
277+
let cloned = session.clone();
278+
let cookie_value = store.store_session(session).await.unwrap();
279+
280+
let (id, expires, serialized, count): (String, Option<i64>, String, i64) =
281+
sqlx::query_as("select id, expires, session, count(*) from async_sessions")
282+
.fetch_one(&mut store.connection().await?)
283+
.await?;
284+
285+
assert_eq!(1, count);
286+
assert_eq!(id, cloned.id());
287+
assert_eq!(expires, None);
288+
289+
let deserialized_session: Session = serde_json::from_str(&serialized)?;
290+
assert_eq!(cloned.id(), deserialized_session.id());
291+
assert_eq!("value", deserialized_session.get("key").unwrap());
292+
293+
let loaded_session = store.load_session(cookie_value).await.unwrap();
294+
assert_eq!(cloned.id(), loaded_session.id());
295+
assert_eq!("value", loaded_session.get("key").unwrap());
296+
297+
assert!(!loaded_session.is_expired());
298+
Ok(())
299+
}
300+
301+
#[async_std::test]
302+
async fn updating_a_session() -> Result {
303+
let store = test_store().await;
304+
let session = Session::new();
305+
let original_id = session.id().to_owned();
306+
307+
session.insert("key".into(), "value".into());
308+
let cookie_value = store.store_session(session).await.unwrap();
309+
310+
let session = store.load_session(cookie_value.clone()).await.unwrap();
311+
session.insert("key".into(), "other value".into());
312+
assert_eq!(None, store.store_session(session).await);
313+
314+
let session = store.load_session(cookie_value.clone()).await.unwrap();
315+
assert_eq!(session.get("key").unwrap(), "other value");
316+
317+
let (id, count): (String, i64) = sqlx::query_as("select id, count(*) from async_sessions")
318+
.fetch_one(&mut store.connection().await?)
319+
.await?;
320+
321+
assert_eq!(1, count);
322+
assert_eq!(original_id, id);
323+
324+
Ok(())
325+
}
326+
327+
#[async_std::test]
328+
async fn updating_a_session_extending_expiry() -> Result {
329+
let store = test_store().await;
330+
let mut session = Session::new();
331+
session.expire_in(Duration::from_secs(10));
332+
let original_id = session.id().to_owned();
333+
let original_expires = session.expiry().unwrap().clone();
334+
let cookie_value = store.store_session(session).await.unwrap();
335+
336+
let mut session = store.load_session(cookie_value.clone()).await.unwrap();
337+
assert_eq!(session.expiry().unwrap(), &original_expires);
338+
session.expire_in(Duration::from_secs(20));
339+
let new_expires = session.expiry().unwrap().clone();
340+
store.store_session(session).await;
341+
342+
let session = store.load_session(cookie_value.clone()).await.unwrap();
343+
assert_eq!(session.expiry().unwrap(), &new_expires);
344+
345+
let (id, expires, count): (String, i64, i64) =
346+
sqlx::query_as("select id, expires, count(*) from async_sessions")
347+
.fetch_one(&mut store.connection().await?)
348+
.await?;
349+
350+
assert_eq!(1, count);
351+
assert_eq!(expires, new_expires.timestamp());
352+
assert_eq!(original_id, id);
353+
354+
Ok(())
355+
}
356+
357+
#[async_std::test]
358+
async fn creating_a_new_session_with_expiry() -> Result {
359+
let store = test_store().await;
360+
let mut session = Session::new();
361+
session.expire_in(Duration::from_secs(1));
362+
session.insert("key".into(), "value".into());
363+
let cloned = session.clone();
364+
365+
let cookie_value = store.store_session(session).await.unwrap();
366+
367+
let (id, expires, serialized, count): (String, Option<i64>, String, i64) =
368+
sqlx::query_as("select id, expires, session, count(*) from async_sessions")
369+
.fetch_one(&mut store.connection().await?)
370+
.await?;
371+
372+
assert_eq!(1, count);
373+
assert_eq!(id, cloned.id());
374+
assert!(expires.unwrap() > Utc::now().timestamp());
375+
dbg!(expires.unwrap() - Utc::now().timestamp());
376+
377+
let deserialized_session: Session = serde_json::from_str(&serialized)?;
378+
assert_eq!(cloned.id(), deserialized_session.id());
379+
assert_eq!("value", deserialized_session.get("key").unwrap());
380+
381+
let loaded_session = store.load_session(cookie_value.clone()).await.unwrap();
382+
assert_eq!(cloned.id(), loaded_session.id());
383+
assert_eq!("value", loaded_session.get("key").unwrap());
384+
385+
assert!(!loaded_session.is_expired());
386+
387+
task::sleep(Duration::from_secs(1)).await;
388+
assert_eq!(None, store.load_session(cookie_value).await);
389+
390+
Ok(())
391+
}
392+
393+
#[async_std::test]
394+
async fn destroying_a_single_session() -> Result {
395+
let store = test_store().await;
396+
for _ in 0..3i8 {
397+
store.store_session(Session::new()).await;
398+
}
399+
400+
let cookie = store.store_session(Session::new()).await.unwrap();
401+
dbg!("storing");
402+
assert_eq!(4, store.count().await?);
403+
let session = store.load_session(cookie.clone()).await.unwrap();
404+
store.destroy_session(session.clone()).await.unwrap();
405+
assert_eq!(None, store.load_session(cookie).await);
406+
assert_eq!(3, store.count().await?);
407+
408+
// attempting to destroy the session again is not an error
409+
assert!(store.destroy_session(session).await.is_ok());
410+
Ok(())
411+
}
412+
413+
#[async_std::test]
414+
async fn clearing_the_whole_store() -> Result {
415+
let store = test_store().await;
416+
for _ in 0..3i8 {
417+
store.store_session(Session::new()).await;
418+
}
419+
420+
assert_eq!(3, store.count().await?);
421+
store.clear_store().await.unwrap();
422+
assert_eq!(0, store.count().await?);
423+
424+
Ok(())
425+
}
426+
}

0 commit comments

Comments
 (0)