Author: Manos Pitsidianakis [manos@pitsidianak.is]
Hash: 21c9fb9586c6857769eddcde80c9221b095be353
Timestamp: Tue, 25 Apr 2023 11:52:59 +0000 (1 year ago)

+296 -45 +/-3 browse
core/db/subscriptions.rs: add subscr ops tests
1diff --git a/core/src/db/subscriptions.rs b/core/src/db/subscriptions.rs
2index e2770e0..1591dde 100644
3--- a/core/src/db/subscriptions.rs
4+++ b/core/src/db/subscriptions.rs
5 @@ -134,7 +134,7 @@ impl Connection {
6 receive_confirmation) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *;",
7 )
8 .unwrap();
9- let ret = stmt.query_row(
10+ let val = stmt.query_row(
11 rusqlite::params![
12 &new_val.list,
13 &new_val.address,
14 @@ -169,14 +169,14 @@ impl Connection {
15 ))
16 },
17 )?;
18-
19- trace!("add_subscription {:?}.", &ret);
20- Ok(ret)
21+ trace!("add_subscription {:?}.", &val);
22+ // table entry might be modified by triggers, so don't rely on RETURNING value.
23+ self.list_subscription(list_pk, val.pk())
24 }
25
26 /// Create subscription candidate.
27 pub fn add_candidate_subscription(
28- &mut self,
29+ &self,
30 list_pk: i64,
31 mut new_val: ListSubscription,
32 ) -> Result<DbVal<ListCandidateSubscription>> {
33 @@ -185,7 +185,7 @@ impl Connection {
34 "INSERT INTO candidate_subscription(list, address, name, accepted) VALUES(?, ?, ?, ?) \
35 RETURNING *;",
36 )?;
37- let ret = stmt.query_row(
38+ let val = stmt.query_row(
39 rusqlite::params![&new_val.list, &new_val.address, &new_val.name, None::<i64>,],
40 |row| {
41 let pk = row.get("pk")?;
42 @@ -203,47 +203,79 @@ impl Connection {
43 )?;
44 drop(stmt);
45
46- trace!("add_candidate_subscription {:?}.", &ret);
47- Ok(ret)
48+ trace!("add_candidate_subscription {:?}.", &val);
49+ // table entry might be modified by triggers, so don't rely on RETURNING value.
50+ self.candidate_subscription(val.pk())
51+ }
52+
53+ /// Fetch subscription candidate by primary key.
54+ pub fn candidate_subscription(&self, pk: i64) -> Result<DbVal<ListCandidateSubscription>> {
55+ let mut stmt = self
56+ .connection
57+ .prepare("SELECT * FROM candidate_subscription WHERE pk = ?;")?;
58+ let val = stmt
59+ .query_row(rusqlite::params![&pk], |row| {
60+ let _pk: i64 = row.get("pk")?;
61+ debug_assert_eq!(pk, _pk);
62+ Ok(DbVal(
63+ ListCandidateSubscription {
64+ pk,
65+ list: row.get("list")?,
66+ address: row.get("address")?,
67+ name: row.get("name")?,
68+ accepted: row.get("accepted")?,
69+ },
70+ pk,
71+ ))
72+ })
73+ .map_err(|err| {
74+ if matches!(err, rusqlite::Error::QueryReturnedNoRows) {
75+ Error::from(err)
76+ .chain_err(|| NotFound("Candidate subscription with this pk not found!"))
77+ } else {
78+ err.into()
79+ }
80+ })?;
81+
82+ Ok(val)
83 }
84
85 /// Accept subscription candidate.
86 pub fn accept_candidate_subscription(&mut self, pk: i64) -> Result<DbVal<ListSubscription>> {
87- let tx = self.connection.transaction()?;
88- let mut stmt = tx.prepare(
89+ let val = self.connection.query_row(
90 "INSERT INTO subscription(list, address, name, enabled, digest, verified, \
91 hide_address, receive_duplicates, receive_own_posts, receive_confirmation) SELECT \
92 list, address, name, 1, 0, 0, 0, 1, 1, 0 FROM candidate_subscription WHERE pk = ? \
93 RETURNING *;",
94- )?;
95- let ret = stmt.query_row(rusqlite::params![&pk], |row| {
96- let pk = row.get("pk")?;
97- Ok(DbVal(
98- ListSubscription {
99+ rusqlite::params![&pk],
100+ |row| {
101+ let pk = row.get("pk")?;
102+ Ok(DbVal(
103+ ListSubscription {
104+ pk,
105+ list: row.get("list")?,
106+ address: row.get("address")?,
107+ account: row.get("account")?,
108+ name: row.get("name")?,
109+ digest: row.get("digest")?,
110+ enabled: row.get("enabled")?,
111+ verified: row.get("verified")?,
112+ hide_address: row.get("hide_address")?,
113+ receive_duplicates: row.get("receive_duplicates")?,
114+ receive_own_posts: row.get("receive_own_posts")?,
115+ receive_confirmation: row.get("receive_confirmation")?,
116+ },
117 pk,
118- list: row.get("list")?,
119- address: row.get("address")?,
120- account: row.get("account")?,
121- name: row.get("name")?,
122- digest: row.get("digest")?,
123- enabled: row.get("enabled")?,
124- verified: row.get("verified")?,
125- hide_address: row.get("hide_address")?,
126- receive_duplicates: row.get("receive_duplicates")?,
127- receive_own_posts: row.get("receive_own_posts")?,
128- receive_confirmation: row.get("receive_confirmation")?,
129- },
130- pk,
131- ))
132- })?;
133- drop(stmt);
134- tx.execute(
135- "UPDATE candidate_subscription SET accepted = ? WHERE pk = ?;",
136- [&ret.pk, &pk],
137+ ))
138+ },
139 )?;
140- tx.commit()?;
141
142- trace!("accept_candidate_subscription {:?}.", &ret);
143+ trace!("accept_candidate_subscription {:?}.", &val);
144+ // table entry might be modified by triggers, so don't rely on RETURNING value.
145+ let ret = self.list_subscription(val.list, val.pk())?;
146+
147+ // assert that [ref:accept_candidate] trigger works.
148+ debug_assert_eq!(Some(ret.pk), self.candidate_subscription(pk)?.accepted);
149 Ok(ret)
150 }
151
152 @@ -553,3 +585,186 @@ impl Connection {
153 Ok(())
154 }
155 }
156+
157+ #[cfg(test)]
158+ mod tests {
159+ use super::*;
160+
161+ #[test]
162+ fn test_subscription_ops() {
163+ use tempfile::TempDir;
164+
165+ let tmp_dir = TempDir::new().unwrap();
166+ let db_path = tmp_dir.path().join("mpot.db");
167+ let config = Configuration {
168+ send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
169+ db_path,
170+ data_path: tmp_dir.path().to_path_buf(),
171+ administrators: vec![],
172+ };
173+
174+ let mut db = Connection::open_or_create_db(config).unwrap().trusted();
175+ let list = db
176+ .create_list(MailingList {
177+ pk: -1,
178+ name: "foobar chat".into(),
179+ id: "foo-chat".into(),
180+ address: "foo-chat@example.com".into(),
181+ description: None,
182+ archive_url: None,
183+ })
184+ .unwrap();
185+ let secondary_list = db
186+ .create_list(MailingList {
187+ pk: -1,
188+ name: "foobar chat2".into(),
189+ id: "foo-chat2".into(),
190+ address: "foo-chat2@example.com".into(),
191+ description: None,
192+ archive_url: None,
193+ })
194+ .unwrap();
195+ for i in 0..4 {
196+ let sub = db
197+ .add_subscription(
198+ list.pk(),
199+ ListSubscription {
200+ pk: -1,
201+ list: list.pk(),
202+ address: format!("{i}@example.com"),
203+ account: None,
204+ name: Some(format!("User{i}")),
205+ digest: false,
206+ hide_address: false,
207+ receive_duplicates: false,
208+ receive_own_posts: false,
209+ receive_confirmation: false,
210+ enabled: true,
211+ verified: false,
212+ },
213+ )
214+ .unwrap();
215+ assert_eq!(db.list_subscription(list.pk(), sub.pk()).unwrap(), sub);
216+ assert_eq!(
217+ db.list_subscription_by_address(list.pk(), &sub.address)
218+ .unwrap(),
219+ sub
220+ );
221+ }
222+
223+ assert_eq!(db.accounts().unwrap(), vec![]);
224+ assert_eq!(
225+ db.remove_subscription(list.pk(), "nonexistent@example.com")
226+ .map_err(|err| err.to_string())
227+ .unwrap_err(),
228+ NotFound("list or list owner not found!").to_string()
229+ );
230+
231+ let cand = db
232+ .add_candidate_subscription(
233+ list.pk(),
234+ ListSubscription {
235+ pk: -1,
236+ list: list.pk(),
237+ address: "4@example.com".into(),
238+ account: None,
239+ name: Some("User4".into()),
240+ digest: false,
241+ hide_address: false,
242+ receive_duplicates: false,
243+ receive_own_posts: false,
244+ receive_confirmation: false,
245+ enabled: true,
246+ verified: false,
247+ },
248+ )
249+ .unwrap();
250+ let accepted = db.accept_candidate_subscription(cand.pk()).unwrap();
251+
252+ assert_eq!(db.account(5).unwrap(), None);
253+ assert_eq!(
254+ db.remove_account("4@example.com")
255+ .map_err(|err| err.to_string())
256+ .unwrap_err(),
257+ NotFound("account not found!").to_string()
258+ );
259+
260+ let acc = db
261+ .add_account(Account {
262+ pk: -1,
263+ name: accepted.name.clone(),
264+ address: accepted.address.clone(),
265+ public_key: None,
266+ password: String::new(),
267+ enabled: true,
268+ })
269+ .unwrap();
270+
271+ // Test [ref:add_account] SQL trigger (see schema.sql)
272+ assert_eq!(
273+ db.list_subscription(list.pk(), accepted.pk())
274+ .unwrap()
275+ .account,
276+ Some(acc.pk())
277+ );
278+ // Test [ref:add_account_to_subscription] SQL trigger (see schema.sql)
279+ let sub = db
280+ .add_subscription(
281+ secondary_list.pk(),
282+ ListSubscription {
283+ pk: -1,
284+ list: secondary_list.pk(),
285+ address: "4@example.com".into(),
286+ account: None,
287+ name: Some("User4".into()),
288+ digest: false,
289+ hide_address: false,
290+ receive_duplicates: false,
291+ receive_own_posts: false,
292+ receive_confirmation: false,
293+ enabled: true,
294+ verified: true,
295+ },
296+ )
297+ .unwrap();
298+ assert_eq!(sub.account, Some(acc.pk()));
299+ // Test [ref:verify_subscription_email] SQL trigger (see schema.sql)
300+ assert!(!sub.verified);
301+
302+ assert_eq!(db.accounts().unwrap(), vec![acc.clone()]);
303+
304+ assert_eq!(
305+ db.update_account(AccountChangeset {
306+ address: "nonexistent@example.com".into(),
307+ ..AccountChangeset::default()
308+ })
309+ .map_err(|err| err.to_string())
310+ .unwrap_err(),
311+ NotFound("account with this address not found!").to_string()
312+ );
313+ assert_eq!(
314+ db.update_account(AccountChangeset {
315+ address: acc.address.clone(),
316+ ..AccountChangeset::default()
317+ })
318+ .map_err(|err| err.to_string()),
319+ Ok(())
320+ );
321+ assert_eq!(
322+ db.update_account(AccountChangeset {
323+ address: acc.address.clone(),
324+ enabled: Some(Some(false)),
325+ ..AccountChangeset::default()
326+ })
327+ .map_err(|err| err.to_string()),
328+ Ok(())
329+ );
330+ assert!(!db.account(acc.pk()).unwrap().unwrap().enabled);
331+ assert_eq!(
332+ db.remove_account("4@example.com")
333+ .map_err(|err| err.to_string()),
334+ Ok(())
335+ );
336+ assert_eq!(db.accounts().unwrap(), vec![]);
337+ }
338+ }
339 diff --git a/core/src/schema.sql b/core/src/schema.sql
340index c5928c7..d556c54 100644
341--- a/core/src/schema.sql
342+++ b/core/src/schema.sql
343 @@ -179,6 +179,7 @@ CREATE INDEX IF NOT EXISTS post_msgid_idx ON post(message_id);
344 CREATE INDEX IF NOT EXISTS list_idx ON list(id);
345 CREATE INDEX IF NOT EXISTS subscription_idx ON subscription(address);
346
347+ -- [tag:accept_candidate]: Update candidacy with 'subscription' foreign key on 'subscription' insert.
348 CREATE TRIGGER IF NOT EXISTS accept_candidate AFTER INSERT ON subscription
349 FOR EACH ROW
350 BEGIN
351 @@ -186,13 +187,17 @@ BEGIN
352 WHERE candidate_subscription.list = NEW.list AND candidate_subscription.address = NEW.address;
353 END;
354
355- CREATE TRIGGER IF NOT EXISTS verify_candidate AFTER INSERT ON subscription
356+ -- [tag:verify_subscription_email]: If list settings require e-mail to be verified,
357+ -- update new subscription's 'verify' column value.
358+ CREATE TRIGGER IF NOT EXISTS verify_subscription_email AFTER INSERT ON subscription
359 FOR EACH ROW
360 BEGIN
361 UPDATE subscription SET verified = 0, last_modified = unixepoch()
362 WHERE subscription.pk = NEW.pk AND EXISTS (SELECT 1 FROM list WHERE pk = NEW.list AND verify = 1);
363 END;
364
365+ -- [tag:add_account]: Update list subscription entries with 'account' foreign
366+ -- key, if addresses match.
367 CREATE TRIGGER IF NOT EXISTS add_account AFTER INSERT ON account
368 FOR EACH ROW
369 BEGIN
370 @@ -200,67 +205,86 @@ BEGIN
371 WHERE subscription.address = NEW.address;
372 END;
373
374+ -- [tag:add_account_to_subscription]: When adding a new 'subscription', auto
375+ -- set 'account' value if there already exists an 'account' entry with the same
376+ -- address.
377 CREATE TRIGGER IF NOT EXISTS add_account_to_subscription AFTER INSERT ON subscription
378 FOR EACH ROW
379+ WHEN NEW.account IS NULL AND EXISTS (SELECT 1 FROM account WHERE address = NEW.address)
380 BEGIN
381 UPDATE subscription
382- SET account = acc.pk,
383+ SET account = (SELECT pk FROM account WHERE address = NEW.address),
384 last_modified = unixepoch()
385- FROM (SELECT * FROM account) AS acc
386- WHERE subscription.account = acc.address;
387+ WHERE subscription.pk = NEW.pk;
388 END;
389
390+ -- [tag:last_modified_list] update last_modified on every change.
391 CREATE TRIGGER IF NOT EXISTS last_modified_list AFTER UPDATE ON list
392 FOR EACH ROW
393+ WHEN NEW.last_modified != OLD.last_modified
394 BEGIN
395 UPDATE list SET last_modified = unixepoch()
396 WHERE pk = NEW.pk;
397 END;
398
399+ -- [tag:last_modified_owner] update last_modified on every change.
400 CREATE TRIGGER IF NOT EXISTS last_modified_owner AFTER UPDATE ON owner
401 FOR EACH ROW
402+ WHEN NEW.last_modified != OLD.last_modified
403 BEGIN
404 UPDATE owner SET last_modified = unixepoch()
405 WHERE pk = NEW.pk;
406 END;
407
408+ -- [tag:last_modified_post_policy] update last_modified on every change.
409 CREATE TRIGGER IF NOT EXISTS last_modified_post_policy AFTER UPDATE ON post_policy
410 FOR EACH ROW
411+ WHEN NEW.last_modified != OLD.last_modified
412 BEGIN
413 UPDATE post_policy SET last_modified = unixepoch()
414 WHERE pk = NEW.pk;
415 END;
416
417+ -- [tag:last_modified_subscription_policy] update last_modified on every change.
418 CREATE TRIGGER IF NOT EXISTS last_modified_subscription_policy AFTER UPDATE ON subscription_policy
419 FOR EACH ROW
420+ WHEN NEW.last_modified != OLD.last_modified
421 BEGIN
422 UPDATE subscription_policy SET last_modified = unixepoch()
423 WHERE pk = NEW.pk;
424 END;
425
426+ -- [tag:last_modified_subscription] update last_modified on every change.
427 CREATE TRIGGER IF NOT EXISTS last_modified_subscription AFTER UPDATE ON subscription
428 FOR EACH ROW
429+ WHEN NEW.last_modified != OLD.last_modified
430 BEGIN
431 UPDATE subscription SET last_modified = unixepoch()
432 WHERE pk = NEW.pk;
433 END;
434
435+ -- [tag:last_modified_account] update last_modified on every change.
436 CREATE TRIGGER IF NOT EXISTS last_modified_account AFTER UPDATE ON account
437 FOR EACH ROW
438+ WHEN NEW.last_modified != OLD.last_modified
439 BEGIN
440 UPDATE account SET last_modified = unixepoch()
441 WHERE pk = NEW.pk;
442 END;
443
444+ -- [tag:last_modified_candidate_subscription] update last_modified on every change.
445 CREATE TRIGGER IF NOT EXISTS last_modified_candidate_subscription AFTER UPDATE ON candidate_subscription
446 FOR EACH ROW
447+ WHEN NEW.last_modified != OLD.last_modified
448 BEGIN
449 UPDATE candidate_subscription SET last_modified = unixepoch()
450 WHERE pk = NEW.pk;
451 END;
452
453+ -- [tag:last_modified_templates] update last_modified on every change.
454 CREATE TRIGGER IF NOT EXISTS last_modified_templates AFTER UPDATE ON templates
455 FOR EACH ROW
456+ WHEN NEW.last_modified != OLD.last_modified
457 BEGIN
458 UPDATE templates SET last_modified = unixepoch()
459 WHERE pk = NEW.pk;
460 diff --git a/core/src/schema.sql.m4 b/core/src/schema.sql.m4
461index f1df735..32df925 100644
462--- a/core/src/schema.sql.m4
463+++ b/core/src/schema.sql.m4
464 @@ -2,8 +2,12 @@ define(xor, `(($1) OR ($2)) AND NOT (($1) AND ($2))')dnl
465 define(BOOLEAN_TYPE, `$1 BOOLEAN CHECK ($1 in (0, 1)) NOT NULL')dnl
466 define(BOOLEAN_FALSE, `0')dnl
467 define(BOOLEAN_TRUE, `1')dnl
468- define(update_last_modified, `CREATE TRIGGER IF NOT EXISTS last_modified_$1 AFTER UPDATE ON $1
469+ define(__TAG, `tag')dnl # Write the string '['+'tag'+':'+... with a macro so that tagref check doesn't pick up on it as a duplicate.
470+ define(TAG, `['__TAG()`:$1]')dnl
471+ define(update_last_modified, `-- 'TAG(last_modified_$1)` update last_modified on every change.
472+ CREATE TRIGGER IF NOT EXISTS last_modified_$1 AFTER UPDATE ON $1
473 FOR EACH ROW
474+ WHEN NEW.last_modified != OLD.last_modified
475 BEGIN
476 UPDATE $1 SET last_modified = unixepoch()
477 WHERE pk = NEW.pk;
478 @@ -189,6 +193,7 @@ CREATE INDEX IF NOT EXISTS post_msgid_idx ON post(message_id);
479 CREATE INDEX IF NOT EXISTS list_idx ON list(id);
480 CREATE INDEX IF NOT EXISTS subscription_idx ON subscription(address);
481
482+ -- TAG(accept_candidate): Update candidacy with 'subscription' foreign key on 'subscription' insert.
483 CREATE TRIGGER IF NOT EXISTS accept_candidate AFTER INSERT ON subscription
484 FOR EACH ROW
485 BEGIN
486 @@ -196,13 +201,17 @@ BEGIN
487 WHERE candidate_subscription.list = NEW.list AND candidate_subscription.address = NEW.address;
488 END;
489
490- CREATE TRIGGER IF NOT EXISTS verify_candidate AFTER INSERT ON subscription
491+ -- TAG(verify_subscription_email): If list settings require e-mail to be verified,
492+ -- update new subscription's 'verify' column value.
493+ CREATE TRIGGER IF NOT EXISTS verify_subscription_email AFTER INSERT ON subscription
494 FOR EACH ROW
495 BEGIN
496 UPDATE subscription SET verified = BOOLEAN_FALSE(), last_modified = unixepoch()
497 WHERE subscription.pk = NEW.pk AND EXISTS (SELECT 1 FROM list WHERE pk = NEW.list AND verify = BOOLEAN_TRUE());
498 END;
499
500+ -- TAG(add_account): Update list subscription entries with 'account' foreign
501+ -- key, if addresses match.
502 CREATE TRIGGER IF NOT EXISTS add_account AFTER INSERT ON account
503 FOR EACH ROW
504 BEGIN
505 @@ -210,14 +219,17 @@ BEGIN
506 WHERE subscription.address = NEW.address;
507 END;
508
509+ -- TAG(add_account_to_subscription): When adding a new 'subscription', auto
510+ -- set 'account' value if there already exists an 'account' entry with the same
511+ -- address.
512 CREATE TRIGGER IF NOT EXISTS add_account_to_subscription AFTER INSERT ON subscription
513 FOR EACH ROW
514+ WHEN NEW.account IS NULL AND EXISTS (SELECT 1 FROM account WHERE address = NEW.address)
515 BEGIN
516 UPDATE subscription
517- SET account = acc.pk,
518+ SET account = (SELECT pk FROM account WHERE address = NEW.address),
519 last_modified = unixepoch()
520- FROM (SELECT * FROM account) AS acc
521- WHERE subscription.account = acc.address;
522+ WHERE subscription.pk = NEW.pk;
523 END;
524
525 update_last_modified(`list')