Skip to main content

koan_core/db/queries/
auth.rs

1//! Auth queries: user CRUD, refresh token management.
2
3use rusqlite::{Connection, params};
4
5use crate::auth::{self, Role};
6
7// ---------------------------------------------------------------------------
8// Row types
9// ---------------------------------------------------------------------------
10
11#[derive(Debug, Clone)]
12pub struct UserRow {
13    pub id: i64,
14    pub username: String,
15    pub password_hash: String,
16    pub role: Role,
17    pub created_at: Option<String>,
18}
19
20#[derive(Debug, Clone)]
21pub struct RefreshTokenRow {
22    pub id: String,
23    pub user_id: i64,
24    pub expires_at: i64,
25    pub revoked: bool,
26    pub created_at: Option<String>,
27    /// The OAuth grant the token descends from, and the client it was granted
28    /// to; `None` for app and web sessions.
29    pub grant: Option<OAuthGrant>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct OAuthGrant {
34    pub id: String,
35    pub client_name: String,
36}
37
38const TOKEN_COLUMNS: &str = "id, user_id, expires_at, revoked, created_at, grant_id, client_name";
39
40fn token_row(row: &rusqlite::Row) -> rusqlite::Result<RefreshTokenRow> {
41    let grant_id: Option<String> = row.get(5)?;
42    let client_name: Option<String> = row.get(6)?;
43    Ok(RefreshTokenRow {
44        id: row.get(0)?,
45        user_id: row.get(1)?,
46        expires_at: row.get(2)?,
47        revoked: row.get::<_, i32>(3)? != 0,
48        created_at: row.get(4)?,
49        grant: grant_id.map(|id| OAuthGrant {
50            id,
51            client_name: client_name.unwrap_or_default(),
52        }),
53    })
54}
55
56// ---------------------------------------------------------------------------
57// Whose data
58// ---------------------------------------------------------------------------
59
60/// The user a caller with no account acts as: the macOS app, the TUI, a server
61/// with auth disabled, the Subsonic shared secret.
62///
63/// Favourites, playlists and play history are per user. An install with no
64/// admin account keeps them under this id; once there is one, the first admin
65/// owns them and this id [resolves](resolve_user) to theirs, so a single-user
66/// server and a local library behave the same.
67pub const LOCAL_USER: i64 = 0;
68
69/// The first admin account, which answers for [`LOCAL_USER`].
70pub fn first_admin(conn: &Connection) -> Result<Option<i64>, rusqlite::Error> {
71    conn.prepare_cached("SELECT MIN(id) FROM users WHERE role = 'admin'")?
72        .query_row([], |r| r.get(0))
73}
74
75/// The id whose rows `user` reads and writes: `user` itself for an account,
76/// the first admin (or [`LOCAL_USER`] while there is none) for the implicit user.
77pub fn resolve_user(conn: &Connection, user: i64) -> Result<i64, rusqlite::Error> {
78    if user != LOCAL_USER {
79        return Ok(user);
80    }
81    Ok(first_admin(conn)?.unwrap_or(LOCAL_USER))
82}
83
84/// Whether `user` is the one [`LOCAL_USER`] resolves to: whose favourites and
85/// playlists this koan syncs with an upstream server.
86pub fn is_local_user(conn: &Connection, user: i64) -> Result<bool, rusqlite::Error> {
87    Ok(resolve_user(conn, user)? == resolve_user(conn, LOCAL_USER)?)
88}
89
90/// Hand the implicit user's rows to the first admin, once there is one.
91///
92/// Where a row would duplicate one the admin already has, theirs is kept.
93pub fn adopt_local_rows(conn: &Connection) -> Result<(), rusqlite::Error> {
94    let Some(admin) = first_admin(conn)? else {
95        return Ok(());
96    };
97    for table in [
98        "favourites",
99        "favourite_albums",
100        "favourite_artists",
101        "play_history",
102        "playlists",
103        "shares",
104    ] {
105        // Runs on every open: a read, so it takes no write lock when there is
106        // nothing to hand over.
107        let pending: bool = conn.query_row(
108            &format!("SELECT EXISTS(SELECT 1 FROM {table} WHERE user_id = ?1)"),
109            params![LOCAL_USER],
110            |r| r.get(0),
111        )?;
112        if !pending {
113            continue;
114        }
115        conn.execute(
116            &format!("UPDATE OR IGNORE {table} SET user_id = ?1 WHERE user_id = ?2"),
117            params![admin, LOCAL_USER],
118        )?;
119        conn.execute(
120            &format!("DELETE FROM {table} WHERE user_id = ?1"),
121            params![LOCAL_USER],
122        )?;
123    }
124    Ok(())
125}
126
127// ---------------------------------------------------------------------------
128// User CRUD
129// ---------------------------------------------------------------------------
130
131/// Store the password sealed for Subsonic token auth (see `auth::seal_password`).
132pub fn set_sealed_password(
133    conn: &Connection,
134    username: &str,
135    sealed: &[u8],
136) -> Result<(), rusqlite::Error> {
137    conn.execute(
138        "UPDATE users SET sealed_password = ?2 WHERE username = ?1",
139        params![username, sealed],
140    )?;
141    Ok(())
142}
143
144pub fn sealed_password(
145    conn: &Connection,
146    username: &str,
147) -> Result<Option<Vec<u8>>, rusqlite::Error> {
148    use rusqlite::OptionalExtension;
149    Ok(conn
150        .query_row(
151            "SELECT sealed_password FROM users WHERE username = ?1",
152            params![username],
153            |r| r.get::<_, Option<Vec<u8>>>(0),
154        )
155        .optional()?
156        .flatten())
157}
158
159/// Seal `password` under the server's key and store it, so the account can
160/// use Subsonic token auth. Call only with a password known to be the user's.
161pub fn remember_password(
162    conn: &Connection,
163    username: &str,
164    password: &str,
165) -> Result<(), Box<dyn std::error::Error>> {
166    let key = auth::subsonic_key()?;
167    set_sealed_password(
168        conn,
169        username,
170        &auth::seal_password(&key, username, password)?,
171    )?;
172    Ok(())
173}
174
175/// Create a new user. Returns the user ID.
176pub fn create_user(
177    conn: &Connection,
178    username: &str,
179    password: &str,
180    role: Role,
181) -> Result<i64, rusqlite::Error> {
182    let hash = auth::hash_password(password)
183        .map_err(|e| rusqlite::Error::ToSqlConversionFailure(e.into()))?;
184    conn.execute(
185        "INSERT INTO users (username, password_hash, role) VALUES (?1, ?2, ?3)",
186        params![username, hash, role.as_str()],
187    )?;
188    let id = conn.last_insert_rowid();
189    adopt_local_rows(conn)?;
190    Ok(id)
191}
192
193/// Get a user by username.
194pub fn get_user_by_username(
195    conn: &Connection,
196    username: &str,
197) -> Result<Option<UserRow>, rusqlite::Error> {
198    let mut stmt = conn.prepare_cached(
199        "SELECT id, username, password_hash, role, created_at FROM users WHERE username = ?1",
200    )?;
201    let mut rows = stmt.query_map(params![username], |row| {
202        let role_str: String = row.get(3)?;
203        Ok(UserRow {
204            id: row.get(0)?,
205            username: row.get(1)?,
206            password_hash: row.get(2)?,
207            role: role_str.parse().unwrap_or(Role::Readonly),
208            created_at: row.get(4)?,
209        })
210    })?;
211    match rows.next() {
212        Some(Ok(user)) => Ok(Some(user)),
213        Some(Err(e)) => Err(e),
214        None => Ok(None),
215    }
216}
217
218/// Get a user by ID.
219pub fn get_user_by_id(conn: &Connection, user_id: i64) -> Result<Option<UserRow>, rusqlite::Error> {
220    let mut stmt = conn
221        .prepare("SELECT id, username, password_hash, role, created_at FROM users WHERE id = ?1")?;
222    let mut rows = stmt.query_map(params![user_id], |row| {
223        let role_str: String = row.get(3)?;
224        Ok(UserRow {
225            id: row.get(0)?,
226            username: row.get(1)?,
227            password_hash: row.get(2)?,
228            role: role_str.parse().unwrap_or(Role::Readonly),
229            created_at: row.get(4)?,
230        })
231    })?;
232    match rows.next() {
233        Some(Ok(user)) => Ok(Some(user)),
234        Some(Err(e)) => Err(e),
235        None => Ok(None),
236    }
237}
238
239/// List all users (no password hashes).
240pub fn list_users(conn: &Connection) -> Result<Vec<UserRow>, rusqlite::Error> {
241    let mut stmt = conn
242        .prepare("SELECT id, username, password_hash, role, created_at FROM users ORDER BY id")?;
243    let rows = stmt.query_map([], |row| {
244        let role_str: String = row.get(3)?;
245        Ok(UserRow {
246            id: row.get(0)?,
247            username: row.get(1)?,
248            password_hash: row.get(2)?,
249            role: role_str.parse().unwrap_or(Role::Readonly),
250            created_at: row.get(4)?,
251        })
252    })?;
253    rows.collect()
254}
255
256/// Delete a user by ID. Returns true if a row was deleted.
257pub fn delete_user(conn: &Connection, user_id: i64) -> Result<bool, rusqlite::Error> {
258    let count = conn.execute("DELETE FROM users WHERE id = ?1", params![user_id])?;
259    Ok(count > 0)
260}
261
262/// Update a user's password. Revokes all their refresh tokens and API keys: a
263/// reset is how an admin shuts out whoever else had the password, and a key
264/// made with it would otherwise outlast it.
265pub fn update_password(
266    conn: &Connection,
267    username: &str,
268    new_password: &str,
269) -> Result<bool, Box<dyn std::error::Error>> {
270    let hash = crate::auth::hash_password(new_password)?;
271    let updated = conn.execute(
272        "UPDATE users SET password_hash = ?1 WHERE username = ?2",
273        params![hash, username],
274    )?;
275    if updated > 0 {
276        // Revoke all existing tokens for this user.
277        if let Some(user) = get_user_by_username(conn, username)? {
278            revoke_all_user_tokens(conn, user.id)?;
279            super::api_keys::revoke_user_api_keys(conn, user.id)?;
280        }
281    }
282    Ok(updated > 0)
283}
284
285/// Update a user's role.
286pub fn update_role(
287    conn: &Connection,
288    username: &str,
289    role: crate::auth::Role,
290) -> Result<bool, rusqlite::Error> {
291    let updated = conn.execute(
292        "UPDATE users SET role = ?1 WHERE username = ?2",
293        params![role.as_str(), username],
294    )?;
295    adopt_local_rows(conn)?;
296    Ok(updated > 0)
297}
298
299/// Check if any users exist (for first-run detection).
300pub fn has_users(conn: &Connection) -> Result<bool, rusqlite::Error> {
301    let count: i64 = conn.query_row("SELECT COUNT(*) FROM users", [], |row| row.get(0))?;
302    Ok(count > 0)
303}
304
305/// Count users with admin role.
306pub fn admin_count(conn: &Connection) -> Result<i64, rusqlite::Error> {
307    conn.query_row(
308        "SELECT COUNT(*) FROM users WHERE role = 'admin'",
309        [],
310        |row| row.get(0),
311    )
312}
313
314// ---------------------------------------------------------------------------
315// Refresh tokens
316// ---------------------------------------------------------------------------
317
318/// Store a refresh token. Only `sha256(token)` is persisted — the raw token is
319/// a bearer credential and read access to the database must not yield one.
320pub fn store_refresh_token(
321    conn: &Connection,
322    token_id: &str,
323    user_id: i64,
324    expires_at: i64,
325) -> Result<(), rusqlite::Error> {
326    store_grant_token(conn, token_id, user_id, expires_at, None)
327}
328
329/// Store a refresh token belonging to an OAuth grant, or to none.
330pub fn store_grant_token(
331    conn: &Connection,
332    token_id: &str,
333    user_id: i64,
334    expires_at: i64,
335    grant: Option<&OAuthGrant>,
336) -> Result<(), rusqlite::Error> {
337    conn.execute(
338        "INSERT INTO refresh_tokens (id, user_id, expires_at, grant_id, client_name)
339         VALUES (?1, ?2, ?3, ?4, ?5)",
340        params![
341            auth::sha256_hex(token_id),
342            user_id,
343            expires_at,
344            grant.map(|g| &g.id),
345            grant.map(|g| &g.client_name),
346        ],
347    )?;
348    Ok(())
349}
350
351/// Look up a refresh token. Returns None if not found, expired, or revoked.
352pub fn get_valid_refresh_token(
353    conn: &Connection,
354    token_id: &str,
355) -> Result<Option<RefreshTokenRow>, rusqlite::Error> {
356    let now = auth::now_unix() as i64;
357    let mut stmt = conn.prepare_cached(&format!(
358        "SELECT {TOKEN_COLUMNS} FROM refresh_tokens
359         WHERE id = ?1 AND revoked = 0 AND expires_at > ?2"
360    ))?;
361    let mut rows = stmt.query_map(params![auth::sha256_hex(token_id), now], token_row)?;
362    match rows.next() {
363        Some(Ok(token)) => Ok(Some(token)),
364        Some(Err(e)) => Err(e),
365        None => Ok(None),
366    }
367}
368
369/// Atomically consume a valid refresh token: revoke it and return the row in one
370/// statement. Returns `None` if the token doesn't exist, is already revoked, or
371/// has expired. This prevents TOCTOU races in refresh-token rotation.
372pub fn consume_refresh_token(
373    conn: &Connection,
374    token_id: &str,
375) -> Result<Option<RefreshTokenRow>, rusqlite::Error> {
376    let now = auth::now_unix() as i64;
377    let mut stmt = conn.prepare(&format!(
378        "UPDATE refresh_tokens SET revoked = 1, used_at = ?2
379         WHERE id = ?1 AND revoked = 0 AND expires_at > ?2
380         RETURNING {TOKEN_COLUMNS}"
381    ))?;
382    let mut rows = stmt.query_map(params![auth::sha256_hex(token_id), now], token_row)?;
383    match rows.next() {
384        Some(Ok(token)) => Ok(Some(token)),
385        Some(Err(e)) => Err(e),
386        None => Ok(None),
387    }
388}
389
390/// Revoke a single refresh token (logout).
391pub fn revoke_refresh_token(conn: &Connection, token_id: &str) -> Result<bool, rusqlite::Error> {
392    let count = conn.execute(
393        "UPDATE refresh_tokens SET revoked = 1 WHERE id = ?1",
394        params![auth::sha256_hex(token_id)],
395    )?;
396    Ok(count > 0)
397}
398
399/// Revoke all refresh tokens for a user (password change, account delete).
400pub fn revoke_all_user_tokens(conn: &Connection, user_id: i64) -> Result<usize, rusqlite::Error> {
401    let count = conn.execute(
402        "UPDATE refresh_tokens SET revoked = 1 WHERE user_id = ?1 AND revoked = 0",
403        params![user_id],
404    )?;
405    Ok(count)
406}
407
408/// A refresh token of an OAuth grant spent once already, more than `grace_secs`
409/// ago, is a copy in someone else's hands: revoke the whole grant, so whichever
410/// side refreshed first loses it too. The grace covers a client retrying a
411/// refresh whose answer it never received. Returns how many tokens were revoked.
412///
413/// OAuth grants only: browser tabs and app tasks share one session and may
414/// race a refresh, which is not theft.
415pub fn revoke_replayed_grant(
416    conn: &Connection,
417    token_id: &str,
418    grace_secs: i64,
419) -> Result<usize, rusqlite::Error> {
420    let cutoff = auth::now_unix() as i64 - grace_secs;
421    conn.execute(
422        "UPDATE refresh_tokens SET revoked = 1
423         WHERE revoked = 0 AND grant_id = (
424           SELECT grant_id FROM refresh_tokens
425           WHERE id = ?1 AND revoked = 1 AND grant_id IS NOT NULL AND used_at < ?2)",
426        params![auth::sha256_hex(token_id), cutoff],
427    )
428}
429
430/// Revoke every refresh token of an OAuth grant.
431pub fn revoke_grant(conn: &Connection, grant_id: &str) -> Result<usize, rusqlite::Error> {
432    conn.execute(
433        "UPDATE refresh_tokens SET revoked = 1 WHERE grant_id = ?1 AND revoked = 0",
434        params![grant_id],
435    )
436}
437
438/// Clean up expired/revoked refresh tokens (housekeeping). A spent token of an
439/// OAuth grant stays until it would have expired, so `revoke_replayed_grant`
440/// can still recognise it.
441pub fn cleanup_expired_tokens(conn: &Connection) -> Result<usize, rusqlite::Error> {
442    let now = auth::now_unix() as i64;
443    let count = conn.execute(
444        "DELETE FROM refresh_tokens
445         WHERE expires_at <= ?1 OR (revoked = 1 AND (grant_id IS NULL OR used_at IS NULL))",
446        params![now],
447    )?;
448    Ok(count)
449}
450
451// ---------------------------------------------------------------------------
452// Tests
453// ---------------------------------------------------------------------------
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use crate::db::connection::Database;
459    use tempfile::TempDir;
460
461    fn test_db() -> (Database, TempDir) {
462        let tmp = TempDir::new().unwrap();
463        let db_path = tmp.path().join("test.db");
464        let db = Database::open(&db_path).unwrap();
465        (db, tmp)
466    }
467
468    #[test]
469    fn create_and_get_user() {
470        let (db, _tmp) = test_db();
471        let id = create_user(&db.conn, "alice", "password123", Role::Admin).unwrap();
472        assert!(id > 0);
473
474        let user = get_user_by_username(&db.conn, "alice").unwrap().unwrap();
475        assert_eq!(user.username, "alice");
476        assert_eq!(user.role, Role::Admin);
477        assert!(user.password_hash.starts_with("$argon2"));
478    }
479
480    #[test]
481    fn duplicate_username_rejected() {
482        let (db, _tmp) = test_db();
483        create_user(&db.conn, "bob", "pass1", Role::User).unwrap();
484        let result = create_user(&db.conn, "bob", "pass2", Role::User);
485        assert!(result.is_err());
486    }
487
488    #[test]
489    fn list_and_delete_users() {
490        let (db, _tmp) = test_db();
491        let id1 = create_user(&db.conn, "user1", "pass", Role::Admin).unwrap();
492        create_user(&db.conn, "user2", "pass", Role::User).unwrap();
493
494        let users = list_users(&db.conn).unwrap();
495        assert_eq!(users.len(), 2);
496
497        assert!(delete_user(&db.conn, id1).unwrap());
498        let users = list_users(&db.conn).unwrap();
499        assert_eq!(users.len(), 1);
500        assert_eq!(users[0].username, "user2");
501    }
502
503    #[test]
504    fn has_users_empty_and_populated() {
505        let (db, _tmp) = test_db();
506        assert!(!has_users(&db.conn).unwrap());
507        create_user(&db.conn, "first", "pass", Role::Admin).unwrap();
508        assert!(has_users(&db.conn).unwrap());
509    }
510
511    #[test]
512    fn refresh_token_lifecycle() {
513        let (db, _tmp) = test_db();
514        let uid = create_user(&db.conn, "user", "pass", Role::User).unwrap();
515
516        let future_ts = auth::now_unix() as i64 + 86400;
517        store_refresh_token(&db.conn, "tok-123", uid, future_ts).unwrap();
518
519        // Valid lookup.
520        let tok = get_valid_refresh_token(&db.conn, "tok-123")
521            .unwrap()
522            .unwrap();
523        assert_eq!(tok.user_id, uid);
524
525        // Revoke.
526        assert!(revoke_refresh_token(&db.conn, "tok-123").unwrap());
527        assert!(
528            get_valid_refresh_token(&db.conn, "tok-123")
529                .unwrap()
530                .is_none()
531        );
532    }
533
534    #[test]
535    fn expired_token_not_returned() {
536        let (db, _tmp) = test_db();
537        let uid = create_user(&db.conn, "user", "pass", Role::User).unwrap();
538
539        // Already expired.
540        store_refresh_token(&db.conn, "tok-old", uid, 0).unwrap();
541        assert!(
542            get_valid_refresh_token(&db.conn, "tok-old")
543                .unwrap()
544                .is_none()
545        );
546    }
547
548    #[test]
549    fn a_spent_grant_token_coming_back_revokes_the_grant() {
550        let (db, _tmp) = test_db();
551        let uid = create_user(&db.conn, "user", "pass", Role::User).unwrap();
552        let future = auth::now_unix() as i64 + 86400;
553        let grant = OAuthGrant {
554            id: "g1".into(),
555            client_name: "Claude".into(),
556        };
557        store_grant_token(&db.conn, "first", uid, future, Some(&grant)).unwrap();
558        let spent = consume_refresh_token(&db.conn, "first").unwrap().unwrap();
559        assert_eq!(spent.grant.as_ref(), Some(&grant));
560        store_grant_token(&db.conn, "second", uid, future, Some(&grant)).unwrap();
561        // An app session, spent the same way, is not a grant.
562        store_refresh_token(&db.conn, "app", uid, future).unwrap();
563        consume_refresh_token(&db.conn, "app").unwrap().unwrap();
564
565        // Within the grace: a retry, nothing revoked.
566        assert_eq!(revoke_replayed_grant(&db.conn, "first", 30).unwrap(), 0);
567        assert_eq!(revoke_replayed_grant(&db.conn, "app", -1).unwrap(), 0);
568        // Spent and kept, so cleanup leaves it to be recognised.
569        cleanup_expired_tokens(&db.conn).unwrap();
570        assert_eq!(revoke_replayed_grant(&db.conn, "first", -1).unwrap(), 1);
571        assert!(
572            get_valid_refresh_token(&db.conn, "second")
573                .unwrap()
574                .is_none()
575        );
576    }
577
578    #[test]
579    fn cleanup_removes_expired_and_revoked() {
580        let (db, _tmp) = test_db();
581        let uid = create_user(&db.conn, "user", "pass", Role::User).unwrap();
582
583        let future = auth::now_unix() as i64 + 86400;
584        store_refresh_token(&db.conn, "active", uid, future).unwrap();
585        store_refresh_token(&db.conn, "expired", uid, 0).unwrap();
586        store_refresh_token(&db.conn, "revoked", uid, future).unwrap();
587        revoke_refresh_token(&db.conn, "revoked").unwrap();
588
589        let cleaned = cleanup_expired_tokens(&db.conn).unwrap();
590        assert_eq!(cleaned, 2);
591
592        // Active token still there.
593        assert!(
594            get_valid_refresh_token(&db.conn, "active")
595                .unwrap()
596                .is_some()
597        );
598    }
599
600    // -- Per-user data ------------------------------------------------------
601
602    use crate::db::queries::{self, sample_meta, upsert_track};
603    use std::path::Path;
604
605    fn count(db: &Database, sql: &str) -> i64 {
606        db.conn.query_row(sql, [], |r| r.get(0)).unwrap()
607    }
608
609    #[test]
610    fn two_users_star_the_same_track_independently() {
611        let (db, _tmp) = test_db();
612        let admin = create_user(&db.conn, "owner", "pw", Role::Admin).unwrap();
613        let mate = create_user(&db.conn, "mate", "pw", Role::User).unwrap();
614        let path = Path::new("/music/a.flac");
615
616        queries::add_favourite(&db.conn, admin, path).unwrap();
617        queries::add_favourite(&db.conn, mate, path).unwrap();
618        queries::remove_favourite(&db.conn, admin, path).unwrap();
619
620        assert!(
621            queries::load_favourites(&db.conn, admin)
622                .unwrap()
623                .is_empty()
624        );
625        assert!(
626            queries::load_favourites(&db.conn, mate)
627                .unwrap()
628                .contains(path)
629        );
630        assert!(queries::toggle_favourite_album(&db.conn, mate, "Coil", "Scatology").unwrap());
631        assert!(queries::toggle_favourite_album(&db.conn, admin, "Coil", "Scatology").unwrap());
632        assert_eq!(count(&db, "SELECT COUNT(*) FROM favourite_albums"), 2);
633    }
634
635    #[test]
636    fn the_local_user_is_the_first_admin_once_there_is_one() {
637        let (db, _tmp) = test_db();
638        let path = Path::new("/music/a.flac");
639        let track = upsert_track(&db.conn, &sample_meta("T", "A", "B")).unwrap();
640        queries::add_favourite(&db.conn, LOCAL_USER, path).unwrap();
641        queries::record_play(&db.conn, LOCAL_USER, track, None).unwrap();
642        let list = queries::create_playlist(&db.conn, LOCAL_USER, "Mine", None).unwrap();
643        assert_eq!(resolve_user(&db.conn, LOCAL_USER).unwrap(), LOCAL_USER);
644
645        create_user(&db.conn, "mate", "pw", Role::User).unwrap();
646        assert_eq!(resolve_user(&db.conn, LOCAL_USER).unwrap(), LOCAL_USER);
647        let admin = create_user(&db.conn, "owner", "pw", Role::Admin).unwrap();
648
649        assert_eq!(resolve_user(&db.conn, LOCAL_USER).unwrap(), admin);
650        assert!(
651            queries::load_favourites(&db.conn, admin)
652                .unwrap()
653                .contains(path)
654        );
655        assert_eq!(queries::play_count(&db.conn, admin, track).unwrap(), 1);
656        assert_eq!(
657            queries::get_playlist(&db.conn, list)
658                .unwrap()
659                .unwrap()
660                .user_id,
661            admin
662        );
663        assert_eq!(
664            count(&db, "SELECT COUNT(*) FROM favourites WHERE user_id = 0"),
665            0
666        );
667    }
668
669    #[test]
670    fn playlists_are_the_owners_plus_everyones_public_ones() {
671        let (db, _tmp) = test_db();
672        let admin = create_user(&db.conn, "owner", "pw", Role::Admin).unwrap();
673        let mate = create_user(&db.conn, "mate", "pw", Role::User).unwrap();
674        let private = queries::create_playlist(&db.conn, admin, "Private", None).unwrap();
675        let public = queries::create_playlist(&db.conn, admin, "Public", None).unwrap();
676        db.conn
677            .execute("UPDATE playlists SET public = 1 WHERE id = ?1", [public])
678            .unwrap();
679        let own = queries::create_playlist(&db.conn, mate, "Mate's", None).unwrap();
680
681        let ids = |user| -> Vec<i64> {
682            let mut ids: Vec<i64> = queries::list_playlists(&db.conn, user)
683                .unwrap()
684                .into_iter()
685                .map(|p| p.id)
686                .collect();
687            ids.sort_unstable();
688            ids
689        };
690        assert_eq!(ids(mate), vec![public, own]);
691        assert_eq!(ids(admin), vec![private, public]);
692        // The implicit user is the first admin.
693        assert_eq!(ids(LOCAL_USER), vec![private, public]);
694
695        let row = queries::get_playlist(&db.conn, public).unwrap().unwrap();
696        assert!(row.readable_by(mate) && !row.editable_by(mate));
697        assert_eq!(row.owner.as_deref(), Some("owner"));
698        let row = queries::get_playlist(&db.conn, private).unwrap().unwrap();
699        assert!(!row.readable_by(mate));
700    }
701
702    #[test]
703    fn deleting_an_account_takes_its_data_with_it() {
704        let (db, _tmp) = test_db();
705        let admin = create_user(&db.conn, "owner", "pw", Role::Admin).unwrap();
706        let mate = create_user(&db.conn, "mate", "pw", Role::User).unwrap();
707        let track = upsert_track(&db.conn, &sample_meta("T", "A", "B")).unwrap();
708        for user in [admin, mate] {
709            queries::add_favourite(&db.conn, user, Path::new("/music/a.flac")).unwrap();
710            queries::set_favourite_album(&db.conn, user, "A", "B", true).unwrap();
711            queries::set_favourite_artist(&db.conn, user, "A", true).unwrap();
712            queries::record_play(&db.conn, user, track, None).unwrap();
713            queries::create_playlist(&db.conn, user, "List", None).unwrap();
714            queries::shares::create_share(
715                &db.conn,
716                user,
717                queries::shares::Slice::TRACKS,
718                &[track],
719                None,
720                0,
721                None,
722            )
723            .unwrap();
724        }
725
726        assert!(delete_user(&db.conn, mate).unwrap());
727
728        for table in [
729            "favourites",
730            "favourite_albums",
731            "favourite_artists",
732            "play_history",
733            "playlists",
734            "shares",
735        ] {
736            assert_eq!(
737                count(
738                    &db,
739                    &format!("SELECT COUNT(*) FROM {table} WHERE user_id = {mate}")
740                ),
741                0,
742                "{table} kept the deleted account's rows"
743            );
744            assert_eq!(
745                count(
746                    &db,
747                    &format!("SELECT COUNT(*) FROM {table} WHERE user_id = {admin}")
748                ),
749                1,
750                "{table} lost another account's rows"
751            );
752        }
753    }
754}