entertainarr_adapter_sqlite/
auth.rs

1use 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}