Skip to content
This repository was archived by the owner on Apr 3, 2019. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/api/config/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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 https://mozilla.org/MPL/2.0/.

//! Route handler
//! for the `POST /config` endpoint.

#[cfg(test)]
mod test;

use rocket::{
data::{self, FromData},
http::Status,
Data, Request, State,
};
use rocket_contrib::Json;

use db::config::{Config as ConfigDb, Data as ConfigData};
use logging::MozlogLogger;
use types::error::{AppError, AppErrorKind, AppResult};

/// Payload for `POST /config`.
#[derive(Debug, Deserialize)]
struct Payload {
/// Flag indicating whether this payload clobbers existing config or should be merged with it.
clobber: Option<bool>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be a little clearer if it was called replace and noted that default is to merge configs.


/// The configuration data.
config: ConfigData,
}

impl FromData for Payload {
type Error = AppError;

fn from_data(request: &Request, data: Data) -> data::Outcome<Self, Self::Error> {
Json::<Payload>::from_data(request, data)
.map_failure(|(_status, error)| {
(
Status::BadRequest,
AppErrorKind::InvalidPayload(error.to_string()).into(),
)
})
.map(|json| json.into_inner())
}
}

#[post("/config", format = "application/json", data = "<payload>")]
fn handler(
payload: AppResult<Payload>,
config_db: State<ConfigDb>,
// TODO: do we need the logger?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it isn't too hard, I think it might be useful.

_logger: State<MozlogLogger>,
) -> AppResult<Json> {
let mut payload = payload?;
if payload.clobber == Some(true) {
config_db.set("fxa", &payload.config)?;
} else {
config_db.merge("fxa", &mut payload.config)?;
};
Ok(Json(json!({})))
}
114 changes: 114 additions & 0 deletions src/api/config/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// 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 https://mozilla.org/MPL/2.0/.

use rocket::{
http::{ContentType, Status},
local::Client,
};

use db::config::Config;
use logging::MozlogLogger;
use settings::Settings;
use types::error::{AppError, AppErrorKind};

fn setup() -> Client {
let mut settings = Settings::new().unwrap();
settings.provider.forcedefault = false;
let config = Config::new(&settings);
let logger = MozlogLogger::new(&settings);
let server = rocket::ignite()
.manage(settings)
.manage(config)
.manage(logger)
.mount("/", routes![super::handler]);

Client::new(server).unwrap()
}

#[test]
fn request_with_all_the_things() {
let client = setup();

let mut response = client
.post("/config")
.header(ContentType::JSON)
.body(
r#"{
"clobber": true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these values documented somewhere?

"config": {
"default_provider": "sendgrid",
"limits": {
"enabled": true,
"complaint": [
{ "period": "1 day", "limit": 0 },
{ "period": "1 year", "limit": 1 }
],
"hard": [
{ "period": "2 days", "limit": 2 },
{ "period": "2 years", "limit": 10 }
],
"soft": [
{ "period": "3 minutes", "limit": 1 }
]
},
"queue": "https://sqs.us-east-1.amazonaws.com/1234567890/blee",
"rules": [
{ "percentage": 50, "precedence": -127, "provider": "sendgrid" },
{ "percentage": 100, "precedence": 0, "provider": "socketlabs", "regex": "^socketlabs@mozilla\\.com$" },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the highest precedence be 0? It does seem strange that the highest precedence would be negative in this case.

{ "percentage": 100, "precedence": 127, "provider": "ses" }
],
"sender": {
"name": "Firefox Accounts",
"address": "[email protected]"
}
}
}"#,
)
.dispatch();

assert_eq!(response.status(), Status::Ok);

let body = response.body().unwrap().into_string().unwrap();
assert_eq!(body, json!({}).to_string());
}

#[test]
fn request_without_optional_fields() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can add more tests for clobber.

let client = setup();

let mut response = client
.post("/config")
.header(ContentType::JSON)
.body(
r#"{
"config": {}
}"#,
)
.dispatch();

assert_eq!(response.status(), Status::Ok);

let body = response.body().unwrap().into_string().unwrap();
assert_eq!(body, json!({}).to_string());
}

#[test]
fn empty_request() {
let client = setup();

let mut response = client
.post("/config")
.header(ContentType::JSON)
.body("{}")
.dispatch();

assert_eq!(response.status(), Status::BadRequest);

let body = response.body().unwrap().into_string().unwrap();
let error: AppError =
AppErrorKind::InvalidPayload(String::from("missing field `config` at line 1 column 2"))
.into();
let expected = serde_json::to_string(&error).unwrap();
assert_eq!(body, expected);
}
1 change: 1 addition & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
//! for modules pertaining to
//! the API layer.

pub mod config;
pub mod healthcheck;
pub mod send;
125 changes: 125 additions & 0 deletions src/db/config/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// 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 https://mozilla.org/MPL/2.0/.

