Seven test modules each built a migrated in-memory store by hand: open, migrate, and (in sharing) wrap it in a Db. local::memory_conn() is that, and open_in_memory uses it too. Each module's db() is now one line, and the schema, Connection and Mutex imports it needed are gone. DRY pass #2, batch 2, F9 (#5372). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
4.4 KiB
Rust
135 lines
4.4 KiB
Rust
//! Sharing a note from a linked device (#5175).
|
|
//!
|
|
//! Shares belong to the server: who is on the instance and who a note is shared
|
|
//! with are never kept here, so each call goes straight to the server over the
|
|
//! device token. The one thing kept locally is the note's `shared` flag, set from
|
|
//! the server's answer so the card's chip changes at once rather than at the next
|
|
//! sync (which brings the same value).
|
|
//!
|
|
//! The server address and token come from the CALLER, read with
|
|
//! `state::credentials` and the caller's seal. A stored token may be sealed
|
|
//! (Android keeps it under a Keystore key), and reading it raw here sent `sealed:…`
|
|
//! as the bearer token, so every share call was refused (#5381).
|
|
|
|
use rusqlite::params;
|
|
|
|
use super::client::{self, Directory, NoteShare, ShareTarget};
|
|
use super::state;
|
|
use crate::local::Db;
|
|
|
|
/// What an unlinked device says when asked to share. Sharing is between accounts on
|
|
/// a server, so there is nothing to do offline and nothing worth queueing.
|
|
pub const NEEDS_SERVER: &str =
|
|
"Sharing is between people on a server. Link this device to one in Sync to share notes.";
|
|
|
|
/// The link to share over, or [`NEEDS_SERVER`]. For a client that keeps its token
|
|
/// plain, as the desktop does; one with a seal reads `state::credentials` itself.
|
|
pub fn stored_link(db: &Db) -> Result<(String, String), String> {
|
|
let conn = db.conn()?;
|
|
let link = state::credentials(&conn, None).map_err(|e| e.to_string())?;
|
|
link.ok_or_else(|| NEEDS_SERVER.to_string())
|
|
}
|
|
|
|
/// Set the local note's `shared` flag from the server's list of its shares. Not a
|
|
/// local edit, so it leaves `dirty` and `updated_at` alone.
|
|
fn mark_shared(db: &Db, note_id: &str, shares: &[NoteShare]) -> Result<(), String> {
|
|
let conn = db.conn()?;
|
|
conn.execute(
|
|
"UPDATE notes SET shared = ?2 WHERE id = ?1 AND permission = 'owner'",
|
|
params![note_id, !shares.is_empty()],
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn directory(url: &str, token: &str) -> Result<Directory, String> {
|
|
client::directory(url, token).await
|
|
}
|
|
|
|
pub async fn list(
|
|
db: &Db,
|
|
url: &str,
|
|
token: &str,
|
|
note_id: &str,
|
|
) -> Result<Vec<NoteShare>, String> {
|
|
let shares = client::list_shares(url, token, note_id).await?;
|
|
mark_shared(db, note_id, &shares)?;
|
|
Ok(shares)
|
|
}
|
|
|
|
pub async fn share(
|
|
db: &Db,
|
|
url: &str,
|
|
token: &str,
|
|
note_id: &str,
|
|
target: &ShareTarget,
|
|
permission: &str,
|
|
) -> Result<Vec<NoteShare>, String> {
|
|
let shares = client::share_note(url, token, note_id, target, permission).await?;
|
|
mark_shared(db, note_id, &shares)?;
|
|
Ok(shares)
|
|
}
|
|
|
|
pub async fn unshare(
|
|
db: &Db,
|
|
url: &str,
|
|
token: &str,
|
|
note_id: &str,
|
|
share_id: &str,
|
|
) -> Result<Vec<NoteShare>, String> {
|
|
let shares = client::unshare_note(url, token, note_id, share_id).await?;
|
|
mark_shared(db, note_id, &shares)?;
|
|
Ok(shares)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::sync::client::Member;
|
|
|
|
fn db() -> Db {
|
|
crate::local::open_in_memory().expect("in-memory db")
|
|
}
|
|
|
|
#[test]
|
|
fn an_unlinked_device_explains_that_sharing_needs_a_server() {
|
|
assert_eq!(stored_link(&db()).unwrap_err(), NEEDS_SERVER);
|
|
}
|
|
|
|
#[test]
|
|
fn the_shared_flag_follows_the_servers_answer_without_dirtying_the_note() {
|
|
let db = db();
|
|
{
|
|
let conn = db.conn().unwrap();
|
|
conn.execute(
|
|
"INSERT INTO notes (id, body, created_at, updated_at, dirty)
|
|
VALUES ('n1', 'x', '2026-01-01', '2026-01-01', 0)",
|
|
[],
|
|
)
|
|
.unwrap();
|
|
}
|
|
let one = NoteShare {
|
|
id: "s1".into(),
|
|
member: Some(Member {
|
|
id: "u2".into(),
|
|
display_name: "Sam".into(),
|
|
email: "sam@example.test".into(),
|
|
}),
|
|
group: None,
|
|
permission: "view".into(),
|
|
created_at: None,
|
|
};
|
|
let read = |db: &Db| -> (bool, i64) {
|
|
let conn = db.conn().unwrap();
|
|
conn.query_row("SELECT shared, dirty FROM notes WHERE id = 'n1'", [], |r| {
|
|
Ok((r.get(0)?, r.get(1)?))
|
|
})
|
|
.unwrap()
|
|
};
|
|
mark_shared(&db, "n1", &[one]).unwrap();
|
|
assert_eq!(read(&db), (true, 0));
|
|
mark_shared(&db, "n1", &[]).unwrap();
|
|
assert_eq!(read(&db), (false, 0));
|
|
}
|
|
}
|