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