Extra tests for SignalProtocolStore migration
This commit is contained in:
parent
ce1daef9f3
commit
039bd072ed
2 changed files with 635 additions and 24 deletions
522
ts/test-node/sql_migrations_test.ts
Normal file
522
ts/test-node/sql_migrations_test.ts
Normal file
|
@ -0,0 +1,522 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import SQL, { Database } from 'better-sqlite3';
|
||||
import { v4 as generateGuid } from 'uuid';
|
||||
|
||||
import { SCHEMA_VERSIONS } from '../sql/Server';
|
||||
|
||||
const THEIR_UUID = generateGuid();
|
||||
const THEIR_CONVO = generateGuid();
|
||||
const ANOTHER_CONVO = generateGuid();
|
||||
const THIRD_CONVO = generateGuid();
|
||||
const OUR_UUID = generateGuid();
|
||||
|
||||
describe('SQL migrations test', () => {
|
||||
let db: Database;
|
||||
|
||||
const updateToVersion = (version: number) => {
|
||||
const startVersion = db.pragma('user_version', { simple: true });
|
||||
|
||||
for (const run of SCHEMA_VERSIONS) {
|
||||
run(startVersion, db);
|
||||
|
||||
const currentVersion = db.pragma('user_version', { simple: true });
|
||||
|
||||
if (currentVersion === version) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Migration to ${version} not found`);
|
||||
};
|
||||
|
||||
const addOurUuid = () => {
|
||||
const value = {
|
||||
id: 'uuid_id',
|
||||
value: `${OUR_UUID}.1`,
|
||||
};
|
||||
db.exec(
|
||||
`
|
||||
INSERT INTO items (id, json) VALUES
|
||||
('uuid_id', '${JSON.stringify(value)}');
|
||||
`
|
||||
);
|
||||
};
|
||||
|
||||
const parseItems = (
|
||||
items: ReadonlyArray<{ json: string }>
|
||||
): Array<unknown> => {
|
||||
return items.map(item => {
|
||||
return {
|
||||
...item,
|
||||
json: JSON.parse(item.json),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const insertSession = (
|
||||
conversationId: string,
|
||||
deviceId: number,
|
||||
data: Record<string, unknown> = {}
|
||||
): void => {
|
||||
const id = `${conversationId}.${deviceId}`;
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO sessions (id, conversationId, json)
|
||||
VALUES ($id, $conversationId, $json)
|
||||
`
|
||||
).run({
|
||||
id,
|
||||
conversationId,
|
||||
json: JSON.stringify({
|
||||
...data,
|
||||
id,
|
||||
conversationId,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
db = new SQL(':memory:');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
describe('updateToSchemaVersion41', () => {
|
||||
it('clears sessions and keys if UUID is not available', () => {
|
||||
updateToVersion(40);
|
||||
|
||||
db.exec(
|
||||
`
|
||||
INSERT INTO senderKeys
|
||||
(id, senderId, distributionId, data, lastUpdatedDate)
|
||||
VALUES
|
||||
('1', '1', '1', '1', 1);
|
||||
INSERT INTO sessions (id, conversationId, json) VALUES
|
||||
('1', '1', '{}');
|
||||
INSERT INTO signedPreKeys (id, json) VALUES
|
||||
('1', '{}');
|
||||
INSERT INTO preKeys (id, json) VALUES
|
||||
('1', '{}');
|
||||
INSERT INTO items (id, json) VALUES
|
||||
('identityKey', '{}'),
|
||||
('registrationId', '{}');
|
||||
`
|
||||
);
|
||||
|
||||
const senderKeyCount = db
|
||||
.prepare('SELECT COUNT(*) FROM senderKeys')
|
||||
.pluck();
|
||||
const sessionCount = db.prepare('SELECT COUNT(*) FROM sessions').pluck();
|
||||
const signedPreKeyCount = db
|
||||
.prepare('SELECT COUNT(*) FROM signedPreKeys')
|
||||
.pluck();
|
||||
const preKeyCount = db.prepare('SELECT COUNT(*) FROM preKeys').pluck();
|
||||
const itemCount = db.prepare('SELECT COUNT(*) FROM items').pluck();
|
||||
|
||||
assert.strictEqual(senderKeyCount.get(), 1);
|
||||
assert.strictEqual(sessionCount.get(), 1);
|
||||
assert.strictEqual(signedPreKeyCount.get(), 1);
|
||||
assert.strictEqual(preKeyCount.get(), 1);
|
||||
assert.strictEqual(itemCount.get(), 2);
|
||||
|
||||
updateToVersion(41);
|
||||
|
||||
assert.strictEqual(senderKeyCount.get(), 0);
|
||||
assert.strictEqual(sessionCount.get(), 0);
|
||||
assert.strictEqual(signedPreKeyCount.get(), 0);
|
||||
assert.strictEqual(preKeyCount.get(), 0);
|
||||
assert.strictEqual(itemCount.get(), 0);
|
||||
});
|
||||
|
||||
it('adds prefix to preKeys/signedPreKeys', () => {
|
||||
updateToVersion(40);
|
||||
|
||||
addOurUuid();
|
||||
|
||||
const signedKeyItem = { id: 1 };
|
||||
const preKeyItem = { id: 2 };
|
||||
|
||||
db.exec(
|
||||
`
|
||||
INSERT INTO signedPreKeys (id, json) VALUES
|
||||
(1, '${JSON.stringify(signedKeyItem)}');
|
||||
INSERT INTO preKeys (id, json) VALUES
|
||||
(2, '${JSON.stringify(preKeyItem)}');
|
||||
`
|
||||
);
|
||||
|
||||
updateToVersion(41);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
parseItems(db.prepare('SELECT * FROM signedPreKeys').all()),
|
||||
[
|
||||
{
|
||||
id: `${OUR_UUID}:1`,
|
||||
json: {
|
||||
id: `${OUR_UUID}:1`,
|
||||
keyId: 1,
|
||||
ourUuid: OUR_UUID,
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
parseItems(db.prepare('SELECT * FROM preKeys').all()),
|
||||
[
|
||||
{
|
||||
id: `${OUR_UUID}:2`,
|
||||
json: {
|
||||
id: `${OUR_UUID}:2`,
|
||||
keyId: 2,
|
||||
ourUuid: OUR_UUID,
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('migrates senderKeys', () => {
|
||||
updateToVersion(40);
|
||||
|
||||
addOurUuid();
|
||||
|
||||
db.exec(
|
||||
`
|
||||
INSERT INTO conversations (id, uuid) VALUES
|
||||
('${THEIR_CONVO}', '${THEIR_UUID}');
|
||||
|
||||
INSERT INTO senderKeys
|
||||
(id, senderId, distributionId, data, lastUpdatedDate)
|
||||
VALUES
|
||||
('${THEIR_CONVO}.1--234', '${THEIR_CONVO}.1', '234', '1', 1);
|
||||
`
|
||||
);
|
||||
|
||||
updateToVersion(41);
|
||||
|
||||
assert.deepStrictEqual(db.prepare('SELECT * FROM senderKeys').all(), [
|
||||
{
|
||||
id: `${OUR_UUID}:${THEIR_UUID}.1--234`,
|
||||
distributionId: '234',
|
||||
data: '1',
|
||||
lastUpdatedDate: 1,
|
||||
senderId: `${THEIR_UUID}.1`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes senderKeys that do not have conversation uuid', () => {
|
||||
updateToVersion(40);
|
||||
|
||||
addOurUuid();
|
||||
|
||||
db.exec(
|
||||
`
|
||||
INSERT INTO conversations (id) VALUES
|
||||
('${THEIR_CONVO}');
|
||||
|
||||
INSERT INTO senderKeys
|
||||
(id, senderId, distributionId, data, lastUpdatedDate)
|
||||
VALUES
|
||||
('${THEIR_CONVO}.1--234', '${THEIR_CONVO}.1', '234', '1', 1),
|
||||
('${ANOTHER_CONVO}.1--234', '${ANOTHER_CONVO}.1', '234', '1', 1);
|
||||
`
|
||||
);
|
||||
|
||||
updateToVersion(41);
|
||||
|
||||
assert.strictEqual(
|
||||
db.prepare('SELECT COUNT(*) FROM senderKeys').pluck().get(),
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('correctly merges senderKeys for conflicting conversations', () => {
|
||||
updateToVersion(40);
|
||||
|
||||
addOurUuid();
|
||||
|
||||
const fullA = generateGuid();
|
||||
const fullB = generateGuid();
|
||||
const fullC = generateGuid();
|
||||
const partial = generateGuid();
|
||||
|
||||
// When merging two keys for different conversations with the same uuid
|
||||
// only the most recent key would be kept in the database. We prefer keys
|
||||
// with either:
|
||||
//
|
||||
// 1. more recent lastUpdatedDate column
|
||||
// 2. conversation with both e164 and uuid
|
||||
// 3. conversation with more recent active_at
|
||||
db.exec(
|
||||
`
|
||||
INSERT INTO conversations (id, uuid, e164, active_at) VALUES
|
||||
('${fullA}', '${THEIR_UUID}', '+12125555555', 1),
|
||||
('${fullB}', '${THEIR_UUID}', '+12125555555', 2),
|
||||
('${fullC}', '${THEIR_UUID}', '+12125555555', 3),
|
||||
('${partial}', '${THEIR_UUID}', NULL, 3);
|
||||
|
||||
INSERT INTO senderKeys
|
||||
(id, senderId, distributionId, data, lastUpdatedDate)
|
||||
VALUES
|
||||
('${fullA}.1--234', '${fullA}.1', 'fullA', '1', 1),
|
||||
('${fullC}.1--234', '${fullC}.1', 'fullC', '2', 2),
|
||||
('${fullB}.1--234', '${fullB}.1', 'fullB', '3', 2),
|
||||
('${partial}.1--234', '${partial}.1', 'partial', '4', 2);
|
||||
`
|
||||
);
|
||||
|
||||
updateToVersion(41);
|
||||
|
||||
assert.deepStrictEqual(db.prepare('SELECT * FROM senderKeys').all(), [
|
||||
{
|
||||
id: `${OUR_UUID}:${THEIR_UUID}.1--234`,
|
||||
senderId: `${THEIR_UUID}.1`,
|
||||
distributionId: 'fullC',
|
||||
lastUpdatedDate: 2,
|
||||
data: '2',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('migrates sessions', () => {
|
||||
updateToVersion(40);
|
||||
|
||||
addOurUuid();
|
||||
|
||||
db.exec(
|
||||
`
|
||||
INSERT INTO conversations (id, uuid) VALUES
|
||||
('${THEIR_CONVO}', '${THEIR_UUID}');
|
||||
`
|
||||
);
|
||||
|
||||
insertSession(THEIR_CONVO, 1);
|
||||
|
||||
updateToVersion(41);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
parseItems(db.prepare('SELECT * FROM sessions').all()),
|
||||
[
|
||||
{
|
||||
conversationId: THEIR_CONVO,
|
||||
id: `${OUR_UUID}:${THEIR_UUID}.1`,
|
||||
uuid: THEIR_UUID,
|
||||
ourUuid: OUR_UUID,
|
||||
json: {
|
||||
id: `${OUR_UUID}:${THEIR_UUID}.1`,
|
||||
conversationId: THEIR_CONVO,
|
||||
uuid: THEIR_UUID,
|
||||
ourUuid: OUR_UUID,
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('removes sessions that do not have conversation id', () => {
|
||||
updateToVersion(40);
|
||||
|
||||
addOurUuid();
|
||||
|
||||
insertSession(THEIR_CONVO, 1);
|
||||
|
||||
updateToVersion(41);
|
||||
|
||||
assert.strictEqual(
|
||||
db.prepare('SELECT COUNT(*) FROM sessions').pluck().get(),
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('removes sessions that do not have conversation uuid', () => {
|
||||
updateToVersion(40);
|
||||
|
||||
addOurUuid();
|
||||
|
||||
db.exec(
|
||||
`
|
||||
INSERT INTO conversations (id) VALUES ('${THEIR_CONVO}');
|
||||
`
|
||||
);
|
||||
|
||||
insertSession(THEIR_CONVO, 1);
|
||||
|
||||
updateToVersion(41);
|
||||
|
||||
assert.strictEqual(
|
||||
db.prepare('SELECT COUNT(*) FROM sessions').pluck().get(),
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('correctly merges sessions for conflicting conversations', () => {
|
||||
updateToVersion(40);
|
||||
|
||||
addOurUuid();
|
||||
|
||||
const fullA = generateGuid();
|
||||
const fullB = generateGuid();
|
||||
const partial = generateGuid();
|
||||
|
||||
// Similar merging logic to senderkeys above. We prefer sessions with
|
||||
// either:
|
||||
//
|
||||
// 1. conversation with both e164 and uuid
|
||||
// 2. conversation with more recent active_at
|
||||
db.exec(
|
||||
`
|
||||
INSERT INTO conversations (id, uuid, e164, active_at) VALUES
|
||||
('${fullA}', '${THEIR_UUID}', '+12125555555', 1),
|
||||
('${fullB}', '${THEIR_UUID}', '+12125555555', 2),
|
||||
('${partial}', '${THEIR_UUID}', NULL, 3);
|
||||
`
|
||||
);
|
||||
|
||||
insertSession(fullA, 1, { name: 'A' });
|
||||
insertSession(fullB, 1, { name: 'B' });
|
||||
insertSession(partial, 1, { name: 'C' });
|
||||
|
||||
updateToVersion(41);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
parseItems(db.prepare('SELECT * FROM sessions').all()),
|
||||
[
|
||||
{
|
||||
id: `${OUR_UUID}:${THEIR_UUID}.1`,
|
||||
conversationId: fullB,
|
||||
ourUuid: OUR_UUID,
|
||||
uuid: THEIR_UUID,
|
||||
json: {
|
||||
id: `${OUR_UUID}:${THEIR_UUID}.1`,
|
||||
conversationId: fullB,
|
||||
ourUuid: OUR_UUID,
|
||||
uuid: THEIR_UUID,
|
||||
name: 'B',
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('moves identity key and registration id into a map', () => {
|
||||
updateToVersion(40);
|
||||
|
||||
addOurUuid();
|
||||
|
||||
const items = [
|
||||
{ id: 'identityKey', value: 'secret' },
|
||||
{ id: 'registrationId', value: 42 },
|
||||
];
|
||||
|
||||
for (const item of items) {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO items (id, json) VALUES ($id, $json);
|
||||
`
|
||||
).run({
|
||||
id: item.id,
|
||||
json: JSON.stringify(item),
|
||||
});
|
||||
}
|
||||
|
||||
updateToVersion(41);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
parseItems(db.prepare('SELECT * FROM items ORDER BY id').all()),
|
||||
[
|
||||
{
|
||||
id: 'identityKeyMap',
|
||||
json: {
|
||||
id: 'identityKeyMap',
|
||||
value: { [OUR_UUID]: 'secret' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'registrationIdMap',
|
||||
json: {
|
||||
id: 'registrationIdMap',
|
||||
value: { [OUR_UUID]: 42 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'uuid_id',
|
||||
json: {
|
||||
id: 'uuid_id',
|
||||
value: `${OUR_UUID}.1`,
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it("migrates other users' identity keys", () => {
|
||||
updateToVersion(40);
|
||||
|
||||
addOurUuid();
|
||||
|
||||
db.exec(
|
||||
`
|
||||
INSERT INTO conversations (id, uuid) VALUES
|
||||
('${THEIR_CONVO}', '${THEIR_UUID}'),
|
||||
('${ANOTHER_CONVO}', NULL);
|
||||
`
|
||||
);
|
||||
|
||||
const identityKeys = [
|
||||
{ id: THEIR_CONVO },
|
||||
{ id: ANOTHER_CONVO },
|
||||
{ id: THIRD_CONVO },
|
||||
];
|
||||
for (const key of identityKeys) {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO identityKeys (id, json) VALUES ($id, $json);
|
||||
`
|
||||
).run({
|
||||
id: key.id,
|
||||
json: JSON.stringify(key),
|
||||
});
|
||||
}
|
||||
|
||||
updateToVersion(41);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
parseItems(db.prepare('SELECT * FROM identityKeys ORDER BY id').all()),
|
||||
[
|
||||
{
|
||||
id: THEIR_UUID,
|
||||
json: {
|
||||
id: THEIR_UUID,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: `conversation:${ANOTHER_CONVO}`,
|
||||
json: {
|
||||
id: `conversation:${ANOTHER_CONVO}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: `conversation:${THIRD_CONVO}`,
|
||||
json: {
|
||||
id: `conversation:${THIRD_CONVO}`,
|
||||
},
|
||||
},
|
||||
].sort((a, b) => {
|
||||
if (a.id === b.id) {
|
||||
return 0;
|
||||
}
|
||||
if (a.id < b.id) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue