entertainarr_adapter_sqlite/
auth.rs1use anyhow::Context;
2use entertainarr_domain::auth::entity::Profile;
3use entertainarr_domain::auth::prelude::SignupError;
4
5const FIND_BY_CREDS_QUERY: &str = "select id from users where email like ? limit 1";
6const CREATE_QUERY: &str = "insert into users (email, password) values (?, ?) returning id";
7
8impl super::Pool {
9 #[tracing::instrument(
10 skip_all,
11 fields(
12 otel.kind = "client",
13 db.system = "sqlite",
14 db.name = "authentication",
15 db.operation = "insert",
16 db.sql.table = "users",
17 db.query.text = CREATE_QUERY,
18 db.response.returned_rows = tracing::field::Empty,
19 error.type = tracing::field::Empty,
20 error.message = tracing::field::Empty,
21 error.stacktrace = tracing::field::Empty,
22 ),
23 err(Debug),
24 )]
25 pub(crate) async fn create_user(
26 &self,
27 email: &str,
28 password: &str,
29 ) -> Result<Profile, SignupError> {
30 sqlx::query_as(CREATE_QUERY)
31 .bind(email)
32 .bind(password)
33 .fetch_one(&self.0)
34 .await
35 .inspect(super::record_one)
36 .inspect_err(super::record_error)
37 .map(super::Wrapper::inner)
38 .map_err(|err| match err.as_database_error() {
39 Some(dberr) if dberr.is_unique_violation() => SignupError::EmailConflict,
40 _ => SignupError::Internal(anyhow::Error::from(err)),
41 })
42 }
43
44 #[tracing::instrument(
45 skip_all,
46 fields(
47 otel.kind = "client",
48 db.system = "sqlite",
49 db.name = "authentication",
50 db.operation = "SELECT",
51 db.sql.table = "users",
52 db.query.text = FIND_BY_CREDS_QUERY,
53 db.response.returned_rows = tracing::field::Empty,
54 error.type = tracing::field::Empty,
55 error.message = tracing::field::Empty,
56 error.stacktrace = tracing::field::Empty,
57 ),
58 err(Debug),
59 )]
60
61 pub(crate) async fn find_user_by_credentials(
62 &self,
63 email: &str,
64 _password: &str,
65 ) -> anyhow::Result<Option<Profile>> {
66 sqlx::query_as(FIND_BY_CREDS_QUERY)
67 .bind(email)
68 .fetch_optional(&self.0)
69 .await
70 .inspect(super::record_optional)
71 .inspect_err(super::record_error)
72 .map(super::Wrapper::maybe_inner)
73 .context("unable to fetch profile by credentials")
74 }
75}
76
77impl entertainarr_domain::auth::prelude::AuthenticationRepository for super::Pool {
78 async fn find_by_credentials(
79 &self,
80 email: &str,
81 password: &str,
82 ) -> anyhow::Result<Option<Profile>> {
83 self.find_user_by_credentials(email, password).await
84 }
85
86 async fn create(&self, email: &str, password: &str) -> Result<Profile, SignupError> {
87 self.create_user(email, password).await
88 }
89}
90
91impl<'r> sqlx::FromRow<'r, sqlx::sqlite::SqliteRow>
92 for super::Wrapper<entertainarr_domain::auth::entity::Profile>
93{
94 fn from_row(row: &'r sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
95 use entertainarr_domain::auth::entity::Profile;
96 use sqlx::Row;
97
98 Ok(Self(Profile {
99 id: row.try_get(0)?,
100 }))
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use entertainarr_domain::auth::prelude::{AuthenticationRepository, SignupError};
107
108 #[tokio::test]
109 async fn should_not_find_user_by_creds_when_missing() {
110 let tmpdir = tempfile::tempdir().unwrap();
111 let pool = crate::Pool::test(&tmpdir.path().join("db")).await;
112 let res = pool
113 .find_by_credentials("user@example.com", "password")
114 .await
115 .unwrap();
116 assert!(res.is_none());
117 }
118
119 #[tokio::test]
120 async fn should_find_user_by_creds_when_exists() {
121 let tmpdir = tempfile::tempdir().unwrap();
122 let pool = crate::Pool::test(&tmpdir.path().join("db")).await;
123
124 let profile = pool
125 .create_user("user@example.com", "password")
126 .await
127 .unwrap();
128 let res = pool
129 .find_by_credentials("user@example.com", "password")
130 .await
131 .unwrap();
132 assert_eq!(res.map(|item| item.id), Some(profile.id));
133 }
134
135 #[tokio::test]
136 async fn should_create() {
137 let tmpdir = tempfile::tempdir().unwrap();
138 let pool = crate::Pool::test(&tmpdir.path().join("db")).await;
139 let res = pool.create("user@example.com", "password").await.unwrap();
140 assert_eq!(res.id, 1);
141 }
142
143 #[tokio::test]
144 async fn should_not_create_if_exists() {
145 let tmpdir = tempfile::tempdir().unwrap();
146 let pool = crate::Pool::test(&tmpdir.path().join("db")).await;
147 let res = pool.create("user@example.com", "password").await.unwrap();
148 assert_eq!(res.id, 1);
149 let err = pool
150 .create("user@example.com", "password")
151 .await
152 .unwrap_err();
153 assert!(matches!(err, SignupError::EmailConflict));
154 }
155}