1use std::{collections::HashMap, sync::{Arc, Mutex, atomic::AtomicBool, mpsc}};
2use game::{ext::ExtensionInfo, search::{GameFilter, GameSearch, PageTuple, ParsedInput}, AdditionalApp, Game, GameRedirect, PartialGame};
3use game_data::{GameData, PartialGameData};
4use platform::PlatformAppPath;
5use r2d2::Pool;
6use r2d2_sqlite::SqliteConnectionManager;
7use rusqlite::Connection;
8use snafu::ResultExt;
9use tag::{PartialTag, Tag, TagSuggestion};
10use tag_category::{TagCategory, PartialTagCategory};
11use chrono::Utc;
12use lazy_static::lazy_static;
13use crate::logger::EventManager;
14
15mod error;
16use error::{Error, Result};
17use update::{RemoteCategory, RemoteDeletedGamesRes, RemoteGamesRes, RemotePlatform, RemoteTag};
18use util::ContentTreeNode;
19
20pub mod game;
21pub mod game_data;
22mod migration;
23pub mod platform;
24pub mod tag;
25pub mod tag_category;
26pub mod update;
27pub mod util;
28mod logger;
29
30#[cfg(feature = "napi")]
31#[macro_use]
32extern crate napi_derive;
33
34static DEBUG_ENABLED: AtomicBool = AtomicBool::new(false);
35
36pub const MAX_SEARCH: i64 = 99999999999;
37
38lazy_static! {
39 static ref LOGGER: Arc<EventManager> = EventManager::new();
40}
41
42pub struct FlashpointArchive {
43 pool: Option<Pool<SqliteConnectionManager>>,
44 extensions: game::ext::ExtensionRegistry,
45 write_mutex: Mutex<()>,
46}
47
48impl FlashpointArchive {
49 pub fn new() -> Self {
50 FlashpointArchive {
51 pool: None,
52 extensions: game::ext::ExtensionRegistry::new(),
53 write_mutex: Mutex::new(()),
54 }
55 }
56
57 pub fn load_database(&mut self, source: &str) -> Result<()> {
61 let conn_manager = if source == ":memory:" {
62 SqliteConnectionManager::memory()
63 } else {
64 SqliteConnectionManager::file(source)
65 };
66
67 let pool = r2d2::Pool::new(conn_manager).expect("Failed to open R2D2 conn pool");
68 let mut conn = pool.get().unwrap();
69
70 migration::up(&mut conn).context(error::DatabaseMigrationSnafu)?;
72 conn.execute("PRAGMA foreign_keys=off;", ()).context(error::SqliteSnafu)?;
73 tag_category::find_or_create(&conn, "default", None).context(error::SqliteSnafu)?;
75
76 self.pool = Some(pool);
77
78 Ok(())
79 }
80
81 pub fn parse_user_input(&self, input: &str) -> ParsedInput {
82 game::search::parse_user_input(input, Some(&self.extensions.searchables))
83 }
84
85 pub fn register_extension(&mut self, ext: ExtensionInfo) -> Result<()> {
86 with_serialized_transaction!(&self, |tx| {
87 self.extensions.create_ext_indices(tx, ext.clone())
88 })?;
89
90 self.extensions.register_ext(ext);
91
92 Ok(())
93 }
94
95 pub async fn search_games(&self, search: &GameSearch) -> Result<Vec<game::Game>> {
96 with_connection!(&self.pool, |conn| {
97 debug_println!("Getting search page");
98 game::search::search(conn, search).context(error::SqliteSnafu)
99 })
100 }
101
102 pub async fn search_games_index(&self, search: &mut GameSearch, limit: Option<i64>) -> Result<Vec<PageTuple>> {
103 with_connection!(&self.pool, |conn| {
104 debug_println!("Getting search index");
105 game::search::search_index(conn, search, limit).context(error::SqliteSnafu)
106 })
107 }
108
109 pub async fn search_games_total(&self, search: &GameSearch) -> Result<i64> {
110 with_connection!(&self.pool, |conn| {
111 debug_println!("Getting search total");
112 game::search::search_count(conn, search).context(error::SqliteSnafu)
113 })
114 }
115
116 pub async fn search_games_with_tag(&self, tag: &str) -> Result<Vec<Game>> {
117 with_connection!(&self.pool, |conn| {
118 game::find_with_tag(conn, tag).context(error::SqliteSnafu)
119 })
120 }
121
122 pub async fn search_games_random(&self, search: &GameSearch, count: i64) -> Result<Vec<Game>> {
123 with_connection!(&self.pool, |conn| {
124 game::search::search_random(conn, search.clone(), count).context(error::SqliteSnafu)
125 })
126 }
127
128 pub async fn search_tag_suggestions(&self, partial: &str, blacklist: Vec<String>) -> Result<Vec<TagSuggestion>> {
129 with_connection!(&self.pool, |conn| {
130 tag::search_tag_suggestions(conn, partial, blacklist).context(error::SqliteSnafu)
131 })
132 }
133
134 pub async fn search_platform_suggestions(&self, partial: &str) -> Result<Vec<TagSuggestion>> {
135 with_connection!(&self.pool, |conn| {
136 platform::search_platform_suggestions(conn, partial).context(error::SqliteSnafu)
137 })
138 }
139
140 pub async fn find_all_game_ids(&self) -> Result<Vec<String>> {
141 with_connection!(&self.pool, |conn| {
142 game::find_all_ids(conn).context(error::SqliteSnafu)
143 })
144 }
145
146 pub async fn find_game(&self, id: &str) -> Result<Option<Game>> {
147 with_connection!(&self.pool, |conn| {
148 game::find(conn, id).context(error::SqliteSnafu)
149 })
150 }
151
152 pub async fn create_game(&self, partial_game: &PartialGame) -> Result<game::Game> {
153 with_serialized_transaction!(&self, |tx| {
154 game::create(tx, partial_game).context(error::SqliteSnafu)
155 })
156 }
157
158 pub async fn save_game(&self, partial_game: &mut PartialGame) -> Result<Game> {
159 with_serialized_transaction!(&self, |tx| {
160 match partial_game.date_modified {
161 Some(_) => (),
162 None => partial_game.date_modified = Some(Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()),
163 }
164 game::save(tx, partial_game).context(error::SqliteSnafu)
165 })
166 }
167
168 pub async fn save_games(&self, partial_games: Vec<&mut PartialGame>) -> Result<()> {
169 with_serialized_transaction!(&self, |tx| {
170 for partial_game in partial_games {
171 match partial_game.date_modified {
172 Some(_) => (),
173 None => partial_game.date_modified = Some(Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()),
174 }
175 game::save(tx, partial_game).context(error::SqliteSnafu)?;
176 }
177 Ok(())
178 })
179 }
180
181 pub async fn delete_game(&self, id: &str) -> Result<()> {
182 with_serialized_transaction!(&self, |conn| {
183 game::delete(conn, id).context(error::SqliteSnafu)
184 })
185 }
186
187 pub async fn count_games(&self) -> Result<i64> {
188 with_connection!(&self.pool, |conn| {
189 game::count(conn).context(error::SqliteSnafu)
190 })
191 }
192
193 pub async fn find_add_app_by_id(&self, id: &str) -> Result<Option<AdditionalApp>> {
194 with_connection!(&self.pool, |conn| {
195 game::find_add_app_by_id(conn, id).context(error::SqliteSnafu)
196 })
197 }
198
199 pub async fn create_add_app(&self, add_app: &mut AdditionalApp) -> Result<()> {
200 with_serialized_transaction!(&self, |conn| {
201 game::create_add_app(conn, add_app).context(error::SqliteSnafu)
202 })
203 }
204
205 pub async fn find_game_data_by_id(&self, game_data_id: i64) -> Result<Option<GameData>> {
206 with_connection!(&self.pool, |conn| {
207 game::find_game_data_by_id(conn, game_data_id).context(error::SqliteSnafu)
208 })
209 }
210
211 pub async fn find_game_data(&self, game_id: &str) -> Result<Vec<GameData>> {
212 with_connection!(&self.pool, |conn| {
213 game::get_game_data(conn, game_id).context(error::SqliteSnafu)
214 })
215 }
216
217 pub async fn create_game_data(&self, game_data: &PartialGameData) -> Result<GameData> {
218 with_connection!(&self.pool, |conn| {
219 game::create_game_data(conn, game_data).context(error::SqliteSnafu)
220 })
221 }
222
223 pub async fn save_game_data(&self, game_data: &PartialGameData) -> Result<GameData> {
224 with_connection!(&self.pool, |conn| {
225 game::save_game_data(conn, game_data).context(error::SqliteSnafu)
226 })
227 }
228
229 pub async fn delete_game_data(&self, id: i64) -> Result<()> {
230 with_connection!(&self.pool, |conn| {
231 game_data::delete(conn, id).context(error::SqliteSnafu)
232 })
233 }
234
235 pub async fn find_all_tags(&self) -> Result<Vec<Tag>> {
236 with_connection!(&self.pool, |conn| {
237 tag::find(conn).context(error::SqliteSnafu)
238 })
239 }
240
241 pub async fn find_tag(&self, name: &str) -> Result<Option<Tag>> {
242 with_connection!(&self.pool, |conn| {
243 tag::find_by_name(conn, name).context(error::SqliteSnafu)
244 })
245 }
246
247 pub async fn find_tag_by_id(&self, id: i64) -> Result<Option<Tag>> {
248 with_connection!(&self.pool, |conn| {
249 tag::find_by_id(conn, id).context(error::SqliteSnafu)
250 })
251 }
252
253 pub async fn create_tag(&self, name: &str, category: Option<String>, id: Option<i64>) -> Result<Tag> {
254 with_serialized_transaction!(&self, |conn| {
255 tag::create(conn, name, category, id).context(error::SqliteSnafu)
256 })
257 }
258
259 pub async fn save_tag(&self, partial: &mut PartialTag) -> Result<Tag> {
260 with_serialized_transaction!(&self, |conn| {
261 match partial.date_modified {
262 Some(_) => (),
263 None => partial.date_modified = Some(Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()),
264 }
265 tag::save(conn, &partial).context(error::SqliteSnafu)
266 })
267 }
268
269 pub async fn delete_tag(&self, name: &str) -> Result<()> {
270 with_serialized_transaction!(&self, |conn| {
271 tag::delete(conn, name).context(error::SqliteSnafu)
272 })
273 }
274
275 pub async fn delete_tag_by_id(&self, id: i64) -> Result<()> {
276 with_serialized_transaction!(&self, |conn| {
277 tag::delete_by_id(conn, id).context(error::SqliteSnafu)
278 })
279 }
280
281 pub async fn count_tags(&self) -> Result<i64> {
282 with_connection!(&self.pool, |conn| {
283 tag::count(conn).context(error::SqliteSnafu)
284 })
285 }
286
287 pub async fn merge_tags(&self, name: &str, merged_into: &str) -> Result<Tag> {
288 with_serialized_transaction!(&self, |conn| {
289 tag::merge_tag(conn, name, merged_into).context(error::SqliteSnafu)
290 })
291 }
292
293 pub async fn find_all_platforms(&self) -> Result<Vec<Tag>> {
294 with_connection!(&self.pool, |conn| {
295 platform::find(conn).context(error::SqliteSnafu)
296 })
297 }
298
299 pub async fn find_platform(&self, name: &str) -> Result<Option<Tag>> {
300 with_connection!(&self.pool, |conn| {
301 platform::find_by_name(conn, name).context(error::SqliteSnafu)
302 })
303 }
304
305 pub async fn find_platform_by_id(&self, id: i64) -> Result<Option<Tag>> {
306 with_connection!(&self.pool, |conn| {
307 platform::find_by_id(conn, id).context(error::SqliteSnafu)
308 })
309 }
310
311 pub async fn create_platform(&self, name: &str, id: Option<i64>) -> Result<Tag> {
312 with_serialized_transaction!(&self, |conn| {
313 platform::create(conn, name, id).context(error::SqliteSnafu)
314 })
315 }
316
317 pub async fn save_platform(&self, partial: &mut PartialTag) -> Result<Tag> {
318 with_serialized_transaction!(&self, |conn| {
319 match partial.date_modified {
320 Some(_) => (),
321 None => partial.date_modified = Some(Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()),
322 }
323 platform::save(conn, &partial).context(error::SqliteSnafu)
324 })
325 }
326
327 pub async fn delete_platform(&self, name: &str) -> Result<()> {
328 with_serialized_transaction!(&self, |conn| {
329 platform::delete(conn, name).context(error::SqliteSnafu)
330 })
331 }
332
333 pub async fn count_platforms(&self) -> Result<i64> {
334 with_connection!(&self.pool, |conn| {
335 platform::count(conn).context(error::SqliteSnafu)
336 })
337 }
338
339 pub async fn find_all_tag_categories(&self) -> Result<Vec<TagCategory>> {
340 with_connection!(&self.pool, |conn| {
341 tag_category::find(conn).context(error::SqliteSnafu)
342 })
343 }
344
345 pub async fn find_tag_category(&self, name: &str) -> Result<Option<TagCategory>> {
346 with_connection!(&self.pool, |conn| {
347 tag_category::find_by_name(conn, name).context(error::SqliteSnafu)
348 })
349 }
350
351 pub async fn find_tag_category_by_id(&self, id: i64) -> Result<Option<TagCategory>> {
352 with_connection!(&self.pool, |conn| {
353 tag_category::find_by_id(conn, id).context(error::SqliteSnafu)
354 })
355 }
356
357 pub async fn create_tag_category(&self, partial: &PartialTagCategory) -> Result<TagCategory> {
358 with_connection!(&self.pool, |conn| {
359 tag_category::create(conn, partial).context(error::SqliteSnafu)
360 })
361 }
362
363 pub async fn save_tag_category(&self, partial: &PartialTagCategory) -> Result<TagCategory> {
364 with_connection!(&self.pool, |conn| {
365 tag_category::save(conn, partial).context(error::SqliteSnafu)
366 })
367 }
368
369 pub async fn new_tag_filter_index(&self, search: &mut GameSearch) -> Result<()> {
370 with_connection!(&self.pool, |conn| {
371 game::search::new_tag_filter_index(conn, search).context(error::SqliteSnafu)
372 })
373 }
374
375 pub async fn find_all_game_developers(&self, search: Option<GameSearch>) -> Result<Vec<String>> {
376 with_connection!(&self.pool, |conn| {
377 game::find_developers(conn, search).context(error::SqliteSnafu)
378 })
379 }
380
381 pub async fn find_all_game_publishers(&self, search: Option<GameSearch>) -> Result<Vec<String>> {
382 with_connection!(&self.pool, |conn| {
383 game::find_publishers(conn, search).context(error::SqliteSnafu)
384 })
385 }
386
387 pub async fn find_all_game_series(&self, search: Option<GameSearch>) -> Result<Vec<String>> {
388 with_connection!(&self.pool, |conn| {
389 game::find_series(conn, search).context(error::SqliteSnafu)
390 })
391 }
392
393 pub async fn find_all_game_libraries(&self) -> Result<Vec<String>> {
394 with_connection!(&self.pool, |conn| {
395 game::find_libraries(conn).context(error::SqliteSnafu)
396 })
397 }
398
399 pub async fn find_all_game_statuses(&self) -> Result<Vec<String>> {
400 with_connection!(&self.pool, |conn| {
401 game::find_statuses(conn).context(error::SqliteSnafu)
402 })
403 }
404
405 pub async fn find_all_game_play_modes(&self) -> Result<Vec<String>> {
406 with_connection!(&self.pool, |conn| {
407 game::find_play_modes(conn).context(error::SqliteSnafu)
408 })
409 }
410
411 pub async fn find_all_game_application_paths(&self) -> Result<Vec<String>> {
412 with_connection!(&self.pool, |conn| {
413 game::find_application_paths(conn).context(error::SqliteSnafu)
414 })
415 }
416
417 pub async fn find_platform_app_paths(&self) -> Result<HashMap<String, Vec<PlatformAppPath>>> {
418 with_connection!(&self.pool, |conn| {
419 game::find_platform_app_paths(conn).context(error::SqliteSnafu)
420 })
421 }
422
423 pub async fn add_game_playtime(&self, game_id: &str, seconds: i64) -> Result<()> {
424 with_serialized_transaction!(&self, |conn| {
425 game::add_playtime(conn, game_id, seconds).context(error::SqliteSnafu)
426 })
427 }
428
429 pub async fn clear_playtime_tracking_by_id(&self, game_id: &str) -> Result<()> {
430 with_connection!(&self.pool, |conn| {
431 game::clear_playtime_tracking_by_id(conn, game_id).context(error::SqliteSnafu)
432 })
433 }
434
435 pub async fn clear_playtime_tracking(&self) -> Result<()> {
436 with_connection!(&self.pool, |conn| {
437 game::clear_playtime_tracking(conn).context(error::SqliteSnafu)
438 })
439 }
440
441 pub async fn force_games_active_data_most_recent(&self) -> Result<()> {
442 with_connection!(&self.pool, |conn| {
443 game::force_active_data_most_recent(conn).context(error::SqliteSnafu)
444 })
445 }
446
447 pub async fn find_game_redirects(&self) -> Result<Vec<GameRedirect>> {
448 with_connection!(&self.pool, |conn| {
449 game::find_redirects(conn).context(error::SqliteSnafu)
450 })
451 }
452
453 pub async fn create_game_redirect(&self, src_id: &str, dest_id: &str) -> Result<()> {
454 with_serialized_transaction!(&self, |conn| {
455 game::create_redirect(conn, src_id, dest_id).context(error::SqliteSnafu)
456 })
457 }
458
459 pub async fn delete_game_redirect(&self, src_id: &str, dest_id: &str) -> Result<()> {
460 with_serialized_transaction!(&self, |conn| {
461 game::delete_redirect(conn, src_id, dest_id).context(error::SqliteSnafu)
462 })
463 }
464
465 pub async fn update_apply_categories(&self, cats: Vec<RemoteCategory>) -> Result<()> {
466 with_serialized_transaction!(&self, |conn| {
467 update::apply_categories(conn, cats)
468 })
469 }
470
471 pub async fn update_apply_platforms(&self, platforms: Vec<RemotePlatform>) -> Result<()> {
472 with_serialized_transaction!(&self, |conn| {
473 update::apply_platforms(conn, platforms)
474 })
475 }
476
477 pub async fn update_apply_tags(&self, tags: Vec<RemoteTag>) -> Result<()> {
478 with_serialized_transaction!(&self, |conn| {
479 update::apply_tags(conn, tags)
480 })
481 }
482
483 pub async fn update_apply_games(&self, games_res: &RemoteGamesRes, owner: &str) -> Result<()> {
484 with_serialized_transaction!(&self, |conn| {
485 update::apply_games(conn, games_res, owner)
486 })
487 }
488
489 pub async fn update_delete_games(&self, games_res: &RemoteDeletedGamesRes) -> Result<()> {
490 with_serialized_transaction!(&self, |conn| {
491 update::delete_games(conn, games_res)
492 })
493 }
494
495 pub async fn update_apply_redirects(&self, redirects_res: Vec<GameRedirect>) -> Result<()> {
496 with_serialized_transaction!(&self, |conn| {
497 update::apply_redirects(conn, redirects_res)
498 })
499 }
500
501 pub async fn optimize_database(&self) -> Result<()> {
502 with_connection!(&self.pool, |conn| {
503 optimize_database(conn).context(error::SqliteSnafu)
504 })
505 }
506
507 pub async fn new_custom_id_order(&self, custom_id_order: Vec<String>) -> Result<()> {
508 with_serialized_transaction!(&self, |conn| {
509 game::search::new_custom_id_order(conn, custom_id_order).context(error::SqliteSnafu)
510 })
511 }
512}
513
514pub fn logger_subscribe() -> (crate::logger::SubscriptionId, mpsc::Receiver<crate::logger::LogEvent>) {
515 LOGGER.subscribe()
516}
517
518pub fn logger_unsubscribe(id: crate::logger::SubscriptionId) {
519 LOGGER.unsubscribe(id)
520}
521
522fn optimize_database(conn: &Connection) -> rusqlite::Result<()> {
523 conn.execute("ANALYZE", ())?;
524 conn.execute("REINDEX", ())?;
525 conn.execute("VACUUM", ())?;
526 Ok(())
527}
528
529pub fn generate_content_tree(root: &str) -> Result<ContentTreeNode> {
530 util::gen_content_tree(root).map_err(|_| snafu::NoneError).context(error::ContentTreeSnafu)
531}
532
533pub fn copy_folder(src: &str, dest: &str) -> Result<u64> {
534 util::copy_folder(src, dest).map_err(|_| snafu::NoneError).context(error::CopyFolderSnafu)
535}
536
537pub fn merge_game_filters(a: &GameFilter, b: &GameFilter) -> GameFilter {
538 let mut new_filter = GameFilter::default();
539 new_filter.subfilters = vec![a.clone(), b.clone()];
540
541 if a.match_any && b.match_any {
542 new_filter.match_any = true;
543 }
544
545 return new_filter;
546}
547
548#[macro_export]
549macro_rules! with_connection {
550 ($pool:expr, $body:expr) => {
551 match $pool {
552 Some(conn) => {
553 let conn = &conn.get().unwrap();
554 conn.execute("PRAGMA foreign_keys=off;", ()).context(error::SqliteSnafu)?;
555 $body(conn)
556 },
557 None => return Err(Error::DatabaseNotInitialized)
558 }
559 };
560}
561
562
563#[macro_export]
564macro_rules! with_transaction {
565 ($pool:expr, $body:expr) => {
566 match $pool {
567 Some(conn) => {
568 let mut conn = conn.get().unwrap();
569 conn.execute("PRAGMA foreign_keys=off;", ()).context(error::SqliteSnafu)?;
570 let tx = conn.transaction().context(error::SqliteSnafu)?;
571 let res = $body(&tx);
572 if res.is_ok() {
573 tx.commit().context(error::SqliteSnafu)?;
574 debug_println!("Applied transaction");
575 }
576 res
577 },
578 None => return Err(Error::DatabaseNotInitialized)
579 }
580 };
581}
582
583#[macro_export]
584macro_rules! with_serialized_transaction {
585 ($archive:expr, $body:expr) => {
586 {
587 let _write_guard = $archive.write_mutex.lock().unwrap();
588 with_transaction!($archive.pool.as_ref(), $body)
589 }
590 };
591}
592
593pub fn enable_debug() {
594 DEBUG_ENABLED.store(true, std::sync::atomic::Ordering::SeqCst);
595}
596
597pub fn disable_debug() {
598 DEBUG_ENABLED.store(false, std::sync::atomic::Ordering::SeqCst);
599}
600
601pub fn debug_enabled() -> bool {
602 DEBUG_ENABLED.load(std::sync::atomic::Ordering::SeqCst)
603}
604
605#[macro_export]
606macro_rules! debug_println {
607 ($($arg:tt)*) => (if $crate::debug_enabled() {
608 ::std::println!($($arg)*);
609 let formatted_message = ::std::format!($($arg)*);
610 $crate::LOGGER.dispatch_event(formatted_message);
611 })
612}
613
614#[cfg(test)]
615mod tests {
616
617 use crate::game::{ext::ExtSearchable, search::{parse_user_input, FieldFilter, GameFilter, GameSearchOffset, GameSearchSortable}};
618
619 use super::*;
620
621 const TEST_DATABASE: &str = "benches/flashpoint.sqlite";
622
623 #[tokio::test]
624 async fn database_not_initialized() {
625 let flashpoint = FlashpointArchive::new();
626 let result = flashpoint.count_games().await;
627 assert!(result.is_err());
628
629 let e = result.unwrap_err();
630 assert!(matches!(e, Error::DatabaseNotInitialized {}));
631 }
632
633 #[tokio::test]
634 async fn migrations_valid() {
635 let migrations = migration::get();
636 assert!(migrations.validate().is_ok());
637 }
638
639 #[tokio::test]
640 async fn count_games() {
641 let mut flashpoint = FlashpointArchive::new();
642 let create = flashpoint.load_database(TEST_DATABASE);
643 assert!(create.is_ok());
644 let result = flashpoint.count_games().await;
645 assert!(result.is_ok());
646
647 let total = result.unwrap();
648 assert_eq!(total, 191150);
649 }
650
651 #[tokio::test]
652 async fn search_full_scan() {
653 let mut flashpoint = FlashpointArchive::new();
654 let create = flashpoint.load_database(TEST_DATABASE);
655 assert!(create.is_ok());
656 let mut search = game::search::GameSearch::default();
657 search.limit = MAX_SEARCH;
658 search.filter.exact_whitelist.library = Some(vec![String::from("arcade")]);
659 let result = flashpoint.search_games(&search).await;
660 assert!(result.is_ok());
661 let games = result.unwrap();
662 assert_eq!(games.len(), 162929);
663 }
664
665 #[tokio::test]
666 async fn search_tags_or() {
667 let mut flashpoint = FlashpointArchive::new();
668 let create = flashpoint.load_database(TEST_DATABASE);
669 assert!(create.is_ok());
670 let mut search = game::search::GameSearch::default();
671 search.limit = MAX_SEARCH;
672 search.filter.match_any = true;
673 search.filter.exact_whitelist.tags = Some(vec!["Action".to_owned(), "Adventure".to_owned()]);
674 let result = flashpoint.search_games(&search).await;
675 assert!(result.is_ok());
676 let games = result.unwrap();
677 assert_eq!(games.len(), 36724);
678 }
679
680 #[tokio::test]
681 async fn search_tags_and() {
682 let mut flashpoint = FlashpointArchive::new();
683 let create = flashpoint.load_database(TEST_DATABASE);
684 assert!(create.is_ok());
685 let mut search = game::search::GameSearch::default();
686 search.limit = MAX_SEARCH;
687 search.filter.match_any = false;
688 search.filter.exact_whitelist.tags = Some(vec!["Action".to_owned(), "Adventure".to_owned()]);
689 let result = flashpoint.search_games(&search).await;
690 assert!(result.is_ok());
691 let games = result.unwrap();
692 assert_eq!(games.len(), 397);
693 }
694
695 #[tokio::test]
696 async fn search_tags_and_or_combined() {
697 let mut flashpoint = FlashpointArchive::new();
699 let create = flashpoint.load_database(TEST_DATABASE);
700 assert!(create.is_ok());
701 let mut search = game::search::GameSearch::default();
702 let mut inner_filter = game::search::GameFilter::default();
703 search.limit = 30000;
705 inner_filter.exact_whitelist.tags = Some(vec!["Action".to_owned(), "Adventure".to_owned()]);
707 inner_filter.match_any = true; search.filter.subfilters = vec![inner_filter];
710 search.filter.exact_blacklist.tags = Some(vec!["Sonic The Hedgehog".to_owned()]);
711 search.filter.match_any = false; search.order.column = GameSearchSortable::TITLE;
713
714 enable_debug();
716 let total_result = flashpoint.search_games_total(&search).await;
717 assert!(total_result.is_ok());
718 let total = total_result.unwrap();
719 assert_eq!(total, 36541);
720
721 let result = flashpoint.search_games(&search).await;
723 assert!(result.is_ok());
724 let games = result.unwrap();
725 assert_eq!(games.len(), 30000);
726 let page_end_game = games.last().unwrap();
727
728 let index_result = flashpoint.search_games_index(&mut search, None).await;
730 assert!(index_result.is_ok());
731 let index = index_result.unwrap();
732 assert_eq!(index.len(), 1);
733 assert_eq!(index[0].id, page_end_game.id);
734
735 search.offset = Some(GameSearchOffset{
737 value: serde_json::Value::String(page_end_game.title.clone()),
738 game_id: page_end_game.id.clone(),
739 title: page_end_game.title.clone(),
740 });
741 let last_result = flashpoint.search_games(&search).await;
742 assert!(last_result.is_ok());
743 let last_page = last_result.unwrap();
744 assert_eq!(last_page.len(), 6541);
745 }
746
747 #[tokio::test]
748 async fn search_multiple_subfilters() {
749 let mut flashpoint = FlashpointArchive::new();
750 let create = flashpoint.load_database(TEST_DATABASE);
751 assert!(create.is_ok());
752 let mut search = GameSearch::default();
753 search.filter.subfilters.push(GameFilter {
754 exact_blacklist: FieldFilter {
755 tags: Some(vec!["Action".to_owned(), "Shooting".to_owned()]),
756 ..Default::default()
757 },
758 ..Default::default()
759 });
760 search.filter.subfilters.push(GameFilter {
761 exact_blacklist: FieldFilter {
762 tags: Some(vec!["Adventure".to_owned()]),
763 ..Default::default()
764 },
765 ..Default::default()
766 });
767 search.filter.exact_whitelist.library = Some(vec!["arcade".to_owned()]);
768 search.filter.match_any = false;
769 assert!(flashpoint.search_games_index(&mut search, None).await.is_ok());
770 }
771
772 #[tokio::test]
773 async fn parse_user_search_input_assorted() {
774 game::search::parse_user_input("test", None);
775 game::search::parse_user_input(r#"tag:"sonic""#, None);
776 game::search::parse_user_input(r#"o_%$ dev:"san" disk t:7 potato"#, None);
777
778 enable_debug();
779
780 let s = game::search::parse_user_input(r#"title:"" series:"sonic:hedgehog" -developer:"""#, None).search;
783 assert!(s.filter.exact_whitelist.title.is_some());
784 assert_eq!(s.filter.exact_whitelist.title.unwrap()[0], "");
785 assert!(s.filter.whitelist.series.is_some());
786 assert_eq!(s.filter.whitelist.series.unwrap()[0], "sonic:hedgehog");
787 assert!(s.filter.exact_blacklist.developer.is_some());
788 assert_eq!(s.filter.exact_blacklist.developer.unwrap()[0], "");
789
790 let s2 = game::search::parse_user_input(r#"playtime>1h30m tags:3 playcount<3"#, None).search;
792 assert!(s2.filter.higher_than.playtime.is_some());
793 assert_eq!(s2.filter.higher_than.playtime.unwrap(), 60 * 90);
794 assert!(s2.filter.equal_to.tags.is_some());
795 assert_eq!(s2.filter.equal_to.tags.unwrap(), 3);
796 assert!(s2.filter.lower_than.playcount.is_some());
797 assert_eq!(s2.filter.lower_than.playcount.unwrap(), 3);
798 }
799
800 #[tokio::test]
801 async fn parse_user_search_input_sizes() {
802 let search = game::search::parse_user_input("tags>5 addapps=3 gamedata<12 test>generic", None).search;
803 assert!(search.filter.higher_than.tags.is_some());
804 assert_eq!(search.filter.higher_than.tags.unwrap(), 5);
805 assert!(search.filter.equal_to.add_apps.is_some());
806 assert_eq!(search.filter.equal_to.add_apps.unwrap(), 3);
807 assert!(search.filter.lower_than.game_data.is_some());
808 assert_eq!(search.filter.lower_than.game_data.unwrap(), 12);
809 assert!(search.filter.whitelist.generic.is_some());
810 let generics = search.filter.whitelist.generic.unwrap();
811 assert_eq!(generics.len(), 1);
812 assert_eq!(generics[0], "test>generic");
813 }
814
815 #[tokio::test]
816 async fn find_game() {
817 let mut flashpoint = FlashpointArchive::new();
818 let create = flashpoint.load_database(TEST_DATABASE);
819 assert!(create.is_ok());
820 let result = flashpoint.find_game("00deff25-5cd2-40d1-a0e7-151d82ce16c5").await;
821 assert!(result.is_ok());
822 let game_opt = result.unwrap();
823 assert!(game_opt.is_some());
824 let game = game_opt.unwrap();
825 assert_eq!(game.title, "Crab Planet");
826 assert!(game.detailed_platforms.is_some());
827 let platforms = game.detailed_platforms.unwrap();
828 assert_eq!(platforms.len(), 1);
829 assert_eq!(platforms[0].name, "Flash");
830 }
831
832 #[tokio::test]
833 async fn game_redirects() {
834 let mut flashpoint = FlashpointArchive::new();
835 let create = flashpoint.load_database(":memory:");
836 assert!(create.is_ok());
837 let partial_game = game::PartialGame {
838 title: Some(String::from("Test Game")),
839 tags: Some(vec!["Action"].into()),
840 ..game::PartialGame::default()
841 };
842 let result = flashpoint.create_game(&partial_game).await;
843 assert!(result.is_ok());
844 let game = result.unwrap();
845
846 let create_redirect_res = flashpoint.create_game_redirect("test", &game.id).await;
847 assert!(create_redirect_res.is_ok());
848
849 let found_game_res = flashpoint.find_game("test").await;
851 assert!(found_game_res.is_ok());
852 assert!(found_game_res.unwrap().is_some());
853
854 let mut search = GameSearch::default();
856 search.filter.exact_whitelist.id = Some(vec!["test".to_owned()]);
857 let search_res = flashpoint.search_games(&search).await;
858 assert!(search_res.is_ok());
859 assert_eq!(search_res.unwrap().len(), 1);
860
861 let found_redirs = flashpoint.find_game_redirects().await;
863 assert!(found_redirs.is_ok());
864 assert_eq!(found_redirs.unwrap().len(), 1);
865
866 let remove_redirect_res = flashpoint.delete_game_redirect("test", &game.id).await;
867 assert!(remove_redirect_res.is_ok());
868
869 let found_redirs2 = flashpoint.find_game_redirects().await;
870 assert!(found_redirs2.is_ok());
871 assert_eq!(found_redirs2.unwrap().len(), 0);
872 }
873
874 #[tokio::test]
875 async fn tag_categories() {
876 let mut flashpoint = FlashpointArchive::new();
877 let create = flashpoint.load_database(":memory:");
878 assert!(create.is_ok());
879 let partial_tc = tag_category::PartialTagCategory {
880 id: -1,
881 name: "test".to_owned(),
882 color: "#FF00FF".to_owned(),
883 description: Some("test".to_owned()),
884 };
885 assert!(flashpoint.create_tag_category(&partial_tc).await.is_ok());
886 let saved_cat_result = flashpoint.find_tag_category("test").await;
887 assert!(saved_cat_result.is_ok());
888 let saved_cat_opt = saved_cat_result.unwrap();
889 assert!(saved_cat_opt.is_some());
890 let saved_cat = saved_cat_opt.unwrap();
891 assert_eq!(saved_cat.name, "test");
892 assert_eq!(saved_cat.color, "#FF00FF");
893 assert!(saved_cat.description.is_some());
894 assert_eq!(saved_cat.description.unwrap(), "test");
895
896 let all_cats_result = flashpoint.find_all_tag_categories().await;
897 assert!(all_cats_result.is_ok());
898 let all_cats = all_cats_result.unwrap();
899 assert_eq!(all_cats.len(), 2);
901 }
902
903 #[tokio::test]
904 async fn create_and_save_game() {
905 let mut flashpoint = FlashpointArchive::new();
906 let create = flashpoint.load_database(":memory:");
907 assert!(create.is_ok());
908 let partial_game = game::PartialGame {
909 title: Some(String::from("Test Game")),
910 tags: Some(vec!["Action"].into()),
911 ..game::PartialGame::default()
912 };
913 let result = flashpoint.create_game(&partial_game).await;
914 assert!(result.is_ok());
915 let mut game = result.unwrap();
916 let found_tag_res = flashpoint.find_tag("Action").await;
917 assert!(found_tag_res.is_ok());
918 let found_tag_opt = found_tag_res.unwrap();
919 assert!(found_tag_opt.is_some());
920 let found_game_res = flashpoint.find_game(&game.id).await;
921 assert!(found_game_res.is_ok());
922 let found_game_opt = found_game_res.unwrap();
923 assert!(found_game_opt.is_some());
924 let found_game = found_game_opt.unwrap();
925 assert!(found_game.detailed_tags.is_some());
926 let found_tags = found_game.detailed_tags.unwrap();
927 assert_eq!(found_tags.len(), 1);
928 assert_eq!(game.title, "Test Game");
929 game.developer = String::from("Newgrounds");
930 game.tags = vec!["Action", "Adventure"].into();
931 game.primary_platform = String::from("Flash");
932 let save_result = flashpoint.save_game(&mut game.into()).await;
933 assert!(save_result.is_ok());
934 let saved_game = save_result.unwrap();
935 assert_eq!(saved_game.developer, "Newgrounds");
936 assert_eq!(saved_game.tags.len(), 2);
937 assert_eq!(saved_game.platforms.len(), 1);
938 assert_eq!(saved_game.platforms[0], "Flash");
939 assert_eq!(saved_game.primary_platform, "Flash");
940 assert!(saved_game.detailed_platforms.is_some());
941 let detailed_platforms = saved_game.detailed_platforms.unwrap();
942 assert_eq!(detailed_platforms.len(), 1);
943 assert!(saved_game.detailed_tags.is_some());
944 let detailed_tags = saved_game.detailed_tags.unwrap();
945 assert_eq!(detailed_tags.len(), 2);
946 assert_eq!(detailed_tags[0].name, "Action");
947 }
948
949 #[tokio::test]
950 async fn create_and_save_game_with_empty_detailed_tags() {
951 let mut flashpoint = FlashpointArchive::new();
952 let create = flashpoint.load_database(":memory:");
953 assert!(create.is_ok());
954 let partial_game = game::PartialGame {
955 title: Some(String::from("Test Game")),
956 detailed_tags: Some(vec![]),
957 tags: Some(vec!["Action"].into()),
958 ..game::PartialGame::default()
959 };
960 let result = flashpoint.create_game(&partial_game).await;
961 assert!(result.is_ok());
962 let game = result.unwrap();
963 let found_tag_res = flashpoint.find_tag("Action").await;
964 assert!(found_tag_res.is_ok());
965 let found_tag_opt = found_tag_res.unwrap();
966 assert!(found_tag_opt.is_some());
967 let found_game_res = flashpoint.find_game(&game.id).await;
968 assert!(found_game_res.is_ok());
969 let found_game_opt = found_game_res.unwrap();
970 assert!(found_game_opt.is_some());
971 let found_game = found_game_opt.unwrap();
972 assert!(found_game.detailed_tags.is_some());
973 let found_tags = found_game.detailed_tags.unwrap();
974 assert_eq!(found_tags.len(), 1);
975 }
976
977 #[tokio::test]
978 async fn game_extension() {
979 let mut flashpoint = FlashpointArchive::new();
980 let create = flashpoint.load_database(":memory:");
981 assert!(create.is_ok());
982 let create_ext = flashpoint.register_extension(ExtensionInfo {
983 id: "user_score".to_owned(),
984 searchables: vec![ExtSearchable {
985 key: "score".to_owned(),
986 search_key: "score".to_owned(),
987 value_type: game::ext::ExtSearchableType::Number
988 }],
989 indexes: vec![]
990 });
991 assert!(create_ext.is_ok());
992
993 let partial_game = game::PartialGame {
995 title: Some(String::from("Test Game")),
996 tags: Some(vec!["Action"].into()),
997 ..game::PartialGame::default()
998 };
999 let game_create_res = flashpoint.create_game(&partial_game).await;
1000 assert!(game_create_res.is_ok());
1001 let mut game = game_create_res.unwrap();
1002 let mut ext_map = HashMap::new();
1003 let ext_data = serde_json::from_str(r#"{"score": 5}"#);
1004 assert!(ext_data.is_ok());
1005 ext_map.insert("user_score".to_owned(), ext_data.unwrap());
1006 game.ext_data = Some(ext_map);
1007 let save_res = flashpoint.save_game(&mut game.into()).await;
1008 assert!(save_res.is_ok());
1009
1010 let search = parse_user_input("score>3", Some(&flashpoint.extensions.searchables)).search;
1012 let search_res = flashpoint.search_games(&search).await;
1013 assert!(search_res.is_ok());
1014 let res = search_res.unwrap();
1015 assert_eq!(res.len(), 1);
1016
1017 let search = parse_user_input("score<3", Some(&flashpoint.extensions.searchables)).search;
1018 let search_res = flashpoint.search_games(&search).await;
1019 assert!(search_res.is_ok());
1020 let res = search_res.unwrap();
1021 assert_eq!(res.len(), 0);
1022 }
1023
1024 #[tokio::test]
1025 async fn game_extension_user_input() {
1026 let mut flashpoint = FlashpointArchive::new();
1027 let create = flashpoint.load_database(":memory:");
1028 assert!(create.is_ok());
1029 let create_ext = flashpoint.register_extension(ExtensionInfo {
1030 id: "user_score".to_owned(),
1031 searchables: vec![
1032 ExtSearchable {
1033 key: "renamed".to_owned(),
1034 search_key: "name".to_owned(),
1035 value_type: game::ext::ExtSearchableType::String,
1036 },
1037 ExtSearchable {
1038 key: "fav".to_owned(),
1039 search_key: "fav".to_owned(),
1040 value_type: game::ext::ExtSearchableType::Boolean,
1041 },
1042 ExtSearchable {
1043 key: "score".to_owned(),
1044 search_key: "score".to_owned(),
1045 value_type: game::ext::ExtSearchableType::Number
1046 }],
1047 indexes: vec![],
1048 });
1049 assert!(create_ext.is_ok());
1050 let search = parse_user_input("score>5 name:sonic fav=1", Some(&flashpoint.extensions.searchables)).search;
1051
1052 assert!(search.filter.higher_than.ext.is_some());
1054 let ext_search = search.filter.higher_than.ext.unwrap();
1055 assert!(ext_search.contains_key("user_score"));
1056 let ext_search_entry = ext_search.get("user_score").unwrap();
1057 assert!(ext_search_entry.contains_key("score"));
1058 let ext_search_entry_score = ext_search_entry.get("score").unwrap();
1059 assert_eq!(*ext_search_entry_score, 5);
1060
1061 assert!(search.filter.bool_comp.ext.is_some());
1063 let ext_search = search.filter.bool_comp.ext.unwrap();
1064 assert!(ext_search.contains_key("user_score"));
1065 let ext_search_entry = ext_search.get("user_score").unwrap();
1066 assert!(ext_search_entry.contains_key("fav"));
1067 let ext_search_entry_score = ext_search_entry.get("fav").unwrap();
1068 assert_eq!(*ext_search_entry_score, true);
1069
1070 assert!(search.filter.whitelist.ext.is_some());
1072 let ext_search = search.filter.whitelist.ext.unwrap();
1073 assert!(ext_search.contains_key("user_score"));
1074 let ext_search_entry = ext_search.get("user_score").unwrap();
1075 assert!(ext_search_entry.contains_key("renamed"));
1076 let ext_search_entry_score = ext_search_entry.get("renamed").unwrap();
1077 assert!(ext_search_entry_score.iter().find(|&s| *s == "sonic").is_some());
1078 }
1079
1080 #[tokio::test]
1081 async fn create_and_save_game_data() {
1082 let mut flashpoint = FlashpointArchive::new();
1083 let create = flashpoint.load_database(":memory:");
1084 assert!(create.is_ok());
1085 let partial_game = game::PartialGame {
1086 title: Some(String::from("Test Game")),
1087 tags: Some(vec!["Action"].into()),
1088 ..game::PartialGame::default()
1089 };
1090 let game_create_res = flashpoint.create_game(&partial_game).await;
1091 assert!(game_create_res.is_ok());
1092 let game = game_create_res.unwrap();
1093 let game_data = PartialGameData {
1094 id: None,
1095 game_id: game.id,
1096 title: Some("Test".to_owned()),
1097 date_added: Some("2023-01-01T01:01:01.000".to_owned()),
1098 sha256: Some("123".to_owned()),
1099 crc32: Some(0),
1100 present_on_disk: Some(false),
1101 path: None,
1102 size: Some(123),
1103 parameters: None,
1104 application_path: Some("Test".to_owned()),
1105 launch_command: Some("Test".to_owned())
1106 };
1107
1108 let game_data_res = flashpoint.create_game_data(&game_data).await;
1109 assert!(game_data_res.is_ok());
1110 let mut gd = game_data_res.unwrap();
1111 gd.path = Some("Test".to_owned());
1112 let save_res = flashpoint.save_game_data(&gd.into()).await;
1113 assert!(save_res.is_ok());
1114 let new_gd = save_res.unwrap();
1115 assert_eq!(new_gd.path.unwrap(), "Test");
1116 }
1117
1118 #[tokio::test]
1119 async fn parse_user_search_input() {
1120 let input = r#"sonic title:"dog cat" -title:"cat dog" tag:Action -mario installed:true"#;
1121 let search = game::search::parse_user_input(input, None).search;
1122 assert!(search.filter.whitelist.generic.is_some());
1123 assert_eq!(search.filter.whitelist.generic.unwrap()[0], "sonic");
1124 assert!(search.filter.whitelist.title.is_some());
1125 assert_eq!(search.filter.whitelist.title.unwrap()[0], "dog cat");
1126 assert!(search.filter.blacklist.title.is_some());
1127 assert_eq!(search.filter.blacklist.title.unwrap()[0], "cat dog");
1128 assert!(search.filter.whitelist.tags.is_some());
1129 assert_eq!(search.filter.whitelist.tags.unwrap()[0], "Action");
1130 assert!(search.filter.blacklist.generic.is_some());
1131 assert_eq!(search.filter.blacklist.generic.unwrap()[0], "mario");
1132 assert!(search.filter.bool_comp.installed.is_some());
1133 assert_eq!(search.filter.bool_comp.installed.unwrap(), true);
1134 }
1135
1136 #[tokio::test]
1137 async fn parse_user_search_input_whitespace() {
1138 let input = r#"series:"紅白Flash合戦 / Red & White Flash Battle 2013""#;
1139 let search = game::search::parse_user_input(input, None).search;
1140 assert!(search.filter.whitelist.series.is_some());
1141 assert_eq!(search.filter.whitelist.series.unwrap()[0], "紅白Flash合戦 / Red & White Flash Battle 2013");
1142 }
1143
1144 #[tokio::test]
1145 async fn parse_user_quick_search_input() {
1146 let input = r#"#Action -!Flash @"armor games" !"#;
1147 let search = game::search::parse_user_input(input, None).search;
1148 assert!(search.filter.whitelist.tags.is_some());
1149 assert_eq!(search.filter.whitelist.tags.unwrap()[0], "Action");
1150 assert!(search.filter.blacklist.platforms.is_some());
1151 assert_eq!(search.filter.blacklist.platforms.unwrap()[0], "Flash");
1152 assert!(search.filter.whitelist.developer.is_some());
1153 assert_eq!(search.filter.whitelist.developer.unwrap()[0], "armor games");
1154 assert!(search.filter.whitelist.generic.is_some());
1155 assert_eq!(search.filter.whitelist.generic.unwrap()[0], "!");
1156 }
1157
1158 #[tokio::test]
1159 async fn parse_user_exact_search_input() {
1160 let input = r#"!Flash -publisher=Newgrounds =sonic"#;
1161 let search = game::search::parse_user_input(input, None).search;
1162 assert!(search.filter.whitelist.platforms.is_some());
1163 assert_eq!(search.filter.whitelist.platforms.unwrap()[0], "Flash");
1164 assert!(search.filter.exact_blacklist.publisher.is_some());
1165 assert_eq!(search.filter.exact_blacklist.publisher.unwrap()[0], "Newgrounds");
1166 assert!(search.filter.whitelist.generic.is_some());
1167 assert!(search.filter.exact_whitelist.generic.is_none());
1168 assert_eq!(search.filter.whitelist.generic.unwrap()[0], "=sonic");
1169 }
1170
1171 #[tokio::test]
1172 async fn find_all_game_libraries() {
1173 let mut flashpoint = FlashpointArchive::new();
1174 let create = flashpoint.load_database(TEST_DATABASE);
1175 assert!(create.is_ok());
1176 let libraries_res = flashpoint.find_all_game_libraries().await;
1177 assert!(libraries_res.is_ok());
1178 let libraries = libraries_res.unwrap();
1179 assert_eq!(libraries.len(), 2);
1180 }
1181
1182 #[tokio::test]
1183 async fn create_tag() {
1184 let mut flashpoint = FlashpointArchive::new();
1185 assert!(flashpoint.load_database(":memory:").is_ok());
1186 let new_tag_res = flashpoint.create_tag("test", None, None).await;
1187 assert!(new_tag_res.is_ok());
1188 let new_tag = new_tag_res.unwrap();
1189 assert!(new_tag.category.is_some());
1190 assert_eq!(new_tag.category.unwrap(), "default");
1191 assert_eq!(new_tag.name, "test");
1192 assert_eq!(new_tag.aliases.len(), 1);
1193 assert_eq!(new_tag.aliases[0], "test");
1194 }
1195
1196 #[tokio::test]
1197 async fn delete_tag() {
1198 let mut flashpoint = FlashpointArchive::new();
1199 assert!(flashpoint.load_database(":memory:").is_ok());
1200 let partial = PartialGame {
1201 title: Some("test".to_owned()),
1202 tags: Some(vec!["Action"].into()),
1203 ..Default::default()
1204 };
1205 let new_game_res = flashpoint.create_game(&partial).await;
1206 assert!(new_game_res.is_ok());
1207 let saved_game = new_game_res.unwrap();
1208 assert_eq!(saved_game.tags.len(), 1);
1209 let delete_res = flashpoint.delete_tag("Action").await;
1210 assert!(delete_res.is_ok());
1211 let modded_game_res = flashpoint.find_game(&saved_game.id).await;
1212 assert!(modded_game_res.is_ok());
1213 let modded_game_opt = modded_game_res.unwrap();
1214 assert!(modded_game_opt.is_some());
1215 let modded_game = modded_game_opt.unwrap();
1216 assert_eq!(modded_game.tags.len(), 0);
1217 }
1218
1219 #[tokio::test]
1220 async fn merge_tags() {
1221 let mut flashpoint = FlashpointArchive::new();
1222 assert!(flashpoint.load_database(":memory:").is_ok());
1223 let partial = PartialGame {
1224 title: Some("test".to_owned()),
1225 tags: Some(vec!["Action"].into()),
1226 ..Default::default()
1227 };
1228 let new_game_res = flashpoint.create_game(&partial).await;
1229 assert!(new_game_res.is_ok());
1230 assert!(flashpoint.create_tag("Adventure", None, None).await.is_ok());
1231 let saved_game = new_game_res.unwrap();
1232 let merged_tag_res = flashpoint.merge_tags("Action", "Adventure").await;
1233 assert!(merged_tag_res.is_ok());
1234 let merged_tag = merged_tag_res.unwrap();
1235 assert_eq!(merged_tag.aliases.len(), 2);
1236 let modded_game_res = flashpoint.find_game(&saved_game.id).await;
1237 assert!(modded_game_res.is_ok());
1238 let modded_game_opt = modded_game_res.unwrap();
1239 assert!(modded_game_opt.is_some());
1240 let modded_game = modded_game_opt.unwrap();
1241 assert_eq!(modded_game.tags.len(), 1);
1242 assert_eq!(modded_game.tags[0], "Adventure");
1243 }
1244
1245 #[tokio::test]
1246 async fn find_tag() {
1247 let mut flashpoint = FlashpointArchive::new();
1248 assert!(flashpoint.load_database(":memory:").is_ok());
1249 let partial = PartialGame {
1250 title: Some("test".to_owned()),
1251 tags: Some(vec!["Action"].into()),
1252 ..Default::default()
1253 };
1254 let new_game_res = flashpoint.create_game(&partial).await;
1255 assert!(new_game_res.is_ok());
1256 let tag_res = flashpoint.find_tag("Action").await;
1257 assert!(tag_res.is_ok());
1258 let tag_opt = tag_res.unwrap();
1259 assert!(tag_opt.is_some());
1260 let tag_id_res = flashpoint.find_tag_by_id(tag_opt.unwrap().id).await;
1261 assert!(tag_id_res.is_ok());
1262 assert!(tag_id_res.unwrap().is_some());
1263 }
1264
1265 #[tokio::test]
1266 async fn delete_platform() {
1267 let mut flashpoint = FlashpointArchive::new();
1268 assert!(flashpoint.load_database(":memory:").is_ok());
1269 let partial = PartialGame {
1270 title: Some("test".to_owned()),
1271 platforms: Some(vec!["Flash"].into()),
1272 ..Default::default()
1273 };
1274 let new_game_res = flashpoint.create_game(&partial).await;
1275 assert!(new_game_res.is_ok());
1276 let saved_game = new_game_res.unwrap();
1277 assert_eq!(saved_game.platforms.len(), 1);
1278 let delete_res = flashpoint.delete_platform("Flash").await;
1279 assert!(delete_res.is_ok());
1280 let modded_game_res = flashpoint.find_game(&saved_game.id).await;
1281 assert!(modded_game_res.is_ok());
1282 let modded_game_opt = modded_game_res.unwrap();
1283 assert!(modded_game_opt.is_some());
1284 let modded_game = modded_game_opt.unwrap();
1285 assert_eq!(modded_game.platforms.len(), 0);
1286 }
1287
1288 #[tokio::test]
1289 async fn create_platform() {
1290 let mut flashpoint = FlashpointArchive::new();
1291 assert!(flashpoint.load_database(":memory:").is_ok());
1292 let new_tag_res = flashpoint.create_platform("test", None).await;
1293 assert!(new_tag_res.is_ok());
1294 let new_tag = new_tag_res.unwrap();
1295 assert!(new_tag.category.is_none());
1296 assert_eq!(new_tag.name, "test");
1297 assert_eq!(new_tag.aliases.len(), 1);
1298 assert_eq!(new_tag.aliases[0], "test");
1299 }
1300
1301 #[tokio::test]
1302 async fn search_tag_suggestions() {
1303 let mut flashpoint = FlashpointArchive::new();
1304 assert!(flashpoint.load_database(":memory:").is_ok());
1305 let new_tag_res = flashpoint.create_tag("Action", None, None).await;
1306 assert!(new_tag_res.is_ok());
1307 let suggs_res = flashpoint.search_tag_suggestions("Act", vec![]).await;
1308 assert!(suggs_res.is_ok());
1309 assert_eq!(suggs_res.unwrap().len(), 1);
1310 let suggs_bad_res = flashpoint.search_tag_suggestions("Adventure", vec![]).await;
1311 assert!(suggs_bad_res.is_ok());
1312 assert_eq!(suggs_bad_res.unwrap().len(), 0);
1313 }
1314
1315 #[tokio::test]
1316 async fn update_game_when_platform_changed() {
1317 let mut flashpoint = FlashpointArchive::new();
1318 assert!(flashpoint.load_database(":memory:").is_ok());
1319 let partial_game = game::PartialGame {
1320 title: Some(String::from("Test Game")),
1321 tags: Some(vec!["Action"].into()),
1322 platforms: Some(vec!["Flash", "HTML5"].into()),
1323 primary_platform: Some("HTML5".into()),
1324 ..game::PartialGame::default()
1325 };
1326 let result = flashpoint.create_game(&partial_game).await;
1327 assert!(result.is_ok());
1328 let old_game = result.unwrap();
1329 let mut platform = flashpoint.find_platform("HTML5").await.unwrap().unwrap();
1330 platform.name = String::from("Wiggle");
1331 let mut partial = PartialTag::from(platform);
1332 let save_res = flashpoint.save_platform(&mut partial).await;
1333 assert!(save_res.is_ok());
1334 assert_eq!(save_res.unwrap().name, "Wiggle");
1335 let new_game = flashpoint.find_game(&old_game.id).await.unwrap().unwrap();
1336 assert_eq!(new_game.primary_platform, "Wiggle");
1337 assert!(new_game.platforms.contains(&"Wiggle".to_string()));
1338 }
1339
1340 #[tokio::test]
1341 async fn search_games_random() {
1342 let mut flashpoint = FlashpointArchive::new();
1343 let create = flashpoint.load_database(TEST_DATABASE);
1344 assert!(create.is_ok());
1345
1346 let mut search = crate::game::search::parse_user_input("", None).search;
1347 let mut new_filter = GameFilter::default();
1348 new_filter.exact_blacklist.tags = Some(vec!["Action".to_owned()]);
1349 search.filter.subfilters.push(new_filter);
1350
1351 let random_res = flashpoint.search_games_random(&search, 5).await;
1352 assert!(random_res.is_ok());
1353 assert_eq!(random_res.unwrap().len(), 5);
1354 }
1355
1356 #[tokio::test]
1357 async fn search_games_installed() {
1358 let mut flashpoint = FlashpointArchive::new();
1359 let create = flashpoint.load_database(TEST_DATABASE);
1360 assert!(create.is_ok());
1361
1362 let mut search = crate::game::search::parse_user_input("installed:true", None).search;
1363 if let Some(installed) = search.filter.bool_comp.installed.as_ref() {
1364 assert_eq!(installed, &true);
1365 } else {
1366 panic!("Expected 'installed' to be Some(true), but it was None.");
1367 }
1368
1369 search.limit = 200;
1370 let games_res = flashpoint.search_games(&search).await;
1371 assert!(games_res.is_ok());
1372 assert_eq!(games_res.unwrap().len(), 20);
1373 }
1374
1375 #[tokio::test]
1376 async fn search_games_index_limited() {
1377 let mut flashpoint = FlashpointArchive::new();
1378 let create = flashpoint.load_database(TEST_DATABASE);
1379 assert!(create.is_ok());
1380
1381 let search = &mut GameSearch::default();
1382 search.filter.whitelist.title = Some(vec!["Super".into()]);
1383 search.limit = 200;
1385 let index_res = flashpoint.search_games_index(&mut search.clone(), Some(1000)).await;
1386 assert!(index_res.is_ok());
1387 let index = index_res.unwrap();
1388 assert_eq!(index.len(), 5);
1389 }
1390
1391
1392 #[tokio::test]
1393 async fn search_bracketting() {
1394 let mut flashpoint = FlashpointArchive::new();
1395 let create = flashpoint.load_database(TEST_DATABASE);
1396 assert!(create.is_ok());
1397
1398 let search = &mut GameSearch::default();
1399
1400 let mut tag_filter = GameFilter::default();
1401 tag_filter.whitelist.tags = Some(vec!["Alien Hominid".into()]);
1402
1403 let mut dev_filter = GameFilter::default();
1404 dev_filter.whitelist.developer = Some(vec!["jmtb".into(), "Tom Fulp".into()]);
1405 dev_filter.match_any = true;
1406
1407 search.filter.match_any = false;
1408 search.filter.subfilters.push(dev_filter);
1409 search.filter.subfilters.push(tag_filter);
1410
1411 let games_res = flashpoint.search_games(&search).await;
1412 assert!(games_res.is_ok());
1413 assert_eq!(games_res.unwrap().len(), 1);
1414 }
1415
1416 #[tokio::test]
1417 async fn get_tag() {
1418 let mut flashpoint = FlashpointArchive::new();
1419 let create = flashpoint.load_database(TEST_DATABASE);
1420 assert!(create.is_ok());
1421
1422 let tag_res = flashpoint.find_tag("Mario Bros.").await;
1423 assert!(tag_res.is_ok());
1424 let tag = tag_res.unwrap();
1425 assert!(tag.is_some());
1426 assert_eq!(tag.unwrap().name, "Super Mario");
1427 }
1428
1429 #[tokio::test]
1430 async fn get_platform() {
1431 let mut flashpoint = FlashpointArchive::new();
1432 let create = flashpoint.load_database(TEST_DATABASE);
1433 assert!(create.is_ok());
1434
1435 let tag_res = flashpoint.find_platform("Jutvision").await;
1436 assert!(tag_res.is_ok());
1437 let tag = tag_res.unwrap();
1438 assert!(tag.is_some());
1439 assert_eq!(tag.unwrap().name, "asdadawdaw");
1440 }
1441
1442 #[tokio::test]
1443 async fn add_playtime() {
1444 let mut flashpoint = FlashpointArchive::new();
1445 let create = flashpoint.load_database(":memory:");
1446 assert!(create.is_ok());
1447 let partial_game = game::PartialGame {
1448 title: Some(String::from("Test Game")),
1449 tags: Some(vec!["Action"].into()),
1450 ..game::PartialGame::default()
1451 };
1452 let result = flashpoint.create_game(&partial_game).await;
1453 assert!(result.is_ok());
1454 let game_id = result.unwrap().id;
1455 let playtime_res = flashpoint.add_game_playtime(&game_id, 30).await;
1456 assert!(playtime_res.is_ok());
1457 let saved_game_res = flashpoint.find_game(&game_id).await;
1458 assert!(saved_game_res.is_ok());
1459 let saved_game_opt = saved_game_res.unwrap();
1460 assert!(saved_game_opt.is_some());
1461 let saved_game = saved_game_opt.unwrap();
1462 assert_eq!(saved_game.playtime, 30);
1463 assert_eq!(saved_game.play_counter, 1);
1464 }
1465
1466 #[tokio::test]
1467 async fn update_tags_clear_existing() {
1468 let mut flashpoint = FlashpointArchive::new();
1469 let create = flashpoint.load_database(":memory:");
1470 assert!(create.is_ok());
1471 let new_tag_res = flashpoint.create_tag("test", None, Some(10)).await;
1472 assert!(new_tag_res.is_ok());
1473 let tag_update = RemoteTag {
1474 id: 10,
1475 name: "hello".to_owned(),
1476 description: String::new(),
1477 category: "default".to_owned(),
1478 date_modified: "2024-01-01 12:00:00".to_owned(),
1479 aliases: vec!["hello".to_owned()],
1480 deleted: false,
1481 };
1482 let update_res = flashpoint.update_apply_tags(vec![tag_update]).await;
1483 assert!(update_res.is_ok());
1484 let saved_tag_res = flashpoint.find_tag_by_id(10).await;
1485 assert!(saved_tag_res.is_ok());
1486 let saved_tag_opt = saved_tag_res.unwrap();
1487 assert!(saved_tag_opt.is_some());
1488 let saved_tag = saved_tag_opt.unwrap();
1489 assert_eq!(saved_tag.aliases.len(), 1);
1490 assert_eq!(saved_tag.aliases[0].as_str(), "hello");
1491 assert_eq!(saved_tag.name.as_str(), "hello");
1492 }
1493}