diff --git a/Cargo.lock b/Cargo.lock index 59f241834e..ddc2bfa042 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8090,12 +8090,15 @@ dependencies = [ "base64 0.21.4", "camino", "chrono", + "dojo-test-utils", "dojo-types", "dojo-world", "lazy_static", + "scarb", "scarb-ui", "serde", "serde_json", + "sozo", "sqlx", "starknet", "starknet-crypto 0.6.0", diff --git a/crates/dojo-test-utils/build.rs b/crates/dojo-test-utils/build.rs index f411e25204..60efa28ba3 100644 --- a/crates/dojo-test-utils/build.rs +++ b/crates/dojo-test-utils/build.rs @@ -10,35 +10,40 @@ fn main() { use scarb::ops; use scarb_ui::Verbosity; - let target_path = - Utf8PathBuf::from_path_buf("../../examples/spawn-and-move/target".into()).unwrap(); - if target_path.exists() { - return; + let project_paths = + vec!["../../examples/spawn-and-move", "../torii/graphql/src/tests/types-test"]; + + project_paths.iter().for_each(|path| compile(path)); + + fn compile(path: &str) { + let target_path = Utf8PathBuf::from_path_buf(format!("{}/target", path).into()).unwrap(); + if target_path.exists() { + return; + } + + let mut compilers = CompilerRepository::empty(); + compilers.add(Box::new(DojoCompiler)).unwrap(); + + let cairo_plugins = CairoPluginRepository::default(); + + let cache_dir = assert_fs::TempDir::new().unwrap(); + let config_dir = assert_fs::TempDir::new().unwrap(); + + let path = Utf8PathBuf::from_path_buf(format!("{}/Scarb.toml", path).into()).unwrap(); + let config = Config::builder(path.canonicalize_utf8().unwrap()) + .global_cache_dir_override(Some(Utf8Path::from_path(cache_dir.path()).unwrap())) + .global_config_dir_override(Some(Utf8Path::from_path(config_dir.path()).unwrap())) + .ui_verbosity(Verbosity::Verbose) + .log_filter_directive(env::var_os("SCARB_LOG")) + .compilers(compilers) + .cairo_plugins(cairo_plugins.into()) + .build() + .unwrap(); + + let ws = ops::read_workspace(config.manifest_path(), &config).unwrap(); + let packages = ws.members().map(|p| p.id).collect(); + ops::compile(packages, &ws).unwrap(); } - - let mut compilers = CompilerRepository::empty(); - compilers.add(Box::new(DojoCompiler)).unwrap(); - - let cairo_plugins = CairoPluginRepository::default(); - - let cache_dir = assert_fs::TempDir::new().unwrap(); - let config_dir = assert_fs::TempDir::new().unwrap(); - - let path = - Utf8PathBuf::from_path_buf("../../examples/spawn-and-move/Scarb.toml".into()).unwrap(); - let config = Config::builder(path.canonicalize_utf8().unwrap()) - .global_cache_dir_override(Some(Utf8Path::from_path(cache_dir.path()).unwrap())) - .global_config_dir_override(Some(Utf8Path::from_path(config_dir.path()).unwrap())) - .ui_verbosity(Verbosity::Verbose) - .log_filter_directive(env::var_os("SCARB_LOG")) - .compilers(compilers) - .cairo_plugins(cairo_plugins.into()) - .build() - .unwrap(); - - let ws = ops::read_workspace(config.manifest_path(), &config).unwrap(); - let packages = ws.members().map(|p| p.id).collect(); - ops::compile(packages, &ws).unwrap(); } #[cfg(not(feature = "build-examples"))] diff --git a/crates/torii/graphql/Cargo.toml b/crates/torii/graphql/Cargo.toml index 7c78f3223c..d86cf76c0d 100644 --- a/crates/torii/graphql/Cargo.toml +++ b/crates/torii/graphql/Cargo.toml @@ -35,6 +35,9 @@ warp.workspace = true [dev-dependencies] camino.workspace = true +dojo-test-utils = { path = "../../dojo-test-utils" } dojo-world = { path = "../../dojo-world" } starknet-crypto.workspace = true starknet.workspace = true +scarb.workspace = true +sozo = { path = "../../sozo" } diff --git a/crates/torii/graphql/src/tests/entities_test.rs b/crates/torii/graphql/src/tests/entities_test.rs index d27bb664de..12b9b32eea 100644 --- a/crates/torii/graphql/src/tests/entities_test.rs +++ b/crates/torii/graphql/src/tests/entities_test.rs @@ -1,136 +1,200 @@ #[cfg(test)] mod tests { - use sqlx::SqlitePool; + use anyhow::Result; + use async_graphql::dynamic::Schema; + use serde_json::Value; use starknet_crypto::{poseidon_hash_many, FieldElement}; - use torii_core::sql::Sql; + use crate::schema::build_schema; use crate::tests::{ - cursor_paginate, entity_fixtures, offset_paginate, run_graphql_query, Entity, Moves, - Paginate, Position, + run_graphql_query, spinup_types_test, Connection, Entity, Record, Subrecord, }; - #[sqlx::test(migrations = "../migrations")] - async fn test_entity(pool: SqlitePool) { - let mut db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); - - entity_fixtures(&mut db).await; - - let entity_id = poseidon_hash_many(&[FieldElement::ONE]); - println!("{:#x}", entity_id); + async fn entities_query(schema: &Schema, arg: &str) -> Value { let query = format!( r#" - {{ - entity(id: "{:#x}") {{ - model_names + {{ + entities {} {{ + total_count + edges {{ + cursor + node {{ + keys + model_names }} + }} }} + }} "#, - entity_id + arg, ); - let value = run_graphql_query(&pool, &query).await; - let entity = value.get("entity").ok_or("no entity found").unwrap(); - let entity: Entity = serde_json::from_value(entity.clone()).unwrap(); - assert_eq!(entity.model_names, "Moves".to_string()); + let result = run_graphql_query(schema, &query).await; + result.get("entities").ok_or("entities not found").unwrap().clone() } - #[sqlx::test(migrations = "../migrations")] - async fn test_entity_models(pool: SqlitePool) { - let mut db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); - entity_fixtures(&mut db).await; - - let entity_id = poseidon_hash_many(&[FieldElement::THREE]); + async fn entity_model_query(schema: &Schema, id: &FieldElement) -> Value { let query = format!( r#" - {{ - entity (id: "{:#x}") {{ - models {{ - __typename - ... on Moves {{ - remaining - last_direction - }} - ... on Position {{ - vec {{ - x - y - }} - }} - }} - }} + {{ + entity (id: "{:#x}") {{ + keys + model_names + models {{ + ... on Record {{ + __typename + record_id + type_u8 + type_u16 + type_u32 + type_u64 + type_u128 + type_u256 + type_bool + type_felt + type_class_hash + type_contract_address + random_u8 + random_u128 }} - "#, - entity_id + ... on Subrecord {{ + __typename + record_id + subrecord_id + type_u8 + random_u8 + }} + }} + }} + }} + "#, + id ); - let value = run_graphql_query(&pool, &query).await; - let entity = value.get("entity").ok_or("no entity found").unwrap(); - let models = entity.get("models").ok_or("no models found").unwrap(); - let model_moves: Moves = serde_json::from_value(models[0].clone()).unwrap(); - let model_position: Position = serde_json::from_value(models[1].clone()).unwrap(); - - assert_eq!(model_moves.__typename, "Moves"); - assert_eq!(model_moves.remaining, 10); - assert_eq!(model_position.__typename, "Position"); - assert_eq!(model_position.vec.x, 42); - assert_eq!(model_position.vec.y, 69); + let result = run_graphql_query(schema, &query).await; + result.get("entity").ok_or("entity not found").unwrap().clone() } - #[sqlx::test(migrations = "../migrations")] - async fn test_entities_cursor_pagination(pool: SqlitePool) { - let mut db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); - entity_fixtures(&mut db).await; - - let page_size = 2; - - // Forward pagination - let entities_connection = cursor_paginate(&pool, None, Paginate::Forward, page_size).await; - assert_eq!(entities_connection.total_count, 3); - assert_eq!(entities_connection.edges.len(), page_size); - - let cursor: String = entities_connection.edges[0].cursor.clone(); - let next_cursor: String = entities_connection.edges[1].cursor.clone(); - let entities_connection = - cursor_paginate(&pool, Some(cursor), Paginate::Forward, page_size).await; - assert_eq!(entities_connection.total_count, 3); - assert_eq!(entities_connection.edges.len(), page_size); - assert_eq!(entities_connection.edges[0].cursor, next_cursor); - - // Backward pagination - let entities_connection = cursor_paginate(&pool, None, Paginate::Backward, page_size).await; - assert_eq!(entities_connection.total_count, 3); - assert_eq!(entities_connection.edges.len(), page_size); - - let cursor: String = entities_connection.edges[0].cursor.clone(); - let next_cursor: String = entities_connection.edges[1].cursor.clone(); - let entities_connection = - cursor_paginate(&pool, Some(cursor), Paginate::Backward, page_size).await; - assert_eq!(entities_connection.total_count, 3); - assert_eq!(entities_connection.edges.len(), page_size); - assert_eq!(entities_connection.edges[0].cursor, next_cursor); - } + // End to end test spins up a test sequencer and deploys types-test project, this takes a while + // to run so combine all related tests into one + #[tokio::test(flavor = "multi_thread")] + async fn entities_test() -> Result<()> { + let pool = spinup_types_test().await?; + let schema = build_schema(&pool).await.unwrap(); + + // default without params + let entities = entities_query(&schema, "").await; + let connection: Connection = serde_json::from_value(entities).unwrap(); + let first_entity = connection.edges.first().unwrap(); + let last_entity = connection.edges.last().unwrap(); + assert_eq!(connection.edges.len(), 10); + assert_eq!(connection.total_count, 20); + assert_eq!(&first_entity.node.model_names, "Subrecord"); + assert_eq!(&last_entity.node.model_names, "Record"); + + // first key param - returns all entities with `0x0` as first key + let entities = entities_query(&schema, "(keys: [\"0x0\"])").await; + let connection: Connection = serde_json::from_value(entities).unwrap(); + let first_entity = connection.edges.first().unwrap(); + let last_entity = connection.edges.last().unwrap(); + assert_eq!(connection.edges.len(), 2); + assert_eq!(connection.total_count, 2); + assert_eq!(first_entity.node.keys.clone().unwrap(), vec!["0x0", "0x1"]); + assert_eq!(last_entity.node.keys.clone().unwrap(), vec!["0x0"]); + + // double key param - returns all entities with `0x0` as first key and `0x1` as second key + let entities = entities_query(&schema, "(keys: [\"0x0\", \"0x1\"])").await; + let connection: Connection = serde_json::from_value(entities).unwrap(); + let first_entity = connection.edges.first().unwrap(); + assert_eq!(connection.edges.len(), 1); + assert_eq!(connection.total_count, 1); + assert_eq!(first_entity.node.keys.clone().unwrap(), vec!["0x0", "0x1"]); + + // pagination testing + let entities = entities_query(&schema, "(first: 20)").await; + let all_entities_connection: Connection = serde_json::from_value(entities).unwrap(); + let one = all_entities_connection.edges.get(0).unwrap(); + let two = all_entities_connection.edges.get(1).unwrap(); + let three = all_entities_connection.edges.get(2).unwrap(); + let four = all_entities_connection.edges.get(3).unwrap(); + let five = all_entities_connection.edges.get(4).unwrap(); + let six = all_entities_connection.edges.get(5).unwrap(); + let seven = all_entities_connection.edges.get(6).unwrap(); + + // cursor based forward pagination + let entities = + entities_query(&schema, &format!("(first: 2, after: \"{}\")", two.cursor)).await; + let connection: Connection = serde_json::from_value(entities).unwrap(); + assert_eq!(connection.edges.len(), 2); + assert_eq!(connection.edges.first().unwrap(), three); + assert_eq!(connection.edges.last().unwrap(), four); + + let entities = + entities_query(&schema, &format!("(first: 3, after: \"{}\")", three.cursor)).await; + let connection: Connection = serde_json::from_value(entities).unwrap(); + assert_eq!(connection.edges.len(), 3); + assert_eq!(connection.edges.first().unwrap(), four); + assert_eq!(connection.edges.last().unwrap(), six); + + // cursor based backward pagination + let entities = + entities_query(&schema, &format!("(last: 2, before: \"{}\")", seven.cursor)).await; + let connection: Connection = serde_json::from_value(entities).unwrap(); + assert_eq!(connection.edges.len(), 2); + assert_eq!(connection.edges.first().unwrap(), six); + assert_eq!(connection.edges.last().unwrap(), five); + + let entities = + entities_query(&schema, &format!("(last: 3, before: \"{}\")", six.cursor)).await; + let connection: Connection = serde_json::from_value(entities).unwrap(); + assert_eq!(connection.edges.len(), 3); + assert_eq!(connection.edges.first().unwrap(), five); + assert_eq!(connection.edges.last().unwrap(), three); + + let empty_entities = entities_query( + &schema, + &format!( + "(first: 1, after: \"{}\")", + all_entities_connection.edges.last().unwrap().cursor + ), + ) + .await; + let connection: Connection = serde_json::from_value(empty_entities).unwrap(); + assert_eq!(connection.edges.len(), 0); + + // offset/limit based pagination + let entities = entities_query(&schema, "(limit: 2)").await; + let connection: Connection = serde_json::from_value(entities).unwrap(); + assert_eq!(connection.edges.len(), 2); + assert_eq!(connection.edges.first().unwrap(), one); + assert_eq!(connection.edges.last().unwrap(), two); + + let entities = entities_query(&schema, "(limit: 3, offset: 2)").await; + let connection: Connection = serde_json::from_value(entities).unwrap(); + assert_eq!(connection.edges.len(), 3); + assert_eq!(connection.edges.first().unwrap(), three); + assert_eq!(connection.edges.last().unwrap(), five); + + let empty_entities = entities_query(&schema, "(limit: 1, offset: 20)").await; + let connection: Connection = serde_json::from_value(empty_entities).unwrap(); + assert_eq!(connection.edges.len(), 0); + + // entity model union + let id = poseidon_hash_many(&[FieldElement::ZERO]); + let entity = entity_model_query(&schema, &id).await; + let models = entity.get("models").ok_or("no models found").unwrap(); + let record: Record = serde_json::from_value(models[0].clone()).unwrap(); + assert_eq!(&record.__typename, "Record"); + assert_eq!(record.record_id, 0); + + let id = poseidon_hash_many(&[FieldElement::ZERO, FieldElement::ONE]); + let entity = entity_model_query(&schema, &id).await; + let models = entity.get("models").ok_or("no models found").unwrap(); + let subrecord: Subrecord = serde_json::from_value(models[0].clone()).unwrap(); + assert_eq!(&subrecord.__typename, "Subrecord"); + assert_eq!(subrecord.subrecord_id, 1); - #[sqlx::test(migrations = "../migrations")] - async fn test_entities_offset_pagination(pool: SqlitePool) { - let mut db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); - entity_fixtures(&mut db).await; - - let limit = 3; - let mut offset = 0; - let entities_connection = offset_paginate(&pool, offset, limit).await; - let offset_plus_one = entities_connection.edges[1].node.model_names.clone(); - let offset_plus_two = entities_connection.edges[2].node.model_names.clone(); - assert_eq!(entities_connection.edges.len(), 3); - - offset = 1; - let entities_connection = offset_paginate(&pool, offset, limit).await; - assert_eq!(entities_connection.edges[0].node.model_names, offset_plus_one); - assert_eq!(entities_connection.edges.len(), 2); - - offset = 2; - let entities_connection = offset_paginate(&pool, offset, limit).await; - assert_eq!(entities_connection.edges[0].node.model_names, offset_plus_two); - assert_eq!(entities_connection.edges.len(), 1); + Ok(()) } } diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index 328803a337..131cbc1d2b 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -1,10 +1,31 @@ +use std::str::FromStr; + +use anyhow::Result; +use async_graphql::dynamic::Schema; +use dojo_test_utils::compiler::build_test_config; +use dojo_test_utils::migration::prepare_migration; +use dojo_test_utils::sequencer::{ + get_default_test_starknet_config, SequencerConfig, TestSequencer, +}; use dojo_types::primitive::Primitive; use dojo_types::schema::{Enum, Member, Struct, Ty}; +use dojo_world::contracts::WorldContractReader; +use dojo_world::utils::TransactionWaiter; +use scarb::ops; use serde::Deserialize; use serde_json::Value; +use sozo::ops::migration::execute_strategy; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use sqlx::SqlitePool; -use starknet::core::types::FieldElement; +use starknet::accounts::{Account, Call}; +use starknet::core::types::{BlockId, BlockTag, FieldElement, InvokeTransactionResult}; +use starknet::macros::selector; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::JsonRpcClient; use tokio_stream::StreamExt; +use torii_core::engine::{Engine, EngineConfig, Processors}; +use torii_core::processors::register_model::RegisterModelProcessor; +use torii_core::processors::store_set_record::StoreSetRecordProcessor; use torii_core::sql::Sql; mod entities_test; @@ -13,19 +34,19 @@ mod subscription_test; use crate::schema::build_schema; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, PartialEq)] pub struct Connection { pub total_count: i64, pub edges: Vec>, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, PartialEq)] pub struct Edge { pub node: T, pub cursor: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, PartialEq)] pub struct Entity { pub model_names: String, pub keys: Option>, @@ -53,13 +74,63 @@ pub struct Position { pub entity: Option, } -pub enum Paginate { - Forward, - Backward, +#[derive(Deserialize, Debug, PartialEq)] +pub struct Record { + pub __typename: String, + pub record_id: u32, + pub type_u8: u8, + pub type_u16: u16, + pub type_u32: u32, + pub type_u64: u64, + pub type_u128: String, + pub type_u256: String, + pub type_bool: bool, + pub type_felt: String, + pub type_class_hash: String, + pub type_contract_address: String, + pub random_u8: u8, + pub random_u128: String, + pub type_nested: Option, + pub entity: Option, +} + +#[derive(Deserialize, Debug, PartialEq)] +pub struct Nested { + pub __typename: String, + pub depth: u8, + pub type_number: u8, + pub type_string: String, + pub type_nested_more: NestedMore, } -pub async fn run_graphql_query(pool: &SqlitePool, query: &str) -> Value { - let schema = build_schema(pool).await.unwrap(); +#[derive(Deserialize, Debug, PartialEq)] +pub struct NestedMore { + pub __typename: String, + pub depth: u8, + pub type_number: u8, + pub type_string: String, + pub type_nested_more_more: NestedMoreMore, +} + +#[derive(Deserialize, Debug, PartialEq)] +pub struct NestedMoreMore { + pub __typename: String, + pub depth: u8, + pub type_number: u8, + pub type_string: String, +} + +#[derive(Deserialize, Debug, PartialEq)] +pub struct Subrecord { + pub __typename: String, + pub record_id: u32, + pub subrecord_id: u32, + pub type_u8: u8, + pub random_u8: u8, + pub entity: Option, +} + +pub async fn run_graphql_query(schema: &Schema, query: &str) -> Value { let res = schema.execute(query).await; assert!(res.errors.is_empty(), "GraphQL query returned errors: {:?}", res.errors); @@ -77,154 +148,6 @@ pub async fn run_graphql_subscription( // fn subscribe() is called from inside dynamic subscription } -pub async fn entity_fixtures(db: &mut Sql) { - model_fixtures(db).await; - db.set_entity( - Ty::Struct(Struct { - name: "Moves".to_string(), - children: vec![ - Member { - name: "player".to_string(), - key: true, - ty: Ty::Primitive(Primitive::ContractAddress(Some(FieldElement::ONE))), - }, - Member { - name: "remaining".to_string(), - key: false, - ty: Ty::Primitive(Primitive::U8(Some(10))), - }, - Member { - name: "last_direction".to_string(), - key: false, - ty: Ty::Enum(Enum { - name: "Direction".to_string(), - option: Some(1), - options: vec![ - ("None".to_string(), Ty::Tuple(vec![])), - ("Left".to_string(), Ty::Tuple(vec![])), - ("Right".to_string(), Ty::Tuple(vec![])), - ("Up".to_string(), Ty::Tuple(vec![])), - ("Down".to_string(), Ty::Tuple(vec![])), - ], - }), - }, - ], - }), - &format!("0x{:064x}:0x{:04x}:0x{:04x}", 0, 0, 0), - ) - .await - .unwrap(); - - db.set_entity( - Ty::Struct(Struct { - name: "Position".to_string(), - children: vec![ - Member { - name: "player".to_string(), - key: true, - ty: Ty::Primitive(Primitive::ContractAddress(Some(FieldElement::TWO))), - }, - Member { - name: "vec".to_string(), - key: false, - ty: Ty::Struct(Struct { - name: "Vec2".to_string(), - children: vec![ - Member { - name: "x".to_string(), - key: false, - ty: Ty::Primitive(Primitive::U32(Some(42))), - }, - Member { - name: "y".to_string(), - key: false, - ty: Ty::Primitive(Primitive::U32(Some(69))), - }, - ], - }), - }, - ], - }), - &format!("0x{:064x}:0x{:04x}:0x{:04x}", 0, 0, 1), - ) - .await - .unwrap(); - - // Set an entity with both moves and position models - db.set_entity( - Ty::Struct(Struct { - name: "Moves".to_string(), - children: vec![ - Member { - name: "player".to_string(), - key: true, - ty: Ty::Primitive(Primitive::ContractAddress(Some(FieldElement::THREE))), - }, - Member { - name: "remaining".to_string(), - key: false, - ty: Ty::Primitive(Primitive::U8(Some(10))), - }, - Member { - name: "last_direction".to_string(), - key: false, - ty: Ty::Enum(Enum { - name: "Direction".to_string(), - option: Some(2), - options: vec![ - ("None".to_string(), Ty::Tuple(vec![])), - ("Left".to_string(), Ty::Tuple(vec![])), - ("Right".to_string(), Ty::Tuple(vec![])), - ("Up".to_string(), Ty::Tuple(vec![])), - ("Down".to_string(), Ty::Tuple(vec![])), - ], - }), - }, - ], - }), - &format!("0x{:064x}:0x{:04x}:0x{:04x}", 0, 0, 2), - ) - .await - .unwrap(); - - db.set_entity( - Ty::Struct(Struct { - name: "Position".to_string(), - children: vec![ - Member { - name: "player".to_string(), - key: true, - ty: Ty::Primitive(Primitive::ContractAddress(Some(FieldElement::THREE))), - }, - Member { - name: "vec".to_string(), - key: false, - ty: Ty::Struct(Struct { - name: "Vec2".to_string(), - children: vec![ - Member { - name: "x".to_string(), - key: false, - ty: Ty::Primitive(Primitive::U32(Some(42))), - }, - Member { - name: "y".to_string(), - key: false, - ty: Ty::Primitive(Primitive::U32(Some(69))), - }, - ], - }), - }, - ], - }), - &format!("0x{:064x}:0x{:04x}:0x{:04x}", 0, 0, 3), - ) - .await - .unwrap(); - - db.execute().await.unwrap(); -} - pub async fn model_fixtures(db: &mut Sql) { db.register_model( Ty::Struct(Struct { @@ -304,59 +227,56 @@ pub async fn model_fixtures(db: &mut Sql) { .unwrap(); } -pub async fn cursor_paginate( - pool: &SqlitePool, - cursor: Option, - direction: Paginate, - page_size: usize, -) -> Connection { - let (first_last, before_after) = match direction { - Paginate::Forward => ("first", "after"), - Paginate::Backward => ("last", "before"), - }; +pub async fn spinup_types_test() -> Result { + // change sqlite::memory: to sqlite:~/.test.db to dump database to disk + let options = SqliteConnectOptions::from_str("sqlite::memory:")?.create_if_missing(true); + let pool = SqlitePoolOptions::new().max_connections(5).connect_with(options).await.unwrap(); + sqlx::migrate!("../migrations").run(&pool).await.unwrap(); - let cursor = cursor.map_or(String::new(), |c| format!(", {before_after}: \"{c}\"")); - let query = format!( - " - {{ - entities ({first_last}: {page_size} {cursor}) - {{ - total_count - edges {{ - cursor - node {{ - model_names - }} - }} - }} - }} - " - ); + let migration = prepare_migration("./src/tests/types-test/target/dev".into()).unwrap(); + let config = build_test_config("./src/tests/types-test/Scarb.toml").unwrap(); + let mut db = Sql::new(pool.clone(), migration.world_address().unwrap()).await.unwrap(); - let value = run_graphql_query(pool, &query).await; - let entities = value.get("entities").ok_or("entities not found").unwrap(); - serde_json::from_value(entities.clone()).unwrap() -} + let sequencer = + TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; + + let mut account = sequencer.account(); + account.set_block_id(BlockId::Tag(BlockTag::Pending)); + + let provider = JsonRpcClient::new(HttpTransport::new(sequencer.url())); + let world = WorldContractReader::new(migration.world_address().unwrap(), &provider); + let ws = ops::read_workspace(config.manifest_path(), &config) + .unwrap_or_else(|op| panic!("Error building workspace: {op:?}")); -pub async fn offset_paginate(pool: &SqlitePool, offset: u64, limit: u64) -> Connection { - let query = format!( - " - {{ - entities (offset: {offset}, limit: {limit}) - {{ - total_count - edges {{ - cursor - node {{ - model_names - }} - }} - }} - }} - " + execute_strategy(&ws, &migration, &account, None).await.unwrap(); + + // Execute `create` and insert 10 records into storage + let records_contract = "0x1f04153fabe135513a5ef65f45089ababccfee01b001f8b29d95639d4cfaa0c"; + let InvokeTransactionResult { transaction_hash } = account + .execute(vec![Call { + calldata: vec![FieldElement::from_str("0xa").unwrap()], + to: FieldElement::from_str(records_contract).unwrap(), + selector: selector!("create"), + }]) + .send() + .await + .unwrap(); + + TransactionWaiter::new(transaction_hash, &provider).await?; + + let mut engine = Engine::new( + world, + &mut db, + &provider, + Processors { + event: vec![Box::new(RegisterModelProcessor), Box::new(StoreSetRecordProcessor)], + ..Processors::default() + }, + EngineConfig::default(), + None, ); - let value = run_graphql_query(pool, &query).await; - let entities = value.get("entities").ok_or("entities not found").unwrap(); - serde_json::from_value(entities.clone()).unwrap() + let _ = engine.sync_to_head(0).await?; + + Ok(pool) } diff --git a/crates/torii/graphql/src/tests/models_test.rs b/crates/torii/graphql/src/tests/models_test.rs index 163544e58f..4d0d091482 100644 --- a/crates/torii/graphql/src/tests/models_test.rs +++ b/crates/torii/graphql/src/tests/models_test.rs @@ -1,244 +1,259 @@ #[cfg(test)] mod tests { - use sqlx::SqlitePool; - use starknet_crypto::FieldElement; - use torii_core::sql::Sql; + use anyhow::Result; + use async_graphql::dynamic::Schema; + use serde_json::Value; - use crate::tests::{entity_fixtures, run_graphql_query, Connection, Edge, Moves, Position}; + use crate::schema::build_schema; + use crate::tests::{run_graphql_query, spinup_types_test, Connection, Record}; - type OrderTestFn = dyn Fn(&Vec>) -> bool; - - struct OrderTest { - direction: &'static str, - field: &'static str, - test_order: Box, - } - //#[ignore] - #[sqlx::test(migrations = "../migrations")] - async fn test_model_no_filter(pool: SqlitePool) { - let mut db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); - - entity_fixtures(&mut db).await; - - let query = r#" - { - movesModels { - total_count - edges { - node { - __typename - remaining - last_direction - } - cursor - } - } - positionModels { - total_count - edges { - node { + async fn records_model_query(schema: &Schema, arg: &str) -> Value { + let query = format!( + r#" + {{ + recordModels {} {{ + total_count + edges {{ + cursor + node {{ + __typename + record_id + type_u8 + type_u16 + type_u32 + type_u64 + type_u128 + type_u256 + type_bool + type_felt + type_class_hash + type_contract_address + random_u8 + random_u128 + type_nested {{ + __typename + depth + type_number + type_string + type_nested_more {{ + __typename + depth + type_number + type_string + type_nested_more_more {{ __typename - vec { - x - y - } - } - cursor - } - } - } - "#; - - let value = run_graphql_query(&pool, query).await; - - let moves_models = value.get("movesModels").ok_or("no moves found").unwrap(); - let moves_connection: Connection = - serde_json::from_value(moves_models.clone()).unwrap(); - - let position_models = value.get("positionModels").ok_or("no position found").unwrap(); - let position_connection: Connection = - serde_json::from_value(position_models.clone()).unwrap(); - - assert_eq!(moves_connection.edges[0].node.remaining, 10); - assert_eq!(position_connection.edges[0].node.vec.x, 42); - assert_eq!(position_connection.edges[0].node.vec.y, 69); + depth + type_number + type_string + }} + }} + }} + entity {{ + keys + model_names + }} + }} + }} + }} + }} + "#, + arg, + ); + + let result = run_graphql_query(schema, &query).await; + result.get("recordModels").ok_or("recordModels not found").unwrap().clone() } - //#[ignore] - #[sqlx::test(migrations = "../migrations")] - async fn test_model_where_filter(pool: SqlitePool) { - let mut db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); + // End to end test spins up a test sequencer and deploys types-test project, this takes a while + // to run so combine all related tests into one + #[tokio::test(flavor = "multi_thread")] + async fn models_test() -> Result<()> { + let pool = spinup_types_test().await?; + let schema = build_schema(&pool).await.unwrap(); - entity_fixtures(&mut db).await; + // default params, test entity relationship, test deeply nested types + let records = records_model_query(&schema, "").await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let record = connection.edges.last().unwrap(); + let entity = record.node.entity.as_ref().unwrap(); + let nested = record.node.type_nested.as_ref().unwrap(); + let nested_more = &nested.type_nested_more; + let nested_more_more = &nested_more.type_nested_more_more; + assert_eq!(connection.total_count, 10); + assert_eq!(connection.edges.len(), 10); + assert_eq!(&record.node.__typename, "Record"); + assert_eq!(&entity.model_names, "Record"); + assert_eq!(entity.keys.clone().unwrap(), vec!["0x0"]); + assert_eq!(nested.depth, 1); + assert_eq!(nested_more.depth, 2); + assert_eq!(nested_more_more.depth, 3); - // fixtures inserts two position mdoels with members (x: 42, y: 69) and (x: 69, y: 42) - // the following filters and expected total results can be simply calculated - let where_filters = Vec::from([ - ( - r#"where: { playerNEQ: "0x0000000000000000000000000000000000000000000000000000000000000002" }"#, - 1, - ), - ( - r#"where: { playerGT: "0x0000000000000000000000000000000000000000000000000000000000000002" }"#, - 1, - ), - ( - r#"where: { playerGTE: "0x0000000000000000000000000000000000000000000000000000000000000002" }"#, - 2, - ), - ( - r#"where: { playerLT: "0x0000000000000000000000000000000000000000000000000000000000000002" }"#, - 0, - ), - ( - r#"where: { playerLTE: "0x0000000000000000000000000000000000000000000000000000000000000002" }"#, - 1, + // *** WHERE FILTER TESTING *** + + // where filter EQ on u8 + let records = records_model_query(&schema, "(where: { type_u8: 0 })").await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let first_record = connection.edges.first().unwrap(); + assert_eq!(connection.total_count, 1); + assert_eq!(first_record.node.record_id, 0); + + // where filter GTE on u16 + let records = records_model_query(&schema, "(where: { type_u16GTE: 5 })").await; + let connection: Connection = serde_json::from_value(records).unwrap(); + assert_eq!(connection.total_count, 5); + + // where filter LTE on u32 + let records = records_model_query(&schema, "(where: { type_u32LTE: 4 })").await; + let connection: Connection = serde_json::from_value(records).unwrap(); + assert_eq!(connection.total_count, 5); + + // where filter LT and GT + let records = + records_model_query(&schema, "(where: { type_u32GT: 2, type_u64LT: 4 })").await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let first_record = connection.edges.first().unwrap(); + assert_eq!(first_record.node.type_u64, 3); + + // NOTE: output leading zeros on hex strings are trimmed, however, we don't do this yet on + // input hex strings + let felt_str_0x5 = format!("0x{:064x}", 5); + + // where filter EQ on class_hash and contract_address + let records = records_model_query( + &schema, + &format!( + "(where: {{ type_class_hash: \"{}\", type_contract_address: \"{}\" }})", + felt_str_0x5, felt_str_0x5 ), - ( - r#"where: { player: "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" }"#, - 0, + ) + .await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let first_record = connection.edges.first().unwrap(); + assert_eq!(first_record.node.type_class_hash, "0x5"); + + // where filter GTE on u128 (string) + let records = records_model_query( + &schema, + &format!("(where: {{ type_u128GTE: \"{}\" }})", felt_str_0x5), + ) + .await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let first_record = connection.edges.first().unwrap(); + let last_record = connection.edges.last().unwrap(); + assert_eq!(connection.total_count, 5); + assert_eq!(first_record.node.type_u128, "0x9"); + assert_eq!(last_record.node.type_u128, "0x5"); + + // where filter LT on u256 (string) + let records = records_model_query( + &schema, + &format!("(where: {{ type_u256LT: \"{}\" }})", felt_str_0x5), + ) + .await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let first_record = connection.edges.first().unwrap(); + let last_record = connection.edges.last().unwrap(); + assert_eq!(connection.total_count, 5); + assert_eq!(first_record.node.type_u256, "0x4"); + assert_eq!(last_record.node.type_u256, "0x0"); + + // where filter on true bool + // TODO: use bool values on input instead of 0 or 1 + let records = records_model_query(&schema, "(where: { type_bool: 1 })").await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let first_record = connection.edges.first().unwrap(); + assert_eq!(connection.total_count, 5); + assert!(first_record.node.type_bool, "should be true"); + + // where filter on false bool + let records = records_model_query(&schema, "(where: { type_bool: 0 })").await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let first_record = connection.edges.first().unwrap(); + assert_eq!(connection.total_count, 5); + assert!(!first_record.node.type_bool, "should be false"); + + // *** ORDER TESTING *** + + // order on random u8 DESC (number) + let records = + records_model_query(&schema, "(order: { field: RANDOM_U8, direction: DESC })").await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let first_record = connection.edges.first().unwrap(); + let last_record = connection.edges.last().unwrap(); + assert_eq!(connection.total_count, 10); + assert!(first_record.node.random_u8 >= last_record.node.random_u8); + + // order on random u128 ASC (string) + let records = + records_model_query(&schema, "(order: { field: RANDOM_U128, direction: ASC })").await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let first_record = connection.edges.first().unwrap(); + let last_record = connection.edges.last().unwrap(); + assert_eq!(connection.total_count, 10); + assert!(first_record.node.random_u128 <= last_record.node.random_u128); + + // *** ORDER + WHERE FILTER TESTING *** + + // order + where filter on felt DESC + let records = records_model_query( + &schema, + &format!( + "(where: {{ type_feltGTE: \"{}\" }}, order: {{ field: TYPE_FELT, direction: DESC \ + }})", + felt_str_0x5 ), - ( - r#"where: { player: "0x0000000000000000000000000000000000000000000000000000000000000002" }"#, - 1, - ), // player is a key - ]); - - for (filter, expected_total) in where_filters { - let query = format!( - r#" - {{ - positionModels ({}) {{ - total_count - edges {{ - node {{ - __typename - vec {{ - x - y - }} - }} - cursor - }} - }} - }} - "#, - filter - ); - - let value = run_graphql_query(&pool, &query).await; - let positions = value.get("positionModels").ok_or("no positions found").unwrap(); - let connection: Connection = - serde_json::from_value(positions.clone()).unwrap(); - assert_eq!(connection.total_count, expected_total); - } - } + ) + .await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let first_record = connection.edges.first().unwrap(); + let last_record = connection.edges.last().unwrap(); + assert_eq!(connection.total_count, 5); + assert!(first_record.node.type_felt > last_record.node.type_felt); - #[ignore] - #[sqlx::test(migrations = "../migrations")] - // Todo: reenable OrderTest struct(test_order field) - // Todo: Refactor fn fetch_multiple_rows(), external_field - async fn test_model_ordering(pool: SqlitePool) { - let mut db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); - - entity_fixtures(&mut db).await; - - let orders: Vec = vec![ - OrderTest { - direction: "ASC", - field: "X", //"PLAYER" - test_order: Box::new(|edges: &Vec>| { - edges[0].node.vec.x < edges[1].node.vec.x - }), - }, - OrderTest { - direction: "DESC", - field: "X", - test_order: Box::new(|edges: &Vec>| { - edges[0].node.vec.x > edges[1].node.vec.x - }), - }, - OrderTest { - direction: "ASC", - field: "Y", //"PLAYER" - test_order: Box::new(|edges: &Vec>| { - edges[0].node.vec.y < edges[1].node.vec.y - }), - }, - OrderTest { - direction: "DESC", - field: "Y", - test_order: Box::new(|edges: &Vec>| { - edges[0].node.vec.y > edges[1].node.vec.y - }), - }, - ]; - - for order in orders { - let query = format!( - r#" - {{ - positionModels (order: {{ direction: {}, field: {} }}) {{ - total_count - edges {{ - node {{ - __typename - vec {{ - x - y - }} - }} - cursor - }} - }} - }} - "#, - order.direction, order.field - ); - - let value = run_graphql_query(&pool, &query).await; - let positions = value.get("positionModels").ok_or("no positions found").unwrap(); - let connection: Connection = - serde_json::from_value(positions.clone()).unwrap(); - assert_eq!(connection.total_count, 2); - assert!((order.test_order)(&connection.edges)); - } - } + // *** WHERE FILTER + PAGINATION TESTING *** + + let records = records_model_query(&schema, "(where: { type_u8GTE: 5 })").await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let one = connection.edges.get(0).unwrap(); + let two = connection.edges.get(1).unwrap(); + let three = connection.edges.get(2).unwrap(); + let four = connection.edges.get(3).unwrap(); + let five = connection.edges.get(4).unwrap(); + + // cursor based pagination + let records = records_model_query(&schema, "(where: { type_u8GTE: 5 }, first: 2)").await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let first_record = connection.edges.first().unwrap(); + let last_record = connection.edges.last().unwrap(); + assert_eq!(connection.total_count, 5); + assert_eq!(connection.edges.len(), 2); + assert_eq!(first_record, one); + assert_eq!(last_record, two); + + let records = records_model_query( + &schema, + &format!("(where: {{ type_u8GTE: 5 }}, first: 3, after: \"{}\")", last_record.cursor), + ) + .await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let first_record = connection.edges.first().unwrap(); + let second_record = connection.edges.last().unwrap(); + assert_eq!(connection.total_count, 5); + assert_eq!(connection.edges.len(), 3); + assert_eq!(first_record, three); + assert_eq!(second_record, five); + + // offset/limit base pagination + let records = + records_model_query(&schema, "(where: { type_u8GTE: 5 }, limit: 2, offset: 2)").await; + let connection: Connection = serde_json::from_value(records).unwrap(); + let first_record = connection.edges.first().unwrap(); + let last_record = connection.edges.last().unwrap(); + assert_eq!(connection.total_count, 5); + assert_eq!(connection.edges.len(), 2); + assert_eq!(first_record, three); + assert_eq!(last_record, four); - //#[ignore] - #[sqlx::test(migrations = "../migrations")] - async fn test_model_entity_relationship(pool: SqlitePool) { - let mut db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); - - entity_fixtures(&mut db).await; - - // Todo: Add `keys` field on `entity` type - // fixme: `keys` field return a single string, but test expects vec of strings - let query = r#" - { - positionModels { - total_count - edges { - node { - __typename - vec { - x - y - } - entity { - model_names - } - } - cursor - } - } - } - "#; - let value = run_graphql_query(&pool, query).await; - - let positions = value.get("positionModels").ok_or("no positions found").unwrap(); - let connection: Connection = serde_json::from_value(positions.clone()).unwrap(); - let entity = connection.edges[0].node.entity.as_ref().unwrap(); - assert_eq!(entity.model_names, "Moves,Position".to_string()); + Ok(()) } } diff --git a/crates/torii/graphql/src/tests/types-test/src/systems.cairo b/crates/torii/graphql/src/tests/types-test/src/contracts.cairo similarity index 98% rename from crates/torii/graphql/src/tests/types-test/src/systems.cairo rename to crates/torii/graphql/src/tests/types-test/src/contracts.cairo index eb622f4625..80ca1e3233 100644 --- a/crates/torii/graphql/src/tests/types-test/src/systems.cairo +++ b/crates/torii/graphql/src/tests/types-test/src/contracts.cairo @@ -59,7 +59,7 @@ mod records { type_u32: record_idx.into(), type_u64: record_idx.into(), type_u128: record_idx.into(), - //type_u256: type_felt.into(), + type_u256: type_felt.into(), type_bool: if record_idx % 2 == 0 { true } else { diff --git a/crates/torii/graphql/src/tests/types-test/src/lib.cairo b/crates/torii/graphql/src/tests/types-test/src/lib.cairo index 81e6b1dd08..0ff5325eaa 100644 --- a/crates/torii/graphql/src/tests/types-test/src/lib.cairo +++ b/crates/torii/graphql/src/tests/types-test/src/lib.cairo @@ -1,5 +1,5 @@ mod models; -mod systems; +mod contracts; fn seed() -> felt252 { diff --git a/crates/torii/graphql/src/tests/types-test/src/models.cairo b/crates/torii/graphql/src/tests/types-test/src/models.cairo index 241d95fa6a..bbdf513fcc 100644 --- a/crates/torii/graphql/src/tests/types-test/src/models.cairo +++ b/crates/torii/graphql/src/tests/types-test/src/models.cairo @@ -10,7 +10,7 @@ struct Record { type_u32: u32, type_u64: u64, type_u128: u128, - //type_u256: u256, + type_u256: u256, type_bool: bool, type_felt: felt252, type_class_hash: ClassHash,