Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
"use strict";
/**
* Exercises RustAutofillAddressesAdapter: JS address records (hyphenated
* keys) round-trip through the Rust Store, computed fields are reconstructed on
* read, and storage-changed observers fire.
*/
const { RustAutofillAddressesAdapter } = ChromeUtils.importESModule(
);
const { Store } = ChromeUtils.importESModule(
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustAutofill.sys.mjs"
);
const TEST_RECORD = {
name: "Jane Doe",
organization: "Mozilla",
"street-address": "331 E Evelyn Ave",
"address-level2": "Mountain View",
"address-level1": "CA",
"postal-code": "94041",
country: "US",
tel: "+16505551234",
email: "jane@example.com",
};
function waitForStorageChanged(expectedAction) {
return new Promise(resolve => {
Services.obs.addObserver(function obs(subject, _topic, action) {
if (action != expectedAction) {
return;
}
Services.obs.removeObserver(obs, "formautofill-storage-changed");
resolve(subject.wrappedJSObject);
}, "formautofill-storage-changed");
});
}
add_task(async function test_adapter_crud_and_computed_fields() {
const dbPath = FileTestUtils.getTempFile("autofill-adapter.sqlite").path;
const adapter = new RustAutofillAddressesAdapter(await Store.init(dbPath));
// isEmpty() answers from a cached count and reports empty until something
// primes it, so prime it first: without this the assertion passes whatever
// the store holds.
await adapter.refreshCount();
Assert.ok(adapter.isEmpty(), "adapter starts empty");
// add -> fires "add" with the addresses collection
const addNotified = waitForStorageChanged("add");
const guid = await adapter.add(TEST_RECORD);
const addSubject = await addNotified;
Assert.ok(guid, "add returns a guid");
Assert.equal(addSubject.collectionName, "addresses", "notifies addresses");
Assert.equal(addSubject.guid, guid, "notifies the added guid");
Assert.ok(!adapter.isEmpty(), "not empty after add");
// get -> canonical fields round-trip (hyphenated keys) + computed fields
const fetched = await adapter.get(guid);
Assert.equal(fetched.guid, guid, "guid round-trips");
Assert.equal(fetched["street-address"], "331 E Evelyn Ave", "street-address");
Assert.equal(fetched["address-level1"], "CA", "address-level1");
Assert.equal(fetched["postal-code"], "94041", "postal-code");
Assert.equal(fetched.email, "jane@example.com", "email");
// computed fields reconstructed by AddressRecord.computeFields
Assert.equal(fetched["given-name"], "Jane", "computed given-name");
Assert.equal(fetched["family-name"], "Doe", "computed family-name");
Assert.ok(fetched["country-name"], "computed country-name present");
Assert.equal(fetched.timesUsed, 0, "timesUsed starts at 0");
// getAll
const all = await adapter.getAll();
Assert.equal(all.length, 1, "getAll returns one");
// getSavedFieldNames
const names = await adapter.getSavedFieldNames();
Assert.ok(
names.has("street-address"),
"saved field names include street-address"
);
Assert.ok(names.has("email"), "saved field names include email");
// notifyUsed -> timesUsed increments
await adapter.notifyUsed(guid);
Assert.equal(
(await adapter.get(guid)).timesUsed,
1,
"timesUsed incremented by notifyUsed"
);
// update
await adapter.update(guid, { ...TEST_RECORD, name: "Jane Q. Doe" });
const updated = await adapter.get(guid);
Assert.equal(updated.name, "Jane Q. Doe", "update persisted name");
Assert.equal(
updated["family-name"],
"Doe",
"computed family-name after update"
);
// remove -> empty
const removeNotified = waitForStorageChanged("remove");
await adapter.remove(guid);
await removeNotified;
Assert.ok(adapter.isEmpty(), "empty after remove");
});
add_task(async function test_add_many_with_meta_bulk_import() {
const dbPath = FileTestUtils.getTempFile("autofill-adapter-bulk.sqlite").path;
const adapter = new RustAutofillAddressesAdapter(await Store.init(dbPath));
const records = [
{
...TEST_RECORD,
guid: "BulkGuid0001",
timeCreated: 1600000000000,
timeLastModified: 1600000000000,
timesUsed: 1,
},
{
...TEST_RECORD,
guid: "BulkGuid0002",
name: "John Roe",
timeCreated: 1600000002000,
timeLastModified: 1600000002000,
timesUsed: 5,
},
];
const results = await adapter.addManyWithMeta(records);
Assert.equal(results.length, 2, "one result per input record");
Assert.deepEqual(
results.map(r => r.guid),
["BulkGuid0001", "BulkGuid0002"],
"each result reports its preserved guid, in order"
);
Assert.ok(
results.every(r => !r.error),
"no per-record errors for valid records"
);
const all = await adapter.getAll();
Assert.equal(all.length, 2, "both records imported");
const byGuid = Object.fromEntries(all.map(r => [r.guid, r]));
Assert.equal(byGuid.BulkGuid0002.name, "John Roe", "fields preserved");
Assert.equal(byGuid.BulkGuid0002.timesUsed, 5, "timesUsed preserved");
Assert.equal(
byGuid.BulkGuid0002["street-address"],
"331 E Evelyn Ave",
"canonical fields preserved"
);
Assert.equal(
byGuid.BulkGuid0001.timeCreated,
1600000000000,
"timeCreated preserved"
);
Assert.equal(
byGuid.BulkGuid0001.timeLastModified,
1600000000000,
"timeLastModified preserved, rather than stamped with now"
);
Assert.equal(
(await adapter.get("BulkGuid0001")).guid,
"BulkGuid0001",
"and each record is retrievable by the guid it kept"
);
});
add_task(async function test_add_many_with_meta_isolates_per_record_failure() {
const dbPath = FileTestUtils.getTempFile(
"autofill-adapter-bulkfail.sqlite"
).path;
const adapter = new RustAutofillAddressesAdapter(await Store.init(dbPath));
// Two records sharing a guid: the second insert collides (duplicate primary
// key) and must be reported as an error without aborting the good one.
const results = await adapter.addManyWithMeta([
{
...TEST_RECORD,
guid: "DupGuid00001",
timeCreated: 1,
timeLastModified: 1,
},
{
...TEST_RECORD,
guid: "DupGuid00001",
timeCreated: 2,
timeLastModified: 2,
},
]);
Assert.equal(results.length, 2, "one result per input record");
Assert.ok(!results[0].error, "first record succeeds");
Assert.ok(
results[1].error,
"second (duplicate guid) record reports an error"
);
Assert.equal(
(await adapter.getAll()).length,
1,
"the good record is still persisted despite the sibling failure"
);
});
add_task(async function test_get_distinguishes_missing_from_unreadable() {
const dbPath = FileTestUtils.getTempFile("autofill-adapter-get.sqlite").path;
const adapter = new RustAutofillAddressesAdapter(await Store.init(dbPath));
Assert.equal(
await adapter.get("NoSuchGuid01"),
null,
"a guid the store does not hold reads as null"
);
// A store that cannot be read must not answer "no such record": callers take
// that as an empty slot. FormAutofillPreferences opens the edit dialog blank
// as though the user were creating an address, and saving it writes a second
// record on top of the one that could not be read.
const unreadable = new RustAutofillAddressesAdapter({
getAddress: () => Promise.reject(new Error("database is locked")),
});
await Assert.rejects(
unreadable.get("AnyGuid00001"),
/database is locked/,
"a failure to read propagates rather than reading as not-found"
);
});