chore: add e2e tests covering invalidation of other sessions (#2385)

This commit is contained in:
Karol Sójko
2023-08-07 12:01:04 +02:00
committed by GitHub
parent 4b278c4193
commit cedd8e1006
4 changed files with 320 additions and 252 deletions

View File

@@ -0,0 +1,222 @@
import { ItemsEncryptionService } from './ItemsEncryption'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
import {
DecryptedParameters,
EncryptionOperatorsInterface,
KeySystemItemsKey,
StandardException,
} from '@standardnotes/encryption'
import { KeySystemKeyManagerInterface } from '../KeySystem/KeySystemKeyManagerInterface'
import { FindDefaultItemsKey } from './../Encryption/UseCase/ItemsKey/FindDefaultItemsKey'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { EncryptedOutputParameters } from '@standardnotes/encryption'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import {
DecryptedPayloadInterface,
EncryptedPayloadInterface,
ItemsKeyInterface,
KeySystemItemsKeyInterface,
} from '@standardnotes/models'
describe('ItemsEncryptionService', () => {
let itemsEncryptionService: ItemsEncryptionService
let mockItems: jest.Mocked<ItemManagerInterface>
let mockPayloads: jest.Mocked<PayloadManagerInterface>
let mockStorage: jest.Mocked<StorageServiceInterface>
let mockOperators: jest.Mocked<EncryptionOperatorsInterface>
let mockKeys: jest.Mocked<KeySystemKeyManagerInterface>
let mockFindDefaultItemsKey: jest.Mocked<FindDefaultItemsKey>
let mockInternalEventBus: jest.Mocked<InternalEventBusInterface>
beforeEach(() => {
mockItems = {
addObserver: jest.fn(),
} as unknown as jest.Mocked<ItemManagerInterface>
mockPayloads = {} as jest.Mocked<PayloadManagerInterface>
mockStorage = {} as jest.Mocked<StorageServiceInterface>
mockOperators = {} as jest.Mocked<EncryptionOperatorsInterface>
mockKeys = {} as jest.Mocked<KeySystemKeyManagerInterface>
mockFindDefaultItemsKey = {} as jest.Mocked<FindDefaultItemsKey>
mockInternalEventBus = {} as jest.Mocked<InternalEventBusInterface>
itemsEncryptionService = new ItemsEncryptionService(
mockItems,
mockPayloads,
mockStorage,
mockOperators,
mockKeys,
mockFindDefaultItemsKey,
mockInternalEventBus,
)
})
describe('decryptPayloadWithKeyLookup', () => {
it('returns decrypted parameters when a key is found', async () => {
const mockPayload = {
uuid: 'payload-uuid',
} as EncryptedPayloadInterface
const mockKey = {
uuid: 'key-uuid',
} as KeySystemItemsKey
const mockDecryptedParameters = {
uuid: 'decrypted-uuid',
content: {},
} as DecryptedParameters
itemsEncryptionService.keyToUseForDecryptionOfPayload = jest.fn().mockReturnValue(mockKey)
itemsEncryptionService.decryptPayload = jest.fn().mockResolvedValue(mockDecryptedParameters)
const result = await itemsEncryptionService.decryptPayloadWithKeyLookup(mockPayload)
expect(itemsEncryptionService.keyToUseForDecryptionOfPayload).toHaveBeenCalledWith(mockPayload)
expect(itemsEncryptionService.decryptPayload).toHaveBeenCalledWith(mockPayload, mockKey)
expect(result).toEqual(mockDecryptedParameters)
})
it('returns error parameters when no key is found', async () => {
const mockPayload = {
uuid: 'payload-uuid',
} as EncryptedPayloadInterface
itemsEncryptionService.keyToUseForDecryptionOfPayload = jest.fn().mockReturnValue(undefined)
const result = await itemsEncryptionService.decryptPayloadWithKeyLookup(mockPayload)
expect(itemsEncryptionService.keyToUseForDecryptionOfPayload).toHaveBeenCalledWith(mockPayload)
expect(result).toEqual({
uuid: mockPayload.uuid,
errorDecrypting: true,
waitingForKey: true,
})
})
})
describe('keyToUseForDecryptionOfPayload', () => {
it('returns itemsKey when payload has items_key_id', () => {
const mockPayload: EncryptedPayloadInterface = {
items_key_id: 'test-key-id',
} as EncryptedPayloadInterface
const mockItemsKey: ItemsKeyInterface = {} as ItemsKeyInterface
itemsEncryptionService.itemsKeyForEncryptedPayload = jest.fn().mockReturnValue(mockItemsKey)
const result = itemsEncryptionService.keyToUseForDecryptionOfPayload(mockPayload)
expect(itemsEncryptionService.itemsKeyForEncryptedPayload).toHaveBeenCalledWith(mockPayload)
expect(result).toBe(mockItemsKey)
})
it('returns defaultKey when payload does not have items_key_id', () => {
const mockPayload: EncryptedPayloadInterface = {} as EncryptedPayloadInterface
const mockDefaultKey: KeySystemItemsKeyInterface = {} as KeySystemItemsKeyInterface
itemsEncryptionService.defaultItemsKeyForItemVersion = jest.fn().mockReturnValue(mockDefaultKey)
const result = itemsEncryptionService.keyToUseForDecryptionOfPayload(mockPayload)
expect(itemsEncryptionService.defaultItemsKeyForItemVersion).toHaveBeenCalledWith(mockPayload.version)
expect(result).toBe(mockDefaultKey)
})
})
describe('encryptPayloadWithKeyLookup', () => {
it('throws an error when keyToUseForItemEncryption returns an instance of StandardException', async () => {
const mockPayload: DecryptedPayloadInterface = {} as DecryptedPayloadInterface
const mockError: StandardException = new StandardException('test-error')
itemsEncryptionService.keyToUseForItemEncryption = jest.fn().mockReturnValue(mockError)
await expect(() => itemsEncryptionService.encryptPayloadWithKeyLookup(mockPayload)).rejects.toThrow('test-error')
})
it('encrypts the payload using the provided key', async () => {
const mockPayload: DecryptedPayloadInterface = {} as DecryptedPayloadInterface
const mockKey: ItemsKeyInterface = {} as ItemsKeyInterface
const encryptedOutputParameters: EncryptedOutputParameters = {
content: 'encrypted-content',
} as EncryptedOutputParameters
itemsEncryptionService.keyToUseForItemEncryption = jest.fn().mockReturnValue(mockKey)
itemsEncryptionService.encryptPayload = jest.fn().mockResolvedValue(encryptedOutputParameters)
const result = await itemsEncryptionService.encryptPayloadWithKeyLookup(mockPayload)
expect(itemsEncryptionService.encryptPayload).toHaveBeenCalledWith(mockPayload, mockKey, undefined)
expect(result).toBe(encryptedOutputParameters)
})
})
describe('encryptPayload', () => {
it('throws an error when the payload has no content', async () => {
const mockPayload: DecryptedPayloadInterface = { content: null } as unknown as DecryptedPayloadInterface
const mockKey: ItemsKeyInterface = {} as ItemsKeyInterface
await expect(() => itemsEncryptionService.encryptPayload(mockPayload, mockKey)).rejects.toThrow(
'Attempting to encrypt payload with no content.',
)
})
it('throws an error when the payload has no UUID', async () => {
const mockPayload: DecryptedPayloadInterface = { uuid: null, content: {} } as unknown as DecryptedPayloadInterface
const mockKey: ItemsKeyInterface = {} as ItemsKeyInterface
await expect(() => itemsEncryptionService.encryptPayload(mockPayload, mockKey)).rejects.toThrow(
'Attempting to encrypt payload with no UuidGenerator.',
)
})
it('returns encrypted output parameters', async () => {
const mockPayload: DecryptedPayloadInterface = {
uuid: 'test-uuid',
content: 'test-content',
} as unknown as DecryptedPayloadInterface
const mockKey: ItemsKeyInterface = {} as ItemsKeyInterface
const encryptedOutputParameters: EncryptedOutputParameters = {
content: 'encrypted-content',
} as EncryptedOutputParameters
jest.spyOn(itemsEncryptionService, 'encryptPayload').mockResolvedValue(encryptedOutputParameters)
const result = await itemsEncryptionService.encryptPayload(mockPayload, mockKey)
expect(result).toBe(encryptedOutputParameters)
})
})
it('repersistAllItems', async () => {
Object.defineProperty(mockItems, 'items', {
get: jest.fn().mockReturnValue([{ payload: { uuid: '123' } }]),
})
mockStorage.savePayloads = jest.fn().mockResolvedValue(undefined)
await itemsEncryptionService.repersistAllItems()
expect(mockStorage.savePayloads).toHaveBeenCalledWith([{ uuid: '123' }])
})
it('encryptPayloadWithKeyLookup', async () => {
const mockPayload: DecryptedPayloadInterface = {
key_system_identifier: 'test-identifier',
} as DecryptedPayloadInterface
const mockKeyPair: PkcKeyPair = { publicKey: 'publicKey', privateKey: 'privateKey' }
const mockKey = { uuid: 'key-id', keyVersion: '004' } as KeySystemItemsKey
mockKeys.getPrimaryKeySystemItemsKey = jest.fn().mockReturnValue(mockKey)
itemsEncryptionService.encryptPayload = jest
.fn()
.mockResolvedValue({ content: '004:...' } as EncryptedOutputParameters)
const result = await itemsEncryptionService.encryptPayloadWithKeyLookup(mockPayload, mockKeyPair)
expect(mockKeys.getPrimaryKeySystemItemsKey).toHaveBeenCalledWith(mockPayload.key_system_identifier)
expect(itemsEncryptionService.encryptPayload).toHaveBeenCalledWith(mockPayload, mockKey, mockKeyPair)
expect(result).toEqual({ content: '004:...' })
})
})

View File

@@ -97,7 +97,7 @@ export class ItemsEncryptionService extends AbstractService {
return this._findDefaultItemsKey.execute(this.getItemsKeys()).getValue()
}
private keyToUseForItemEncryption(
keyToUseForItemEncryption(
payload: DecryptedPayloadInterface,
): ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | StandardException {
if (payload.key_system_identifier) {
@@ -132,7 +132,7 @@ export class ItemsEncryptionService extends AbstractService {
return result
}
private keyToUseForDecryptionOfPayload(
keyToUseForDecryptionOfPayload(
payload: EncryptedPayloadInterface,
): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined {
if (payload.items_key_id) {

View File

@@ -198,7 +198,7 @@ export class AppContext {
const responses = []
const accountPassword = this.passwordToUseForAccountPasswordChallenge || this.password
const accountPassword = this.password
for (const prompt of challenge.prompts) {
if (prompt.validation === ChallengeValidation.LocalPasscode) {
@@ -300,6 +300,17 @@ export class AppContext {
})
}
awaitNextSyncError() {
return new Promise((resolve) => {
const removeObserver = this.application.sync.addEventObserver((event, data) => {
if (event === SyncEvent.SyncError) {
removeObserver()
resolve(data)
}
})
})
}
awaitNextSyncEvent(eventName) {
return new Promise((resolve) => {
const removeObserver = this.application.sync.addEventObserver((event, data) => {
@@ -322,10 +333,30 @@ export class AppContext {
})
}
async restoreSession(latestPassword) {
const promise = this.resolveWhenSessionIsReauthenticated()
this.password = latestPassword
await this.sync()
await promise
await this.sync()
}
resolveWhenSessionIsReauthenticated() {
return new Promise((resolve) => {
const removeObserver = this.application.sessions.addEventObserver((event, data) => {
if (event === SessionEvent.Restored) {
removeObserver()
resolve()
}
})
})
}
resolveWithUploadedPayloads() {
return new Promise((resolve) => {
this.application.sync.addEventObserver((event, data) => {
const removeObserver = this.application.sync.addEventObserver((event, data) => {
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
removeObserver()
resolve(data.uploadedPayloads)
}
})
@@ -334,8 +365,9 @@ export class AppContext {
resolveWithSyncRetrievedPayloads() {
return new Promise((resolve) => {
this.application.sync.addEventObserver((event, data) => {
const removeObserver = this.application.sync.addEventObserver((event, data) => {
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
removeObserver()
resolve(data.retrievedPayloads)
}
})
@@ -344,8 +376,9 @@ export class AppContext {
resolveWithConflicts() {
return new Promise((resolve) => {
this.application.sync.addEventObserver((event, response) => {
const removeObserver = this.application.sync.addEventObserver((event, response) => {
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
removeObserver()
resolve(response.rawConflictObjects)
}
})
@@ -354,10 +387,11 @@ export class AppContext {
resolveWhenSavedSyncPayloadsIncludesItemUuid(uuid) {
return new Promise((resolve) => {
this.application.sync.addEventObserver((event, response) => {
const removeObserver = this.application.sync.addEventObserver((event, response) => {
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
const savedPayload = response.savedPayloads.find((payload) => payload.uuid === uuid)
if (savedPayload) {
removeObserver()
resolve()
}
}
@@ -367,10 +401,11 @@ export class AppContext {
resolveWhenSavedSyncPayloadsIncludesItemThatIsDuplicatedOf(uuid) {
return new Promise((resolve) => {
this.application.sync.addEventObserver((event, response) => {
const removeObserver = this.application.sync.addEventObserver((event, response) => {
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
const savedPayload = response.savedPayloads.find((payload) => payload.duplicate_of === uuid)
if (savedPayload) {
removeObserver()
resolve()
}
}
@@ -513,6 +548,12 @@ export class AppContext {
this.password = newPassword
}
async changeEmail(newEmail) {
await this.application.changeEmail(newEmail, this.password, this.passcode)
this.email = newEmail
}
findItem(uuid) {
return this.application.items.findItem(uuid)
}
@@ -535,26 +576,6 @@ export class AppContext {
}
}
disableKeyRecoveryServerSignIn() {
this.keyRecovery.performServerSignIn = () => {
console.warn('application.keyRecovery.performServerSignIn has been stubbed with an empty implementation')
}
}
preventKeyRecoveryOfKeys(ids) {
const originalImpl = this.keyRecovery.handleUndecryptableItemsKeys
this.keyRecovery.handleUndecryptableItemsKeys = function (keys) {
const filtered = keys.filter((k) => !ids.includes(k.uuid))
originalImpl.apply(this, [filtered])
}
}
respondToAccountPasswordChallengeWith(password) {
this.passwordToUseForAccountPasswordChallenge = password
}
spyOnChangedItems(callback) {
this.application.items.addObserver(ContentType.TYPES.Any, ({ changed, unerrored }) => {
callback([...changed, ...unerrored])

View File

@@ -1,12 +1,10 @@
import * as Factory from './lib/factory.js'
describe.skip('session invalidation tests to revisit', function () {
this.timeout(Factory.TwentySecondTimeout)
chai.use(chaiAsPromised)
const expect = chai.expect
const syncOptions = {
checkIntegrity: true,
awaitAll: true,
}
describe('session invalidation', function () {
this.timeout(Factory.TwentySecondTimeout)
beforeEach(function () {
localStorage.clear()
@@ -16,254 +14,81 @@ describe.skip('session invalidation tests to revisit', function () {
localStorage.clear()
})
it('recovered keys with key params not matching servers should not be synced if local root key does not match server', async function () {
/**
* Assume Application A has been through these states:
* 1. Registration + Items Key A + Root Key A
* 2. Password change + Items Key B + Root Key B
* 3. Password change + Items Key C + Root Key C + Failure to correctly re-encrypt Items Key A and B with Root Key C
*
* Application B is not correctly in sync, and is only at State 1 (Registration + Items Key A)
*
* Application B receives Items Key B of Root Key B but for whatever reason ignores Items Key C of Root Key C.
*
* When it recovers Items Key B, it should not re-upload it to the server, because Application B's Root Key is not
* the current account's root key.
*/
it('changing password on one client should invalidate other sessions', async function () {
const contextA = await Factory.createAppContextWithFakeCrypto()
await contextA.launch()
await contextA.register()
contextA.preventKeyRecoveryOfKeys()
const contextB = await Factory.createAppContextWithFakeCrypto('app-b', contextA.email, contextA.password)
const contextB = await Factory.createAppContextWithFakeCrypto('other', contextA.email, contextA.password)
await contextB.launch()
await contextB.signIn()
await contextA.changePassword('new-password-1')
const itemsKeyARootKeyB = contextA.itemsKeys[0]
const itemsKeyBRootKeyB = contextA.itemsKeys[1]
await contextB.changePassword('new-password')
const note = await contextB.createSyncedNote()
contextA.disableSyncingOfItems([itemsKeyARootKeyB.uuid, itemsKeyBRootKeyB.uuid])
await contextA.changePassword('new-password-2')
const itemsKeyCRootKeyC = contextA.itemsKeys[2]
contextA.ignoreChallenges()
contextB.disableKeyRecoveryServerSignIn()
contextB.preventKeyRecoveryOfKeys([itemsKeyCRootKeyC.uuid])
contextB.respondToAccountPasswordChallengeWith('new-password-1')
const errorPromise = contextA.awaitNextSyncError()
await contextA.sync()
const error = await errorPromise
const recoveryPromise = Promise.all([
contextB.resolveWhenKeyRecovered(itemsKeyARootKeyB.uuid),
contextB.resolveWhenKeyRecovered(itemsKeyBRootKeyB.uuid),
])
const observedDirtyItemUuids = []
contextB.spyOnChangedItems((changed) => {
const dirty = changed.filter((i) => i.dirty)
extendArray(observedDirtyItemUuids, Uuids(dirty))
})
await contextB.sync()
await recoveryPromise
expect(observedDirtyItemUuids.includes(itemsKeyARootKeyB.uuid)).to.equal(false)
expect(observedDirtyItemUuids.includes(itemsKeyBRootKeyB.uuid)).to.equal(false)
expect(error).to.be.ok
expect(contextA.items.findItem(note.uuid)).to.not.be.ok
await contextA.deinit()
await contextB.deinit()
})
it('when changing password on client B, client A should perform recovery flow', async function () {
it('changing email on one client should invalidate other sessions', async function () {
const contextA = await Factory.createAppContextWithFakeCrypto()
await contextA.launch()
await contextA.register()
const contextB = await Factory.createAppContextWithFakeCrypto('other', contextA.email, contextA.password)
await contextB.launch()
await contextB.signIn()
await contextB.changeEmail('new-email')
const note = await contextB.createSyncedNote()
contextA.ignoreChallenges()
const errorPromise = contextA.awaitNextSyncError()
await contextA.sync()
const error = await errorPromise
expect(error).to.be.ok
expect(contextA.items.findItem(note.uuid)).to.not.be.ok
await contextA.deinit()
await contextB.deinit()
})
it('should restore session on second client by reauthenticating', async function () {
const contextA = await Factory.createAppContextWithFakeCrypto()
await contextA.launch()
await contextA.register()
const originalItemsKey = contextA.application.items.getDisplayableItemsKeys()[0]
const contextB = await Factory.createAppContextWithFakeCrypto(
'another-namespace',
new Date().toDateString(),
contextA.email,
contextA.password,
)
contextB.ignoreChallenges()
await contextB.launch()
await contextB.signIn()
const newPassword = `${Math.random()}`
await contextB.changePassword('new-password')
const note = await contextB.createSyncedNote('foo', 'bar')
const result = await contextB.application.changePassword(contextA.password, newPassword)
await contextA.restoreSession('new-password')
expect(result.error).to.not.be.ok
expect(contextB.application.items.getAnyItems(ContentType.TYPES.ItemsKey).length).to.equal(2)
const newItemsKey = contextB.application.items
.getDisplayableItemsKeys()
.find((k) => k.uuid !== originalItemsKey.uuid)
const note = await Factory.createSyncedNote(contextB.application)
const recoveryPromise = contextA.resolveWhenKeyRecovered(newItemsKey.uuid)
contextA.password = newPassword
await contextA.sync(syncOptions)
await recoveryPromise
/** Same previously errored key should now no longer be errored, */
expect(contextA.application.items.getAnyItems(ContentType.TYPES.ItemsKey).length).to.equal(2)
for (const key of contextA.application.items.getDisplayableItemsKeys()) {
expect(key.errorDecrypting).to.not.be.ok
}
const aKey = await contextA.application.encryption.getRootKey()
const bKey = await contextB.application.encryption.getRootKey()
expect(aKey.compare(bKey)).to.equal(true)
expect(contextA.application.items.findItem(note.uuid).errorDecrypting).to.not.be.ok
expect(contextB.application.items.findItem(note.uuid).errorDecrypting).to.not.be.ok
expect(contextA.application.sync.isOutOfSync()).to.equal(false)
expect(contextB.application.sync.isOutOfSync()).to.equal(false)
const contextANote = contextA.items.findItem(note.uuid)
expect(contextANote).to.be.ok
expect(contextANote.errorDecrypting).to.not.be.ok
expect(contextANote.title).to.equal('foo')
expect(contextANote.text).to.equal('bar')
await contextA.deinit()
await contextB.deinit()
}).timeout(80000)
it('when items key associated with item is errored, item should be marked waiting for key', async function () {
const namespace = Factory.randomString()
const newPassword = `${Math.random()}`
const contextA = await Factory.createAppContextWithFakeCrypto(namespace)
const appA = contextA.application
await appA.prepareForLaunch({ receiveChallenge: () => {} })
await appA.launch(true)
await Factory.registerUserToApplication({
application: appA,
email: contextA.email,
password: contextA.password,
})
expect(appA.items.getItems(ContentType.TYPES.ItemsKey).length).to.equal(1)
/** Create simultaneous appB signed into same account */
const appB = await Factory.createApplicationWithFakeCrypto('another-namespace')
await appB.prepareForLaunch({ receiveChallenge: () => {} })
await appB.launch(true)
await Factory.loginToApplication({
application: appB,
email: contextA.email,
password: contextA.password,
})
/** Change password on appB */
await appB.changePassword(contextA.password, newPassword)
const note = await Factory.createSyncedNote(appB)
await appB.sync.sync()
/** We expect the item in appA to be errored at this point, but we do not want it to recover */
await appA.sync.sync()
expect(appA.payloads.findOne(note.uuid).waitingForKey).to.equal(true)
console.warn('Expecting exceptions below as we destroy app during key recovery')
await Factory.safeDeinit(appA)
await Factory.safeDeinit(appB)
const recreatedAppA = await Factory.createApplicationWithFakeCrypto(namespace)
await recreatedAppA.prepareForLaunch({ receiveChallenge: () => {} })
await recreatedAppA.launch(true)
expect(recreatedAppA.payloads.findOne(note.uuid).errorDecrypting).to.equal(true)
expect(recreatedAppA.payloads.findOne(note.uuid).waitingForKey).to.equal(true)
await Factory.safeDeinit(recreatedAppA)
})
it('errored second client should not upload its items keys', async function () {
/**
* The original source of this issue was that when changing password on client A and syncing with B,
* the newly encrypted items key retrieved on B would be included as "ignored", so its timestamps
* would not be emitted, and thus the application would be in sync. The app would then download
* the items key independently, and make duplicates erroneously.
*/
const contextA = this.context
const email = Utils.generateUuid()
const password = Utils.generateUuid()
await Factory.registerUserToApplication({
application: contextA.application,
email,
password: password,
})
const contextB = await Factory.createAppContext({ email, password })
await contextB.launch()
await contextB.signIn()
contextA.ignoreChallenges()
contextB.ignoreChallenges()
const newPassword = Utils.generateUuid()
await contextA.application.user.changeCredentials({
currentPassword: password,
newPassword: newPassword,
origination: KeyParamsOrigination.PasswordChange,
})
await contextB.syncWithIntegrityCheck()
await contextA.syncWithIntegrityCheck()
const clientAUndecryptables = contextA.keyRecovery.getUndecryptables()
const clientBUndecryptables = contextB.keyRecovery.getUndecryptables()
expect(Object.keys(clientBUndecryptables).length).to.equal(1)
expect(Object.keys(clientAUndecryptables).length).to.equal(0)
await contextB.deinit()
})
it('changing password on one client should not invalidate other sessions', async function () {
await Factory.registerUserToApplication({
application: this.application,
email: this.email,
password: this.password,
})
const appA = await Factory.createApplicationWithFakeCrypto(Factory.randomString())
await appA.prepareForLaunch({})
await appA.launch(true)
const email = `${Math.random()}`
const password = `${Math.random()}`
await Factory.registerUserToApplication({
application: appA,
email: email,
password: password,
})
/** Create simultaneous appB signed into same account */
const appB = await Factory.createApplicationWithFakeCrypto('another-namespace')
await appB.prepareForLaunch({})
await appB.launch(true)
await Factory.loginToApplication({
application: appB,
email: email,
password: password,
})
/** Change password on appB */
const newPassword = 'random'
await appB.changePassword(password, newPassword)
/** Create an item and sync it */
const note = await Factory.createSyncedNote(appB)
/** Expect appA session to still be valid */
await appA.sync.sync()
expect(appA.items.findItem(note.uuid)).to.be.ok
await Factory.safeDeinit(appA)
await Factory.safeDeinit(appB)
})
})