Bug 1620186 - Fetch from changeset endpoint r=glasserc

Differential Revision: https://phabricator.services.mozilla.com/D71570
This commit is contained in:
Mathieu Leplatre 2020-04-24 09:15:43 +00:00
Родитель 3c64c7a3b2
Коммит e95ee4d2f4
4 изменённых файлов: 214 добавлений и 238 удалений

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

@ -765,24 +765,17 @@ class RemoteSettingsClient extends EventEmitter {
options = {} options = {}
) { ) {
const { retry = false } = options; const { retry = false } = options;
const since = retry || !localTimestamp ? undefined : `${localTimestamp}`;
// Fetch collection metadata and list of changes from server // Fetch collection metadata and list of changes from server.
// (or all records on retry). console.debug(
const client = this.httpClient(); `Fetch changes from server (expected=${expectedTimestamp}, since=${since})`
const [ );
const {
metadata, metadata,
{ data: remoteRecords, last_modified: remoteTimestamp }, remoteTimestamp,
] = await Promise.all([ remoteRecords,
client.getData({ } = await this._fetchChangeset(expectedTimestamp, since);
query: { _expected: expectedTimestamp },
}),
client.listRecords({
filters: {
_expected: expectedTimestamp,
},
since: retry || !localTimestamp ? undefined : `${localTimestamp}`,
}),
]);
// We build a sync result, based on remote changes. // We build a sync result, based on remote changes.
const syncResult = { const syncResult = {
@ -893,6 +886,36 @@ class RemoteSettingsClient extends EventEmitter {
return syncResult; return syncResult;
} }
/**
* Fetch information from changeset endpoint.
*
* @param expectedTimestamp cache busting value
* @param since timestamp of last sync (optional)
*/
async _fetchChangeset(expectedTimestamp, since) {
const client = this.httpClient();
const {
metadata,
timestamp: remoteTimestamp,
changes: remoteRecords,
} = await client.execute(
{
path: `/buckets/${this.bucketName}/collections/${this.collectionName}/changeset`,
},
{
query: {
_expected: expectedTimestamp,
_since: since,
},
}
);
return {
remoteTimestamp,
metadata,
remoteRecords,
};
}
/** /**
* Use the filter func to filter the lists of changes obtained from synchronization, * Use the filter func to filter the lists of changes obtained from synchronization,
* and return them along with the filtered list of local records. * and return them along with the filtered list of local records.

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

@ -69,11 +69,7 @@ function run_test() {
handleResponse handleResponse
); );
server.registerPathHandler( server.registerPathHandler(
"/v1/buckets/main/collections/password-fields", "/v1/buckets/main/collections/password-fields/changeset",
handleResponse
);
server.registerPathHandler(
"/v1/buckets/main/collections/password-fields/records",
handleResponse handleResponse
); );
server.registerPathHandler( server.registerPathHandler(
@ -81,15 +77,11 @@ function run_test() {
handleResponse handleResponse
); );
server.registerPathHandler( server.registerPathHandler(
"/v1/buckets/main/collections/language-dictionaries/records", "/v1/buckets/main/collections/language-dictionaries/changeset",
handleResponse handleResponse
); );
server.registerPathHandler( server.registerPathHandler(
"/v1/buckets/main/collections/with-local-fields", "/v1/buckets/main/collections/with-local-fields/changeset",
handleResponse
);
server.registerPathHandler(
"/v1/buckets/main/collections/with-local-fields/records",
handleResponse handleResponse
); );
server.registerPathHandler("/fake-x5u", handleResponse); server.registerPathHandler("/fake-x5u", handleResponse);
@ -910,26 +902,6 @@ function getSampleResponse(req, port) {
], ],
}, },
}, },
"GET:/v1/buckets/main/collections/password-fields": {
sampleHeaders: [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
'Etag: "1234"',
],
status: { status: 200, statusText: "OK" },
responseBody: JSON.stringify({
data: {
id: "password-fields",
last_modified: 1234,
signature: {
signature: "abcdef",
x5u: `http://localhost:${port}/fake-x5u`,
},
},
}),
},
"GET:/fake-x5u": { "GET:/fake-x5u": {
sampleHeaders: ["Content-Type: application/octet-stream"], sampleHeaders: ["Content-Type: application/octet-stream"],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
@ -940,7 +912,7 @@ ZARKjbu1TuYQHf0fs+GwID8zeLc2zJL7UzcHFwwQ6Nda9OJN4uPAuC/BKaIpxCLL
wNuvFqc= wNuvFqc=
-----END CERTIFICATE-----`, -----END CERTIFICATE-----`,
}, },
"GET:/v1/buckets/main/collections/password-fields/records?_expected=2000&_sort=-last_modified": { "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=2000": {
sampleHeaders: [ sampleHeaders: [
"Access-Control-Allow-Origin: *", "Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
@ -950,7 +922,16 @@ wNuvFqc=
], ],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: { responseBody: {
data: [ timestamp: 3000,
metadata: {
id: "password-fields",
last_modified: 1234,
signature: {
signature: "abcdef",
x5u: `http://localhost:${port}/fake-x5u`,
},
},
changes: [
{ {
id: "9d500963-d80e-3a91-6e74-66f3811b99cc", id: "9d500963-d80e-3a91-6e74-66f3811b99cc",
last_modified: 3000, last_modified: 3000,
@ -960,7 +941,7 @@ wNuvFqc=
], ],
}, },
}, },
"GET:/v1/buckets/main/collections/password-fields/records?_expected=3001&_sort=-last_modified&_since=3000": { "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=3001&_since=3000": {
sampleHeaders: [ sampleHeaders: [
"Access-Control-Allow-Origin: *", "Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
@ -970,7 +951,9 @@ wNuvFqc=
], ],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: { responseBody: {
data: [ metadata: {},
timestamp: 4000,
changes: [
{ {
id: "aabad965-e556-ffe7-4191-074f5dee3df3", id: "aabad965-e556-ffe7-4191-074f5dee3df3",
last_modified: 4000, last_modified: 4000,
@ -986,7 +969,7 @@ wNuvFqc=
], ],
}, },
}, },
"GET:/v1/buckets/main/collections/password-fields/records?_expected=4001&_sort=-last_modified&_since=4000": { "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=4001&_since=4000": {
sampleHeaders: [ sampleHeaders: [
"Access-Control-Allow-Origin: *", "Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
@ -996,7 +979,9 @@ wNuvFqc=
], ],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: { responseBody: {
data: [ metadata: {},
timestamp: 5000,
changes: [
{ {
id: "aabad965-e556-ffe7-4191-074f5dee3df3", id: "aabad965-e556-ffe7-4191-074f5dee3df3",
deleted: true, deleted: true,
@ -1004,7 +989,7 @@ wNuvFqc=
], ],
}, },
}, },
"GET:/v1/buckets/main/collections/password-fields/records?_expected=10000&_sort=-last_modified&_since=9999": { "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10000&_since=9999": {
sampleHeaders: [ sampleHeaders: [
"Access-Control-Allow-Origin: *", "Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
@ -1018,7 +1003,7 @@ wNuvFqc=
error: "Service Unavailable", error: "Service Unavailable",
}, },
}, },
"GET:/v1/buckets/main/collections/password-fields/records?_expected=10001&_sort=-last_modified&_since=10000": { "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10001&_since=10000": {
sampleHeaders: [ sampleHeaders: [
"Access-Control-Allow-Origin: *", "Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
@ -1029,7 +1014,7 @@ wNuvFqc=
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: "<invalid json", responseBody: "<invalid json",
}, },
"GET:/v1/buckets/main/collections/password-fields/records?_expected=11001&_sort=-last_modified&_since=11000": { "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=11001&_since=11000": {
sampleHeaders: [ sampleHeaders: [
"Access-Control-Allow-Origin: *", "Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
@ -1038,7 +1023,7 @@ wNuvFqc=
], ],
status: { status: 503, statusText: "Service Unavailable" }, status: { status: 503, statusText: "Service Unavailable" },
responseBody: { responseBody: {
data: [ changes: [
{ {
id: "c4f021e3-f68c-4269-ad2a-d4ba87762b35", id: "c4f021e3-f68c-4269-ad2a-d4ba87762b35",
last_modified: 4000, last_modified: 4000,
@ -1083,7 +1068,7 @@ wNuvFqc=
], ],
}, },
}, },
"GET:/v1/buckets/main/collections/password-fields/records?_expected=1337&_sort=-last_modified": { "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=1337": {
sampleHeaders: [ sampleHeaders: [
"Access-Control-Allow-Origin: *", "Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
@ -1093,7 +1078,9 @@ wNuvFqc=
], ],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: { responseBody: {
data: [ metadata: {},
timestamp: 3000,
changes: [
{ {
id: "312cc78d-9c1f-4291-a4fa-a1be56f6cc69", id: "312cc78d-9c1f-4291-a4fa-a1be56f6cc69",
last_modified: 3000, last_modified: 3000,
@ -1123,7 +1110,7 @@ wNuvFqc=
}, },
}), }),
}, },
"GET:/v1/buckets/main/collections/language-dictionaries/records": { "GET:/v1/buckets/main/collections/language-dictionaries/changeset": {
sampleHeaders: [ sampleHeaders: [
"Access-Control-Allow-Origin: *", "Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
@ -1133,7 +1120,16 @@ wNuvFqc=
], ],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: { responseBody: {
data: [ timestamp: 5000000000000,
metadata: {
id: "language-dictionaries",
last_modified: 1234,
signature: {
signature: "xyz",
x5u: `http://localhost:${port}/fake-x5u`,
},
},
changes: [
{ {
id: "xx", id: "xx",
last_modified: 5000000000000, last_modified: 5000000000000,
@ -1152,27 +1148,7 @@ wNuvFqc=
], ],
}, },
}, },
"GET:/v1/buckets/main/collections/with-local-fields": { "GET:/v1/buckets/main/collections/with-local-fields/changeset?_expected=2000": {
sampleHeaders: [
"Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
"Content-Type: application/json; charset=UTF-8",
"Server: waitress",
'Etag: "1234"',
],
status: { status: 200, statusText: "OK" },
responseBody: JSON.stringify({
data: {
id: "with-local-fields",
last_modified: 1234,
signature: {
signature: "xyz",
x5u: `http://localhost:${port}/fake-x5u`,
},
},
}),
},
"GET:/v1/buckets/main/collections/with-local-fields/records?_expected=2000&_sort=-last_modified": {
sampleHeaders: [ sampleHeaders: [
"Access-Control-Allow-Origin: *", "Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
@ -1182,7 +1158,16 @@ wNuvFqc=
], ],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: { responseBody: {
data: [ timestamp: 2000,
metadata: {
id: "with-local-fields",
last_modified: 1234,
signature: {
signature: "xyz",
x5u: `http://localhost:${port}/fake-x5u`,
},
},
changes: [
{ {
id: "c74279ce-fb0a-42a6-ae11-386b567a6119", id: "c74279ce-fb0a-42a6-ae11-386b567a6119",
last_modified: 2000, last_modified: 2000,
@ -1190,7 +1175,7 @@ wNuvFqc=
], ],
}, },
}, },
"GET:/v1/buckets/main/collections/with-local-fields/records?_expected=3000&_sort=-last_modified&_since=2000": { "GET:/v1/buckets/main/collections/with-local-fields/changeset?_expected=3000&_since=2000": {
sampleHeaders: [ sampleHeaders: [
"Access-Control-Allow-Origin: *", "Access-Control-Allow-Origin: *",
"Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff", "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
@ -1200,7 +1185,9 @@ wNuvFqc=
], ],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: { responseBody: {
data: [ timestamp: 3000,
metadata: {},
changes: [
{ {
id: "1f5c98b9-6d93-4c13-aa26-978b38695096", id: "1f5c98b9-6d93-4c13-aa26-978b38695096",
last_modified: 3000, last_modified: 3000,

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

@ -114,40 +114,11 @@ add_task(async function test_check_signatures() {
add_task(async function test_check_synchronization_with_signatures() { add_task(async function test_check_synchronization_with_signatures() {
const port = server.identity.primaryPort; const port = server.identity.primaryPort;
const x5u = `http://localhost:${port}/test_remote_settings_signatures/test_cert_chain.pem`;
// Telemetry reports. // Telemetry reports.
const TELEMETRY_HISTOGRAM_KEY = client.identifier; const TELEMETRY_HISTOGRAM_KEY = client.identifier;
// a response to give the client when the cert chain is expected
function makeMetaResponseBody(lastModified, signature) {
return {
data: {
id: "signed",
last_modified: lastModified,
signature: {
x5u: `http://localhost:${port}/test_remote_settings_signatures/test_cert_chain.pem`,
public_key: "fake",
"content-signature": `x5u=http://localhost:${port}/test_remote_settings_signatures/test_cert_chain.pem;p384ecdsa=${signature}`,
signature_encoding: "rs_base64url",
signature,
hash_algorithm: "sha384",
ref: "1yryrnmzou5rf31ou80znpnq8n",
},
},
};
}
function makeMetaResponse(eTag, body, comment) {
return {
comment,
sampleHeaders: [
"Content-Type: application/json; charset=UTF-8",
`ETag: \"${eTag}\"`,
],
status: { status: 200, statusText: "OK" },
responseBody: JSON.stringify(body),
};
}
function registerHandlers(responses) { function registerHandlers(responses) {
function handleResponse(serverTimeMillis, request, response) { function handleResponse(serverTimeMillis, request, response) {
const key = `${request.method}:${request.path}?${request.queryString}`; const key = `${request.method}:${request.path}?${request.queryString}`;
@ -292,35 +263,28 @@ add_task(async function test_check_synchronization_with_signatures() {
'ETag: "1000"', 'ETag: "1000"',
], ],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: JSON.stringify({ data: [] }), responseBody: JSON.stringify({
timestamp: 1000,
metadata: {
signature: {
x5u,
signature:
"vxuAg5rDCB-1pul4a91vqSBQRXJG_j7WOYUTswxRSMltdYmbhLRH8R8brQ9YKuNDF56F-w6pn4HWxb076qgKPwgcEBtUeZAO_RtaHXRkRUUgVzAr86yQL4-aJTbv3D6u",
},
},
changes: [],
}),
}; };
// Valid signature for empty collection.
const RESPONSE_BODY_META_EMPTY_SIG = makeMetaResponseBody(
1000,
"vxuAg5rDCB-1pul4a91vqSBQRXJG_j7WOYUTswxRSMltdYmbhLRH8R8brQ9YKuNDF56F-w6pn4HWxb076qgKPwgcEBtUeZAO_RtaHXRkRUUgVzAr86yQL4-aJTbv3D6u"
);
// The collection metadata containing the signature for the empty
// collection.
const RESPONSE_META_EMPTY_SIG = makeMetaResponse(
1000,
RESPONSE_BODY_META_EMPTY_SIG,
"RESPONSE_META_EMPTY_SIG"
);
// Here, we map request method and path to the available responses // Here, we map request method and path to the available responses
const emptyCollectionResponses = { const emptyCollectionResponses = {
"GET:/test_remote_settings_signatures/test_cert_chain.pem?": [ "GET:/test_remote_settings_signatures/test_cert_chain.pem?": [
RESPONSE_CERT_CHAIN, RESPONSE_CERT_CHAIN,
], ],
"GET:/v1/?": [RESPONSE_SERVER_SETTINGS], "GET:/v1/?": [RESPONSE_SERVER_SETTINGS],
"GET:/v1/buckets/main/collections/signed/records?_expected=1000&_sort=-last_modified": [ "GET:/v1/buckets/main/collections/signed/changeset?_expected=1000": [
RESPONSE_EMPTY_INITIAL, RESPONSE_EMPTY_INITIAL,
], ],
"GET:/v1/buckets/main/collections/signed?_expected=1000": [
RESPONSE_META_EMPTY_SIG,
],
}; };
// //
@ -362,28 +326,23 @@ add_task(async function test_check_synchronization_with_signatures() {
'ETag: "3000"', 'ETag: "3000"',
], ],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: JSON.stringify({ data: [RECORD2, RECORD1] }), responseBody: JSON.stringify({
timestamp: 3000,
metadata: {
signature: {
x5u,
signature:
"dwhJeypadNIyzGj3QdI0KMRTPnHhFPF_j73mNrsPAHKMW46S2Ftf4BzsPMvPMB8h0TjDus13wo_R4l432DHe7tYyMIWXY0PBeMcoe5BREhFIxMxTsh9eGVXBD1e3UwRy",
},
},
changes: [RECORD2, RECORD1],
}),
}; };
const RESPONSE_BODY_META_TWO_ITEMS_SIG = makeMetaResponseBody(
3000,
"dwhJeypadNIyzGj3QdI0KMRTPnHhFPF_j73mNrsPAHKMW46S2Ftf4BzsPMvPMB8h0TjDus13wo_R4l432DHe7tYyMIWXY0PBeMcoe5BREhFIxMxTsh9eGVXBD1e3UwRy"
);
// A signature response for the collection containg RECORD1 and RECORD2
const RESPONSE_META_TWO_ITEMS_SIG = makeMetaResponse(
3000,
RESPONSE_BODY_META_TWO_ITEMS_SIG,
"RESPONSE_META_TWO_ITEMS_SIG"
);
const twoItemsResponses = { const twoItemsResponses = {
"GET:/v1/buckets/main/collections/signed/records?_expected=3000&_sort=-last_modified&_since=1000": [ "GET:/v1/buckets/main/collections/signed/changeset?_expected=3000&_since=1000": [
RESPONSE_TWO_ADDED, RESPONSE_TWO_ADDED,
], ],
"GET:/v1/buckets/main/collections/signed?_expected=3000": [
RESPONSE_META_TWO_ITEMS_SIG,
],
}; };
registerHandlers(twoItemsResponses); registerHandlers(twoItemsResponses);
await client.maybeSync(3000); await client.maybeSync(3000);
@ -397,6 +356,8 @@ add_task(async function test_check_synchronization_with_signatures() {
// //
// Check the collection with one addition and one removal has a valid // Check the collection with one addition and one removal has a valid
// signature // signature
const THREE_ITEMS_SIG =
"MIEmNghKnkz12UodAAIc3q_Y4a3IJJ7GhHF4JYNYmm8avAGyPM9fYU7NzVo94pzjotG7vmtiYuHyIX2rTHTbT587w0LdRWxipgFd_PC1mHiwUyjFYNqBBG-kifYk7kEw";
// Remove RECORD1, add RECORD3 // Remove RECORD1, add RECORD3
const RESPONSE_ONE_ADDED_ONE_REMOVED = { const RESPONSE_ONE_ADDED_ONE_REMOVED = {
@ -406,28 +367,22 @@ add_task(async function test_check_synchronization_with_signatures() {
'ETag: "4000"', 'ETag: "4000"',
], ],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: JSON.stringify({ data: [RECORD3, RECORD1_DELETION] }), responseBody: JSON.stringify({
timestamp: 4000,
metadata: {
signature: {
x5u,
signature: THREE_ITEMS_SIG,
},
},
changes: [RECORD3, RECORD1_DELETION],
}),
}; };
const RESPONSE_BODY_META_THREE_ITEMS_SIG = makeMetaResponseBody(
4000,
"MIEmNghKnkz12UodAAIc3q_Y4a3IJJ7GhHF4JYNYmm8avAGyPM9fYU7NzVo94pzjotG7vmtiYuHyIX2rTHTbT587w0LdRWxipgFd_PC1mHiwUyjFYNqBBG-kifYk7kEw"
);
// signature response for the collection containing RECORD2 and RECORD3
const RESPONSE_META_THREE_ITEMS_SIG = makeMetaResponse(
4000,
RESPONSE_BODY_META_THREE_ITEMS_SIG,
"RESPONSE_META_THREE_ITEMS_SIG"
);
const oneAddedOneRemovedResponses = { const oneAddedOneRemovedResponses = {
"GET:/v1/buckets/main/collections/signed/records?_expected=4000&_sort=-last_modified&_since=3000": [ "GET:/v1/buckets/main/collections/signed/changeset?_expected=4000&_since=3000": [
RESPONSE_ONE_ADDED_ONE_REMOVED, RESPONSE_ONE_ADDED_ONE_REMOVED,
], ],
"GET:/v1/buckets/main/collections/signed?_expected=4000": [
RESPONSE_META_THREE_ITEMS_SIG,
],
}; };
registerHandlers(oneAddedOneRemovedResponses); registerHandlers(oneAddedOneRemovedResponses);
await client.maybeSync(4000); await client.maybeSync(4000);
@ -449,22 +404,29 @@ add_task(async function test_check_synchronization_with_signatures() {
'ETag: "4000"', 'ETag: "4000"',
], ],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: JSON.stringify({ data: [] }), responseBody: JSON.stringify({
timestamp: 4000,
metadata: {
signature: {
x5u,
signature: THREE_ITEMS_SIG,
},
},
changes: [],
}),
}; };
const noOpResponses = { const noOpResponses = {
"GET:/v1/buckets/main/collections/signed/records?_expected=4100&_sort=-last_modified&_since=4000": [ "GET:/v1/buckets/main/collections/signed/changeset?_expected=4100&_since=4000": [
RESPONSE_EMPTY_NO_UPDATE, RESPONSE_EMPTY_NO_UPDATE,
], ],
"GET:/v1/buckets/main/collections/signed?_expected=4100": [
RESPONSE_META_THREE_ITEMS_SIG,
],
}; };
registerHandlers(noOpResponses); registerHandlers(noOpResponses);
await client.maybeSync(4100); await client.maybeSync(4100);
equal((await client.get()).length, 2); equal((await client.get()).length, 2);
console.info("---------------------------------------------------------");
// //
// 5. // 5.
// - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3] // - collection: [RECORD2, RECORD3] -> [RECORD2, RECORD3]
@ -484,38 +446,42 @@ add_task(async function test_check_synchronization_with_signatures() {
'ETag: "4000"', 'ETag: "4000"',
], ],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: JSON.stringify({ data: [RECORD2, RECORD3] }), responseBody: JSON.stringify({
timestamp: 4000,
metadata: {
signature: {
x5u,
signature: THREE_ITEMS_SIG,
},
},
changes: [RECORD2, RECORD3],
}),
}; };
// Prepare a (deliberately) bad signature to check the collection state is const RESPONSE_EMPTY_NO_UPDATE_BAD_SIG = {
// reset if something is inconsistent ...RESPONSE_EMPTY_NO_UPDATE,
const RESPONSE_BODY_META_BAD_SIG = makeMetaResponseBody( responseBody: JSON.stringify({
4000, timestamp: 4000,
"aW52YWxpZCBzaWduYXR1cmUK" metadata: {
); signature: {
const RESPONSE_META_BAD_SIG = makeMetaResponse( x5u,
4000, signature: "aW52YWxpZCBzaWduYXR1cmUK",
RESPONSE_BODY_META_BAD_SIG, },
"RESPONSE_META_BAD_SIG" },
); changes: [],
}),
};
const badSigGoodSigResponses = { const badSigGoodSigResponses = {
// In this test, we deliberately serve a bad signature initially. The
// subsequent signature returned is a valid one for the three item
// collection.
"GET:/v1/buckets/main/collections/signed?_expected=5000": [
RESPONSE_META_BAD_SIG,
RESPONSE_META_THREE_ITEMS_SIG,
],
// The first collection state is the three item collection (since // The first collection state is the three item collection (since
// there's a sync with no updates) - but, since the signature is wrong, // there was sync with no updates before) - but, since the signature is wrong,
// another request will be made... // another request will be made...
"GET:/v1/buckets/main/collections/signed/records?_expected=5000&_sort=-last_modified&_since=4000": [ "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=4000": [
RESPONSE_EMPTY_NO_UPDATE, RESPONSE_EMPTY_NO_UPDATE_BAD_SIG,
], ],
// The next request is for the full collection. This will be checked against the valid signature // Subsequent signature returned is a valid one for the three item
// - so the sync should succeed. // collection.
"GET:/v1/buckets/main/collections/signed/records?_expected=5000&_sort=-last_modified": [ "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000": [
RESPONSE_COMPLETE_INITIAL, RESPONSE_COMPLETE_INITIAL,
], ],
}; };
@ -558,23 +524,16 @@ add_task(async function test_check_synchronization_with_signatures() {
// - Sync will be no-op since local is equal to server, no changes to emit. // - Sync will be no-op since local is equal to server, no changes to emit.
const badSigGoodOldResponses = { const badSigGoodOldResponses = {
// In this test, we deliberately serve a bad signature initially. The
// subsequent sitnature returned is a valid one for the three item
// collection.
"GET:/v1/buckets/main/collections/signed?_expected=5000": [
RESPONSE_META_BAD_SIG,
RESPONSE_META_EMPTY_SIG,
],
// The first collection state is the current state (since there's no update // The first collection state is the current state (since there's no update
// - but, since the signature is wrong, another request will be made) // - but, since the signature is wrong, another request will be made)
"GET:/v1/buckets/main/collections/signed/records?_expected=5000&_sort=-last_modified&_since=4000": [ "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=4000": [
RESPONSE_EMPTY_NO_UPDATE, RESPONSE_EMPTY_NO_UPDATE_BAD_SIG,
], ],
// The next request is for the full collection. This will be // The next request is for the full collection. This will be
// checked against the valid signature and last_modified times will be // checked against the valid signature and last_modified times will be
// compared. Sync should be a no-op, even though the signature is good, // compared. Sync should be a no-op, even though the signature is good,
// because the local collection is newer. // because the local collection is newer.
"GET:/v1/buckets/main/collections/signed/records?_expected=5000&_sort=-last_modified": [ "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000": [
RESPONSE_EMPTY_INITIAL, RESPONSE_EMPTY_INITIAL,
], ],
}; };
@ -603,17 +562,23 @@ add_task(async function test_check_synchronization_with_signatures() {
// Check that a tampered local DB will be overwritten and // Check that a tampered local DB will be overwritten and
// sync event contain the appropriate data. // sync event contain the appropriate data.
const RESPONSE_COMPLETE_BAD_SIG = {
...RESPONSE_EMPTY_NO_UPDATE,
responseBody: JSON.stringify({
timestamp: 5000,
metadata: {
signature: {
x5u,
signature: "aW52YWxpZCBzaWduYXR1cmUK",
},
},
changes: [RECORD2, RECORD3],
}),
};
const badLocalContentGoodSigResponses = { const badLocalContentGoodSigResponses = {
// In this test, we deliberately serve a bad signature initially. The "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000": [
// subsequent signature returned is a valid one for the three item RESPONSE_COMPLETE_BAD_SIG,
// collection.
"GET:/v1/buckets/main/collections/signed?_expected=5000": [
RESPONSE_META_BAD_SIG,
RESPONSE_META_THREE_ITEMS_SIG,
],
// The next request is for the full collection. This will be checked
// against the valid signature - so the sync should succeed.
"GET:/v1/buckets/main/collections/signed/records?_expected=5000&_sort=-last_modified": [
RESPONSE_COMPLETE_INITIAL, RESPONSE_COMPLETE_INITIAL,
], ],
}; };
@ -659,7 +624,7 @@ add_task(async function test_check_synchronization_with_signatures() {
// Check that a failing signature throws after retry, and that sync changes // Check that a failing signature throws after retry, and that sync changes
// are not applied. // are not applied.
const RESPONSE_ONLY_RECORD4 = { const RESPONSE_ONLY_RECORD4_BAD_SIG = {
comment: "Delete RECORD3, create RECORD4", comment: "Delete RECORD3, create RECORD4",
sampleHeaders: [ sampleHeaders: [
"Content-Type: application/json; charset=UTF-8", "Content-Type: application/json; charset=UTF-8",
@ -667,7 +632,14 @@ add_task(async function test_check_synchronization_with_signatures() {
], ],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: JSON.stringify({ responseBody: JSON.stringify({
data: [ timestamp: 6000,
metadata: {
signature: {
x5u,
signature: "wrong-sig-here-too",
},
},
changes: [
{ {
id: "f765df30-b2f1-42f6-9803-7bd5a07b5098", id: "f765df30-b2f1-42f6-9803-7bd5a07b5098",
last_modified: 6000, last_modified: 6000,
@ -676,19 +648,11 @@ add_task(async function test_check_synchronization_with_signatures() {
}), }),
}; };
const allBadSigResponses = { const allBadSigResponses = {
// In this test, we deliberately serve only a bad signature. "GET:/v1/buckets/main/collections/signed/changeset?_expected=6000&_since=4000": [
"GET:/v1/buckets/main/collections/signed?_expected=6000": [ RESPONSE_EMPTY_NO_UPDATE_BAD_SIG,
RESPONSE_META_BAD_SIG,
], ],
// The first collection state is the three item collection (since "GET:/v1/buckets/main/collections/signed/changeset?_expected=6000": [
// there's a sync with no updates) - but, since the signature is wrong, RESPONSE_ONLY_RECORD4_BAD_SIG,
// another request will be made...
"GET:/v1/buckets/main/collections/signed/records?_expected=6000&_sort=-last_modified&_since=4000": [
RESPONSE_EMPTY_NO_UPDATE,
],
// The next request is for the full collection.
"GET:/v1/buckets/main/collections/signed/records?_expected=6000&_sort=-last_modified": [
RESPONSE_ONLY_RECORD4,
], ],
}; };
@ -747,24 +711,26 @@ add_task(async function test_check_synchronization_with_signatures() {
// //
// Check that we don't apply changes when signature is missing in remote. // Check that we don't apply changes when signature is missing in remote.
const RESPONSE_META_NO_SIG = { const RESPONSE_NO_SIG = {
sampleHeaders: [ sampleHeaders: [
"Content-Type: application/json; charset=UTF-8", "Content-Type: application/json; charset=UTF-8",
`ETag: \"123456\"`, `ETag: \"123456\"`,
], ],
status: { status: 200, statusText: "OK" }, status: { status: 200, statusText: "OK" },
responseBody: JSON.stringify({ responseBody: JSON.stringify({
data: { metadata: {
last_modified: 123456, last_modified: 123456,
}, },
changes: [],
timestamp: 123456,
}), }),
}; };
const missingSigResponses = { const missingSigResponses = {
// In this test, we deliberately serve metadata without the signature attribute. // In this test, we deliberately serve metadata without the signature attribute.
// As if the collection was not signed. // As if the collection was not signed.
"GET:/v1/buckets/main/collections/signed?_expected=6000": [ "GET:/v1/buckets/main/collections/signed/changeset?_expected=6000": [
RESPONSE_META_NO_SIG, RESPONSE_NO_SIG,
], ],
}; };