From b8a4ed3b9ba0c0090ff09f404f3a5ff440555300 Mon Sep 17 00:00:00 2001 From: Jake Champion Date: Thu, 30 Mar 2023 11:44:38 +0100 Subject: [PATCH] feat: allow config-stores to be defined using `[local_server.config_stores]` --- cli/tests/integration/config_store_lookup.rs | 80 +++++ lib/src/config.rs | 1 + lib/src/config/unit_tests.rs | 327 ++++++++++++++++++- test-fixtures/data/json-config_store.json | 4 + 4 files changed, 406 insertions(+), 6 deletions(-) create mode 100644 cli/tests/integration/config_store_lookup.rs create mode 100644 test-fixtures/data/json-config_store.json diff --git a/cli/tests/integration/config_store_lookup.rs b/cli/tests/integration/config_store_lookup.rs new file mode 100644 index 00000000..24c6fb90 --- /dev/null +++ b/cli/tests/integration/config_store_lookup.rs @@ -0,0 +1,80 @@ +use crate::common::{Test, TestResult}; +use hyper::{body::to_bytes, StatusCode}; + +#[tokio::test(flavor = "multi_thread")] +async fn json_config_store_lookup_works() -> TestResult { + const FASTLY_TOML: &str = r#" + name = "json-config_store-lookup" + description = "json config_store lookup test" + authors = ["Jill Bryson ", "Rose McDowall "] + language = "rust" + [local_server] + [local_server.config_stores] + [local_server.config_stores.animals] + file = "../test-fixtures/data/json-config_store.json" + format = "json" + "#; + + let resp = Test::using_fixture("config_store-lookup.wasm") + .using_fastly_toml(FASTLY_TOML)? + .against_empty() + .await; + + assert_eq!(resp.status(), StatusCode::OK); + assert!(to_bytes(resp.into_body()) + .await + .expect("can read body") + .to_vec() + .is_empty()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn inline_toml_config_store_lookup_works() -> TestResult { + const FASTLY_TOML: &str = r#" + name = "inline-toml-config_store-lookup" + description = "inline toml config_store lookup test" + authors = ["Jill Bryson ", "Rose McDowall "] + language = "rust" + [local_server] + [local_server.config_stores] + [local_server.config_stores.animals] + format = "inline-toml" + [local_server.config_stores.animals.contents] + dog = "woof" + cat = "meow" + "#; + + let resp = Test::using_fixture("config_store-lookup.wasm") + .using_fastly_toml(FASTLY_TOML)? + .against_empty() + .await; + + assert_eq!(resp.status(), StatusCode::OK); + assert!(to_bytes(resp.into_body()) + .await + .expect("can read body") + .to_vec() + .is_empty()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn missing_config_store_works() -> TestResult { + const FASTLY_TOML: &str = r#" + name = "missing-config_store-config" + description = "missing config_store test" + language = "rust" + "#; + + let resp = Test::using_fixture("config_store-lookup.wasm") + .using_fastly_toml(FASTLY_TOML)? + .against_empty() + .await; + + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + + Ok(()) +} diff --git a/lib/src/config.rs b/lib/src/config.rs index 6d759693..9b9ec8c7 100644 --- a/lib/src/config.rs +++ b/lib/src/config.rs @@ -202,6 +202,7 @@ pub enum ExperimentalModule { struct RawLocalServerConfig { backends: Option, geolocation: Option
, + #[serde(alias = "config_stores")] dictionaries: Option
, #[serde(alias = "object_store")] object_stores: Option
, diff --git a/lib/src/config/unit_tests.rs b/lib/src/config/unit_tests.rs index df9c8d6d..d4cd1573 100644 --- a/lib/src/config/unit_tests.rs +++ b/lib/src/config/unit_tests.rs @@ -138,6 +138,45 @@ fn fastly_toml_files_with_simple_dictionary_configurations_can_be_read() { assert!(dictionary.is_json()); } +/// Show that we can successfully parse a `fastly.toml` with local_server.config_stores configurations. +/// +/// This provides an example `fastly.toml` file including a `#[local_server.config_stores]` section. +#[test] +fn fastly_toml_files_with_simple_config_store_configurations_can_be_read() { + let dir = tempdir().unwrap(); + + let file_path = dir.path().join("a.json"); + let mut file = File::create(&file_path).unwrap(); + writeln!(file, "{{}}").unwrap(); + let config = FastlyConfig::from_str(format!( + r#" + manifest_version = "1.2.3" + name = "dictionary-config-example" + description = "a toml example with config store configuration" + authors = [ + "Amelia Watson ", + "Inugami Korone ", + ] + language = "rust" + + [local_server] + [local_server.config_stores] + [local_server.config_stores.a] + file='{}' + format = "json" + "#, + &file_path.to_str().unwrap() + )) + .expect("can read toml data containing local dictionary configurations"); + + let dictionary = config + .dictionaries() + .get(&DictionaryName::new("a".to_string())) + .expect("dictionary configurations can be accessed"); + assert_eq!(dictionary.file_path().unwrap(), file_path); + assert!(dictionary.is_json()); +} + /// Check that the `local_server` section can be deserialized. // This case is technically redundant, but it is nice to have a unit test that demonstrates the // happy path for this group of unit tests. @@ -307,7 +346,7 @@ mod backend_config_tests { } } -/// Unit tests for dictionaries in the `local_server` section of a `fastly.toml` package manifest. +/// Unit tests for dictionaries/config_stores in the `local_server` section of a `fastly.toml` package manifest. /// /// These tests check that we deserialize and validate the dictionary configurations section of /// the TOML data properly regardless of the format. @@ -334,12 +373,30 @@ mod dictionary_config_tests { res => panic!("unexpected result: {:?}", res), } } + + /// Check that config_store definitions have a valid `format`. + #[test] + fn config_store_configs_have_a_valid_format() { + use DictionaryConfigError::InvalidDictionaryFormat; + let invalid_format_field = r#" + [config_stores.a] + format = "foo" + contents = { apple = "fruit", potato = "vegetable" } + "#; + match read_local_server_config(&invalid_format_field) { + Err(InvalidDictionaryDefinition { + err: InvalidDictionaryFormat(format), + .. + }) if format == "foo" => {} + res => panic!("unexpected result: {:?}", res), + } + } } -/// Unit tests for dictionaries in the `local_server` section of a `fastly.toml` package manifest. +/// Unit tests for dictionaries/config-stores in the `local_server` section of a `fastly.toml` package manifest. /// -/// These tests check that we deserialize and validate the dictionary configurations section of -/// the TOML data properly for dictionaries using JSON files to store their data. +/// These tests check that we deserialize and validate the dictionary/config-store configurations section of +/// the TOML data properly for dictionaries/config-stores using JSON files to store their data. mod json_dictionary_config_tests { use { super::read_local_server_config, @@ -365,6 +422,23 @@ mod json_dictionary_config_tests { } } + /// Check that config_store definitions must be given as TOML tables. + #[test] + fn config_store_configs_must_use_toml_tables() { + use DictionaryConfigError::InvalidEntryType; + static BAD_DEF: &str = r#" + [config_stores] + "thing" = "stuff" + "#; + match read_local_server_config(BAD_DEF) { + Err(InvalidDictionaryDefinition { + err: InvalidEntryType, + .. + }) => {} + res => panic!("unexpected result: {:?}", res), + } + } + /// Check that dictionary definitions cannot contain unrecognized keys. #[test] fn dictionary_configs_cannot_contain_unrecognized_keys() { @@ -390,6 +464,31 @@ mod json_dictionary_config_tests { } } + /// Check that config_store definitions cannot contain unrecognized keys. + #[test] + fn config_store_configs_cannot_contain_unrecognized_keys() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("secrets.json"); + let mut file = File::create(&file_path).unwrap(); + writeln!(file, "{{}}").unwrap(); + + use DictionaryConfigError::UnrecognizedKey; + let bad_default = format!( + r#" + [config_stores] + thing = {{ file = '{}', format = "json", shrimp = true }} + "#, + file_path.to_str().unwrap() + ); + match read_local_server_config(&bad_default) { + Err(InvalidDictionaryDefinition { + err: UnrecognizedKey(key), + .. + }) if key == "shrimp" => {} + res => panic!("unexpected result: {:?}", res), + } + } + /// Check that dictionary definitions *must* include a `file` field. #[test] fn dictionary_configs_must_provide_a_file() { @@ -406,6 +505,22 @@ mod json_dictionary_config_tests { } } + /// Check that config_store definitions *must* include a `file` field. + #[test] + fn config_store_configs_must_provide_a_file() { + use DictionaryConfigError::MissingFile; + static NO_FILE: &str = r#" + [config_stores] + thing = {format = "json"} + "#; + match read_local_server_config(NO_FILE) { + Err(InvalidDictionaryDefinition { + err: MissingFile, .. + }) => {} + res => panic!("unexpected result: {:?}", res), + } + } + /// Check that dictionary definitions *must* include a `format` field. #[test] fn dictionary_configs_must_provide_a_format() { @@ -430,6 +545,30 @@ mod json_dictionary_config_tests { } } + /// Check that config_store definitions *must* include a `format` field. + #[test] + fn config_store_configs_must_provide_a_format() { + use DictionaryConfigError::MissingFormat; + let dir = tempdir().unwrap(); + let file_path = dir.path().join("secrets.json"); + let mut file = File::create(&file_path).unwrap(); + writeln!(file, "{{}}").unwrap(); + + let no_format_field = format!( + r#" + [config_stores] + "thing" = {{ file = '{}' }} + "#, + file_path.to_str().unwrap() + ); + match read_local_server_config(&no_format_field) { + Err(InvalidDictionaryDefinition { + err: MissingFormat, .. + }) => {} + res => panic!("unexpected result: {:?}", res), + } + } + /// Check that dictionary definitions must include a *valid* `name` field. #[test] fn dictionary_configs_must_provide_a_valid_name() { @@ -455,6 +594,31 @@ mod json_dictionary_config_tests { } } + /// Check that config_store definitions must include a *valid* `name` field. + #[test] + fn config_store_configs_must_provide_a_valid_name() { + use DictionaryConfigError::InvalidName; + let dir = tempdir().unwrap(); + let file_path = dir.path().join("secrets.json"); + let mut file = File::create(&file_path).unwrap(); + writeln!(file, "{{}}").unwrap(); + + let bad_name_field = format!( + r#" + [config_stores] + "1" = {{ file = '{}', format = "json" }} + "#, + file_path.to_str().unwrap() + ); + match read_local_server_config(&bad_name_field) { + Err(InvalidDictionaryDefinition { + err: InvalidName(_), + .. + }) => {} + res => panic!("unexpected result: {:?}", res), + } + } + /// Check that file field is a string. #[test] fn dictionary_configs_must_provide_file_as_a_string() { @@ -472,6 +636,23 @@ mod json_dictionary_config_tests { } } + /// Check that file field is a string. + #[test] + fn config_store_configs_must_provide_file_as_a_string() { + use DictionaryConfigError::InvalidFileEntry; + static BAD_FILE_FIELD: &str = r#" + [config_stores] + "thing" = { file = 3, format = "json" } + "#; + match read_local_server_config(BAD_FILE_FIELD) { + Err(InvalidDictionaryDefinition { + err: InvalidFileEntry, + .. + }) => {} + res => panic!("unexpected result: {:?}", res), + } + } + /// Check that file field is non empty. #[test] fn dictionary_configs_must_provide_a_non_empty_file() { @@ -489,6 +670,23 @@ mod json_dictionary_config_tests { } } + /// Check that file field is non empty. + #[test] + fn config_store_configs_must_provide_a_non_empty_file() { + use DictionaryConfigError::EmptyFileEntry; + static EMPTY_FILE_FIELD: &str = r#" + [config_stores] + "thing" = { file = "", format = "json" } + "#; + match read_local_server_config(EMPTY_FILE_FIELD) { + Err(InvalidDictionaryDefinition { + err: EmptyFileEntry, + .. + }) => {} + res => panic!("unexpected result: {:?}", res), + } + } + /// Check that format field is a string. #[test] fn dictionary_configs_must_provide_format_as_a_string() { @@ -506,6 +704,23 @@ mod json_dictionary_config_tests { } } + /// Check that format field is a string. + #[test] + fn config_store_configs_must_provide_format_as_a_string() { + use DictionaryConfigError::InvalidFormatEntry; + static BAD_FORMAT_FIELD: &str = r#" + [config_stores] + "thing" = { format = 3} + "#; + match read_local_server_config(BAD_FORMAT_FIELD) { + Err(InvalidDictionaryDefinition { + err: InvalidFormatEntry, + .. + }) => {} + res => panic!("unexpected result: {:?}", res), + } + } + /// Check that format field is non empty. #[test] fn dictionary_configs_must_provide_a_non_empty_format() { @@ -523,6 +738,23 @@ mod json_dictionary_config_tests { } } + /// Check that format field is non empty. + #[test] + fn config_store_configs_must_provide_a_non_empty_format() { + use DictionaryConfigError::EmptyFormatEntry; + static EMPTY_FORMAT_FIELD: &str = r#" + [config_stores] + "thing" = { format = "" } + "#; + match read_local_server_config(EMPTY_FORMAT_FIELD) { + Err(InvalidDictionaryDefinition { + err: EmptyFormatEntry, + .. + }) => {} + res => panic!("unexpected result: {:?}", res), + } + } + /// Check that format field set to json is valid. #[test] fn valid_dictionary_config_with_format_set_to_json() { @@ -542,12 +774,32 @@ mod json_dictionary_config_tests { "can read toml data containing local dictionary configurations using json format", ); } + + /// Check that format field set to json is valid. + #[test] + fn valid_config_store_config_with_format_set_to_json() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("secrets.json"); + let mut file = File::create(&file_path).unwrap(); + writeln!(file, "{{}}").unwrap(); + + let dictionary = format!( + r#" + [config_stores] + "thing" = {{ file = '{}', format = "json" }} + "#, + file_path.to_str().unwrap() + ); + read_local_server_config(&dictionary).expect( + "can read toml data containing local dictionary configurations using json format", + ); + } } -/// Unit tests for dictionaries in the `local_server` section of a `fastly.toml` package manifest. +/// Unit tests for dictionaries/config_stores in the `local_server` section of a `fastly.toml` package manifest. /// /// These tests check that we deserialize and validate the dictionary configurations section of -/// the TOML data properly for dictionaries using inline TOML to store their data. +/// the TOML data properly for dictionaries/config_stores using inline TOML to store their data. mod inline_toml_dictionary_config_tests { use { super::read_local_server_config, @@ -566,6 +818,18 @@ mod inline_toml_dictionary_config_tests { ); } + #[test] + fn valid_inline_toml_config_stores_can_be_parsed() { + let dictionary = r#" + [config_stores.inline_toml_example] + format = "inline-toml" + contents = { apple = "fruit", potato = "vegetable" } + "#; + read_local_server_config(&dictionary).expect( + "can read toml data containing local dictionary configurations using json format", + ); + } + /// Check that dictionary definitions *must* include a `format` field. #[test] fn dictionary_configs_must_provide_a_format() { @@ -582,6 +846,22 @@ mod inline_toml_dictionary_config_tests { } } + /// Check that config_store definitions *must* include a `format` field. + #[test] + fn config_store_configs_must_provide_a_format() { + use DictionaryConfigError::MissingFormat; + let no_format_field = r#" + [config_stores.missing_format] + contents = { apple = "fruit", potato = "vegetable" } + "#; + match read_local_server_config(&no_format_field) { + Err(InvalidDictionaryDefinition { + err: MissingFormat, .. + }) => {} + res => panic!("unexpected result: {:?}", res), + } + } + /// Check that dictionary definitions *must* include a `contents` field. #[test] fn dictionary_configs_must_provide_contents() { @@ -599,6 +879,23 @@ mod inline_toml_dictionary_config_tests { } } + /// Check that config_store definitions *must* include a `contents` field. + #[test] + fn config_store_configs_must_provide_contents() { + use DictionaryConfigError::MissingContents; + let missing_contents = r#" + [config_stores.missing_contents] + format = "inline-toml" + "#; + match read_local_server_config(&missing_contents) { + Err(InvalidDictionaryDefinition { + err: MissingContents, + .. + }) => {} + res => panic!("unexpected result: {:?}", res), + } + } + /// Check that dictionary definitions must include a *valid* `name` field. #[test] fn dictionary_configs_must_provide_a_valid_name() { @@ -616,6 +913,24 @@ mod inline_toml_dictionary_config_tests { res => panic!("unexpected result: {:?}", res), } } + + /// Check that config_store definitions must include a *valid* `name` field. + #[test] + fn config_store_configs_must_provide_a_valid_name() { + use DictionaryConfigError::InvalidName; + let bad_name_field = r#" + [config_stores."1"] + format = "inline-toml" + contents = { apple = "fruit", potato = "vegetable" } + "#; + match read_local_server_config(&bad_name_field) { + Err(InvalidDictionaryDefinition { + err: InvalidName(_), + .. + }) => {} + res => panic!("unexpected result: {:?}", res), + } + } } /// Unit tests for Geolocation mapping in the `local_server` section of a `fastly.toml` package manifest. diff --git a/test-fixtures/data/json-config_store.json b/test-fixtures/data/json-config_store.json new file mode 100644 index 00000000..430d0dc7 --- /dev/null +++ b/test-fixtures/data/json-config_store.json @@ -0,0 +1,4 @@ +{ + "cat": "meow", + "dog": "woof" +}