internal: incomplete vault systems behind feature flag (#2340)

This commit is contained in:
Mo
2023-06-30 09:01:56 -05:00
committed by GitHub
parent d16e401bb9
commit b032eb9c9b
638 changed files with 20321 additions and 4813 deletions

View File

@@ -22,7 +22,7 @@ describe('000 legacy protocol operations', () => {
let error
try {
protocol004.generateDecryptedParametersSync({
protocol004.generateDecryptedParameters({
uuid: 'foo',
content: string,
content_type: 'foo',

View File

@@ -7,16 +7,16 @@ const expect = chai.expect
describe('004 protocol operations', function () {
const _identifier = 'hello@test.com'
const _password = 'password'
let _keyParams
let _key
let rootKeyParams
let rootKey
const application = Factory.createApplicationWithRealCrypto()
const protocol004 = new SNProtocolOperator004(new SNWebCrypto())
before(async function () {
await Factory.initializeApplication(application)
_key = await protocol004.createRootKey(_identifier, _password, KeyParamsOrigination.Registration)
_keyParams = _key.keyParams
rootKey = await protocol004.createRootKey(_identifier, _password, KeyParamsOrigination.Registration)
rootKeyParams = rootKey.keyParams
})
after(async function () {
@@ -69,43 +69,58 @@ describe('004 protocol operations', function () {
})
it('properly encrypts and decrypts', async function () {
const text = 'hello world'
const rawKey = _key.masterKey
const nonce = await application.protocolService.crypto.generateRandomKey(192)
const payload = new DecryptedPayload({
uuid: '123',
content_type: ContentType.ItemsKey,
content: FillItemContent({
title: 'foo',
text: 'bar',
}),
})
const operator = application.protocolService.operatorManager.operatorForVersion(ProtocolVersion.V004)
const authenticatedData = { foo: 'bar' }
const encString = await operator.encryptString004(text, rawKey, nonce, authenticatedData)
const decString = await operator.decryptString004(
encString,
rawKey,
nonce,
await operator.authenticatedDataToString(authenticatedData),
)
expect(decString).to.equal(text)
const encrypted = await operator.generateEncryptedParameters(payload, rootKey)
const decrypted = await operator.generateDecryptedParameters(encrypted, rootKey)
expect(decrypted.content.title).to.equal('foo')
expect(decrypted.content.text).to.equal('bar')
})
it('fails to decrypt non-matching aad', async function () {
const text = 'hello world'
const rawKey = _key.masterKey
const nonce = await application.protocolService.crypto.generateRandomKey(192)
const payload = new DecryptedPayload({
uuid: '123',
content_type: ContentType.ItemsKey,
content: FillItemContent({
title: 'foo',
text: 'bar',
}),
})
const operator = application.protocolService.operatorManager.operatorForVersion(ProtocolVersion.V004)
const aad = { foo: 'bar' }
const nonmatchingAad = { foo: 'rab' }
const encString = await operator.encryptString004(text, rawKey, nonce, aad)
const decString = await operator.decryptString004(encString, rawKey, nonce, nonmatchingAad)
expect(decString).to.not.be.ok
const encrypted = await operator.generateEncryptedParameters(payload, rootKey)
const decrypted = await operator.generateDecryptedParameters(
{
...encrypted,
uuid: 'nonmatching',
},
rootKey,
)
expect(decrypted.errorDecrypting).to.equal(true)
})
it('generates existing keys for key params', async function () {
const key = await application.protocolService.computeRootKey(_password, _keyParams)
expect(key.compare(_key)).to.be.true
const key = await application.protocolService.computeRootKey(_password, rootKeyParams)
expect(key.compare(rootKey)).to.be.true
})
it('can decrypt encrypted params', async function () {
const payload = Factory.createNotePayload()
const key = await protocol004.createItemsKey()
const params = await protocol004.generateEncryptedParametersSync(payload, key)
const decrypted = await protocol004.generateDecryptedParametersSync(params, key)
const params = await protocol004.generateEncryptedParameters(payload, key)
const decrypted = await protocol004.generateDecryptedParameters(params, key)
expect(decrypted.errorDecrypting).to.not.be.ok
expect(decrypted.content).to.eql(payload.content)
})
@@ -113,9 +128,9 @@ describe('004 protocol operations', function () {
it('modifying the uuid of the payload should fail to decrypt', async function () {
const payload = Factory.createNotePayload()
const key = await protocol004.createItemsKey()
const params = await protocol004.generateEncryptedParametersSync(payload, key)
const params = await protocol004.generateEncryptedParameters(payload, key)
params.uuid = 'foo'
const result = await protocol004.generateDecryptedParametersSync(params, key)
const result = await protocol004.generateDecryptedParameters(params, key)
expect(result.errorDecrypting).to.equal(true)
})
})

View File

@@ -0,0 +1,58 @@
export const BaseTests = [
'memory.test.js',
'protocol.test.js',
'utils.test.js',
'000.test.js',
'001.test.js',
'002.test.js',
'003.test.js',
'004.test.js',
'username.test.js',
'app-group.test.js',
'application.test.js',
'payload.test.js',
'payload_encryption.test.js',
'item.test.js',
'item_manager.test.js',
'features.test.js',
'settings.test.js',
'mfa_service.test.js',
'mutator.test.js',
'mutator_service.test.js',
'payload_manager.test.js',
'collections.test.js',
'note_display_criteria.test.js',
'keys.test.js',
'key_params.test.js',
'key_recovery_service.test.js',
'backups.test.js',
'upgrading.test.js',
'model_tests/importing.test.js',
'model_tests/appmodels.test.js',
'model_tests/items.test.js',
'model_tests/mapping.test.js',
'model_tests/notes_smart_tags.test.js',
'model_tests/notes_tags.test.js',
'model_tests/notes_tags_folders.test.js',
'model_tests/performance.test.js',
'sync_tests/offline.test.js',
'sync_tests/notes_tags.test.js',
'sync_tests/online.test.js',
'sync_tests/conflicting.test.js',
'sync_tests/integrity.test.js',
'auth-fringe-cases.test.js',
'auth.test.js',
'device_auth.test.js',
'storage.test.js',
'protection.test.js',
'singletons.test.js',
'migrations/migration.test.js',
'migrations/tags-to-folders.test.js',
'history.test.js',
'actions.test.js',
'preferences.test.js',
'files.test.js',
'session.test.js',
'subscriptions.test.js',
'recovery.test.js',
];

View File

@@ -0,0 +1,7 @@
import { BaseTests } from './BaseTests.js'
import { VaultTests } from './VaultTests.js'
export default {
BaseTests,
VaultTests,
}

View File

@@ -0,0 +1,16 @@
export const VaultTests = [
'vaults/vaults.test.js',
'vaults/pkc.test.js',
'vaults/contacts.test.js',
'vaults/crypto.test.js',
'vaults/asymmetric-messages.test.js',
'vaults/shared_vaults.test.js',
'vaults/invites.test.js',
'vaults/items.test.js',
'vaults/conflicts.test.js',
'vaults/deletion.test.js',
'vaults/permissions.test.js',
'vaults/key_rotation.test.js',
'vaults/files.test.js',
];

View File

@@ -170,10 +170,7 @@ describe('actions service', () => {
})
// Extension item
const extensionItem = await this.application.itemManager.createItem(
ContentType.ActionsExtension,
this.actionsExtension,
)
const extensionItem = await this.application.mutator.createItem(ContentType.ActionsExtension, this.actionsExtension)
this.extensionItemUuid = extensionItem.uuid
})
@@ -185,7 +182,7 @@ describe('actions service', () => {
})
it('should get extension items', async function () {
await this.itemManager.createItem(ContentType.Note, {
await this.application.mutator.createItem(ContentType.Note, {
title: 'A simple note',
text: 'Standard Notes rocks! lml.',
})
@@ -194,7 +191,7 @@ describe('actions service', () => {
})
it('should get extensions in context of item', async function () {
const noteItem = await this.itemManager.createItem(ContentType.Note, {
const noteItem = await this.application.mutator.createItem(ContentType.Note, {
title: 'Another note',
text: 'Whiskey In The Jar',
})
@@ -205,7 +202,7 @@ describe('actions service', () => {
})
it('should get actions based on item context', async function () {
const tagItem = await this.itemManager.createItem(ContentType.Tag, {
const tagItem = await this.application.mutator.createItem(ContentType.Tag, {
title: 'Music',
})
@@ -217,7 +214,7 @@ describe('actions service', () => {
})
it('should load extension in context of item', async function () {
const noteItem = await this.itemManager.createItem(ContentType.Note, {
const noteItem = await this.application.mutator.createItem(ContentType.Note, {
title: 'Yet another note',
text: 'And all things will end ♫',
})
@@ -249,7 +246,7 @@ describe('actions service', () => {
const sandbox = sinon.createSandbox()
before(async function () {
this.noteItem = await this.itemManager.createItem(ContentType.Note, {
this.noteItem = await this.application.mutator.createItem(ContentType.Note, {
title: 'Hey',
text: 'Welcome To Paradise',
})
@@ -331,7 +328,7 @@ describe('actions service', () => {
const sandbox = sinon.createSandbox()
before(async function () {
this.noteItem = await this.itemManager.createItem(ContentType.Note, {
this.noteItem = await this.application.mutator.createItem(ContentType.Note, {
title: 'Excuse Me',
text: 'Time To Be King 8)',
})

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import { BaseItemCounts } from './lib/Applications.js'
import { BaseItemCounts } from './lib/BaseItemCounts.js'
import * as Factory from './lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -75,7 +75,7 @@ describe('application instances', () => {
/** Recreate app with different host */
const recreatedContext = await Factory.createAppContext({
identifier: 'app',
host: 'http://nonsense.host'
host: 'http://nonsense.host',
})
await recreatedContext.launch()
@@ -134,7 +134,7 @@ describe('application instances', () => {
})
it('shows confirmation dialog when there are unsaved changes', async () => {
await testSNApp.itemManager.setItemDirty(testNote1)
await testSNApp.mutator.setItemDirty(testNote1)
await testSNApp.user.signOut()
const expectedConfirmMessage = signOutConfirmMessage(1)
@@ -154,7 +154,7 @@ describe('application instances', () => {
})
it('does not show confirmation dialog when there are unsaved changes and the "force" option is set to true', async () => {
await testSNApp.itemManager.setItemDirty(testNote1)
await testSNApp.mutator.setItemDirty(testNote1)
await testSNApp.user.signOut(true)
expect(confirmAlert.callCount).to.equal(0)
@@ -166,7 +166,7 @@ describe('application instances', () => {
confirmAlert.restore()
confirmAlert = sinon.stub(testSNApp.alertService, 'confirm').callsFake((_message) => false)
await testSNApp.itemManager.setItemDirty(testNote1)
await testSNApp.mutator.setItemDirty(testNote1)
await testSNApp.user.signOut()
const expectedConfirmMessage = signOutConfirmMessage(1)

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import { BaseItemCounts } from './lib/Applications.js'
import { BaseItemCounts } from './lib/BaseItemCounts.js'
import * as Factory from './lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -85,14 +85,14 @@ describe('auth fringe cases', () => {
const serverText = 'server text'
await context.application.mutator.changeAndSaveItem(firstVersionOfNote, (mutator) => {
await context.application.changeAndSaveItem(firstVersionOfNote, (mutator) => {
mutator.text = serverText
})
const newApplication = await Factory.signOutApplicationAndReturnNew(context.application)
/** Create same note but now offline */
await newApplication.itemManager.emitItemFromPayload(firstVersionOfNote.payload)
await newApplication.mutator.emitItemFromPayload(firstVersionOfNote.payload)
/** Sign in and merge local data */
await newApplication.signIn(context.email, context.password, undefined, undefined, true, true)

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import { BaseItemCounts } from './lib/Applications.js'
import { BaseItemCounts } from './lib/BaseItemCounts.js'
import * as Factory from './lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -15,7 +15,7 @@ describe('basic auth', function () {
beforeEach(async function () {
localStorage.clear()
this.expectedItemCount = BaseItemCounts.DefaultItems
this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount
this.context = await Factory.createAppContext()
await this.context.launch()
this.application = this.context.application
@@ -262,7 +262,7 @@ describe('basic auth', function () {
if (!didCompleteDownloadFirstSync) {
return
}
if (!didCompletePostDownloadFirstSync && eventName === SyncEvent.SingleRoundTripSyncCompleted) {
if (!didCompletePostDownloadFirstSync && eventName === SyncEvent.PaginatedSyncRequestCompleted) {
didCompletePostDownloadFirstSync = true
/** Should be in sync */
outOfSync = this.application.syncService.isOutOfSync()

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import { BaseItemCounts } from './lib/Applications.js'
import { BaseItemCounts } from './lib/BaseItemCounts.js'
import * as Factory from './lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -25,9 +25,6 @@ describe('backups', function () {
this.application = null
})
const BASE_ITEM_COUNT_ENCRYPTED = BaseItemCounts.DefaultItems
const BASE_ITEM_COUNT_DECRYPTED = ['UserPreferences', 'DarkTheme'].length
it('backup file should have a version number', async function () {
let data = await this.application.createDecryptedBackupFile()
expect(data.version).to.equal(this.application.protocolService.getLatestVersion())
@@ -39,7 +36,9 @@ describe('backups', function () {
it('no passcode + no account backup file should have correct number of items', async function () {
await Promise.all([Factory.createSyncedNote(this.application), Factory.createSyncedNote(this.application)])
const data = await this.application.createDecryptedBackupFile()
expect(data.items.length).to.equal(BASE_ITEM_COUNT_DECRYPTED + 2)
const offsetForNewItems = 2
const offsetForNoItemsKey = -1
expect(data.items.length).to.equal(BaseItemCounts.DefaultItems + offsetForNewItems + offsetForNoItemsKey)
})
it('passcode + no account backup file should have correct number of items', async function () {
@@ -49,12 +48,12 @@ describe('backups', function () {
// Encrypted backup without authorization
const encryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
expect(encryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2)
expect(encryptedData.items.length).to.equal(BaseItemCounts.DefaultItems + 2)
// Encrypted backup with authorization
Factory.handlePasswordChallenges(this.application, passcode)
const authorizedEncryptedData = await this.application.createEncryptedBackupFile()
expect(authorizedEncryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2)
expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItems + 2)
})
it('no passcode + account backup file should have correct number of items', async function () {
@@ -68,17 +67,17 @@ describe('backups', function () {
// Encrypted backup without authorization
const encryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
expect(encryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2)
expect(encryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2)
Factory.handlePasswordChallenges(this.application, this.password)
// Decrypted backup
const decryptedData = await this.application.createDecryptedBackupFile()
expect(decryptedData.items.length).to.equal(BASE_ITEM_COUNT_DECRYPTED + 2)
expect(decryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccountWithoutItemsKey + 2)
// Encrypted backup with authorization
const authorizedEncryptedData = await this.application.createEncryptedBackupFile()
expect(authorizedEncryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2)
expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2)
})
it('passcode + account backup file should have correct number of items', async function () {
@@ -91,17 +90,17 @@ describe('backups', function () {
// Encrypted backup without authorization
const encryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
expect(encryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2)
expect(encryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2)
Factory.handlePasswordChallenges(this.application, passcode)
// Decrypted backup
const decryptedData = await this.application.createDecryptedBackupFile()
expect(decryptedData.items.length).to.equal(BASE_ITEM_COUNT_DECRYPTED + 2)
expect(decryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccountWithoutItemsKey + 2)
// Encrypted backup with authorization
const authorizedEncryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
expect(authorizedEncryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2)
expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2)
})
it('backup file item should have correct fields', async function () {
@@ -154,7 +153,7 @@ describe('backups', function () {
errorDecrypting: true,
})
await this.application.itemManager.emitItemFromPayload(errored)
await this.application.payloadManager.emitPayload(errored)
const erroredItem = this.application.itemManager.findAnyItem(errored.uuid)
@@ -162,7 +161,7 @@ describe('backups', function () {
const backupData = await this.application.createDecryptedBackupFile()
expect(backupData.items.length).to.equal(BASE_ITEM_COUNT_DECRYPTED + 2)
expect(backupData.items.length).to.equal(BaseItemCounts.DefaultItemsNoAccounNoItemsKey + 2)
})
it('decrypted backup file should not have keyParams', async function () {

View File

@@ -31,9 +31,9 @@ describe('features', () => {
expires_at: tomorrow,
}
sinon.spy(application.itemManager, 'createItem')
sinon.spy(application.itemManager, 'changeComponent')
sinon.spy(application.itemManager, 'setItemsToBeDeleted')
sinon.spy(application.mutator, 'createItem')
sinon.spy(application.mutator, 'changeComponent')
sinon.spy(application.mutator, 'setItemsToBeDeleted')
getUserFeatures = sinon.stub(application.apiService, 'getUserFeatures').callsFake(() => {
return Promise.resolve({
@@ -82,7 +82,7 @@ describe('features', () => {
it('should fetch user features and create items for features with content type', async () => {
expect(application.apiService.getUserFeatures.callCount).to.equal(1)
expect(application.itemManager.createItem.callCount).to.equal(2)
expect(application.mutator.createItem.callCount).to.equal(2)
const themeItems = application.items.getItems(ContentType.Theme)
const systemThemeCount = 1
@@ -117,7 +117,7 @@ describe('features', () => {
// Wipe roles from initial sync
await application.featuresService.setOnlineRoles([])
// Create pre-existing item for theme without all the info
await application.itemManager.createItem(
await application.mutator.createItem(
ContentType.Theme,
FillItemContent({
package_info: {
@@ -129,7 +129,7 @@ describe('features', () => {
await application.sync.sync()
// Timeout since we don't await for features update
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(application.itemManager.changeComponent.callCount).to.equal(1)
expect(application.mutator.changeComponent.callCount).to.equal(1)
const themeItems = application.items.getItems(ContentType.Theme)
expect(themeItems).to.have.lengthOf(1)
expect(themeItems[0].content).to.containSubset(
@@ -172,7 +172,7 @@ describe('features', () => {
// Timeout since we don't await for features update
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(application.itemManager.setItemsToBeDeleted.calledWith([sinon.match({ uuid: themeItem.uuid })])).to.equal(
expect(application.mutator.setItemsToBeDeleted.calledWith([sinon.match({ uuid: themeItem.uuid })])).to.equal(
true,
)
@@ -202,7 +202,7 @@ describe('features', () => {
sinon.stub(application.featuresService, 'migrateFeatureRepoToUserSetting').callsFake(resolve)
})
await application.itemManager.createItem(
await application.mutator.createItem(
ContentType.ExtensionRepo,
FillItemContent({
url: `https://extensions.standardnotes.org/${extensionKey}`,
@@ -224,7 +224,7 @@ describe('features', () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
.callsFake(() => {})
const extensionKey = UuidGenerator.GenerateUuid().split('-').join('')
await application.itemManager.createItem(
await application.mutator.createItem(
ContentType.ExtensionRepo,
FillItemContent({
url: `https://extensions.standardnotes.org/${extensionKey}`,
@@ -255,7 +255,7 @@ describe('features', () => {
return false
})
const extensionKey = UuidGenerator.GenerateUuid().split('-').join('')
await application.itemManager.createItem(
await application.mutator.createItem(
ContentType.ExtensionRepo,
FillItemContent({
url: `https://extensions.standardnotes.org/${extensionKey}`,
@@ -290,7 +290,7 @@ describe('features', () => {
}
})
})
await application.itemManager.createItem(
await application.mutator.createItem(
ContentType.ExtensionRepo,
FillItemContent({
url: `https://extensions.standardnotes.org/${extensionKey}`,
@@ -304,7 +304,7 @@ describe('features', () => {
it('previous extension repo should be migrated to offline feature repo', async () => {
application = await Factory.signOutApplicationAndReturnNew(application)
const extensionKey = UuidGenerator.GenerateUuid().split('-').join('')
await application.itemManager.createItem(
await application.mutator.createItem(
ContentType.ExtensionRepo,
FillItemContent({
url: `https://extensions.standardnotes.org/${extensionKey}`,

View File

@@ -1,4 +1,5 @@
import * as Factory from './lib/factory.js'
import * as Events from './lib/Events.js'
import * as Utils from './lib/Utils.js'
import * as Files from './lib/Files.js'
@@ -38,22 +39,7 @@ describe('files', function () {
})
if (subscription) {
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
userEmail: context.email,
subscriptionId: subscriptionId++,
subscriptionName: 'PRO_PLAN',
subscriptionExpiresAt: (new Date().getTime() + 3_600_000) * 1_000,
timestamp: Date.now(),
offline: false,
discountCode: null,
limitedDiscountPurchased: false,
newSubscriber: true,
totalActiveSubscriptionsCount: 1,
userRegisteredAt: 1,
billingFrequency: 12,
payAmount: 59.00
})
await Factory.sleep(2)
await context.publicMockSubscriptionPurchaseEvent()
}
}
@@ -66,7 +52,7 @@ describe('files', function () {
await setup({ fakeCrypto: true, subscription: true })
const remoteIdentifier = Utils.generateUuid()
const token = await application.apiService.createFileValetToken(remoteIdentifier, 'write')
const token = await application.apiService.createUserFileValetToken(remoteIdentifier, 'write')
expect(token.length).to.be.above(0)
})
@@ -75,15 +61,15 @@ describe('files', function () {
await setup({ fakeCrypto: true, subscription: false })
const remoteIdentifier = Utils.generateUuid()
const tokenOrError = await application.apiService.createFileValetToken(remoteIdentifier, 'write')
const tokenOrError = await application.apiService.createUserFileValetToken(remoteIdentifier, 'write')
expect(tokenOrError.tag).to.equal('no-subscription')
expect(isClientDisplayableError(tokenOrError)).to.equal(true)
})
it('should not create valet token from server when user has an expired subscription - @paidfeature', async function () {
await setup({ fakeCrypto: true, subscription: false })
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
userEmail: context.email,
subscriptionId: subscriptionId++,
subscriptionName: 'PLUS_PLAN',
@@ -96,27 +82,27 @@ describe('files', function () {
totalActiveSubscriptionsCount: 1,
userRegisteredAt: 1,
billingFrequency: 12,
payAmount: 59.00
payAmount: 59.0,
})
await Factory.sleep(2)
const remoteIdentifier = Utils.generateUuid()
const tokenOrError = await application.apiService.createFileValetToken(remoteIdentifier, 'write')
const tokenOrError = await application.apiService.createUserFileValetToken(remoteIdentifier, 'write')
expect(tokenOrError.tag).to.equal('expired-subscription')
expect(isClientDisplayableError(tokenOrError)).to.equal(true)
})
it('creating two upload sessions successively should succeed - @paidfeature', async function () {
await setup({ fakeCrypto: true, subscription: true })
const firstToken = await application.apiService.createFileValetToken(Utils.generateUuid(), 'write')
const firstSession = await application.apiService.startUploadSession(firstToken)
const firstToken = await application.apiService.createUserFileValetToken(Utils.generateUuid(), 'write')
const firstSession = await application.apiService.startUploadSession(firstToken, 'user')
expect(firstSession.uploadId).to.be.ok
const secondToken = await application.apiService.createFileValetToken(Utils.generateUuid(), 'write')
const secondSession = await application.apiService.startUploadSession(secondToken)
const secondToken = await application.apiService.createUserFileValetToken(Utils.generateUuid(), 'write')
const secondSession = await application.apiService.startUploadSession(secondToken, 'user')
expect(secondSession.uploadId).to.be.ok
})
@@ -129,7 +115,7 @@ describe('files', function () {
const file = await Files.uploadFile(fileService, buffer, 'my-file', 'md', 1000)
const downloadedBytes = await Files.downloadFile(fileService, itemManager, file.remoteIdentifier)
const downloadedBytes = await Files.downloadFile(fileService, file)
expect(downloadedBytes).to.eql(buffer)
})
@@ -142,7 +128,7 @@ describe('files', function () {
const file = await Files.uploadFile(fileService, buffer, 'my-file', 'md', 100000)
const downloadedBytes = await Files.downloadFile(fileService, itemManager, file.remoteIdentifier)
const downloadedBytes = await Files.downloadFile(fileService, file)
expect(downloadedBytes).to.eql(buffer)
})

View File

@@ -35,7 +35,7 @@ describe('history manager', () => {
})
function setTextAndSync(application, item, text) {
return application.mutator.changeAndSaveItem(
return application.changeAndSaveItem(
item,
(mutator) => {
mutator.text = text
@@ -59,7 +59,7 @@ describe('history manager', () => {
expect(this.historyManager.sessionHistoryForItem(item).length).to.equal(0)
/** Sync with different contents, should create new entry */
await this.application.mutator.changeAndSaveItem(
await this.application.changeAndSaveItem(
item,
(mutator) => {
mutator.title = Math.random()
@@ -79,7 +79,7 @@ describe('history manager', () => {
const context = await Factory.createAppContext({ identifier })
await context.launch()
expect(context.application.historyManager.sessionHistoryForItem(item).length).to.equal(0)
await context.application.mutator.changeAndSaveItem(
await context.application.changeAndSaveItem(
item,
(mutator) => {
mutator.title = Math.random()
@@ -97,13 +97,13 @@ describe('history manager', () => {
it('creating new item and making 1 change should create 0 revisions', async function () {
const context = await Factory.createAppContext()
await context.launch()
const item = await context.application.mutator.createTemplateItem(ContentType.Note, {
const item = await context.application.items.createTemplateItem(ContentType.Note, {
references: [],
})
await context.application.mutator.insertItem(item)
expect(context.application.historyManager.sessionHistoryForItem(item).length).to.equal(0)
await context.application.mutator.changeAndSaveItem(
await context.application.changeAndSaveItem(
item,
(mutator) => {
mutator.title = Math.random()
@@ -172,8 +172,8 @@ describe('history manager', () => {
text: Factory.randomString(100),
}),
)
let item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
await this.application.itemManager.setItemDirty(item)
let item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
await this.application.mutator.setItemDirty(item)
await this.application.syncService.sync(syncOptions)
/** It should keep the first and last by default */
item = await setTextAndSync(this.application, item, item.content.text)
@@ -202,9 +202,9 @@ describe('history manager', () => {
}),
)
let item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
let item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
await this.application.itemManager.setItemDirty(item)
await this.application.mutator.setItemDirty(item)
await this.application.syncService.sync(syncOptions)
item = await setTextAndSync(this.application, item, item.content.text + Factory.randomString(1))
@@ -241,9 +241,9 @@ describe('history manager', () => {
it('unsynced entries should use payload created_at for preview titles', async function () {
const payload = Factory.createNotePayload()
await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
const item = this.application.items.findItem(payload.uuid)
await this.application.mutator.changeAndSaveItem(
await this.application.changeAndSaveItem(
item,
(mutator) => {
mutator.title = Math.random()
@@ -306,7 +306,7 @@ describe('history manager', () => {
expect(itemHistory.length).to.equal(1)
/** Sync with different contents, should not create a new entry */
await this.application.mutator.changeAndSaveItem(
await this.application.changeAndSaveItem(
item,
(mutator) => {
mutator.title = Math.random()
@@ -327,7 +327,7 @@ describe('history manager', () => {
await Factory.sleep(Factory.ServerRevisionFrequency)
/** Sync with different contents, should create new entry */
const newTitleAfterFirstChange = `The title should be: ${Math.random()}`
await this.application.mutator.changeAndSaveItem(
await this.application.changeAndSaveItem(
item,
(mutator) => {
mutator.title = newTitleAfterFirstChange
@@ -343,7 +343,10 @@ describe('history manager', () => {
expect(itemHistory.length).to.equal(2)
const oldestEntry = lastElement(itemHistory)
let revisionFromServerOrError = await this.application.getRevision.execute({ itemUuid: item.uuid, revisionUuid: oldestEntry.uuid })
let revisionFromServerOrError = await this.application.getRevision.execute({
itemUuid: item.uuid,
revisionUuid: oldestEntry.uuid,
})
const revisionFromServer = revisionFromServerOrError.getValue()
expect(revisionFromServer).to.be.ok
@@ -359,7 +362,7 @@ describe('history manager', () => {
it('duplicate revisions should not have the originals uuid', async function () {
const note = await Factory.createSyncedNote(this.application)
await Factory.markDirtyAndSyncItem(this.application, note)
const dupe = await this.application.itemManager.duplicateItem(note, true)
const dupe = await this.application.mutator.duplicateItem(note, true)
await Factory.markDirtyAndSyncItem(this.application, dupe)
await Factory.sleep(Factory.ServerRevisionCreationDelay)
@@ -367,7 +370,10 @@ describe('history manager', () => {
const dupeHistoryOrError = await this.application.listRevisions.execute({ itemUuid: dupe.uuid })
const dupeHistory = dupeHistoryOrError.getValue()
const dupeRevisionOrError = await this.application.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: dupeHistory[0].uuid })
const dupeRevisionOrError = await this.application.getRevision.execute({
itemUuid: dupe.uuid,
revisionUuid: dupeHistory[0].uuid,
})
const dupeRevision = dupeRevisionOrError.getValue()
expect(dupeRevision.payload.uuid).to.equal(dupe.uuid)
})
@@ -384,7 +390,7 @@ describe('history manager', () => {
await Factory.sleep(Factory.ServerRevisionFrequency)
await Factory.markDirtyAndSyncItem(this.application, note)
const dupe = await this.application.itemManager.duplicateItem(note, true)
const dupe = await this.application.mutator.duplicateItem(note, true)
await Factory.markDirtyAndSyncItem(this.application, dupe)
await Factory.sleep(Factory.ServerRevisionCreationDelay)
@@ -405,12 +411,12 @@ describe('history manager', () => {
await Factory.sleep(Factory.ServerRevisionFrequency)
const changedText = `${Math.random()}`
await this.application.mutator.changeAndSaveItem(note, (mutator) => {
await this.application.changeAndSaveItem(note, (mutator) => {
mutator.title = changedText
})
await Factory.markDirtyAndSyncItem(this.application, note)
const dupe = await this.application.itemManager.duplicateItem(note, true)
const dupe = await this.application.mutator.duplicateItem(note, true)
await Factory.markDirtyAndSyncItem(this.application, dupe)
await Factory.sleep(Factory.ServerRevisionCreationDelay)
@@ -420,7 +426,10 @@ describe('history manager', () => {
expect(itemHistory.length).to.be.above(1)
const newestRevision = itemHistory[0]
const fetchedOrError = await this.application.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: newestRevision.uuid })
const fetchedOrError = await this.application.getRevision.execute({
itemUuid: dupe.uuid,
revisionUuid: newestRevision.uuid,
})
const fetched = fetchedOrError.getValue()
expect(fetched.payload.errorDecrypting).to.not.be.ok
expect(fetched.payload.content.title).to.equal(changedText)

View File

@@ -1,167 +1,120 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import * as Factory from './lib/factory.js'
import { BaseItemCounts } from './lib/BaseItemCounts.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('item manager', function () {
let context
let application
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
this.payloadManager = new PayloadManager()
this.itemManager = new ItemManager(this.payloadManager)
this.createNote = async () => {
return this.itemManager.createItem(ContentType.Note, {
title: 'hello',
text: 'world',
})
}
localStorage.clear()
this.createTag = async (notes = []) => {
const references = notes.map((note) => {
return {
uuid: note.uuid,
content_type: note.content_type,
}
})
return this.itemManager.createItem(ContentType.Tag, {
title: 'thoughts',
references: references,
})
}
context = await Factory.createAppContextWithFakeCrypto()
application = context.application
await context.launch()
})
it('create item', async function () {
const item = await this.createNote()
const createNote = async () => {
return application.mutator.createItem(ContentType.Note, {
title: 'hello',
text: 'world',
})
}
expect(item).to.be.ok
expect(item.title).to.equal('hello')
})
it('emitting item through payload and marking dirty should have userModifiedDate', async function () {
const payload = Factory.createNotePayload()
const item = await this.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
const result = await this.itemManager.setItemDirty(item)
const appData = result.payload.content.appData
expect(appData[DecryptedItem.DefaultAppDomain()][AppDataField.UserModifiedDate]).to.be.ok
})
const createTag = async (notes = []) => {
const references = notes.map((note) => {
return {
uuid: note.uuid,
content_type: note.content_type,
}
})
return application.mutator.createItem(ContentType.Tag, {
title: 'thoughts',
references: references,
})
}
it('find items with valid uuid', async function () {
const item = await this.createNote()
const item = await createNote()
const results = await this.itemManager.findItems([item.uuid])
const results = await application.items.findItems([item.uuid])
expect(results.length).to.equal(1)
expect(results[0]).to.equal(item)
})
it('find items with invalid uuid no blanks', async function () {
const results = await this.itemManager.findItems([Factory.generateUuidish()])
const results = await application.items.findItems([Factory.generateUuidish()])
expect(results.length).to.equal(0)
})
it('find items with invalid uuid include blanks', async function () {
const includeBlanks = true
const results = await this.itemManager.findItemsIncludingBlanks([Factory.generateUuidish()])
const results = await application.items.findItemsIncludingBlanks([Factory.generateUuidish()])
expect(results.length).to.equal(1)
expect(results[0]).to.not.be.ok
})
it('item state', async function () {
await this.createNote()
await createNote()
expect(this.itemManager.items.length).to.equal(1)
expect(this.itemManager.getDisplayableNotes().length).to.equal(1)
expect(application.items.items.length).to.equal(1 + BaseItemCounts.DefaultItems)
expect(application.items.getDisplayableNotes().length).to.equal(1)
})
it('find item', async function () {
const item = await this.createNote()
const item = await createNote()
const foundItem = this.itemManager.findItem(item.uuid)
const foundItem = application.items.findItem(item.uuid)
expect(foundItem).to.be.ok
})
it('reference map', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
const note = await createNote()
const tag = await createTag([note])
expect(this.itemManager.collection.referenceMap.directMap.get(tag.uuid)).to.eql([note.uuid])
expect(application.items.collection.referenceMap.directMap.get(tag.uuid)).to.eql([note.uuid])
})
it('inverse reference map', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
const note = await createNote()
const tag = await createTag([note])
expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([tag.uuid])
expect(application.items.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([tag.uuid])
})
it('inverse reference map should not have duplicates', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
await this.itemManager.changeItem(tag)
const note = await createNote()
const tag = await createTag([note])
await application.mutator.changeItem(tag)
expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([tag.uuid])
})
it('deleting from reference map', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
await this.itemManager.setItemToBeDeleted(note)
expect(this.itemManager.collection.referenceMap.directMap.get(tag.uuid)).to.eql([])
expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid).length).to.equal(0)
})
it('deleting referenced item should update referencing item references', async function () {
const note = await this.createNote()
let tag = await this.createTag([note])
await this.itemManager.setItemToBeDeleted(note)
tag = this.itemManager.findItem(tag.uuid)
expect(tag.content.references.length).to.equal(0)
})
it('removing relationship should update reference map', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
await this.itemManager.changeItem(tag, (mutator) => {
mutator.removeItemAsRelationship(note)
})
expect(this.itemManager.collection.referenceMap.directMap.get(tag.uuid)).to.eql([])
expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([])
})
it('emitting discardable payload should remove it from our collection', async function () {
const note = await this.createNote()
const payload = new DeletedPayload({
...note.payload.ejected(),
content: undefined,
deleted: true,
dirty: false,
})
expect(payload.discardable).to.equal(true)
await this.itemManager.emitItemFromPayload(payload)
expect(this.itemManager.findItem(note.uuid)).to.not.be.ok
expect(application.items.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([tag.uuid])
})
it('items that reference item', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
const note = await createNote()
const tag = await createTag([note])
const itemsThatReference = this.itemManager.itemsReferencingItem(note)
const itemsThatReference = application.items.itemsReferencingItem(note)
expect(itemsThatReference.length).to.equal(1)
expect(itemsThatReference[0]).to.equal(tag)
})
it('observer', async function () {
const observed = []
this.itemManager.addObserver(ContentType.Any, ({ changed, inserted, removed, source, sourceKey }) => {
application.items.addObserver(ContentType.Any, ({ changed, inserted, removed, source, sourceKey }) => {
observed.push({ changed, inserted, removed, source, sourceKey })
})
const note = await this.createNote()
const tag = await this.createTag([note])
const note = await createNote()
const tag = await createTag([note])
expect(observed.length).to.equal(2)
const firstObserved = observed[0]
@@ -171,59 +124,23 @@ describe('item manager', function () {
expect(secondObserved.inserted).to.eql([tag])
})
it('change existing item', async function () {
const note = await this.createNote()
const newTitle = String(Math.random())
await this.itemManager.changeItem(note, (mutator) => {
mutator.title = newTitle
})
const latestVersion = this.itemManager.findItem(note.uuid)
expect(latestVersion.title).to.equal(newTitle)
})
it('change non-existant item through uuid should fail', async function () {
const note = await this.itemManager.createTemplateItem(ContentType.Note, {
title: 'hello',
text: 'world',
})
const changeFn = async () => {
const newTitle = String(Math.random())
return this.itemManager.changeItem(note, (mutator) => {
mutator.title = newTitle
})
}
await Factory.expectThrowsAsync(() => changeFn(), 'Attempting to change non-existant item')
})
it('set items dirty', async function () {
const note = await this.createNote()
await this.itemManager.setItemDirty(note)
const dirtyItems = this.itemManager.getDirtyItems()
expect(dirtyItems.length).to.equal(1)
expect(dirtyItems[0].uuid).to.equal(note.uuid)
expect(dirtyItems[0].dirty).to.equal(true)
})
it('dirty items should not include errored items', async function () {
const note = await this.itemManager.setItemDirty(await this.createNote())
const note = await application.mutator.setItemDirty(await createNote())
const errorred = new EncryptedPayload({
...note.payload,
content: '004:...',
errorDecrypting: true,
})
await this.itemManager.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
await application.mutator.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
const dirtyItems = this.itemManager.getDirtyItems()
const dirtyItems = application.items.getDirtyItems()
expect(dirtyItems.length).to.equal(0)
})
it('dirty items should include errored items if they are being deleted', async function () {
const note = await this.itemManager.setItemDirty(await this.createNote())
const note = await application.mutator.setItemDirty(await createNote())
const errorred = new DeletedPayload({
...note.payload,
content: undefined,
@@ -231,181 +148,63 @@ describe('item manager', function () {
deleted: true,
})
await this.itemManager.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
await application.mutator.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
const dirtyItems = this.itemManager.getDirtyItems()
const dirtyItems = application.items.getDirtyItems()
expect(dirtyItems.length).to.equal(1)
})
describe('duplicateItem', async function () {
const sandbox = sinon.createSandbox()
beforeEach(async function () {
this.emitPayloads = sandbox.spy(this.itemManager.payloadManager, 'emitPayloads')
})
afterEach(async function () {
sandbox.restore()
})
it('should duplicate the item and set the duplicate_of property', async function () {
const note = await this.createNote()
await this.itemManager.duplicateItem(note)
sinon.assert.calledTwice(this.emitPayloads)
const originalNote = this.itemManager.getDisplayableNotes()[0]
const duplicatedNote = this.itemManager.getDisplayableNotes()[1]
expect(this.itemManager.items.length).to.equal(2)
expect(this.itemManager.getDisplayableNotes().length).to.equal(2)
expect(originalNote.uuid).to.not.equal(duplicatedNote.uuid)
expect(originalNote.uuid).to.equal(duplicatedNote.duplicateOf)
expect(originalNote.uuid).to.equal(duplicatedNote.payload.duplicate_of)
expect(duplicatedNote.conflictOf).to.be.undefined
expect(duplicatedNote.payload.content.conflict_of).to.be.undefined
})
it('should duplicate the item and set the duplicate_of and conflict_of properties', async function () {
const note = await this.createNote()
await this.itemManager.duplicateItem(note, true)
sinon.assert.calledTwice(this.emitPayloads)
const originalNote = this.itemManager.getDisplayableNotes()[0]
const duplicatedNote = this.itemManager.getDisplayableNotes()[1]
expect(this.itemManager.items.length).to.equal(2)
expect(this.itemManager.getDisplayableNotes().length).to.equal(2)
expect(originalNote.uuid).to.not.equal(duplicatedNote.uuid)
expect(originalNote.uuid).to.equal(duplicatedNote.duplicateOf)
expect(originalNote.uuid).to.equal(duplicatedNote.payload.duplicate_of)
expect(originalNote.uuid).to.equal(duplicatedNote.conflictOf)
expect(originalNote.uuid).to.equal(duplicatedNote.payload.content.conflict_of)
})
it('duplicate item with relationships', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
const duplicate = await this.itemManager.duplicateItem(tag)
expect(duplicate.content.references).to.have.length(1)
expect(this.itemManager.items).to.have.length(3)
expect(this.itemManager.getDisplayableTags()).to.have.length(2)
})
it('adds duplicated item as a relationship to items referencing it', async function () {
const note = await this.createNote()
let tag = await this.createTag([note])
const duplicateNote = await this.itemManager.duplicateItem(note)
expect(tag.content.references).to.have.length(1)
tag = this.itemManager.findItem(tag.uuid)
const references = tag.content.references.map((ref) => ref.uuid)
expect(references).to.have.length(2)
expect(references).to.include(note.uuid, duplicateNote.uuid)
})
it('duplicates item with additional content', async function () {
const note = await this.itemManager.createItem(ContentType.Note, {
title: 'hello',
text: 'world',
})
const duplicateNote = await this.itemManager.duplicateItem(note, false, {
title: 'hello (copy)',
})
expect(duplicateNote.title).to.equal('hello (copy)')
expect(duplicateNote.text).to.equal('world')
})
})
it('set item deleted', async function () {
const note = await this.createNote()
await this.itemManager.setItemToBeDeleted(note)
/** Items should never be mutated directly */
expect(note.deleted).to.not.be.ok
const latestVersion = this.payloadManager.findOne(note.uuid)
expect(latestVersion.deleted).to.equal(true)
expect(latestVersion.dirty).to.equal(true)
expect(latestVersion.content).to.not.be.ok
/** Deleted items do not show up in item manager's public interface */
expect(this.itemManager.items.length).to.equal(0)
expect(this.itemManager.getDisplayableNotes().length).to.equal(0)
})
it('system smart views', async function () {
expect(this.itemManager.systemSmartViews.length).to.be.above(0)
expect(application.items.systemSmartViews.length).to.be.above(0)
})
it('find tag by title', async function () {
const tag = await this.createTag()
const tag = await createTag()
expect(this.itemManager.findTagByTitle(tag.title)).to.be.ok
expect(application.items.findTagByTitle(tag.title)).to.be.ok
})
it('find tag by title should be case insensitive', async function () {
const tag = await this.createTag()
const tag = await createTag()
expect(this.itemManager.findTagByTitle(tag.title.toUpperCase())).to.be.ok
expect(application.items.findTagByTitle(tag.title.toUpperCase())).to.be.ok
})
it('find or create tag by title', async function () {
const title = 'foo'
expect(await this.itemManager.findOrCreateTagByTitle(title)).to.be.ok
expect(await application.mutator.findOrCreateTagByTitle({ title: title })).to.be.ok
})
it('note count', async function () {
await this.createNote()
expect(this.itemManager.noteCount).to.equal(1)
})
it('trash', async function () {
const note = await this.createNote()
const versionTwo = await this.itemManager.changeItem(note, (mutator) => {
mutator.trashed = true
})
expect(this.itemManager.trashSmartView).to.be.ok
expect(versionTwo.trashed).to.equal(true)
expect(versionTwo.dirty).to.equal(true)
expect(versionTwo.content).to.be.ok
expect(this.itemManager.items.length).to.equal(1)
expect(this.itemManager.trashedItems.length).to.equal(1)
await this.itemManager.emptyTrash()
const versionThree = this.payloadManager.findOne(note.uuid)
expect(versionThree.deleted).to.equal(true)
expect(this.itemManager.trashedItems.length).to.equal(0)
await createNote()
expect(application.items.noteCount).to.equal(1)
})
it('remove all items from memory', async function () {
const observed = []
this.itemManager.addObserver(ContentType.Any, ({ changed, inserted, removed, ignored }) => {
application.items.addObserver(ContentType.Any, ({ changed, inserted, removed, ignored }) => {
observed.push({ changed, inserted, removed, ignored })
})
await this.createNote()
await this.itemManager.removeAllItemsFromMemory()
await createNote()
await application.items.removeAllItemsFromMemory()
const deletionEvent = observed[1]
expect(deletionEvent.removed[0].deleted).to.equal(true)
expect(this.itemManager.items.length).to.equal(0)
expect(application.items.items.length).to.equal(0)
})
it('remove item locally', async function () {
const observed = []
this.itemManager.addObserver(ContentType.Any, ({ changed, inserted, removed, ignored }) => {
application.items.addObserver(ContentType.Any, ({ changed, inserted, removed, ignored }) => {
observed.push({ changed, inserted, removed, ignored })
})
const note = await this.createNote()
await this.itemManager.removeItemLocally(note)
const note = await createNote()
await application.items.removeItemLocally(note)
expect(observed.length).to.equal(1)
expect(this.itemManager.findItem(note.uuid)).to.not.be.ok
expect(application.items.findItem(note.uuid)).to.not.be.ok
})
it('emitting a payload from within observer should queue to end', async function () {
@@ -421,7 +220,7 @@ describe('item manager', function () {
const changedTitle = 'changed title'
let didEmit = false
let latestVersion
this.itemManager.addObserver(ContentType.Note, ({ changed, inserted }) => {
application.items.addObserver(ContentType.Note, ({ changed, inserted }) => {
const all = changed.concat(inserted)
if (!didEmit) {
didEmit = true
@@ -431,60 +230,60 @@ describe('item manager', function () {
title: changedTitle,
},
})
this.itemManager.emitItemFromPayload(changedPayload)
application.mutator.emitItemFromPayload(changedPayload)
}
latestVersion = all[0]
})
await this.itemManager.emitItemFromPayload(payload)
await application.mutator.emitItemFromPayload(payload)
expect(latestVersion.title).to.equal(changedTitle)
})
describe('searchTags', async function () {
it('should return tag with query matching title', async function () {
const tag = await this.itemManager.findOrCreateTagByTitle('tag')
const tag = await application.mutator.findOrCreateTagByTitle({ title: 'tag' })
const results = this.itemManager.searchTags('tag')
const results = application.items.searchTags('tag')
expect(results).lengthOf(1)
expect(results[0].title).to.equal(tag.title)
})
it('should return all tags with query partially matching title', async function () {
const firstTag = await this.itemManager.findOrCreateTagByTitle('tag one')
const secondTag = await this.itemManager.findOrCreateTagByTitle('tag two')
const firstTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag one' })
const secondTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag two' })
const results = this.itemManager.searchTags('tag')
const results = application.items.searchTags('tag')
expect(results).lengthOf(2)
expect(results[0].title).to.equal(firstTag.title)
expect(results[1].title).to.equal(secondTag.title)
})
it('should be case insensitive', async function () {
const tag = await this.itemManager.findOrCreateTagByTitle('Tag')
const tag = await application.mutator.findOrCreateTagByTitle({ title: 'Tag' })
const results = this.itemManager.searchTags('tag')
const results = application.items.searchTags('tag')
expect(results).lengthOf(1)
expect(results[0].title).to.equal(tag.title)
})
it('should return tag with query matching delimiter separated component', async function () {
const tag = await this.itemManager.findOrCreateTagByTitle('parent.child')
const tag = await application.mutator.findOrCreateTagByTitle({ title: 'parent.child' })
const results = this.itemManager.searchTags('child')
const results = application.items.searchTags('child')
expect(results).lengthOf(1)
expect(results[0].title).to.equal(tag.title)
})
it('should return tags with matching query including delimiter', async function () {
const tag = await this.itemManager.findOrCreateTagByTitle('parent.child')
const tag = await application.mutator.findOrCreateTagByTitle({ title: 'parent.child' })
const results = this.itemManager.searchTags('parent.chi')
const results = application.items.searchTags('parent.chi')
expect(results).lengthOf(1)
expect(results[0].title).to.equal(tag.title)
})
it('should return tags in natural order', async function () {
const firstTag = await this.itemManager.findOrCreateTagByTitle('tag 100')
const secondTag = await this.itemManager.findOrCreateTagByTitle('tag 2')
const thirdTag = await this.itemManager.findOrCreateTagByTitle('tag b')
const fourthTag = await this.itemManager.findOrCreateTagByTitle('tag a')
const firstTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag 100' })
const secondTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag 2' })
const thirdTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag b' })
const fourthTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag a' })
const results = this.itemManager.searchTags('tag')
const results = application.items.searchTags('tag')
expect(results).lengthOf(4)
expect(results[0].title).to.equal(secondTag.title)
expect(results[1].title).to.equal(firstTag.title)
@@ -493,15 +292,15 @@ describe('item manager', function () {
})
it('should not return tags associated with note', async function () {
const firstTag = await this.itemManager.findOrCreateTagByTitle('tag one')
const secondTag = await this.itemManager.findOrCreateTagByTitle('tag two')
const firstTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag one' })
const secondTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag two' })
const note = await this.createNote()
await this.itemManager.changeItem(firstTag, (mutator) => {
const note = await createNote()
await application.mutator.changeItem(firstTag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(note)
})
const results = this.itemManager.searchTags('tag', note)
const results = application.items.searchTags('tag', note)
expect(results).lengthOf(1)
expect(results[0].title).to.equal(secondTag.title)
})
@@ -509,68 +308,68 @@ describe('item manager', function () {
describe('smart views', async function () {
it('all view should not include archived notes by default', async function () {
const normal = await this.createNote()
const normal = await createNote()
await this.itemManager.changeItem(normal, (mutator) => {
await application.mutator.changeItem(normal, (mutator) => {
mutator.archived = true
})
this.itemManager.setPrimaryItemDisplayOptions({
views: [this.itemManager.allNotesSmartView],
application.items.setPrimaryItemDisplayOptions({
views: [application.items.allNotesSmartView],
})
expect(this.itemManager.getDisplayableNotes().length).to.equal(0)
expect(application.items.getDisplayableNotes().length).to.equal(0)
})
it('archived view should not include trashed notes by default', async function () {
const normal = await this.createNote()
const normal = await createNote()
await this.itemManager.changeItem(normal, (mutator) => {
await application.mutator.changeItem(normal, (mutator) => {
mutator.archived = true
mutator.trashed = true
})
this.itemManager.setPrimaryItemDisplayOptions({
views: [this.itemManager.archivedSmartView],
application.items.setPrimaryItemDisplayOptions({
views: [application.items.archivedSmartView],
})
expect(this.itemManager.getDisplayableNotes().length).to.equal(0)
expect(application.items.getDisplayableNotes().length).to.equal(0)
})
it('trashed view should include archived notes by default', async function () {
const normal = await this.createNote()
const normal = await createNote()
await this.itemManager.changeItem(normal, (mutator) => {
await application.mutator.changeItem(normal, (mutator) => {
mutator.archived = true
mutator.trashed = true
})
this.itemManager.setPrimaryItemDisplayOptions({
views: [this.itemManager.trashSmartView],
application.items.setPrimaryItemDisplayOptions({
views: [application.items.trashSmartView],
})
expect(this.itemManager.getDisplayableNotes().length).to.equal(1)
expect(application.items.getDisplayableNotes().length).to.equal(1)
})
})
describe('getSortedTagsForNote', async function () {
it('should return tags associated with a note in natural order', async function () {
const tags = [
await this.itemManager.findOrCreateTagByTitle('tag 100'),
await this.itemManager.findOrCreateTagByTitle('tag 2'),
await this.itemManager.findOrCreateTagByTitle('tag b'),
await this.itemManager.findOrCreateTagByTitle('tag a'),
await application.mutator.findOrCreateTagByTitle({ title: 'tag 100' }),
await application.mutator.findOrCreateTagByTitle({ title: 'tag 2' }),
await application.mutator.findOrCreateTagByTitle({ title: 'tag b' }),
await application.mutator.findOrCreateTagByTitle({ title: 'tag a' }),
]
const note = await this.createNote()
const note = await createNote()
tags.map(async (tag) => {
await this.itemManager.changeItem(tag, (mutator) => {
await application.mutator.changeItem(tag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(note)
})
})
const results = this.itemManager.getSortedTagsForItem(note)
const results = application.items.getSortedTagsForItem(note)
expect(results).lengthOf(tags.length)
expect(results[0].title).to.equal(tags[1].title)
@@ -583,16 +382,16 @@ describe('item manager', function () {
describe('getTagParentChain', function () {
it('should return parent tags for a tag', async function () {
const [parent, child, grandchild, _other] = await Promise.all([
this.itemManager.findOrCreateTagByTitle('parent'),
this.itemManager.findOrCreateTagByTitle('parent.child'),
this.itemManager.findOrCreateTagByTitle('parent.child.grandchild'),
this.itemManager.findOrCreateTagByTitle('some other tag'),
application.mutator.findOrCreateTagByTitle({ title: 'parent' }),
application.mutator.findOrCreateTagByTitle({ title: 'parent.child' }),
application.mutator.findOrCreateTagByTitle({ title: 'parent.child.grandchild' }),
application.mutator.findOrCreateTagByTitle({ title: 'some other tag' }),
])
await this.itemManager.setTagParent(parent, child)
await this.itemManager.setTagParent(child, grandchild)
await application.mutator.setTagParent(parent, child)
await application.mutator.setTagParent(child, grandchild)
const results = this.itemManager.getTagParentChain(grandchild)
const results = application.items.getTagParentChain(grandchild)
expect(results).lengthOf(2)
expect(results[0].uuid).to.equal(parent.uuid)

View File

@@ -200,7 +200,9 @@ describe('key recovery service', function () {
const receiveChallenge = (challenge) => {
totalPromptCount++
/** Give unassociated password when prompted */
application.submitValuesForChallenge(challenge, [CreateChallengeValue(challenge.prompts[0], unassociatedPassword)])
application.submitValuesForChallenge(challenge, [
CreateChallengeValue(challenge.prompts[0], unassociatedPassword),
])
}
await application.prepareForLaunch({ receiveChallenge })
await application.launch(true)
@@ -272,7 +274,9 @@ describe('key recovery service', function () {
expect(result.error).to.not.be.ok
expect(contextB.application.items.getAnyItems(ContentType.ItemsKey).length).to.equal(2)
const newItemsKey = contextB.application.items.getDisplayableItemsKeys().find((k) => k.uuid !== originalItemsKey.uuid)
const newItemsKey = contextB.application.items
.getDisplayableItemsKeys()
.find((k) => k.uuid !== originalItemsKey.uuid)
const note = await Factory.createSyncedNote(contextB.application)
@@ -432,6 +436,7 @@ describe('key recovery service', function () {
expect(decryptedKey.content.itemsKey).to.equal(correctItemsKey.content.itemsKey)
expect(application.syncService.isOutOfSync()).to.equal(false)
await context.deinit()
})
@@ -457,6 +462,8 @@ describe('key recovery service', function () {
updated_at: newUpdated,
})
context.disableKeyRecovery()
await context.receiveServerResponse({ retrievedItems: [errored.ejected()] })
/** Our current items key should not be overwritten */
@@ -567,7 +574,9 @@ describe('key recovery service', function () {
const application = context.application
const receiveChallenge = (challenge) => {
/** Give unassociated password when prompted */
application.submitValuesForChallenge(challenge, [CreateChallengeValue(challenge.prompts[0], unassociatedPassword)])
application.submitValuesForChallenge(challenge, [
CreateChallengeValue(challenge.prompts[0], unassociatedPassword),
])
}
await application.prepareForLaunch({ receiveChallenge })
await application.launch(true)
@@ -667,13 +676,15 @@ describe('key recovery service', function () {
const stored = (await appA.deviceInterface.getAllDatabaseEntries(appA.identifier)).find(
(payload) => payload.uuid === newDefaultKey.uuid,
)
const storedParams = await appA.protocolService.getKeyEmbeddedKeyParams(new EncryptedPayload(stored))
const storedParams = await appA.protocolService.getKeyEmbeddedKeyParamsFromItemsKey(new EncryptedPayload(stored))
const correctStored = (await appB.deviceInterface.getAllDatabaseEntries(appB.identifier)).find(
(payload) => payload.uuid === newDefaultKey.uuid,
)
const correctParams = await appB.protocolService.getKeyEmbeddedKeyParams(new EncryptedPayload(correctStored))
const correctParams = await appB.protocolService.getKeyEmbeddedKeyParamsFromItemsKey(
new EncryptedPayload(correctStored),
)
expect(storedParams).to.eql(correctParams)

View File

@@ -141,7 +141,8 @@ describe('keys', function () {
})
it('should use items key for encryption of note', async function () {
const keyToUse = await this.application.protocolService.itemsEncryption.keyToUseForItemEncryption()
const notePayload = Factory.createNotePayload()
const keyToUse = await this.application.protocolService.itemsEncryption.keyToUseForItemEncryption(notePayload)
expect(keyToUse.content_type).to.equal(ContentType.ItemsKey)
})
@@ -153,7 +154,7 @@ describe('keys', function () {
},
})
const itemsKey = this.application.protocolService.itemsKeyForPayload(encryptedPayload)
const itemsKey = this.application.protocolService.itemsKeyForEncryptedPayload(encryptedPayload)
expect(itemsKey).to.be.ok
})
@@ -166,7 +167,7 @@ describe('keys', function () {
},
})
const itemsKey = this.application.protocolService.itemsKeyForPayload(encryptedPayload)
const itemsKey = this.application.protocolService.itemsKeyForEncryptedPayload(encryptedPayload)
expect(itemsKey).to.be.ok
const decryptedPayload = await this.application.protocolService.decryptSplitSingle({
@@ -187,7 +188,7 @@ describe('keys', function () {
},
})
const itemsKey = this.application.protocolService.itemsKeyForPayload(encryptedPayload)
const itemsKey = this.application.protocolService.itemsKeyForEncryptedPayload(encryptedPayload)
await this.application.itemManager.removeItemLocally(itemsKey)
@@ -197,14 +198,14 @@ describe('keys', function () {
},
})
await this.application.itemManager.emitItemsFromPayloads([erroredPayload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([erroredPayload], PayloadEmitSource.LocalChanged)
const note = this.application.itemManager.findAnyItem(notePayload.uuid)
expect(note.errorDecrypting).to.equal(true)
expect(note.waitingForKey).to.equal(true)
const keyPayload = new DecryptedPayload(itemsKey.payload.ejected())
await this.application.itemManager.emitItemsFromPayloads([keyPayload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([keyPayload], PayloadEmitSource.LocalChanged)
/**
* Sleeping is required to trigger asyncronous protocolService.decryptItemsWaitingForKeys,
@@ -238,7 +239,7 @@ describe('keys', function () {
},
})
await this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [] }, response)
await this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [], options: {} }, response)
const refreshedKey = this.application.payloadManager.findOne(itemsKey.uuid)
@@ -273,10 +274,8 @@ describe('keys', function () {
const rawPayloads = await this.application.diskStorageService.getAllRawPayloads()
const itemsKeyRawPayload = rawPayloads.find((p) => p.uuid === itemsKey.uuid)
const itemsKeyPayload = new EncryptedPayload(itemsKeyRawPayload)
const operator = this.application.protocolService.operatorManager.operatorForVersion(ProtocolVersion.V004)
const comps = operator.deconstructEncryptedPayloadString(itemsKeyPayload.content)
const rawAuthenticatedData = comps.authenticatedData
const authenticatedData = await operator.stringToAuthenticatedData(rawAuthenticatedData)
const authenticatedData = this.context.encryption.getEmbeddedPayloadAuthenticatedData(itemsKeyPayload)
const rootKeyParams = await this.application.protocolService.getRootKeyParams()
expect(authenticatedData.kp).to.be.ok
@@ -649,7 +648,7 @@ describe('keys', function () {
await contextB.deinit()
})
describe('changing password on 003 client while signed into 004 client should', function () {
describe('changing password on 003 client while signed into 004 client', function () {
/**
* When an 004 client signs into 003 account, it creates a root key based items key.
* Then, if the 003 client changes its account password, and the 004 client
@@ -658,7 +657,7 @@ describe('keys', function () {
* items sync to the 004 client, it can't decrypt them with its existing items key
* because its based on the old root key.
*/
it.skip('add new items key', async function () {
it.skip('should add new items key', async function () {
this.timeout(Factory.TwentySecondTimeout * 3)
let oldClient = this.application
@@ -718,7 +717,13 @@ describe('keys', function () {
await Factory.safeDeinit(oldClient)
})
it('add new items key from migration if pw change already happened', async function () {
it('should add new items key from migration if pw change already happened', async function () {
this.context.anticipateConsoleError('Shared vault network errors due to not accepting JWT-based token')
this.context.anticipateConsoleError(
'Cannot find items key to use for encryption',
'No items keys being created in this test',
)
/** Register an 003 account */
await Factory.registerOldUser({
application: this.application,
@@ -734,7 +739,15 @@ describe('keys', function () {
await this.application.protocolService.getRootKeyParams(),
)
const operator = this.application.protocolService.operatorManager.operatorForVersion(ProtocolVersion.V003)
const newRootKey = await operator.createRootKey(this.email, this.password)
const newRootKeyTemplate = await operator.createRootKey(this.email, this.password)
const newRootKey = CreateNewRootKey({
...newRootKeyTemplate.content,
...{
encryptionKeyPair: {},
signingKeyPair: {},
},
})
Object.defineProperty(this.application.apiService, 'apiVersion', {
get: function () {
return '20190520'
@@ -748,7 +761,7 @@ describe('keys', function () {
currentServerPassword: currentRootKey.serverPassword,
newRootKey,
})
await this.application.protocolService.reencryptItemsKeys()
await this.application.protocolService.reencryptApplicableItemsAfterUserRootKeyChange()
/** Note: this may result in a deadlock if features_service syncs and results in an error */
await this.application.sync.sync({ awaitAll: true })
@@ -776,11 +789,16 @@ describe('keys', function () {
* The corrective action was to do a final check in protocolService.handleDownloadFirstSyncCompletion
* to ensure there exists an items key corresponding to the user's account version.
*/
const promise = this.context.awaitNextSucessfulSync()
await this.context.sync()
await promise
await this.application.itemManager.removeAllItemsFromMemory()
expect(this.application.protocolService.getSureDefaultItemsKey()).to.not.be.ok
const protocol003 = new SNProtocolOperator003(new SNWebCrypto())
const key = await protocol003.createItemsKey()
await this.application.itemManager.emitItemFromPayload(
await this.application.mutator.emitItemFromPayload(
key.payload.copy({
content: {
...key.payload.content,
@@ -791,17 +809,21 @@ describe('keys', function () {
updated_at: Date.now(),
}),
)
const defaultKey = this.application.protocolService.getSureDefaultItemsKey()
expect(defaultKey.keyVersion).to.equal(ProtocolVersion.V003)
expect(defaultKey.uuid).to.equal(key.uuid)
await Factory.registerUserToApplication({ application: this.application })
expect(await this.application.protocolService.itemsEncryption.keyToUseForItemEncryption()).to.be.ok
const notePayload = Factory.createNotePayload()
expect(await this.application.protocolService.itemsEncryption.keyToUseForItemEncryption(notePayload)).to.be.ok
})
it('having unsynced items keys should resync them upon download first sync completion', async function () {
await Factory.registerUserToApplication({ application: this.application })
const itemsKey = this.application.itemManager.getDisplayableItemsKeys()[0]
await this.application.itemManager.emitItemFromPayload(
await this.application.mutator.emitItemFromPayload(
itemsKey.payload.copy({
dirty: false,
updated_at: new Date(0),

View File

@@ -2,6 +2,7 @@ import FakeWebCrypto from './fake_web_crypto.js'
import * as Applications from './Applications.js'
import * as Utils from './Utils.js'
import * as Defaults from './Defaults.js'
import * as Events from './Events.js'
import { createNotePayload } from './Items.js'
UuidGenerator.SetGenerator(new FakeWebCrypto().generateUUID)
@@ -11,6 +12,8 @@ const MaximumSyncOptions = {
awaitAll: true,
}
let GlobalSubscriptionIdCounter = 1001
export class AppContext {
constructor({ identifier, crypto, email, password, passcode, host } = {}) {
this.identifier = identifier || `${Math.random()}`
@@ -46,6 +49,62 @@ export class AppContext {
)
}
get vaults() {
return this.application.vaultService
}
get sessions() {
return this.application.sessions
}
get items() {
return this.application.items
}
get mutator() {
return this.application.mutator
}
get payloads() {
return this.application.payloadManager
}
get encryption() {
return this.application.protocolService
}
get contacts() {
return this.application.contactService
}
get sharedVaults() {
return this.application.sharedVaultService
}
get files() {
return this.application.fileService
}
get keys() {
return this.application.keySystemKeyManager
}
get asymmetric() {
return this.application.asymmetricMessageService
}
get publicKey() {
return this.sessions.getPublicKey()
}
get signingPublicKey() {
return this.sessions.getSigningPublicKey()
}
get privateKey() {
return this.encryption.getKeyPair().privateKey
}
ignoreChallenges() {
this.ignoringChallenges = true
}
@@ -118,7 +177,10 @@ export class AppContext {
},
})
return this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [] }, response)
return this.application.syncService.handleSuccessServerResponse(
{ payloadsSavedOrSaving: [], options: {} },
response,
)
}
resolveWhenKeyRecovered(uuid) {
@@ -131,6 +193,16 @@ export class AppContext {
})
}
resolveWhenSharedVaultUserKeysResolved() {
return new Promise((resolve) => {
this.application.vaultService.collaboration.addEventObserver((eventName) => {
if (eventName === SharedVaultServiceEvent.SharedVaultStatusChanged) {
resolve()
}
})
})
}
async awaitSignInEvent() {
return new Promise((resolve) => {
this.application.userService.addEventObserver((eventName) => {
@@ -182,6 +254,155 @@ export class AppContext {
})
}
awaitNextSyncSharedVaultFromScratchEvent() {
return new Promise((resolve) => {
const removeObserver = this.application.syncService.addEventObserver((event, data) => {
if (event === SyncEvent.PaginatedSyncRequestCompleted && data?.options?.sharedVaultUuids) {
removeObserver()
resolve(data)
}
})
})
}
resolveWithUploadedPayloads() {
return new Promise((resolve) => {
this.application.syncService.addEventObserver((event, data) => {
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
resolve(data.uploadedPayloads)
}
})
})
}
resolveWithConflicts() {
return new Promise((resolve) => {
this.application.syncService.addEventObserver((event, response) => {
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
resolve(response.rawConflictObjects)
}
})
})
}
resolveWhenSavedSyncPayloadsIncludesItemUuid(uuid) {
return new Promise((resolve) => {
this.application.syncService.addEventObserver((event, response) => {
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
const savedPayload = response.savedPayloads.find((payload) => payload.uuid === uuid)
if (savedPayload) {
resolve()
}
}
})
})
}
resolveWhenSavedSyncPayloadsIncludesItemThatIsDuplicatedOf(uuid) {
return new Promise((resolve) => {
this.application.syncService.addEventObserver((event, response) => {
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
const savedPayload = response.savedPayloads.find((payload) => payload.duplicate_of === uuid)
if (savedPayload) {
resolve()
}
}
})
})
}
resolveWhenItemCompletesAddingToVault(targetItem) {
return new Promise((resolve) => {
const objectToSpy = this.vaults
sinon.stub(objectToSpy, 'moveItemToVault').callsFake(async (vault, item) => {
objectToSpy.moveItemToVault.restore()
const result = await objectToSpy.moveItemToVault(vault, item)
if (!targetItem || item.uuid === targetItem.uuid) {
resolve()
}
return result
})
})
}
resolveWhenItemCompletesRemovingFromVault(targetItem) {
return new Promise((resolve) => {
const objectToSpy = this.vaults
sinon.stub(objectToSpy, 'removeItemFromVault').callsFake(async (item) => {
objectToSpy.removeItemFromVault.restore()
const result = await objectToSpy.removeItemFromVault(item)
if (item.uuid === targetItem.uuid) {
resolve()
}
return result
})
})
}
resolveWhenAsymmetricMessageProcessingCompletes() {
return new Promise((resolve) => {
const objectToSpy = this.asymmetric
sinon.stub(objectToSpy, 'handleRemoteReceivedAsymmetricMessages').callsFake(async (messages) => {
objectToSpy.handleRemoteReceivedAsymmetricMessages.restore()
const result = await objectToSpy.handleRemoteReceivedAsymmetricMessages(messages)
resolve()
return result
})
})
}
resolveWhenUserMessagesProcessingCompletes() {
return new Promise((resolve) => {
const objectToSpy = this.application.userEventService
sinon.stub(objectToSpy, 'handleReceivedUserEvents').callsFake(async (params) => {
objectToSpy.handleReceivedUserEvents.restore()
const result = await objectToSpy.handleReceivedUserEvents(params)
resolve()
return result
})
})
}
resolveWhenSharedVaultServiceSendsContactShareMessage() {
return new Promise((resolve) => {
const objectToSpy = this.sharedVaults
sinon.stub(objectToSpy, 'shareContactWithUserAdministeredSharedVaults').callsFake(async (contact) => {
objectToSpy.shareContactWithUserAdministeredSharedVaults.restore()
const result = await objectToSpy.shareContactWithUserAdministeredSharedVaults(contact)
resolve()
return result
})
})
}
resolveWhenSharedVaultKeyRotationInvitesGetSent(targetVault) {
return new Promise((resolve) => {
const objectToSpy = this.sharedVaults
sinon.stub(objectToSpy, 'handleVaultRootKeyRotatedEvent').callsFake(async (vault) => {
objectToSpy.handleVaultRootKeyRotatedEvent.restore()
const result = await objectToSpy.handleVaultRootKeyRotatedEvent(vault)
if (vault.systemIdentifier === targetVault.systemIdentifier) {
resolve()
}
return result
})
})
}
resolveWhenSharedVaultChangeInvitesAreSent(sharedVaultUuid) {
return new Promise((resolve) => {
const objectToSpy = this.sharedVaults
sinon.stub(objectToSpy, 'handleVaultRootKeyRotatedEvent').callsFake(async (vault) => {
objectToSpy.handleVaultRootKeyRotatedEvent.restore()
const result = await objectToSpy.handleVaultRootKeyRotatedEvent(vault)
if (vault.sharing.sharedVaultUuid === sharedVaultUuid) {
resolve()
}
return result
})
})
}
awaitUserPrefsSingletonCreation() {
const preferences = this.application.preferencesService.preferences
if (preferences) {
@@ -232,6 +453,10 @@ export class AppContext {
await this.application.sync.sync(options || { awaitAll: true })
}
async clearSyncPositionTokens() {
await this.application.sync.clearSyncPositionTokens()
}
async maximumSync() {
await this.sync(MaximumSyncOptions)
}
@@ -290,22 +515,31 @@ export class AppContext {
})
}
async createSyncedNote(title, text) {
async createSyncedNote(title = 'foo', text = 'bar') {
const payload = createNotePayload(title, text)
const item = await this.application.items.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
await this.application.items.setItemDirty(item)
const item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
await this.application.mutator.setItemDirty(item)
await this.application.syncService.sync(MaximumSyncOptions)
const note = this.application.items.findItem(payload.uuid)
return note
}
lockSyncing() {
this.application.syncService.lockSyncing()
}
unlockSyncing() {
this.application.syncService.unlockSyncing()
}
async deleteItemAndSync(item) {
await this.application.mutator.deleteItem(item)
await this.sync()
}
async changeNoteTitle(note, title) {
return this.application.items.changeNote(note, (mutator) => {
return this.application.mutator.changeNote(note, (mutator) => {
mutator.title = title
})
}
@@ -325,6 +559,10 @@ export class AppContext {
return this.application.items.getDisplayableNotes().length
}
get notes() {
return this.application.items.getDisplayableNotes()
}
async createConflictedNotes(otherContext) {
const note = await this.createSyncedNote()
@@ -341,4 +579,41 @@ export class AppContext {
conflict: this.findNoteByTitle('title-2'),
}
}
findDuplicateNote(duplicateOfUuid) {
const items = this.items.getDisplayableNotes()
return items.find((note) => note.duplicateOf === duplicateOfUuid)
}
get userUuid() {
return this.application.sessions.user.uuid
}
sleep(seconds) {
return Utils.sleep(seconds)
}
anticipateConsoleError(message, _reason) {
console.warn('Anticipating a console error with message:', message)
}
async publicMockSubscriptionPurchaseEvent() {
await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
userEmail: this.email,
subscriptionId: GlobalSubscriptionIdCounter++,
subscriptionName: 'PRO_PLAN',
subscriptionExpiresAt: (new Date().getTime() + 3_600_000) * 1_000,
timestamp: Date.now(),
offline: false,
discountCode: null,
limitedDiscountPurchased: false,
newSubscriber: true,
totalActiveSubscriptionsCount: 1,
userRegisteredAt: 1,
billingFrequency: 12,
payAmount: 59.0,
})
await Utils.sleep(2)
}
}

View File

@@ -2,10 +2,6 @@ import WebDeviceInterface from './web_device_interface.js'
import FakeWebCrypto from './fake_web_crypto.js'
import * as Defaults from './Defaults.js'
export const BaseItemCounts = {
DefaultItems: ['ItemsKey', 'UserPreferences', 'DarkTheme'].length,
}
export function createApplicationWithOptions({ identifier, environment, platform, host, crypto, device }) {
if (!device) {
device = new WebDeviceInterface()

View File

@@ -0,0 +1,35 @@
const ExpectedItemCountsWithVaultFeatureEnabled = {
Items: ['ItemsKey', 'UserPreferences', 'DarkTheme'].length,
ItemsWithAccount: ['ItemsKey', 'UserPreferences', 'DarkTheme', 'TrustedSelfContact'].length,
ItemsWithAccountWithoutItemsKey: ['UserPreferences', 'DarkTheme', 'TrustedSelfContact'].length,
ItemsNoAccounNoItemsKey: ['UserPreferences', 'DarkTheme'].length,
BackupFileRootKeyEncryptedItems: ['TrustedSelfContact'].length,
}
const ExpectedItemCountsWithVaultFeatureDisabled = {
Items: ['ItemsKey', 'UserPreferences', 'DarkTheme'].length,
ItemsWithAccount: ['ItemsKey', 'UserPreferences', 'DarkTheme'].length,
ItemsWithAccountWithoutItemsKey: ['UserPreferences', 'DarkTheme'].length,
ItemsNoAccounNoItemsKey: ['UserPreferences', 'DarkTheme'].length,
BackupFileRootKeyEncryptedItems: [].length,
}
const isVaultsEnabled = InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)
export const BaseItemCounts = {
DefaultItems: isVaultsEnabled
? ExpectedItemCountsWithVaultFeatureEnabled.Items
: ExpectedItemCountsWithVaultFeatureDisabled.Items,
DefaultItemsWithAccount: isVaultsEnabled
? ExpectedItemCountsWithVaultFeatureEnabled.ItemsWithAccount
: ExpectedItemCountsWithVaultFeatureDisabled.ItemsWithAccount,
DefaultItemsWithAccountWithoutItemsKey: isVaultsEnabled
? ExpectedItemCountsWithVaultFeatureEnabled.ItemsWithAccountWithoutItemsKey
: ExpectedItemCountsWithVaultFeatureDisabled.ItemsWithAccountWithoutItemsKey,
DefaultItemsNoAccounNoItemsKey: isVaultsEnabled
? ExpectedItemCountsWithVaultFeatureEnabled.ItemsNoAccounNoItemsKey
: ExpectedItemCountsWithVaultFeatureDisabled.ItemsNoAccounNoItemsKey,
BackupFileRootKeyEncryptedItems: isVaultsEnabled
? ExpectedItemCountsWithVaultFeatureEnabled.BackupFileRootKeyEncryptedItems
: ExpectedItemCountsWithVaultFeatureDisabled.BackupFileRootKeyEncryptedItems,
}

View File

@@ -0,0 +1,140 @@
import * as Factory from './factory.js'
export const createContactContext = async () => {
const contactContext = await Factory.createAppContextWithRealCrypto()
await contactContext.launch()
await contactContext.register()
return {
contactContext,
deinitContactContext: contactContext.deinit.bind(contactContext),
}
}
export const createTrustedContactForUserOfContext = async (
contextAddingNewContact,
contextImportingContactInfoFrom,
) => {
const contact = await contextAddingNewContact.application.contactService.createOrEditTrustedContact({
name: 'John Doe',
publicKey: contextImportingContactInfoFrom.publicKey,
signingPublicKey: contextImportingContactInfoFrom.signingPublicKey,
contactUuid: contextImportingContactInfoFrom.userUuid,
})
return contact
}
export const acceptAllInvites = async (context) => {
const inviteRecords = context.sharedVaults.getCachedPendingInviteRecords()
for (const record of inviteRecords) {
await context.sharedVaults.acceptPendingSharedVaultInvite(record)
}
}
export const createSharedVaultWithAcceptedInvite = async (context, permissions = SharedVaultPermission.Write) => {
const { sharedVault, contact, contactContext, deinitContactContext } =
await createSharedVaultWithUnacceptedButTrustedInvite(context, permissions)
const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent()
await acceptAllInvites(contactContext)
await promise
const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
return { sharedVault, contact, contactVault, contactContext, deinitContactContext }
}
export const createSharedVaultWithAcceptedInviteAndNote = async (
context,
permissions = SharedVaultPermission.Write,
) => {
const { sharedVault, contactContext, contact, deinitContactContext } = await createSharedVaultWithAcceptedInvite(
context,
permissions,
)
const note = await context.createSyncedNote('foo', 'bar')
const updatedNote = await moveItemToVault(context, sharedVault, note)
await contactContext.sync()
return { sharedVault, note: updatedNote, contact, contactContext, deinitContactContext }
}
export const createSharedVaultWithUnacceptedButTrustedInvite = async (
context,
permissions = SharedVaultPermission.Write,
) => {
const sharedVault = await createSharedVault(context)
const { contactContext, deinitContactContext } = await createContactContext()
const contact = await createTrustedContactForUserOfContext(context, contactContext)
await createTrustedContactForUserOfContext(contactContext, context)
const invite = await context.sharedVaults.inviteContactToSharedVault(sharedVault, contact, permissions)
await contactContext.sync()
return { sharedVault, contact, contactContext, deinitContactContext, invite }
}
export const createSharedVaultWithUnacceptedAndUntrustedInvite = async (
context,
permissions = SharedVaultPermission.Write,
) => {
const sharedVault = await createSharedVault(context)
const { contactContext, deinitContactContext } = await createContactContext()
const contact = await createTrustedContactForUserOfContext(context, contactContext)
const invite = await context.sharedVaults.inviteContactToSharedVault(sharedVault, contact, permissions)
await contactContext.sync()
return { sharedVault, contact, contactContext, deinitContactContext, invite }
}
export const inviteNewPartyToSharedVault = async (context, sharedVault, permissions = SharedVaultPermission.Write) => {
const { contactContext: thirdPartyContext, deinitContactContext: deinitThirdPartyContext } =
await createContactContext()
const thirdPartyContact = await createTrustedContactForUserOfContext(context, thirdPartyContext)
await createTrustedContactForUserOfContext(thirdPartyContext, context)
await context.sharedVaults.inviteContactToSharedVault(sharedVault, thirdPartyContact, permissions)
await thirdPartyContext.sync()
return { thirdPartyContext, thirdPartyContact, deinitThirdPartyContext }
}
export const createPrivateVault = async (context) => {
const privateVault = await context.vaults.createRandomizedVault({
name: 'My Private Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
return privateVault
}
export const createSharedVault = async (context) => {
const sharedVault = await context.sharedVaults.createSharedVault({ name: 'My Shared Vault' })
if (isClientDisplayableError(sharedVault)) {
throw new Error(sharedVault.text)
}
return sharedVault
}
export const createSharedVaultWithNote = async (context) => {
const sharedVault = await createSharedVault(context)
const note = await context.createSyncedNote()
const updatedNote = await moveItemToVault(context, sharedVault, note)
return { sharedVault, note: updatedNote }
}
export const moveItemToVault = async (context, sharedVault, item) => {
const promise = context.resolveWhenItemCompletesAddingToVault(item)
const updatedItem = await context.vaults.moveItemToVault(sharedVault, item)
await promise
return updatedItem
}

View File

@@ -0,0 +1,19 @@
import * as Defaults from './Defaults.js'
export async function publishMockedEvent(eventType, eventPayload) {
const response = await fetch(`${Defaults.getDefaultMockedEventServiceUrl()}/events`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
eventType,
eventPayload,
}),
})
if (!response.ok) {
console.error(`Failed to publish mocked event: ${response.status} ${response.statusText}`)
}
}

View File

@@ -1,5 +1,5 @@
export async function uploadFile(fileService, buffer, name, ext, chunkSize) {
const operation = await fileService.beginNewFileUpload(buffer.byteLength)
export async function uploadFile(fileService, buffer, name, ext, chunkSize, vault) {
const operation = await fileService.beginNewFileUpload(buffer.byteLength, vault)
let chunkId = 1
for (let i = 0; i < buffer.length; i += chunkSize) {
@@ -18,14 +18,16 @@ export async function uploadFile(fileService, buffer, name, ext, chunkSize) {
return file
}
export async function downloadFile(fileService, itemManager, remoteIdentifier) {
const file = itemManager.getItems(ContentType.File).find((file) => file.remoteIdentifier === remoteIdentifier)
export async function downloadFile(fileService, file) {
let receivedBytes = new Uint8Array()
await fileService.downloadFile(file, (decryptedBytes) => {
const error = await fileService.downloadFile(file, (decryptedBytes) => {
receivedBytes = new Uint8Array([...receivedBytes, ...decryptedBytes])
})
if (error) {
throw new Error('Could not download file', error.text)
}
return receivedBytes
}

View File

@@ -63,7 +63,7 @@ export function createRelatedNoteTagPairPayload({ noteTitle, noteText, tagTitle,
export async function createSyncedNoteWithTag(application) {
const payloads = createRelatedNoteTagPairPayload()
await application.itemManager.emitItemsFromPayloads(payloads)
await application.mutator.emitItemsFromPayloads(payloads)
return application.sync.sync(MaximumSyncOptions)
}

View File

@@ -71,24 +71,6 @@ export function getDefaultHost() {
return Defaults.getDefaultHost()
}
export async function publishMockedEvent(eventType, eventPayload) {
const response = await fetch(`${Defaults.getDefaultMockedEventServiceUrl()}/events`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
eventType,
eventPayload,
}),
})
if (!response.ok) {
console.error(`Failed to publish mocked event: ${response.status} ${response.statusText}`)
}
}
export function createApplicationWithFakeCrypto(identifier, environment, platform, host) {
return Applications.createApplicationWithFakeCrypto(identifier, environment, platform, host)
}
@@ -154,7 +136,7 @@ export async function registerOldUser({ application, email, password, version })
keyParams: accountKey.keyParams,
})
/** Mark all existing items as dirty. */
await application.itemManager.changeItems(application.itemManager.items, (m) => {
await application.mutator.changeItems(application.itemManager.items, (m) => {
m.dirty = true
})
await application.sessionManager.handleSuccessAuthResponse(response, accountKey)
@@ -188,18 +170,18 @@ export function itemToStoragePayload(item) {
export function createMappedNote(application, title, text, dirty = true) {
const payload = createNotePayload(title, text, dirty)
return application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
return application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
}
export async function createMappedTag(application, tagParams = {}) {
const payload = createStorageItemTagPayload(tagParams)
return application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
return application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
}
export async function createSyncedNote(application, title, text) {
const payload = createNotePayload(title, text)
const item = await application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
await application.itemManager.setItemDirty(item)
const item = await application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
await application.mutator.setItemDirty(item)
await application.syncService.sync(syncOptions)
const note = application.items.findItem(payload.uuid)
return note
@@ -218,7 +200,7 @@ export async function createManyMappedNotes(application, count) {
const createdNotes = []
for (let i = 0; i < count; i++) {
const note = await createMappedNote(application)
await application.itemManager.setItemDirty(note)
await application.mutator.setItemDirty(note)
createdNotes.push(note)
}
return createdNotes
@@ -406,7 +388,7 @@ export function pinNote(application, note) {
}
export async function insertItemWithOverride(application, contentType, content, needsSync = false, errorDecrypting) {
const item = await application.itemManager.createItem(contentType, content, needsSync)
const item = await application.mutator.createItem(contentType, content, needsSync)
if (errorDecrypting) {
const encrypted = new EncryptedPayload({
@@ -415,12 +397,12 @@ export async function insertItemWithOverride(application, contentType, content,
errorDecrypting,
})
await application.itemManager.emitItemFromPayload(encrypted)
await application.payloadManager.emitPayload(encrypted)
} else {
const decrypted = new DecryptedPayload({
...item.payload.ejected(),
})
await application.itemManager.emitItemFromPayload(decrypted)
await application.payloadManager.emitPayload(decrypted)
}
return application.itemManager.findAnyItem(item.uuid)
@@ -441,7 +423,7 @@ export async function markDirtyAndSyncItem(application, itemToLookupUuidFor) {
throw Error('Attempting to save non-inserted item')
}
if (!item.dirty) {
await application.itemManager.changeItem(item, undefined, MutationType.NoUpdateUserTimestamps)
await application.mutator.changeItem(item, undefined, MutationType.NoUpdateUserTimestamps)
}
await application.sync.sync()
}
@@ -467,23 +449,22 @@ export async function changePayloadTimeStamp(application, payload, timestamp, co
updated_at_timestamp: timestamp,
})
await application.itemManager.emitItemFromPayload(changedPayload)
await application.mutator.emitItemFromPayload(changedPayload)
return application.itemManager.findAnyItem(payload.uuid)
}
export async function changePayloadUpdatedAt(application, payload, updatedAt) {
const latestPayload = application.payloadManager.collection.find(payload.uuid)
const changedPayload = new DecryptedPayload({
...latestPayload,
...latestPayload.ejected(),
dirty: true,
dirtyIndex: getIncrementedDirtyIndex(),
updated_at: updatedAt,
})
await application.itemManager.emitItemFromPayload(changedPayload)
return application.itemManager.findAnyItem(payload.uuid)
return application.mutator.emitItemFromPayload(changedPayload)
}
export async function changePayloadTimeStampDeleteAndSync(application, payload, timestamp, syncOptions) {
@@ -497,6 +478,6 @@ export async function changePayloadTimeStampDeleteAndSync(application, payload,
updated_at_timestamp: timestamp,
})
await application.itemManager.emitItemFromPayload(changedPayload)
await application.payloadManager.emitPayload(changedPayload)
await application.sync.sync(syncOptions)
}

View File

@@ -158,8 +158,39 @@ export default class FakeWebCrypto {
return data.message
}
sodiumCryptoBoxGenerateKeypair() {
return { publicKey: this.randomString(64), privateKey: this.randomString(64), keyType: 'x25519' }
sodiumCryptoSign(message, secretKey) {
const data = {
message,
secretKey,
}
return btoa(JSON.stringify(data))
}
sodiumCryptoKdfDeriveFromKey(key, subkeyNumber, subkeyLength, context) {
return btoa(key + subkeyNumber + subkeyLength + context)
}
sodiumCryptoGenericHash(message, key) {
return btoa(message + key)
}
sodiumCryptoSignVerify(message, signature, publicKey) {
return true
}
sodiumCryptoBoxSeedKeypair(seed) {
return {
privateKey: seed,
publicKey: seed,
}
}
sodiumCryptoSignSeedKeypair(seed) {
return {
privateKey: seed,
publicKey: seed,
}
}
generateOtpSecret() {

View File

@@ -37,6 +37,10 @@ export default class WebDeviceInterface {
return {}
}
clearAllDataFromDevice() {
localStorage.clear()
}
_getDatabaseKeyPrefix(identifier) {
if (identifier) {
return `${identifier}-item-`
@@ -61,29 +65,45 @@ export default class WebDeviceInterface {
async getDatabaseLoadChunks(options, identifier) {
const entries = await this.getAllDatabaseEntries(identifier)
const sorted = GetSortedPayloadsByPriority(entries, options)
const {
itemsKeyPayloads,
keySystemRootKeyPayloads,
keySystemItemsKeyPayloads,
contentTypePriorityPayloads,
remainingPayloads,
} = GetSortedPayloadsByPriority(entries, options)
const itemsKeysChunk = {
entries: sorted.itemsKeyPayloads,
entries: itemsKeyPayloads,
}
const keySystemRootKeysChunk = {
entries: keySystemRootKeyPayloads,
}
const keySystemItemsKeysChunk = {
entries: keySystemItemsKeyPayloads,
}
const contentTypePriorityChunk = {
entries: sorted.contentTypePriorityPayloads,
entries: contentTypePriorityPayloads,
}
const remainingPayloadsChunks = []
for (let i = 0; i < sorted.remainingPayloads.length; i += options.batchSize) {
for (let i = 0; i < remainingPayloads.length; i += options.batchSize) {
remainingPayloadsChunks.push({
entries: sorted.remainingPayloads.slice(i, i + options.batchSize),
entries: remainingPayloads.slice(i, i + options.batchSize),
})
}
const result = {
fullEntries: {
itemsKeys: itemsKeysChunk,
keySystemRootKeys: keySystemRootKeysChunk,
keySystemItemsKeys: keySystemItemsKeysChunk,
remainingChunks: [contentTypePriorityChunk, ...remainingPayloadsChunks],
},
remainingChunksItemCount: sorted.contentTypePriorityPayloads.length + sorted.remainingPayloads.length,
remainingChunksItemCount: contentTypePriorityPayloads.length + remainingPayloads.length,
}
return result

View File

@@ -68,7 +68,7 @@ describe('migrations', () => {
}),
}),
)
await application.mutator.insertItem(mfaItem)
await application.mutator.insertItem(mfaItem, true)
await application.sync.sync()
expect(application.items.getItems('SF|MFA').length).to.equal(1)

View File

@@ -1,7 +1,7 @@
/* eslint-disable camelcase */
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import { BaseItemCounts } from '../lib/Applications.js'
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -70,8 +70,8 @@ describe('app models', () => {
},
})
await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
await this.application.itemManager.emitItemsFromPayloads([params2], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([params2], PayloadEmitSource.LocalChanged)
const item1 = this.application.itemManager.findItem(params1.uuid)
const item2 = this.application.itemManager.findItem(params2.uuid)
@@ -93,11 +93,11 @@ describe('app models', () => {
},
})
let items = await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
let items = await this.application.mutator.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
let item = items[0]
expect(item).to.be.ok
items = await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
items = await this.application.mutator.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
item = items[0]
expect(item.content.foo).to.equal('bar')
@@ -108,10 +108,10 @@ describe('app models', () => {
const item1 = await Factory.createMappedNote(this.application)
const item2 = await Factory.createMappedNote(this.application)
await this.application.itemManager.changeItem(item1, (mutator) => {
await this.application.mutator.changeItem(item1, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
})
await this.application.itemManager.changeItem(item2, (mutator) => {
await this.application.mutator.changeItem(item2, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
})
@@ -123,10 +123,10 @@ describe('app models', () => {
var item1 = await Factory.createMappedNote(this.application)
var item2 = await Factory.createMappedNote(this.application)
await this.application.itemManager.changeItem(item1, (mutator) => {
await this.application.mutator.changeItem(item1, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
})
await this.application.itemManager.changeItem(item2, (mutator) => {
await this.application.mutator.changeItem(item2, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
})
@@ -143,7 +143,7 @@ describe('app models', () => {
references: [],
},
})
await this.application.itemManager.emitItemsFromPayloads([damagedPayload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([damagedPayload], PayloadEmitSource.LocalChanged)
const refreshedItem1_2 = this.application.itemManager.findItem(item1.uuid)
const refreshedItem2_2 = this.application.itemManager.findItem(item2.uuid)
@@ -155,10 +155,10 @@ describe('app models', () => {
it('creating and removing relationships between two items should have valid references', async function () {
var item1 = await Factory.createMappedNote(this.application)
var item2 = await Factory.createMappedNote(this.application)
await this.application.itemManager.changeItem(item1, (mutator) => {
await this.application.mutator.changeItem(item1, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
})
await this.application.itemManager.changeItem(item2, (mutator) => {
await this.application.mutator.changeItem(item2, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
})
@@ -171,10 +171,10 @@ describe('app models', () => {
expect(this.application.itemManager.itemsReferencingItem(item1)).to.include(refreshedItem2)
expect(this.application.itemManager.itemsReferencingItem(item2)).to.include(refreshedItem1)
await this.application.itemManager.changeItem(item1, (mutator) => {
await this.application.mutator.changeItem(item1, (mutator) => {
mutator.removeItemAsRelationship(item2)
})
await this.application.itemManager.changeItem(item2, (mutator) => {
await this.application.mutator.changeItem(item2, (mutator) => {
mutator.removeItemAsRelationship(item1)
})
@@ -190,7 +190,7 @@ describe('app models', () => {
it('properly duplicates item with no relationships', async function () {
const item = await Factory.createMappedNote(this.application)
const duplicate = await this.application.itemManager.duplicateItem(item)
const duplicate = await this.application.mutator.duplicateItem(item)
expect(duplicate.uuid).to.not.equal(item.uuid)
expect(item.isItemContentEqualWith(duplicate)).to.equal(true)
expect(item.created_at.toISOString()).to.equal(duplicate.created_at.toISOString())
@@ -201,13 +201,13 @@ describe('app models', () => {
const item1 = await Factory.createMappedNote(this.application)
const item2 = await Factory.createMappedNote(this.application)
const refreshedItem1 = await this.application.itemManager.changeItem(item1, (mutator) => {
const refreshedItem1 = await this.application.mutator.changeItem(item1, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
})
expect(refreshedItem1.content.references.length).to.equal(1)
const duplicate = await this.application.itemManager.duplicateItem(item1)
const duplicate = await this.application.mutator.duplicateItem(item1)
expect(duplicate.uuid).to.not.equal(item1.uuid)
expect(duplicate.content.references.length).to.equal(1)
@@ -223,11 +223,11 @@ describe('app models', () => {
it('removing references should update cross-refs', async function () {
const item1 = await Factory.createMappedNote(this.application)
const item2 = await Factory.createMappedNote(this.application)
const refreshedItem1 = await this.application.itemManager.changeItem(item1, (mutator) => {
const refreshedItem1 = await this.application.mutator.changeItem(item1, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
})
const refreshedItem1_2 = await this.application.itemManager.emitItemFromPayload(
const refreshedItem1_2 = await this.application.mutator.emitItemFromPayload(
refreshedItem1.payloadRepresentation({
deleted: true,
content: {
@@ -247,7 +247,7 @@ describe('app models', () => {
const item1 = await Factory.createMappedNote(this.application)
const item2 = await Factory.createMappedNote(this.application)
const refreshedItem1 = await this.application.itemManager.changeItem(item1, (mutator) => {
const refreshedItem1 = await this.application.mutator.changeItem(item1, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
})
@@ -290,12 +290,12 @@ describe('app models', () => {
waitingForKey: true,
})
await this.application.itemManager.emitItemFromPayload(errored)
await this.application.payloadManager.emitPayload(errored)
expect(this.application.payloadManager.findOne(item1.uuid).errorDecrypting).to.equal(true)
expect(this.application.payloadManager.findOne(item1.uuid).items_key_id).to.equal(itemsKey.uuid)
sinon.stub(this.application.protocolService.itemsEncryption, 'decryptErroredPayloads').callsFake(() => {
sinon.stub(this.application.protocolService.itemsEncryption, 'decryptErroredItemPayloads').callsFake(() => {
// prevent auto decryption
})
@@ -310,7 +310,7 @@ describe('app models', () => {
const item2 = await Factory.createMappedNote(this.application)
this.expectedItemCount += 2
await this.application.itemManager.changeItem(item1, (mutator) => {
await this.application.mutator.changeItem(item1, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
})
@@ -339,13 +339,13 @@ describe('app models', () => {
it('maintains referencing relationships when duplicating', async function () {
const tag = await Factory.createMappedTag(this.application)
const note = await Factory.createMappedNote(this.application)
const refreshedTag = await this.application.itemManager.changeItem(tag, (mutator) => {
const refreshedTag = await this.application.mutator.changeItem(tag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(note)
})
expect(refreshedTag.content.references.length).to.equal(1)
const noteCopy = await this.application.itemManager.duplicateItem(note)
const noteCopy = await this.application.mutator.duplicateItem(note)
expect(note.uuid).to.not.equal(noteCopy.uuid)
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(2)
@@ -358,7 +358,7 @@ describe('app models', () => {
})
it('maintains editor reference when duplicating note', async function () {
const editor = await this.application.itemManager.createItem(
const editor = await this.application.mutator.createItem(
ContentType.Component,
{ area: ComponentArea.Editor, package_info: { identifier: 'foo-editor' } },
true,
@@ -369,7 +369,7 @@ describe('app models', () => {
expect(this.application.componentManager.editorForNote(note).uuid).to.equal(editor.uuid)
const duplicate = await this.application.itemManager.duplicateItem(note, true)
const duplicate = await this.application.mutator.duplicateItem(note, true)
expect(this.application.componentManager.editorForNote(duplicate).uuid).to.equal(editor.uuid)
})
})

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import { BaseItemCounts } from '../lib/Applications.js'
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
import * as Factory from '../lib/factory.js'
import { createRelatedNoteTagPairPayload } from '../lib/Items.js'
chai.use(chaiAsPromised)
@@ -43,7 +43,7 @@ describe('importing', function () {
it('should not import backups made from unsupported versions', async function () {
await setup({ fakeCrypto: true })
const result = await application.mutator.importData({
const result = await application.importData({
version: '-1',
items: [],
})
@@ -58,7 +58,7 @@ describe('importing', function () {
password,
version: ProtocolVersion.V003,
})
const result = await application.mutator.importData({
const result = await application.importData({
version: ProtocolVersion.V004,
items: [],
})
@@ -71,7 +71,7 @@ describe('importing', function () {
const notePayload = pair[0]
const tagPayload = pair[1]
await application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
await application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
expectedItemCount += 2
const note = application.itemManager.getItems([ContentType.Note])[0]
const tag = application.itemManager.getItems([ContentType.Tag])[0]
@@ -82,7 +82,7 @@ describe('importing', function () {
expect(note.content.references.length).to.equal(0)
expect(application.itemManager.itemsReferencingItem(note).length).to.equal(1)
await application.mutator.importData(
await application.importData(
{
items: [notePayload, tagPayload],
},
@@ -105,7 +105,7 @@ describe('importing', function () {
*/
await setup({ fakeCrypto: true })
const notePayload = Factory.createNotePayload()
await application.itemManager.emitItemFromPayload(notePayload, PayloadEmitSource.LocalChanged)
await application.mutator.emitItemFromPayload(notePayload, PayloadEmitSource.LocalChanged)
expectedItemCount++
const mutatedNote = new DecryptedPayload({
...notePayload,
@@ -114,7 +114,7 @@ describe('importing', function () {
title: `${Math.random()}`,
},
})
await application.mutator.importData(
await application.importData(
{
items: [mutatedNote, mutatedNote, mutatedNote],
},
@@ -130,7 +130,7 @@ describe('importing', function () {
await setup({ fakeCrypto: true })
const pair = createRelatedNoteTagPairPayload()
const tagPayload = pair[1]
await application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
await application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
const mutatedTag = new DecryptedPayload({
...tagPayload,
content: {
@@ -138,7 +138,7 @@ describe('importing', function () {
references: [],
},
})
await application.mutator.importData(
await application.importData(
{
items: [mutatedTag],
},
@@ -153,7 +153,7 @@ describe('importing', function () {
const pair = createRelatedNoteTagPairPayload()
const notePayload = pair[0]
const tagPayload = pair[1]
await application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
await application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
expectedItemCount += 2
const note = application.itemManager.getDisplayableNotes()[0]
const tag = application.itemManager.getDisplayableTags()[0]
@@ -171,7 +171,7 @@ describe('importing', function () {
title: `${Math.random()}`,
},
})
await application.mutator.importData(
await application.importData(
{
items: [mutatedNote, mutatedTag],
},
@@ -217,7 +217,7 @@ describe('importing', function () {
const tag = await Factory.createMappedTag(application)
expectedItemCount += 2
await application.itemManager.changeItem(tag, (mutator) => {
await application.mutator.changeItem(tag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(note)
})
@@ -240,7 +240,7 @@ describe('importing', function () {
},
)
await application.mutator.importData(
await application.importData(
{
items: [externalNote, externalTag],
},
@@ -272,12 +272,14 @@ describe('importing', function () {
await application.sync.sync({ awaitAll: true })
await application.mutator.deleteItem(note)
await application.sync.sync()
expect(application.items.findItem(note.uuid)).to.not.exist
await application.mutator.deleteItem(tag)
await application.sync.sync()
expect(application.items.findItem(tag.uuid)).to.not.exist
await application.mutator.importData(
await application.importData(
{
items: [note, tag],
},
@@ -311,7 +313,7 @@ describe('importing', function () {
password: password,
})
await application.mutator.importData(
await application.importData(
{
items: [note.payload],
},
@@ -341,7 +343,7 @@ describe('importing', function () {
password: password,
})
await application.mutator.importData(
await application.importData(
{
items: [note],
},
@@ -372,12 +374,14 @@ describe('importing', function () {
await application.sync.sync({ awaitAll: true })
await application.mutator.deleteItem(note)
await application.sync.sync()
expect(application.items.findItem(note.uuid)).to.not.exist
await application.mutator.deleteItem(tag)
await application.sync.sync()
expect(application.items.findItem(tag.uuid)).to.not.exist
await application.mutator.importData(backupData, true)
await application.importData(backupData, true)
expect(application.itemManager.getDisplayableNotes().length).to.equal(1)
expect(application.items.findItem(note.uuid).deleted).to.not.be.ok
@@ -402,7 +406,7 @@ describe('importing', function () {
application = await Factory.createInitAppWithFakeCrypto()
Factory.handlePasswordChallenges(application, password)
await application.mutator.importData(backupData, true)
await application.importData(backupData, true)
const importedNote = application.items.findItem(note.uuid)
const importedTag = application.items.findItem(tag.uuid)
@@ -427,7 +431,7 @@ describe('importing', function () {
application = await Factory.createInitAppWithFakeCrypto()
Factory.handlePasswordChallenges(application, password)
await application.mutator.importData(backupData, true)
await application.importData(backupData, true)
const importedNote = application.items.findItem(note.uuid)
const importedTag = application.items.findItem(tag.uuid)
@@ -445,7 +449,7 @@ describe('importing', function () {
version: oldVersion,
})
const noteItem = await application.itemManager.createItem(ContentType.Note, {
const noteItem = await application.mutator.createItem(ContentType.Note, {
title: 'Encrypted note',
text: 'On protocol version 003.',
})
@@ -456,7 +460,7 @@ describe('importing', function () {
application = await Factory.createInitAppWithFakeCrypto()
Factory.handlePasswordChallenges(application, password)
const result = await application.mutator.importData(backupData, true)
const result = await application.importData(backupData, true)
expect(result).to.not.be.undefined
expect(result.affectedItems.length).to.be.eq(backupData.items.length)
expect(result.errorCount).to.be.eq(0)
@@ -512,7 +516,7 @@ describe('importing', function () {
application = await Factory.createInitAppWithRealCrypto()
Factory.handlePasswordChallenges(application, password)
const result = await application.mutator.importData(backupData, true)
const result = await application.importData(backupData, true)
expect(result).to.not.be.undefined
expect(result.affectedItems.length).to.be.eq(backupData.items.length)
expect(result.errorCount).to.be.eq(0)
@@ -526,7 +530,7 @@ describe('importing', function () {
password: password,
})
const noteItem = await application.itemManager.createItem(ContentType.Note, {
const noteItem = await application.mutator.createItem(ContentType.Note, {
title: 'Encrypted note',
text: 'On protocol version 004.',
})
@@ -537,7 +541,7 @@ describe('importing', function () {
application = await Factory.createInitAppWithFakeCrypto()
Factory.handlePasswordChallenges(application, password)
const result = await application.mutator.importData(backupData, true)
const result = await application.importData(backupData, true)
expect(result).to.not.be.undefined
expect(result.affectedItems.length).to.be.eq(backupData.items.length)
expect(result.errorCount).to.be.eq(0)
@@ -556,7 +560,7 @@ describe('importing', function () {
password: password,
})
const noteItem = await application.itemManager.createItem(ContentType.Note, {
const noteItem = await application.mutator.createItem(ContentType.Note, {
title: 'This is a valid, encrypted note',
text: 'On protocol version 004.',
})
@@ -577,7 +581,7 @@ describe('importing', function () {
backupData.items = [...backupData.items, madeUpPayload]
const result = await application.mutator.importData(backupData, true)
const result = await application.importData(backupData, true)
expect(result).to.not.be.undefined
expect(result.affectedItems.length).to.be.eq(backupData.items.length - 1)
expect(result.errorCount).to.be.eq(1)
@@ -594,7 +598,7 @@ describe('importing', function () {
version: oldVersion,
})
await application.itemManager.createItem(ContentType.Note, {
await application.mutator.createItem(ContentType.Note, {
title: 'Encrypted note',
text: 'On protocol version 003.',
})
@@ -615,7 +619,7 @@ describe('importing', function () {
},
})
const result = await application.mutator.importData(backupData, true)
const result = await application.importData(backupData, true)
expect(result).to.not.be.undefined
expect(result.affectedItems.length).to.be.eq(0)
@@ -631,7 +635,7 @@ describe('importing', function () {
password: password,
})
await application.itemManager.createItem(ContentType.Note, {
await application.mutator.createItem(ContentType.Note, {
title: 'This is a valid, encrypted note',
text: 'On protocol version 004.',
})
@@ -647,7 +651,7 @@ describe('importing', function () {
},
})
const result = await application.mutator.importData(backupData, true)
const result = await application.importData(backupData, true)
expect(result).to.not.be.undefined
expect(result.affectedItems.length).to.be.eq(0)
expect(result.errorCount).to.be.eq(backupData.items.length)
@@ -662,7 +666,7 @@ describe('importing', function () {
password: password,
})
await application.itemManager.createItem(ContentType.Note, {
await application.mutator.createItem(ContentType.Note, {
title: 'Encrypted note',
text: 'On protocol version 004.',
})
@@ -673,7 +677,7 @@ describe('importing', function () {
await Factory.safeDeinit(application)
application = await Factory.createInitAppWithFakeCrypto()
const result = await application.mutator.importData(backupData)
const result = await application.importData(backupData)
expect(result.error).to.be.ok
})
@@ -687,7 +691,7 @@ describe('importing', function () {
})
Factory.handlePasswordChallenges(application, password)
await application.itemManager.createItem(ContentType.Note, {
await application.mutator.createItem(ContentType.Note, {
title: 'Encrypted note',
text: 'On protocol version 004.',
})
@@ -699,11 +703,13 @@ describe('importing', function () {
application = await Factory.createInitAppWithFakeCrypto()
Factory.handlePasswordChallenges(application, password)
const result = await application.mutator.importData(backupData, true)
const result = await application.importData(backupData, true)
expect(result).to.not.be.undefined
expect(result.affectedItems.length).to.be.eq(0)
expect(result.errorCount).to.be.eq(backupData.items.length)
expect(result.affectedItems.length).to.equal(BaseItemCounts.BackupFileRootKeyEncryptedItems)
expect(result.errorCount).to.be.eq(backupData.items.length - BaseItemCounts.BackupFileRootKeyEncryptedItems)
expect(application.itemManager.getDisplayableNotes().length).to.equal(0)
})
@@ -784,7 +790,14 @@ describe('importing', function () {
await application.prepareForLaunch({
receiveChallenge: (challenge) => {
if (challenge.prompts.length === 2) {
if (challenge.reason === ChallengeReason.Custom) {
return
}
if (
challenge.reason === ChallengeReason.DecryptEncryptedFile ||
challenge.reason === ChallengeReason.ImportFile
) {
application.submitValuesForChallenge(
challenge,
challenge.prompts.map((prompt) =>
@@ -796,9 +809,6 @@ describe('importing', function () {
),
),
)
} else {
const prompt = challenge.prompts[0]
application.submitValuesForChallenge(challenge, [CreateChallengeValue(prompt, password)])
}
},
})
@@ -827,7 +837,7 @@ describe('importing', function () {
},
}
const result = await application.mutator.importData(backupFile, false)
const result = await application.importData(backupFile, false)
expect(result.errorCount).to.equal(0)
await Factory.safeDeinit(application)
})
@@ -846,7 +856,7 @@ describe('importing', function () {
Factory.handlePasswordChallenges(application, password)
const pair = createRelatedNoteTagPairPayload()
await application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
await application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
await application.sync.sync()
@@ -862,7 +872,7 @@ describe('importing', function () {
password: password,
})
await application.mutator.importData(backupData, true)
await application.importData(backupData, true)
expect(application.itemManager.getDisplayableNotes().length).to.equal(1)
expect(application.itemManager.getDisplayableTags().length).to.equal(1)
@@ -872,4 +882,8 @@ describe('importing', function () {
expect(application.itemManager.referencesForItem(importedTag).length).to.equal(1)
expect(application.itemManager.itemsReferencingItem(importedNote).length).to.equal(1)
})
it('should decrypt backup file which contains a vaulted note without a synced key system root key', async () => {
console.error('TODO: Implement this test')
})
})

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import { BaseItemCounts } from '../lib/Applications.js'
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -22,11 +22,11 @@ describe('items', () => {
it('setting an item as dirty should update its client updated at', async function () {
const params = Factory.createNotePayload()
await this.application.itemManager.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged)
const item = this.application.itemManager.items[0]
const prevDate = item.userModifiedDate.getTime()
await Factory.sleep(0.1)
await this.application.itemManager.setItemDirty(item, true)
await this.application.mutator.setItemDirty(item, true)
const refreshedItem = this.application.itemManager.findItem(item.uuid)
const newDate = refreshedItem.userModifiedDate.getTime()
expect(prevDate).to.not.equal(newDate)
@@ -34,23 +34,23 @@ describe('items', () => {
it('setting an item as dirty with option to skip client updated at', async function () {
const params = Factory.createNotePayload()
await this.application.itemManager.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged)
const item = this.application.itemManager.items[0]
const prevDate = item.userModifiedDate.getTime()
await Factory.sleep(0.1)
await this.application.itemManager.setItemDirty(item)
await this.application.mutator.setItemDirty(item)
const newDate = item.userModifiedDate.getTime()
expect(prevDate).to.equal(newDate)
})
it('properly pins, archives, and locks', async function () {
const params = Factory.createNotePayload()
await this.application.itemManager.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged)
const item = this.application.itemManager.items[0]
expect(item.pinned).to.not.be.ok
const refreshedItem = await this.application.mutator.changeAndSaveItem(
const refreshedItem = await this.application.changeAndSaveItem(
item,
(mutator) => {
mutator.pinned = true
@@ -69,7 +69,7 @@ describe('items', () => {
it('properly compares item equality', async function () {
const params1 = Factory.createNotePayload()
const params2 = Factory.createNotePayload()
await this.application.itemManager.emitItemsFromPayloads([params1, params2], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([params1, params2], PayloadEmitSource.LocalChanged)
let item1 = this.application.itemManager.getDisplayableNotes()[0]
let item2 = this.application.itemManager.getDisplayableNotes()[1]
@@ -77,7 +77,7 @@ describe('items', () => {
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
// items should ignore this field when checking for equality
item1 = await this.application.mutator.changeAndSaveItem(
item1 = await this.application.changeAndSaveItem(
item1,
(mutator) => {
mutator.userModifiedDate = new Date()
@@ -86,7 +86,7 @@ describe('items', () => {
undefined,
syncOptions,
)
item2 = await this.application.mutator.changeAndSaveItem(
item2 = await this.application.changeAndSaveItem(
item2,
(mutator) => {
mutator.userModifiedDate = undefined
@@ -98,7 +98,7 @@ describe('items', () => {
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
item1 = await this.application.mutator.changeAndSaveItem(
item1 = await this.application.changeAndSaveItem(
item1,
(mutator) => {
mutator.mutableContent.foo = 'bar'
@@ -110,7 +110,7 @@ describe('items', () => {
expect(item1.isItemContentEqualWith(item2)).to.equal(false)
item2 = await this.application.mutator.changeAndSaveItem(
item2 = await this.application.changeAndSaveItem(
item2,
(mutator) => {
mutator.mutableContent.foo = 'bar'
@@ -123,7 +123,7 @@ describe('items', () => {
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
expect(item2.isItemContentEqualWith(item1)).to.equal(true)
item1 = await this.application.mutator.changeAndSaveItem(
item1 = await this.application.changeAndSaveItem(
item1,
(mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
@@ -132,7 +132,7 @@ describe('items', () => {
undefined,
syncOptions,
)
item2 = await this.application.mutator.changeAndSaveItem(
item2 = await this.application.changeAndSaveItem(
item2,
(mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
@@ -147,7 +147,7 @@ describe('items', () => {
expect(item1.isItemContentEqualWith(item2)).to.equal(false)
item1 = await this.application.mutator.changeAndSaveItem(
item1 = await this.application.changeAndSaveItem(
item1,
(mutator) => {
mutator.removeItemAsRelationship(item2)
@@ -156,7 +156,7 @@ describe('items', () => {
undefined,
syncOptions,
)
item2 = await this.application.mutator.changeAndSaveItem(
item2 = await this.application.changeAndSaveItem(
item2,
(mutator) => {
mutator.removeItemAsRelationship(item1)
@@ -174,12 +174,12 @@ describe('items', () => {
it('content equality should not have side effects', async function () {
const params1 = Factory.createNotePayload()
const params2 = Factory.createNotePayload()
await this.application.itemManager.emitItemsFromPayloads([params1, params2], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([params1, params2], PayloadEmitSource.LocalChanged)
let item1 = this.application.itemManager.getDisplayableNotes()[0]
const item2 = this.application.itemManager.getDisplayableNotes()[1]
item1 = await this.application.mutator.changeAndSaveItem(
item1 = await this.application.changeAndSaveItem(
item1,
(mutator) => {
mutator.mutableContent.foo = 'bar'
@@ -203,7 +203,7 @@ describe('items', () => {
// There was an issue where calling that function would modify values directly to omit keys
// in contentKeysToIgnoreWhenCheckingEquality.
await this.application.itemManager.setItemsDirty([item1, item2])
await this.application.mutator.setItemsDirty([item1, item2])
expect(item1.userModifiedDate).to.be.ok
expect(item2.userModifiedDate).to.be.ok

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import { BaseItemCounts } from '../lib/Applications.js'
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
import * as Factory from '../lib/factory.js'
import { createNoteParams } from '../lib/Items.js'
chai.use(chaiAsPromised)
@@ -20,7 +20,7 @@ describe('model manager mapping', () => {
it('mapping nonexistent item creates it', async function () {
const payload = Factory.createNotePayload()
await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
this.expectedItemCount++
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
})
@@ -31,13 +31,13 @@ describe('model manager mapping', () => {
dirty: false,
deleted: true,
})
await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
await this.application.payloadManager.emitPayload(payload, PayloadEmitSource.LocalChanged)
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
})
it('mapping and deleting nonexistent item creates and deletes it', async function () {
const payload = Factory.createNotePayload()
await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
this.expectedItemCount++
@@ -51,7 +51,7 @@ describe('model manager mapping', () => {
this.expectedItemCount--
await this.application.itemManager.emitItemsFromPayloads([changedParams], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([changedParams], PayloadEmitSource.LocalChanged)
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
})
@@ -59,22 +59,22 @@ describe('model manager mapping', () => {
it('mapping deleted but dirty item should not delete it', async function () {
const payload = Factory.createNotePayload()
const [item] = await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
const [item] = await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
this.expectedItemCount++
await this.application.itemManager.emitItemFromPayload(new DeleteItemMutator(item).getDeletedResult())
await this.application.payloadManager.emitPayload(new DeleteItemMutator(item).getDeletedResult())
const payload2 = new DeletedPayload(this.application.payloadManager.findOne(payload.uuid).ejected())
await this.application.itemManager.emitItemsFromPayloads([payload2], PayloadEmitSource.LocalChanged)
await this.application.payloadManager.emitPayloads([payload2], PayloadEmitSource.LocalChanged)
expect(this.application.payloadManager.collection.all().length).to.equal(this.expectedItemCount)
})
it('mapping existing item updates its properties', async function () {
const payload = Factory.createNotePayload()
await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
const newTitle = 'updated title'
const mutated = new DecryptedPayload({
@@ -84,7 +84,7 @@ describe('model manager mapping', () => {
title: newTitle,
},
})
await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
const item = this.application.itemManager.getDisplayableNotes()[0]
expect(item.content.title).to.equal(newTitle)
@@ -92,9 +92,9 @@ describe('model manager mapping', () => {
it('setting an item dirty should retrieve it in dirty items', async function () {
const payload = Factory.createNotePayload()
await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
const note = this.application.itemManager.getDisplayableNotes()[0]
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
const dirtyItems = this.application.itemManager.getDirtyItems()
expect(Uuids(dirtyItems).includes(note.uuid))
})
@@ -106,7 +106,7 @@ describe('model manager mapping', () => {
for (let i = 0; i < count; i++) {
payloads.push(Factory.createNotePayload())
}
await this.application.itemManager.emitItemsFromPayloads(payloads, PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads(payloads, PayloadEmitSource.LocalChanged)
await this.application.syncService.markAllItemsAsNeedingSyncAndPersist()
const dirtyItems = this.application.itemManager.getDirtyItems()
@@ -115,14 +115,14 @@ describe('model manager mapping', () => {
it('sync observers should be notified of changes', async function () {
const payload = Factory.createNotePayload()
await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
const item = this.application.itemManager.items[0]
return new Promise((resolve) => {
this.application.itemManager.addObserver(ContentType.Any, ({ changed }) => {
expect(changed[0].uuid === item.uuid)
resolve()
})
this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
})
})
})

View File

@@ -2,7 +2,7 @@
import * as Factory from '../lib/factory.js'
import * as Utils from '../lib/Utils.js'
import { createRelatedNoteTagPairPayload } from '../lib/Items.js'
import { BaseItemCounts } from '../lib/Applications.js'
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -25,7 +25,7 @@ describe('notes and tags', () => {
it('uses proper class for note', async function () {
const payload = Factory.createNotePayload()
await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
const note = this.application.itemManager.getItems([ContentType.Note])[0]
expect(note.constructor === SNNote).to.equal(true)
})
@@ -33,7 +33,7 @@ describe('notes and tags', () => {
it('properly constructs syncing params', async function () {
const title = 'Foo'
const text = 'Bar'
const note = await this.application.mutator.createTemplateItem(ContentType.Note, {
const note = await this.application.items.createTemplateItem(ContentType.Note, {
title,
text,
})
@@ -41,7 +41,7 @@ describe('notes and tags', () => {
expect(note.content.title).to.equal(title)
expect(note.content.text).to.equal(text)
const tag = await this.application.mutator.createTemplateItem(ContentType.Tag, {
const tag = await this.application.items.createTemplateItem(ContentType.Tag, {
title,
})
@@ -73,7 +73,7 @@ describe('notes and tags', () => {
},
})
await this.application.itemManager.emitItemsFromPayloads([mutatedNote, mutatedTag], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([mutatedNote, mutatedTag], PayloadEmitSource.LocalChanged)
const note = this.application.itemManager.getItems([ContentType.Note])[0]
const tag = this.application.itemManager.getItems([ContentType.Tag])[0]
@@ -89,7 +89,7 @@ describe('notes and tags', () => {
expect(notePayload.content.references.length).to.equal(0)
expect(tagPayload.content.references.length).to.equal(1)
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
let note = this.application.itemManager.getDisplayableNotes()[0]
let tag = this.application.itemManager.getDisplayableTags()[0]
@@ -106,7 +106,7 @@ describe('notes and tags', () => {
expect(note.payload.references.length).to.equal(0)
expect(tag.noteCount).to.equal(1)
await this.application.itemManager.setItemToBeDeleted(note)
await this.application.mutator.setItemToBeDeleted(note)
tag = this.application.itemManager.getDisplayableTags()[0]
@@ -130,7 +130,7 @@ describe('notes and tags', () => {
const notePayload = pair[0]
const tagPayload = pair[1]
await this.application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
let note = this.application.itemManager.getItems([ContentType.Note])[0]
let tag = this.application.itemManager.getItems([ContentType.Tag])[0]
@@ -147,7 +147,7 @@ describe('notes and tags', () => {
references: [],
},
})
await this.application.itemManager.emitItemsFromPayloads([mutatedTag], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([mutatedTag], PayloadEmitSource.LocalChanged)
note = this.application.itemManager.findItem(note.uuid)
tag = this.application.itemManager.findItem(tag.uuid)
@@ -177,14 +177,14 @@ describe('notes and tags', () => {
const notePayload = pair[0]
const tagPayload = pair[1]
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
const note = this.application.itemManager.getItems([ContentType.Note])[0]
let tag = this.application.itemManager.getItems([ContentType.Tag])[0]
expect(note.content.references.length).to.equal(0)
expect(tag.content.references.length).to.equal(1)
tag = await this.application.mutator.changeAndSaveItem(
tag = await this.application.changeAndSaveItem(
tag,
(mutator) => {
mutator.removeItemAsRelationship(note)
@@ -200,11 +200,11 @@ describe('notes and tags', () => {
it('properly handles tag duplication', async function () {
const pair = createRelatedNoteTagPairPayload()
await this.application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
let note = this.application.itemManager.getDisplayableNotes()[0]
let tag = this.application.itemManager.getDisplayableTags()[0]
const duplicateTag = await this.application.itemManager.duplicateItem(tag, true)
const duplicateTag = await this.application.mutator.duplicateItem(tag, true)
await this.application.syncService.sync(syncOptions)
note = this.application.itemManager.findItem(note.uuid)
@@ -232,9 +232,9 @@ describe('notes and tags', () => {
const pair = createRelatedNoteTagPairPayload()
const notePayload = pair[0]
const tagPayload = pair[1]
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
const note = this.application.itemManager.getItems([ContentType.Note])[0]
const duplicateNote = await this.application.itemManager.duplicateItem(note, true)
const duplicateNote = await this.application.mutator.duplicateItem(note, true)
expect(note.uuid).to.not.equal(duplicateNote.uuid)
expect(this.application.itemManager.itemsReferencingItem(duplicateNote).length).to.equal(
@@ -246,7 +246,7 @@ describe('notes and tags', () => {
const pair = createRelatedNoteTagPairPayload()
const notePayload = pair[0]
const tagPayload = pair[1]
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
const note = this.application.itemManager.getItems([ContentType.Note])[0]
let tag = this.application.itemManager.getItems([ContentType.Tag])[0]
@@ -256,16 +256,16 @@ describe('notes and tags', () => {
expect(note.content.references.length).to.equal(0)
expect(this.application.itemManager.itemsReferencingItem(note).length).to.equal(1)
await this.application.itemManager.setItemToBeDeleted(tag)
await this.application.mutator.setItemToBeDeleted(tag)
tag = this.application.itemManager.findItem(tag.uuid)
expect(tag).to.not.be.ok
})
it('modifying item content should not modify payload content', async function () {
const notePayload = Factory.createNotePayload()
await this.application.itemManager.emitItemsFromPayloads([notePayload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([notePayload], PayloadEmitSource.LocalChanged)
let note = this.application.itemManager.getItems([ContentType.Note])[0]
note = await this.application.mutator.changeAndSaveItem(
note = await this.application.changeAndSaveItem(
note,
(mutator) => {
mutator.mutableContent.title = Math.random()
@@ -285,12 +285,12 @@ describe('notes and tags', () => {
const notePayload = pair[0]
const tagPayload = pair[1]
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
let note = this.application.itemManager.getItems([ContentType.Note])[0]
let tag = this.application.itemManager.getItems([ContentType.Tag])[0]
await this.application.syncService.sync(syncOptions)
await this.application.itemManager.setItemToBeDeleted(tag)
await this.application.mutator.setItemToBeDeleted(tag)
note = this.application.itemManager.findItem(note.uuid)
this.application.itemManager.findItem(tag.uuid)
@@ -302,7 +302,7 @@ describe('notes and tags', () => {
await Promise.all(
['Y', 'Z', 'A', 'B'].map(async (title) => {
return this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, { title }),
await this.application.items.createTemplateItem(ContentType.Note, { title }),
)
}),
)
@@ -316,7 +316,7 @@ describe('notes and tags', () => {
})
it('setting a note dirty should collapse its properties into content', async function () {
let note = await this.application.mutator.createTemplateItem(ContentType.Note, {
let note = await this.application.items.createTemplateItem(ContentType.Note, {
title: 'Foo',
})
await this.application.mutator.insertItem(note)
@@ -339,7 +339,7 @@ describe('notes and tags', () => {
mutator.e2ePendingRefactor_addItemAsRelationship(taggedNote)
})
await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
@@ -379,7 +379,7 @@ describe('notes and tags', () => {
await Promise.all(
['Y', 'Z', 'A', 'B'].map(async (title) => {
return this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title,
}),
)
@@ -413,17 +413,17 @@ describe('notes and tags', () => {
describe('Smart views', function () {
it('"title", "startsWith", "Foo"', async function () {
const note = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'Foo 🎲',
}),
)
await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'Not Foo 🎲',
}),
)
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
await this.application.items.createTemplateItem(ContentType.SmartView, {
title: 'Foo Notes',
predicate: {
keypath: 'title',
@@ -447,7 +447,7 @@ describe('notes and tags', () => {
it('"pinned", "=", true', async function () {
const note = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
@@ -455,13 +455,13 @@ describe('notes and tags', () => {
mutator.pinned = true
})
await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'B',
pinned: false,
}),
)
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
await this.application.items.createTemplateItem(ContentType.SmartView, {
title: 'Pinned',
predicate: {
keypath: 'pinned',
@@ -485,7 +485,7 @@ describe('notes and tags', () => {
it('"pinned", "=", false', async function () {
const pinnedNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
@@ -493,12 +493,12 @@ describe('notes and tags', () => {
mutator.pinned = true
})
const unpinnedNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'B',
}),
)
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
await this.application.items.createTemplateItem(ContentType.SmartView, {
title: 'Not pinned',
predicate: {
keypath: 'pinned',
@@ -522,19 +522,19 @@ describe('notes and tags', () => {
it('"text.length", ">", 500', async function () {
const longNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'A',
text: Array(501).fill(0).join(''),
}),
)
await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'B',
text: 'b',
}),
)
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
await this.application.items.createTemplateItem(ContentType.SmartView, {
title: 'Long',
predicate: {
keypath: 'text.length',
@@ -563,18 +563,20 @@ describe('notes and tags', () => {
})
const recentNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'A',
}),
true,
)
await this.application.sync.sync()
const olderNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'B',
text: 'b',
}),
true,
)
const threeDays = 3 * 24 * 60 * 60 * 1000
@@ -582,13 +584,13 @@ describe('notes and tags', () => {
/** Create an unsynced note which shouldn't get an updated_at */
await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'B',
text: 'b',
}),
)
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
await this.application.items.createTemplateItem(ContentType.SmartView, {
title: 'One day ago',
predicate: {
keypath: 'serverUpdatedAt',
@@ -598,6 +600,9 @@ describe('notes and tags', () => {
}),
)
const matches = this.application.items.notesMatchingSmartView(view)
expect(matches.length).to.equal(1)
expect(matches[0].uuid).to.equal(recentNote.uuid)
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'asc',
@@ -605,13 +610,11 @@ describe('notes and tags', () => {
})
const displayedNotes = this.application.items.getDisplayableNotes()
expect(displayedNotes).to.deep.equal(matches)
expect(matches.length).to.equal(1)
expect(matches[0].uuid).to.equal(recentNote.uuid)
})
it('"tags.length", "=", 0', async function () {
const untaggedNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
@@ -622,7 +625,7 @@ describe('notes and tags', () => {
})
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
await this.application.items.createTemplateItem(ContentType.SmartView, {
title: 'Untagged',
predicate: {
keypath: 'tags.length',
@@ -650,13 +653,13 @@ describe('notes and tags', () => {
mutator.e2ePendingRefactor_addItemAsRelationship(taggedNote)
})
await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
await this.application.items.createTemplateItem(ContentType.SmartView, {
title: 'B-tags',
predicate: {
keypath: 'tags',
@@ -685,7 +688,7 @@ describe('notes and tags', () => {
})
const pinnedNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
@@ -694,7 +697,7 @@ describe('notes and tags', () => {
})
const lockedNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
@@ -703,7 +706,7 @@ describe('notes and tags', () => {
})
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
await this.application.items.createTemplateItem(ContentType.SmartView, {
title: 'Pinned & Locked',
predicate: {
operator: 'and',
@@ -733,7 +736,7 @@ describe('notes and tags', () => {
})
const pinnedNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
@@ -742,7 +745,7 @@ describe('notes and tags', () => {
})
const pinnedAndProtectedNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
@@ -752,13 +755,13 @@ describe('notes and tags', () => {
})
await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
await this.application.items.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
await this.application.items.createTemplateItem(ContentType.SmartView, {
title: 'Protected or Pinned',
predicate: {
operator: 'or',
@@ -794,7 +797,7 @@ describe('notes and tags', () => {
const notePayload3 = Factory.createNotePayload('Bar')
const notePayload4 = Factory.createNotePayload('Testing')
await this.application.itemManager.emitItemsFromPayloads(
await this.application.mutator.emitItemsFromPayloads(
[notePayload1, notePayload2, notePayload3, notePayload4, tagPayload1],
PayloadEmitSource.LocalChanged,
)
@@ -824,7 +827,7 @@ describe('notes and tags', () => {
const notePayload3 = Factory.createNotePayload('Testing FOO (Bar)')
const notePayload4 = Factory.createNotePayload('This should not match')
await this.application.itemManager.emitItemsFromPayloads(
await this.application.mutator.emitItemsFromPayloads(
[notePayload1, notePayload2, notePayload3, notePayload4, tagPayload1],
PayloadEmitSource.LocalChanged,
)

View File

@@ -75,8 +75,8 @@ describe('tags as folders', () => {
const note2 = await Factory.createMappedNote(this.application, 'my second note')
// ## The user add a note to the child tag
await this.application.items.addTagToNote(note1, tags.child, true)
await this.application.items.addTagToNote(note2, tags.another, true)
await this.application.mutator.addTagToNote(note1, tags.child, true)
await this.application.mutator.addTagToNote(note2, tags.another, true)
// ## The note has been added to other tags
const note1Tags = await this.application.items.getSortedTagsForItem(note1)

View File

@@ -56,7 +56,7 @@ describe('mapping performance', () => {
const batchSize = 100
for (let i = 0; i < payloads.length; i += batchSize) {
const subArray = payloads.slice(currentIndex, currentIndex + batchSize)
await application.itemManager.emitItemsFromPayloads(subArray, PayloadEmitSource.LocalChanged)
await application.mutator.emitItemsFromPayloads(subArray, PayloadEmitSource.LocalChanged)
currentIndex += batchSize
}
@@ -117,7 +117,7 @@ describe('mapping performance', () => {
const batchSize = 100
for (let i = 0; i < payloads.length; i += batchSize) {
var subArray = payloads.slice(currentIndex, currentIndex + batchSize)
await application.itemManager.emitItemsFromPayloads(subArray, PayloadEmitSource.LocalChanged)
await application.mutator.emitItemsFromPayloads(subArray, PayloadEmitSource.LocalChanged)
currentIndex += batchSize
}

View File

@@ -4,7 +4,7 @@ import * as Factory from './lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('mutator', () => {
describe('item mutator', () => {
beforeEach(async function () {
this.createBarePayload = () => {
return new DecryptedPayload({

View File

@@ -0,0 +1,271 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import * as Factory from './lib/factory.js'
import { BaseItemCounts } from './lib/BaseItemCounts.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('mutator service', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let application
let mutator
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithFakeCrypto()
application = context.application
mutator = application.mutator
await context.launch()
})
const createNote = async () => {
return mutator.createItem(ContentType.Note, {
title: 'hello',
text: 'world',
})
}
const createTag = async (notes = []) => {
const references = notes.map((note) => {
return {
uuid: note.uuid,
content_type: note.content_type,
}
})
return mutator.createItem(ContentType.Tag, {
title: 'thoughts',
references: references,
})
}
it('create item', async function () {
const item = await createNote()
expect(item).to.be.ok
expect(item.title).to.equal('hello')
})
it('emitting item through payload and marking dirty should have userModifiedDate', async function () {
const payload = Factory.createNotePayload()
const item = await mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
const result = await mutator.setItemDirty(item)
const appData = result.payload.content.appData
expect(appData[DecryptedItem.DefaultAppDomain()][AppDataField.UserModifiedDate]).to.be.ok
})
it('deleting an item should make it immediately unfindable', async () => {
const note = await context.createSyncedNote()
await mutator.setItemToBeDeleted(note)
const foundNote = application.items.findItem(note.uuid)
expect(foundNote).to.not.be.ok
})
it('deleting from reference map', async function () {
const note = await createNote()
const tag = await createTag([note])
await mutator.setItemToBeDeleted(note)
expect(application.items.collection.referenceMap.directMap.get(tag.uuid)).to.eql([])
expect(application.items.collection.referenceMap.inverseMap.get(note.uuid).length).to.equal(0)
})
it('deleting referenced item should update referencing item references', async function () {
const note = await createNote()
let tag = await createTag([note])
await mutator.setItemToBeDeleted(note)
tag = application.items.findItem(tag.uuid)
expect(tag.content.references.length).to.equal(0)
})
it('removing relationship should update reference map', async function () {
const note = await createNote()
const tag = await createTag([note])
await mutator.changeItem(tag, (mutator) => {
mutator.removeItemAsRelationship(note)
})
expect(application.items.collection.referenceMap.directMap.get(tag.uuid)).to.eql([])
expect(application.items.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([])
})
it('emitting discardable payload should remove it from our collection', async function () {
const note = await createNote()
const payload = new DeletedPayload({
...note.payload.ejected(),
content: undefined,
deleted: true,
dirty: false,
})
expect(payload.discardable).to.equal(true)
await context.payloads.emitPayload(payload)
expect(application.items.findItem(note.uuid)).to.not.be.ok
})
it('change existing item', async function () {
const note = await createNote()
const newTitle = String(Math.random())
await mutator.changeItem(note, (mutator) => {
mutator.title = newTitle
})
const latestVersion = application.items.findItem(note.uuid)
expect(latestVersion.title).to.equal(newTitle)
})
it('change non-existant item through uuid should fail', async function () {
const note = await application.items.createTemplateItem(ContentType.Note, {
title: 'hello',
text: 'world',
})
const changeFn = async () => {
const newTitle = String(Math.random())
return mutator.changeItem(note, (mutator) => {
mutator.title = newTitle
})
}
await Factory.expectThrowsAsync(() => changeFn(), 'Attempting to change non-existant item')
})
it('set items dirty', async function () {
const note = await createNote()
await mutator.setItemDirty(note)
const dirtyItems = application.items.getDirtyItems()
expect(dirtyItems.length).to.equal(1)
expect(dirtyItems[0].uuid).to.equal(note.uuid)
expect(dirtyItems[0].dirty).to.equal(true)
})
describe('duplicateItem', async function () {
const sandbox = sinon.createSandbox()
beforeEach(async function () {
this.emitPayloads = sandbox.spy(application.items.payloadManager, 'emitPayloads')
})
afterEach(async function () {
sandbox.restore()
})
it('should duplicate the item and set the duplicate_of property', async function () {
const note = await createNote()
await mutator.duplicateItem(note)
sinon.assert.calledTwice(this.emitPayloads)
const originalNote = application.items.getDisplayableNotes()[0]
const duplicatedNote = application.items.getDisplayableNotes()[1]
expect(application.items.items.length).to.equal(2 + BaseItemCounts.DefaultItems)
expect(application.items.getDisplayableNotes().length).to.equal(2)
expect(originalNote.uuid).to.not.equal(duplicatedNote.uuid)
expect(originalNote.uuid).to.equal(duplicatedNote.duplicateOf)
expect(originalNote.uuid).to.equal(duplicatedNote.payload.duplicate_of)
expect(duplicatedNote.conflictOf).to.be.undefined
expect(duplicatedNote.payload.content.conflict_of).to.be.undefined
})
it('should duplicate the item and set the duplicate_of and conflict_of properties', async function () {
const note = await createNote()
await mutator.duplicateItem(note, true)
sinon.assert.calledTwice(this.emitPayloads)
const originalNote = application.items.getDisplayableNotes()[0]
const duplicatedNote = application.items.getDisplayableNotes()[1]
expect(application.items.items.length).to.equal(2 + BaseItemCounts.DefaultItems)
expect(application.items.getDisplayableNotes().length).to.equal(2)
expect(originalNote.uuid).to.not.equal(duplicatedNote.uuid)
expect(originalNote.uuid).to.equal(duplicatedNote.duplicateOf)
expect(originalNote.uuid).to.equal(duplicatedNote.payload.duplicate_of)
expect(originalNote.uuid).to.equal(duplicatedNote.conflictOf)
expect(originalNote.uuid).to.equal(duplicatedNote.payload.content.conflict_of)
})
it('duplicate item with relationships', async function () {
const note = await createNote()
const tag = await createTag([note])
const duplicate = await mutator.duplicateItem(tag)
expect(duplicate.content.references).to.have.length(1)
expect(application.items.items).to.have.length(3 + BaseItemCounts.DefaultItems)
expect(application.items.getDisplayableTags()).to.have.length(2)
})
it('adds duplicated item as a relationship to items referencing it', async function () {
const note = await createNote()
let tag = await createTag([note])
const duplicateNote = await mutator.duplicateItem(note)
expect(tag.content.references).to.have.length(1)
tag = application.items.findItem(tag.uuid)
const references = tag.content.references.map((ref) => ref.uuid)
expect(references).to.have.length(2)
expect(references).to.include(note.uuid, duplicateNote.uuid)
})
it('duplicates item with additional content', async function () {
const note = await mutator.createItem(ContentType.Note, {
title: 'hello',
text: 'world',
})
const duplicateNote = await mutator.duplicateItem(note, false, {
title: 'hello (copy)',
})
expect(duplicateNote.title).to.equal('hello (copy)')
expect(duplicateNote.text).to.equal('world')
})
})
it('set item deleted', async function () {
const note = await createNote()
await mutator.setItemToBeDeleted(note)
/** Items should never be mutated directly */
expect(note.deleted).to.not.be.ok
const latestVersion = context.payloads.findOne(note.uuid)
expect(latestVersion.deleted).to.equal(true)
expect(latestVersion.dirty).to.equal(true)
expect(latestVersion.content).to.not.be.ok
/** Deleted items do not show up in item manager's public interface */
expect(application.items.items.length).to.equal(BaseItemCounts.DefaultItems)
expect(application.items.getDisplayableNotes().length).to.equal(0)
})
it('should empty trash', async function () {
const note = await createNote()
const versionTwo = await mutator.changeItem(note, (mutator) => {
mutator.trashed = true
})
expect(application.items.trashSmartView).to.be.ok
expect(versionTwo.trashed).to.equal(true)
expect(versionTwo.dirty).to.equal(true)
expect(versionTwo.content).to.be.ok
expect(application.items.items.length).to.equal(1 + BaseItemCounts.DefaultItems)
expect(application.items.trashedItems.length).to.equal(1)
await application.mutator.emptyTrash()
const versionThree = context.payloads.findOne(note.uuid)
expect(versionThree.deleted).to.equal(true)
expect(application.items.trashedItems.length).to.equal(0)
})
})

View File

@@ -6,9 +6,10 @@ describe('note display criteria', function () {
beforeEach(async function () {
this.payloadManager = new PayloadManager()
this.itemManager = new ItemManager(this.payloadManager)
this.mutator = new MutatorService(this.itemManager, this.payloadManager)
this.createNote = async (title = 'hello', text = 'world') => {
return this.itemManager.createItem(ContentType.Note, {
return this.mutator.createItem(ContentType.Note, {
title: title,
text: text,
})
@@ -21,7 +22,7 @@ describe('note display criteria', function () {
content_type: note.content_type,
}
})
return this.itemManager.createItem(ContentType.Tag, {
return this.mutator.createItem(ContentType.Tag, {
title: title,
references: references,
})
@@ -31,138 +32,168 @@ describe('note display criteria', function () {
it('includePinned off', async function () {
await this.createNote()
const pendingPin = await this.createNote()
await this.itemManager.changeItem(pendingPin, (mutator) => {
await this.mutator.changeItem(pendingPin, (mutator) => {
mutator.pinned = true
})
const criteria = {
includePinned: false,
}
expect(
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
.length,
notesAndFilesMatchingOptions(
criteria,
this.itemManager.collection.all(ContentType.Note),
this.itemManager.collection,
).length,
).to.equal(1)
})
it('includePinned on', async function () {
await this.createNote()
const pendingPin = await this.createNote()
await this.itemManager.changeItem(pendingPin, (mutator) => {
await this.mutator.changeItem(pendingPin, (mutator) => {
mutator.pinned = true
})
const criteria = { includePinned: true }
expect(
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
.length,
notesAndFilesMatchingOptions(
criteria,
this.itemManager.collection.all(ContentType.Note),
this.itemManager.collection,
).length,
).to.equal(2)
})
it('includeTrashed off', async function () {
await this.createNote()
const pendingTrash = await this.createNote()
await this.itemManager.changeItem(pendingTrash, (mutator) => {
await this.mutator.changeItem(pendingTrash, (mutator) => {
mutator.trashed = true
})
const criteria = { includeTrashed: false }
expect(
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
.length,
notesAndFilesMatchingOptions(
criteria,
this.itemManager.collection.all(ContentType.Note),
this.itemManager.collection,
).length,
).to.equal(1)
})
it('includeTrashed on', async function () {
await this.createNote()
const pendingTrash = await this.createNote()
await this.itemManager.changeItem(pendingTrash, (mutator) => {
await this.mutator.changeItem(pendingTrash, (mutator) => {
mutator.trashed = true
})
const criteria = { includeTrashed: true }
expect(
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
.length,
notesAndFilesMatchingOptions(
criteria,
this.itemManager.collection.all(ContentType.Note),
this.itemManager.collection,
).length,
).to.equal(2)
})
it('includeArchived off', async function () {
await this.createNote()
const pendingArchive = await this.createNote()
await this.itemManager.changeItem(pendingArchive, (mutator) => {
await this.mutator.changeItem(pendingArchive, (mutator) => {
mutator.archived = true
})
const criteria = { includeArchived: false }
expect(
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
.length,
notesAndFilesMatchingOptions(
criteria,
this.itemManager.collection.all(ContentType.Note),
this.itemManager.collection,
).length,
).to.equal(1)
})
it('includeArchived on', async function () {
await this.createNote()
const pendingArchive = await this.createNote()
await this.itemManager.changeItem(pendingArchive, (mutator) => {
await this.mutator.changeItem(pendingArchive, (mutator) => {
mutator.archived = true
})
const criteria = {
includeArchived: true,
}
expect(
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
.length,
notesAndFilesMatchingOptions(
criteria,
this.itemManager.collection.all(ContentType.Note),
this.itemManager.collection,
).length,
).to.equal(2)
})
it('includeProtected off', async function () {
await this.createNote()
const pendingProtected = await this.createNote()
await this.itemManager.changeItem(pendingProtected, (mutator) => {
await this.mutator.changeItem(pendingProtected, (mutator) => {
mutator.protected = true
})
const criteria = { includeProtected: false }
expect(
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
.length,
notesAndFilesMatchingOptions(
criteria,
this.itemManager.collection.all(ContentType.Note),
this.itemManager.collection,
).length,
).to.equal(1)
})
it('includeProtected on', async function () {
await this.createNote()
const pendingProtected = await this.createNote()
await this.itemManager.changeItem(pendingProtected, (mutator) => {
await this.mutator.changeItem(pendingProtected, (mutator) => {
mutator.protected = true
})
const criteria = {
includeProtected: true,
}
expect(
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
.length,
notesAndFilesMatchingOptions(
criteria,
this.itemManager.collection.all(ContentType.Note),
this.itemManager.collection,
).length,
).to.equal(2)
})
it('protectedSearchEnabled false', async function () {
const normal = await this.createNote('hello', 'world')
await this.itemManager.changeItem(normal, (mutator) => {
await this.mutator.changeItem(normal, (mutator) => {
mutator.protected = true
})
const criteria = {
searchQuery: { query: 'world', includeProtectedNoteText: false },
}
expect(
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
.length,
notesAndFilesMatchingOptions(
criteria,
this.itemManager.collection.all(ContentType.Note),
this.itemManager.collection,
).length,
).to.equal(0)
})
it('protectedSearchEnabled true', async function () {
const normal = await this.createNote()
await this.itemManager.changeItem(normal, (mutator) => {
await this.mutator.changeItem(normal, (mutator) => {
mutator.protected = true
})
const criteria = {
searchQuery: { query: 'world', includeProtectedNoteText: true },
}
expect(
itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection)
.length,
notesAndFilesMatchingOptions(
criteria,
this.itemManager.collection.all(ContentType.Note),
this.itemManager.collection,
).length,
).to.equal(1)
})
@@ -175,7 +206,7 @@ describe('note display criteria', function () {
tags: [tag],
}
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
matchingCriteria,
this.itemManager.collection.all(ContentType.Note),
this.itemManager.collection,
@@ -186,7 +217,7 @@ describe('note display criteria', function () {
tags: [looseTag],
}
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
nonmatchingCriteria,
this.itemManager.collection.all(ContentType.Note),
this.itemManager.collection,
@@ -198,7 +229,7 @@ describe('note display criteria', function () {
it('normal note', async function () {
await this.createNote()
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
},
@@ -208,7 +239,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.trashSmartView],
},
@@ -218,7 +249,7 @@ describe('note display criteria', function () {
).to.equal(0)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.archivedSmartView],
},
@@ -230,12 +261,12 @@ describe('note display criteria', function () {
it('trashed note', async function () {
const normal = await this.createNote()
await this.itemManager.changeItem(normal, (mutator) => {
await this.mutator.changeItem(normal, (mutator) => {
mutator.trashed = true
})
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
includeTrashed: false,
@@ -246,7 +277,7 @@ describe('note display criteria', function () {
).to.equal(0)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.trashSmartView],
},
@@ -256,7 +287,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.archivedSmartView],
},
@@ -268,12 +299,12 @@ describe('note display criteria', function () {
it('archived note', async function () {
const normal = await this.createNote()
await this.itemManager.changeItem(normal, (mutator) => {
await this.mutator.changeItem(normal, (mutator) => {
mutator.trashed = false
mutator.archived = true
})
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
includeArchived: false,
@@ -284,7 +315,7 @@ describe('note display criteria', function () {
).to.equal(0)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.trashSmartView],
},
@@ -294,7 +325,7 @@ describe('note display criteria', function () {
).to.equal(0)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.archivedSmartView],
},
@@ -306,13 +337,13 @@ describe('note display criteria', function () {
it('archived + trashed note', async function () {
const normal = await this.createNote()
await this.itemManager.changeItem(normal, (mutator) => {
await this.mutator.changeItem(normal, (mutator) => {
mutator.trashed = true
mutator.archived = true
})
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
},
@@ -322,7 +353,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.trashSmartView],
},
@@ -332,7 +363,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.archivedSmartView],
},
@@ -348,7 +379,7 @@ describe('note display criteria', function () {
await this.createNote()
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
includeTrashed: true,
@@ -359,7 +390,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.trashSmartView],
includeTrashed: true,
@@ -373,12 +404,12 @@ describe('note display criteria', function () {
it('trashed note', async function () {
const normal = await this.createNote()
await this.itemManager.changeItem(normal, (mutator) => {
await this.mutator.changeItem(normal, (mutator) => {
mutator.trashed = true
})
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
includeTrashed: false,
@@ -389,7 +420,7 @@ describe('note display criteria', function () {
).to.equal(0)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
includeTrashed: true,
@@ -400,7 +431,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.trashSmartView],
includeTrashed: true,
@@ -411,7 +442,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.archivedSmartView],
includeTrashed: true,
@@ -425,13 +456,13 @@ describe('note display criteria', function () {
it('archived + trashed note', async function () {
const normal = await this.createNote()
await this.itemManager.changeItem(normal, (mutator) => {
await this.mutator.changeItem(normal, (mutator) => {
mutator.trashed = true
mutator.archived = true
})
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
},
@@ -441,7 +472,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.trashSmartView],
},
@@ -451,7 +482,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.archivedSmartView],
},
@@ -467,7 +498,7 @@ describe('note display criteria', function () {
await this.createNote()
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
includeArchived: true,
@@ -478,7 +509,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.trashSmartView],
includeArchived: true,
@@ -491,12 +522,12 @@ describe('note display criteria', function () {
it('archived note', async function () {
const normal = await this.createNote()
await this.itemManager.changeItem(normal, (mutator) => {
await this.mutator.changeItem(normal, (mutator) => {
mutator.archived = true
})
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
includeArchived: false,
@@ -507,7 +538,7 @@ describe('note display criteria', function () {
).to.equal(0)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
includeArchived: true,
@@ -518,7 +549,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.trashSmartView],
includeArchived: true,
@@ -529,7 +560,7 @@ describe('note display criteria', function () {
).to.equal(0)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.archivedSmartView],
includeArchived: false,
@@ -542,13 +573,13 @@ describe('note display criteria', function () {
it('archived + trashed note', async function () {
const normal = await this.createNote()
await this.itemManager.changeItem(normal, (mutator) => {
await this.mutator.changeItem(normal, (mutator) => {
mutator.trashed = true
mutator.archived = true
})
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
includeArchived: true,
@@ -559,7 +590,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.trashSmartView],
includeArchived: true,
@@ -570,7 +601,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.archivedSmartView],
includeArchived: true,
@@ -587,7 +618,7 @@ describe('note display criteria', function () {
await this.createNote()
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [
this.itemManager.allNotesSmartView,
@@ -601,7 +632,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.trashSmartView],
},
@@ -613,12 +644,12 @@ describe('note display criteria', function () {
it('archived note', async function () {
const normal = await this.createNote()
await this.itemManager.changeItem(normal, (mutator) => {
await this.mutator.changeItem(normal, (mutator) => {
mutator.archived = true
})
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
includeArchived: false,
@@ -629,7 +660,7 @@ describe('note display criteria', function () {
).to.equal(0)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
includeArchived: true,
@@ -640,7 +671,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.trashSmartView],
includeArchived: true,
@@ -651,7 +682,7 @@ describe('note display criteria', function () {
).to.equal(0)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.archivedSmartView],
includeArchived: false,
@@ -664,13 +695,13 @@ describe('note display criteria', function () {
it('archived + trashed note', async function () {
const normal = await this.createNote()
await this.itemManager.changeItem(normal, (mutator) => {
await this.mutator.changeItem(normal, (mutator) => {
mutator.trashed = true
mutator.archived = true
})
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.allNotesSmartView],
includeArchived: true,
@@ -681,7 +712,7 @@ describe('note display criteria', function () {
).to.equal(0)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.trashSmartView],
includeArchived: true,
@@ -692,7 +723,7 @@ describe('note display criteria', function () {
).to.equal(1)
expect(
itemsMatchingOptions(
notesAndFilesMatchingOptions(
{
views: [this.itemManager.archivedSmartView],
includeArchived: true,

View File

@@ -48,7 +48,7 @@ describe('protections', function () {
})
let note = await Factory.createMappedNote(application)
note = await application.mutator.protectNote(note)
note = await application.protections.protectNote(note)
expect(await application.authorizeNoteAccess(note)).to.be.true
expect(challengePrompts).to.equal(1)
@@ -57,7 +57,7 @@ describe('protections', function () {
it('sets `note.protected` to true', async function () {
application = await Factory.createInitAppWithFakeCrypto()
let note = await Factory.createMappedNote(application)
note = await application.mutator.protectNote(note)
note = await application.protections.protectNote(note)
expect(note.protected).to.be.true
})
@@ -87,7 +87,7 @@ describe('protections', function () {
await application.addPasscode(passcode)
let note = await Factory.createMappedNote(application)
note = await application.mutator.protectNote(note)
note = await application.protections.protectNote(note)
expect(await application.authorizeNoteAccess(note)).to.be.true
expect(challengePrompts).to.equal(1)
@@ -120,8 +120,8 @@ describe('protections', function () {
await application.addPasscode(passcode)
let note = await Factory.createMappedNote(application)
const uuid = note.uuid
note = await application.mutator.protectNote(note)
note = await application.mutator.unprotectNote(note)
note = await application.protections.protectNote(note)
note = await application.protections.unprotectNote(note)
expect(note.uuid).to.equal(uuid)
expect(note.protected).to.equal(false)
expect(challengePrompts).to.equal(1)
@@ -142,8 +142,8 @@ describe('protections', function () {
await application.addPasscode(passcode)
let note = await Factory.createMappedNote(application)
note = await application.mutator.protectNote(note)
const result = await application.mutator.unprotectNote(note)
note = await application.protections.protectNote(note)
const result = await application.protections.unprotectNote(note)
expect(result).to.be.undefined
expect(challengePrompts).to.equal(1)
})
@@ -174,7 +174,7 @@ describe('protections', function () {
await application.addPasscode(passcode)
let note = await Factory.createMappedNote(application)
note = await application.mutator.protectNote(note)
note = await application.protections.protectNote(note)
expect(await application.authorizeNoteAccess(note)).to.be.true
expect(await application.authorizeNoteAccess(note)).to.be.true
@@ -226,7 +226,7 @@ describe('protections', function () {
application = await Factory.createInitAppWithFakeCrypto()
let note = await Factory.createMappedNote(application)
note = await application.mutator.protectNote(note)
note = await application.protections.protectNote(note)
expect(await application.authorizeNoteAccess(note)).to.be.true
})
@@ -431,8 +431,8 @@ describe('protections', function () {
const NOTE_COUNT = 3
let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT)
notes[0] = await application.mutator.protectNote(notes[0])
notes[1] = await application.mutator.protectNote(notes[1])
notes[0] = await application.protections.protectNote(notes[0])
notes[1] = await application.protections.protectNote(notes[1])
expect(await application.authorizeProtectedActionForNotes(notes, ChallengeReason.SelectProtectedNote)).lengthOf(
NOTE_COUNT,
@@ -468,8 +468,8 @@ describe('protections', function () {
const NOTE_COUNT = 3
let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT)
notes[0] = await application.mutator.protectNote(notes[0])
notes[1] = await application.mutator.protectNote(notes[1])
notes[0] = await application.protections.protectNote(notes[0])
notes[1] = await application.protections.protectNote(notes[1])
expect(await application.authorizeProtectedActionForNotes(notes, ChallengeReason.SelectProtectedNote)).lengthOf(
NOTE_COUNT,
@@ -493,8 +493,8 @@ describe('protections', function () {
const NOTE_COUNT = 3
let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT)
notes[0] = await application.mutator.protectNote(notes[0])
notes[1] = await application.mutator.protectNote(notes[1])
notes[0] = await application.protections.protectNote(notes[0])
notes[1] = await application.protections.protectNote(notes[1])
expect(await application.authorizeProtectedActionForNotes(notes, ChallengeReason.SelectProtectedNote)).lengthOf(1)
expect(challengePrompts).to.equal(1)
@@ -513,7 +513,7 @@ describe('protections', function () {
const NOTE_COUNT = 3
let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT)
notes = await application.mutator.protectNotes(notes)
notes = await application.protections.protectNotes(notes)
for (const note of notes) {
expect(note.protected).to.be.true
@@ -550,8 +550,8 @@ describe('protections', function () {
const NOTE_COUNT = 3
let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT)
notes = await application.mutator.protectNotes(notes)
notes = await application.mutator.unprotectNotes(notes)
notes = await application.protections.protectNotes(notes)
notes = await application.protections.unprotectNotes(notes)
for (const note of notes) {
expect(note.protected).to.be.false
@@ -587,8 +587,8 @@ describe('protections', function () {
const NOTE_COUNT = 3
let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT)
notes = await application.mutator.protectNotes(notes)
notes = await application.mutator.unprotectNotes(notes)
notes = await application.protections.protectNotes(notes)
notes = await application.protections.unprotectNotes(notes)
for (const note of notes) {
expect(note.protected).to.be.false
@@ -612,8 +612,8 @@ describe('protections', function () {
const NOTE_COUNT = 3
let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT)
notes = await application.mutator.protectNotes(notes)
notes = await application.mutator.unprotectNotes(notes)
notes = await application.protections.protectNotes(notes)
notes = await application.protections.unprotectNotes(notes)
for (const note of notes) {
expect(note.protected).to.be(true)

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import { BaseItemCounts } from './lib/Applications.js'
import { BaseItemCounts } from './lib/BaseItemCounts.js'
import * as Factory from './lib/factory.js'
import WebDeviceInterface from './lib/web_device_interface.js'
chai.use(chaiAsPromised)

View File

@@ -1,5 +1,6 @@
import * as Factory from './lib/factory.js'
import * as Files from './lib/Files.js'
import * as Events from './lib/Events.js'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -98,26 +99,43 @@ describe('settings service', function () {
})
it('reads a nonexistent sensitive setting', async () => {
const setting = await application.settings.getDoesSensitiveSettingExist(SettingName.create(SettingName.NAMES.MfaSecret).getValue())
const setting = await application.settings.getDoesSensitiveSettingExist(
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
)
expect(setting).to.equal(false)
})
it('creates and reads a sensitive setting', async () => {
await application.settings.updateSetting(SettingName.create(SettingName.NAMES.MfaSecret).getValue(), 'fake_secret', true)
const setting = await application.settings.getDoesSensitiveSettingExist(SettingName.create(SettingName.NAMES.MfaSecret).getValue())
await application.settings.updateSetting(
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
'fake_secret',
true,
)
const setting = await application.settings.getDoesSensitiveSettingExist(
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
)
expect(setting).to.equal(true)
})
it('creates and lists a sensitive setting', async () => {
await application.settings.updateSetting(SettingName.create(SettingName.NAMES.MfaSecret).getValue(), 'fake_secret', true)
await application.settings.updateSetting(SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue(), MuteFailedBackupsEmailsOption.Muted)
await application.settings.updateSetting(
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
'fake_secret',
true,
)
await application.settings.updateSetting(
SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue(),
MuteFailedBackupsEmailsOption.Muted,
)
const settings = await application.settings.listSettings()
expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue())).to.eql(MuteFailedBackupsEmailsOption.Muted)
expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue())).to.eql(
MuteFailedBackupsEmailsOption.Muted,
)
expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.MfaSecret).getValue())).to.not.be.ok
})
it('reads a subscription setting - @paidfeature', async () => {
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
userEmail: context.email,
subscriptionId: subscriptionId++,
subscriptionName: 'PRO_PLAN',
@@ -130,19 +148,21 @@ describe('settings service', function () {
totalActiveSubscriptionsCount: 1,
userRegisteredAt: 1,
billingFrequency: 12,
payAmount: 59.00
payAmount: 59.0,
})
await Factory.sleep(2)
const setting = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue())
const setting = await application.settings.getSubscriptionSetting(
SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(),
)
expect(setting).to.be.a('string')
})
it('persist irreplaceable subscription settings between subsequent subscriptions - @paidfeature', async () => {
await reInitializeApplicationWithRealCrypto()
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
userEmail: context.email,
subscriptionId: subscriptionId,
subscriptionName: 'PRO_PLAN',
@@ -155,7 +175,7 @@ describe('settings service', function () {
totalActiveSubscriptionsCount: 1,
userRegisteredAt: 1,
billingFrequency: 12,
payAmount: 59.00
payAmount: 59.0,
})
await Factory.sleep(1)
@@ -166,13 +186,17 @@ describe('settings service', function () {
await Factory.sleep(1)
const limitSettingBefore = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue())
const limitSettingBefore = await application.settings.getSubscriptionSetting(
SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(),
)
expect(limitSettingBefore).to.equal('107374182400')
const usedSettingBefore = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue())
const usedSettingBefore = await application.settings.getSubscriptionSetting(
SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
)
expect(usedSettingBefore).to.equal('196')
await Factory.publishMockedEvent('SUBSCRIPTION_EXPIRED', {
await Events.publishMockedEvent('SUBSCRIPTION_EXPIRED', {
userEmail: context.email,
subscriptionId: subscriptionId++,
subscriptionName: 'PRO_PLAN',
@@ -181,11 +205,11 @@ describe('settings service', function () {
totalActiveSubscriptionsCount: 1,
userExistingSubscriptionsCount: 1,
billingFrequency: 12,
payAmount: 59.00
payAmount: 59.0,
})
await Factory.sleep(1)
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
userEmail: context.email,
subscriptionId: subscriptionId++,
subscriptionName: 'PRO_PLAN',
@@ -198,14 +222,18 @@ describe('settings service', function () {
totalActiveSubscriptionsCount: 2,
userRegisteredAt: 1,
billingFrequency: 12,
payAmount: 59.00
payAmount: 59.0,
})
await Factory.sleep(1)
const limitSettingAfter = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue())
const limitSettingAfter = await application.settings.getSubscriptionSetting(
SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(),
)
expect(limitSettingAfter).to.equal(limitSettingBefore)
const usedSettingAfter = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue())
const usedSettingAfter = await application.settings.getSubscriptionSetting(
SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
)
expect(usedSettingAfter).to.equal(usedSettingBefore)
})
})

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import { BaseItemCounts } from './lib/Applications.js'
import { BaseItemCounts } from './lib/BaseItemCounts.js'
import * as Factory from './lib/factory.js'
import WebDeviceInterface from './lib/web_device_interface.js'
chai.use(chaiAsPromised)
@@ -38,7 +38,9 @@ describe('singletons', function () {
this.email = UuidGenerator.GenerateUuid()
this.password = UuidGenerator.GenerateUuid()
this.registerUser = async () => {
this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount
await Factory.registerUserToApplication({
application: this.application,
email: this.email,
@@ -62,7 +64,7 @@ describe('singletons', function () {
])
this.createExtMgr = () => {
return this.application.itemManager.createItem(
return this.application.mutator.createItem(
ContentType.Component,
{
package_info: {
@@ -93,11 +95,11 @@ describe('singletons', function () {
const prefs2 = createPrefsPayload()
const prefs3 = createPrefsPayload()
const items = await this.application.itemManager.emitItemsFromPayloads(
const items = await this.application.mutator.emitItemsFromPayloads(
[prefs1, prefs2, prefs3],
PayloadEmitSource.LocalChanged,
)
await this.application.itemManager.setItemsDirty(items)
await this.application.mutator.setItemsDirty(items)
await this.application.syncService.sync(syncOptions)
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
})
@@ -192,7 +194,7 @@ describe('singletons', function () {
if (!beginCheckingResponse) {
return
}
if (!didCompleteRelevantSync && eventName === SyncEvent.SingleRoundTripSyncCompleted) {
if (!didCompleteRelevantSync && eventName === SyncEvent.PaginatedSyncRequestCompleted) {
didCompleteRelevantSync = true
const saved = data.savedPayloads
expect(saved.length).to.equal(1)
@@ -327,7 +329,7 @@ describe('singletons', function () {
it('alternating the uuid of a singleton should return correct result', async function () {
const payload = createPrefsPayload()
const item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
const item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
await this.application.syncService.sync(syncOptions)
const predicate = new Predicate('content_type', '=', item.content_type)
let resolvedItem = await this.application.singletonManager.findOrCreateContentTypeSingleton(

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import { BaseItemCounts } from './lib/Applications.js'
import { BaseItemCounts } from './lib/BaseItemCounts.js'
import * as Factory from './lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -279,7 +279,7 @@ describe('storage manager', function () {
})
await Factory.createSyncedNote(this.application)
expect(await Factory.storagePayloadCount(this.application)).to.equal(BaseItemCounts.DefaultItems + 1)
expect(await Factory.storagePayloadCount(this.application)).to.equal(BaseItemCounts.DefaultItemsWithAccount + 1)
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
await Factory.sleep(0.1, 'Allow all untrackable singleton syncs to complete')
expect(await Factory.storagePayloadCount(this.application)).to.equal(BaseItemCounts.DefaultItems)

View File

@@ -1,4 +1,5 @@
import * as Factory from './lib/factory.js'
import * as Events from './lib/Events.js'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -31,7 +32,7 @@ describe('subscriptions', function () {
password: context.password,
})
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
userEmail: context.email,
subscriptionId: subscriptionId++,
subscriptionName: 'PRO_PLAN',

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-undef */
import { BaseItemCounts } from '../lib/Applications.js'
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
import * as Factory from '../lib/factory.js'
import { createSyncedNoteWithTag } from '../lib/Items.js'
import * as Utils from '../lib/Utils.js'
@@ -16,7 +16,7 @@ describe('online conflict handling', function () {
beforeEach(async function () {
localStorage.clear()
this.expectedItemCount = BaseItemCounts.DefaultItems
this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount
this.context = await Factory.createAppContextWithFakeCrypto('AppA')
await this.context.launch()
@@ -64,7 +64,7 @@ describe('online conflict handling', function () {
it('components should not be duplicated under any circumstances', async function () {
const payload = createDirtyPayload(ContentType.Component)
const item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
const item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
this.expectedItemCount++
@@ -91,7 +91,7 @@ describe('online conflict handling', function () {
it('items keys should not be duplicated under any circumstances', async function () {
const payload = createDirtyPayload(ContentType.ItemsKey)
const item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
const item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
this.expectedItemCount++
await this.application.syncService.sync(syncOptions)
/** First modify the item without saving so that
@@ -118,7 +118,7 @@ describe('online conflict handling', function () {
// create an item and sync it
const note = await Factory.createMappedNote(this.application)
this.expectedItemCount++
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
await this.application.syncService.sync(syncOptions)
const rawPayloads = await this.application.diskStorageService.getAllRawPayloads()
@@ -128,11 +128,11 @@ describe('online conflict handling', function () {
const dirtyValue = `${Math.random()}`
/** Modify nonsense first to get around strategyWhenConflictingWithItem with previousRevision check */
await this.application.itemManager.changeNote(note, (mutator) => {
await this.application.mutator.changeNote(note, (mutator) => {
mutator.title = 'any'
})
await this.application.itemManager.changeNote(note, (mutator) => {
await this.application.mutator.changeNote(note, (mutator) => {
// modify this item locally to have differing contents from server
mutator.title = dirtyValue
// Intentionally don't change updated_at. We want to simulate a chaotic case where
@@ -238,7 +238,7 @@ describe('online conflict handling', function () {
it('should duplicate item if saving a modified item and clearing our sync token', async function () {
let note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
await this.application.syncService.sync(syncOptions)
this.expectedItemCount++
@@ -279,11 +279,11 @@ describe('online conflict handling', function () {
it('should handle sync conflicts by not duplicating same data', async function () {
const note = await Factory.createMappedNote(this.application)
this.expectedItemCount++
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
await this.application.syncService.sync(syncOptions)
// keep item as is and set dirty
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
// clear sync token so that all items are retrieved on next sync
this.application.syncService.clearSyncPositionTokens()
@@ -295,10 +295,10 @@ describe('online conflict handling', function () {
it('clearing conflict_of on two clients simultaneously should keep us in sync', async function () {
const note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
this.expectedItemCount++
await this.application.mutator.changeAndSaveItem(
await this.application.changeAndSaveItem(
note,
(mutator) => {
// client A
@@ -311,7 +311,7 @@ describe('online conflict handling', function () {
// client B
await this.application.syncService.clearSyncPositionTokens()
await this.application.itemManager.changeItem(
await this.application.mutator.changeItem(
note,
(mutator) => {
mutator.mutableContent.conflict_of = 'bar'
@@ -329,10 +329,10 @@ describe('online conflict handling', function () {
it('setting property on two clients simultaneously should create conflict', async function () {
const note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
this.expectedItemCount++
await this.application.mutator.changeAndSaveItem(
await this.application.changeAndSaveItem(
note,
(mutator) => {
// client A
@@ -369,12 +369,12 @@ describe('online conflict handling', function () {
const note = await Factory.createMappedNote(this.application)
const originalPayload = note.payloadRepresentation()
this.expectedItemCount++
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
await this.application.syncService.sync(syncOptions)
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
// client A
await this.application.itemManager.setItemToBeDeleted(note)
await this.application.mutator.setItemToBeDeleted(note)
await this.application.syncService.sync(syncOptions)
this.expectedItemCount--
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
@@ -387,10 +387,10 @@ describe('online conflict handling', function () {
deleted: false,
updated_at: Factory.yesterday(),
})
await this.application.itemManager.emitItemsFromPayloads([mutatedPayload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([mutatedPayload], PayloadEmitSource.LocalChanged)
const resultNote = this.application.itemManager.findItem(note.uuid)
expect(resultNote.uuid).to.equal(note.uuid)
await this.application.itemManager.setItemDirty(resultNote)
await this.application.mutator.setItemDirty(resultNote)
await this.application.syncService.sync(syncOptions)
// We expect that this item is now gone for good, and a duplicate has not been created.
@@ -400,7 +400,7 @@ describe('online conflict handling', function () {
it('if server says not deleted but client says deleted, keep server state', async function () {
const note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
this.expectedItemCount++
// client A
@@ -426,7 +426,7 @@ describe('online conflict handling', function () {
it('should create conflict if syncing an item that is stale', async function () {
let note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
await this.application.syncService.sync(syncOptions)
note = this.application.items.findItem(note.uuid)
expect(note.dirty).to.equal(false)
@@ -462,7 +462,7 @@ describe('online conflict handling', function () {
it('creating conflict with exactly equal content should keep us in sync', async function () {
const note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
this.expectedItemCount++
await this.application.syncService.sync(syncOptions)
@@ -505,7 +505,7 @@ describe('online conflict handling', function () {
for (const note of this.application.itemManager.getDisplayableNotes()) {
/** First modify the item without saving so that
* our local contents digress from the server's */
await this.application.itemManager.changeItem(note, (mutator) => {
await this.application.mutator.changeItem(note, (mutator) => {
mutator.text = '1'
})
@@ -530,18 +530,18 @@ describe('online conflict handling', function () {
const payload1 = Factory.createStorageItemPayload(ContentType.Tag)
const payload2 = Factory.createStorageItemPayload(ContentType.UserPrefs)
this.expectedItemCount -= 1 /** auto-created user preferences */
await this.application.itemManager.emitItemsFromPayloads([payload1, payload2], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([payload1, payload2], PayloadEmitSource.LocalChanged)
this.expectedItemCount += 2
let tag = this.application.itemManager.getItems(ContentType.Tag)[0]
let userPrefs = this.application.itemManager.getItems(ContentType.UserPrefs)[0]
expect(tag).to.be.ok
expect(userPrefs).to.be.ok
tag = await this.application.itemManager.changeItem(tag, (mutator) => {
tag = await this.application.mutator.changeItem(tag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(userPrefs)
})
await this.application.itemManager.setItemDirty(userPrefs)
await this.application.mutator.setItemDirty(userPrefs)
userPrefs = this.application.items.findItem(userPrefs.uuid)
expect(this.application.itemManager.itemsReferencingItem(userPrefs).length).to.equal(1)
@@ -599,7 +599,7 @@ describe('online conflict handling', function () {
*/
let tag = await Factory.createMappedTag(this.application)
let note = await Factory.createMappedNote(this.application)
tag = await this.application.mutator.changeAndSaveItem(
tag = await this.application.changeAndSaveItem(
tag,
(mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(note)
@@ -608,7 +608,7 @@ describe('online conflict handling', function () {
undefined,
syncOptions,
)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
this.expectedItemCount += 2
await this.application.syncService.sync(syncOptions)
@@ -663,18 +663,18 @@ describe('online conflict handling', function () {
const baseTitle = 'base title'
/** Change the note */
const noteAfterChange = await this.application.itemManager.changeItem(note, (mutator) => {
const noteAfterChange = await this.application.mutator.changeItem(note, (mutator) => {
mutator.title = baseTitle
})
await this.application.sync.sync()
/** Simulate a dropped response by reverting the note back its post-change, pre-sync state */
const retroNote = await this.application.itemManager.emitItemFromPayload(noteAfterChange.payload)
const retroNote = await this.application.mutator.emitItemFromPayload(noteAfterChange.payload)
expect(retroNote.serverUpdatedAt.getTime()).to.equal(noteAfterChange.serverUpdatedAt.getTime())
/** Change the item to its final title and sync */
const finalTitle = 'final title'
await this.application.itemManager.changeItem(note, (mutator) => {
await this.application.mutator.changeItem(note, (mutator) => {
mutator.title = finalTitle
})
await this.application.sync.sync()
@@ -708,7 +708,7 @@ describe('online conflict handling', function () {
errorDecrypting: true,
dirty: true,
})
await this.application.itemManager.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
/**
* Retrieve this note from the server by clearing sync token
@@ -758,7 +758,7 @@ describe('online conflict handling', function () {
email: Utils.generateUuid(),
password: Utils.generateUuid(),
})
await newApp.itemManager.emitItemsFromPayloads(priorData.map((i) => i.payload))
await newApp.mutator.emitItemsFromPayloads(priorData.map((i) => i.payload))
await newApp.syncService.markAllItemsAsNeedingSyncAndPersist()
await newApp.syncService.sync(syncOptions)
expect(newApp.payloadManager.invalidPayloads.length).to.equal(0)
@@ -786,7 +786,7 @@ describe('online conflict handling', function () {
password: password,
})
Factory.handlePasswordChallenges(newApp, password)
await newApp.mutator.importData(backupFile, true)
await newApp.importData(backupFile, true)
expect(newApp.itemManager.getDisplayableTags().length).to.equal(1)
expect(newApp.itemManager.getDisplayableNotes().length).to.equal(1)
await Factory.safeDeinit(newApp)
@@ -801,7 +801,7 @@ describe('online conflict handling', function () {
await createSyncedNoteWithTag(this.application)
const tag = this.application.itemManager.getDisplayableTags()[0]
const note2 = await Factory.createMappedNote(this.application)
await this.application.mutator.changeAndSaveItem(tag, (mutator) => {
await this.application.changeAndSaveItem(tag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(note2)
})
let backupFile = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
@@ -821,7 +821,7 @@ describe('online conflict handling', function () {
password: password,
})
Factory.handlePasswordChallenges(newApp, password)
await newApp.mutator.importData(backupFile, true)
await newApp.importData(backupFile, true)
const newTag = newApp.itemManager.getDisplayableTags()[0]
const notes = newApp.items.referencesForItem(newTag)
expect(notes.length).to.equal(2)
@@ -855,7 +855,7 @@ describe('online conflict handling', function () {
},
dirty: true,
})
await this.application.itemManager.emitItemFromPayload(modified)
await this.application.mutator.emitItemFromPayload(modified)
await this.application.sync.sync()
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(1)
await this.sharedFinalAssertions()
@@ -879,7 +879,7 @@ describe('online conflict handling', function () {
dirty: true,
})
this.expectedItemCount++
await this.application.itemManager.emitItemFromPayload(modified)
await this.application.mutator.emitItemFromPayload(modified)
await this.application.sync.sync()
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(2)
await this.sharedFinalAssertions()
@@ -911,7 +911,7 @@ describe('online conflict handling', function () {
dirty: true,
})
this.expectedItemCount++
await this.application.itemManager.emitItemFromPayload(modified)
await this.application.mutator.emitItemFromPayload(modified)
await this.application.sync.sync()
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(2)
await this.sharedFinalAssertions()

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import { BaseItemCounts } from '../lib/Applications.js'
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -15,7 +15,7 @@ describe('sync integrity', () => {
})
beforeEach(async function () {
this.expectedItemCount = BaseItemCounts.DefaultItems
this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount
this.application = await Factory.createInitAppWithFakeCrypto()
this.email = UuidGenerator.GenerateUuid()
this.password = UuidGenerator.GenerateUuid()
@@ -44,7 +44,7 @@ describe('sync integrity', () => {
})
it('should detect when out of sync', async function () {
const item = await this.application.itemManager.emitItemFromPayload(
const item = await this.application.mutator.emitItemFromPayload(
Factory.createNotePayload(),
PayloadEmitSource.LocalChanged,
)
@@ -60,7 +60,7 @@ describe('sync integrity', () => {
})
it('should self heal after out of sync', async function () {
const item = await this.application.itemManager.emitItemFromPayload(
const item = await this.application.mutator.emitItemFromPayload(
Factory.createNotePayload(),
PayloadEmitSource.LocalChanged,
)

View File

@@ -33,7 +33,7 @@ describe('notes + tags syncing', function () {
it('syncing an item then downloading it should include items_key_id', async function () {
const note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
await this.application.syncService.sync(syncOptions)
await this.application.payloadManager.resetState()
await this.application.itemManager.resetState()
@@ -52,14 +52,14 @@ describe('notes + tags syncing', function () {
const notePayload = pair[0]
const tagPayload = pair[1]
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
const note = this.application.itemManager.getItems([ContentType.Note])[0]
const tag = this.application.itemManager.getItems([ContentType.Tag])[0]
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(1)
expect(this.application.itemManager.getDisplayableTags().length).to.equal(1)
for (let i = 0; i < 9; i++) {
await this.application.itemManager.setItemsDirty([note, tag])
await this.application.mutator.setItemsDirty([note, tag])
await this.application.syncService.sync(syncOptions)
this.application.syncService.clearSyncPositionTokens()
expect(tag.content.references.length).to.equal(1)
@@ -76,10 +76,10 @@ describe('notes + tags syncing', function () {
const pair = createRelatedNoteTagPairPayload()
const notePayload = pair[0]
const tagPayload = pair[1]
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
const originalNote = this.application.itemManager.getDisplayableNotes()[0]
const originalTag = this.application.itemManager.getDisplayableTags()[0]
await this.application.itemManager.setItemsDirty([originalNote, originalTag])
await this.application.mutator.setItemsDirty([originalNote, originalTag])
await this.application.syncService.sync(syncOptions)
@@ -109,12 +109,12 @@ describe('notes + tags syncing', function () {
const pair = createRelatedNoteTagPairPayload()
const notePayload = pair[0]
const tagPayload = pair[1]
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
let note = this.application.itemManager.getDisplayableNotes()[0]
let tag = this.application.itemManager.getDisplayableTags()[0]
expect(this.application.itemManager.itemsReferencingItem(note).length).to.equal(1)
await this.application.itemManager.setItemsDirty([note, tag])
await this.application.mutator.setItemsDirty([note, tag])
await this.application.syncService.sync(syncOptions)
await this.application.syncService.clearSyncPositionTokens()

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import { BaseItemCounts } from '../lib/Applications.js'
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -31,6 +31,21 @@ describe('offline syncing', () => {
localStorage.clear()
})
it('uuid alternation should delete original payload', async function () {
const note = await Factory.createMappedNote(this.application)
this.expectedItemCount++
await Factory.alternateUuidForItem(this.application, note.uuid)
await this.application.sync.sync(syncOptions)
const notes = this.application.itemManager.getDisplayableNotes()
expect(notes.length).to.equal(1)
expect(notes[0].uuid).to.not.equal(note.uuid)
const items = this.application.itemManager.allTrackedItems()
expect(items.length).to.equal(this.expectedItemCount)
})
it('should sync item with no passcode', async function () {
let note = await Factory.createMappedNote(this.application)
expect(Uuids(this.application.itemManager.getDirtyItems()).includes(note.uuid))

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-undef */
import { BaseItemCounts } from '../lib/Applications.js'
import { BaseItemCounts } from '../lib/BaseItemCounts.js'
import * as Factory from '../lib/factory.js'
import * as Utils from '../lib/Utils.js'
chai.use(chaiAsPromised)
@@ -15,7 +15,7 @@ describe('online syncing', function () {
beforeEach(async function () {
localStorage.clear()
this.expectedItemCount = BaseItemCounts.DefaultItems
this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount
this.context = await Factory.createAppContext()
await this.context.launch()
@@ -43,8 +43,10 @@ describe('online syncing', function () {
afterEach(async function () {
expect(this.application.syncService.isOutOfSync()).to.equal(false)
const items = this.application.itemManager.allTrackedItems()
expect(items.length).to.equal(this.expectedItemCount)
const rawPayloads = await this.application.diskStorageService.getAllRawPayloads()
expect(rawPayloads.length).to.equal(this.expectedItemCount)
await Factory.safeDeinit(this.application)
@@ -119,18 +121,6 @@ describe('online syncing', function () {
await Factory.sleep(0.5)
}).timeout(20000)
it('uuid alternation should delete original payload', async function () {
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
const note = await Factory.createMappedNote(this.application)
this.expectedItemCount++
await Factory.alternateUuidForItem(this.application, note.uuid)
await this.application.sync.sync(syncOptions)
const notes = this.application.itemManager.getDisplayableNotes()
expect(notes.length).to.equal(1)
expect(notes[0].uuid).to.not.equal(note.uuid)
})
it('having offline data then signing in should not alternate uuid and merge with account', async function () {
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
const note = await Factory.createMappedNote(this.application)
@@ -222,7 +212,7 @@ describe('online syncing', function () {
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
const promise = new Promise((resolve) => {
this.application.syncService.addEventObserver(async (event) => {
if (event === SyncEvent.SingleRoundTripSyncCompleted) {
if (event === SyncEvent.PaginatedSyncRequestCompleted) {
const note = this.application.items.findItem(originalNote.uuid)
if (note) {
expect(note.dirty).to.not.be.ok
@@ -241,7 +231,7 @@ describe('online syncing', function () {
expect(this.application.itemManager.getDisplayableItemsKeys().length).to.equal(1)
const note = await Factory.createMappedNote(this.application)
this.expectedItemCount++
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
await this.application.syncService.sync(syncOptions)
const rawPayloads = await this.application.diskStorageService.getAllRawPayloads()
const notePayload = noteObjectsFromObjects(rawPayloads)
@@ -283,7 +273,7 @@ describe('online syncing', function () {
const originalTitle = note.content.title
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
await this.application.syncService.sync(syncOptions)
const encrypted = CreateEncryptedServerSyncPushPayload(
@@ -299,7 +289,7 @@ describe('online syncing', function () {
errorDecrypting: true,
})
const items = await this.application.itemManager.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
const items = await this.application.mutator.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
const mappedItem = this.application.itemManager.findAnyItem(errorred.uuid)
@@ -311,7 +301,7 @@ describe('online syncing', function () {
},
})
const mappedItems2 = await this.application.itemManager.emitItemsFromPayloads(
const mappedItems2 = await this.application.mutator.emitItemsFromPayloads(
[decryptedPayload],
PayloadEmitSource.LocalChanged,
)
@@ -336,14 +326,14 @@ describe('online syncing', function () {
let note = await Factory.createMappedNote(this.application)
this.expectedItemCount++
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
await this.application.syncService.sync(syncOptions)
note = this.application.items.findItem(note.uuid)
expect(note.dirty).to.equal(false)
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
await this.application.itemManager.setItemToBeDeleted(note)
await this.application.mutator.setItemToBeDeleted(note)
note = this.application.items.findAnyItem(note.uuid)
expect(note.dirty).to.equal(true)
this.expectedItemCount--
@@ -361,7 +351,7 @@ describe('online syncing', function () {
it('retrieving item with no content should correctly map local state', async function () {
const note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
await this.application.syncService.sync(syncOptions)
const syncToken = await this.application.syncService.getLastSyncToken()
@@ -370,7 +360,7 @@ describe('online syncing', function () {
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
// client A
await this.application.itemManager.setItemToBeDeleted(note)
await this.application.mutator.setItemToBeDeleted(note)
await this.application.syncService.sync(syncOptions)
// Subtract 1
@@ -399,7 +389,7 @@ describe('online syncing', function () {
await Factory.sleep(0.1)
await this.application.itemManager.changeItem(note, (mutator) => {
await this.application.mutator.changeItem(note, (mutator) => {
mutator.title = 'latest title'
})
@@ -427,7 +417,7 @@ describe('online syncing', function () {
await Factory.sleep(0.1)
await this.application.itemManager.setItemToBeDeleted(note)
await this.application.mutator.setItemToBeDeleted(note)
this.expectedItemCount--
@@ -444,8 +434,8 @@ describe('online syncing', function () {
it('items that are never synced and deleted should not be uploaded to server', async function () {
const note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.itemManager.setItemToBeDeleted(note)
await this.application.mutator.setItemDirty(note)
await this.application.mutator.setItemToBeDeleted(note)
let success = true
let didCompleteRelevantSync = false
@@ -457,7 +447,7 @@ describe('online syncing', function () {
if (!beginCheckingResponse) {
return
}
if (!didCompleteRelevantSync && eventName === SyncEvent.SingleRoundTripSyncCompleted) {
if (!didCompleteRelevantSync && eventName === SyncEvent.PaginatedSyncRequestCompleted) {
didCompleteRelevantSync = true
const response = data
const matching = response.savedPayloads.find((p) => p.uuid === note.uuid)
@@ -474,20 +464,20 @@ describe('online syncing', function () {
it('items that are deleted after download first sync complete should not be uploaded to server', async function () {
/** The singleton manager may delete items are download first. We dont want those uploaded to server. */
const note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
let success = true
let didCompleteRelevantSync = false
let beginCheckingResponse = false
this.application.syncService.addEventObserver(async (eventName, data) => {
if (eventName === SyncEvent.DownloadFirstSyncCompleted) {
await this.application.itemManager.setItemToBeDeleted(note)
await this.application.mutator.setItemToBeDeleted(note)
beginCheckingResponse = true
}
if (!beginCheckingResponse) {
return
}
if (!didCompleteRelevantSync && eventName === SyncEvent.SingleRoundTripSyncCompleted) {
if (!didCompleteRelevantSync && eventName === SyncEvent.PaginatedSyncRequestCompleted) {
didCompleteRelevantSync = true
const response = data
const matching = response.savedPayloads.find((p) => p.uuid === note.uuid)
@@ -527,7 +517,7 @@ describe('online syncing', function () {
const decryptionResults = await this.application.protocolService.decryptSplit(keyedSplit)
await this.application.itemManager.emitItemsFromPayloads(decryptionResults, PayloadEmitSource.LocalChanged)
await this.application.mutator.emitItemsFromPayloads(decryptionResults, PayloadEmitSource.LocalChanged)
expect(this.application.itemManager.allTrackedItems().length).to.equal(this.expectedItemCount)
@@ -543,7 +533,7 @@ describe('online syncing', function () {
const largeItemCount = SyncUpDownLimit + 10
for (let i = 0; i < largeItemCount; i++) {
const note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
}
this.expectedItemCount += largeItemCount
@@ -558,7 +548,7 @@ describe('online syncing', function () {
const largeItemCount = SyncUpDownLimit + 10
for (let i = 0; i < largeItemCount; i++) {
const note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
}
/** Upload */
this.application.syncService.sync({ awaitAll: true, checkIntegrity: false })
@@ -583,7 +573,7 @@ describe('online syncing', function () {
it('syncing an item should storage it encrypted', async function () {
const note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
await this.application.syncService.sync(syncOptions)
this.expectedItemCount++
const rawPayloads = await this.application.diskStorageService.getAllRawPayloads()
@@ -593,7 +583,7 @@ describe('online syncing', function () {
it('syncing an item before data load should storage it encrypted', async function () {
const note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
this.expectedItemCount++
/** Simulate database not loaded */
@@ -610,7 +600,7 @@ describe('online syncing', function () {
it('saving an item after sync should persist it with content property', async function () {
const note = await Factory.createMappedNote(this.application)
const text = Factory.randomString(10000)
await this.application.mutator.changeAndSaveItem(
await this.application.changeAndSaveItem(
note,
(mutator) => {
mutator.text = text
@@ -634,7 +624,7 @@ describe('online syncing', function () {
expect(this.application.itemManager.getDirtyItems().length).to.equal(0)
let note = await Factory.createMappedNote(this.application)
note = await this.application.itemManager.changeItem(note, (mutator) => {
note = await this.application.mutator.changeItem(note, (mutator) => {
mutator.text = `${Math.random()}`
})
/** This sync request should exit prematurely as we called ut_setDatabaseNotLoaded */
@@ -705,13 +695,13 @@ describe('online syncing', function () {
it('valid sync date tracking', async function () {
let note = await Factory.createMappedNote(this.application)
note = await this.application.itemManager.setItemDirty(note)
note = await this.application.mutator.setItemDirty(note)
this.expectedItemCount++
expect(note.dirty).to.equal(true)
expect(note.payload.dirtyIndex).to.be.at.most(getCurrentDirtyIndex())
note = await this.application.itemManager.changeItem(note, (mutator) => {
note = await this.application.mutator.changeItem(note, (mutator) => {
mutator.text = `${Math.random()}`
})
const sync = this.application.sync.sync(syncOptions)
@@ -748,7 +738,7 @@ describe('online syncing', function () {
* It will do based on comparing whether item.dirtyIndex > item.globalDirtyIndexAtLastSync
*/
let note = await Factory.createMappedNote(this.application)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
this.expectedItemCount++
// client A. Don't await, we want to do other stuff.
@@ -759,12 +749,12 @@ describe('online syncing', function () {
// While that sync is going on, we want to modify this item many times.
const text = `${Math.random()}`
note = await this.application.itemManager.changeItem(note, (mutator) => {
note = await this.application.mutator.changeItem(note, (mutator) => {
mutator.text = text
})
await this.application.itemManager.setItemDirty(note)
await this.application.itemManager.setItemDirty(note)
await this.application.itemManager.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
await this.application.mutator.setItemDirty(note)
expect(note.payload.dirtyIndex).to.be.above(note.payload.globalDirtyIndexAtLastSync)
// Now do a regular sync with no latency.
@@ -817,7 +807,7 @@ describe('online syncing', function () {
setTimeout(
async function () {
await this.application.itemManager.changeItem(note, (mutator) => {
await this.application.mutator.changeItem(note, (mutator) => {
mutator.text = newText
})
}.bind(this),
@@ -862,9 +852,9 @@ describe('online syncing', function () {
const newText = `${Math.random()}`
this.application.syncService.addEventObserver(async (eventName) => {
if (eventName === SyncEvent.SyncWillBegin && !didPerformMutatation) {
if (eventName === SyncEvent.SyncDidBeginProcessing && !didPerformMutatation) {
didPerformMutatation = true
await this.application.itemManager.changeItem(note, (mutator) => {
await this.application.mutator.changeItem(note, (mutator) => {
mutator.text = newText
})
}
@@ -898,7 +888,7 @@ describe('online syncing', function () {
dirtyIndex: changed[0].payload.globalDirtyIndexAtLastSync + 1,
})
await this.application.itemManager.emitItemFromPayload(mutated)
await this.application.mutator.emitItemFromPayload(mutated)
}
})
@@ -916,6 +906,7 @@ describe('online syncing', function () {
const note = await Factory.createSyncedNote(this.application)
const preDeleteSyncToken = await this.application.syncService.getLastSyncToken()
await this.application.mutator.deleteItem(note)
await this.application.sync.sync()
await this.application.syncService.setLastSyncToken(preDeleteSyncToken)
await this.application.sync.sync(syncOptions)
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
@@ -938,7 +929,7 @@ describe('online syncing', function () {
dirty: true,
})
await this.application.itemManager.emitItemFromPayload(errored)
await this.application.payloadManager.emitPayload(errored)
await this.application.sync.sync(syncOptions)
const updatedNote = this.application.items.findAnyItem(note.uuid)
@@ -966,7 +957,7 @@ describe('online syncing', function () {
},
})
await this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [] }, response)
await this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [], options: {} }, response)
expect(this.application.payloadManager.findOne(invalidPayload.uuid)).to.not.be.ok
expect(this.application.payloadManager.findOne(validPayload.uuid)).to.be.ok
@@ -995,7 +986,7 @@ describe('online syncing', function () {
content: {},
})
this.expectedItemCount++
await this.application.itemManager.emitItemsFromPayloads([payload])
await this.application.mutator.emitItemsFromPayloads([payload])
await this.application.sync.sync(syncOptions)
/** Item should no longer be dirty, otherwise it would keep syncing */
@@ -1006,7 +997,7 @@ describe('online syncing', function () {
it('should call onPresyncSave before sync begins', async function () {
const events = []
this.application.syncService.addEventObserver((event) => {
if (event === SyncEvent.SyncWillBegin) {
if (event === SyncEvent.SyncDidBeginProcessing) {
events.push('sync-will-begin')
}
})
@@ -1032,6 +1023,7 @@ describe('online syncing', function () {
const note = await Factory.createSyncedNote(this.application)
await this.application.mutator.deleteItem(note)
await this.application.sync.sync()
expect(conditionMet).to.equal(true)
})

View File

@@ -12,14 +12,9 @@
<script src="https://unpkg.com/sinon@13.0.2/pkg/sinon.js"></script>
<script src="./vendor/sncrypto-web.js"></script>
<script src="../dist/snjs.js"></script>
<script>
const urlParams = new URLSearchParams(window.location.search);
const syncServerHostName = urlParams.get('sync_server_host_name') ?? 'syncing-server-proxy';
const bail = urlParams.get('bail') === 'false' ? false : true;
const skipPaidFeatures = urlParams.get('skip_paid_features') === 'true' ? true : false;
<script type="module">
Object.assign(window, SNCrypto);
Object.assign(window, SNLibrary);
SNLog.onLog = (message) => {
@@ -30,6 +25,10 @@
console.error(error);
};
const urlParams = new URLSearchParams(window.location.search);
const bail = urlParams.get('bail') === 'false' ? false : true;
const skipPaidFeatures = urlParams.get('skip_paid_features') === 'true' ? true : false;
mocha.setup({
ui: 'bdd',
timeout: 5000,
@@ -39,63 +38,42 @@
mocha.grep('@paidfeature').invert();
}
</script>
<script type="module" src="memory.test.js"></script>
<script type="module" src="protocol.test.js"></script>
<script type="module" src="utils.test.js"></script>
<script type="module" src="000.test.js"></script>
<script type="module" src="001.test.js"></script>
<script type="module" src="002.test.js"></script>
<script type="module" src="003.test.js"></script>
<script type="module" src="004.test.js"></script>
<script type="module" src="username.test.js"></script>
<script type="module" src="app-group.test.js"></script>
<script type="module" src="application.test.js"></script>
<script type="module" src="payload.test.js"></script>
<script type="module" src="payload_encryption.test.js"></script>
<script type="module" src="item.test.js"></script>
<script type="module" src="item_manager.test.js"></script>
<script type="module" src="features.test.js"></script>
<script type="module" src="settings.test.js"></script>
<script type="module" src="mfa_service.test.js"></script>
<script type="module" src="mutator.test.js"></script>
<script type="module" src="payload_manager.test.js"></script>
<script type="module" src="collections.test.js"></script>
<script type="module" src="note_display_criteria.test.js"></script>
<script type="module" src="keys.test.js"></script>
<script type="module" src="key_params.test.js"></script>
<script type="module" src="key_recovery_service.test.js"></script>
<script type="module" src="backups.test.js"></script>
<script type="module" src="upgrading.test.js"></script>
<script type="module" src="model_tests/importing.test.js"></script>
<script type="module" src="model_tests/appmodels.test.js"></script>
<script type="module" src="model_tests/items.test.js"></script>
<script type="module" src="model_tests/mapping.test.js"></script>
<script type="module" src="model_tests/notes_smart_tags.test.js"></script>
<script type="module" src="model_tests/notes_tags.test.js"></script>
<script type="module" src="model_tests/notes_tags_folders.test.js"></script>
<script type="module" src="model_tests/performance.test.js"></script>
<script type="module" src="sync_tests/offline.test.js"></script>
<script type="module" src="sync_tests/notes_tags.test.js"></script>
<script type="module" src="sync_tests/online.test.js"></script>
<script type="module" src="sync_tests/conflicting.test.js"></script>
<script type="module" src="sync_tests/integrity.test.js"></script>
<script type="module" src="auth-fringe-cases.test.js"></script>
<script type="module" src="auth.test.js"></script>
<script type="module" src="device_auth.test.js"></script>
<script type="module" src="storage.test.js"></script>
<script type="module" src="protection.test.js"></script>
<script type="module" src="singletons.test.js"></script>
<script type="module" src="migrations/migration.test.js"></script>
<script type="module" src="migrations/tags-to-folders.test.js"></script>
<script type="module" src="history.test.js"></script>
<script type="module" src="actions.test.js"></script>
<script type="module" src="preferences.test.js"></script>
<script type="module" src="files.test.js"></script>
<script type="module" src="session.test.js"></script>
<script type="module" src="subscriptions.test.js"></script>
<script type="module" src="recovery.test.js"></script>
<script type="module">
mocha.run();
import MainRegistry from './TestRegistry/MainRegistry.js'
const InternalFeatureStatus = {
[InternalFeature.Vaults]: { enabled: false, exclusive: false },
}
const loadTest = (fileName) => {
return new Promise((resolve) => {
const script = document.createElement('script');
script.type = 'module';
script.src = fileName;
script.async = false;
script.defer = false;
script.addEventListener('load', resolve);
document.head.append(script);
})
}
const loadTests = async (fileNames) => {
for (const fileName of fileNames) {
await loadTest(fileName);
}
}
if (InternalFeatureStatus[InternalFeature.Vaults].enabled) {
InternalFeatureService.get().enableFeature(InternalFeature.Vaults);
await loadTests(MainRegistry.VaultTests);
}
if (!InternalFeatureStatus[InternalFeature.Vaults].exclusive) {
await loadTests(MainRegistry.BaseTests);
}
mocha.run()
</script>
</head>
@@ -103,4 +81,4 @@
<div id="mocha"></div>
</body>
</html>
</html>

View File

@@ -173,7 +173,7 @@ describe('upgrading', () => {
it('protocol version should be upgraded on password change', async function () {
/** Delete default items key that is created on launch */
const itemsKey = await this.application.protocolService.getSureDefaultItemsKey()
await this.application.itemManager.setItemToBeDeleted(itemsKey)
await this.application.mutator.setItemToBeDeleted(itemsKey)
expect(Uuids(this.application.itemManager.getDisplayableItemsKeys()).includes(itemsKey.uuid)).to.equal(false)
Factory.createMappedNote(this.application)

View File

@@ -0,0 +1,277 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('asymmetric messages', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let service
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
service = context.asymmetric
})
it('should not trust message if the trusted payload data recipientUuid does not match the message user uuid', async () => {
console.error('TODO: implement')
})
it('should delete message after processing it', async () => {
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
const eventData = {
oldKeyPair: context.encryption.getKeyPair(),
oldSigningKeyPair: context.encryption.getSigningKeyPair(),
newKeyPair: context.encryption.getKeyPair(),
newSigningKeyPair: context.encryption.getSigningKeyPair(),
}
await service.sendOwnContactChangeEventToAllContacts(eventData)
const deleteFunction = sinon.spy(contactContext.asymmetric, 'deleteMessageAfterProcessing')
const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await promise
expect(deleteFunction.callCount).to.equal(1)
const messages = await contactContext.asymmetric.getInboundMessages()
expect(messages.length).to.equal(0)
await deinitContactContext()
})
it('should send contact share message after trusted contact belonging to group changes', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
context,
sharedVault,
)
await Collaboration.acceptAllInvites(thirdPartyContext)
const sendContactSharePromise = context.resolveWhenSharedVaultServiceSendsContactShareMessage()
await context.contacts.createOrEditTrustedContact({
contactUuid: thirdPartyContext.userUuid,
publicKey: thirdPartyContext.publicKey,
signingPublicKey: thirdPartyContext.signingPublicKey,
name: 'Changed 3rd Party Name',
})
await sendContactSharePromise
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findTrustedContact(thirdPartyContext.userUuid)
expect(updatedContact.name).to.equal('Changed 3rd Party Name')
await deinitContactContext()
await deinitThirdPartyContext()
})
it('should not send contact share message to self or to contact who is changed', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
context,
sharedVault,
)
const handleInitialContactShareMessage = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await Collaboration.acceptAllInvites(thirdPartyContext)
await contactContext.sync()
await handleInitialContactShareMessage
const sendContactSharePromise = context.resolveWhenSharedVaultServiceSendsContactShareMessage()
await context.contacts.createOrEditTrustedContact({
contactUuid: thirdPartyContext.userUuid,
publicKey: thirdPartyContext.publicKey,
signingPublicKey: thirdPartyContext.signingPublicKey,
name: 'Changed 3rd Party Name',
})
await sendContactSharePromise
const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedContactShareMessage')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedContactShareMessage')
const thirdPartySpy = sinon.spy(thirdPartyContext.asymmetric, 'handleTrustedContactShareMessage')
await context.sync()
await contactContext.sync()
await thirdPartyContext.sync()
expect(firstPartySpy.callCount).to.equal(0)
expect(secondPartySpy.callCount).to.equal(1)
expect(thirdPartySpy.callCount).to.equal(0)
await deinitThirdPartyContext()
await deinitContactContext()
})
it('should send shared vault root key change message after root key change', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
await context.vaults.rotateVaultRootKey(sharedVault)
const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
await context.sync()
await contactContext.sync()
expect(firstPartySpy.callCount).to.equal(0)
expect(secondPartySpy.callCount).to.equal(1)
await deinitContactContext()
})
it('should send shared vault metadata change message after shared vault name change', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
await context.vaults.changeVaultNameAndDescription(sharedVault, {
name: 'New Name',
description: 'New Description',
})
const firstPartySpy = sinon.spy(context.asymmetric, 'handleVaultMetadataChangedMessage')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleVaultMetadataChangedMessage')
await context.sync()
await contactContext.sync()
expect(firstPartySpy.callCount).to.equal(0)
expect(secondPartySpy.callCount).to.equal(1)
const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(updatedVault.name).to.equal('New Name')
expect(updatedVault.description).to.equal('New Description')
await deinitContactContext()
})
it('should send sender keypair changed message to trusted contacts', async () => {
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
await context.changePassword('new password')
const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSenderKeypairChangedMessage')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage')
await context.sync()
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await completedProcessingMessagesPromise
expect(firstPartySpy.callCount).to.equal(0)
expect(secondPartySpy.callCount).to.equal(1)
const contact = contactContext.contacts.findTrustedContact(context.userUuid)
expect(contact.publicKeySet.encryption).to.equal(context.publicKey)
expect(contact.publicKeySet.signing).to.equal(context.signingPublicKey)
await deinitContactContext()
})
it('should process sender keypair changed message', async () => {
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
await Collaboration.createTrustedContactForUserOfContext(contactContext, context)
const originalContact = contactContext.contacts.findTrustedContact(context.userUuid)
await context.changePassword('new_password')
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findTrustedContact(context.userUuid)
expect(updatedContact.publicKeySet.encryption).to.not.equal(originalContact.publicKeySet.encryption)
expect(updatedContact.publicKeySet.signing).to.not.equal(originalContact.publicKeySet.signing)
expect(updatedContact.publicKeySet.encryption).to.equal(context.publicKey)
expect(updatedContact.publicKeySet.signing).to.equal(context.signingPublicKey)
await deinitContactContext()
})
it('sender keypair changed message should be signed using old key pair', async () => {
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
const oldKeyPair = context.encryption.getKeyPair()
const oldSigningKeyPair = context.encryption.getSigningKeyPair()
await context.changePassword('new password')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage')
await context.sync()
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await completedProcessingMessagesPromise
const message = secondPartySpy.args[0][0]
const encryptedMessage = message.encrypted_message
const publicKeySet =
contactContext.encryption.getSenderPublicKeySetFromAsymmetricallyEncryptedString(encryptedMessage)
expect(publicKeySet.encryption).to.equal(oldKeyPair.publicKey)
expect(publicKeySet.signing).to.equal(oldSigningKeyPair.publicKey)
await deinitContactContext()
})
it('sender keypair changed message should contain new keypair and be trusted', async () => {
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
await context.changePassword('new password')
const newKeyPair = context.encryption.getKeyPair()
const newSigningKeyPair = context.encryption.getSigningKeyPair()
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findTrustedContact(context.userUuid)
expect(updatedContact.publicKeySet.encryption).to.equal(newKeyPair.publicKey)
expect(updatedContact.publicKeySet.signing).to.equal(newSigningKeyPair.publicKey)
await deinitContactContext()
})
it('should delete all inbound messages after changing user password', async () => {
/** Messages to user are encrypted with old keypair and are no longer decryptable */
console.error('TODO: implement test')
})
})

View File

@@ -0,0 +1,186 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault conflicts', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
})
it('after being removed from shared vault, attempting to sync previous vault item should result in SharedVaultNotMemberError. The item should be duplicated then removed.', async () => {
const { sharedVault, note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
contactContext.lockSyncing()
await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
const promise = contactContext.resolveWithConflicts()
contactContext.unlockSyncing()
await contactContext.changeNoteTitleAndSync(note, 'new title')
const conflicts = await promise
await contactContext.sync()
expect(conflicts.length).to.equal(1)
expect(conflicts[0].type).to.equal(ConflictType.SharedVaultNotMemberError)
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
const collaboratorNotes = contactContext.items.getDisplayableNotes()
expect(collaboratorNotes.length).to.equal(1)
expect(collaboratorNotes[0].duplicate_of).to.not.be.undefined
expect(collaboratorNotes[0].title).to.equal('new title')
await deinitContactContext()
})
it('conflicts created should be associated with the vault', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await context.changeNoteTitle(note, 'new title first client')
await contactContext.changeNoteTitle(note, 'new title second client')
const doneAddingConflictToSharedVault = contactContext.resolveWhenSavedSyncPayloadsIncludesItemThatIsDuplicatedOf(
note.uuid,
)
await context.sync({ desc: 'First client sync' })
await contactContext.sync({
desc: 'Second client sync with conflicts to be created',
})
await doneAddingConflictToSharedVault
await context.sync({ desc: 'First client sync with conflicts to be pulled in' })
expect(context.items.invalidItems.length).to.equal(0)
expect(contactContext.items.invalidItems.length).to.equal(0)
const collaboratorNotes = contactContext.items.getDisplayableNotes()
expect(collaboratorNotes.length).to.equal(2)
expect(collaboratorNotes.find((note) => !!note.duplicate_of)).to.not.be.undefined
const originatorNotes = context.items.getDisplayableNotes()
expect(originatorNotes.length).to.equal(2)
expect(originatorNotes.find((note) => !!note.duplicate_of)).to.not.be.undefined
await deinitContactContext()
})
it('attempting to modify note as read user should result in SharedVaultInsufficientPermissionsError', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context, SharedVaultPermission.Read)
const promise = contactContext.resolveWithConflicts()
await contactContext.changeNoteTitleAndSync(note, 'new title')
const conflicts = await promise
expect(conflicts.length).to.equal(1)
expect(conflicts[0].type).to.equal(ConflictType.SharedVaultInsufficientPermissionsError)
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
await deinitContactContext()
})
it('should handle SharedVaultNotMemberError by duplicating item to user non-vault', async () => {
const { sharedVault, note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
await contactContext.changeNoteTitleAndSync(note, 'new title')
const notes = contactContext.notes
expect(notes.length).to.equal(1)
expect(notes[0].title).to.equal('new title')
expect(notes[0].key_system_identifier).to.not.be.ok
expect(notes[0].duplicate_of).to.equal(note.uuid)
await deinitContactContext()
})
it('attempting to save note to non-existent vault should result in SharedVaultNotMemberError conflict', async () => {
context.anticipateConsoleError(
'Error decrypting contentKey from parameters',
'An invalid shared vault uuid is being assigned to an item',
)
const { note } = await Collaboration.createSharedVaultWithNote(context)
const promise = context.resolveWithConflicts()
const objectToSpy = context.application.sync
sinon.stub(objectToSpy, 'payloadsByPreparingForServer').callsFake(async (params) => {
objectToSpy.payloadsByPreparingForServer.restore()
const payloads = await objectToSpy.payloadsByPreparingForServer(params)
for (const payload of payloads) {
payload.shared_vault_uuid = 'non-existent-vault-uuid-123'
}
return payloads
})
await context.changeNoteTitleAndSync(note, 'new-title')
const conflicts = await promise
expect(conflicts.length).to.equal(1)
expect(conflicts[0].type).to.equal(ConflictType.SharedVaultNotMemberError)
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
})
it('should create a non-vaulted copy if attempting to move item from vault to user and item belongs to someone else', async () => {
const { note, sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const promise = contactContext.resolveWithConflicts()
await contactContext.vaults.removeItemFromVault(note)
const conflicts = await promise
expect(conflicts.length).to.equal(1)
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
const duplicateNote = contactContext.findDuplicateNote(note.uuid)
expect(duplicateNote).to.not.be.undefined
expect(duplicateNote.key_system_identifier).to.not.be.ok
const existingNote = contactContext.items.findItem(note.uuid)
expect(existingNote.key_system_identifier).to.equal(sharedVault.systemIdentifier)
await deinitContactContext()
})
it('should created a non-vaulted copy if admin attempts to move item from vault to user if the item belongs to someone else', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const note = await contactContext.createSyncedNote('foo', 'bar')
await Collaboration.moveItemToVault(contactContext, sharedVault, note)
await context.sync()
const promise = context.resolveWithConflicts()
await context.vaults.removeItemFromVault(note)
const conflicts = await promise
expect(conflicts.length).to.equal(1)
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
const duplicateNote = context.findDuplicateNote(note.uuid)
expect(duplicateNote).to.not.be.undefined
expect(duplicateNote.key_system_identifier).to.not.be.ok
const existingNote = context.items.findItem(note.uuid)
expect(existingNote.key_system_identifier).to.equal(sharedVault.systemIdentifier)
await deinitContactContext()
})
})

View File

@@ -0,0 +1,83 @@
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('contacts', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
})
it('should create contact', async () => {
const contact = await context.contacts.createOrEditTrustedContact({
name: 'John Doe',
publicKey: 'my_public_key',
signingPublicKey: 'my_signing_public_key',
contactUuid: '123',
})
expect(contact).to.not.be.undefined
expect(contact.name).to.equal('John Doe')
expect(contact.publicKeySet.encryption).to.equal('my_public_key')
expect(contact.publicKeySet.signing).to.equal('my_signing_public_key')
expect(contact.contactUuid).to.equal('123')
})
it('should create self contact on registration', async () => {
const selfContact = context.contacts.getSelfContact()
expect(selfContact).to.not.be.undefined
expect(selfContact.publicKeySet.encryption).to.equal(context.publicKey)
expect(selfContact.publicKeySet.signing).to.equal(context.signingPublicKey)
})
it('should create self contact on sign in if it does not exist', async () => {
let selfContact = context.contacts.getSelfContact()
await context.mutator.setItemToBeDeleted(selfContact)
await context.sync()
await context.signout()
await context.signIn()
selfContact = context.contacts.getSelfContact()
expect(selfContact).to.not.be.undefined
})
it('should update self contact on password change', async () => {
const selfContact = context.contacts.getSelfContact()
await context.changePassword('new_password')
const updatedSelfContact = context.contacts.getSelfContact()
expect(updatedSelfContact.publicKeySet.encryption).to.not.equal(selfContact.publicKeySet.encryption)
expect(updatedSelfContact.publicKeySet.signing).to.not.equal(selfContact.publicKeySet.signing)
expect(updatedSelfContact.publicKeySet.encryption).to.equal(context.publicKey)
expect(updatedSelfContact.publicKeySet.signing).to.equal(context.signingPublicKey)
})
it('should not be able to delete self contact', async () => {
const selfContact = context.contacts.getSelfContact()
await Factory.expectThrowsAsync(() => context.contacts.deleteContact(selfContact), 'Cannot delete self')
})
it('should not be able to delete a trusted contact if it belongs to a vault I administer', async () => {
console.error('TODO: implement test')
})
})

View File

@@ -0,0 +1,204 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault crypto', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
})
describe('root key', () => {
it('root key loaded from disk should have keypairs', async () => {
const appIdentifier = context.identifier
await context.deinit()
let recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
expect(recreatedContext.encryption.getKeyPair()).to.not.be.undefined
expect(recreatedContext.encryption.getSigningKeyPair()).to.not.be.undefined
})
it('changing user password should re-encrypt all key system root keys', async () => {
console.error('TODO: implement')
})
it('changing user password should re-encrypt all trusted contacts', async () => {
console.error('TODO: implement')
})
})
describe('persistent content signature', () => {
it('storage payloads should include signatureData', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await contactContext.changeNoteTitleAndSync(note, 'new title')
await context.sync()
const rawPayloads = await context.application.diskStorageService.getAllRawPayloads()
const noteRawPayload = rawPayloads.find((payload) => payload.uuid === note.uuid)
expect(noteRawPayload.signatureData).to.not.be.undefined
await deinitContactContext()
})
it('changing item content should erase existing signatureData', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await contactContext.changeNoteTitleAndSync(note, 'new title')
await context.sync()
let updatedNote = context.items.findItem(note.uuid)
await context.changeNoteTitleAndSync(updatedNote, 'new title 2')
updatedNote = context.items.findItem(note.uuid)
expect(updatedNote.signatureData).to.be.undefined
await deinitContactContext()
})
it('encrypting an item into storage then loading it should verify authenticity of original content rather than most recent symmetric signature', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await contactContext.changeNoteTitleAndSync(note, 'new title')
/** Override decrypt result to return failing signature */
const objectToSpy = context.encryption
sinon.stub(objectToSpy, 'decryptSplit').callsFake(async (split) => {
objectToSpy.decryptSplit.restore()
const decryptedPayloads = await objectToSpy.decryptSplit(split)
expect(decryptedPayloads.length).to.equal(1)
const payload = decryptedPayloads[0]
const mutatedPayload = new DecryptedPayload({
...payload.ejected(),
signatureData: {
...payload.signatureData,
result: {
...payload.signatureData.result,
passes: false,
},
},
})
return [mutatedPayload]
})
await context.sync()
let updatedNote = context.items.findItem(note.uuid)
expect(updatedNote.content.title).to.equal('new title')
expect(updatedNote.signatureData.result.passes).to.equal(false)
const appIdentifier = context.identifier
await context.deinit()
let recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
updatedNote = recreatedContext.items.findItem(note.uuid)
expect(updatedNote.signatureData.result.passes).to.equal(false)
/** Changing the content now should clear failing signature */
await recreatedContext.changeNoteTitleAndSync(updatedNote, 'new title 2')
updatedNote = recreatedContext.items.findItem(note.uuid)
expect(updatedNote.signatureData).to.be.undefined
await recreatedContext.deinit()
recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
/** Decrypting from storage will now verify current user symmetric signature only */
updatedNote = recreatedContext.items.findItem(note.uuid)
expect(updatedNote.signatureData.result.passes).to.equal(true)
await recreatedContext.deinit()
await deinitContactContext()
})
})
describe('symmetrically encrypted items', () => {
it('created items with a payload source of remote saved should not have signature data', async () => {
const note = await context.createSyncedNote()
expect(note.payload.source).to.equal(PayloadSource.RemoteSaved)
expect(note.signatureData).to.be.undefined
})
it('retrieved items that are then remote saved should have their signature data cleared', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await contactContext.changeNoteTitleAndSync(contactContext.items.findItem(note.uuid), 'new title')
await context.sync()
expect(context.items.findItem(note.uuid).signatureData).to.not.be.undefined
await context.changeNoteTitleAndSync(context.items.findItem(note.uuid), 'new title')
expect(context.items.findItem(note.uuid).signatureData).to.be.undefined
await deinitContactContext()
})
it('should allow client verification of authenticity of shared item changes', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
expect(context.contacts.isItemAuthenticallySigned(note)).to.equal('not-applicable')
const contactNote = contactContext.items.findItem(note.uuid)
expect(contactContext.contacts.isItemAuthenticallySigned(contactNote)).to.equal('yes')
await contactContext.changeNoteTitleAndSync(contactNote, 'new title')
await context.sync()
let updatedNote = context.items.findItem(note.uuid)
expect(context.contacts.isItemAuthenticallySigned(updatedNote)).to.equal('yes')
await deinitContactContext()
})
})
describe('keypair revocation', () => {
it('should be able to revoke non-current keypair', async () => {
console.error('TODO')
})
it('revoking a keypair should send a keypair revocation event to trusted contacts', async () => {
console.error('TODO')
})
it('should not be able to revoke current key pair', async () => {
console.error('TODO')
})
it('should distrust revoked keypair as contact', async () => {
console.error('TODO')
})
})
})

View File

@@ -0,0 +1,159 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault deletion', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let sharedVaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
sharedVaults = context.sharedVaults
})
it('should remove item from all user devices when item is deleted permanently', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const promise = context.resolveWhenSavedSyncPayloadsIncludesItemUuid(note.uuid)
await context.mutator.setItemToBeDeleted(note)
await context.sync()
await contactContext.sync()
await promise
const originatorNote = context.items.findItem(note.uuid)
expect(originatorNote).to.be.undefined
const collaboratorNote = contactContext.items.findItem(note.uuid)
expect(collaboratorNote).to.be.undefined
await deinitContactContext()
})
it('attempting to delete a note received by and already deleted by another person should not cause infinite conflicts', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const promise = context.resolveWhenSavedSyncPayloadsIncludesItemUuid(note.uuid)
await context.mutator.setItemToBeDeleted(note)
await contactContext.mutator.setItemToBeDeleted(note)
await context.sync()
await contactContext.sync()
await promise
const originatorNote = context.items.findItem(note.uuid)
expect(originatorNote).to.be.undefined
const collaboratorNote = contactContext.items.findItem(note.uuid)
expect(collaboratorNote).to.be.undefined
await deinitContactContext()
})
it('deleting a shared vault should remove all vault items from collaborator devices', async () => {
const { sharedVault, note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await sharedVaults.deleteSharedVault(sharedVault)
await contactContext.sync()
const originatorNote = context.items.findItem(note.uuid)
expect(originatorNote).to.be.undefined
const contactNote = contactContext.items.findItem(note.uuid)
expect(contactNote).to.be.undefined
await deinitContactContext()
})
it('being removed from shared vault should remove shared vault items locally', async () => {
const { sharedVault, note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const contactNote = contactContext.items.findItem(note.uuid)
expect(contactNote).to.not.be.undefined
await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
await contactContext.sync()
const updatedContactNote = contactContext.items.findItem(note.uuid)
expect(updatedContactNote).to.be.undefined
await deinitContactContext()
})
it('leaving a shared vault should remove its items locally', async () => {
const { sharedVault, note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context, SharedVaultPermission.Admin)
const originalNote = contactContext.items.findItem(note.uuid)
expect(originalNote).to.not.be.undefined
const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
await contactContext.sharedVaults.leaveSharedVault(contactVault)
const updatedContactNote = contactContext.items.findItem(note.uuid)
expect(updatedContactNote).to.be.undefined
const vault = await contactContext.vaults.getVault({ keySystemIdentifier: contactVault.systemIdentifier })
expect(vault).to.be.undefined
await deinitContactContext()
})
it('removing an item from a vault should remove it from collaborator devices', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await context.vaults.removeItemFromVault(note)
await context.changeNoteTitleAndSync(note, 'new title')
const receivedNote = contactContext.items.findItem(note.uuid)
expect(receivedNote).to.not.be.undefined
expect(receivedNote.title).to.not.equal('new title')
expect(receivedNote.title).to.equal(note.title)
await deinitContactContext()
})
it('should remove shared vault member', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const originalSharedVaultUsers = await sharedVaults.getSharedVaultUsers(sharedVault)
expect(originalSharedVaultUsers.length).to.equal(2)
const result = await sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
expect(isClientDisplayableError(result)).to.be.false
const updatedSharedVaultUsers = await sharedVaults.getSharedVaultUsers(sharedVault)
expect(updatedSharedVaultUsers.length).to.equal(1)
await deinitContactContext()
})
it('being removed from a shared vault should delete respective vault listing', async () => {
console.error('TODO: implement test')
})
})

View File

@@ -0,0 +1,259 @@
import * as Factory from '../lib/factory.js'
import * as Files from '../lib/Files.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault files', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let vaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
vaults = context.vaults
await context.publicMockSubscriptionPurchaseEvent()
})
describe('private vaults', () => {
it('should be able to upload and download file to vault as owner', async () => {
const vault = await Collaboration.createPrivateVault(context)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, vault)
const file = context.items.findItem(uploadedFile.uuid)
expect(file).to.not.be.undefined
expect(file.remoteIdentifier).to.equal(file.remoteIdentifier)
expect(file.key_system_identifier).to.equal(vault.systemIdentifier)
const downloadedBytes = await Files.downloadFile(context.files, file)
expect(downloadedBytes).to.eql(buffer)
})
})
it('should be able to upload and download file to vault as owner', async () => {
const sharedVault = await Collaboration.createSharedVault(context)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault)
const file = context.items.findItem(uploadedFile.uuid)
expect(file).to.not.be.undefined
expect(file.remoteIdentifier).to.equal(file.remoteIdentifier)
expect(file.key_system_identifier).to.equal(sharedVault.systemIdentifier)
const downloadedBytes = await Files.downloadFile(context.files, file)
expect(downloadedBytes).to.eql(buffer)
})
it('should be able to move a user file to a vault', async () => {
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000)
const sharedVault = await Collaboration.createSharedVault(context)
const addedFile = await vaults.moveItemToVault(sharedVault, uploadedFile)
const downloadedBytes = await Files.downloadFile(context.files, addedFile)
expect(downloadedBytes).to.eql(buffer)
})
it('should be able to move a shared vault file to another shared vault', async () => {
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const firstVault = await Collaboration.createSharedVault(context)
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, firstVault)
const secondVault = await Collaboration.createSharedVault(context)
const movedFile = await vaults.moveItemToVault(secondVault, uploadedFile)
const downloadedBytes = await Files.downloadFile(context.files, movedFile)
expect(downloadedBytes).to.eql(buffer)
})
it('should be able to move a shared vault file to a non-shared vault', async () => {
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const firstVault = await Collaboration.createSharedVault(context)
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, firstVault)
const privateVault = await Collaboration.createPrivateVault(context)
const addedFile = await vaults.moveItemToVault(privateVault, uploadedFile)
const downloadedBytes = await Files.downloadFile(context.files, addedFile)
expect(downloadedBytes).to.eql(buffer)
})
it('moving a note to a vault should also moved linked files', async () => {
const note = await context.createSyncedNote()
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const file = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000)
const updatedFile = await context.application.mutator.associateFileWithNote(file, note)
const sharedVault = await Collaboration.createSharedVault(context)
vaults.alerts.confirmV2 = () => Promise.resolve(true)
await vaults.moveItemToVault(sharedVault, note)
const latestFile = context.items.findItem(updatedFile.uuid)
expect(vaults.getItemVault(latestFile).uuid).to.equal(sharedVault.uuid)
expect(vaults.getItemVault(context.items.findItem(note.uuid)).uuid).to.equal(sharedVault.uuid)
const downloadedBytes = await Files.downloadFile(context.files, latestFile)
expect(downloadedBytes).to.eql(buffer)
})
it('should be able to move a file out of its vault', async () => {
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const sharedVault = await Collaboration.createSharedVault(context)
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault)
const removedFile = await vaults.removeItemFromVault(uploadedFile)
expect(removedFile.key_system_identifier).to.not.be.ok
const downloadedBytes = await Files.downloadFile(context.files, removedFile)
expect(downloadedBytes).to.eql(buffer)
})
it('should be able to download vault file as collaborator', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault)
await contactContext.sync()
const sharedFile = contactContext.items.findItem(uploadedFile.uuid)
expect(sharedFile).to.not.be.undefined
expect(sharedFile.remoteIdentifier).to.equal(uploadedFile.remoteIdentifier)
const downloadedBytes = await Files.downloadFile(contactContext.files, sharedFile)
expect(downloadedBytes).to.eql(buffer)
await deinitContactContext()
})
it('should be able to upload vault file as collaborator', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(contactContext.files, buffer, 'my-file', 'md', 1000, sharedVault)
await context.sync()
const file = context.items.findItem(uploadedFile.uuid)
expect(file).to.not.be.undefined
expect(file.remoteIdentifier).to.equal(file.remoteIdentifier)
const downloadedBytes = await Files.downloadFile(context.files, file)
expect(downloadedBytes).to.eql(buffer)
await deinitContactContext()
})
it('should be able to delete vault file as write user', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context, SharedVaultPermission.Write)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault)
await contactContext.sync()
const file = contactContext.items.findItem(uploadedFile.uuid)
const result = await contactContext.files.deleteFile(file)
expect(result).to.be.undefined
const foundFile = contactContext.items.findItem(file.uuid)
expect(foundFile).to.be.undefined
await deinitContactContext()
})
it('should not be able to delete vault file as read user', async () => {
context.anticipateConsoleError('Could not create valet token')
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context, SharedVaultPermission.Read)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault)
await contactContext.sync()
const file = contactContext.items.findItem(uploadedFile.uuid)
const result = await contactContext.files.deleteFile(file)
expect(isClientDisplayableError(result)).to.be.true
const foundFile = contactContext.items.findItem(file.uuid)
expect(foundFile).to.not.be.undefined
await deinitContactContext()
})
it('should be able to download recently moved vault file as collaborator', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000)
const addedFile = await vaults.moveItemToVault(sharedVault, uploadedFile)
await contactContext.sync()
const sharedFile = contactContext.items.findItem(addedFile.uuid)
expect(sharedFile).to.not.be.undefined
expect(sharedFile.remoteIdentifier).to.equal(addedFile.remoteIdentifier)
const downloadedBytes = await Files.downloadFile(contactContext.files, sharedFile)
expect(downloadedBytes).to.eql(buffer)
await deinitContactContext()
})
it('should not be able to download file after being removed from vault', async () => {
context.anticipateConsoleError('Could not create valet token')
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault)
await contactContext.sync()
await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
const file = contactContext.items.findItem(uploadedFile.uuid)
await Factory.expectThrowsAsync(() => Files.downloadFile(contactContext.files, file), 'Could not download file')
await deinitContactContext()
})
})

View File

@@ -0,0 +1,229 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault invites', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let sharedVaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
sharedVaults = context.sharedVaults
})
it('should invite contact to vault', async () => {
const sharedVault = await Collaboration.createSharedVault(context)
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
const contact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
const vaultInvite = await sharedVaults.inviteContactToSharedVault(sharedVault, contact, SharedVaultPermission.Write)
expect(vaultInvite).to.not.be.undefined
expect(vaultInvite.shared_vault_uuid).to.equal(sharedVault.sharing.sharedVaultUuid)
expect(vaultInvite.user_uuid).to.equal(contact.contactUuid)
expect(vaultInvite.encrypted_message).to.not.be.undefined
expect(vaultInvite.permissions).to.equal(SharedVaultPermission.Write)
expect(vaultInvite.updated_at_timestamp).to.not.be.undefined
expect(vaultInvite.created_at_timestamp).to.not.be.undefined
await deinitContactContext()
})
it('invites from trusted contact should be pending as trusted', async () => {
const { contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context)
const invites = contactContext.sharedVaults.getCachedPendingInviteRecords()
expect(invites[0].trusted).to.be.true
await deinitContactContext()
})
it('invites from untrusted contact should be pending as untrusted', async () => {
const { contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithUnacceptedAndUntrustedInvite(context)
const invites = contactContext.sharedVaults.getCachedPendingInviteRecords()
expect(invites[0].trusted).to.be.false
await deinitContactContext()
})
it('invite should include delegated trusted contacts', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
context,
sharedVault,
)
const invites = thirdPartyContext.sharedVaults.getCachedPendingInviteRecords()
const message = invites[0].message
const delegatedContacts = message.data.trustedContacts
expect(delegatedContacts.length).to.equal(1)
expect(delegatedContacts[0].contactUuid).to.equal(contactContext.userUuid)
await deinitThirdPartyContext()
await deinitContactContext()
})
it('should sync a shared vault from scratch after accepting an invitation', async () => {
const sharedVault = await Collaboration.createSharedVault(context)
const note = await context.createSyncedNote('foo', 'bar')
await Collaboration.moveItemToVault(context, sharedVault, note)
/** Create a mutually trusted contact */
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
const contact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
await Collaboration.createTrustedContactForUserOfContext(contactContext, context)
/** Sync the contact context so that they wouldn't naturally receive changes made before this point */
await contactContext.sync()
await sharedVaults.inviteContactToSharedVault(sharedVault, contact, SharedVaultPermission.Write)
/** Contact should now sync and expect to find note */
const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent()
await contactContext.sync()
await Collaboration.acceptAllInvites(contactContext)
await promise
const receivedNote = contactContext.items.findItem(note.uuid)
expect(receivedNote).to.not.be.undefined
expect(receivedNote.title).to.equal('foo')
expect(receivedNote.text).to.equal(note.text)
await deinitContactContext()
})
it('received invites from untrusted contact should not be trusted', async () => {
await context.createSyncedNote('foo', 'bar')
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
const sharedVault = await Collaboration.createSharedVault(context)
const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write)
await contactContext.sharedVaults.downloadInboundInvites()
expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.false
await deinitContactContext()
})
it('received invites from contact who becomes trusted after receipt of invite should be trusted', async () => {
await context.createSyncedNote('foo', 'bar')
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
const sharedVault = await Collaboration.createSharedVault(context)
const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write)
await contactContext.sharedVaults.downloadInboundInvites()
expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.false
await Collaboration.createTrustedContactForUserOfContext(contactContext, context)
expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.true
await deinitContactContext()
})
it('received items should contain the uuid of the contact who sent the item', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const receivedNote = contactContext.items.findItem(note.uuid)
expect(receivedNote).to.not.be.undefined
expect(receivedNote.user_uuid).to.equal(context.userUuid)
await deinitContactContext()
})
it('items should contain the uuid of the last person who edited it', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const receivedNote = contactContext.items.findItem(note.uuid)
expect(receivedNote.last_edited_by_uuid).to.not.be.undefined
expect(receivedNote.last_edited_by_uuid).to.equal(context.userUuid)
await contactContext.changeNoteTitleAndSync(receivedNote, 'new title')
await context.sync()
const updatedNote = context.items.findItem(note.uuid)
expect(updatedNote.last_edited_by_uuid).to.not.be.undefined
expect(updatedNote.last_edited_by_uuid).to.equal(contactContext.userUuid)
await deinitContactContext()
})
it('canceling an invite should remove it from recipient pending invites', async () => {
const { invite, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context)
const preInvites = await contactContext.sharedVaults.downloadInboundInvites()
expect(preInvites.length).to.equal(1)
await sharedVaults.deleteInvite(invite)
const postInvites = await contactContext.sharedVaults.downloadInboundInvites()
expect(postInvites.length).to.equal(0)
await deinitContactContext()
})
it('when inviter keypair changes, recipient should still be able to trust and decrypt previous invite', async () => {
console.error('TODO: implement test')
})
it('should delete all inbound invites after changing user password', async () => {
/** Invites to user are encrypted with old keypair and are no longer decryptable */
console.error('TODO: implement test')
})
it('sharing a vault with user inputted and ephemeral password should share the key as synced for the recipient', async () => {
const privateVault = await context.vaults.createUserInputtedPasswordVault({
name: 'My Private Vault',
userInputtedPassword: 'password',
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
})
const note = await context.createSyncedNote('foo', 'bar')
await context.vaults.moveItemToVault(privateVault, note)
const sharedVault = await context.sharedVaults.convertVaultToSharedVault(privateVault)
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
context,
sharedVault,
)
await Collaboration.acceptAllInvites(thirdPartyContext)
const contextNote = thirdPartyContext.items.findItem(note.uuid)
expect(contextNote).to.not.be.undefined
expect(contextNote.title).to.equal('foo')
expect(contextNote.text).to.equal(note.text)
await deinitThirdPartyContext()
})
})

View File

@@ -0,0 +1,121 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault items', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let sharedVaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
sharedVaults = context.sharedVaults
})
it('should add item to shared vault with no other members', async () => {
const note = await context.createSyncedNote('foo', 'bar')
const sharedVault = await Collaboration.createSharedVault(context)
await Collaboration.moveItemToVault(context, sharedVault, note)
const updatedNote = context.items.findItem(note.uuid)
expect(updatedNote.key_system_identifier).to.equal(sharedVault.systemIdentifier)
expect(updatedNote.shared_vault_uuid).to.equal(sharedVault.sharing.sharedVaultUuid)
})
it('should add item to shared vault with contact', async () => {
const note = await context.createSyncedNote('foo', 'bar')
const { sharedVault, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
await Collaboration.moveItemToVault(context, sharedVault, note)
const updatedNote = context.items.findItem(note.uuid)
expect(updatedNote.key_system_identifier).to.equal(sharedVault.systemIdentifier)
await deinitContactContext()
})
it('received items from previously trusted contact should be decrypted', async () => {
const note = await context.createSyncedNote('foo', 'bar')
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
const sharedVault = await Collaboration.createSharedVault(context)
await Collaboration.createTrustedContactForUserOfContext(contactContext, context)
const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
contactContext.lockSyncing()
await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write)
await Collaboration.moveItemToVault(context, sharedVault, note)
const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent()
contactContext.unlockSyncing()
await contactContext.sync()
await Collaboration.acceptAllInvites(contactContext)
await promise
const receivedItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier)
expect(receivedItemsKey).to.not.be.undefined
expect(receivedItemsKey.itemsKey).to.not.be.undefined
const receivedNote = contactContext.items.findItem(note.uuid)
expect(receivedNote.title).to.equal('foo')
expect(receivedNote.text).to.equal(note.text)
await deinitContactContext()
})
it('shared vault creator should receive changes from other members', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const note = await context.createSyncedNote('foo', 'bar')
await Collaboration.moveItemToVault(context, sharedVault, note)
await contactContext.sync()
await contactContext.mutator.changeItem({ uuid: note.uuid }, (mutator) => {
mutator.title = 'new title'
})
await contactContext.sync()
await context.sync()
const receivedNote = context.items.findItem(note.uuid)
expect(receivedNote.title).to.equal('new title')
await deinitContactContext()
})
it('items added by collaborator should be received by shared vault owner', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const newNote = await contactContext.createSyncedNote('new note', 'new note text')
await Collaboration.moveItemToVault(contactContext, sharedVault, newNote)
await context.sync()
const receivedNote = context.items.findItem(newNote.uuid)
expect(receivedNote).to.not.be.undefined
expect(receivedNote.title).to.equal('new note')
await deinitContactContext()
})
it('adding item to vault while belonging to other vault should move the item to new vault', async () => {
console.error('TODO: implement test')
})
})

View File

@@ -0,0 +1,269 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault key rotation', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let vaults
let sharedVaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
vaults = context.vaults
sharedVaults = context.sharedVaults
})
it('should reencrypt all items keys belonging to key system', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.lockSyncing()
const spy = sinon.spy(context.encryption, 'reencryptKeySystemItemsKeysForVault')
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await promise
expect(spy.callCount).to.equal(1)
deinitContactContext()
})
it("rotating a vault's key should send an asymmetric message to all members", async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await promise
const outboundMessages = await context.asymmetric.getOutboundMessages()
const expectedMessages = ['root key change', 'vault metadata change']
expect(outboundMessages.length).to.equal(expectedMessages.length)
const message = outboundMessages[0]
expect(message).to.not.be.undefined
expect(message.user_uuid).to.equal(contactContext.userUuid)
expect(message.encrypted_message).to.not.be.undefined
await deinitContactContext()
})
it('should update recipient vault display listing with new key params', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.anticipateConsoleError(
'(2x) Error decrypting contentKey from parameters',
'Items keys are encrypted with new root key and are later decrypted in the test',
)
contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await promise
const rootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
contactContext.unlockSyncing()
await contactContext.sync()
const vault = await contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(vault.rootKeyParams).to.eql(rootKey.keyParams)
await deinitContactContext()
})
it('should receive new key system items key', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.anticipateConsoleError(
'(2x) Error decrypting contentKey from parameters',
'Items keys are encrypted with new root key and are later decrypted in the test',
)
contactContext.lockSyncing()
const previousPrimaryItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier)
expect(previousPrimaryItemsKey).to.not.be.undefined
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await promise
const contactPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
contactContext.unlockSyncing()
await contactContext.sync()
await contactPromise
const newPrimaryItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier)
expect(newPrimaryItemsKey).to.not.be.undefined
expect(newPrimaryItemsKey.uuid).to.not.equal(previousPrimaryItemsKey.uuid)
expect(newPrimaryItemsKey.itemsKey).to.not.eql(previousPrimaryItemsKey.itemsKey)
await deinitContactContext()
})
it("rotating a vault's key with a pending invite should create new invite and delete old", async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context)
contactContext.lockSyncing()
const originalOutboundInvites = await sharedVaults.getOutboundInvites()
expect(originalOutboundInvites.length).to.equal(1)
const originalInviteMessage = originalOutboundInvites[0].encrypted_message
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await promise
const updatedOutboundInvites = await sharedVaults.getOutboundInvites()
expect(updatedOutboundInvites.length).to.equal(1)
const joinInvite = updatedOutboundInvites[0]
expect(joinInvite.encrypted_message).to.not.be.undefined
expect(joinInvite.encrypted_message).to.not.equal(originalInviteMessage)
await deinitContactContext()
})
it('new key system items key in rotated shared vault should belong to shared vault', async () => {
const sharedVault = await Collaboration.createSharedVault(context)
await vaults.rotateVaultRootKey(sharedVault)
const keySystemItemsKeys = context.keys
.getAllKeySystemItemsKeys()
.filter((key) => key.key_system_identifier === sharedVault.systemIdentifier)
expect(keySystemItemsKeys.length).to.equal(2)
for (const key of keySystemItemsKeys) {
expect(key.shared_vault_uuid).to.equal(sharedVault.sharing.sharedVaultUuid)
}
})
it('should update existing key-change messages instead of creating new ones', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.lockSyncing()
const firstPromise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await firstPromise
const asymmetricMessageAfterFirstChange = await context.asymmetric.getOutboundMessages()
const expectedMessages = ['root key change', 'vault metadata change']
expect(asymmetricMessageAfterFirstChange.length).to.equal(expectedMessages.length)
const messageAfterFirstChange = asymmetricMessageAfterFirstChange[0]
const secondPromise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await secondPromise
const asymmetricMessageAfterSecondChange = await context.asymmetric.getOutboundMessages()
expect(asymmetricMessageAfterSecondChange.length).to.equal(expectedMessages.length)
const messageAfterSecondChange = asymmetricMessageAfterSecondChange[0]
expect(messageAfterSecondChange.encrypted_message).to.not.equal(messageAfterFirstChange.encrypted_message)
expect(messageAfterSecondChange.uuid).to.not.equal(messageAfterFirstChange.uuid)
await deinitContactContext()
})
it('key change messages should be automatically processed by trusted contacts', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.anticipateConsoleError(
'(2x) Error decrypting contentKey from parameters',
'Items keys are encrypted with new root key and are later decrypted in the test',
)
contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await promise
const acceptMessage = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
contactContext.unlockSyncing()
await contactContext.sync()
expect(acceptMessage.callCount).to.equal(1)
await deinitContactContext()
})
it('should rotate key system root key after removing vault member', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const originalKeySystemRootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
await sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
const newKeySystemRootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
expect(newKeySystemRootKey.keyParams.creationTimestamp).to.be.greaterThan(
originalKeySystemRootKey.keyParams.creationTimestamp,
)
expect(newKeySystemRootKey.key).to.not.equal(originalKeySystemRootKey.key)
await deinitContactContext()
})
it('should throw if attempting to change password of locked vault', async () => {
console.error('TODO: implement')
})
it('should respect storage preference when rotating key system root key', async () => {
console.error('TODO: implement')
})
it('should change storage preference from synced to local', async () => {
console.error('TODO: implement')
})
it('should change storage preference from local to synced', async () => {
console.error('TODO: implement')
})
it('should resync key system items key if it is encrypted with noncurrent key system root key', async () => {
console.error('TODO: implement')
})
it('should change password type from user inputted to randomized', async () => {
console.error('TODO: implement')
})
it('should change password type from randomized to user inputted', async () => {
console.error('TODO: implement')
})
it('should not be able to change storage mode of third party vault', async () => {
console.error('TODO: implement')
})
})

View File

@@ -0,0 +1,133 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault permissions', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let sharedVaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
sharedVaults = context.sharedVaults
})
it('non-admin user should not be able to invite user', async () => {
context.anticipateConsoleError('Could not create invite')
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const thirdParty = await Collaboration.createContactContext()
const thirdPartyContact = await Collaboration.createTrustedContactForUserOfContext(
contactContext,
thirdParty.contactContext,
)
const result = await contactContext.sharedVaults.inviteContactToSharedVault(
sharedVault,
thirdPartyContact,
SharedVaultPermission.Write,
)
expect(isClientDisplayableError(result)).to.be.true
await deinitContactContext()
})
it('should not be able to leave shared vault as creator', async () => {
context.anticipateConsoleError('Could not delete user')
const sharedVault = await Collaboration.createSharedVault(context)
const result = await sharedVaults.removeUserFromSharedVault(sharedVault, context.userUuid)
expect(isClientDisplayableError(result)).to.be.true
})
it('should be able to leave shared vault as added admin', async () => {
const { contactVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context, SharedVaultPermission.Admin)
const result = await contactContext.sharedVaults.leaveSharedVault(contactVault)
expect(isClientDisplayableError(result)).to.be.false
await deinitContactContext()
})
it('non-admin user should not be able to create or update vault items keys with the server', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const keySystemItemsKey = contactContext.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)[0]
await contactContext.mutator.changeItem(keySystemItemsKey, () => {})
const promise = contactContext.resolveWithConflicts()
await contactContext.sync()
const conflicts = await promise
expect(conflicts.length).to.equal(1)
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.KeySystemItemsKey)
await deinitContactContext()
})
it('read user should not be able to make changes to items', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context, SharedVaultPermission.Read)
const note = await context.createSyncedNote('foo', 'bar')
await Collaboration.moveItemToVault(context, sharedVault, note)
await contactContext.sync()
await contactContext.mutator.changeItem({ uuid: note.uuid }, (mutator) => {
mutator.title = 'new title'
})
const promise = contactContext.resolveWithConflicts()
await contactContext.sync()
const conflicts = await promise
expect(conflicts.length).to.equal(1)
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
await deinitContactContext()
})
it('should be able to move item from vault to user as a write user if the item belongs to me', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const note = await contactContext.createSyncedNote('foo', 'bar')
await Collaboration.moveItemToVault(contactContext, sharedVault, note)
await contactContext.sync()
const promise = contactContext.resolveWithConflicts()
await contactContext.vaults.removeItemFromVault(note)
const conflicts = await promise
expect(conflicts.length).to.equal(0)
const duplicateNote = contactContext.findDuplicateNote(note.uuid)
expect(duplicateNote).to.be.undefined
const existingNote = contactContext.items.findItem(note.uuid)
expect(existingNote.key_system_identifier).to.not.be.ok
await deinitContactContext()
})
})

View File

@@ -0,0 +1,98 @@
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('public key cryptography', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let sessions
let encryption
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
sessions = context.application.sessions
encryption = context.encryption
})
it('should create keypair during registration', () => {
expect(sessions.getPublicKey()).to.not.be.undefined
expect(encryption.getKeyPair().privateKey).to.not.be.undefined
expect(sessions.getSigningPublicKey()).to.not.be.undefined
expect(encryption.getSigningKeyPair().privateKey).to.not.be.undefined
})
it('should populate keypair during sign in', async () => {
const email = context.email
const password = context.password
await context.signout()
const recreatedContext = await Factory.createAppContextWithRealCrypto()
await recreatedContext.launch()
recreatedContext.email = email
recreatedContext.password = password
await recreatedContext.signIn()
expect(recreatedContext.sessions.getPublicKey()).to.not.be.undefined
expect(recreatedContext.encryption.getKeyPair().privateKey).to.not.be.undefined
expect(recreatedContext.sessions.getSigningPublicKey()).to.not.be.undefined
expect(recreatedContext.encryption.getSigningKeyPair().privateKey).to.not.be.undefined
})
it('should rotate keypair during password change', async () => {
const oldPublicKey = sessions.getPublicKey()
const oldPrivateKey = encryption.getKeyPair().privateKey
const oldSigningPublicKey = sessions.getSigningPublicKey()
const oldSigningPrivateKey = encryption.getSigningKeyPair().privateKey
await context.changePassword('new_password')
expect(sessions.getPublicKey()).to.not.be.undefined
expect(encryption.getKeyPair().privateKey).to.not.be.undefined
expect(sessions.getPublicKey()).to.not.equal(oldPublicKey)
expect(encryption.getKeyPair().privateKey).to.not.equal(oldPrivateKey)
expect(sessions.getSigningPublicKey()).to.not.be.undefined
expect(encryption.getSigningKeyPair().privateKey).to.not.be.undefined
expect(sessions.getSigningPublicKey()).to.not.equal(oldSigningPublicKey)
expect(encryption.getSigningKeyPair().privateKey).to.not.equal(oldSigningPrivateKey)
})
it('should allow option to enable collaboration for previously signed in accounts', async () => {
const newContext = await Factory.createAppContextWithRealCrypto()
await newContext.launch()
await newContext.register()
const rootKey = await newContext.encryption.getRootKey()
const mutatedRootKey = CreateNewRootKey({
...rootKey.content,
encryptionKeyPair: undefined,
signingKeyPair: undefined,
})
await newContext.encryption.setRootKey(mutatedRootKey)
expect(newContext.application.sessions.isUserMissingKeyPair()).to.be.true
const result = await newContext.application.user.updateAccountWithFirstTimeKeyPair()
expect(result.error).to.be.undefined
expect(newContext.application.sessions.isUserMissingKeyPair()).to.be.false
})
})

View File

@@ -0,0 +1,114 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vaults', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let vaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
vaults = context.vaults
})
it('should update vault name and description', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
await vaults.changeVaultNameAndDescription(sharedVault, {
name: 'new vault name',
description: 'new vault description',
})
const updatedVault = vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(updatedVault.name).to.equal('new vault name')
expect(updatedVault.description).to.equal('new vault description')
const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await promise
const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(contactVault.name).to.equal('new vault name')
expect(contactVault.description).to.equal('new vault description')
await deinitContactContext()
})
it('being removed from a shared vault should remove the vault', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const result = await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
expect(result).to.be.undefined
const promise = contactContext.resolveWhenUserMessagesProcessingCompletes()
await contactContext.sync()
await promise
expect(contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined
expect(contactContext.encryption.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined
expect(contactContext.encryption.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty
const recreatedContext = await Factory.createAppContextWithRealCrypto(contactContext.identifier)
await recreatedContext.launch()
expect(recreatedContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined
expect(recreatedContext.encryption.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined
expect(recreatedContext.encryption.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty
await deinitContactContext()
await recreatedContext.deinit()
})
it('deleting a shared vault should remove vault from contact context', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const result = await context.sharedVaults.deleteSharedVault(sharedVault)
expect(result).to.be.undefined
const promise = contactContext.resolveWhenUserMessagesProcessingCompletes()
await contactContext.sync()
await promise
expect(contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined
expect(contactContext.encryption.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined
expect(contactContext.encryption.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty
const recreatedContext = await Factory.createAppContextWithRealCrypto(contactContext.identifier)
await recreatedContext.launch()
expect(recreatedContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined
expect(recreatedContext.encryption.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined
expect(recreatedContext.encryption.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty
await deinitContactContext()
await recreatedContext.deinit()
})
it('should convert a vault to a shared vault', async () => {
console.error('TODO')
})
it('should send metadata change message when changing name or description', async () => {
console.error('TODO')
})
})

View File

@@ -0,0 +1,250 @@
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('vaults', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let vaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
vaults = context.vaults
})
describe('locking', () => {
it('should throw if attempting to add item to locked vault', async () => {
console.error('TODO: implement')
})
it('should throw if attempting to remove item from locked vault', async () => {
console.error('TODO: implement')
})
it('locking vault should remove root key and items keys from memory', async () => {
console.error('TODO: implement')
})
})
describe('offline', function () {
it('should be able to create an offline vault', async () => {
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
expect(vault.systemIdentifier).to.not.be.undefined
expect(typeof vault.systemIdentifier).to.equal('string')
const keySystemItemsKey = context.keys.getPrimaryKeySystemItemsKey(vault.systemIdentifier)
expect(keySystemItemsKey).to.not.be.undefined
expect(keySystemItemsKey.key_system_identifier).to.equal(vault.systemIdentifier)
expect(keySystemItemsKey.creationTimestamp).to.not.be.undefined
expect(keySystemItemsKey.keyVersion).to.not.be.undefined
})
it('should be able to create an offline vault with app passcode', async () => {
await context.application.addPasscode('123')
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
expect(vault.systemIdentifier).to.not.be.undefined
expect(typeof vault.systemIdentifier).to.equal('string')
const keySystemItemsKey = context.keys.getPrimaryKeySystemItemsKey(vault.systemIdentifier)
expect(keySystemItemsKey).to.not.be.undefined
expect(keySystemItemsKey.key_system_identifier).to.equal(vault.systemIdentifier)
expect(keySystemItemsKey.creationTimestamp).to.not.be.undefined
expect(keySystemItemsKey.keyVersion).to.not.be.undefined
})
it('should add item to offline vault', async () => {
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
const item = await context.createSyncedNote()
await vaults.moveItemToVault(vault, item)
const updatedItem = context.items.findItem(item.uuid)
expect(updatedItem.key_system_identifier).to.equal(vault.systemIdentifier)
})
it('should load data in the correct order at startup to allow vault items and their keys to decrypt', async () => {
const appIdentifier = context.identifier
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
const note = await context.createSyncedNote('foo', 'bar')
await vaults.moveItemToVault(vault, note)
await context.deinit()
const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
const updatedNote = recreatedContext.items.findItem(note.uuid)
expect(updatedNote.title).to.equal('foo')
expect(updatedNote.text).to.equal('bar')
await recreatedContext.deinit()
})
describe('porting from offline to online', () => {
it('should maintain vault system identifiers across items after registration', async () => {
const appIdentifier = context.identifier
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
const note = await context.createSyncedNote('foo', 'bar')
await vaults.moveItemToVault(vault, note)
await context.register()
await context.sync()
await context.deinit()
const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
const notes = recreatedContext.notes
expect(notes.length).to.equal(1)
const updatedNote = recreatedContext.items.findItem(note.uuid)
expect(updatedNote.title).to.equal('foo')
expect(updatedNote.text).to.equal('bar')
expect(updatedNote.key_system_identifier).to.equal(vault.systemIdentifier)
await recreatedContext.deinit()
})
it('should decrypt vault items', async () => {
const appIdentifier = context.identifier
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
const note = await context.createSyncedNote('foo', 'bar')
await vaults.moveItemToVault(vault, note)
await context.register()
await context.sync()
await context.deinit()
const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
const updatedNote = recreatedContext.items.findItem(note.uuid)
expect(updatedNote.title).to.equal('foo')
expect(updatedNote.text).to.equal('bar')
await recreatedContext.deinit()
})
})
})
describe('online', () => {
beforeEach(async () => {
await context.register()
})
it('should create a vault', async () => {
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
expect(vault).to.not.be.undefined
const keySystemItemsKeys = context.keys.getKeySystemItemsKeys(vault.systemIdentifier)
expect(keySystemItemsKeys.length).to.equal(1)
const keySystemItemsKey = keySystemItemsKeys[0]
expect(keySystemItemsKey instanceof KeySystemItemsKey).to.be.true
expect(keySystemItemsKey.key_system_identifier).to.equal(vault.systemIdentifier)
})
it('should add item to vault', async () => {
const note = await context.createSyncedNote('foo', 'bar')
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
await vaults.moveItemToVault(vault, note)
const updatedNote = context.items.findItem(note.uuid)
expect(updatedNote.key_system_identifier).to.equal(vault.systemIdentifier)
})
describe('client timing', () => {
it('should load data in the correct order at startup to allow vault items and their keys to decrypt', async () => {
const appIdentifier = context.identifier
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
const note = await context.createSyncedNote('foo', 'bar')
await vaults.moveItemToVault(vault, note)
await context.deinit()
const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
const updatedNote = recreatedContext.items.findItem(note.uuid)
expect(updatedNote.title).to.equal('foo')
expect(updatedNote.text).to.equal('bar')
await recreatedContext.deinit()
})
})
describe('key system root key rotation', () => {
it('rotating a key system root key should create a new vault items key', async () => {
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
const keySystemItemsKey = context.keys.getKeySystemItemsKeys(vault.systemIdentifier)[0]
await vaults.rotateVaultRootKey(vault)
const updatedKeySystemItemsKey = context.keys.getKeySystemItemsKeys(vault.systemIdentifier)[0]
expect(updatedKeySystemItemsKey).to.not.be.undefined
expect(updatedKeySystemItemsKey.uuid).to.not.equal(keySystemItemsKey.uuid)
})
it('deleting a vault should delete all its items', async () => {
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
const note = await context.createSyncedNote('foo', 'bar')
await vaults.moveItemToVault(vault, note)
await vaults.deleteVault(vault)
const updatedNote = context.items.findItem(note.uuid)
expect(updatedNote).to.be.undefined
})
})
})
})