use std::{
collections::{hash_map::Entry, BTreeSet, HashMap, HashSet},
path::{Path, PathBuf},
sync::Arc,
};
use error_support::{breadcrumb, handle_error};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use remote_settings::{self, RemoteSettingsConfig, RemoteSettingsServer};
use serde::de::DeserializeOwned;
use crate::{
config::{SuggestGlobalConfig, SuggestProviderConfig},
db::{ConnectionType, IngestedRecord, Sqlite3Extension, SuggestDao, SuggestDb},
error::Error,
geoname::{Geoname, GeonameMatch, GeonameType},
metrics::{MetricsContext, SuggestIngestionMetrics, SuggestQueryMetrics},
provider::{SuggestionProvider, SuggestionProviderConstraints, DEFAULT_INGEST_PROVIDERS},
rs::{
Client, Collection, DownloadedExposureRecord, Record, RemoteSettingsClient,
SuggestAttachment, SuggestRecord, SuggestRecordId, SuggestRecordType,
},
suggestion::AmpSuggestionType,
QueryWithMetricsResult, Result, SuggestApiResult, Suggestion, SuggestionQuery,
};
#[derive(uniffi::Object)]
pub struct SuggestStoreBuilder(Mutex<SuggestStoreBuilderInner>);
#[derive(Default)]
struct SuggestStoreBuilderInner {
data_path: Option<String>,
remote_settings_server: Option<RemoteSettingsServer>,
remote_settings_bucket_name: Option<String>,
extensions_to_load: Vec<Sqlite3Extension>,
}
impl Default for SuggestStoreBuilder {
fn default() -> Self {
Self::new()
}
}
#[uniffi::export]
impl SuggestStoreBuilder {
#[uniffi::constructor]
pub fn new() -> SuggestStoreBuilder {
Self(Mutex::new(SuggestStoreBuilderInner::default()))
}
pub fn data_path(self: Arc<Self>, path: String) -> Arc<Self> {
self.0.lock().data_path = Some(path);
self
}
pub fn cache_path(self: Arc<Self>, _path: String) -> Arc<Self> {
self
}
pub fn remote_settings_server(self: Arc<Self>, server: RemoteSettingsServer) -> Arc<Self> {
self.0.lock().remote_settings_server = Some(server);
self
}
pub fn remote_settings_bucket_name(self: Arc<Self>, bucket_name: String) -> Arc<Self> {
self.0.lock().remote_settings_bucket_name = Some(bucket_name);
self
}
pub fn load_extension(
self: Arc<Self>,
library: String,
entry_point: Option<String>,
) -> Arc<Self> {
self.0.lock().extensions_to_load.push(Sqlite3Extension {
library,
entry_point,
});
self
}
#[handle_error(Error)]
pub fn build(&self) -> SuggestApiResult<Arc<SuggestStore>> {
let inner = self.0.lock();
let extensions_to_load = inner.extensions_to_load.clone();
let data_path = inner
.data_path
.clone()
.ok_or_else(|| Error::SuggestStoreBuilder("data_path not specified".to_owned()))?;
let client = RemoteSettingsClient::new(
inner.remote_settings_server.clone(),
inner.remote_settings_bucket_name.clone(),
None,
)?;
Ok(Arc::new(SuggestStore {
inner: SuggestStoreInner::new(data_path, extensions_to_load, client),
}))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, uniffi::Enum)]
pub enum InterruptKind {
Read,
Write,
ReadWrite,
}
#[derive(uniffi::Object)]
pub struct SuggestStore {
inner: SuggestStoreInner<RemoteSettingsClient>,
}
#[uniffi::export]
impl SuggestStore {
#[handle_error(Error)]
#[uniffi::constructor(default(settings_config = None))]
pub fn new(
path: &str,
settings_config: Option<RemoteSettingsConfig>,
) -> SuggestApiResult<Self> {
let client = match settings_config {
Some(settings_config) => RemoteSettingsClient::new(
settings_config.server,
settings_config.bucket_name,
settings_config.server_url,
)?,
None => RemoteSettingsClient::new(None, None, None)?,
};
Ok(Self {
inner: SuggestStoreInner::new(path.to_owned(), vec![], client),
})
}
#[handle_error(Error)]
pub fn query(&self, query: SuggestionQuery) -> SuggestApiResult<Vec<Suggestion>> {
Ok(self.inner.query(query)?.suggestions)
}
#[handle_error(Error)]
pub fn query_with_metrics(
&self,
query: SuggestionQuery,
) -> SuggestApiResult<QueryWithMetricsResult> {
self.inner.query(query)
}
#[handle_error(Error)]
pub fn dismiss_suggestion(&self, suggestion_url: String) -> SuggestApiResult<()> {
self.inner.dismiss_suggestion(suggestion_url)
}
#[handle_error(Error)]
pub fn clear_dismissed_suggestions(&self) -> SuggestApiResult<()> {
self.inner.clear_dismissed_suggestions()
}
#[uniffi::method(default(kind = None))]
pub fn interrupt(&self, kind: Option<InterruptKind>) {
self.inner.interrupt(kind)
}
#[handle_error(Error)]
pub fn ingest(
&self,
constraints: SuggestIngestionConstraints,
) -> SuggestApiResult<SuggestIngestionMetrics> {
self.inner.ingest(constraints)
}
#[handle_error(Error)]
pub fn clear(&self) -> SuggestApiResult<()> {
self.inner.clear()
}
#[handle_error(Error)]
pub fn fetch_global_config(&self) -> SuggestApiResult<SuggestGlobalConfig> {
self.inner.fetch_global_config()
}
#[handle_error(Error)]
pub fn fetch_provider_config(
&self,
provider: SuggestionProvider,
) -> SuggestApiResult<Option<SuggestProviderConfig>> {
self.inner.fetch_provider_config(provider)
}
#[handle_error(Error)]
pub fn fetch_geonames(
&self,
query: &str,
match_name_prefix: bool,
geoname_type: Option<GeonameType>,
filter: Option<Vec<Geoname>>,
) -> SuggestApiResult<Vec<GeonameMatch>> {
self.inner
.fetch_geonames(query, match_name_prefix, geoname_type, filter)
}
}
impl SuggestStore {
pub fn force_reingest(&self) {
self.inner.force_reingest()
}
}
#[cfg(feature = "benchmark_api")]
impl SuggestStore {
pub fn checkpoint(&self) {
self.inner.checkpoint();
}
}
#[derive(Clone, Default, Debug, uniffi::Record)]
pub struct SuggestIngestionConstraints {
#[uniffi(default = None)]
pub providers: Option<Vec<SuggestionProvider>>,
#[uniffi(default = None)]
pub provider_constraints: Option<SuggestionProviderConstraints>,
#[uniffi(default = false)]
pub empty_only: bool,
}
impl SuggestIngestionConstraints {
pub fn all_providers() -> Self {
Self {
providers: Some(vec![
SuggestionProvider::Amp,
SuggestionProvider::Wikipedia,
SuggestionProvider::Amo,
SuggestionProvider::Pocket,
SuggestionProvider::Yelp,
SuggestionProvider::Mdn,
SuggestionProvider::Weather,
SuggestionProvider::AmpMobile,
SuggestionProvider::Fakespot,
SuggestionProvider::Exposure,
]),
..Self::default()
}
}
fn matches_exposure_record(&self, record: &DownloadedExposureRecord) -> bool {
match self
.provider_constraints
.as_ref()
.and_then(|c| c.exposure_suggestion_types.as_ref())
{
None => false,
Some(suggestion_types) => suggestion_types
.iter()
.any(|t| *t == record.suggestion_type),
}
}
}
pub(crate) struct SuggestStoreInner<S> {
#[allow(unused)]
data_path: PathBuf,
dbs: OnceCell<SuggestStoreDbs>,
extensions_to_load: Vec<Sqlite3Extension>,
settings_client: S,
}
impl<S> SuggestStoreInner<S> {
pub fn new(
data_path: impl Into<PathBuf>,
extensions_to_load: Vec<Sqlite3Extension>,
settings_client: S,
) -> Self {
Self {
data_path: data_path.into(),
extensions_to_load,
dbs: OnceCell::new(),
settings_client,
}
}
fn dbs(&self) -> Result<&SuggestStoreDbs> {
self.dbs
.get_or_try_init(|| SuggestStoreDbs::open(&self.data_path, &self.extensions_to_load))
}
fn query(&self, query: SuggestionQuery) -> Result<QueryWithMetricsResult> {
let mut metrics = SuggestQueryMetrics::default();
let mut suggestions = vec![];
let unique_providers = query.providers.iter().collect::<HashSet<_>>();
let reader = &self.dbs()?.reader;
for provider in unique_providers {
let new_suggestions = metrics.measure_query(provider.to_string(), || {
reader.read(|dao| match provider {
SuggestionProvider::Amp => {
dao.fetch_amp_suggestions(&query, AmpSuggestionType::Desktop)
}
SuggestionProvider::AmpMobile => {
dao.fetch_amp_suggestions(&query, AmpSuggestionType::Mobile)
}
SuggestionProvider::Wikipedia => dao.fetch_wikipedia_suggestions(&query),
SuggestionProvider::Amo => dao.fetch_amo_suggestions(&query),
SuggestionProvider::Pocket => dao.fetch_pocket_suggestions(&query),
SuggestionProvider::Yelp => dao.fetch_yelp_suggestions(&query),
SuggestionProvider::Mdn => dao.fetch_mdn_suggestions(&query),
SuggestionProvider::Weather => dao.fetch_weather_suggestions(&query),
SuggestionProvider::Fakespot => dao.fetch_fakespot_suggestions(&query),
SuggestionProvider::Exposure => dao.fetch_exposure_suggestions(&query),
})
})?;
suggestions.extend(new_suggestions);
}
suggestions.sort();
if let Some(limit) = query.limit.and_then(|limit| usize::try_from(limit).ok()) {
suggestions.truncate(limit);
}
Ok(QueryWithMetricsResult {
suggestions,
query_times: metrics.times,
})
}
fn dismiss_suggestion(&self, suggestion_url: String) -> Result<()> {
self.dbs()?
.writer
.write(|dao| dao.insert_dismissal(&suggestion_url))
}
fn clear_dismissed_suggestions(&self) -> Result<()> {
self.dbs()?.writer.write(|dao| dao.clear_dismissals())?;
Ok(())
}
fn interrupt(&self, kind: Option<InterruptKind>) {
if let Some(dbs) = self.dbs.get() {
match kind.unwrap_or(InterruptKind::Read) {
InterruptKind::Read => {
dbs.reader.interrupt_handle.interrupt();
}
InterruptKind::Write => {
dbs.writer.interrupt_handle.interrupt();
}
InterruptKind::ReadWrite => {
dbs.reader.interrupt_handle.interrupt();
dbs.writer.interrupt_handle.interrupt();
}
}
}
}
fn clear(&self) -> Result<()> {
self.dbs()?.writer.write(|dao| dao.clear())
}
pub fn fetch_global_config(&self) -> Result<SuggestGlobalConfig> {
self.dbs()?.reader.read(|dao| dao.get_global_config())
}
pub fn fetch_provider_config(
&self,
provider: SuggestionProvider,
) -> Result<Option<SuggestProviderConfig>> {
self.dbs()?
.reader
.read(|dao| dao.get_provider_config(provider))
}
pub fn force_reingest(&self) {
let writer = &self.dbs().unwrap().writer;
writer.write(|dao| dao.force_reingest()).unwrap();
}
fn fetch_geonames(
&self,
query: &str,
match_name_prefix: bool,
geoname_type: Option<GeonameType>,
filter: Option<Vec<Geoname>>,
) -> Result<Vec<GeonameMatch>> {
self.dbs()?.reader.read(|dao| {
dao.fetch_geonames(
query,
match_name_prefix,
geoname_type,
filter.as_ref().map(|f| f.iter().collect()),
)
})
}
}
impl<S> SuggestStoreInner<S>
where
S: Client,
{
pub fn ingest(
&self,
constraints: SuggestIngestionConstraints,
) -> Result<SuggestIngestionMetrics> {
breadcrumb!("Ingestion starting");
let writer = &self.dbs()?.writer;
let mut metrics = SuggestIngestionMetrics::default();
if constraints.empty_only && !writer.read(|dao| dao.suggestions_table_empty())? {
return Ok(metrics);
}
let mut record_types_by_collection = HashMap::<Collection, BTreeSet<_>>::new();
for p in constraints
.providers
.as_ref()
.unwrap_or(&DEFAULT_INGEST_PROVIDERS.to_vec())
.iter()
{
for t in p.record_types() {
record_types_by_collection
.entry(t.collection())
.or_default()
.insert(t);
}
}
for rt in [SuggestRecordType::Icon, SuggestRecordType::GlobalConfig] {
record_types_by_collection
.entry(rt.collection())
.or_default()
.insert(rt);
}
let mut write_scope = writer.write_scope()?;
let ingested_records = write_scope.read(|dao| dao.get_ingested_records())?;
for (collection, record_types) in record_types_by_collection {
breadcrumb!("Ingesting collection {}", collection.name());
let records =
write_scope.write(|dao| self.settings_client.get_records(collection, dao))?;
for record_type in record_types {
breadcrumb!("Ingesting record_type: {record_type}");
metrics.measure_ingest(record_type.to_string(), |context| {
let changes = RecordChanges::new(
records.iter().filter(|r| r.record_type() == record_type),
ingested_records.iter().filter(|i| {
i.record_type == record_type.as_str()
&& i.collection == collection.name()
}),
);
write_scope.write(|dao| {
self.process_changes(dao, collection, changes, &constraints, context)
})
})?;
write_scope.err_if_interrupted()?;
}
}
breadcrumb!("Ingestion complete");
Ok(metrics)
}
fn process_changes(
&self,
dao: &mut SuggestDao,
collection: Collection,
changes: RecordChanges<'_>,
constraints: &SuggestIngestionConstraints,
context: &mut MetricsContext,
) -> Result<()> {
for record in &changes.new {
log::trace!("Ingesting record ID: {}", record.id.as_str());
self.process_record(dao, record, constraints, context)?;
}
for record in &changes.updated {
log::trace!("Reingesting updated record ID: {}", record.id.as_str());
dao.delete_record_data(&record.id)?;
self.process_record(dao, record, constraints, context)?;
}
for record in &changes.unchanged {
if self.should_reprocess_record(dao, record, constraints)? {
log::trace!("Reingesting unchanged record ID: {}", record.id.as_str());
self.process_record(dao, record, constraints, context)?;
}
}
for record in &changes.deleted {
log::trace!("Deleting record ID: {:?}", record.id);
dao.delete_record_data(&record.id)?;
}
dao.update_ingested_records(
collection.name(),
&changes.new,
&changes.updated,
&changes.deleted,
)?;
Ok(())
}
fn process_record(
&self,
dao: &mut SuggestDao,
record: &Record,
constraints: &SuggestIngestionConstraints,
context: &mut MetricsContext,
) -> Result<()> {
match &record.payload {
SuggestRecord::AmpWikipedia => {
self.download_attachment(dao, record, context, |dao, record_id, suggestions| {
dao.insert_amp_wikipedia_suggestions(record_id, suggestions)
})?;
}
SuggestRecord::AmpMobile => {
self.download_attachment(dao, record, context, |dao, record_id, suggestions| {
dao.insert_amp_mobile_suggestions(record_id, suggestions)
})?;
}
SuggestRecord::Icon => {
let (Some(icon_id), Some(attachment)) =
(record.id.as_icon_id(), record.attachment.as_ref())
else {
return Ok(());
};
let data = context
.measure_download(|| self.settings_client.download_attachment(record))?;
dao.put_icon(icon_id, &data, &attachment.mimetype)?;
}
SuggestRecord::Amo => {
self.download_attachment(dao, record, context, |dao, record_id, suggestions| {
dao.insert_amo_suggestions(record_id, suggestions)
})?;
}
SuggestRecord::Pocket => {
self.download_attachment(dao, record, context, |dao, record_id, suggestions| {
dao.insert_pocket_suggestions(record_id, suggestions)
})?;
}
SuggestRecord::Yelp => {
self.download_attachment(dao, record, context, |dao, record_id, suggestions| {
match suggestions.first() {
Some(suggestion) => dao.insert_yelp_suggestions(record_id, suggestion),
None => Ok(()),
}
})?;
}
SuggestRecord::Mdn => {
self.download_attachment(dao, record, context, |dao, record_id, suggestions| {
dao.insert_mdn_suggestions(record_id, suggestions)
})?;
}
SuggestRecord::Weather => self.process_weather_record(dao, record, context)?,
SuggestRecord::GlobalConfig(config) => {
dao.put_global_config(&SuggestGlobalConfig::from(config))?
}
SuggestRecord::Fakespot => {
self.download_attachment(dao, record, context, |dao, record_id, suggestions| {
dao.insert_fakespot_suggestions(record_id, suggestions)
})?;
}
SuggestRecord::Exposure(r) => {
if constraints.matches_exposure_record(r) {
self.download_attachment(
dao,
record,
context,
|dao, record_id, suggestions| {
dao.insert_exposure_suggestions(
record_id,
&r.suggestion_type,
suggestions,
)
},
)?;
}
}
SuggestRecord::Geonames => self.process_geoname_record(dao, record, context)?,
}
Ok(())
}
pub(crate) fn download_attachment<T>(
&self,
dao: &mut SuggestDao,
record: &Record,
context: &mut MetricsContext,
ingestion_handler: impl FnOnce(&mut SuggestDao<'_>, &SuggestRecordId, &[T]) -> Result<()>,
) -> Result<()>
where
T: DeserializeOwned,
{
if record.attachment.is_none() {
return Ok(());
};
let attachment_data =
context.measure_download(|| self.settings_client.download_attachment(record))?;
match serde_json::from_slice::<SuggestAttachment<T>>(&attachment_data) {
Ok(attachment) => ingestion_handler(dao, &record.id, attachment.suggestions()),
Err(_) => Ok(()),
}
}
fn should_reprocess_record(
&self,
dao: &mut SuggestDao,
record: &Record,
constraints: &SuggestIngestionConstraints,
) -> Result<bool> {
match &record.payload {
SuggestRecord::Exposure(r) => {
Ok(!dao.is_exposure_suggestion_ingested(&record.id)?
&& constraints.matches_exposure_record(r))
}
_ => Ok(false),
}
}
}
struct RecordChanges<'a> {
new: Vec<&'a Record>,
updated: Vec<&'a Record>,
deleted: Vec<&'a IngestedRecord>,
unchanged: Vec<&'a Record>,
}
impl<'a> RecordChanges<'a> {
fn new(
current: impl Iterator<Item = &'a Record>,
previously_ingested: impl Iterator<Item = &'a IngestedRecord>,
) -> Self {
let mut ingested_map: HashMap<&str, &IngestedRecord> =
previously_ingested.map(|i| (i.id.as_str(), i)).collect();
let mut new = vec![];
let mut updated = vec![];
let mut unchanged = vec![];
for r in current {
match ingested_map.entry(r.id.as_str()) {
Entry::Vacant(_) => new.push(r),
Entry::Occupied(e) => {
if e.remove().last_modified != r.last_modified {
updated.push(r);
} else {
unchanged.push(r);
}
}
}
}
let deleted = ingested_map.into_values().collect();
Self {
new,
deleted,
updated,
unchanged,
}
}
}
#[cfg(feature = "benchmark_api")]
impl<S> SuggestStoreInner<S>
where
S: Client,
{
pub fn into_settings_client(self) -> S {
self.settings_client
}
pub fn ensure_db_initialized(&self) {
self.dbs().unwrap();
}
fn checkpoint(&self) {
let conn = self.dbs().unwrap().writer.conn.lock();
conn.pragma_update(None, "wal_checkpoint", "TRUNCATE")
.expect("Error performing checkpoint");
}
pub fn ingest_records_by_type(&self, ingest_record_type: SuggestRecordType) {
let writer = &self.dbs().unwrap().writer;
let mut context = MetricsContext::default();
let ingested_records = writer.read(|dao| dao.get_ingested_records()).unwrap();
let records = writer
.write(|dao| {
self.settings_client
.get_records(ingest_record_type.collection(), dao)
})
.unwrap();
let changes = RecordChanges::new(
records
.iter()
.filter(|r| r.record_type() == ingest_record_type),
ingested_records
.iter()
.filter(|i| i.record_type == ingest_record_type.as_str()),
);
writer
.write(|dao| {
self.process_changes(
dao,
ingest_record_type.collection(),
changes,
&SuggestIngestionConstraints::default(),
&mut context,
)
})
.unwrap();
}
pub fn table_row_counts(&self) -> Vec<(String, u32)> {
use sql_support::ConnExt;
let reader = &self.dbs().unwrap().reader;
let conn = reader.conn.lock();
let table_names: Vec<String> = conn
.query_rows_and_then(
"SELECT name FROM sqlite_master where type = 'table'",
(),
|row| row.get(0),
)
.unwrap();
let mut table_names_with_counts: Vec<(String, u32)> = table_names
.into_iter()
.map(|name| {
let count: u32 = conn
.query_one(&format!("SELECT COUNT(*) FROM {name}"))
.unwrap();
(name, count)
})
.collect();
table_names_with_counts.sort_by(|a, b| (b.1.cmp(&a.1)));
table_names_with_counts
}
pub fn db_size(&self) -> usize {
use sql_support::ConnExt;
let reader = &self.dbs().unwrap().reader;
let conn = reader.conn.lock();
conn.query_one("SELECT page_size * page_count FROM pragma_page_count(), pragma_page_size()")
.unwrap()
}
}
struct SuggestStoreDbs {
writer: SuggestDb,
reader: SuggestDb,
}
impl SuggestStoreDbs {
fn open(path: &Path, extensions_to_load: &[Sqlite3Extension]) -> Result<Self> {
let writer = SuggestDb::open(path, extensions_to_load, ConnectionType::ReadWrite)?;
let reader = SuggestDb::open(path, extensions_to_load, ConnectionType::ReadOnly)?;
Ok(Self { writer, reader })
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::{testing::*, SuggestionProvider};
pub(crate) struct TestStore {
pub inner: SuggestStoreInner<MockRemoteSettingsClient>,
}
impl TestStore {
pub fn new(client: MockRemoteSettingsClient) -> Self {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let db_path = format!(
"file:test_store_data_{}?mode=memory&cache=shared",
COUNTER.fetch_add(1, Ordering::Relaxed),
);
Self {
inner: SuggestStoreInner::new(db_path, vec![], client),
}
}
pub fn client_mut(&mut self) -> &mut MockRemoteSettingsClient {
&mut self.inner.settings_client
}
pub fn read<T>(&self, op: impl FnOnce(&SuggestDao) -> Result<T>) -> Result<T> {
self.inner.dbs().unwrap().reader.read(op)
}
pub fn count_rows(&self, table_name: &str) -> u64 {
let sql = format!("SELECT count(*) FROM {table_name}");
self.read(|dao| Ok(dao.conn.query_one(&sql)?))
.unwrap_or_else(|e| panic!("SQL error in count: {e}"))
}
pub fn ingest(&self, constraints: SuggestIngestionConstraints) {
self.inner.ingest(constraints).unwrap();
}
pub fn fetch_suggestions(&self, query: SuggestionQuery) -> Vec<Suggestion> {
self.inner.query(query).unwrap().suggestions
}
pub fn fetch_global_config(&self) -> SuggestGlobalConfig {
self.inner
.fetch_global_config()
.expect("Error fetching global config")
}
pub fn fetch_provider_config(
&self,
provider: SuggestionProvider,
) -> Option<SuggestProviderConfig> {
self.inner
.fetch_provider_config(provider)
.expect("Error fetching provider config")
}
pub fn fetch_geonames(
&self,
query: &str,
match_name_prefix: bool,
geoname_type: Option<GeonameType>,
filter: Option<Vec<Geoname>>,
) -> Vec<GeonameMatch> {
self.inner
.fetch_geonames(query, match_name_prefix, geoname_type, filter)
.expect("Error fetching geonames")
}
}
#[test]
fn is_thread_safe() {
before_each();
fn is_send_sync<T: Send + Sync>() {}
is_send_sync::<SuggestStore>();
}
#[test]
fn ingest_suggestions() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record("data", "1234", json![los_pollos_amp()])
.with_icon(los_pollos_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp("lo")),
vec![los_pollos_suggestion("los")],
);
Ok(())
}
#[test]
fn ingest_empty_only() -> anyhow::Result<()> {
before_each();
let mut store = TestStore::new(MockRemoteSettingsClient::default().with_record(
"data",
"1234",
json![los_pollos_amp()],
));
assert!(store.read(|dao| dao.suggestions_table_empty())?);
store.ingest(SuggestIngestionConstraints {
empty_only: true,
..SuggestIngestionConstraints::all_providers()
});
assert!(!store.read(|dao| dao.suggestions_table_empty())?);
store.client_mut().update_record(
"data",
"1234",
json!([los_pollos_amp(), good_place_eats_amp()]),
);
store.ingest(SuggestIngestionConstraints {
empty_only: true,
..SuggestIngestionConstraints::all_providers()
});
assert_eq!(store.fetch_suggestions(SuggestionQuery::amp("la")), vec![]);
Ok(())
}
#[test]
fn ingest_amp_icons() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record(
"data",
"1234",
json!([los_pollos_amp(), good_place_eats_amp()]),
)
.with_icon(los_pollos_icon())
.with_icon(good_place_eats_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp("lo")),
vec![los_pollos_suggestion("los")]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp("la")),
vec![good_place_eats_suggestion("lasagna")]
);
Ok(())
}
#[test]
fn ingest_full_keywords() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(MockRemoteSettingsClient::default()
.with_record("data", "1234", json!([
los_pollos_amp().merge(json!({
"keywords": ["lo", "los", "los p", "los pollos", "los pollos h", "los pollos hermanos"],
"full_keywords": [
("los pollos", 4),
("los pollos hermanos (restaurant)", 2),
],
})),
good_place_eats_amp(),
california_wiki(),
]))
.with_record("amp-mobile-suggestions", "2468", json!([
a1a_amp_mobile().merge(json!({
"keywords": ["a1a", "ca", "car", "car wash"],
"full_keywords": [
("A1A Car Wash", 1),
("car wash", 3),
],
})),
]))
.with_icon(los_pollos_icon())
.with_icon(good_place_eats_icon())
.with_icon(california_icon())
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp("lo")),
vec![los_pollos_suggestion("los pollos")],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp("la")),
vec![good_place_eats_suggestion("lasagna")],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::wikipedia("cal")),
vec![california_suggestion("california")],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp_mobile("a1a")),
vec![a1a_suggestion("A1A Car Wash")],
);
Ok(())
}
#[test]
fn ingest_one_suggestion_in_data_attachment() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record("data", "1234", los_pollos_amp())
.with_icon(los_pollos_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp("lo")),
vec![los_pollos_suggestion("los")],
);
Ok(())
}
#[test]
fn reingest_amp_suggestions() -> anyhow::Result<()> {
before_each();
let mut store = TestStore::new(MockRemoteSettingsClient::default().with_record(
"data",
"1234",
json!([los_pollos_amp(), good_place_eats_amp()]),
));
store.ingest(SuggestIngestionConstraints::all_providers());
store.client_mut().update_record(
"data",
"1234",
json!([
los_pollos_amp().merge(json!({
"title": "Los Pollos Hermanos - Now Serving at 14 Locations!",
})),
good_place_eats_amp().merge(json!({
"keywords": ["pe", "pen", "penne", "penne for your thoughts"],
"title": "Penne for Your Thoughts",
"url": "https://penne.biz",
}))
]),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert!(matches!(
store.fetch_suggestions(SuggestionQuery::amp("lo")).as_slice(),
[ Suggestion::Amp { title, .. } ] if title == "Los Pollos Hermanos - Now Serving at 14 Locations!",
));
assert_eq!(store.fetch_suggestions(SuggestionQuery::amp("la")), vec![]);
assert!(matches!(
store.fetch_suggestions(SuggestionQuery::amp("pe")).as_slice(),
[ Suggestion::Amp { title, url, .. } ] if title == "Penne for Your Thoughts" && url == "https://penne.biz"
));
Ok(())
}
#[test]
fn reingest_icons() -> anyhow::Result<()> {
before_each();
let mut store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record(
"data",
"1234",
json!([los_pollos_amp(), good_place_eats_amp()]),
)
.with_icon(los_pollos_icon())
.with_icon(good_place_eats_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
store
.client_mut()
.update_record(
"data",
"1234",
json!([
los_pollos_amp().merge(json!({"icon": "1000"})),
good_place_eats_amp()
]),
)
.delete_icon(los_pollos_icon())
.add_icon(MockIcon {
id: "1000",
data: "new-los-pollos-icon",
..los_pollos_icon()
})
.update_icon(MockIcon {
data: "new-good-place-eats-icon",
..good_place_eats_icon()
});
store.ingest(SuggestIngestionConstraints::all_providers());
assert!(matches!(
store.fetch_suggestions(SuggestionQuery::amp("lo")).as_slice(),
[ Suggestion::Amp { icon, .. } ] if *icon == Some("new-los-pollos-icon".as_bytes().to_vec())
));
assert!(matches!(
store.fetch_suggestions(SuggestionQuery::amp("la")).as_slice(),
[ Suggestion::Amp { icon, .. } ] if *icon == Some("new-good-place-eats-icon".as_bytes().to_vec())
));
Ok(())
}
#[test]
fn reingest_amo_suggestions() -> anyhow::Result<()> {
before_each();
let mut store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record("amo-suggestions", "data-1", json!([relay_amo()]))
.with_record(
"amo-suggestions",
"data-2",
json!([dark_mode_amo(), foxy_guestures_amo()]),
),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amo("masking e")),
vec![relay_suggestion()],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amo("night")),
vec![dark_mode_suggestion()],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amo("grammar")),
vec![foxy_guestures_suggestion()],
);
store
.client_mut()
.update_record("amo-suggestions", "data-1", json!([relay_amo()]))
.update_record(
"amo-suggestions",
"data-2",
json!([
dark_mode_amo().merge(json!({"title": "Updated second suggestion"})),
new_tab_override_amo(),
]),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amo("masking e")),
vec![relay_suggestion()],
);
assert!(matches!(
store.fetch_suggestions(SuggestionQuery::amo("night")).as_slice(),
[Suggestion::Amo { title, .. } ] if title == "Updated second suggestion"
));
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amo("grammar")),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amo("image search")),
vec![new_tab_override_suggestion()],
);
Ok(())
}
#[test]
fn ingest_with_deletions() -> anyhow::Result<()> {
before_each();
let mut store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record("data", "data-1", json!([los_pollos_amp()]))
.with_record("data", "data-2", json!([good_place_eats_amp()]))
.with_icon(los_pollos_icon())
.with_icon(good_place_eats_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp("lo")),
vec![los_pollos_suggestion("los")],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp("la")),
vec![good_place_eats_suggestion("lasagna")],
);
store
.client_mut()
.delete_record("quicksuggest", "data-1")
.delete_icon(good_place_eats_icon());
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(store.fetch_suggestions(SuggestionQuery::amp("lo")), vec![]);
assert!(matches!(
store.fetch_suggestions(SuggestionQuery::amp("la")).as_slice(),
[
Suggestion::Amp { icon, icon_mimetype, .. }
] if icon.is_none() && icon_mimetype.is_none(),
));
Ok(())
}
#[test]
fn clear() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record("data", "data-1", json!([los_pollos_amp()]))
.with_record("data", "data-2", json!([good_place_eats_amp()]))
.with_icon(los_pollos_icon())
.with_icon(good_place_eats_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert!(store.count_rows("suggestions") > 0);
assert!(store.count_rows("keywords") > 0);
assert!(store.count_rows("icons") > 0);
store.inner.clear()?;
assert!(store.count_rows("suggestions") == 0);
assert!(store.count_rows("keywords") == 0);
assert!(store.count_rows("icons") == 0);
Ok(())
}
#[test]
fn query() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record(
"data",
"data-1",
json!([
good_place_eats_amp(),
california_wiki(),
caltech_wiki(),
multimatch_wiki(),
]),
)
.with_record(
"amo-suggestions",
"data-2",
json!([relay_amo(), multimatch_amo(),]),
)
.with_record(
"pocket-suggestions",
"data-3",
json!([burnout_pocket(), multimatch_pocket(),]),
)
.with_record("yelp-suggestions", "data-4", json!([ramen_yelp(),]))
.with_record("mdn-suggestions", "data-5", json!([array_mdn(),]))
.with_icon(good_place_eats_icon())
.with_icon(california_icon())
.with_icon(caltech_icon())
.with_icon(yelp_favicon())
.with_icon(multimatch_wiki_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::all_providers("")),
vec![]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::all_providers("la")),
vec![good_place_eats_suggestion("lasagna"),]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::all_providers("multimatch")),
vec![
multimatch_pocket_suggestion(true),
multimatch_amo_suggestion(),
multimatch_wiki_suggestion(),
]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::all_providers("MultiMatch")),
vec![
multimatch_pocket_suggestion(true),
multimatch_amo_suggestion(),
multimatch_wiki_suggestion(),
]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::all_providers("multimatch").limit(2)),
vec![
multimatch_pocket_suggestion(true),
multimatch_amo_suggestion(),
],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp("la")),
vec![good_place_eats_suggestion("lasagna")],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::all_providers_except(
"la",
SuggestionProvider::Amp
)),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::with_providers("la", vec![])),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::with_providers(
"cal",
vec![
SuggestionProvider::Amp,
SuggestionProvider::Amo,
SuggestionProvider::Pocket,
]
)),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::wikipedia("cal")),
vec![
california_suggestion("california"),
caltech_suggestion("california"),
],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::wikipedia("cal").limit(1)),
vec![california_suggestion("california"),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::with_providers("cal", vec![])),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amo("spam")),
vec![relay_suggestion()],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amo("masking")),
vec![relay_suggestion()],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amo("masking e")),
vec![relay_suggestion()],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amo("masking s")),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::with_providers(
"soft",
vec![SuggestionProvider::Amp, SuggestionProvider::Wikipedia]
)),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::pocket("soft")),
vec![burnout_suggestion(false),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::pocket("soft l")),
vec![burnout_suggestion(false),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::pocket("sof")),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::pocket("burnout women")),
vec![burnout_suggestion(true),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::pocket("burnout person")),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("best spicy ramen delivery in tokyo")),
vec![ramen_suggestion(
"best spicy ramen delivery in tokyo",
"https://www.yelp.com/search?find_desc=best+spicy+ramen+delivery&find_loc=tokyo"
),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("BeSt SpIcY rAmEn DeLiVeRy In ToKyO")),
vec![ramen_suggestion(
"BeSt SpIcY rAmEn DeLiVeRy In ToKyO",
"https://www.yelp.com/search?find_desc=BeSt+SpIcY+rAmEn+DeLiVeRy&find_loc=ToKyO"
),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("best ramen delivery in tokyo")),
vec![ramen_suggestion(
"best ramen delivery in tokyo",
"https://www.yelp.com/search?find_desc=best+ramen+delivery&find_loc=tokyo"
),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp(
"best invalid_ramen delivery in tokyo"
)),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("best in tokyo")),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("super best ramen in tokyo")),
vec![ramen_suggestion(
"super best ramen in tokyo",
"https://www.yelp.com/search?find_desc=super+best+ramen&find_loc=tokyo"
),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("invalid_best ramen in tokyo")),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen delivery in tokyo")),
vec![ramen_suggestion(
"ramen delivery in tokyo",
"https://www.yelp.com/search?find_desc=ramen+delivery&find_loc=tokyo"
),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen super delivery in tokyo")),
vec![ramen_suggestion(
"ramen super delivery in tokyo",
"https://www.yelp.com/search?find_desc=ramen+super+delivery&find_loc=tokyo"
),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen invalid_delivery in tokyo")),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen in tokyo")),
vec![ramen_suggestion(
"ramen in tokyo",
"https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo"
),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen near tokyo")),
vec![ramen_suggestion(
"ramen near tokyo",
"https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo"
),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen invalid_in tokyo")),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen in San Francisco")),
vec![ramen_suggestion(
"ramen in San Francisco",
"https://www.yelp.com/search?find_desc=ramen&find_loc=San+Francisco"
),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen in")),
vec![ramen_suggestion(
"ramen in",
"https://www.yelp.com/search?find_desc=ramen"
),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen near by")),
vec![ramen_suggestion(
"ramen near by",
"https://www.yelp.com/search?find_desc=ramen+near+by"
)
.has_location_sign(false),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen near me")),
vec![ramen_suggestion(
"ramen near me",
"https://www.yelp.com/search?find_desc=ramen+near+me"
)
.has_location_sign(false),],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen near by tokyo")),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen")),
vec![
ramen_suggestion("ramen", "https://www.yelp.com/search?find_desc=ramen")
.has_location_sign(false),
],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp(
"012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
)),
vec![
ramen_suggestion(
"012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789",
"https://www.yelp.com/search?find_desc=012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
).has_location_sign(false),
],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp(
"012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789Z"
)),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("best delivery")),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("same_modifier same_modifier")),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("same_modifier ")),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("yelp ramen")),
vec![
ramen_suggestion("ramen", "https://www.yelp.com/search?find_desc=ramen")
.has_location_sign(false),
],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("yelp keyword ramen")),
vec![
ramen_suggestion("ramen", "https://www.yelp.com/search?find_desc=ramen")
.has_location_sign(false),
],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen in tokyo yelp")),
vec![ramen_suggestion(
"ramen in tokyo",
"https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo"
)],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen in tokyo yelp keyword")),
vec![ramen_suggestion(
"ramen in tokyo",
"https://www.yelp.com/search?find_desc=ramen&find_loc=tokyo"
)],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("yelp ramen yelp")),
vec![
ramen_suggestion("ramen", "https://www.yelp.com/search?find_desc=ramen")
.has_location_sign(false)
],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("best yelp ramen")),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("Spicy R")),
vec![ramen_suggestion(
"Spicy Ramen",
"https://www.yelp.com/search?find_desc=Spicy+Ramen"
)
.has_location_sign(false)
.subject_exact_match(false)],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("BeSt Ramen")),
vec![ramen_suggestion(
"BeSt Ramen",
"https://www.yelp.com/search?find_desc=BeSt+Ramen"
)
.has_location_sign(false)],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("BeSt Spicy R")),
vec![ramen_suggestion(
"BeSt Spicy Ramen",
"https://www.yelp.com/search?find_desc=BeSt+Spicy+Ramen"
)
.has_location_sign(false)
.subject_exact_match(false)],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("BeSt R")),
vec![],
);
assert_eq!(store.fetch_suggestions(SuggestionQuery::yelp("r")), vec![],);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ra")),
vec![
ramen_suggestion("rats", "https://www.yelp.com/search?find_desc=rats")
.has_location_sign(false)
.subject_exact_match(false)
],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("ram")),
vec![
ramen_suggestion("ramen", "https://www.yelp.com/search?find_desc=ramen")
.has_location_sign(false)
.subject_exact_match(false)
],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("rac")),
vec![
ramen_suggestion("raccoon", "https://www.yelp.com/search?find_desc=raccoon")
.has_location_sign(false)
.subject_exact_match(false)
],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("best r")),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("best ra")),
vec![ramen_suggestion(
"best rats",
"https://www.yelp.com/search?find_desc=best+rats"
)
.has_location_sign(false)
.subject_exact_match(false)],
);
Ok(())
}
#[test]
fn query_with_multiple_providers_and_diff_scores() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record(
"data",
"data-1",
json!([
los_pollos_amp().merge(json!({
"keywords": ["amp wiki match"],
"score": 0.3,
})),
good_place_eats_amp().merge(json!({
"keywords": ["amp wiki match"],
"score": 0.1,
})),
california_wiki().merge(json!({
"keywords": ["amp wiki match", "pocket wiki match"],
})),
]),
)
.with_record(
"pocket-suggestions",
"data-3",
json!([
burnout_pocket().merge(json!({
"lowConfidenceKeywords": ["work-life balance", "pocket wiki match"],
"score": 0.05,
})),
multimatch_pocket().merge(json!({
"highConfidenceKeywords": ["pocket wiki match"],
"score": 0.88,
})),
]),
)
.with_icon(los_pollos_icon())
.with_icon(good_place_eats_icon())
.with_icon(california_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::all_providers("amp wiki match")),
vec![
los_pollos_suggestion("amp wiki match").with_score(0.3),
california_suggestion("amp wiki match"),
good_place_eats_suggestion("amp wiki match").with_score(0.1),
]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::all_providers("amp wiki match").limit(2)),
vec![
los_pollos_suggestion("amp wiki match").with_score(0.3),
california_suggestion("amp wiki match"),
]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::all_providers("pocket wiki match")),
vec![
multimatch_pocket_suggestion(true).with_score(0.88),
california_suggestion("pocket wiki match"),
burnout_suggestion(false).with_score(0.05),
]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::all_providers("pocket wiki match").limit(1)),
vec![multimatch_pocket_suggestion(true).with_score(0.88),]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::with_providers(
"work-life balance",
vec![SuggestionProvider::Pocket, SuggestionProvider::Pocket],
)),
vec![burnout_suggestion(false).with_score(0.05),]
);
Ok(())
}
#[test]
fn query_with_amp_mobile_provider() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record(
"amp-mobile-suggestions",
"amp-mobile-1",
json!([good_place_eats_amp()]),
)
.with_record("data", "data-1", json!([good_place_eats_amp()]))
.with_icon(good_place_eats_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp_mobile("las")),
vec![good_place_eats_suggestion("lasagna")]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp("las")),
vec![good_place_eats_suggestion("lasagna")]
);
Ok(())
}
#[test]
fn ingest_malformed() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record_but_no_attachment("data", "data-1")
.with_record_but_no_attachment("icon", "icon-1")
.with_record("icon", "bad-icon-id", json!("i-am-an-icon")),
);
store.ingest(SuggestIngestionConstraints::all_providers());
store.read(|dao| {
assert_eq!(
dao.conn
.query_one::<i64>("SELECT count(*) FROM suggestions")?,
0
);
assert_eq!(dao.conn.query_one::<i64>("SELECT count(*) FROM icons")?, 0);
Ok(())
})?;
Ok(())
}
#[test]
fn ingest_constraints_provider() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record("data", "data-1", json!([los_pollos_amp()]))
.with_record("yelp-suggestions", "yelp-1", json!([ramen_yelp()]))
.with_icon(los_pollos_icon()),
);
let constraints = SuggestIngestionConstraints {
providers: Some(vec![SuggestionProvider::Amp, SuggestionProvider::Pocket]),
..SuggestIngestionConstraints::all_providers()
};
store.ingest(constraints);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp("lo")),
vec![los_pollos_suggestion("los")]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("best ramen")),
vec![]
);
Ok(())
}
#[test]
fn skip_over_invalid_records() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record("data", "data-1", json!([good_place_eats_amp()]))
.with_record(
"data",
"data-2",
json!([{
"id": 1,
"advertiser": "Los Pollos Hermanos",
"iab_category": "8 - Food & Drink",
"keywords": ["lo", "los", "los pollos"],
"url": "https://www.lph-nm.biz",
"icon": "5678",
"impression_url": "https://example.com/impression_url",
"click_url": "https://example.com/click_url",
"score": 0.3
}]),
)
.with_icon(good_place_eats_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp("la")),
vec![good_place_eats_suggestion("lasagna")]
);
assert_eq!(store.fetch_suggestions(SuggestionQuery::amp("lo")), vec![]);
Ok(())
}
#[test]
fn query_mdn() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(MockRemoteSettingsClient::default().with_record(
"mdn-suggestions",
"mdn-1",
json!([array_mdn()]),
));
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::mdn("array")),
vec![array_suggestion(),]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::mdn("array java")),
vec![array_suggestion(),]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::mdn("javascript array")),
vec![array_suggestion(),]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::mdn("wild")),
vec![]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::mdn("wildcard")),
vec![array_suggestion()]
);
Ok(())
}
#[test]
fn query_no_yelp_icon_data() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default().with_record(
"yelp-suggestions",
"yelp-1",
json!([ramen_yelp()]),
), );
store.ingest(SuggestIngestionConstraints::all_providers());
assert!(matches!(
store.fetch_suggestions(SuggestionQuery::yelp("ramen")).as_slice(),
[Suggestion::Yelp { icon, icon_mimetype, .. }] if icon.is_none() && icon_mimetype.is_none()
));
Ok(())
}
#[test]
fn fetch_global_config() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(MockRemoteSettingsClient::default().with_inline_record(
"configuration",
"configuration-1",
json!({
"configuration": {
"show_less_frequently_cap": 3,
},
}),
));
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_global_config(),
SuggestGlobalConfig {
show_less_frequently_cap: 3,
}
);
Ok(())
}
#[test]
fn fetch_global_config_default() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(MockRemoteSettingsClient::default());
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_global_config(),
SuggestGlobalConfig {
show_less_frequently_cap: 0,
}
);
Ok(())
}
#[test]
fn fetch_provider_config_none() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(MockRemoteSettingsClient::default());
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(store.fetch_provider_config(SuggestionProvider::Amp), None);
assert_eq!(
store.fetch_provider_config(SuggestionProvider::Weather),
None
);
Ok(())
}
#[test]
fn fetch_provider_config_other() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(MockRemoteSettingsClient::default().with_record(
"weather",
"weather-1",
json!({
"min_keyword_length": 3,
"score": 0.24,
"max_keyword_length": 1,
"max_keyword_word_count": 1,
"keywords": []
}),
));
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_provider_config(SuggestionProvider::Weather),
Some(SuggestProviderConfig::Weather {
min_keyword_length: 3,
score: 0.24,
})
);
assert_eq!(store.fetch_provider_config(SuggestionProvider::Amp), None);
Ok(())
}
#[test]
fn remove_dismissed_suggestions() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record(
"data",
"data-1",
json!([
good_place_eats_amp().merge(json!({"keywords": ["cats"]})),
california_wiki().merge(json!({"keywords": ["cats"]})),
]),
)
.with_record(
"amo-suggestions",
"amo-1",
json!([relay_amo().merge(json!({"keywords": ["cats"]})),]),
)
.with_record(
"pocket-suggestions",
"pocket-1",
json!([burnout_pocket().merge(json!({
"lowConfidenceKeywords": ["cats"],
}))]),
)
.with_record(
"mdn-suggestions",
"mdn-1",
json!([array_mdn().merge(json!({"keywords": ["cats"]})),]),
)
.with_record(
"amp-mobile-suggestions",
"amp-mobile-1",
json!([a1a_amp_mobile().merge(json!({"keywords": ["cats"]})),]),
)
.with_icon(good_place_eats_icon())
.with_icon(caltech_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
let query = SuggestionQuery::all_providers("cats");
let results = store.fetch_suggestions(query.clone());
assert_eq!(results.len(), 6);
for result in results {
store
.inner
.dismiss_suggestion(result.raw_url().unwrap().to_string())?;
}
assert_eq!(store.fetch_suggestions(query.clone()).len(), 0);
store.inner.clear_dismissed_suggestions()?;
assert_eq!(store.fetch_suggestions(query.clone()).len(), 6);
Ok(())
}
#[test]
fn query_fakespot() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record(
"fakespot-suggestions",
"fakespot-1",
json!([snowglobe_fakespot(), simpsons_fakespot()]),
)
.with_icon(fakespot_amazon_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::fakespot("globe")),
vec![snowglobe_suggestion().with_fakespot_product_type_bonus(0.5)],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::fakespot("simpsons")),
vec![simpsons_suggestion()],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::fakespot("snow")),
vec![
snowglobe_suggestion().with_fakespot_product_type_bonus(0.5),
simpsons_suggestion(),
],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::fakespot("simpsons snow")),
vec![simpsons_suggestion()],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::fakespot("simpsons + snow")),
vec![simpsons_suggestion()],
);
Ok(())
}
#[test]
fn fakespot_keywords() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record(
"fakespot-suggestions",
"fakespot-1",
json!([
snowglobe_fakespot(),
simpsons_fakespot().merge(json!({"keywords": "snow"})),
]),
)
.with_icon(fakespot_amazon_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::fakespot("snow")),
vec![
simpsons_suggestion().with_fakespot_keyword_bonus(),
snowglobe_suggestion().with_fakespot_product_type_bonus(0.5),
],
);
Ok(())
}
#[test]
fn fakespot_prefix_matching() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record(
"fakespot-suggestions",
"fakespot-1",
json!([snowglobe_fakespot(), simpsons_fakespot()]),
)
.with_icon(fakespot_amazon_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::fakespot("simp")),
vec![simpsons_suggestion()],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::fakespot("simps")),
vec![simpsons_suggestion()],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::fakespot("simpson")),
vec![simpsons_suggestion()],
);
Ok(())
}
#[test]
fn fakespot_updates_and_deletes() -> anyhow::Result<()> {
before_each();
let mut store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record(
"fakespot-suggestions",
"fakespot-1",
json!([snowglobe_fakespot(), simpsons_fakespot()]),
)
.with_icon(fakespot_amazon_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
store.client_mut().update_record(
"fakespot-suggestions",
"fakespot-1",
json!([
snowglobe_fakespot().merge(json!({"title": "Make Your Own Sea Glass Snow Globes"}))
]),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::fakespot("glitter")),
vec![],
);
assert!(matches!(
store.fetch_suggestions(SuggestionQuery::fakespot("sea glass")).as_slice(),
[
Suggestion::Fakespot { title, .. }
]
if title == "Make Your Own Sea Glass Snow Globes"
));
assert_eq!(
store.fetch_suggestions(SuggestionQuery::fakespot("simpsons")),
vec![],
);
Ok(())
}
#[test]
fn same_record_id_different_collections() -> anyhow::Result<()> {
before_each();
let mut store = TestStore::new(
MockRemoteSettingsClient::default()
.with_record(
"fakespot-suggestions",
"fakespot-1",
json!([snowglobe_fakespot()]),
)
.with_record("data", "fakespot-1", json![los_pollos_amp()])
.with_icon(los_pollos_icon())
.with_icon(fakespot_amazon_icon()),
);
store.ingest(SuggestIngestionConstraints::all_providers());
assert_eq!(
store.fetch_suggestions(SuggestionQuery::fakespot("globe")),
vec![snowglobe_suggestion().with_fakespot_product_type_bonus(0.5)],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::amp("lo")),
vec![los_pollos_suggestion("los")],
);
store
.client_mut()
.delete_record("quicksuggest", "fakespot-1")
.delete_icon(los_pollos_icon());
store.ingest(SuggestIngestionConstraints::all_providers());
let record_keys = store
.read(|dao| dao.get_ingested_records())
.unwrap()
.into_iter()
.map(|r| format!("{}:{}", r.collection, r.id.as_str()))
.collect::<Vec<_>>();
assert_eq!(
record_keys
.iter()
.map(String::as_str)
.collect::<HashSet<_>>(),
HashSet::from([
"quicksuggest:icon-fakespot-amazon",
"fakespot-suggest-products:fakespot-1"
]),
);
Ok(())
}
#[test]
fn exposure_basic() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_full_record(
"exposure-suggestions",
"exposure-0",
Some(json!({
"suggestion_type": "aaa",
})),
Some(json!({
"keywords": [
"aaa keyword",
"both keyword",
["common prefix", [" aaa"]],
["choco", ["bo", "late"]],
["dup", ["licate 1", "licate 2"]],
],
})),
)
.with_full_record(
"exposure-suggestions",
"exposure-1",
Some(json!({
"suggestion_type": "bbb",
})),
Some(json!({
"keywords": [
"bbb keyword",
"both keyword",
["common prefix", [" bbb"]],
],
})),
),
);
store.ingest(SuggestIngestionConstraints {
providers: Some(vec![SuggestionProvider::Exposure]),
provider_constraints: Some(SuggestionProviderConstraints {
exposure_suggestion_types: Some(vec!["aaa".to_string(), "bbb".to_string()]),
}),
..SuggestIngestionConstraints::all_providers()
});
let no_matches = vec!["aaa", "both", "common prefi", "choc", "chocolate extra"];
for query in &no_matches {
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["aaa"])),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["bbb"])),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["aaa", "bbb"])),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["aaa", "zzz"])),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["zzz"])),
vec![],
);
}
let aaa_only_matches = vec![
"aaa keyword",
"common prefix a",
"common prefix aa",
"common prefix aaa",
"choco",
"chocob",
"chocobo",
"chocol",
"chocolate",
"dup",
"dupl",
"duplicate",
"duplicate ",
"duplicate 1",
"duplicate 2",
];
for query in &aaa_only_matches {
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["aaa"])),
vec![Suggestion::Exposure {
suggestion_type: "aaa".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["aaa", "bbb"])),
vec![Suggestion::Exposure {
suggestion_type: "aaa".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["bbb", "aaa"])),
vec![Suggestion::Exposure {
suggestion_type: "aaa".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["aaa", "zzz"])),
vec![Suggestion::Exposure {
suggestion_type: "aaa".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["zzz", "aaa"])),
vec![Suggestion::Exposure {
suggestion_type: "aaa".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["bbb"])),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["zzz"])),
vec![],
);
}
let bbb_only_matches = vec![
"bbb keyword",
"common prefix b",
"common prefix bb",
"common prefix bbb",
];
for query in &bbb_only_matches {
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["bbb"])),
vec![Suggestion::Exposure {
suggestion_type: "bbb".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["bbb", "aaa"])),
vec![Suggestion::Exposure {
suggestion_type: "bbb".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["aaa", "bbb"])),
vec![Suggestion::Exposure {
suggestion_type: "bbb".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["bbb", "zzz"])),
vec![Suggestion::Exposure {
suggestion_type: "bbb".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["zzz", "bbb"])),
vec![Suggestion::Exposure {
suggestion_type: "bbb".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["aaa"])),
vec![],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["zzz"])),
vec![],
);
}
let both_matches = vec!["both keyword", "common prefix", "common prefix "];
for query in &both_matches {
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["aaa"])),
vec![Suggestion::Exposure {
suggestion_type: "aaa".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["bbb"])),
vec![Suggestion::Exposure {
suggestion_type: "bbb".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["aaa", "bbb"])),
vec![
Suggestion::Exposure {
suggestion_type: "aaa".into(),
score: 1.0,
},
Suggestion::Exposure {
suggestion_type: "bbb".into(),
score: 1.0,
},
],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["bbb", "aaa"])),
vec![
Suggestion::Exposure {
suggestion_type: "aaa".into(),
score: 1.0,
},
Suggestion::Exposure {
suggestion_type: "bbb".into(),
score: 1.0,
},
],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["aaa", "zzz"])),
vec![Suggestion::Exposure {
suggestion_type: "aaa".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["zzz", "aaa"])),
vec![Suggestion::Exposure {
suggestion_type: "aaa".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["bbb", "zzz"])),
vec![Suggestion::Exposure {
suggestion_type: "bbb".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["zzz", "bbb"])),
vec![Suggestion::Exposure {
suggestion_type: "bbb".into(),
score: 1.0,
}],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["aaa", "zzz", "bbb"])),
vec![
Suggestion::Exposure {
suggestion_type: "aaa".into(),
score: 1.0,
},
Suggestion::Exposure {
suggestion_type: "bbb".into(),
score: 1.0,
},
],
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["zzz"])),
vec![],
);
}
Ok(())
}
#[test]
fn exposure_spread_across_multiple_records() -> anyhow::Result<()> {
before_each();
let mut store = TestStore::new(
MockRemoteSettingsClient::default()
.with_full_record(
"exposure-suggestions",
"exposure-0",
Some(json!({
"suggestion_type": "aaa",
})),
Some(json!({
"keywords": [
"record 0 keyword",
["sug", ["gest"]],
],
})),
)
.with_full_record(
"exposure-suggestions",
"exposure-1",
Some(json!({
"suggestion_type": "aaa",
})),
Some(json!({
"keywords": [
"record 1 keyword",
["sug", ["arplum"]],
],
})),
),
);
store.ingest(SuggestIngestionConstraints {
providers: Some(vec![SuggestionProvider::Exposure]),
provider_constraints: Some(SuggestionProviderConstraints {
exposure_suggestion_types: Some(vec!["aaa".to_string()]),
}),
..SuggestIngestionConstraints::all_providers()
});
let matches = vec![
"record 0 keyword",
"sug",
"sugg",
"sugge",
"sugges",
"suggest",
"record 1 keyword",
"suga",
"sugar",
"sugarp",
"sugarpl",
"sugarplu",
"sugarplum",
];
for query in &matches {
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["aaa"])),
vec![Suggestion::Exposure {
suggestion_type: "aaa".into(),
score: 1.0,
}],
);
}
store
.client_mut()
.delete_record(Collection::Quicksuggest.name(), "exposure-0");
store.ingest(SuggestIngestionConstraints {
providers: Some(vec![SuggestionProvider::Exposure]),
provider_constraints: Some(SuggestionProviderConstraints {
exposure_suggestion_types: Some(vec!["aaa".to_string()]),
}),
..SuggestIngestionConstraints::all_providers()
});
let record_1_matches = vec![
"record 1 keyword",
"sug",
"suga",
"sugar",
"sugarp",
"sugarpl",
"sugarplu",
"sugarplum",
];
for query in &record_1_matches {
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["aaa"])),
vec![Suggestion::Exposure {
suggestion_type: "aaa".into(),
score: 1.0,
}],
);
}
let record_0_matches = vec!["record 0 keyword", "sugg", "sugge", "sugges", "suggest"];
for query in &record_0_matches {
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, &["exposure-test"])),
vec![]
);
}
Ok(())
}
#[test]
fn exposure_ingest() -> anyhow::Result<()> {
before_each();
let store = TestStore::new(
MockRemoteSettingsClient::default()
.with_full_record(
"exposure-suggestions",
"exposure-0",
Some(json!({
"suggestion_type": "aaa",
})),
Some(json!({
"keywords": ["aaa keyword", "both keyword"],
})),
)
.with_full_record(
"exposure-suggestions",
"exposure-1",
Some(json!({
"suggestion_type": "bbb",
})),
Some(json!({
"keywords": ["bbb keyword", "both keyword"],
})),
),
);
store.ingest(SuggestIngestionConstraints {
providers: Some(vec![SuggestionProvider::Exposure]),
provider_constraints: None,
..SuggestIngestionConstraints::all_providers()
});
let ingest_1_queries = [
("aaa keyword", vec!["aaa"]),
("aaa keyword", vec!["bbb"]),
("aaa keyword", vec!["aaa", "bbb"]),
("bbb keyword", vec!["aaa"]),
("bbb keyword", vec!["bbb"]),
("bbb keyword", vec!["aaa", "bbb"]),
("both keyword", vec!["aaa"]),
("both keyword", vec!["bbb"]),
("both keyword", vec!["aaa", "bbb"]),
];
for (query, types) in &ingest_1_queries {
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, types)),
vec![],
);
}
store.ingest(SuggestIngestionConstraints {
providers: Some(vec![SuggestionProvider::Exposure]),
provider_constraints: Some(SuggestionProviderConstraints {
exposure_suggestion_types: Some(vec!["bbb".to_string()]),
}),
..SuggestIngestionConstraints::all_providers()
});
let ingest_2_queries = [
("aaa keyword", vec!["aaa"], vec![]),
("aaa keyword", vec!["bbb"], vec![]),
("aaa keyword", vec!["aaa", "bbb"], vec![]),
("bbb keyword", vec!["aaa"], vec![]),
("bbb keyword", vec!["bbb"], vec!["bbb"]),
("bbb keyword", vec!["aaa", "bbb"], vec!["bbb"]),
("both keyword", vec!["aaa"], vec![]),
("both keyword", vec!["bbb"], vec!["bbb"]),
("both keyword", vec!["aaa", "bbb"], vec!["bbb"]),
];
for (query, types, expected_types) in &ingest_2_queries {
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, types)),
expected_types
.iter()
.map(|t| Suggestion::Exposure {
suggestion_type: t.to_string(),
score: 1.0,
})
.collect::<Vec<Suggestion>>(),
);
}
store.ingest(SuggestIngestionConstraints {
providers: Some(vec![SuggestionProvider::Exposure]),
provider_constraints: Some(SuggestionProviderConstraints {
exposure_suggestion_types: Some(vec!["aaa".to_string()]),
}),
..SuggestIngestionConstraints::all_providers()
});
let ingest_3_queries = [
("aaa keyword", vec!["aaa"], vec!["aaa"]),
("aaa keyword", vec!["bbb"], vec![]),
("aaa keyword", vec!["aaa", "bbb"], vec!["aaa"]),
("bbb keyword", vec!["aaa"], vec![]),
("bbb keyword", vec!["bbb"], vec!["bbb"]),
("bbb keyword", vec!["aaa", "bbb"], vec!["bbb"]),
("both keyword", vec!["aaa"], vec!["aaa"]),
("both keyword", vec!["bbb"], vec!["bbb"]),
("both keyword", vec!["aaa", "bbb"], vec!["aaa", "bbb"]),
];
for (query, types, expected_types) in &ingest_3_queries {
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure(query, types)),
expected_types
.iter()
.map(|t| Suggestion::Exposure {
suggestion_type: t.to_string(),
score: 1.0,
})
.collect::<Vec<Suggestion>>(),
);
}
Ok(())
}
#[test]
fn exposure_ingest_new_record() -> anyhow::Result<()> {
before_each();
let mut store = TestStore::new(MockRemoteSettingsClient::default().with_full_record(
"exposure-suggestions",
"exposure-0",
Some(json!({
"suggestion_type": "aaa",
})),
Some(json!({
"keywords": ["old keyword"],
})),
));
store.ingest(SuggestIngestionConstraints {
providers: Some(vec![SuggestionProvider::Exposure]),
provider_constraints: Some(SuggestionProviderConstraints {
exposure_suggestion_types: Some(vec!["aaa".to_string()]),
}),
..SuggestIngestionConstraints::all_providers()
});
store.client_mut().add_full_record(
"exposure-suggestions",
"exposure-1",
Some(json!({
"suggestion_type": "aaa",
})),
Some(json!({
"keywords": ["new keyword"],
})),
);
store.ingest(SuggestIngestionConstraints {
providers: Some(vec![SuggestionProvider::Exposure]),
provider_constraints: None,
..SuggestIngestionConstraints::all_providers()
});
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure("new keyword", &["aaa"])),
vec![],
);
store.ingest(SuggestIngestionConstraints {
providers: Some(vec![SuggestionProvider::Exposure]),
provider_constraints: Some(SuggestionProviderConstraints {
exposure_suggestion_types: Some(vec!["aaa".to_string()]),
}),
..SuggestIngestionConstraints::all_providers()
});
assert_eq!(
store.fetch_suggestions(SuggestionQuery::exposure("new keyword", &["aaa"])),
vec![Suggestion::Exposure {
suggestion_type: "aaa".to_string(),
score: 1.0,
}]
);
Ok(())
}
}