From 20a75dbb25dfd97abd04a84d04a3a29e5af2d0f8 Mon Sep 17 00:00:00 2001 From: scroll Date: Sun, 23 Aug 2026 07:51:04 +0300 Subject: [PATCH] nuke the tests and the dead code data/ is runtime state, keep it out of git. --- .gitignore | 1 + Cargo.lock | 2 - data/accounts.json | 15 - data/players.json | 182 ----------- logic/logic/tests/data_tables.rs | 194 ------------ logic/logic/tests/own_home_data.rs | 163 ---------- services/game-service/Cargo.toml | 2 - services/game-service/src/home_mode.rs | 2 +- services/game-service/src/service.rs | 2 +- services/game-service/tests/real_catalog.rs | 99 ------ services/gateway/Cargo.toml | 5 - services/gateway/tests/lobby.rs | 331 -------------------- services/service-rpc/Cargo.toml | 4 - services/service-rpc/tests/transport.rs | 61 ---- titan/titan/Cargo.toml | 2 - titan/titan/tests/csv.rs | 90 ------ titan/titan/tests/wire.rs | 168 ---------- 17 files changed, 3 insertions(+), 1320 deletions(-) delete mode 100644 data/accounts.json delete mode 100644 data/players.json delete mode 100644 logic/logic/tests/data_tables.rs delete mode 100644 logic/logic/tests/own_home_data.rs delete mode 100644 services/game-service/tests/real_catalog.rs delete mode 100644 services/gateway/tests/lobby.rs delete mode 100644 services/service-rpc/tests/transport.rs delete mode 100644 titan/titan/tests/csv.rs delete mode 100644 titan/titan/tests/wire.rs diff --git a/.gitignore b/.gitignore index bc79a7d..2aa83a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target +/data *.json.tmp docs/protocol.md diff --git a/Cargo.lock b/Cargo.lock index d880d1a..563beac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,8 +81,6 @@ name = "gateway" version = "0.1.0" dependencies = [ "async-trait", - "auth-service", - "game-service", "logic", "serde", "serde_json", diff --git a/data/accounts.json b/data/accounts.json deleted file mode 100644 index 54d16b9..0000000 --- a/data/accounts.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "next_low": 2, - "accounts": [ - { - "high": 0, - "low": 1, - "pass_token": "KIzXwApNr3fe6Yn0SOtv1gm9kOFIZnl3CAFT92EQ", - "created_at": 1787460399, - "last_seen_at": 1787460399, - "session_count": 1, - "play_time_seconds": 0, - "banned": false - } - ] -} \ No newline at end of file diff --git a/data/players.json b/data/players.json deleted file mode 100644 index 2175b3c..0000000 --- a/data/players.json +++ /dev/null @@ -1,182 +0,0 @@ -{ - "profiles": [ - { - "account": { - "high": 0, - "low": 1 - }, - "name": "Scroll1", - "name_set_by_user": false, - "exp_level": 1, - "exp_points": 0, - "gold": 5026, - "diamonds": 1000, - "free_diamonds": 0, - "trophies": 0, - "arena": { - "table": 54, - "name": "Arena1" - }, - "chest_slot_count": 4, - "deck": [ - { - "card": { - "table": 26, - "name": "Knight" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "Archer" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "Goblins" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "Giant" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "Pekka" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "Minions" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "Balloon" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "Witch" - }, - "level_index": 0, - "count": 10 - } - ], - "collection": [ - { - "card": { - "table": 26, - "name": "Barbarians" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "Golem" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "Skeletons" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "Valkyrie" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "SkeletonArmy" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "Bomber" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "Musketeer" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "BabyDragon" - }, - "level_index": 0, - "count": 10 - }, - { - "card": { - "table": 26, - "name": "Prince" - }, - "level_index": 0, - "count": 3 - }, - { - "card": { - "table": 26, - "name": "SpearGoblins" - }, - "level_index": 1, - "count": 1 - } - ], - "battle_count": 0, - "win_count": 0, - "lose_count": 0, - "npc_win_count": 1, - "npc_lose_count": 0, - "three_crown_wins": 0, - "tutorials_finished": true, - "gold_resource": { - "table": 5, - "name": "Gold" - } - } - ] -} \ No newline at end of file diff --git a/logic/logic/tests/data_tables.rs b/logic/logic/tests/data_tables.rs deleted file mode 100644 index 30cd634..0000000 --- a/logic/logic/tests/data_tables.rs +++ /dev/null @@ -1,194 +0,0 @@ -use std::path::{Path, PathBuf}; -use logic::{table, LogicDataTables}; -use titan::GlobalId; -struct TempTree { - root: PathBuf, -} -impl TempTree { - fn new(label: &str) -> Self { - let root = std::env::temp_dir().join(format!( - "scroll-data-{label}-{}-{:?}", - std::process::id(), - std::thread::current().id() - )); - let _ = std::fs::remove_dir_all(&root); - std::fs::create_dir_all(root.join("csv_logic")).expect("create tree"); - Self { root } - } - fn write(&self, relative: &str, contents: &str) { - let path = self.root.join(relative); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).expect("create parent"); - } - std::fs::write(path, contents).expect("write fixture"); - } - fn path(&self) -> &Path { - &self.root - } -} -impl Drop for TempTree { - fn drop(&mut self) { - let _ = std::fs::remove_dir_all(&self.root); - } -} -fn fixture(label: &str) -> TempTree { - let tree = TempTree::new(label); - tree.write( - "csv_logic/spells_characters.csv", - concat!( - "Name,Rarity,ManaCost\n", - "string,string,int\n", - "Knight,Common,3\n", - "Archers,Common,3\n", - "Giant,Rare,5\n" - ), - ); - tree.write( - "csv_logic/spells_buildings.csv", - concat!( - "Name,Rarity,ManaCost\n", - "string,string,int\n", - "Cannon,Common,3\n", - "Tesla,Common,4\n" - ), - ); - tree.write( - "csv_logic/arenas.csv", - concat!( - "Name,Arena,TrophyLimit\n", - "string,int,int\n", - "TrainingCamp,0,0\n", - "Arena1,1,400\n" - ), - ); - tree.write( - "csv_logic/resources.csv", - concat!( - "Name,Cap\n", - "string,int\n", - "Gold,1000000\n", - "Diamonds,0\n", - "FreeGold,0\n" - ), - ); - tree -} -#[test] -fn tables_are_indexed_by_the_client_table_ids() { - let tree = fixture("ids"); - let tables = LogicDataTables::load_from_dir(tree.path()).expect("load"); - assert!(tables.is_loaded()); - assert_eq!(tables.loaded_files().len(), 4); - assert!(!tables.skipped_files().is_empty()); - assert_eq!( - tables.table(table::SPELLS_CHARACTERS).map(|t| t.count()), - Some(3) - ); - assert_eq!(tables.table(table::ARENAS).map(|t| t.count()), Some(2)); - assert!(tables.table(table::ACHIEVEMENTS).is_none()); -} -#[test] -fn names_resolve_to_the_global_id_that_travels_on_the_wire() { - let tree = fixture("names"); - let tables = LogicDataTables::load_from_dir(tree.path()).expect("load"); - assert_eq!( - tables.global_id_by_name(table::SPELLS, "Knight"), - Some(GlobalId::new(table::SPELLS_CHARACTERS, 0)) - ); - assert_eq!( - tables.global_id_by_name(table::SPELLS, "Giant"), - Some(GlobalId::new(table::SPELLS_CHARACTERS, 2)) - ); - assert_eq!( - tables.global_id_by_name(table::SPELLS, "Tesla"), - Some(GlobalId::new(table::SPELLS_BUILDINGS, 1)) - ); - assert_eq!(tables.global_id_by_name(table::SPELLS, "Golem"), None); - assert_eq!( - tables.global_id_by_name(table::ARENAS, "Arena1"), - Some(GlobalId::new(table::ARENAS, 1)) - ); - assert_eq!( - tables.global_id_by_name(table::RESOURCES, "Gold"), - Some(GlobalId::new(table::RESOURCES, 0)) - ); -} -#[test] -fn a_global_id_resolves_back_to_its_row() { - let tree = fixture("roundtrip"); - let tables = LogicDataTables::load_from_dir(tree.path()).expect("load"); - let knight = tables - .data_by_id(GlobalId::new(table::SPELLS_CHARACTERS, 0)) - .expect("knight"); - assert_eq!(knight.name(), "Knight"); - assert_eq!(knight.int("ManaCost"), 3); - assert_eq!(knight.string("Rarity"), "Common"); - assert_eq!(knight.global_id(), GlobalId::new(table::SPELLS_CHARACTERS, 0)); - assert!(tables - .data_by_id(GlobalId::new(table::SPELLS_CHARACTERS, 99)) - .is_none()); -} -#[test] -fn an_empty_directory_is_reported_rather_than_silently_accepted() { - let tree = TempTree::new("empty"); - let error = LogicDataTables::load_from_dir(tree.path()).expect_err("must fail"); - assert!(error.to_string().contains("no data table")); - assert!(!LogicDataTables::empty().is_loaded()); -} -fn shipped_assets() -> Option { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../assets") - .canonicalize() - .ok()?; - root.join("csv_logic/spells_characters.csv").is_file().then_some(root) -} -#[test] -fn data_references_round_trip_whether_or_not_the_tables_are_installed() { - use titan::Payload; - let raw = logic::LogicDataRef::of(table::SPELLS_CHARACTERS, 3); - let bytes = raw.to_bytes().expect("encode"); - let decoded = logic::LogicDataRef::from_bytes(&bytes).expect("decode"); - assert_eq!(decoded, raw); - assert_eq!(decoded.global_id(), Some(GlobalId::new(table::SPELLS_CHARACTERS, 3))); - let empty = logic::LogicDataRef::None; - let bytes = empty.to_bytes().expect("encode"); - assert_eq!(bytes, vec![0]); - assert!(logic::LogicDataRef::from_bytes(&bytes).expect("decode").is_none()); -} -#[test] -fn installed_tables_turn_references_into_typed_data_classes() { - let Some(root) = shipped_assets() else { - eprintln!("assets/ is absent, skipping the typed data class check"); - return; - }; - let tables = LogicDataTables::load_from_dir(&root).expect("load"); - logic::LogicDataTables::install(std::sync::Arc::new(tables)); - let knight = logic::LogicDataRef::spell("Knight"); - assert!(knight.is_resolved()); - assert_eq!(knight.name(), "Knight"); - assert_eq!(knight.to_string(), "Knight"); - assert_eq!( - knight.global_id(), - Some(GlobalId::new(table::SPELLS_CHARACTERS, 0)) - ); - let spell = knight.as_spell().expect("spell data"); - assert_eq!(spell.rarity(), "Common"); - assert_eq!(spell.mana_cost(), 3); - assert!(knight.as_arena().is_none()); - let arena = logic::LogicDataRef::arena("TrainingCamp"); - let arena_data = arena.as_arena().expect("arena data"); - assert!(arena_data.is_training_camp()); - assert_eq!(arena_data.trophy_limit(), 0); - let card = logic::model::LogicSpell::card(knight.clone(), 0, 10); - assert_eq!(card.mana_cost(), 3); - assert_eq!(card.level(), 1); - let rarity = card.rarity().expect("rarity data"); - assert_eq!(rarity.name(), "Common"); - assert_eq!(rarity.level_count(), 12); - assert_eq!(rarity.upgrade_cost(0), 5); - let chest = logic::LogicDataRef::treasure_chest("Silver") - .as_treasure_chest() - .expect("chest data"); - assert!(chest.total_time_taken_seconds() > 0); - assert!(chest.max_gold() >= chest.min_gold()); -} diff --git a/logic/logic/tests/own_home_data.rs b/logic/logic/tests/own_home_data.rs deleted file mode 100644 index 22c2412..0000000 --- a/logic/logic/tests/own_home_data.rs +++ /dev/null @@ -1,163 +0,0 @@ -use logic::model::{ - ChestSource, LogicAllianceInfo, LogicChest, LogicClientAvatar, LogicClientHome, LogicDataSlot, - LogicSpell, LogicSpellCollection, LogicSpellDeck, LogicTimer, -}; -use logic::{DataId, LogicScrollMessageFactory, OwnHomeDataMessage}; -use titan::{ByteStreamReader, ByteStreamWriter, LogicLong, MessageFactory, MessageMeta, Payload}; -fn sample_message() -> OwnHomeDataMessage { - let deck = LogicSpellDeck::from_cards((0..8).map(|index| { - LogicSpell::card(DataId::character_card(index), index % 3, 10 + index) - })); - let collection = LogicSpellCollection::from_spells( - (0..12) - .map(|index| LogicSpell::card(DataId::character_card(index), 1, index)) - .collect(), - ); - let mut home = LogicClientHome { - home_id: LogicLong::new(0, 42), - chest_id_counter: 7, - free_chest_collect_count: 3, - donation_capacity_limit_timer: LogicTimer::started(86_400, 1_000), - donated_capacity: 12, - decks: vec![deck, LogicSpellDeck::default()], - spell_collection: collection, - selected_deck_index: 0, - chest_slots: vec![ - Some(LogicChest::locked( - DataId::treasure_chest(0), - 1, - 0, - ChestSource::Battle, - )), - None, - Some(LogicChest { - unlock_timer: Some(LogicTimer::started(3_600, 500)), - ..LogicChest::locked(DataId::treasure_chest(1), 2, 2, ChestSource::Crown) - }), - ], - free_chest_timer: LogicTimer::started(14_400, 100), - donation_cooldown_timer: LogicTimer::idle(), - free_chest: Some(LogicChest::locked( - DataId::treasure_chest(2), - 3, - -1, - ChestSource::Free, - )), - purchased_chest: None, - crowns_towards_crown_chest: 4, - star_chest_timer_pending: true, - star_chest_timer: LogicTimer::idle(), - crown_chest: None, - request_spells_cooldown_timer: LogicTimer::idle(), - finished_tutorials: [0; 8], - opened_pages: 0b1011, - last_shown_level_up: 2, - arena: DataId::arena(1), - shop_seed: -1, - shop_weekday_index: 3, - shop_weekday_index_seen: 3, - shop_timer: LogicTimer::started(600, 900), - shop_spells: vec![ - LogicDataSlot::new(DataId::character_card(4), 1), - LogicDataSlot::new(DataId::other_card(2), 0), - ], - share_replay_cooldown_timer: LogicTimer::idle(), - send_team_mail_cooldown_timer: LogicTimer::idle(), - free_chest_diamond_index: 1, - crown_chest_diamond_index: 2, - card_usage_stats_battle_count: 25, - royal_tv_replay_version_major: 2, - royal_tv_replay_version_build: 1306, - royal_tv_replay_version_content: 8, - seen_replay_ids: vec![5, -9, 1_000_000], - card_request_message: Some("need arrows".to_owned()), - requested_cards: vec![DataId::other_card(1), DataId::character_card(3)], - }; - home.set_tutorial_finished(0); - home.set_tutorial_finished(37); - let mut avatar = LogicClientAvatar { - avatar_id: LogicLong::new(0, 42), - account_id: LogicLong::new(0, 42), - home_id: LogicLong::new(0, 42), - name: "BladeWise".to_owned(), - name_set_by_user: true, - name_change_state: -1, - arena: DataId::arena(1), - score: 1_337, - legacy_flag: false, - commodities: Default::default(), - diamonds: 500, - free_diamonds: 25, - exp_points: 90, - exp_level: 6, - cumulative_purchased_diamonds: 0, - alliance: Some(LogicAllianceInfo { - alliance_id: LogicLong::new(0, 9), - name: "Scroll Devs".to_owned(), - badge: DataId::alliance_badge(16_000_000), - role: 2, - }), - battle_count: 40, - win_count: 22, - lose_count: 18, - win_loose_streak: 3, - npc_win_count: 4, - npc_lose_count: 0, - arena_before_last_npc_match: true, - }; - avatar - .commodities - .set(0, vec![LogicDataSlot::new(DataId::resource(0), 12_500)]); - avatar.commodities.set( - 1, - vec![LogicDataSlot::new(DataId::character_card(0), 3)], - ); - OwnHomeDataMessage::new(home, avatar, 0x5EED) -} -#[test] -fn own_home_data_roundtrips_byte_for_byte() { - let message = sample_message(); - let bytes = message.to_bytes().unwrap(); - let decoded = OwnHomeDataMessage::from_bytes(&bytes).unwrap(); - assert_eq!(decoded, message); - assert_eq!(decoded.to_bytes().unwrap(), bytes); - let mut reader = ByteStreamReader::new(&bytes); - OwnHomeDataMessage::decode(&mut reader).unwrap(); - assert_eq!(reader.remaining(), 0); -} -#[test] -fn own_home_data_starts_with_the_raw_big_endian_home_id() { - let message = sample_message(); - let bytes = message.to_bytes().unwrap(); - assert_eq!(&bytes[0..8], &[0, 0, 0, 0, 0, 0, 0, 42]); - assert_eq!(bytes[8], 7); -} -#[test] -fn message_metadata_matches_the_client_definition() { - assert_eq!(OwnHomeDataMessage::MESSAGE_TYPE, 24101); - assert_eq!(OwnHomeDataMessage::MESSAGE_VERSION, 0); - assert_eq!(OwnHomeDataMessage::NAME, "OwnHomeDataMessage"); -} -#[test] -fn the_factory_decodes_registered_messages() { - let registry = LogicScrollMessageFactory::registry(); - let message = sample_message(); - let bytes = message.to_bytes().unwrap(); - let decoded = registry.create(24101, 0, &bytes).unwrap(); - assert_eq!(decoded.message_type(), 24101); - assert_eq!(decoded.name(), "OwnHomeDataMessage"); - let concrete = decoded - .as_any() - .downcast_ref::() - .expect("downcast"); - assert_eq!(concrete, &message); -} -#[test] -fn empty_optionals_only_cost_a_single_packed_bit() { - let mut writer = ByteStreamWriter::new(); - let home = LogicClientHome::default(); - home.encode(&mut writer).unwrap(); - let bytes = writer.into_inner(); - let decoded = LogicClientHome::from_bytes(&bytes).unwrap(); - assert_eq!(decoded, home); -} diff --git a/services/game-service/Cargo.toml b/services/game-service/Cargo.toml index 855e021..8c8d5c6 100644 --- a/services/game-service/Cargo.toml +++ b/services/game-service/Cargo.toml @@ -21,5 +21,3 @@ async-trait = { workspace = true } [[bin]] name = "game-service" path = "src/main.rs" - -[dev-dependencies] diff --git a/services/game-service/src/home_mode.rs b/services/game-service/src/home_mode.rs index 31c9c65..2a903ec 100644 --- a/services/game-service/src/home_mode.rs +++ b/services/game-service/src/home_mode.rs @@ -159,7 +159,7 @@ impl HomeModeRegistry { pub async fn close(&self, account: AccountRef) -> Option>> { self.sessions.write().await.remove(&account) } - pub async fn len(&self) -> usize { + pub async fn session_count(&self) -> usize { self.sessions.read().await.len() } } diff --git a/services/game-service/src/service.rs b/services/game-service/src/service.rs index 5de63f8..87204ea 100644 --- a/services/game-service/src/service.rs +++ b/services/game-service/src/service.rs @@ -181,7 +181,7 @@ impl GameApi for GameService { } async fn disconnect(&self, account: AccountRef) -> RpcResult<()> { self.persist(account).await?; - let sessions = self.sessions.len().await; + let sessions = self.sessions.session_count().await; tracing::debug!(%account, sessions, "session closed"); Ok(()) } diff --git a/services/game-service/tests/real_catalog.rs b/services/game-service/tests/real_catalog.rs deleted file mode 100644 index b255cc9..0000000 --- a/services/game-service/tests/real_catalog.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::path::{Path, PathBuf}; -use game_service::{CardRef, Catalog, DataSelector}; -use logic::table; -fn assets_root() -> Option { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../assets") - .canonicalize() - .ok()?; - root.join("csv_logic/spells_characters.csv").is_file().then_some(root) -} -#[test] -fn the_shipped_client_tables_load_and_resolve_by_name() { - let Some(root) = assets_root() else { - eprintln!("assets/ is absent, skipping the shipped catalog check"); - return; - }; - let catalog = Catalog::load(Some(&root)); - assert!(catalog.is_loaded(), "catalog failed to load from {root:?}"); - let tables = catalog.tables(); - assert_eq!(tables.loaded_files().len(), 44); - assert!(tables.skipped_files().is_empty(), "{:?}", tables.skipped_files()); - assert_eq!( - tables.global_id_by_name(table::ARENAS, "TrainingCamp"), - Some(titan::GlobalId::new(table::ARENAS, 0)) - ); - assert_eq!( - tables.global_id_by_name(table::RESOURCES, "Gold"), - Some(titan::GlobalId::new(table::RESOURCES, 1)) - ); - assert_eq!( - tables.global_id_by_name(table::RESOURCES, "Diamonds"), - Some(titan::GlobalId::new(table::RESOURCES, 0)) - ); - let knight = tables.spell_by_name("Knight").expect("Knight"); - assert_eq!(knight.global_id().class_id, table::SPELLS_CHARACTERS); - assert_eq!(knight.global_id().instance_id, 0); - let fireball = tables.spell_by_name("Fireball").expect("Fireball"); - assert_eq!(fireball.global_id().class_id, table::SPELLS_OTHER); - let cannon = tables.spell_by_name("Cannon").expect("Cannon"); - assert_eq!(cannon.global_id().class_id, table::SPELLS_BUILDINGS); -} -#[test] -fn continuation_rows_build_the_per_level_arrays() { - let Some(root) = assets_root() else { - return; - }; - let catalog = Catalog::load(Some(&root)); - let tables = catalog.tables(); - let common = tables - .data_by_name(table::RARITIES, "Common") - .expect("Common rarity"); - assert!( - common.array_size() > 1, - "rarities.csv uses continuation rows for the upgrade curve" - ); - assert_eq!(common.int_at("UpgradeCost", 0), 5); - assert_eq!(common.int_at("UpgradeCost", 1), 20); - assert_eq!(common.int("LevelCount"), 12); - assert_eq!( - common.int_at("UpgradeCost", 999), - common.int_at("UpgradeCost", common.array_size() - 1), - "reading past the end clamps to the last level" - ); - let newbie = tables - .treasure_chest_by_name("Newbie1") - .expect("Newbie1 chest"); - assert_eq!(newbie.string("Arena"), "TrainingCamp"); - let row = newbie.row().expect("row"); - let guaranteed = newbie - .csv_table() - .column_index("GuaranteedSpells") - .expect("column"); - assert_eq!(row.value_count(guaranteed), 2); - assert_eq!(row.string_at(guaranteed, 0), "Knight"); - assert_eq!(row.string_at(guaranteed, 1), "Arrows"); -} -#[test] -fn the_default_starter_profile_resolves_against_the_real_tables() { - let Some(root) = assets_root() else { - return; - }; - let catalog = Catalog::load(Some(&root)); - let starter = game_service::StarterProfile::default(); - for card in starter.deck.iter().chain(starter.extra_collection.iter()) { - assert!( - catalog.resolve_card(card).is_some(), - "starter card {card:?} does not resolve" - ); - } - let gold = catalog.resolve_resource(&DataSelector::named("Gold")); - assert_eq!(gold.name(), "Gold"); - assert_eq!( - gold.global_id(), - Some(titan::GlobalId::new(table::RESOURCES, 1)) - ); - let arena = catalog.resolve_arena(&starter.arena); - assert_eq!(arena.table_index(), Some(table::ARENAS)); - assert!(catalog.resolve_card(&CardRef::named("NotACard")).is_none()); -} diff --git a/services/gateway/Cargo.toml b/services/gateway/Cargo.toml index a04fda7..2762f22 100644 --- a/services/gateway/Cargo.toml +++ b/services/gateway/Cargo.toml @@ -20,8 +20,3 @@ async-trait = { workspace = true } [[bin]] name = "gateway" path = "src/main.rs" - -[dev-dependencies] -auth-service = { workspace = true } -game-service = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net", "io-util", "sync", "time", "fs"] } diff --git a/services/gateway/tests/lobby.rs b/services/gateway/tests/lobby.rs deleted file mode 100644 index 0b99839..0000000 --- a/services/gateway/tests/lobby.rs +++ /dev/null @@ -1,331 +0,0 @@ -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::time::Duration; -use auth_service::{AuthConfig, AuthService}; -use game_service::{GameConfig, GameService}; -use gateway::{Backends, GatewayConfig}; -use game_service::{CardRef, DataSelector, StarterProfile}; -use logic::{message_type, table, KeepAliveMessage, LoginMessage, LoginOkMessage, OwnHomeDataMessage}; -use titan::GlobalId; -use titan::crypto::SessionCipher; -use titan::{FrameCodec, FrameHeader, MessageFrame, Payload, HEADER_LEN}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpStream; -struct FakeClient { - stream: TcpStream, - cipher: SessionCipher, -} -impl FakeClient { - async fn connect(port: u16) -> Self { - let stream = TcpStream::connect(("127.0.0.1", port)).await.expect("connect"); - stream.set_nodelay(true).expect("nodelay"); - Self { - stream, - cipher: SessionCipher::scroll_rc4(), - } - } - async fn send(&mut self, message: &M) { - let payload = message.to_bytes().expect("encode"); - let cipher_text = self.cipher.encrypt_outbound(&payload).expect("encrypt"); - let frame = MessageFrame::new(M::MESSAGE_TYPE, M::MESSAGE_VERSION, cipher_text); - let bytes = FrameCodec::encode(&frame).expect("frame"); - self.stream.write_all(&bytes).await.expect("write"); - self.stream.flush().await.expect("flush"); - } - async fn close(self) { - drop(self.stream); - tokio::time::sleep(Duration::from_millis(50)).await; - } - async fn receive(&mut self) -> (u16, Vec) { - let mut header_bytes = [0u8; HEADER_LEN]; - tokio::time::timeout( - Duration::from_secs(5), - self.stream.read_exact(&mut header_bytes), - ) - .await - .expect("header timeout") - .expect("header read"); - let header = FrameHeader::parse(&header_bytes); - let mut payload = vec![0u8; header.payload_len]; - tokio::time::timeout(Duration::from_secs(5), self.stream.read_exact(&mut payload)) - .await - .expect("payload timeout") - .expect("payload read"); - let plain = self.cipher.decrypt_inbound(&payload).expect("decrypt"); - (header.message_type, plain) - } -} -struct TestServer { - port: u16, - directory: PathBuf, -} -impl Drop for TestServer { - fn drop(&mut self) { - for attempt in 0..5 { - if std::fs::remove_dir_all(&self.directory).is_ok() && !self.directory.exists() { - return; - } - if attempt < 4 { - std::thread::sleep(Duration::from_millis(20)); - } - } - } -} -async fn start_server(label: &str) -> TestServer { - start_server_with(label, |config| config).await -} -async fn start_server_with( - label: &str, - customise: impl FnOnce(GameConfig) -> GameConfig, -) -> TestServer { - let directory = std::env::temp_dir().join(format!("scroll-test-{label}-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&directory); - std::fs::create_dir_all(&directory).expect("temp dir"); - let auth = AuthService::bootstrap(AuthConfig { - store_path: directory.join("accounts.json"), - ..AuthConfig::default() - }) - .await - .expect("auth"); - let game = GameService::bootstrap(customise(GameConfig { - store_path: directory.join("players.json"), - ..GameConfig::default() - })) - .await - .expect("game"); - let config = GatewayConfig { - listen: "127.0.0.1:0".to_owned(), - ..GatewayConfig::default() - }; - let listener = gateway::bind(&config).await.expect("bind"); - let port = listener.local_addr().expect("addr").port(); - let backends = Backends::new(auth, game); - tokio::spawn(async move { - let _ = gateway::run_on(listener, config, Arc::clone(&backends)).await; - }); - TestServer { port, directory } -} -fn write_card_catalog(root: &Path) { - std::fs::create_dir_all(root.join("csv_logic")).expect("csv dir"); - std::fs::write( - root.join("csv_logic/spells_characters.csv"), - concat!( - "Name,Rarity,ManaCost\n", - "string,string,int\n", - "Knight,Common,3\n", - "Archers,Common,3\n", - "Giant,Rare,5\n" - ), - ) - .expect("write characters"); - std::fs::write( - root.join("csv_logic/spells_other.csv"), - concat!( - "Name,Rarity,ManaCost\n", - "string,string,int\n", - "Fireball,Rare,4\n", - "Arrows,Common,3\n" - ), - ) - .expect("write other"); - std::fs::write( - root.join("csv_logic/arenas.csv"), - concat!( - "Name,Arena,TrophyLimit\n", - "string,int,int\n", - "TrainingCamp,0,0\n", - "Arena1,1,400\n" - ), - ) - .expect("write arenas"); - std::fs::write( - root.join("csv_logic/resources.csv"), - concat!("Name,Cap\n", "string,int\n", "Gold,1000000\n", "Diamonds,0\n"), - ) - .expect("write resources"); -} -fn fresh_login() -> LoginMessage { - LoginMessage { - client_major_version: 2, - client_minor_version: 0, - client_build: 1306, - resource_sha: Some("0".repeat(40)), - udid: Some("integration-test".to_owned()), - device: Some("iPhone8,1".to_owned()), - os_version: Some("12.5.7".to_owned()), - preferred_device_language: "EN".to_owned(), - ..LoginMessage::default() - } -} -#[tokio::test] -async fn a_fresh_client_reaches_the_lobby() { - let server = start_server("lobby").await; - let mut client = FakeClient::connect(server.port).await; - client.send(&fresh_login()).await; - let (message_type, payload) = client.receive().await; - assert_eq!(message_type, message_type::LOGIN_OK); - let login_ok = LoginOkMessage::from_bytes(&payload).expect("decode login ok"); - assert_eq!(login_ok.server_major_version, 2); - assert_eq!(login_ok.server_build, 1306); - assert!(!login_ok.account_id.is_zero()); - assert_eq!(login_ok.pass_token.as_deref().map(str::len), Some(40)); - assert_eq!(login_ok.account_id, login_ok.home_id); - let (message_type, payload) = client.receive().await; - assert_eq!(message_type, message_type::OWN_HOME_DATA); - let home_data = OwnHomeDataMessage::from_bytes(&payload).expect("decode own home data"); - assert_eq!(home_data.home.home_id, login_ok.home_id); - assert_eq!(home_data.avatar.account_id, login_ok.account_id); - assert_eq!(home_data.home.decks.len(), 1); - assert_eq!(home_data.home.decks[0].filled_slot_count(), 8); - assert_eq!(home_data.home.spell_collection.spells.len(), 8); - assert_eq!(home_data.home.chest_slots.len(), 4); - assert_eq!(home_data.avatar.diamonds, 1_000); - assert_eq!(home_data.avatar.exp_level, 1); - assert_eq!(home_data.avatar.name, format!("Scroll{}", login_ok.account_id.low)); - client.send(&KeepAliveMessage::default()).await; - let (message_type, payload) = client.receive().await; - assert_eq!(message_type, message_type::KEEP_ALIVE_SERVER); - assert!(payload.is_empty()); - client.close().await; -} -#[tokio::test] -async fn a_fresh_avatar_does_not_trigger_the_first_tutorial_battle() { - let server = start_server("tutorial").await; - let mut client = FakeClient::connect(server.port).await; - client.send(&fresh_login()).await; - let _ = client.receive().await; - let (message_type, payload) = client.receive().await; - assert_eq!(message_type, message_type::OWN_HOME_DATA); - let home_data = OwnHomeDataMessage::from_bytes(&payload).expect("decode"); - let avatar = &home_data.avatar; - assert!( - avatar.npc_lose_count != 0 || avatar.npc_win_count != 0, - "LogicClientAvatar::shouldGoToFirstTutorialBattle returns true when both npc counters \ - are zero, which sends InitState into GameMode(1) instead of the home screen" - ); - client.close().await; -} -#[tokio::test] -async fn reconnecting_with_the_issued_pass_token_keeps_the_account() { - let server = start_server("reconnect").await; - let (account_id, pass_token) = { - let mut client = FakeClient::connect(server.port).await; - client.send(&fresh_login()).await; - let (_, payload) = client.receive().await; - let login_ok = LoginOkMessage::from_bytes(&payload).expect("decode"); - let _ = client.receive().await; - let credentials = (login_ok.account_id, login_ok.pass_token.expect("token")); - client.close().await; - credentials - }; - let mut client = FakeClient::connect(server.port).await; - client - .send(&LoginMessage { - account_id, - pass_token: Some(pass_token.clone()), - ..fresh_login() - }) - .await; - let (message_type, payload) = client.receive().await; - assert_eq!(message_type, message_type::LOGIN_OK); - let login_ok = LoginOkMessage::from_bytes(&payload).expect("decode"); - assert_eq!(login_ok.account_id, account_id); - assert_eq!(login_ok.pass_token.as_deref(), Some(pass_token.as_str())); - assert!(login_ok.session_count >= 2); - let (message_type, _) = client.receive().await; - assert_eq!(message_type, message_type::OWN_HOME_DATA); - client.close().await; -} -#[tokio::test] -async fn a_wrong_pass_token_is_rejected_with_login_failed() { - let server = start_server("rejected").await; - let account_id = { - let mut client = FakeClient::connect(server.port).await; - client.send(&fresh_login()).await; - let (_, payload) = client.receive().await; - let login_ok = LoginOkMessage::from_bytes(&payload).expect("decode"); - let _ = client.receive().await; - let account = login_ok.account_id; - client.close().await; - account - }; - let mut client = FakeClient::connect(server.port).await; - client - .send(&LoginMessage { - account_id, - pass_token: Some("not-the-issued-token".to_owned()), - ..fresh_login() - }) - .await; - let (message_type, payload) = client.receive().await; - assert_eq!(message_type, message_type::LOGIN_FAILED); - let failure = logic::LoginFailedMessage::from_bytes(&payload).expect("decode"); - assert_eq!(failure.error_code, 1); - client.close().await; -} -#[tokio::test] -async fn go_home_reissues_the_lobby_snapshot() { - let server = start_server("gohome").await; - let mut client = FakeClient::connect(server.port).await; - client.send(&fresh_login()).await; - let _ = client.receive().await; - let (_, first) = client.receive().await; - client - .send(&logic::GoHomeMessage { - from_offer_wall: false, - }) - .await; - let (message_type, second) = client.receive().await; - assert_eq!(message_type, message_type::OWN_HOME_DATA); - assert_eq!(first, second); - client.close().await; -} -#[tokio::test] -async fn a_starter_profile_can_name_its_cards_when_the_csv_catalog_is_present() { - let catalog_root = - std::env::temp_dir().join(format!("scroll-test-cards-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&catalog_root); - write_card_catalog(&catalog_root); - let root_for_config = catalog_root.clone(); - let server = start_server_with("catalog", move |config| GameConfig { - csv_root: Some(root_for_config), - starter: StarterProfile { - arena: DataSelector::named("Arena1"), - gold_resource: DataSelector::named("Gold"), - deck: vec![ - CardRef::named("Knight"), - CardRef::named("Archers"), - CardRef::named("Fireball"), - CardRef::named("Arrows"), - ], - extra_collection: vec![CardRef::named("Giant"), CardRef::named("NotACard")], - ..StarterProfile::default() - }, - ..config - }) - .await; - let mut client = FakeClient::connect(server.port).await; - client.send(&fresh_login()).await; - let _ = client.receive().await; - let (message_type, payload) = client.receive().await; - assert_eq!(message_type, message_type::OWN_HOME_DATA); - let home_data = OwnHomeDataMessage::from_bytes(&payload).expect("decode"); - let deck = &home_data.home.decks[0]; - assert_eq!(deck.filled_slot_count(), 4); - assert_eq!( - deck.slots[0].as_ref().expect("knight").data.global_id(), - Some(GlobalId::new(table::SPELLS_CHARACTERS, 0)) - ); - assert_eq!( - deck.slots[2].as_ref().expect("fireball").data.global_id(), - Some(GlobalId::new(table::SPELLS_OTHER, 0)) - ); - assert_eq!( - home_data.home.arena.global_id(), - Some(GlobalId::new(table::ARENAS, 1)) - ); - assert_eq!(home_data.avatar.arena, home_data.home.arena); - assert_eq!(home_data.home.spell_collection.spells.len(), 1); - client.close().await; - let _ = std::fs::remove_dir_all(&catalog_root); -} diff --git a/services/service-rpc/Cargo.toml b/services/service-rpc/Cargo.toml index 13f7ebd..0fb9690 100644 --- a/services/service-rpc/Cargo.toml +++ b/services/service-rpc/Cargo.toml @@ -11,7 +11,3 @@ tracing = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } async-trait = { workspace = true } - -[dev-dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net", "io-util", "sync", "time"] } -async-trait = { workspace = true } diff --git a/services/service-rpc/tests/transport.rs b/services/service-rpc/tests/transport.rs deleted file mode 100644 index 23555aa..0000000 --- a/services/service-rpc/tests/transport.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::sync::Arc; -use service_rpc::base64; -use service_rpc::{ - serve, AccountRef, GameRequest, GameResponse, HomeRequestKind, RpcClient, RpcResult, RpcService, - WireMessage, -}; -use tokio::net::TcpListener; -struct EchoGame; -#[async_trait::async_trait] -impl RpcService for EchoGame { - type Request = GameRequest; - type Response = GameResponse; - fn service_name(&self) -> &'static str { - "echo-game" - } - async fn call(&self, request: GameRequest) -> RpcResult { - match request { - GameRequest::LoadHome { account, .. } => Ok(GameResponse::messages(vec![ - WireMessage::new(24101, 0, vec![account.low as u8, 0xFF, 0x00, 0x7F]), - ])), - _ => Ok(GameResponse::Empty), - } - } -} -#[tokio::test] -async fn requests_round_trip_over_the_length_prefixed_transport() { - let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); - let endpoint = listener.local_addr().expect("addr").to_string(); - tokio::spawn(async move { - let _ = serve(listener, Arc::new(EchoGame)).await; - }); - let client: RpcClient = RpcClient::new(endpoint); - let response = client - .call(&GameRequest::LoadHome { - account: AccountRef::new(0, 9), - kind: HomeRequestKind::Login, - }) - .await - .expect("call"); - let messages = response.into_messages(); - assert_eq!(messages.len(), 1); - assert_eq!(messages[0].message_type, 24101); - assert_eq!(messages[0].payload, vec![9, 0xFF, 0x00, 0x7F]); - let empty = client - .call(&GameRequest::Disconnect { - account: AccountRef::new(0, 9), - }) - .await - .expect("call"); - assert_eq!(empty, GameResponse::Empty); -} -#[tokio::test] -async fn payloads_survive_base64_encoding() { - for length in 0..64usize { - let payload: Vec = (0..length).map(|index| (index * 7 % 251) as u8).collect(); - let encoded = base64::encode(&payload); - assert_eq!(base64::decode(&encoded).expect("decode"), payload); - } - assert_eq!(base64::encode(b"scroll"), "c2Nyb2xs"); - assert_eq!(base64::decode("c2Nyb2xs").unwrap(), b"scroll"); -} diff --git a/titan/titan/Cargo.toml b/titan/titan/Cargo.toml index d243726..f57f117 100644 --- a/titan/titan/Cargo.toml +++ b/titan/titan/Cargo.toml @@ -11,5 +11,3 @@ titan-derive = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } - -[dev-dependencies] diff --git a/titan/titan/tests/csv.rs b/titan/titan/tests/csv.rs deleted file mode 100644 index 7ca02ed..0000000 --- a/titan/titan/tests/csv.rs +++ /dev/null @@ -1,90 +0,0 @@ -use titan::csv::{ColumnType, CsvNode, CsvReader}; -const SPELLS: &str = concat!( - "Name,Rarity,Mana,SummonCharacter,Damage,Enabled\n", - "string,string,int,string,int,boolean\n", - "Knight,Common,3,Knight,75,true\n", - ",,,,82,\n", - ",,,,90,\n", - "Archers,Common,3,Archer,42,true\n", - ",,,,46,\n", - "\"Mini P.E.K.K.A\",Rare,4,MiniPekka,325,false\n" -); -#[test] -fn the_first_two_lines_declare_names_and_types() { - let table = CsvReader::parse("spells_characters.csv", SPELLS).expect("parse"); - assert_eq!(table.column_count(), 6); - assert_eq!(table.column_name(0), "Name"); - assert_eq!(table.column_type(0), Some(ColumnType::String)); - assert_eq!(table.column_type(2), Some(ColumnType::Int)); - assert_eq!(table.column_type(5), Some(ColumnType::Boolean)); - assert_eq!(table.column_index("Damage"), Some(4)); -} -#[test] -fn a_row_opens_only_when_the_first_column_is_populated() { - let table = CsvReader::parse("spells_characters.csv", SPELLS).expect("parse"); - assert_eq!(table.row_count(), 3); - assert_eq!(table.row_at(0).unwrap().string(0), "Knight"); - assert_eq!(table.row_at(1).unwrap().string(0), "Archers"); - assert_eq!(table.row_at(2).unwrap().string(0), "Mini P.E.K.K.A"); -} -#[test] -fn continuation_lines_extend_the_per_level_arrays() { - let table = CsvReader::parse("spells_characters.csv", SPELLS).expect("parse"); - let knight = table.row_at(0).expect("knight"); - assert_eq!(knight.array_size(), 3); - assert_eq!(knight.value_count(4), 3); - assert_eq!(knight.int_at(4, 0), 75); - assert_eq!(knight.int_at(4, 1), 82); - assert_eq!(knight.int_at(4, 2), 90); - assert_eq!(knight.scaled_int(4, 9), 90); - assert_eq!(knight.int(2), 3); - assert!(knight.boolean(5)); - let archers = table.row_at(1).expect("archers"); - assert_eq!(archers.value_count(4), 2); - assert!(!table.row_at(2).unwrap().boolean(5)); -} -#[test] -fn rows_are_addressable_by_their_first_column() { - let table = CsvReader::parse("spells_characters.csv", SPELLS).expect("parse"); - assert_eq!(table.row_index_of("Archers"), Some(1)); - assert_eq!( - table.row_by_name("Mini P.E.K.K.A").map(|row| row.int(2)), - Some(4) - ); - assert!(table.row_by_name("Golem").is_none()); -} -#[test] -fn quoted_cells_keep_their_commas() { - let source = concat!( - "Name,Text\n", - "string,string\n", - "Greeting,\"hello, world\"\n", - "Quote,\"say \"\"hi\"\"\"\n" - ); - let table = CsvReader::parse("hints.csv", source).expect("parse"); - assert_eq!(table.row_at(0).unwrap().string(1), "hello, world"); - assert_eq!(table.row_at(1).unwrap().string(1), "say \"hi\""); -} -#[test] -fn missing_trailing_cells_decode_as_empty() { - let source = concat!("Name,A,B\n", "string,int,int\n", "Row,5\n"); - let table = CsvReader::parse("t.csv", source).expect("parse"); - let row = table.row_at(0).expect("row"); - assert_eq!(row.int(1), 5); - assert_eq!(row.int(2), 0); - assert!(row.value(2).expect("value").is_empty()); -} -#[test] -fn an_unknown_column_type_is_rejected() { - let source = concat!("Name,A\n", "string,float\n", "Row,1\n"); - let error = CsvReader::parse("bad.csv", source).expect_err("must fail"); - assert!(error.to_string().contains("float")); -} -#[test] -fn a_node_wraps_the_table_with_its_file_identity() { - let node = CsvNode::load("csv_logic/spells_characters.csv", SPELLS).expect("load"); - assert_eq!(node.name(), "spells_characters"); - assert_eq!(node.file_name(), "csv_logic/spells_characters.csv"); - assert_eq!(node.table().row_count(), 3); - assert_eq!(node.node_count(), 0); -} diff --git a/titan/titan/tests/wire.rs b/titan/titan/tests/wire.rs deleted file mode 100644 index 5c16a63..0000000 --- a/titan/titan/tests/wire.rs +++ /dev/null @@ -1,168 +0,0 @@ -use titan::crypto::{Rc4Encrypter, StreamEncrypter}; -use titan::{ByteStreamReader, ByteStreamWriter, FrameCodec, FrameHeader, MessageFrame, HEADER_LEN}; -fn roundtrip_vint(value: i32) { - let mut writer = ByteStreamWriter::new(); - writer.write_vint(value); - let bytes = writer.into_inner(); - let mut reader = ByteStreamReader::new(&bytes); - assert_eq!(reader.read_vint().unwrap(), value, "vint {value} roundtrip"); - assert_eq!(reader.remaining(), 0, "vint {value} length"); -} -#[test] -fn vint_roundtrips_across_every_width() { - let boundaries = [ - 0, - 1, - 63, - 64, - 0x1FFF, - 0x2000, - 0x000F_FFFF, - 0x0010_0000, - 0x07FF_FFFF, - 0x0800_0000, - i32::MAX, - -1, - -63, - -64, - -8191, - -8192, - -1_048_575, - -1_048_576, - -134_217_727, - -134_217_728, - i32::MIN, - ]; - for value in boundaries { - roundtrip_vint(value); - } - let mut value: i64 = -2_000_000; - while value < 2_000_000 { - roundtrip_vint(value as i32); - value += 9_973; - } -} -#[test] -fn vint_encodes_to_the_expected_byte_widths() { - let cases = [ - (0i32, 1usize), - (63, 1), - (64, 2), - (0x1FFF, 2), - (0x2000, 3), - (0x000F_FFFF, 3), - (0x0010_0000, 4), - (0x07FF_FFFF, 4), - (0x0800_0000, 5), - (-63, 1), - (-64, 2), - (-8191, 2), - (-8192, 3), - (-1_048_575, 3), - (-1_048_576, 4), - (-134_217_727, 4), - (-134_217_728, 5), - ]; - for (value, width) in cases { - let mut writer = ByteStreamWriter::new(); - writer.write_vint(value); - assert_eq!(writer.len(), width, "width of {value}"); - } -} -#[test] -fn booleans_are_bit_packed_until_another_primitive_is_written() { - let mut writer = ByteStreamWriter::new(); - for bit in 0..8 { - writer.write_boolean(bit % 2 == 0); - } - writer.write_boolean(true); - writer.write_vint(7); - writer.write_boolean(true); - let bytes = writer.into_inner(); - assert_eq!(bytes[0], 0b0101_0101); - assert_eq!(bytes[1], 0b0000_0001); - assert_eq!(bytes[2], 7); - assert_eq!(bytes[3], 0b0000_0001); - let mut reader = ByteStreamReader::new(&bytes); - for bit in 0..8 { - assert_eq!(reader.read_boolean().unwrap(), bit % 2 == 0); - } - assert!(reader.read_boolean().unwrap()); - assert_eq!(reader.read_vint().unwrap(), 7); - assert!(reader.read_boolean().unwrap()); - assert_eq!(reader.remaining(), 0); -} -#[test] -fn strings_carry_a_big_endian_length_prefix() { - let mut writer = ByteStreamWriter::new(); - writer.write_string(Some("scroll")).unwrap(); - writer.write_string(None).unwrap(); - writer.write_string_reference("").unwrap(); - let bytes = writer.into_inner(); - assert_eq!(&bytes[0..4], &[0, 0, 0, 6]); - assert_eq!(&bytes[4..10], b"scroll"); - assert_eq!(&bytes[10..14], &[0xFF, 0xFF, 0xFF, 0xFF]); - assert_eq!(&bytes[14..18], &[0, 0, 0, 0]); - let mut reader = ByteStreamReader::new(&bytes); - assert_eq!(reader.read_string().unwrap().as_deref(), Some("scroll")); - assert_eq!(reader.read_string().unwrap(), None); - assert_eq!(reader.read_string_reference().unwrap(), ""); -} -#[test] -fn frame_header_is_seven_bytes_big_endian() { - let frame = MessageFrame::new(24101, 0, vec![1, 2, 3]); - let encoded = FrameCodec::encode(&frame).unwrap(); - assert_eq!(encoded.len(), HEADER_LEN + 3); - assert_eq!(&encoded[0..HEADER_LEN], &[0x5E, 0x25, 0, 0, 3, 0, 0]); - let mut header_bytes = [0u8; HEADER_LEN]; - header_bytes.copy_from_slice(&encoded[0..HEADER_LEN]); - let header = FrameHeader::parse(&header_bytes); - assert_eq!(header.message_type, 24101); - assert_eq!(header.payload_len, 3); - assert_eq!(header.message_version, 0); -} -#[test] -fn rc4_streams_are_symmetric_and_stateful() { - let mut client_to_server = Rc4Encrypter::scroll_default(); - let mut server_side = Rc4Encrypter::scroll_default(); - let first = client_to_server.encrypt(b"LoginMessage payload").unwrap(); - let second = client_to_server.encrypt(b"KeepAliveMessage").unwrap(); - assert_eq!(server_side.decrypt(&first).unwrap(), b"LoginMessage payload"); - assert_eq!(server_side.decrypt(&second).unwrap(), b"KeepAliveMessage"); -} -#[test] -fn rc4_matches_a_reference_implementation_with_the_supercell_keystream_discard() { - fn reference(key: &[u8], nonce: &[u8], length: usize) -> Vec { - let mut seed = key.to_vec(); - seed.extend_from_slice(nonce); - let mut state: Vec = (0..=255u8).collect(); - let mut j: u8 = 0; - for i in 0..256usize { - j = j.wrapping_add(state[i]).wrapping_add(seed[i % seed.len()]); - state.swap(i, j as usize); - } - let mut i: u8 = 0; - let mut j: u8 = 0; - let next = |state: &mut Vec, i: &mut u8, j: &mut u8| { - *i = i.wrapping_add(1); - *j = j.wrapping_add(state[*i as usize]); - state.swap(*i as usize, *j as usize); - let index = state[*i as usize].wrapping_add(state[*j as usize]); - state[index as usize] - }; - for _ in 0..seed.len() { - next(&mut state, &mut i, &mut j); - } - (0..length) - .map(|_| next(&mut state, &mut i, &mut j)) - .collect() - } - let expected = reference( - titan::crypto::SCROLL_RC4_KEY.as_bytes(), - titan::crypto::SCROLL_RC4_NONCE.as_bytes(), - 32, - ); - let mut cipher = Rc4Encrypter::scroll_default(); - let produced = cipher.encrypt(&[0u8; 32]).unwrap(); - assert_eq!(produced, expected); -}