Format the Rust code with rustfmt
The code was formatted by hand, and `cargo fmt --check` differed in 866 places. rustfmt.toml keeps the house's long lines (120) and short expressions on one line, which halves what changes; this commit is only what rustfmt does with it, and is named in .git-blame-ignore-revs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
//! The database, through SeaORM: SQLite today, Postgres to come (issue #18). Replaces the
|
||||
//! per-feed .ipxd plists, history.dat and qmcache.dat.
|
||||
|
||||
use crate::entity::{
|
||||
api_tokens, blocklists, catalogue, deadair_prefs, enclosures, entries, feeds, hidden, sessions, settings,
|
||||
signal_verdicts, signals, subscriptions, users,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use crate::entity::{api_tokens, blocklists, catalogue, deadair_prefs, enclosures, entries, feeds, hidden, sessions, settings, signal_verdicts, signals, subscriptions, users};
|
||||
use sea_orm::sea_query::{Expr, Func};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, Set,
|
||||
@@ -94,10 +97,9 @@ async fn create_missing(orm: &sea_orm::DatabaseConnection) -> Result<()> {
|
||||
// A feed is read whole only when it has changed since the last read, so each one's
|
||||
// description would wait for its next item. Forgetting how each was last seen makes
|
||||
// its next read, when it is due anyway, a whole one.
|
||||
for sql in [
|
||||
"ALTER TABLE feeds ADD COLUMN description TEXT",
|
||||
"UPDATE feeds SET etag = NULL, last_modified = NULL",
|
||||
] {
|
||||
for sql in
|
||||
["ALTER TABLE feeds ADD COLUMN description TEXT", "UPDATE feeds SET etag = NULL, last_modified = NULL"]
|
||||
{
|
||||
orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?;
|
||||
}
|
||||
}
|
||||
@@ -125,7 +127,10 @@ async fn create_missing(orm: &sea_orm::DatabaseConnection) -> Result<()> {
|
||||
// Postgres guessed a third of entries were recent and read them all, entries_recent unused
|
||||
// (#274). Analysed once when they are missing; autovacuum keeps them after.
|
||||
if orm.get_database_backend() == sea_orm::DbBackend::Postgres {
|
||||
let has = Statement::from_string(sea_orm::DbBackend::Postgres, "SELECT 1 FROM pg_stats WHERE tablename = 'entries_recent'");
|
||||
let has = Statement::from_string(
|
||||
sea_orm::DbBackend::Postgres,
|
||||
"SELECT 1 FROM pg_stats WHERE tablename = 'entries_recent'",
|
||||
);
|
||||
if orm.query_one_raw(has).await?.is_none() {
|
||||
orm.execute_unprepared("ANALYZE entries").await.context("analysing entries")?;
|
||||
}
|
||||
@@ -222,9 +227,7 @@ async fn connect(location: &str, keep_open: bool) -> Result<sea_orm::DatabaseCon
|
||||
if keep_open && is_postgres(location) {
|
||||
opts.min_connections(20);
|
||||
}
|
||||
sea_orm::Database::connect(opts)
|
||||
.await
|
||||
.with_context(|| format!("opening {}", redact(location)))
|
||||
sea_orm::Database::connect(opts).await.with_context(|| format!("opening {}", redact(location)))
|
||||
}
|
||||
|
||||
/// The URL `connect` hands sqlx for a location: a SQLite file, created if missing, or a Postgres
|
||||
@@ -244,7 +247,6 @@ fn url_for(location: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// One person's wants for one feed. `None` in a field means the feed's own setting stands.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Sub {
|
||||
@@ -330,7 +332,6 @@ pub struct Managed {
|
||||
pub group_id: String,
|
||||
}
|
||||
|
||||
|
||||
/// What `ipx list` shows next to each configured feed.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FeedSummary {
|
||||
@@ -414,9 +415,11 @@ impl Db {
|
||||
admin.execute_unprepared(&format!("CREATE SCHEMA {schema}")).await?;
|
||||
// Every connection in the pool starts in it, so each test sees only its own tables.
|
||||
let sep = if url.contains('?') { '&' } else { '?' };
|
||||
let orm =
|
||||
connect(&format!("{url}{sep}options=-c%20search_path%3D{schema}%20-c%20client_min_messages%3Dwarning"), false)
|
||||
.await?;
|
||||
let orm = connect(
|
||||
&format!("{url}{sep}options=-c%20search_path%3D{schema}%20-c%20client_min_messages%3Dwarning"),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
create_missing(&orm).await?;
|
||||
return Ok(Self { orm, tmp: None });
|
||||
}
|
||||
@@ -530,9 +533,9 @@ impl Db {
|
||||
step("blocked"),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|b| (b.feed_id, keywords(Some(b.words)).unwrap_or_default()))
|
||||
.collect();
|
||||
.into_iter()
|
||||
.map(|b| (b.feed_id, keywords(Some(b.words)).unwrap_or_default()))
|
||||
.collect();
|
||||
let mut rows = feeds::Entity::find();
|
||||
if let Some(id) = only {
|
||||
rows = rows.filter(feeds::Column::Id.eq(id));
|
||||
@@ -574,7 +577,6 @@ pub struct FeedListing {
|
||||
pub unread: i64,
|
||||
}
|
||||
|
||||
|
||||
/// Everything `fetch` needs to decide whether to poll a feed, and how.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct HttpState {
|
||||
@@ -611,7 +613,12 @@ impl Db {
|
||||
/// Every feed's, in one query: a scan, and the daemon working out when the next is due, asked
|
||||
/// feed by feed, 180 queries a minute (#114).
|
||||
pub async fn http_states(&self) -> Result<std::collections::HashMap<String, HttpState>> {
|
||||
Ok(feeds::Entity::find().all(&self.orm).await?.into_iter().map(|f| (f.id.clone(), HttpState::from(f))).collect())
|
||||
Ok(feeds::Entity::find()
|
||||
.all(&self.orm)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|f| (f.id.clone(), HttpState::from(f)))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Upsert after a successful poll. Clears any previous error.
|
||||
@@ -786,30 +793,18 @@ impl Db {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn record_enclosure(
|
||||
&self,
|
||||
feed_id: &str,
|
||||
guid: &str,
|
||||
enc: &crate::feed::Enclosure,
|
||||
) -> Result<bool> {
|
||||
pub async fn record_enclosure(&self, feed_id: &str, guid: &str, enc: &crate::feed::Enclosure) -> Result<bool> {
|
||||
let inserted = self
|
||||
.exec(
|
||||
"INSERT INTO enclosures (feed_id, guid, url, mime, length, state)
|
||||
VALUES ($1, $2, $3, $4, $5, 'pending') ON CONFLICT DO NOTHING",
|
||||
vec![
|
||||
feed_id.into(),
|
||||
guid.into(),
|
||||
enc.url.clone().into(),
|
||||
enc.mime.clone().into(),
|
||||
enc.length.into(),
|
||||
],
|
||||
vec![feed_id.into(), guid.into(), enc.url.clone().into(), enc.mime.clone().into(), enc.length.into()],
|
||||
)
|
||||
.await?;
|
||||
Ok(inserted == 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// An enclosure waiting to be downloaded.
|
||||
#[derive(Debug)]
|
||||
pub struct Pending {
|
||||
@@ -848,7 +843,9 @@ impl Db {
|
||||
.await?;
|
||||
// And back, when a limit is raised or a feed downloads again.
|
||||
self.exec(
|
||||
&format!("UPDATE enclosures SET state = 'pending' WHERE feed_id = $1 AND state = 'held' AND guid IN ({newest})"),
|
||||
&format!(
|
||||
"UPDATE enclosures SET state = 'pending' WHERE feed_id = $1 AND state = 'held' AND guid IN ({newest})"
|
||||
),
|
||||
vec![feed_id.into(), limit],
|
||||
)
|
||||
.await?;
|
||||
@@ -881,7 +878,6 @@ impl Db {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A downloaded file the reaper may consider.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Candidate {
|
||||
@@ -1019,7 +1015,9 @@ impl Db {
|
||||
for table in ["entries", "feeds"] {
|
||||
let col = if table == "feeds" { "id" } else { "feed_id" };
|
||||
self.exec(
|
||||
&format!("UPDATE {table} SET image = 'https://' || substr(image, 8) WHERE {col} = $1 AND image LIKE $2"),
|
||||
&format!(
|
||||
"UPDATE {table} SET image = 'https://' || substr(image, 8) WHERE {col} = $1 AND image LIKE $2"
|
||||
),
|
||||
vec![feed_id.into(), like.clone().into()],
|
||||
)
|
||||
.await?;
|
||||
@@ -1108,19 +1106,14 @@ impl Db {
|
||||
|
||||
/// (pending, downloaded) across all feeds, for the status command.
|
||||
pub async fn counts(&self) -> Result<(i64, i64)> {
|
||||
let pending = enclosures::Entity::find()
|
||||
.filter(enclosures::Column::State.eq("pending"))
|
||||
.count(&self.orm)
|
||||
.await?;
|
||||
let downloaded = enclosures::Entity::find()
|
||||
.filter(enclosures::Column::Path.is_not_null())
|
||||
.count(&self.orm)
|
||||
.await?;
|
||||
let pending =
|
||||
enclosures::Entity::find().filter(enclosures::Column::State.eq("pending")).count(&self.orm).await?;
|
||||
let downloaded =
|
||||
enclosures::Entity::find().filter(enclosures::Column::Path.is_not_null()).count(&self.orm).await?;
|
||||
Ok((pending as i64, downloaded as i64))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// An entry plus its enclosures, for the web UI.
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct EntryRow {
|
||||
@@ -1182,9 +1175,7 @@ fn entries_from(
|
||||
let search = match search.map(|q| q.trim().to_lowercase()).filter(|q| !q.is_empty()) {
|
||||
Some(q) => {
|
||||
let like = a.p(format!("%{q}%"));
|
||||
format!(
|
||||
"(lower(coalesce(e.title, '')) LIKE {like} OR lower(coalesce(e.description, '')) LIKE {like})"
|
||||
)
|
||||
format!("(lower(coalesce(e.title, '')) LIKE {like} OR lower(coalesce(e.description, '')) LIKE {like})")
|
||||
}
|
||||
None => "true".into(),
|
||||
};
|
||||
@@ -1340,7 +1331,8 @@ impl Db {
|
||||
|
||||
pub async fn signal_count(&self, user_id: i64, signal: i64, filter: Filter, search: Option<&str>) -> Result<i64> {
|
||||
let mut a = Args::default();
|
||||
let sql = format!("SELECT count(*) AS n {}", entries_from(&mut a, user_id, None, filter, search, None, Some(signal)));
|
||||
let sql =
|
||||
format!("SELECT count(*) AS n {}", entries_from(&mut a, user_id, None, filter, search, None, Some(signal)));
|
||||
let r = self.rows(&sql, a.0).await?;
|
||||
Ok(r.first().context("count(*) always returns a row")?.try_get("", "n")?)
|
||||
}
|
||||
@@ -1432,7 +1424,8 @@ impl Db {
|
||||
search: Option<&str>,
|
||||
) -> Result<i64> {
|
||||
let mut a = Args::default();
|
||||
let sql = format!("SELECT count(*) AS n {}", entries_from(&mut a, user_id, feed_id, filter, search, None, None));
|
||||
let sql =
|
||||
format!("SELECT count(*) AS n {}", entries_from(&mut a, user_id, feed_id, filter, search, None, None));
|
||||
let r = self.rows(&sql, a.0).await?;
|
||||
Ok(r.first().context("count(*) always returns a row")?.try_get("", "n")?)
|
||||
}
|
||||
@@ -1457,13 +1450,7 @@ impl Db {
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (user_id, feed_id, guid) DO UPDATE SET position = excluded.position,
|
||||
duration = coalesce(excluded.duration, entry_state.duration)",
|
||||
vec![
|
||||
user_id.into(),
|
||||
feed_id.into(),
|
||||
guid.into(),
|
||||
secs.max(0).into(),
|
||||
duration.filter(|d| *d > 0).into(),
|
||||
],
|
||||
vec![user_id.into(), feed_id.into(), guid.into(), secs.max(0).into(), duration.filter(|d| *d > 0).into()],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -1508,9 +1495,7 @@ impl Db {
|
||||
return Ok(false); // dropped, so rolled back
|
||||
}
|
||||
for (id, feed) in &cfg.feeds {
|
||||
catalogue::ActiveModel { id: Set(id.clone()), spec: Set(serde_json::to_string(feed)?) }
|
||||
.insert(&tx)
|
||||
.await?;
|
||||
catalogue::ActiveModel { id: Set(id.clone()), spec: Set(serde_json::to_string(feed)?) }.insert(&tx).await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
Ok(true)
|
||||
@@ -1531,9 +1516,7 @@ impl Db {
|
||||
.await?;
|
||||
catalogue::Entity::delete_many().exec(&tx).await?;
|
||||
for (id, feed) in &cfg.feeds {
|
||||
catalogue::ActiveModel { id: Set(id.clone()), spec: Set(serde_json::to_string(feed)?) }
|
||||
.insert(&tx)
|
||||
.await?;
|
||||
catalogue::ActiveModel { id: Set(id.clone()), spec: Set(serde_json::to_string(feed)?) }.insert(&tx).await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
@@ -1583,10 +1566,7 @@ impl Db {
|
||||
|
||||
/// What this person wants from a feed. Absent means they do not subscribe at all.
|
||||
pub async fn subscription(&self, user_id: i64, feed_id: &str) -> Result<Option<Sub>> {
|
||||
Ok(subscriptions::Entity::find_by_id((user_id, feed_id.to_owned()))
|
||||
.one(&self.orm)
|
||||
.await?
|
||||
.map(Sub::from))
|
||||
Ok(subscriptions::Entity::find_by_id((user_id, feed_id.to_owned())).one(&self.orm).await?.map(Sub::from))
|
||||
}
|
||||
|
||||
/// Every feed this person subscribes to, with their settings.
|
||||
@@ -1651,14 +1631,11 @@ impl Db {
|
||||
/// Feeds with any audio or video enclosure: the Directory's Podcasts, with the rest Blogs.
|
||||
/// Reaped files keep their rows, so a show whose files have all been purged still counts.
|
||||
pub async fn media_feeds(&self) -> Result<std::collections::HashSet<String>> {
|
||||
self.rows(
|
||||
"SELECT DISTINCT feed_id FROM enclosures WHERE mime LIKE 'audio/%' OR mime LIKE 'video/%'",
|
||||
vec![],
|
||||
)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|r| Ok(r.try_get("", "feed_id")?))
|
||||
.collect()
|
||||
self.rows("SELECT DISTINCT feed_id FROM enclosures WHERE mime LIKE 'audio/%' OR mime LIKE 'video/%'", vec![])
|
||||
.await?
|
||||
.iter()
|
||||
.map(|r| Ok(r.try_get("", "feed_id")?))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Who else would miss this file: subscribers other than `user_id` who have starred
|
||||
@@ -1769,14 +1746,13 @@ impl Db {
|
||||
.iter()
|
||||
.flat_map(|e| {
|
||||
let hay = [e.title.as_deref().unwrap_or(""), e.description.as_deref().unwrap_or("")];
|
||||
lists
|
||||
.iter()
|
||||
.filter(move |(_, words)| crate::download::blocked(words, &hay))
|
||||
.map(|(user, _)| hidden::ActiveModel {
|
||||
lists.iter().filter(move |(_, words)| crate::download::blocked(words, &hay)).map(|(user, _)| {
|
||||
hidden::ActiveModel {
|
||||
user_id: Set(*user),
|
||||
feed_id: Set(feed_id.to_owned()),
|
||||
guid: Set(e.guid.clone()),
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if !rows.is_empty() {
|
||||
@@ -1925,17 +1901,12 @@ impl Db {
|
||||
|
||||
/// Sets some of one person's columns, whatever else is in their row.
|
||||
async fn update_user(&self, id: i64, m: users::ActiveModel) -> Result<()> {
|
||||
users::Entity::update_many()
|
||||
.set(m)
|
||||
.filter(users::Column::Id.eq(id))
|
||||
.exec(&self.orm)
|
||||
.await?;
|
||||
users::Entity::update_many().set(m).filter(users::Column::Id.eq(id)).exec(&self.orm).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_password(&self, id: i64, hash: &str) -> Result<()> {
|
||||
self.update_user(id, users::ActiveModel { pass_hash: Set(Some(hash.to_owned())), ..Default::default() })
|
||||
.await
|
||||
self.update_user(id, users::ActiveModel { pass_hash: Set(Some(hash.to_owned())), ..Default::default() }).await
|
||||
}
|
||||
|
||||
pub async fn set_admin(&self, id: i64, admin: bool) -> Result<()> {
|
||||
@@ -2233,11 +2204,8 @@ impl Db {
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn adopt(&self, parent: &str, child: &str, listed: &[(&str, &str)]) -> Result<()> {
|
||||
use sea_orm::TransactionTrait;
|
||||
let holds = enclosures::Entity::find()
|
||||
.filter(enclosures::Column::FeedId.eq(parent))
|
||||
.count(&self.orm)
|
||||
.await?
|
||||
> 0;
|
||||
let holds =
|
||||
enclosures::Entity::find().filter(enclosures::Column::FeedId.eq(parent)).count(&self.orm).await? > 0;
|
||||
if !holds {
|
||||
return Ok(()); // An OPML, or a creator already shared out.
|
||||
}
|
||||
@@ -2432,7 +2400,12 @@ impl Db {
|
||||
.await?
|
||||
.iter()
|
||||
.map(|r| {
|
||||
Ok((r.try_get("", "user_id")?, r.try_get("", "feed_id")?, r.try_get("", "wanted")?, r.try_get("", "pause_ms")?))
|
||||
Ok((
|
||||
r.try_get("", "user_id")?,
|
||||
r.try_get("", "feed_id")?,
|
||||
r.try_get("", "wanted")?,
|
||||
r.try_get("", "pause_ms")?,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -2646,7 +2619,6 @@ pub struct Unjudged {
|
||||
pub signals: Vec<Ask>,
|
||||
}
|
||||
|
||||
|
||||
impl Db {
|
||||
pub async fn signals(&self, user_id: i64) -> Result<Vec<SignalRow>> {
|
||||
self.signal_rows(user_id, None).await
|
||||
@@ -2695,7 +2667,13 @@ impl Db {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn add_signal(&self, user_id: i64, name: &str, words: &[String], description: Option<&str>) -> Result<i64> {
|
||||
pub async fn add_signal(
|
||||
&self,
|
||||
user_id: i64,
|
||||
name: &str,
|
||||
words: &[String],
|
||||
description: Option<&str>,
|
||||
) -> Result<i64> {
|
||||
let m = signals::ActiveModel {
|
||||
user_id: Set(user_id),
|
||||
name: Set(name.to_owned()),
|
||||
@@ -2760,8 +2738,11 @@ impl Db {
|
||||
}
|
||||
|
||||
pub async fn set_typesafe_key(&self, user_id: i64, key: Option<&str>) -> Result<()> {
|
||||
self.update_user(user_id, users::ActiveModel { typesafe_key: Set(key.map(str::to_owned)), ..Default::default() })
|
||||
.await
|
||||
self.update_user(
|
||||
user_id,
|
||||
users::ActiveModel { typesafe_key: Set(key.map(str::to_owned)), ..Default::default() },
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Up to `limit` items, newest first, that someone with a key has a Signal to ask about: not
|
||||
@@ -2804,7 +2785,9 @@ impl Db {
|
||||
"JOIN signal_verdicts v ON v.signal_id = g.id AND v.feed_id = s.feed_id AND v.recheck
|
||||
JOIN entries e ON e.feed_id = v.feed_id AND e.guid = v.guid",
|
||||
);
|
||||
let sql = format!("SELECT * FROM ({recent} UNION ALL {again}) q ORDER BY at DESC, feed_id, guid, user_id LIMIT {limit}");
|
||||
let sql = format!(
|
||||
"SELECT * FROM ({recent} UNION ALL {again}) q ORDER BY at DESC, feed_id, guid, user_id LIMIT {limit}"
|
||||
);
|
||||
let mut out: Vec<Unjudged> = Vec::new();
|
||||
for r in self.rows(&sql, a.0).await? {
|
||||
let (user_id, feed_id, guid): (i64, String, String) =
|
||||
@@ -2853,10 +2836,7 @@ impl Db {
|
||||
}
|
||||
|
||||
pub fn now() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0)
|
||||
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -2889,9 +2869,15 @@ mod tests {
|
||||
db.store_plans(1, r#"{"300": {"cuts": [[1000,3000]], "pieces": [[0,10,1.0]]}}"#).await.unwrap();
|
||||
assert!(db.deadair_todo().await.unwrap().is_empty());
|
||||
let p = db.plans(&[1, 2]).await.unwrap();
|
||||
assert_eq!((p.len(), p[&1][&300].cuts.as_slice(), p[&1][&300].saved_ms()), (1, &[crate::deadair::Cut(1000, 3000)][..], 2000));
|
||||
assert_eq!(
|
||||
(p.len(), p[&1][&300].cuts.as_slice(), p[&1][&300].saved_ms()),
|
||||
(1, &[crate::deadair::Cut(1000, 3000)][..], 2000)
|
||||
);
|
||||
// Downloaded again since: looked at again before it plays shortened.
|
||||
db.orm.execute_unprepared(&format!("UPDATE enclosures SET downloaded_at = {} WHERE id = 1", now() + 60)).await.unwrap();
|
||||
db.orm
|
||||
.execute_unprepared(&format!("UPDATE enclosures SET downloaded_at = {} WHERE id = 1", now() + 60))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(db.deadair_todo().await.unwrap().len(), 1);
|
||||
assert!(db.plans(&[1]).await.unwrap().is_empty());
|
||||
|
||||
@@ -2929,7 +2915,9 @@ mod tests {
|
||||
// And writers at once all get their turn, not a refusal.
|
||||
let writes = (0..16).map(|i| {
|
||||
let db = &db;
|
||||
async move { db.record_feed(&format!("f{i}"), "http://x/f", Some("F"), None, None, None, None, None, None).await }
|
||||
async move {
|
||||
db.record_feed(&format!("f{i}"), "http://x/f", Some("F"), None, None, None, None, None, None).await
|
||||
}
|
||||
});
|
||||
for r in futures_util::future::join_all(writes).await {
|
||||
r.unwrap();
|
||||
@@ -2942,10 +2930,18 @@ mod tests {
|
||||
let backend = db.orm.get_database_backend();
|
||||
if backend == sea_orm::DbBackend::Sqlite {
|
||||
let row = db.orm.query_one_raw(Statement::from_string(backend, "PRAGMA synchronous")).await.unwrap();
|
||||
assert_eq!(row.unwrap().try_get_by_index::<i32>(0).unwrap(), 1, "NORMAL, on every connection in the pool (#135)");
|
||||
assert_eq!(
|
||||
row.unwrap().try_get_by_index::<i32>(0).unwrap(),
|
||||
1,
|
||||
"NORMAL, on every connection in the pool (#135)"
|
||||
);
|
||||
} else {
|
||||
let row = db.orm.query_one_raw(Statement::from_string(backend, "SHOW synchronous_commit")).await.unwrap();
|
||||
assert_eq!(row.unwrap().try_get_by_index::<String>(0).unwrap(), "off", "with the test's own search_path as well (#140)");
|
||||
assert_eq!(
|
||||
row.unwrap().try_get_by_index::<String>(0).unwrap(),
|
||||
"off",
|
||||
"with the test's own search_path as well (#140)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2954,7 +2950,11 @@ mod tests {
|
||||
let db = Db::memory().await.unwrap();
|
||||
let record = |etag: &'static str, about: Option<&'static str>| {
|
||||
let db = &db;
|
||||
async move { db.record_feed("f", "https://x/f", Some("F"), Some(etag), Some("then"), None, None, None, about).await.unwrap() }
|
||||
async move {
|
||||
db.record_feed("f", "https://x/f", Some("F"), Some(etag), Some("then"), None, None, None, about)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
};
|
||||
record("\"a\"", Some("About F")).await;
|
||||
assert_eq!(db.feed_description("f").await.unwrap().as_deref(), Some("About F"));
|
||||
@@ -2965,7 +2965,10 @@ mod tests {
|
||||
create_missing(&db.orm).await.unwrap();
|
||||
assert_eq!(db.feed_description("f").await.unwrap(), None);
|
||||
let st = db.http_state("f").await.unwrap();
|
||||
assert!(st.etag.is_none() && st.last_modified.is_none(), "every feed is read whole next time, for its description");
|
||||
assert!(
|
||||
st.etag.is_none() && st.last_modified.is_none(),
|
||||
"every feed is read whole next time, for its description"
|
||||
);
|
||||
// With the column there, opening the database again writes nothing.
|
||||
record("\"b\"", Some("About F")).await;
|
||||
create_missing(&db.orm).await.unwrap();
|
||||
@@ -3001,10 +3004,12 @@ mod tests {
|
||||
INSERT INTO enclosures (id, feed_id, guid, url, mime, length, state) VALUES
|
||||
(1,'f','a','u1','audio/mpeg',300,'pending'),(2,'g','b','u2','image/png',10,'pending'),
|
||||
(3,'f','c','u3','video/mp4',2000,'pending');",
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let order = async |col: &str, dir: &str| -> Vec<String> {
|
||||
db.entries_in(1, None, Filter::All, None, 0, 50, &order_sql(col, dir)).await
|
||||
db.entries_in(1, None, Filter::All, None, 0, 50, &order_sql(col, dir))
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|e| e.guid)
|
||||
@@ -3025,8 +3030,13 @@ mod tests {
|
||||
db.exec_for_test("DELETE FROM entries WHERE guid = 'n';").await.unwrap();
|
||||
assert_eq!(order("published", "desc").await, ["c", "b", "a"]);
|
||||
// A page from the middle: the keys are paged before the rows are read (#188).
|
||||
let page: Vec<String> = db.entries_in(1, None, Filter::All, None, 1, 1, &order_sql("published", "desc")).await
|
||||
.unwrap().into_iter().map(|e| e.guid).collect();
|
||||
let page: Vec<String> = db
|
||||
.entries_in(1, None, Filter::All, None, 1, 1, &order_sql("published", "desc"))
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|e| e.guid)
|
||||
.collect();
|
||||
assert_eq!(page, ["b"]);
|
||||
db.set_entry_flag(1, "f", "a", EntryFlag::Flagged, true).await.unwrap();
|
||||
assert_eq!(order("kept", "desc").await[0], "a");
|
||||
@@ -3055,7 +3065,10 @@ mod tests {
|
||||
let list = db.feed_list(1, None).await.unwrap();
|
||||
let g = db.feed_list(1, Some("g")).await.unwrap();
|
||||
assert_eq!(g.keys().collect::<Vec<_>>(), ["g"]);
|
||||
assert_eq!((g["g"].unread, g["g"].summary.entries, g["g"].blocked.clone()), (1, 1, vec!["spoiler".to_string()]));
|
||||
assert_eq!(
|
||||
(g["g"].unread, g["g"].summary.entries, g["g"].blocked.clone()),
|
||||
(1, 1, vec!["spoiler".to_string()])
|
||||
);
|
||||
for id in ["f", "g"] {
|
||||
let one = &list[id];
|
||||
let s = db.feed_summary(id).await.unwrap();
|
||||
@@ -3084,8 +3097,8 @@ mod tests {
|
||||
db.secure_images("f", "a.example").await.unwrap();
|
||||
assert_eq!(db.feed_summary("f").await.unwrap().image.as_deref(), Some("https://a.example/logo.png"));
|
||||
assert!(db.names_image("https://a.example/1.jpg").await.unwrap());
|
||||
assert!(db.names_image("http://b.example/2.jpg").await.unwrap()); // another host
|
||||
assert!(db.names_image("http://a.example/3.jpg").await.unwrap()); // another feed
|
||||
assert!(db.names_image("http://b.example/2.jpg").await.unwrap()); // another host
|
||||
assert!(db.names_image("http://a.example/3.jpg").await.unwrap()); // another feed
|
||||
assert_eq!(db.feed_summary("g").await.unwrap().image.as_deref(), Some("http://a.example/g.png"));
|
||||
}
|
||||
|
||||
@@ -3102,7 +3115,8 @@ mod tests {
|
||||
INSERT INTO entries (feed_id, guid, first_seen, published) VALUES
|
||||
('quiet','q',0,100),('news','n',0,950),('dead','d',0,950),('kept','k',0,100);
|
||||
INSERT INTO enclosures (id, feed_id, guid, url, path, state) VALUES (1,'kept','k','u','/tmp/k','done');",
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// Dead before 500, or nothing newer than 500: dead fails since 100, quiet's newest is 100.
|
||||
// news published lately, new has no items yet, wanted has a subscriber, kept a file,
|
||||
@@ -3128,7 +3142,8 @@ mod tests {
|
||||
INSERT INTO entries (feed_id, guid, first_seen) VALUES ('gone','a',0),('kept','b',0);
|
||||
INSERT INTO enclosures (id, feed_id, guid, url, path, state) VALUES
|
||||
(1,'gone','a','u1',null,'pending'),(2,'kept','b','u2','/tmp/b','done');",
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// Subscribed, holding a file, working, or in the catalogue: all stay.
|
||||
assert_eq!(db.prune_abandoned_failures().await.unwrap(), vec!["gone".to_string()]);
|
||||
@@ -3147,21 +3162,27 @@ mod tests {
|
||||
INSERT INTO enclosures (id, feed_id, guid, url, state, path) VALUES
|
||||
(1,'f','e1','u1','pending',null),(2,'f','e2','u2','pending',null),
|
||||
(3,'f','e3','u3','done','/tmp/3'),(4,'f','e4','u4','pending',null);",
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let states = || async {
|
||||
let mut v: Vec<(i64, String)> = db.rows("SELECT id, state FROM enclosures ORDER BY id", vec![]).await.unwrap()
|
||||
.iter().map(|r| (r.try_get("", "id").unwrap(), r.try_get("", "state").unwrap())).collect();
|
||||
let mut v: Vec<(i64, String)> = db
|
||||
.rows("SELECT id, state FROM enclosures ORDER BY id", vec![])
|
||||
.await
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|r| (r.try_get("", "id").unwrap(), r.try_get("", "state").unwrap()))
|
||||
.collect();
|
||||
v.sort();
|
||||
v.into_iter().map(|(_, s)| s).collect::<Vec<_>>()
|
||||
};
|
||||
db.hold_back("f", 2).await.unwrap(); // newest two: e4, e3
|
||||
db.hold_back("f", 2).await.unwrap(); // newest two: e4, e3
|
||||
assert_eq!(states().await, ["held", "held", "done", "pending"]);
|
||||
db.hold_back("f", 3).await.unwrap(); // a limit raised brings e2 back
|
||||
db.hold_back("f", 3).await.unwrap(); // a limit raised brings e2 back
|
||||
assert_eq!(states().await, ["held", "pending", "done", "pending"]);
|
||||
db.hold_back("f", 0).await.unwrap(); // a feed nobody downloads: all held
|
||||
db.hold_back("f", 0).await.unwrap(); // a feed nobody downloads: all held
|
||||
assert_eq!(states().await, ["held", "held", "done", "held"]);
|
||||
db.hold_back("f", usize::MAX).await.unwrap(); // unlimited, an archive: all queued
|
||||
db.hold_back("f", usize::MAX).await.unwrap(); // unlimited, an archive: all queued
|
||||
assert_eq!(states().await, ["pending", "pending", "done", "pending"]);
|
||||
}
|
||||
|
||||
@@ -3175,7 +3196,8 @@ mod tests {
|
||||
(1,'f','e1','u1','pending',null),(2,'f','e2','u2','pending',null),
|
||||
(3,'f','e3','u3','done','/tmp/3'),(4,'f','e4','u4','done','/tmp/4'),
|
||||
(5,'f','e5','u5','skipped',null);",
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let ids = |p: Vec<Pending>| p.into_iter().map(|p| p.id).collect::<Vec<_>>();
|
||||
// The newest three with a file worth having are e4, e3 and e2 (e5's was skipped): only
|
||||
@@ -3225,12 +3247,15 @@ mod tests {
|
||||
db.record_entry("f", &item("b", "B", Some("the next review"))).await.unwrap();
|
||||
db.record_entry("f", &item("c", "C", Some("as it was"))).await.unwrap();
|
||||
let changed = db
|
||||
.changed_items("f", &[
|
||||
item("a", "A", Some("the review, fixed")), // the text corrected
|
||||
item("b", "B renamed", Some("the next review")), // retitled
|
||||
item("c", "C", None), // says nothing of its text: what is stored stands
|
||||
item("d", "D", Some("new")), // not stored: the insert's, not this
|
||||
])
|
||||
.changed_items(
|
||||
"f",
|
||||
&[
|
||||
item("a", "A", Some("the review, fixed")), // the text corrected
|
||||
item("b", "B renamed", Some("the next review")), // retitled
|
||||
item("c", "C", None), // says nothing of its text: what is stored stands
|
||||
item("d", "D", Some("new")), // not stored: the insert's, not this
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(changed, ["a", "b"].map(String::from).into());
|
||||
@@ -3243,7 +3268,8 @@ mod tests {
|
||||
db.exec_for_test(
|
||||
"INSERT INTO feeds (id, url, image) VALUES ('f','u','http://cdn.example/show.jpg');
|
||||
INSERT INTO entries (feed_id, guid, first_seen, image) VALUES ('f','a',0,'http://cdn.example/ep.jpg');",
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(db.names_image("http://cdn.example/show.jpg").await.unwrap());
|
||||
assert!(db.names_image("http://cdn.example/ep.jpg").await.unwrap());
|
||||
@@ -3259,7 +3285,8 @@ mod tests {
|
||||
INSERT INTO entries (feed_id, guid, first_seen) VALUES ('f','a',0);
|
||||
INSERT INTO enclosures (id, feed_id, guid, url, path, state) VALUES
|
||||
(1,'f','a','u1','/tmp/a','done');",
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Nobody has touched it: both others still have it unplayed.
|
||||
@@ -3283,7 +3310,8 @@ mod tests {
|
||||
"INSERT INTO users (id, name, is_admin) VALUES (1,'ray',true),(2,'sam',false);
|
||||
INSERT INTO entries (feed_id, guid, title, first_seen) VALUES
|
||||
('f','a','One',100),('f','b','Two',200);",
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(db.unread_count(1, "f").await.unwrap(), 2);
|
||||
@@ -3362,8 +3390,7 @@ mod tests {
|
||||
// Cutting this along with the dead read columns left the browser suite's admin with an
|
||||
// empty sidebar: it is how a fresh install's first account gets config.toml's feeds.
|
||||
let db = Db::memory().await.unwrap();
|
||||
db.exec_for_test("INSERT INTO users (id, name, is_admin) VALUES (1,'admin',true);").await
|
||||
.unwrap();
|
||||
db.exec_for_test("INSERT INTO users (id, name, is_admin) VALUES (1,'admin',true);").await.unwrap();
|
||||
let subs = async || db.i64s_for_test("SELECT count(*) FROM subscriptions").await[0];
|
||||
let catalogue = ["a".to_string(), "b".to_string()];
|
||||
assert_eq!(db.adopt_catalogue(1, &catalogue).await.unwrap(), 2);
|
||||
@@ -3405,7 +3432,8 @@ mod tests {
|
||||
-- Opened, and so read, but 42 of 900 seconds in: still Currently Listening.
|
||||
-- Filtering on read hid exactly these (issue #14).
|
||||
(7,'f','h',true,false,42);",
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for f in [Filter::All, Filter::Unread, Filter::Downloaded, Filter::Flagged, Filter::InProgress] {
|
||||
@@ -3425,8 +3453,11 @@ mod tests {
|
||||
assert_eq!(db.count_in(7, Some("f"), Filter::Downloaded, None).await.unwrap(), 1);
|
||||
assert_eq!(db.count_in(7, Some("f"), Filter::Flagged, None).await.unwrap(), 1);
|
||||
assert_eq!(db.count_in(7, Some("f"), Filter::All, Some("dive")).await.unwrap(), 2);
|
||||
assert_eq!(db.count_in(7, Some("f"), Filter::All, Some("NOTES two")).await.unwrap(), 1,
|
||||
"search is case-insensitive and covers the description");
|
||||
assert_eq!(
|
||||
db.count_in(7, Some("f"), Filter::All, Some("NOTES two")).await.unwrap(),
|
||||
1,
|
||||
"search is case-insensitive and covers the description"
|
||||
);
|
||||
|
||||
// Currently Listening: started, not finished, and not just an accidental tap.
|
||||
let order = order_sql("published", "desc");
|
||||
@@ -3473,14 +3504,26 @@ mod tests {
|
||||
let everyone = db.unjudged(&[], 50, true, 30).await.unwrap();
|
||||
let mut seen: Vec<_> = everyone.iter().map(|u| (u.user_id, u.guid.as_str(), u.key.is_none())).collect();
|
||||
seen.sort();
|
||||
assert_eq!(seen, [(1, "x", true), (1, "y", true), (2, "x", true)], "on the server's key, everyone's Signals, no key of their own needed");
|
||||
assert_eq!(
|
||||
seen,
|
||||
[(1, "x", true), (1, "y", true), (2, "x", true)],
|
||||
"on the server's key, everyone's Signals, no key of their own needed"
|
||||
);
|
||||
db.set_typesafe_key(1, Some("k")).await.unwrap();
|
||||
let queue = db.unjudged(&[], 50, false, 30).await.unwrap();
|
||||
let mut seen: Vec<_> = queue.iter().map(|u| (u.guid.as_str(), u.signals.len())).collect();
|
||||
seen.sort();
|
||||
assert_eq!(seen, [("x", 2), ("y", 2)], "each recent subscribed item once, with both Signals; old and unsubscribed left out");
|
||||
assert_eq!(
|
||||
seen,
|
||||
[("x", 2), ("y", 2)],
|
||||
"each recent subscribed item once, with both Signals; old and unsubscribed left out"
|
||||
);
|
||||
let ask = queue[0].signals.iter().find(|a| a.id == food).unwrap();
|
||||
assert_eq!((ask.name.as_str(), ask.description.as_deref()), ("Food", Some("Bread and cakes")), "Jev is asked about the name and the sentence");
|
||||
assert_eq!(
|
||||
(ask.name.as_str(), ask.description.as_deref()),
|
||||
("Food", Some("Bread and cakes")),
|
||||
"Jev is asked about the name and the sentence"
|
||||
);
|
||||
assert!(db.unjudged(&[1], 50, false, 30).await.unwrap().is_empty(), "a refused key is passed over");
|
||||
|
||||
db.record_verdicts("f", "x", &[(f1, true), (food, false)]).await.unwrap();
|
||||
@@ -3491,10 +3534,15 @@ mod tests {
|
||||
let page = db.signal_entries(1, f1, Filter::All, None, 0, 50, &order).await.unwrap();
|
||||
assert_eq!(page.iter().map(|e| e.guid.as_str()).collect::<Vec<_>>(), ["x"]);
|
||||
assert_eq!(db.signal_count(1, food, Filter::All, None).await.unwrap(), 1);
|
||||
assert_eq!(db.signals(1).await.unwrap().iter().map(|s| (s.items, s.unread)).collect::<Vec<_>>(), [(1, 1), (1, 1)]);
|
||||
db.exec_for_test("INSERT INTO entry_state (user_id, feed_id, guid, read, flagged, position) VALUES (1,'f','x',true,false,0)")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
db.signals(1).await.unwrap().iter().map(|s| (s.items, s.unread)).collect::<Vec<_>>(),
|
||||
[(1, 1), (1, 1)]
|
||||
);
|
||||
db.exec_for_test(
|
||||
"INSERT INTO entry_state (user_id, feed_id, guid, read, flagged, position) VALUES (1,'f','x',true,false,0)",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let f = db.signal(1, f1).await.unwrap().unwrap();
|
||||
assert_eq!((f.items, f.unread), (1, 0), "read in its feed is read in the Signal, and still one of its items");
|
||||
|
||||
@@ -3505,11 +3553,22 @@ mod tests {
|
||||
// An edit asks again only what was picked out, however old, and leaves what was passed
|
||||
// over (#276). Until answered, what was picked out stays in the list.
|
||||
db.record_verdicts("g", "old", &[(f1, true)]).await.unwrap();
|
||||
db.set_signal(1, f1, SignalChange { description: Some(Some("Grand prix racing")), ..Default::default() }).await.unwrap();
|
||||
db.set_signal(1, f1, SignalChange { description: Some(Some("Grand prix racing")), ..Default::default() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(db.signal(1, f1).await.unwrap().unwrap().description.as_deref(), Some("Grand prix racing"));
|
||||
let again: Vec<_> = db.unjudged(&[], 50, false, 30).await.unwrap().iter()
|
||||
.map(|u| (u.guid.clone(), u.signals.iter().map(|a| a.id).collect::<Vec<_>>())).collect();
|
||||
assert_eq!(again, [("x".to_string(), vec![f1]), ("old".to_string(), vec![f1])], "y was passed over, and stays so");
|
||||
let again: Vec<_> = db
|
||||
.unjudged(&[], 50, false, 30)
|
||||
.await
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|u| (u.guid.clone(), u.signals.iter().map(|a| a.id).collect::<Vec<_>>()))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
again,
|
||||
[("x".to_string(), vec![f1]), ("old".to_string(), vec![f1])],
|
||||
"y was passed over, and stays so"
|
||||
);
|
||||
assert_eq!(db.signal_count(1, f1, Filter::All, None).await.unwrap(), 2);
|
||||
db.record_verdicts("f", "x", &[(f1, false)]).await.unwrap();
|
||||
db.record_verdicts("g", "old", &[(f1, true)]).await.unwrap();
|
||||
@@ -3518,7 +3577,9 @@ mod tests {
|
||||
assert!(db.unjudged(&[], 50, false, 1).await.unwrap().is_empty());
|
||||
|
||||
db.record_verdicts("f", "x", &[(f1, true)]).await.unwrap();
|
||||
db.set_signal(1, f1, SignalChange { icon: Some(Some("solid/flag-checkered")), ..Default::default() }).await.unwrap();
|
||||
db.set_signal(1, f1, SignalChange { icon: Some(Some("solid/flag-checkered")), ..Default::default() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(db.signal_count(1, f1, Filter::All, None).await.unwrap(), 2, "x and old, kept");
|
||||
assert!(db.unjudged(&[], 50, false, 30).await.unwrap().is_empty(), "a new icon asks nothing again");
|
||||
assert_eq!(db.signal(1, f1).await.unwrap().unwrap().icon.as_deref(), Some("solid/flag-checkered"));
|
||||
@@ -3574,7 +3635,8 @@ mod tests {
|
||||
(1,'f','old','u-old','pending'),
|
||||
(2,'f','mid','u-mid','pending'),
|
||||
(3,'f','new','u-new','pending');",
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let got: Vec<String> = db.pending("f", 2).await.unwrap().into_iter().map(|p| p.url).collect();
|
||||
assert_eq!(got, vec!["u-new", "u-mid"], "newest first, oldest left for later");
|
||||
@@ -3589,7 +3651,8 @@ mod tests {
|
||||
(2,'f','b','u2','pending',NULL),
|
||||
(3,'f','c','u3','downloading','/tmp/already-here'),
|
||||
(4,'f','d','u4','done','/tmp/x');",
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(db.requeue_interrupted().await.unwrap(), 1, "only the in-flight, fileless one");
|
||||
let state = async |id: i64| db.strings_for_test(&format!("SELECT state FROM enclosures WHERE id = {id}")).await;
|
||||
@@ -3609,10 +3672,12 @@ mod tests {
|
||||
(3,'creator','c','u3','skipped',NULL,'explicit'),
|
||||
(4,'other','d','u4','skipped',NULL,'torrents disabled');
|
||||
INSERT INTO entry_state (user_id, feed_id, guid, read) VALUES (1,'creator','a',true);",
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.adopt("creator", "show", &[("a", "u1"), ("b", "u2"), ("d", "u4")]).await.unwrap();
|
||||
let owner = async |id: i64| db.strings_for_test(&format!("SELECT feed_id FROM enclosures WHERE id = {id}")).await;
|
||||
let owner =
|
||||
async |id: i64| db.strings_for_test(&format!("SELECT feed_id FROM enclosures WHERE id = {id}")).await;
|
||||
assert_eq!(owner(1).await, ["show"], "a downloaded file moves with its item");
|
||||
assert_eq!(owner(2).await, ["show"]);
|
||||
assert_eq!(owner(3).await, ["creator"], "this show does not list it");
|
||||
@@ -3637,7 +3702,8 @@ mod tests {
|
||||
"INSERT INTO users (id, name, is_admin) VALUES (1,'ray',true),(2,'sam',false);
|
||||
INSERT INTO subscriptions (user_id, feed_id, allow_explicit) VALUES
|
||||
(1,'group',true),(1,'show',NULL),(2,'group',true),(2,'show',false);",
|
||||
).await
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let explicit = async |group| -> Vec<Option<bool>> {
|
||||
let mut v: Vec<_> =
|
||||
@@ -3645,7 +3711,11 @@ mod tests {
|
||||
v.sort();
|
||||
v
|
||||
};
|
||||
assert_eq!(explicit(Some("group")).await, [Some(false), Some(true)], "ray inherits; sam's own choice on the show wins");
|
||||
assert_eq!(
|
||||
explicit(Some("group")).await,
|
||||
[Some(false), Some(true)],
|
||||
"ray inherits; sam's own choice on the show wins"
|
||||
);
|
||||
assert_eq!(explicit(None).await, [None, Some(false)], "outside a group nothing is inherited");
|
||||
}
|
||||
|
||||
@@ -3677,7 +3747,8 @@ mod tests {
|
||||
db.subscribe(me, "f").await.unwrap();
|
||||
assert!(db.set_pinned(me, "f", true).await.unwrap());
|
||||
// set_subscription writes the rest of the row; it must leave the pin alone.
|
||||
db.set_subscription(me, &Sub { feed_id: "f".into(), auto_download: Some(false), ..Default::default() }).await
|
||||
db.set_subscription(me, &Sub { feed_id: "f".into(), auto_download: Some(false), ..Default::default() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(db.pinned_feeds(me).await.unwrap().contains("f"));
|
||||
db.set_pinned(me, "f", false).await.unwrap();
|
||||
@@ -3687,8 +3758,15 @@ mod tests {
|
||||
#[test]
|
||||
fn postgres_is_asked_for_no_notices_and_a_password_never_reaches_a_log() {
|
||||
assert_eq!(url_for("postgres://u:p@h/d"), format!("postgres://u:p@h/d?{QUIET}"));
|
||||
assert_eq!(url_for("postgres://u:p@h/d?sslmode=disable"), format!("postgres://u:p@h/d?sslmode=disable&{QUIET}"));
|
||||
assert_eq!(url_for("postgres://u:p@h/d?options=-c%20x%3Dy"), "postgres://u:p@h/d?options=-c%20x%3Dy", "theirs stands");
|
||||
assert_eq!(
|
||||
url_for("postgres://u:p@h/d?sslmode=disable"),
|
||||
format!("postgres://u:p@h/d?sslmode=disable&{QUIET}")
|
||||
);
|
||||
assert_eq!(
|
||||
url_for("postgres://u:p@h/d?options=-c%20x%3Dy"),
|
||||
"postgres://u:p@h/d?options=-c%20x%3Dy",
|
||||
"theirs stands"
|
||||
);
|
||||
assert_eq!(url_for("/data/state.db"), "sqlite:///data/state.db?mode=rwc");
|
||||
assert_eq!(redact("postgres://ipodderx:s3cret@h:5433/ipodderx"), "postgres://ipodderx:***@h:5433/ipodderx");
|
||||
}
|
||||
@@ -3732,7 +3810,8 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn enclosure_url_is_the_dedupe_key() {
|
||||
let db = Db::memory().await.unwrap();
|
||||
let insert = "INSERT INTO enclosures (feed_id, guid, url, state) VALUES ('f', 'g', 'http://x/a.mp3', 'pending')";
|
||||
let insert =
|
||||
"INSERT INTO enclosures (feed_id, guid, url, state) VALUES ('f', 'g', 'http://x/a.mp3', 'pending')";
|
||||
db.exec_for_test(insert).await.unwrap();
|
||||
assert!(db.exec_for_test(insert).await.is_err(), "duplicate url must be rejected");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user