From 2207650b201aa4b9869925387d0d27841a3cf8ed Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 28 Jun 2024 19:55:45 +0100 Subject: [PATCH 1/3] Implement types for autoconfig, and deserialize using serde --- tools/validation/Cargo.lock | 166 ++++++++++++++++++ tools/validation/Cargo.toml | 3 + tools/validation/README.md | 6 + .../validation/autoconfig_validate/Cargo.toml | 12 ++ .../resources/test_data/valid_config.xml | 57 ++++++ .../autoconfig_validate/src/error.rs | 45 +++++ .../validation/autoconfig_validate/src/lib.rs | 50 ++++++ .../autoconfig_validate/src/types.rs | 134 ++++++++++++++ 8 files changed, 473 insertions(+) create mode 100644 tools/validation/Cargo.lock create mode 100644 tools/validation/Cargo.toml create mode 100644 tools/validation/README.md create mode 100644 tools/validation/autoconfig_validate/Cargo.toml create mode 100644 tools/validation/autoconfig_validate/resources/test_data/valid_config.xml create mode 100644 tools/validation/autoconfig_validate/src/error.rs create mode 100644 tools/validation/autoconfig_validate/src/lib.rs create mode 100644 tools/validation/autoconfig_validate/src/types.rs diff --git a/tools/validation/Cargo.lock b/tools/validation/Cargo.lock new file mode 100644 index 0000000..6dd8ef3 --- /dev/null +++ b/tools/validation/Cargo.lock @@ -0,0 +1,166 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "autoconfig_validate" +version = "0.1.0" +dependencies = [ + "anyhow", + "quick-xml", + "regex", + "serde", + "serde_path_to_error", + "thiserror", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/tools/validation/Cargo.toml b/tools/validation/Cargo.toml new file mode 100644 index 0000000..7539b83 --- /dev/null +++ b/tools/validation/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "2" +members = ["autoconfig_validate"] \ No newline at end of file diff --git a/tools/validation/README.md b/tools/validation/README.md new file mode 100644 index 0000000..82d8af3 --- /dev/null +++ b/tools/validation/README.md @@ -0,0 +1,6 @@ +# Autoconfig validation + +Tools to use for reading and validating autoconfig configuration files. + +For more information on autoconfig files and the autoconfig format, +see https://github.com/thunderbird/autoconfig \ No newline at end of file diff --git a/tools/validation/autoconfig_validate/Cargo.toml b/tools/validation/autoconfig_validate/Cargo.toml new file mode 100644 index 0000000..92edde1 --- /dev/null +++ b/tools/validation/autoconfig_validate/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "autoconfig_validate" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.86" +quick-xml = { version = "0.34.0", features = ["serde", "serialize"] } +regex = "1.10.5" +serde = { version = "1.0.203", features = ["derive"] } +serde_path_to_error = "0.1.16" +thiserror = "1.0.61" diff --git a/tools/validation/autoconfig_validate/resources/test_data/valid_config.xml b/tools/validation/autoconfig_validate/resources/test_data/valid_config.xml new file mode 100644 index 0000000..0c3d511 --- /dev/null +++ b/tools/validation/autoconfig_validate/resources/test_data/valid_config.xml @@ -0,0 +1,57 @@ + + + + + office365.com + onmicrosoft.com + + mail.protection.outlook.com + Microsoft 365 + Microsoft 365 + + outlook.office365.com + 993 + SSL + OAuth2 + %EMAILADDRESS% + + + outlook.office365.com + 995 + SSL + OAuth2 + %EMAILADDRESS% + + true + + + + outlook.office365.com + 443 + %EMAILADDRESS% + SSL + OAuth2 + https://outlook.office365.com/owa/ + https://outlook.office365.com/ews/exchange.asmx + true + + + smtp.office365.com + 587 + STARTTLS + OAuth2 + %EMAILADDRESS% + + + + + login.microsoftonline.com + https://outlook.office365.com/IMAP.AccessAsUser.All https://outlook.office365.com/POP.AccessAsUser.All + https://outlook.office365.com/SMTP.Send offline_access + + + https://login.microsoftonline.com/common/oauth2/v2.0/authorize + https://login.microsoftonline.com/common/oauth2/v2.0/token + + + diff --git a/tools/validation/autoconfig_validate/src/error.rs b/tools/validation/autoconfig_validate/src/error.rs new file mode 100644 index 0000000..5bfadb1 --- /dev/null +++ b/tools/validation/autoconfig_validate/src/error.rs @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::{env, io}; +use thiserror::Error; + +use crate::types::IncomingServerType; + +/// An error that arose while deserializing or validating the autoconfig file. +#[derive(Debug, Error)] +pub enum ValidationError { + #[error("failed to deserialize structure from XML: {0}")] + Deserialize(#[from] serde_path_to_error::Error), + + #[error("unsupported clientConfig version: {0}")] + UnsupportedVersion(String), + + #[error( + "found protocol-specific config for {found:?} on incoming server of type {server_type:?}" + )] + InvalidIncomingConfig { + server_type: IncomingServerType, + found: IncomingServerType, + }, + + #[error("invalid username template: {0}")] + InvalidUsernameTemplate(String), + + #[error("encountered an unexpected error: {0}")] + Unexpected(#[source] anyhow::Error), +} + +/// An error that arose in a test. +#[derive(Debug, Error)] +pub(crate) enum TestError { + #[error("failed to read variable from environment: {0}")] + EnvRead(#[from] env::VarError), + + #[error("failed to read file: {0}")] + Io(#[from] io::Error), + + #[error("validation error: {0}")] + Validation(#[from] ValidationError), +} diff --git a/tools/validation/autoconfig_validate/src/lib.rs b/tools/validation/autoconfig_validate/src/lib.rs new file mode 100644 index 0000000..c035ae8 --- /dev/null +++ b/tools/validation/autoconfig_validate/src/lib.rs @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod error; +pub mod types; +mod validate; + +pub use crate::validate::*; + +use crate::error::ValidationError; +use crate::types::ClientConfig; + +/// Deserializes the provided XML and returns the result as an instance of +/// `ClientConfig`. +pub fn parse_client_config(bytes: &[u8]) -> Result { + let de = &mut quick_xml::de::Deserializer::from_reader(bytes); + let cfg: ClientConfig = serde_path_to_error::deserialize(de)?; + Ok(cfg) +} + +#[cfg(test)] +mod tests { + // TODO: Test parsing an invalid file. + use super::*; + use crate::error::TestError; + use std::{env, fs}; + + /// Reads and returns the contents of the test data file with the provided + /// name. + /// + /// Test data files are located in `resources/test_data/` from the root of + /// the crate (not the workspace). + fn read_test_data(filename: &str) -> Result, TestError> { + let root = env::var("CARGO_MANIFEST_DIR")?; + let path = format!("{root}/resources/test_data/{filename}"); + + let content = fs::read(path)?; + Ok(content) + } + + /// Tests that a valid autoconfig file parses correctly. + #[test] + fn test_de() -> Result<(), TestError> { + let valid_sample = read_test_data("valid_config.xml")?; + parse_client_config(valid_sample.as_slice())?; + + Ok(()) + } +} diff --git a/tools/validation/autoconfig_validate/src/types.rs b/tools/validation/autoconfig_validate/src/types.rs new file mode 100644 index 0000000..7f28a28 --- /dev/null +++ b/tools/validation/autoconfig_validate/src/types.rs @@ -0,0 +1,134 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use serde::Deserialize; + +/// The top-level `clientConfig` element. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientConfig { + #[serde(rename = "@version")] + pub version: String, + + pub email_provider: EmailProvider, +} + +/// An email provider (the `emailProvider` element). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EmailProvider { + #[serde(rename = "@id")] + pub provider_id: String, + + pub display_name: String, + pub display_short_name: String, + + #[serde(rename = "domain")] + pub domains: Vec, + + #[serde(rename = "incomingServer")] + pub incoming_servers: Vec, + + #[serde(rename = "outgoingServer")] + pub outgoing_servers: Vec, +} + +/// An incoming server, represented with the `incomingServer` element in the +/// autoconfig file. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IncomingServer { + #[serde(rename = "@type")] + pub server_type: IncomingServerType, + + pub hostname: String, + pub port: u16, + pub socket_type: SocketType, + pub username: String, + pub authentication: AuthType, + pub use_global_preferred_server: Option, + + /// Additional configuration for servers of type `exchange`. + #[serde(rename = "ewsURL")] + pub ews_url: Option, + #[serde(rename = "owaURL")] + pub owa_url: Option, + + /// Additional configuration for servers of type `pop3`. + pub pop3: Option, +} + +/// The supported values for an incoming server's `type` attribute. +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum IncomingServerType { + IMAP, + POP3, + Exchange, +} + +/// The supported values for a server's `socketType` attribute. +#[derive(Debug, Deserialize)] +pub enum SocketType { + #[serde(rename = "plain")] + Plain, + SSL, + STARTTLS, +} + +/// The supported values for a server's `authentication` attribute. +#[derive(Debug, Deserialize)] +pub enum AuthType { + #[serde(rename = "password-cleartext")] + PasswordCleartext, + #[serde(rename = "password-encrypted")] + PasswordEncrypted, + NTLM, + GSSAPI, + #[serde(rename = "client-IP-address")] + ClientIPAddress, + #[serde(rename = "TLS-client-cert")] + TLSClientCert, + OAuth2, + None, +} + +/// Additional configuration for POP3 servers. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct POP3Config { + pub leave_messages_on_server: Option, + pub download_on_biff: Option, + pub check_interval: Option, +} + +/// A setting for POP3 servers to indicate how long to wait before checking for +/// new messages. +#[derive(Debug, Deserialize)] +pub struct POP3CheckInterval { + #[serde(rename = "@minutes")] + pub minutes: u32, +} + +/// An outgoing server, represented with the `outgoingServer` element in the +/// autoconfig file. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OutgoingServer { + #[serde(rename = "@type")] + pub server_type: OutgoingServerType, + + pub hostname: String, + pub port: u16, + pub socket_type: SocketType, + pub username: String, + pub authentication: AuthType, +} + +/// The supported values for an outgoing server's `type` attribute. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum OutgoingServerType { + SMTP, +} From 257f3e60718e6a804125b8c48e7b707b028cfc8e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 28 Jun 2024 19:56:16 +0100 Subject: [PATCH 2/3] Implement additional (offline) validation --- .../autoconfig_validate/src/validate.rs | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 tools/validation/autoconfig_validate/src/validate.rs diff --git a/tools/validation/autoconfig_validate/src/validate.rs b/tools/validation/autoconfig_validate/src/validate.rs new file mode 100644 index 0000000..c7a5095 --- /dev/null +++ b/tools/validation/autoconfig_validate/src/validate.rs @@ -0,0 +1,267 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::error::ValidationError; +use crate::types::{ClientConfig, IncomingServer, IncomingServerType}; +use regex::Regex; + +/// The values that are known and supported for templating in a server's +/// `username` element. +const SUPPORTED_USERNAME_TEMPLATE: [&str; 4] = [ + "%EMAILADDRESS%", + "%EMAILLOCALPART%", + "%EMAILDOMAIN%", + "%REALNAME%", +]; + +/// Performs a series of checks on the provided `ClientConfig`, such as ensuring +/// its version is supported, that a server's configuration is consistent with +/// its type, etc. +/// +/// This function only ensures the configuration *looks* valid, but does not +/// perform any connectivity check. +// +// TODO: Return multiple errors at once (without bailing on the first one), +// associating each with a path to the invalid element in the config structure. +pub fn validate(cfg: &ClientConfig) -> Result<(), ValidationError> { + // Ensure the config uses a supported version. + if cfg.version.as_str() != "1.1" { + return Err(ValidationError::UnsupportedVersion(cfg.version.clone())); + } + + // Validate the configurations for incoming servers. + cfg.email_provider + .incoming_servers + .iter() + .try_for_each(validate_incoming_server)?; + + // Validate the configuration for outgoing servers. This involve + cfg.email_provider + .outgoing_servers + .iter() + .try_for_each(|server| validate_username(&server.username)) +} + +/// Performs a series of checks on an individual [`IncomingServer`]. +/// +/// Ensures any optional protocol-specific configuration on the server matches +/// with the server's type, and that it isn't using any unsupported username +/// template placeholder. +fn validate_incoming_server(server: &IncomingServer) -> Result<(), ValidationError> { + // Ensure that Exchange-specific configuration is only set for Exchange + // servers. + if (server.ews_url.is_some() || server.owa_url.is_some()) + && server.server_type != IncomingServerType::Exchange + { + return Err(ValidationError::InvalidIncomingConfig { + server_type: server.server_type.clone(), + found: IncomingServerType::Exchange, + }); + } + + // Ensure that POP3-specific configuration is only set for POP3 servers. + if server.pop3.is_some() && server.server_type != IncomingServerType::POP3 { + return Err(ValidationError::InvalidIncomingConfig { + server_type: server.server_type.clone(), + found: IncomingServerType::POP3, + }); + } + + // Ensure that the username uses valid template placeholder(s), if any. + validate_username(&server.username)?; + + Ok(()) +} + +/// Ensures the given username uses only supported template placeholder(s), if +/// any. +/// +/// The same `username` element can include multiple placeholders, but they all +/// must be supported. +fn validate_username(username: &String) -> Result<(), ValidationError> { + let re = match Regex::new("%[^%]*%") { + Ok(re) => re, + Err(e) => return Err(ValidationError::Unexpected(e.into())), + }; + + for re_match in re.find_iter(username.as_str()) { + if !SUPPORTED_USERNAME_TEMPLATE.contains(&re_match.as_str()) { + return Err(ValidationError::InvalidUsernameTemplate( + re_match.as_str().into(), + )); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + /// Tests for the validation checks that are implemented in this module. + /// + /// TODO: validation of a full `ClientConfig` class that: + /// * is valid + /// * contains at least one invalid incoming server + /// * contains at least one invalid outgoing server (i.e. invalid username) + use super::*; + + use crate::error::ValidationError; + use crate::types::{AuthType, POP3Config, SocketType}; + + /// Instantiates a [`IncomingServer`] to use in tests. + /// + /// The instance generated by this method is valid as far as the validation + /// checks in this module are concerned. + fn generate_incoming_server() -> IncomingServer { + IncomingServer { + server_type: IncomingServerType::IMAP, + hostname: "test.invalid".to_string(), + port: 993, + socket_type: SocketType::SSL, + username: "%EMAILADDRESS%".to_string(), + authentication: AuthType::PasswordCleartext, + ews_url: None, + owa_url: None, + pop3: None, + use_global_preferred_server: None, + } + } + + /// Tests that a valid [`IncomingServer`] passes validation with no error. + #[test] + fn test_validate_incoming_server_valid() -> Result<(), ValidationError> { + let server = generate_incoming_server(); + validate_incoming_server(&server) + } + + /// Tests that an [`IncomingServer`] which type isn't POP3 and has a POP3 + /// configuration fails validation with the correct error. + #[test] + fn test_validate_incoming_server_unexpected_pop3() -> Result<(), ValidationError> { + let mut server = generate_incoming_server(); + server.pop3 = Some(POP3Config { + leave_messages_on_server: None, + download_on_biff: None, + check_interval: None, + }); + + let result = validate_incoming_server(&server); + + assert!(result.is_err(), "result should be an error"); + let err = result.err().unwrap(); + assert!( + matches!( + err, + ValidationError::InvalidIncomingConfig { + server_type: IncomingServerType::IMAP, + found: IncomingServerType::POP3, + }, + ), + "error should be a ValidationError::InvalidIncomingConfig, with found = POP3" + ); + + Ok(()) + } + + /// Tests that an [`IncomingServer`] which type isn't Exchange and has an + /// EWS URL correctly fails validation. + #[test] + fn test_validate_incoming_server_unexpected_exchange_ews() -> Result<(), ValidationError> { + let mut server = generate_incoming_server(); + server.ews_url = Some("test.invalid".to_string()); + + let result = validate_incoming_server(&server); + + assert!(result.is_err(), "result should be an error"); + let err = result.err().unwrap(); + assert!( + matches!( + err, + ValidationError::InvalidIncomingConfig { + server_type: IncomingServerType::IMAP, + found: IncomingServerType::Exchange, + } + ), + "error should be a ValidationError::InvalidIncomingConfig, with found = Exchange" + ); + + Ok(()) + } + + /// Tests that an [`IncomingServer`] which type isn't Exchange and has an + /// OWA URL correctly fails validation. + #[test] + fn test_validate_incoming_server_unexpected_exchange_owa() -> Result<(), ValidationError> { + let mut server = generate_incoming_server(); + server.owa_url = Some("test.invalid".to_string()); + + let result = validate_incoming_server(&server); + + assert!(result.is_err(), "result should be an error"); + let err = result.err().unwrap(); + assert!( + matches!( + err, + ValidationError::InvalidIncomingConfig { + server_type: IncomingServerType::IMAP, + found: IncomingServerType::Exchange, + } + ), + "error should be a ValidationError::InvalidIncomingConfig, with found = Exchange" + ); + + Ok(()) + } + + /// Tests that a username that contains no placeholder passes username + /// validation with no error. + #[test] + fn test_validate_username_no_placeholder() -> Result<(), ValidationError> { + validate_username(&"foo".into()) + } + + /// Tests that a username that contains a known placeholder passes username + /// validation with no error. + #[test] + fn test_validate_username_known_placeholder() -> Result<(), ValidationError> { + validate_username(&"%EMAILADDRESS%".into()) + } + + /// Tests that a username that contains an unknown placeholder correctly + /// fails username validation. + #[test] + fn test_validate_username_unknown_placeholder() -> Result<(), ValidationError> { + assert!( + matches!( + validate_username(&"%FOO%".to_string()), + Err(ValidationError::InvalidUsernameTemplate(..)) + ), + "username validation should return a ValidationError::InvalidUsernameTemplate error" + ); + + Ok(()) + } + + /// Tests that a username that contains multiple known placeholders passes + /// username validation with no error. + #[test] + fn test_validate_username_multiple_placeholder_valid() -> Result<(), ValidationError> { + validate_username(&"%EMAILLOCALPART%.%EMAILDOMAIN%".to_string()) + } + + /// Tests that a username that contains multiple placeholders with at least + /// one unknown correctly fails username validation + #[test] + fn test_validate_username_multiple_placeholder_one_unknown() -> Result<(), ValidationError> { + assert!( + matches!( + validate_username(&"%EMAILLOCALPART%.%FOO%".to_string()), + Err(ValidationError::InvalidUsernameTemplate(..)) + ), + "username validation should return a ValidationError::InvalidUsernameTemplate error" + ); + + Ok(()) + } +} From d330ee1848f1671bc0270fa60a320274601fc1be Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 28 Jun 2024 19:56:48 +0100 Subject: [PATCH 3/3] Add CI, and ignore build/dev artifacts --- .github/workflows/rust.yaml | 57 +++++++++++++++++++++++++++++++++++++ .gitignore | 2 ++ 2 files changed, 59 insertions(+) create mode 100644 .github/workflows/rust.yaml create mode 100644 .gitignore diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml new file mode 100644 index 0000000..bb18fd5 --- /dev/null +++ b/.github/workflows/rust.yaml @@ -0,0 +1,57 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + paths: + - tools/validation/** + schedule: + - cron: '30 2 * * *' + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + name: "Build & Test: ./tools/validation" + defaults: + run: + working-directory: ./tools/validation + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Check Formatting + run: cargo fmt --all -- --check + + - name: Cargo Cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build project + run: cargo build + + - name: Test project + run: cargo test --all + + - name: Run clippy + uses: giraffate/clippy-action@v1 + with: + reporter: 'github-pr-check' + clippy_flags: --no-deps + filter_mode: nofilter + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5bd05d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +.idea \ No newline at end of file