Logins: Validate and fixup hostname and formSubmitURL. (#2380)

This commit is contained in:
Mark Hammond 2019-12-18 15:33:55 +11:00 коммит произвёл GitHub
Родитель 094fd42d02
Коммит 0478aadc00
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 395 добавлений и 95 удалений

Просмотреть файл

@ -147,12 +147,11 @@ class DatabaseLoginsStorageTest : LoginsStorageTest() {
assert(!LoginsStoreMetrics.writeQueryErrorCount["invalid_record"].testHasValue())
try {
// N.B. this is invalid due to both `httpRealm` and `formSubmitURL`
// N.B. this is invalid due to `formSubmitURL` being an invalid url.
store.add(ServerPassword(
id = "bbbbbbbbbbbb",
hostname = "https://test.example.com",
httpRealm = "Something",
formSubmitURL = "https://www.example.com",
formSubmitURL = "not a url",
username = "Foobar2000",
password = "hunter2",
usernameField = "users_name",

Просмотреть файл

@ -365,12 +365,11 @@ abstract class LoginsStorageTest {
companion object {
val INVALID_RECORDS: List<ServerPassword> = listOf(
// Both formSubmitURL and httpRealm
// Invalid formSubmitURL
ServerPassword(
id = "",
hostname = "https://www.foo.org",
httpRealm = "Test Realm",
formSubmitURL = "https://www.foo.org/login",
formSubmitURL = "invalid\u0000value",
password = "MyPassword",
username = "MyUsername",
usernameField = "users_name",

Просмотреть файл

@ -1194,7 +1194,7 @@ mod tests {
let db = LoginDb::open_in_memory(Some("testing")).unwrap();
db.add(Login {
guid: "dummy_000001".into(),
form_submit_url: Some("https://www.example.com/submit".into()),
form_submit_url: Some("https://www.example.com".into()),
hostname: "https://www.example.com".into(),
http_realm: None,
username: "test".into(),
@ -1205,7 +1205,7 @@ mod tests {
let duplicate_login_check = db.check_valid_with_no_dupes(&Login {
guid: Guid::empty(),
form_submit_url: Some("https://www.example.com/submit".into()),
form_submit_url: Some("https://www.example.com".into()),
hostname: "https://www.example.com".into(),
http_realm: None,
username: "test".into(),
@ -1220,12 +1220,60 @@ mod tests {
)
}
#[test]
fn test_unicode_submit() {
let db = LoginDb::open_in_memory(Some("testing")).unwrap();
db.add(Login {
guid: "dummy_000001".into(),
form_submit_url: Some("http://😍.com".into()),
hostname: "http://😍.com".into(),
http_realm: None,
username: "😍".into(),
username_field: "😍".into(),
password: "😍".into(),
password_field: "😍".into(),
..Login::default()
})
.unwrap();
let fetched = db
.get_by_id("dummy_000001")
.expect("should work")
.expect("should get a record");
assert_eq!(fetched.hostname, "http://xn--r28h.com");
assert_eq!(fetched.form_submit_url.unwrap(), "http://xn--r28h.com");
assert_eq!(fetched.username, "😍");
assert_eq!(fetched.username_field, "😍");
assert_eq!(fetched.password, "😍");
assert_eq!(fetched.password_field, "😍");
}
#[test]
fn test_unicode_realm() {
let db = LoginDb::open_in_memory(Some("testing")).unwrap();
db.add(Login {
guid: "dummy_000001".into(),
form_submit_url: None,
hostname: "http://😍.com".into(),
http_realm: Some("😍😍".into()),
username: "😍".into(),
password: "😍".into(),
..Login::default()
})
.unwrap();
let fetched = db
.get_by_id("dummy_000001")
.expect("should work")
.expect("should get a record");
assert_eq!(fetched.hostname, "http://xn--r28h.com");
assert_eq!(fetched.http_realm.unwrap(), "😍😍");
}
#[test]
fn test_check_valid_with_no_dupes_with_unique_login() {
let db = LoginDb::open_in_memory(Some("testing")).unwrap();
db.add(Login {
guid: "dummy_000001".into(),
form_submit_url: Some("https://www.example.com/submit".into()),
form_submit_url: Some("https://www.example.com".into()),
hostname: "https://www.example.com".into(),
http_realm: None,
username: "test".into(),
@ -1306,10 +1354,6 @@ mod tests {
"https://sub.example.com:8080",
"https://sub.sub.example.com",
"ftp://sub.example.com",
// Handling file:// is questionable - it would also be fine to
// not handle it! It's left here more to document the fact that
// we do!
"file://example.com",
],
vec![
"https://badexample.com",
@ -1323,8 +1367,7 @@ mod tests {
// on insert.
check_good_bad(
vec![
"http://😍.com",
"http://xn--r28h.com", // punycoded version of the above.
"http://xn--r28h.com", // punycoded version of "http://😍.com"
],
vec!["http://💖.com"],
vec!["😍.com", "xn--r28h.com"],
@ -1345,11 +1388,7 @@ mod tests {
#[test]
fn test_get_by_base_domain_ipv6() {
check_good_bad(
vec![
"http://[::1]",
"https://[::1]:8000",
"https://[0:0:0:0:0:0:0:1]", // this is [::1]
],
vec!["http://[::1]", "https://[::1]:8000"],
vec!["https://[0:0:0:0:0:0:1:1]", "https://example.com"],
vec!["[::1]", "[0:0:0:0:0:0:0:1]"],
vec!["[0:0:0:0:0:0:1:2]"],

Просмотреть файл

@ -172,7 +172,7 @@ mod test {
let a = Login {
guid: "aaaaaaaaaaaa".into(),
hostname: "https://www.example.com".into(),
form_submit_url: Some("https://www.example.com/login".into()),
form_submit_url: Some("https://www.example.com".into()),
username: "coolperson21".into(),
password: "p4ssw0rd".into(),
username_field: "user_input".into(),

Просмотреть файл

@ -39,7 +39,8 @@
//! - "ftp://ftp.site.com"
//! - "moz-proxy://127.0.0.1:8888"
//! - "chrome://MyLegacyExtension"
//! - "file:///"
//! - "file://"
//! - "https://[::1]"
//!
//! If invalid data is received in this field (either from the application, or via sync)
//! then the logins store will attempt to coerce it into valid data by:
@ -47,33 +48,21 @@
//! - converting values with non-ascii characters into punycode
//!
//! **XXX TODO:**
//! - test that we validate as an origin.
//! - test that we fixup full URLs by turning them into origins?
//! - the thing where we normalize to punycode, including tests
//! - how to provide the non-punycode version to the app easily?
//! - the great renaming (maybe we can do the punycode thing at the same time?)
//! - return a "display" field (exact name TBD) in the serialized
//! version, which will be the unicode version of punycode urls.
//! - the great renaming
//!
//! - `password`: The saved password, as a string.
//!
//! This field is required, and must not be set to the empty string. It must not contain
//! null bytes, but can otherwise be an arbitrary unicode string.
//!
//! **XXX TODO:**
//! - test that it cannot by null or the empty string
//! - test that it cannot contain null bytes (do we want to fixup by just removing them?)
//! - test that it *can* contain non-ascii characters (esp. when round-tripped through the db)
//!
//! - `username`: The username associated with this login, if any, as a string.
//!
//! This field is required, but may be set to the empty string if no username is associated
//! with the login. It must not contain null bytes, but can otherwise be an arbitrary unicode
//! string.
//!
//! **XXX TODO:**:
//! - test that it cannot by null
//! - test that it cannot contain null bytes (do we want to fixup by just removing them?)
//! - test that it *can* contain non-ascii characters (esp. when round-tripped through the db)
//!
//! - `httpRealm`: The challenge string for HTTP Basic authentication, if any.
//!
//! If present, the login should only be used in response to a HTTP Basic Auth
@ -86,11 +75,6 @@
//! of login (HTTP-Auth based versus form-based). Exactly one of `httpRealm` and `formSubmitURL`
//! must be present.
//!
//! **XXX TODO**:
//! - test that it cannot contain null bytes, carriage returns or newlines (do we want to fixup by just removing them?)
//! - test that it cannot be present with formSubmitURL
//! - test that it *can* contain non-ascii characters (esp. when round-tripped through the db)
//!
//! - `formSubmitURL`: The target origin of forms in which this login can be used, if any, as a string.
//!
//! If present, the login should only be used in forms whose target submission URL matches this origin.
@ -112,15 +96,8 @@
//! - replacing invalid values with null if a valid 'httpRealm' field is present
//!
//! **XXX TODO**:
//! - test that we validate as an origin.
//! - test that we fixup full URLs by turning them into origins?
//! - test that we fixup "." to the empty string
//! - test that we allow the special "javascript:" value
//! - test that it *can* contain non-ascii characters (esp. when round-tripped through the db)
//! - test that it cannot be present with 'httpRealm'
//! - test that we set invalid values to null when 'httpRealm' is present
//! - the thing where we normalize to punycode, including tests
//! - how to provide the non-punycode version to the app easily?
//! - return a "display" field (exact name TBD) in the serialized
//! version, which will be the unicode version of punycode urls.
//! - the great renaming (maybe we can do the punycode thing at the same time?)
//!
//! - `usernameField`: The name of the form field into which the 'username' should be filled, if any.
@ -133,10 +110,6 @@
//! then the logins store will attempt to coerce it into valid data by:
//! - setting to the empty string if 'formSubmitURL' is not present
//!
//! **XXX TODO:**
//! - test that we reject invalid characters; should we fix them up at all?
//! - test that it *can* contain non-ascii characters (esp. when round-tripped through the db)
//!
//! - `passwordField`: The name of the form field into which the 'password' should be filled, if any.
//!
//! This value is stored if provided by the application, but does not imply any restrictions on
@ -147,10 +120,6 @@
//! then the logins store will attempt to coerce it into valid data by:
//! - setting to the empty string if 'formSubmitURL' is not present
//!
//! **XXX TODO:**
//! - test that we reject invalid characters; should we fix them up at all?
//! - test that it *can* contain non-ascii characters (esp. when round-tripped through the db)
//!
//! - `timesUsed`: A lower bound on the number of times the password from this record has been used, as an integer.
//!
//! Applications should use the `touch()` method of the logins store to indicate when a password
@ -173,7 +142,7 @@
//! - replacing missing or negative values with 0
//!
//! **XXX TODO:**
//! - test that we prevent this timestamp from moving backwards.
//! - test that we prevent this counter from moving backwards.
//! - test fixups of missing or negative values
//! - test that we correctly merge dupes
//!
@ -249,13 +218,13 @@
//!
//! In order to deal with data from legacy clients in a robust way, it is necessary to be able to build
//! and manipulate `Login` structs that contain invalid data. The following methods can be used by callers
//! to ensure that they're only work with valid records:
//! to ensure that they're only working with valid records:
//!
//! - `Login::check_valid()`: Checks valdity of a login record, returning `()` if it is valid
//! or an error if it is not.
//!
//! - `Login::fixup()`: Returns either the existing login if it is valid, a clone with invalid fields
//! fixed up if it was safe to do so, or an error if the login is irreperably invalid.
//! fixed up if it was safe to do so, or an error if the login is irreparably invalid.
use crate::error::*;
use crate::util;
@ -264,6 +233,7 @@ use serde_derive::*;
use std::time::{self, SystemTime};
use sync15::ServerTimestamp;
use sync_guid::Guid;
use url::Url;
#[derive(Debug, Clone, Hash, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
@ -361,6 +331,54 @@ impl Login {
self.validate_and_fixup(true)
}
/// Internal helper for validation and fixups of an "origin" stored as
/// a string.
fn validate_and_fixup_origin(origin: &str) -> Result<Option<String>> {
// Check we can parse the origin, then use the normalized version of it.
match Url::parse(&origin) {
Ok(mut u) => {
// Presumably this is a faster path than always setting?
if u.path() != "/"
|| u.fragment().is_some()
|| u.query().is_some()
|| u.username() != "/"
|| u.password().is_some()
{
// Not identical - we only want the origin part, so kill
// any other parts which may exist.
// But first special case `file://` URLs which always
// resolve to `file://`
if u.scheme() == "file" {
return Ok(if origin == "file://" {
None
} else {
Some("file://".into())
});
}
u.set_path("");
u.set_fragment(None);
u.set_query(None);
let _ = u.set_username("");
let _ = u.set_password(None);
// We always store without the trailing "/" which Urls have.
let mut href = u.into_string();
href.pop().expect("url must have a length");
if origin != href {
// Needs to be fixed up.
return Ok(Some(href));
}
}
Ok(None)
}
Err(_) => {
// We can't fixup completely invalid records, so always throw.
throw!(InvalidLogin::IllegalFieldValue {
field_info: "Origin is Malformed".into()
});
}
}
}
/// Internal helper for doing validation and fixups.
fn validate_and_fixup(&self, fixup: bool) -> Result<Option<Self>> {
// XXX TODO: we've definitely got more validation and fixups to add here!
@ -393,7 +411,7 @@ impl Login {
}
if self.form_submit_url.is_some() && self.http_realm.is_some() {
throw!(InvalidLogin::BothTargets);
get_fixed_or_throw!(InvalidLogin::BothTargets)?.http_realm = None;
}
if self.form_submit_url.is_none() && self.http_realm.is_none() {
@ -401,7 +419,12 @@ impl Login {
}
let form_submit_url = self.form_submit_url.clone().unwrap_or_default();
let http_realm = self.http_realm.clone().unwrap_or_default();
let http_realm = maybe_fixed
.as_ref()
.unwrap_or(self)
.http_realm
.clone()
.unwrap_or_default();
let field_data = [
("formSubmitUrl", &form_submit_url),
@ -440,32 +463,49 @@ impl Login {
});
}
if form_submit_url == "." {
throw!(InvalidLogin::IllegalFieldValue {
field_info: "`formSubmitUrl` is a period".into()
});
// Check we can parse the origin, then use the normalized version of it.
if let Some(fixed) = Login::validate_and_fixup_origin(&self.hostname)? {
get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
field_info: "Origin is not normalized".into()
})?
.hostname = fixed;
}
if self.hostname.contains(" (") {
throw!(InvalidLogin::IllegalFieldValue {
field_info: "Origin is Malformed".into()
});
}
if self.form_submit_url.is_none() {
if !self.username_field.is_empty() {
get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
field_info: "usernameField must be empty when formSubmitURL is null".into()
})?
.username_field
.clear();
match &maybe_fixed.as_ref().unwrap_or(self).form_submit_url {
None => {
if !self.username_field.is_empty() {
get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
field_info: "usernameField must be empty when formSubmitURL is null".into()
})?
.username_field
.clear();
}
if !self.password_field.is_empty() {
get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
field_info: "passwordField must be empty when formSubmitURL is null".into()
})?
.password_field
.clear();
}
}
if !self.password_field.is_empty() {
get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
field_info: "passwordField must be empty when formSubmitURL is null".into()
})?
.password_field
.clear();
Some(href) => {
// "." and "javascript:" are special cases documented at the top of this file.
if href == "." {
// A bit of a special case - if we are being asked to fixup, we replace
// "." with an empty string - but if not fixing up we don't complain.
if fixup {
maybe_fixed
.get_or_insert_with(|| self.clone())
.form_submit_url = Some("".into());
}
} else if href != "javascript:" {
if let Some(fixed) = Login::validate_and_fixup_origin(&href)? {
get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
field_info: "formActionOrigin is not normalized".into()
})?
.form_submit_url = Some(fixed);
}
}
}
}
@ -890,6 +930,58 @@ mod tests {
assert_eq!(login.time_password_changed, now64 - 25);
}
#[test]
fn test_url_fixups() -> Result<()> {
// Start with URLs which are all valid and already normalized.
for input in &[
// The list of valid hostnames documented at the top of this file.
"https://site.com",
"http://site.com:1234",
"ftp://ftp.site.com",
"moz-proxy://127.0.0.1:8888",
"chrome://MyLegacyExtension",
"file://",
"https://[::1]",
] {
assert_eq!(Login::validate_and_fixup_origin(input)?, None);
}
// And URLs which get normalized.
for (input, output) in &[
("https://site.com/", "https://site.com"),
("http://site.com:1234/", "http://site.com:1234"),
("http://example.com/foo?query=wtf#bar", "http://example.com"),
("http://example.com/foo#bar", "http://example.com"),
(
"http://username:password@example.com/",
"http://example.com",
),
("http://😍.com/", "http://xn--r28h.com"),
("https://[0:0:0:0:0:0:0:1]", "https://[::1]"),
// All `file://` URLs normalize to exactly `file://`. See #2384 for
// why we might consider changing that later.
("file:///", "file://"),
("file://foo/bar", "file://"),
("file://foo/bar/", "file://"),
("moz-proxy://127.0.0.1:8888/", "moz-proxy://127.0.0.1:8888"),
(
"moz-proxy://127.0.0.1:8888/foo",
"moz-proxy://127.0.0.1:8888",
),
("chrome://MyLegacyExtension/", "chrome://MyLegacyExtension"),
(
"chrome://MyLegacyExtension/foo",
"chrome://MyLegacyExtension",
),
] {
assert_eq!(
Login::validate_and_fixup_origin(input)?,
Some((*output).into())
);
}
Ok(())
}
#[test]
fn test_check_valid() {
struct TestCase {
@ -936,7 +1028,7 @@ mod tests {
..Login::default()
};
let login_with_null_html_realm = Login {
let login_with_null_http_realm = Login {
hostname: "https://www.example.com".into(),
http_realm: Some("https://www.example.\0com".into()),
username: "test".into(),
@ -952,6 +1044,14 @@ mod tests {
..Login::default()
};
let login_with_null_password = Login {
hostname: "https://www.example.com".into(),
http_realm: Some("https://www.example.com".into()),
username: "username".into(),
password: "test\0".into(),
..Login::default()
};
let login_with_newline_hostname = Login {
hostname: "\rhttps://www.example.com".into(),
http_realm: Some("https://www.example.com".into()),
@ -969,6 +1069,14 @@ mod tests {
..Login::default()
};
let login_with_newline_realm = Login {
hostname: "https://www.example.com".into(),
http_realm: Some("foo\nbar".into()),
username: "test".into(),
password: "test".into(),
..Login::default()
};
let login_with_newline_password = Login {
hostname: "https://www.example.com".into(),
http_realm: Some("https://www.example.com".into()),
@ -994,6 +1102,14 @@ mod tests {
..Login::default()
};
let login_with_javascript_form_submit_url = Login {
form_submit_url: Some("javascript:".into()),
hostname: "https://www.example.com".into(),
username: "test".into(),
password: "test".into(),
..Login::default()
};
let login_with_malformed_origin_parens = Login {
hostname: " (".into(),
http_realm: Some("https://www.example.com".into()),
@ -1002,6 +1118,38 @@ mod tests {
..Login::default()
};
let login_with_host_unicode = Login {
hostname: "http://💖.com".into(),
http_realm: Some("https://www.example.com".into()),
username: "test".into(),
password: "test".into(),
..Login::default()
};
let login_with_hostname_trailing_slash = Login {
hostname: "https://www.example.com/".into(),
http_realm: Some("https://www.example.com".into()),
username: "test".into(),
password: "test".into(),
..Login::default()
};
let login_with_hostname_expanded_ipv6 = Login {
hostname: "https://[0:0:0:0:0:0:1:1]".into(),
http_realm: Some("https://www.example.com".into()),
username: "test".into(),
password: "test".into(),
..Login::default()
};
let login_with_unknown_protocol = Login {
hostname: "moz-proxy://127.0.0.1:8888".into(),
http_realm: Some("https://www.example.com".into()),
username: "test".into(),
password: "test".into(),
..Login::default()
};
let test_cases = [
TestCase {
login: valid_login,
@ -1029,7 +1177,7 @@ mod tests {
expected_err: "Invalid login: Neither `formSubmitUrl` or `httpRealm` are present",
},
TestCase {
login: login_with_null_html_realm,
login: login_with_null_http_realm,
should_err: true,
expected_err: "Invalid login: Login has illegal field: `httpRealm` contains Nul",
},
@ -1038,11 +1186,22 @@ mod tests {
should_err: true,
expected_err: "Invalid login: Login has illegal field: `username` contains Nul",
},
TestCase {
login: login_with_null_password,
should_err: true,
expected_err: "Invalid login: Login has illegal field: `password` contains Nul",
},
TestCase {
login: login_with_newline_hostname,
should_err: true,
expected_err: "Invalid login: Login has illegal field: `hostname` contains newline",
},
TestCase {
login: login_with_newline_realm,
should_err: true,
expected_err:
"Invalid login: Login has illegal field: `httpRealm` contains newline",
},
TestCase {
login: login_with_newline_username_field,
should_err: true,
@ -1061,14 +1220,39 @@ mod tests {
},
TestCase {
login: login_with_period_form_submit_url,
should_err: true,
expected_err: "Invalid login: Login has illegal field: `formSubmitUrl` is a period",
should_err: false,
expected_err: "",
},
TestCase {
login: login_with_javascript_form_submit_url,
should_err: false,
expected_err: "",
},
TestCase {
login: login_with_malformed_origin_parens,
should_err: true,
expected_err: "Invalid login: Login has illegal field: Origin is Malformed",
},
TestCase {
login: login_with_host_unicode,
should_err: true,
expected_err: "Invalid login: Login has illegal field: Origin is not normalized",
},
TestCase {
login: login_with_hostname_trailing_slash,
should_err: true,
expected_err: "Invalid login: Login has illegal field: Origin is not normalized",
},
TestCase {
login: login_with_hostname_expanded_ipv6,
should_err: true,
expected_err: "Invalid login: Login has illegal field: Origin is not normalized",
},
TestCase {
login: login_with_unknown_protocol,
should_err: false,
expected_err: "",
},
];
for tc in &test_cases {
@ -1083,6 +1267,84 @@ mod tests {
}
}
#[test]
fn test_fixup() {
#[derive(Default)]
struct TestCase {
login: Login,
fixedup_host: Option<&'static str>,
fixedup_form_submit_url: Option<String>,
}
// Note that most URL fixups are tested above, but we have one or 2 here.
let login_with_full_url = Login {
hostname: "http://example.com/foo?query=wtf#bar".into(),
form_submit_url: Some("http://example.com/foo?query=wtf#bar".into()),
username: "test".into(),
password: "test".into(),
..Login::default()
};
let login_with_host_unicode = Login {
hostname: "http://😍.com".into(),
form_submit_url: Some("http://😍.com".into()),
username: "test".into(),
password: "test".into(),
..Login::default()
};
let login_with_period_fsu = Login {
hostname: "https://example.com".into(),
form_submit_url: Some(".".into()),
username: "test".into(),
password: "test".into(),
..Login::default()
};
let login_with_form_submit_and_http_realm = Login {
hostname: "https://www.example.com".into(),
form_submit_url: Some("https://www.example.com".into()),
// If both http_realm and form_submit_url are specified, we drop
// the former when fixing up. So for this test we must have an
// invalid value in http_realm to ensure we don't validate a value
// we end up dropping.
http_realm: Some("\n".into()),
password: "test".into(),
..Login::default()
};
let test_cases = [
TestCase {
login: login_with_full_url,
fixedup_host: "http://example.com".into(),
fixedup_form_submit_url: Some("http://example.com".into()),
},
TestCase {
login: login_with_host_unicode,
fixedup_host: "http://xn--r28h.com".into(),
fixedup_form_submit_url: Some("http://xn--r28h.com".into()),
},
TestCase {
login: login_with_period_fsu,
fixedup_form_submit_url: Some("".into()),
..TestCase::default()
},
TestCase {
login: login_with_form_submit_and_http_realm,
fixedup_form_submit_url: Some("https://www.example.com".into()),
..TestCase::default()
},
];
for tc in &test_cases {
let login = tc.login.clone().fixup().expect("should work");
if let Some(expected) = tc.fixedup_host {
assert_eq!(login.hostname, expected);
}
assert_eq!(login.form_submit_url, tc.fixedup_form_submit_url);
}
}
#[test]
fn test_username_field_requires_a_form_target() {
let bad_payload: sync15::Payload = serde_json::from_value(serde_json::json!({

Просмотреть файл

@ -63,7 +63,8 @@ class LoginsTests: XCTestCase {
let record0 = try! storage.get(id: id0)!
XCTAssertNil(record0.httpRealm)
XCTAssertEqual(record0.formSubmitURL, "https://www.example.com/login")
// We fixed up the formSubmitURL to just be the origin part of the url.
XCTAssertEqual(record0.formSubmitURL, "https://www.example.com")
let id1 = try! storage.add(login: LoginRecord(
id: "",
@ -95,7 +96,7 @@ class LoginsTests: XCTestCase {
password: "hunter5",
hostname: "https://www.example5.com",
username: "cooluser55",
formSubmitURL: "https://www.example5.com/login",
formSubmitURL: "https://www.example5.com",
httpRealm: nil,
timesUsed: nil,
timeLastUsed: nil,
@ -110,7 +111,7 @@ class LoginsTests: XCTestCase {
password: "hunter3",
hostname: "https://www.example5.com",
username: "cooluser55",
formSubmitURL: "https://www.example5.com/login",
formSubmitURL: "https://www.example5.com",
httpRealm: nil,
timesUsed: nil,
timeLastUsed: nil,
@ -125,7 +126,7 @@ class LoginsTests: XCTestCase {
password: "hunter3",
hostname: "https://www.example6.com",
username: "\0cooluser56",
formSubmitURL: "https://www.example6.com/login",
formSubmitURL: "https://www.example6.com",
httpRealm: nil,
timesUsed: nil,
timeLastUsed: nil,