//! Storage for configuration data.

use super::core::{Client as DbClient, DataType, Merge};
use settings::{DeliveryProblemLimits, Sender, Settings, SqsUrl};
use types::{error::AppResult, provider::Provider, regex::SerializableRegex};

#[cfg(test)]
mod test;

/// Configuration data store.
///
/// Data is keyed by client id.
#[derive(Debug)]
pub struct Config {
client: DbClient,
}

impl Config {
/// Instantiate a storage client.
pub fn new(settings: &Settings) -> Self {
Self {
client: DbClient::new(settings),
}
}

/// Read configuration data.
pub fn get(&self, client_id: &str) -> AppResult<Option<Data>> {
self.client.get(client_id, DataType::Configuration)
}

/// Store configuration data.
///
/// Any data previously stored for the client id
/// will be replaced.
pub fn set(&self, client_id: &str, config: &Data) -> AppResult<()> {
self.client.set(client_id, config, DataType::Configuration)
}

/// Merge configuration data with existing.
pub fn merge(&self, client_id: &str, config: &mut Data) -> AppResult<()> {
self.client
.merge(client_id, config, DataType::Configuration)
}
}

/// Configuration data.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Data {
/// The fallback provider if no rules match and it's not specified in the call to `/send`.
default_provider: Option<Provider>,

/// Thresholds for bounce and complaint reports.
limits: Option<DeliveryProblemLimits>,

/// The queue for outgoing bounce, complaint and delivery notifications.
queue: Option<SqsUrl>,

/// Rules for selectively diverting subsets of email traffic via different providers.
rules: Option<Vec<ProviderRules>>,

/// The name and email address to use in the `From` and `Sender` headers.
sender: Option<Sender>,
}

impl Merge<Data> for Data {
fn merge(&self, with: &Self) -> Self {
Self {
default_provider: self
.default_provider
.as_ref()
.map(|default_provider| default_provider.clone())
.or_else(|| with.default_provider.clone()),
limits: self
.limits
.as_ref()
.map(|limits| limits.clone())
.or_else(|| with.limits.clone()),
queue: self
.queue
.as_ref()
.map(|queue| queue.clone())
.or_else(|| with.queue.clone()),
rules: self
.rules
.as_ref()
.map(|rules| {
with.rules.as_ref().map_or_else(
|| rules.clone(),
|with_rules| {
let mut with_rules = with_rules.clone();
with_rules.extend(rules.clone());
with_rules
},
)
})
.or_else(|| with.rules.clone()),
sender: self
.sender
.as_ref()
.map(|sender| sender.clone())
.or_else(|| with.sender.clone()),
}
}
}

/// Rules for selectively diverting subsets of email traffic via different providers.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ProviderRules {
/// The percentage of prospective addresses to divert via this provider.
percentage: Option<u8>,

/// Relative precedence of this ruleset against its peers.
/// Default is `0` (lower number equals higher precedence).
precedence: Option<i8>,

/// The email provider for this ruleset.
provider: Provider,

/// Filter for matching email addresses.
regex: Option<SerializableRegex>,
}
66 changes: 66 additions & 0 deletions src/db/config/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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 https://mozilla.org/MPL/2.0/.

use std::time::SystemTime;

use regex::Regex;

use super::*;
use db::core::test::TestFixture;
use settings::SenderName;

#[test]
fn set() {
let settings = Settings::new().unwrap();
let config = Config::new(&settings);
let key = create_key("set");
let test = TestFixture::setup(&settings, &key, DataType::Configuration);
let expected = Data {
default_provider: Some(Provider::SocketLabs),
limits: Some(DeliveryProblemLimits {
enabled: true,
complaint: vec![],
hard: vec![],
soft: vec![],
}),
queue: Some(SqsUrl(
"https://sqs.us-east-1.amazonaws.com/1234567890/wibble".to_owned(),
)),
rules: Some(vec![ProviderRules {
percentage: Some(50),
precedence: Some(-127),
provider: Provider::Sendgrid,
regex: Some(SerializableRegex(Regex::new("wibble").unwrap())),
}]),
sender: Some(Sender {
name: SenderName("Firefox Accounts".to_owned()),
address: "[email protected]".parse().unwrap(),
}),
};

if let Err(error) = config.set(&key, &expected) {
assert!(false, format!("{:?}", error));
} else {
test.assert_data(&expected);
}

let data = config.get(&key).unwrap();
assert_eq!(data, Some(expected));
}

#[test]
fn merge() {
// TODO
}

fn create_key(test: &str) -> String {
format!("fxa-email-service.test.config.{}.{}", test, now())
}

fn now() -> u64 {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("system time error");
now.as_secs() * 1000 + u64::from(now.subsec_millis())
}
Loading