feat: add snjs package

This commit is contained in:
Karol Sójko
2022-07-06 14:04:18 +02:00
parent 321a055bae
commit 0e40469e2f
296 changed files with 46109 additions and 187 deletions

View File

@@ -0,0 +1,374 @@
/* eslint-disable camelcase */
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('app models', () => {
const BASE_ITEM_COUNT = 2 /** Default items key, user preferences */
const sharedApplication = Factory.createApplicationWithFakeCrypto()
before(async function () {
localStorage.clear()
await Factory.initializeApplication(sharedApplication)
})
after(async function () {
localStorage.clear()
await Factory.safeDeinit(sharedApplication)
})
beforeEach(async function () {
this.expectedItemCount = BASE_ITEM_COUNT
this.application = await Factory.createInitAppWithFakeCrypto()
})
afterEach(async function () {
await Factory.safeDeinit(this.application)
})
it('payloadManager should be defined', () => {
expect(sharedApplication.payloadManager).to.be.ok
})
it('item should be defined', () => {
expect(GenericItem).to.be.ok
})
it('item content should be assigned', () => {
const params = Factory.createNotePayload()
const item = CreateDecryptedItemFromPayload(params)
expect(item.content.title).to.equal(params.content.title)
})
it('should default updated_at to 1970 and created_at to the present', () => {
const params = Factory.createNotePayload()
const item = CreateDecryptedItemFromPayload(params)
const epoch = new Date(0)
expect(item.serverUpdatedAt - epoch).to.equal(0)
expect(item.created_at - epoch).to.be.above(0)
expect(new Date() - item.created_at).to.be.below(5) // < 5ms
})
it('handles delayed mapping', async function () {
const params1 = Factory.createNotePayload()
const params2 = Factory.createNotePayload()
const mutated = new DecryptedPayload({
...params1,
content: {
...params1.content,
references: [
{
uuid: params2.uuid,
content_type: params2.content_type,
},
],
},
})
await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
await this.application.itemManager.emitItemsFromPayloads([params2], PayloadEmitSource.LocalChanged)
const item1 = this.application.itemManager.findItem(params1.uuid)
const item2 = this.application.itemManager.findItem(params2.uuid)
expect(item1.content.references.length).to.equal(1)
expect(item2.content.references.length).to.equal(0)
expect(this.application.itemManager.itemsReferencingItem(item1).length).to.equal(0)
expect(this.application.itemManager.itemsReferencingItem(item2).length).to.equal(1)
})
it('mapping an item twice shouldnt cause problems', async function () {
const payload = Factory.createNotePayload()
const mutated = new DecryptedPayload({
...payload,
content: {
...payload.content,
foo: 'bar',
},
})
let items = await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
let item = items[0]
expect(item).to.be.ok
items = await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
item = items[0]
expect(item.content.foo).to.equal('bar')
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(1)
})
it('mapping item twice should preserve references', async function () {
const item1 = await Factory.createMappedNote(this.application)
const item2 = await Factory.createMappedNote(this.application)
await this.application.itemManager.changeItem(item1, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
})
await this.application.itemManager.changeItem(item2, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
})
const refreshedItem = this.application.itemManager.findItem(item1.uuid)
expect(refreshedItem.content.references.length).to.equal(1)
})
it('fixes relationship integrity', async function () {
var item1 = await Factory.createMappedNote(this.application)
var item2 = await Factory.createMappedNote(this.application)
await this.application.itemManager.changeItem(item1, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
})
await this.application.itemManager.changeItem(item2, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
})
const refreshedItem1 = this.application.itemManager.findItem(item1.uuid)
const refreshedItem2 = this.application.itemManager.findItem(item2.uuid)
expect(refreshedItem1.content.references.length).to.equal(1)
expect(refreshedItem2.content.references.length).to.equal(1)
const damagedPayload = refreshedItem1.payload.copy({
content: {
...refreshedItem1.content,
// damage references of one object
references: [],
},
})
await this.application.itemManager.emitItemsFromPayloads([damagedPayload], PayloadEmitSource.LocalChanged)
const refreshedItem1_2 = this.application.itemManager.findItem(item1.uuid)
const refreshedItem2_2 = this.application.itemManager.findItem(item2.uuid)
expect(refreshedItem1_2.content.references.length).to.equal(0)
expect(refreshedItem2_2.content.references.length).to.equal(1)
})
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) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
})
await this.application.itemManager.changeItem(item2, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
})
const refreshedItem1 = this.application.itemManager.findItem(item1.uuid)
const refreshedItem2 = this.application.itemManager.findItem(item2.uuid)
expect(refreshedItem1.content.references.length).to.equal(1)
expect(refreshedItem2.content.references.length).to.equal(1)
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) => {
mutator.removeItemAsRelationship(item2)
})
await this.application.itemManager.changeItem(item2, (mutator) => {
mutator.removeItemAsRelationship(item1)
})
const refreshedItem1_2 = this.application.itemManager.findItem(item1.uuid)
const refreshedItem2_2 = this.application.itemManager.findItem(item2.uuid)
expect(refreshedItem1_2.content.references.length).to.equal(0)
expect(refreshedItem2_2.content.references.length).to.equal(0)
expect(this.application.itemManager.itemsReferencingItem(item1).length).to.equal(0)
expect(this.application.itemManager.itemsReferencingItem(item2).length).to.equal(0)
})
it('properly duplicates item with no relationships', async function () {
const item = await Factory.createMappedNote(this.application)
const duplicate = await this.application.itemManager.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())
expect(item.content_type).to.equal(duplicate.content_type)
})
it('properly duplicates item with relationships', 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) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
})
expect(refreshedItem1.content.references.length).to.equal(1)
const duplicate = await this.application.itemManager.duplicateItem(item1)
expect(duplicate.uuid).to.not.equal(item1.uuid)
expect(duplicate.content.references.length).to.equal(1)
expect(this.application.itemManager.itemsReferencingItem(item1).length).to.equal(0)
expect(this.application.itemManager.itemsReferencingItem(item2).length).to.equal(2)
const refreshedItem1_2 = this.application.itemManager.findItem(item1.uuid)
expect(refreshedItem1_2.isItemContentEqualWith(duplicate)).to.equal(true)
expect(refreshedItem1_2.created_at.toISOString()).to.equal(duplicate.created_at.toISOString())
expect(refreshedItem1_2.content_type).to.equal(duplicate.content_type)
})
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) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
})
const refreshedItem1_2 = await this.application.itemManager.emitItemFromPayload(
refreshedItem1.payloadRepresentation({
deleted: true,
content: {
...refreshedItem1.payload.content,
references: [],
},
}),
PayloadEmitSource.LocalChanged,
)
expect(this.application.itemManager.itemsReferencingItem(item2).length).to.equal(0)
expect(this.application.itemManager.itemsReferencingItem(item1).length).to.equal(0)
expect(refreshedItem1_2.content.references.length).to.equal(0)
})
it('properly handles single item uuid alternation', 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) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
})
expect(refreshedItem1.content.references.length).to.equal(1)
expect(this.application.itemManager.itemsReferencingItem(item2).length).to.equal(1)
const alternatedItem = await Factory.alternateUuidForItem(this.application, item1.uuid)
const refreshedItem1_2 = this.application.itemManager.findItem(item1.uuid)
expect(refreshedItem1_2).to.not.be.ok
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(2)
expect(alternatedItem.content.references.length).to.equal(1)
expect(this.application.itemManager.itemsReferencingItem(alternatedItem.uuid).length).to.equal(0)
expect(this.application.itemManager.itemsReferencingItem(item2).length).to.equal(1)
expect(alternatedItem.isReferencingItem(item2)).to.equal(true)
expect(alternatedItem.dirty).to.equal(true)
})
it('alterating uuid of item should fill its duplicateOf value', async function () {
const item1 = await Factory.createMappedNote(this.application)
const alternatedItem = await Factory.alternateUuidForItem(this.application, item1.uuid)
expect(alternatedItem.duplicateOf).to.equal(item1.uuid)
})
it('alterating itemskey uuid should update errored items encrypted with that key', async function () {
const item1 = await Factory.createMappedNote(this.application)
const itemsKey = this.application.itemManager.getDisplayableItemsKeys()[0]
/** Encrypt item1 and emit as errored so it persists with items_key_id */
const encrypted = await this.application.protocolService.encryptSplitSingle({
usesItemsKeyWithKeyLookup: {
items: [item1.payload],
},
})
const errored = encrypted.copy({
errorDecrypting: true,
waitingForKey: true,
})
await this.application.itemManager.emitItemFromPayload(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(() => {
// prevent auto decryption
})
const alternatedKey = await Factory.alternateUuidForItem(this.application, itemsKey.uuid)
const updatedPayload = this.application.payloadManager.findOne(item1.uuid)
expect(updatedPayload.items_key_id).to.equal(alternatedKey.uuid)
})
it('properly handles mutli item uuid alternation', async function () {
const item1 = await Factory.createMappedNote(this.application)
const item2 = await Factory.createMappedNote(this.application)
this.expectedItemCount += 2
await this.application.itemManager.changeItem(item1, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
})
expect(this.application.itemManager.itemsReferencingItem(item2).length).to.equal(1)
const alternatedItem1 = await Factory.alternateUuidForItem(this.application, item1.uuid)
const alternatedItem2 = await Factory.alternateUuidForItem(this.application, item2.uuid)
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
expect(item1.uuid).to.not.equal(alternatedItem1.uuid)
expect(item2.uuid).to.not.equal(alternatedItem2.uuid)
const refreshedAltItem1 = this.application.itemManager.findItem(alternatedItem1.uuid)
expect(refreshedAltItem1.content.references.length).to.equal(1)
expect(refreshedAltItem1.content.references[0].uuid).to.equal(alternatedItem2.uuid)
expect(alternatedItem2.content.references.length).to.equal(0)
expect(this.application.itemManager.itemsReferencingItem(alternatedItem2).length).to.equal(1)
expect(refreshedAltItem1.isReferencingItem(alternatedItem2)).to.equal(true)
expect(alternatedItem2.isReferencingItem(refreshedAltItem1)).to.equal(false)
expect(refreshedAltItem1.dirty).to.equal(true)
})
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) => {
mutator.e2ePendingRefactor_addItemAsRelationship(note)
})
expect(refreshedTag.content.references.length).to.equal(1)
const noteCopy = await this.application.itemManager.duplicateItem(note)
expect(note.uuid).to.not.equal(noteCopy.uuid)
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(2)
expect(this.application.itemManager.getDisplayableTags().length).to.equal(1)
expect(note.content.references.length).to.equal(0)
expect(noteCopy.content.references.length).to.equal(0)
const refreshedTag_2 = this.application.itemManager.findItem(tag.uuid)
expect(refreshedTag_2.content.references.length).to.equal(2)
})
it('maintains editor reference when duplicating note', async function () {
const note = await Factory.createMappedNote(this.application)
const editor = await this.application.itemManager.createItem(
ContentType.Component,
{ area: ComponentArea.Editor },
true,
)
await this.application.itemManager.changeComponent(editor, (mutator) => {
mutator.associateWithItem(note.uuid)
})
expect(this.application.componentManager.editorForNote(note).uuid).to.equal(editor.uuid)
const duplicate = await this.application.itemManager.duplicateItem(note, true)
expect(this.application.componentManager.editorForNote(duplicate).uuid).to.equal(editor.uuid)
})
})

View File

@@ -0,0 +1,871 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import * as Factory from '../lib/factory.js'
import { createRelatedNoteTagPairPayload } from '../lib/Items.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('importing', function () {
this.timeout(Factory.TenSecondTimeout)
const BASE_ITEM_COUNT = 2 /** Default items key, user preferences */
let expectedItemCount
let application
let email
let password
beforeEach(function () {
localStorage.clear()
})
const setup = async ({ fakeCrypto }) => {
expectedItemCount = BASE_ITEM_COUNT
if (fakeCrypto) {
application = await Factory.createInitAppWithFakeCrypto()
} else {
application = await Factory.createInitAppWithRealCrypto()
}
email = UuidGenerator.GenerateUuid()
password = UuidGenerator.GenerateUuid()
Factory.handlePasswordChallenges(application, password)
}
afterEach(async function () {
await Factory.safeDeinit(application)
localStorage.clear()
})
it('should not import backups made from unsupported versions', async function () {
await setup({ fakeCrypto: true })
const result = await application.mutator.importData({
version: '-1',
items: [],
})
expect(result.error).to.exist
})
it('should not import backups made from 004 into 003 account', async function () {
await setup({ fakeCrypto: true })
await Factory.registerOldUser({
application,
email,
password,
version: ProtocolVersion.V003,
})
const result = await application.mutator.importData({
version: ProtocolVersion.V004,
items: [],
})
expect(result.error).to.exist
})
it('importing existing data should keep relationships valid', async function () {
await setup({ fakeCrypto: true })
const pair = createRelatedNoteTagPairPayload()
const notePayload = pair[0]
const tagPayload = pair[1]
await application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
expectedItemCount += 2
const note = application.itemManager.getItems([ContentType.Note])[0]
const tag = application.itemManager.getItems([ContentType.Tag])[0]
expect(tag.content.references.length).to.equal(1)
expect(tag.noteCount).to.equal(1)
expect(note.content.references.length).to.equal(0)
expect(application.itemManager.itemsReferencingItem(note).length).to.equal(1)
await application.mutator.importData(
{
items: [notePayload, tagPayload],
},
true,
)
expect(application.itemManager.items.length).to.equal(expectedItemCount)
expect(tag.content.references.length).to.equal(1)
expect(tag.noteCount).to.equal(1)
expect(note.content.references.length).to.equal(0)
expect(application.itemManager.itemsReferencingItem(note).length).to.equal(1)
})
it('importing same note many times should create only one duplicate', async function () {
/**
* Used strategy here will be KEEP_LEFT_DUPLICATE_RIGHT
* which means that new right items will be created with different
*/
await setup({ fakeCrypto: true })
const notePayload = Factory.createNotePayload()
await application.itemManager.emitItemFromPayload(notePayload, PayloadEmitSource.LocalChanged)
expectedItemCount++
const mutatedNote = new DecryptedPayload({
...notePayload,
content: {
...notePayload.content,
title: `${Math.random()}`,
},
})
await application.mutator.importData(
{
items: [mutatedNote, mutatedNote, mutatedNote],
},
true,
)
expectedItemCount++
expect(application.itemManager.getDisplayableNotes().length).to.equal(2)
const imported = application.itemManager.getDisplayableNotes().find((n) => n.uuid !== notePayload.uuid)
expect(imported.content.title).to.equal(mutatedNote.content.title)
})
it('importing a tag with lesser references should not create duplicate', async function () {
await setup({ fakeCrypto: true })
const pair = createRelatedNoteTagPairPayload()
const tagPayload = pair[1]
await application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
const mutatedTag = new DecryptedPayload({
...tagPayload,
content: {
...tagPayload.content,
references: [],
},
})
await application.mutator.importData(
{
items: [mutatedTag],
},
true,
)
expect(application.itemManager.getDisplayableTags().length).to.equal(1)
expect(application.itemManager.findItem(tagPayload.uuid).content.references.length).to.equal(1)
})
it('importing data with differing content should create duplicates', async function () {
await setup({ fakeCrypto: true })
const pair = createRelatedNoteTagPairPayload()
const notePayload = pair[0]
const tagPayload = pair[1]
await application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
expectedItemCount += 2
const note = application.itemManager.getDisplayableNotes()[0]
const tag = application.itemManager.getDisplayableTags()[0]
const mutatedNote = new DecryptedPayload({
...notePayload,
content: {
...notePayload.content,
title: `${Math.random()}`,
},
})
const mutatedTag = new DecryptedPayload({
...tagPayload,
content: {
...tagPayload.content,
title: `${Math.random()}`,
},
})
await application.mutator.importData(
{
items: [mutatedNote, mutatedTag],
},
true,
)
expectedItemCount += 2
expect(application.itemManager.items.length).to.equal(expectedItemCount)
const newNote = application.itemManager.getDisplayableNotes().find((n) => n.uuid !== notePayload.uuid)
const newTag = application.itemManager.getDisplayableTags().find((t) => t.uuid !== tagPayload.uuid)
expect(newNote.uuid).to.not.equal(note.uuid)
expect(newTag.uuid).to.not.equal(tag.uuid)
const refreshedTag = application.itemManager.findItem(tag.uuid)
expect(refreshedTag.content.references.length).to.equal(2)
expect(refreshedTag.noteCount).to.equal(2)
const refreshedNote = application.itemManager.findItem(note.uuid)
expect(refreshedNote.content.references.length).to.equal(0)
expect(application.itemManager.itemsReferencingItem(refreshedNote).length).to.equal(2)
expect(newTag.content.references.length).to.equal(1)
expect(newTag.noteCount).to.equal(1)
expect(newNote.content.references.length).to.equal(0)
expect(application.itemManager.itemsReferencingItem(newNote).length).to.equal(1)
})
it('when importing items, imported values should not be used to determine if changed', async function () {
/**
* If you have a note and a tag, and the tag has 1 reference to the note,
* and you import the same two items, except modify the note value so that
* a duplicate is created, we expect only the note to be duplicated, and the
* tag not to. However, if only the note changes, and you duplicate the note,
* which causes the tag's references content to change, then when the incoming
* tag is being processed, it will also think it has changed, since our local
* value now doesn't match what's coming in. The solution is to get all values
* ahead of time before any changes are made.
*/
await setup({ fakeCrypto: true })
const note = await Factory.createMappedNote(application)
const tag = await Factory.createMappedTag(application)
expectedItemCount += 2
await application.itemManager.changeItem(tag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(note)
})
const externalNote = Object.assign(
{},
{
uuid: note.uuid,
content: note.getContentCopy(),
content_type: note.content_type,
},
)
externalNote.content.text = `${Math.random()}`
const externalTag = Object.assign(
{},
{
uuid: tag.uuid,
content: tag.getContentCopy(),
content_type: tag.content_type,
},
)
await application.mutator.importData(
{
items: [externalNote, externalTag],
},
true,
)
expectedItemCount += 1
/** We expect now that the total item count is 3, not 4. */
expect(application.itemManager.items.length).to.equal(expectedItemCount)
const refreshedTag = application.itemManager.findItem(tag.uuid)
/** References from both items have merged. */
expect(refreshedTag.content.references.length).to.equal(2)
})
it('should import decrypted data and keep items that were previously deleted', async function () {
await setup({ fakeCrypto: true })
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
Factory.handlePasswordChallenges(application, password)
const [note, tag] = await Promise.all([Factory.createMappedNote(application), Factory.createMappedTag(application)])
await application.sync.sync({ awaitAll: true })
await application.mutator.deleteItem(note)
expect(application.items.findItem(note.uuid)).to.not.exist
await application.mutator.deleteItem(tag)
expect(application.items.findItem(tag.uuid)).to.not.exist
await application.mutator.importData(
{
items: [note, tag],
},
true,
)
expect(application.itemManager.getDisplayableNotes().length).to.equal(1)
expect(application.items.findItem(note.uuid).deleted).to.not.be.ok
expect(application.itemManager.getDisplayableTags().length).to.equal(1)
expect(application.items.findItem(tag.uuid).deleted).to.not.be.ok
})
it('should duplicate notes by alternating UUIDs when dealing with conflicts during importing', async function () {
await setup({ fakeCrypto: true })
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
const note = await Factory.createSyncedNote(application)
/** Sign into another account and import the same item. It should get a different UUID. */
application = await Factory.signOutApplicationAndReturnNew(application)
email = UuidGenerator.GenerateUuid()
Factory.handlePasswordChallenges(application, password)
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
await application.mutator.importData(
{
items: [note.payload],
},
true,
)
expect(application.itemManager.getDisplayableNotes().length).to.equal(1)
expect(application.itemManager.getDisplayableNotes()[0].uuid).to.not.equal(note.uuid)
})
it('should maintain consistency between storage and PayloadManager after an import with conflicts', async function () {
await setup({ fakeCrypto: true })
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
const note = await Factory.createSyncedNote(application)
/** Sign into another account and import the same items. They should get a different UUID. */
application = await Factory.signOutApplicationAndReturnNew(application)
email = UuidGenerator.GenerateUuid()
Factory.handlePasswordChallenges(application, password)
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
await application.mutator.importData(
{
items: [note],
},
true,
)
const storedPayloads = await application.diskStorageService.getAllRawPayloads()
expect(application.itemManager.items.length).to.equal(storedPayloads.length)
const notes = storedPayloads.filter((p) => p.content_type === ContentType.Note)
const itemsKeys = storedPayloads.filter((p) => p.content_type === ContentType.ItemsKey)
expect(notes.length).to.equal(1)
expect(itemsKeys.length).to.equal(1)
})
it('should import encrypted data and keep items that were previously deleted', async function () {
await setup({ fakeCrypto: true })
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
const [note, tag] = await Promise.all([Factory.createMappedNote(application), Factory.createMappedTag(application)])
const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups()
await application.sync.sync({ awaitAll: true })
await application.mutator.deleteItem(note)
expect(application.items.findItem(note.uuid)).to.not.exist
await application.mutator.deleteItem(tag)
expect(application.items.findItem(tag.uuid)).to.not.exist
await application.mutator.importData(backupData, true)
expect(application.itemManager.getDisplayableNotes().length).to.equal(1)
expect(application.items.findItem(note.uuid).deleted).to.not.be.ok
expect(application.itemManager.getDisplayableTags().length).to.equal(1)
expect(application.items.findItem(tag.uuid).deleted).to.not.be.ok
})
it('should import decrypted data and all items payload source should be FileImport', async function () {
await setup({ fakeCrypto: true })
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
const [note, tag] = await Promise.all([Factory.createMappedNote(application), Factory.createMappedTag(application)])
const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups()
await Factory.safeDeinit(application)
application = await Factory.createInitAppWithFakeCrypto()
Factory.handlePasswordChallenges(application, password)
await application.mutator.importData(backupData, true)
const importedNote = application.items.findItem(note.uuid)
const importedTag = application.items.findItem(tag.uuid)
expect(importedNote.payload.source).to.be.equal(PayloadSource.FileImport)
expect(importedTag.payload.source).to.be.equal(PayloadSource.FileImport)
})
it('should import encrypted data and all items payload source should be FileImport', async function () {
await setup({ fakeCrypto: true })
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
const [note, tag] = await Promise.all([Factory.createMappedNote(application), Factory.createMappedTag(application)])
const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups()
await Factory.safeDeinit(application)
application = await Factory.createInitAppWithFakeCrypto()
Factory.handlePasswordChallenges(application, password)
await application.mutator.importData(backupData, true)
const importedNote = application.items.findItem(note.uuid)
const importedTag = application.items.findItem(tag.uuid)
expect(importedNote.payload.source).to.be.equal(PayloadSource.FileImport)
expect(importedTag.payload.source).to.be.equal(PayloadSource.FileImport)
})
it('should import data from 003 encrypted payload using client generated backup', async function () {
await setup({ fakeCrypto: true })
const oldVersion = ProtocolVersion.V003
await Factory.registerOldUser({
application: application,
email: email,
password: password,
version: oldVersion,
})
const noteItem = await application.itemManager.createItem(ContentType.Note, {
title: 'Encrypted note',
text: 'On protocol version 003.',
})
const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups()
await Factory.safeDeinit(application)
application = await Factory.createInitAppWithFakeCrypto()
Factory.handlePasswordChallenges(application, password)
const result = await application.mutator.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)
const decryptedNote = application.itemManager.findItem(noteItem.uuid)
expect(decryptedNote.title).to.be.eq('Encrypted note')
expect(decryptedNote.text).to.be.eq('On protocol version 003.')
expect(application.itemManager.getDisplayableNotes().length).to.equal(1)
})
it('should import data from 003 encrypted payload using server generated backup with 004 key params', async function () {
await setup({ fakeCrypto: false })
const backupData = {
items: [
{
uuid: 'eb1b7eed-e43d-48dd-b257-b7fc8ccba3da',
duplicate_of: null,
items_key_id: null,
content:
'003:618138e365a13f8aed17d4f52e3da47d4b5d6e02004a0f827118e8a981a57c35:eb1b7eed-e43d-48dd-b257-b7fc8ccba3da:9f38642b7a3f57546520a9e32aa7c0ad:qa9rUcaD904m1Knv63dnATEHwfHJjsbq9bWb06zGTsyQxzLaAYT7uRGp2KB2g1eo5Aqxc5FqhvuF0+dE1f4+uQOeiRFNX73V2pJJY0w5Qq7l7ZuhB08ZtOMY4Ctq7evBBSIVZ+PEIfFnACelNJhsB5Uhn3kS4ZBx6qtvQ6ciSQGfYAwc6wSKhjUm1umEINeb08LNgwbP6XAm8U/la1bdtdMO112XjUW7ixkWi3POWcM=:eyJpZGVudGlmaWVyIjoicGxheWdyb3VuZEBiaXRhci5pbyIsInB3X2Nvc3QiOjExMDAwMCwicHdfbm9uY2UiOiJhZmIwYjE3NGJlYjViMmJmZTIyNTk1NDlmMTgxNDI1NzlkMDE1ZmE3ZTBhMjE4YzVmNDIxNmU0Mzg2ZGI3OWFiIiwidmVyc2lvbiI6IjAwMyJ9',
content_type: 'Note',
enc_item_key:
'003:5a01e913c52899ba10c16dbe7e713dd9caf9b9554c82176ddfcf1424f5bfd94f:eb1b7eed-e43d-48dd-b257-b7fc8ccba3da:14721ff8dbdd36fb57ae4bf7414c5eab:odmq91dfaTZG/zeSUA09fD/PdB2OkiDxcQZ0FL06GPstxdvxnU17k1rtsWoA7HoNNnd5494BZ/b7YiKqUb76ddd8x3/+cTZgCa4tYxNINmb1T3wwUX0Ebxc8xynAhg6nTY/BGq+ba6jTyl8zw12dL3kBEGGglRCHnO0ZTeylwQW7asfONN8s0BwrvHdonRlx:eyJpZGVudGlmaWVyIjoicGxheWdyb3VuZEBiaXRhci5pbyIsInB3X2Nvc3QiOjExMDAwMCwicHdfbm9uY2UiOiJhZmIwYjE3NGJlYjViMmJmZTIyNTk1NDlmMTgxNDI1NzlkMDE1ZmE3ZTBhMjE4YzVmNDIxNmU0Mzg2ZGI3OWFiIiwidmVyc2lvbiI6IjAwMyJ9',
auth_hash: null,
created_at: '2019-05-12T02:29:21.789000Z',
updated_at: '2019-11-12T21:47:48.382708Z',
deleted: false,
},
{
uuid: '10051be7-4ca2-4af3-aae9-021939df4fab',
duplicate_of: null,
items_key_id: null,
content:
'004:77a986823b8ffdd87164b6f541de6ed420b70ac67e055774:+8cjww1QbyXNX+PSKeCwmnysv0rAoEaKh409VWQJpDbEy/pPZCT6c0rKxLzvyMiSq6EwkOiduZMzokRgCKP7RuRqNPJceWsxNnpIUwa40KR1IP2tdreW4J8v9pFEzPMec1oq40u+c+UI/Y6ChOLV/4ozyWmpQCK3y8Ugm7B1/FzaeDs9Ie6Mvf98+XECoi0fWv9SO2TeBvq1G24LXd4zf0j8jd0sKZbLPXH0+gaUXtBH7A56lHvB0ED9NuiHI8xopTBd9ogKlz/b5+JB4zA2zQCQ3WMEE1qz6WeB2S4FMomgeO1e3trabdU0ICu0WMvDVii4qNlQo/inD41oHXKeV5QwnYoGjPrLJIaP0hiLKhDURTHygCdvWdp63OWI+aGxv0/HI+nfcRsqSE+aYECrWB/kp/c5yTrEqBEafuWZkw==:eyJrcCI6eyJpZGVudGlmaWVyIjoicGxheWdyb3VuZEBiaXRhci5pbyIsInB3X25vbmNlIjoiNjUxYWUxZWM5NTgwMzM5YTM1NjdlZTdmMGY4NjcyNDkyZGUyYzE2NmE1NTZjMTNkMTE5NzI4YTAzYzYwZjc5MyIsInZlcnNpb24iOiIwMDQiLCJvcmlnaW5hdGlvbiI6InByb3RvY29sLXVwZ3JhZGUiLCJjcmVhdGVkIjoiMTYxNDc4NDE5MjQ5NyJ9LCJ1IjoiMTAwNTFiZTctNGNhMi00YWYzLWFhZTktMDIxOTM5ZGY0ZmFiIiwidiI6IjAwNCJ9',
content_type: 'SN|ItemsKey',
enc_item_key:
'004:d25deb224251b4705a44d8ce125a62f6a2f0e0e856603e8f:FEv1pfU/VfY7XhJrTfpcdhaSBfmNySTQtHohFYDm8V84KlyF5YaXRKV7BfXsa77DKTjOCU/EHHsWwhBEEfsNnzNySHxTHNc26bpoz0V8h50=:eyJrcCI6eyJpZGVudGlmaWVyIjoicGxheWdyb3VuZEBiaXRhci5pbyIsInB3X25vbmNlIjoiNjUxYWUxZWM5NTgwMzM5YTM1NjdlZTdmMGY4NjcyNDkyZGUyYzE2NmE1NTZjMTNkMTE5NzI4YTAzYzYwZjc5MyIsInZlcnNpb24iOiIwMDQiLCJvcmlnaW5hdGlvbiI6InByb3RvY29sLXVwZ3JhZGUiLCJjcmVhdGVkIjoiMTYxNDc4NDE5MjQ5NyJ9LCJ1IjoiMTAwNTFiZTctNGNhMi00YWYzLWFhZTktMDIxOTM5ZGY0ZmFiIiwidiI6IjAwNCJ9',
auth_hash: null,
created_at: '2020-09-07T12:22:06.562000Z',
updated_at: '2021-03-03T15:09:55.741107Z',
deleted: false,
},
],
auth_params: {
identifier: 'playground@bitar.io',
pw_nonce: '651ae1ec9580339a3567ee7f0f8672492de2c166a556c13d119728a03c60f793',
version: '004',
},
}
const password = 'password'
application = await Factory.createInitAppWithRealCrypto()
Factory.handlePasswordChallenges(application, password)
const result = await application.mutator.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)
})
it('should import data from 004 encrypted payload', async function () {
await setup({ fakeCrypto: true })
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
const noteItem = await application.itemManager.createItem(ContentType.Note, {
title: 'Encrypted note',
text: 'On protocol version 004.',
})
const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups()
await Factory.safeDeinit(application)
application = await Factory.createInitAppWithFakeCrypto()
Factory.handlePasswordChallenges(application, password)
const result = await application.mutator.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)
const decryptedNote = application.itemManager.findItem(noteItem.uuid)
expect(decryptedNote.title).to.be.eq('Encrypted note')
expect(decryptedNote.text).to.be.eq('On protocol version 004.')
expect(application.itemManager.getDisplayableNotes().length).to.equal(1)
})
it('should return correct errorCount', async function () {
await setup({ fakeCrypto: true })
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
const noteItem = await application.itemManager.createItem(ContentType.Note, {
title: 'This is a valid, encrypted note',
text: 'On protocol version 004.',
})
const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups()
await Factory.safeDeinit(application)
application = await Factory.createInitAppWithFakeCrypto()
Factory.handlePasswordChallenges(application, password)
const madeUpPayload = JSON.parse(JSON.stringify(noteItem))
madeUpPayload.items_key_id = undefined
madeUpPayload.content = '004:somenonsense'
madeUpPayload.enc_item_key = '003:anothernonsense'
madeUpPayload.version = '004'
madeUpPayload.uuid = 'fake-uuid'
backupData.items = [...backupData.items, madeUpPayload]
const result = await application.mutator.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)
})
it('should not import data from 003 encrypted payload if an invalid password is provided', async function () {
await setup({ fakeCrypto: true })
const oldVersion = ProtocolVersion.V003
await Factory.registerOldUser({
application: application,
email: email,
password: UuidGenerator.GenerateUuid(),
version: oldVersion,
})
await application.itemManager.createItem(ContentType.Note, {
title: 'Encrypted note',
text: 'On protocol version 003.',
})
const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups()
await Factory.safeDeinit(application)
application = await Factory.createInitAppWithFakeCrypto()
application.setLaunchCallback({
receiveChallenge: (challenge) => {
const values = challenge.prompts.map(
(prompt) =>
CreateChallengeValue(
prompt,
prompt.validation === ChallengeValidation.None ? 'incorrect password' : password,
),
)
application.submitValuesForChallenge(challenge, values)
},
})
const result = await application.mutator.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(application.itemManager.getDisplayableNotes().length).to.equal(0)
})
it('should not import data from 004 encrypted payload if an invalid password is provided', async function () {
await setup({ fakeCrypto: true })
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
await application.itemManager.createItem(ContentType.Note, {
title: 'This is a valid, encrypted note',
text: 'On protocol version 004.',
})
const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups()
await Factory.safeDeinit(application)
application = await Factory.createInitAppWithFakeCrypto()
application.setLaunchCallback({
receiveChallenge: (challenge) => {
const values = challenge.prompts.map((prompt) => CreateChallengeValue(prompt, 'incorrect password'))
application.submitValuesForChallenge(challenge, values)
},
})
const result = await application.mutator.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(application.itemManager.getDisplayableNotes().length).to.equal(0)
})
it('should not import encrypted data with no keyParams or auth_params', async function () {
await setup({ fakeCrypto: true })
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
await application.itemManager.createItem(ContentType.Note, {
title: 'Encrypted note',
text: 'On protocol version 004.',
})
const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups()
delete backupData.keyParams
await Factory.safeDeinit(application)
application = await Factory.createInitAppWithFakeCrypto()
const result = await application.mutator.importData(backupData)
expect(result.error).to.be.ok
})
it('should not import payloads if the corresponding ItemsKey is not present within the backup file', async function () {
await setup({ fakeCrypto: true })
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
Factory.handlePasswordChallenges(application, password)
await application.itemManager.createItem(ContentType.Note, {
title: 'Encrypted note',
text: 'On protocol version 004.',
})
const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups()
backupData.items = backupData.items.filter((payload) => payload.content_type !== ContentType.ItemsKey)
await Factory.safeDeinit(application)
application = await Factory.createInitAppWithFakeCrypto()
Factory.handlePasswordChallenges(application, password)
const result = await application.mutator.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(application.itemManager.getDisplayableNotes().length).to.equal(0)
})
it('importing data with no items key should use the root key generated by the file password', async function () {
await setup({ fakeCrypto: false })
/**
* In SNJS 2.0.12, this file import would fail with "incorrect password" on file.
* The reason was that we would use the default items key we had for the current account
* instead of using the password generated root key for the file.
*
* Note this test will not be able to properly sync as the credentials are invalid.
* This test is only meant to test successful local importing.
*/
const identifier = 'standardnotes'
const application = await Factory.createApplicationWithRealCrypto(identifier)
/** Create legacy migrations value so that base migration detects old app */
await application.deviceInterface.setRawStorageValue(
'keychain',
JSON.stringify({
[identifier]: {
version: '003',
masterKey: '30bae65687b45b20100be219df983bded23868baa44f4bbef1026403daee0a9d',
dataAuthenticationKey: 'c9b382ff1f7adb5c6cad620605ad139cd9f1e7700f507345ef1a1d46a6413712',
},
}),
)
await application.deviceInterface.setRawStorageValue(
'descriptors',
JSON.stringify({
[identifier]: {
identifier: 'standardnotes',
label: 'Main Application',
primary: true,
},
}),
)
await application.deviceInterface.setRawStorageValue('standardnotes-snjs_version', '2.0.11')
await application.deviceInterface.saveRawDatabasePayload(
{
content:
'003:9f2c7527eb8b2a1f8bfb3ea6b885403b6886bce2640843ebd57a6c479cbf7597:58e3322b-269a-4be3-a658-b035dffcd70f:9140b23a0fa989e224e292049f133154:SESTNOgIGf2+ZqmJdFnGU4EMgQkhKOzpZNoSzx76SJaImsayzctAgbUmJ+UU2gSQAHADS3+Z5w11bXvZgIrStTsWriwvYkNyyKmUPadKHNSBwOk4WeBZpWsA9gtI5zgI04Q5pvb8hS+kNW2j1DjM4YWqd0JQxMOeOrMIrxr/6Awn5TzYE+9wCbXZdYHyvRQcp9ui/G02ZJ67IA86vNEdjTTBAAWipWqTqKH9VDZbSQ2W/IOKfIquB373SFDKZb1S1NmBFvcoG2G7w//fAl/+ehYiL6UdiNH5MhXCDAOTQRFNfOh57HFDWVnz1VIp8X+VAPy6d9zzQH+8aws1JxHq/7BOhXrFE8UCueV6kERt9njgQxKJzd9AH32ShSiUB9X/sPi0fUXbS178xAZMJrNx3w==:eyJwd19ub25jZSI6IjRjYjEwM2FhODljZmY0NTYzYTkxMWQzZjM5NjU4M2NlZmM2ODMzYzY2Zjg4MGZiZWUwNmJkYTk0YzMxZjg2OGIiLCJwd19jb3N0IjoxMTAwMDAsImlkZW50aWZpZXIiOiJub3YyMzIyQGJpdGFyLmlvIiwidmVyc2lvbiI6IjAwMyIsIm9yaWdpbmF0aW9uIjoicmVnaXN0cmF0aW9uIn0=',
content_type: 'SN|ItemsKey',
created_at: new Date(),
enc_item_key:
'003:d7267919b07864ccc1da87a48db6c6192e2e892be29ce882e981c36f673b3847:58e3322b-269a-4be3-a658-b035dffcd70f:2384a22d8f8bf671ba6517c6e1d0be30:0qXjBDPLCcMlNTnuUDcFiJPIXU9OP6b4ttTVE58n2Jn7971xMhx6toLbAZWWLPk/ezX/19EYE9xmRngWsG4jJaZMxGZIz/melU08K7AHH3oahQpHwZvSM3iV2ufsN7liQywftdVH6NNzULnZnFX+FgEfpDquru++R4aWDLvsSegWYmde9zD62pPNUB9Kik6P:eyJwd19ub25jZSI6IjRjYjEwM2FhODljZmY0NTYzYTkxMWQzZjM5NjU4M2NlZmM2ODMzYzY2Zjg4MGZiZWUwNmJkYTk0YzMxZjg2OGIiLCJwd19jb3N0IjoxMTAwMDAsImlkZW50aWZpZXIiOiJub3YyMzIyQGJpdGFyLmlvIiwidmVyc2lvbiI6IjAwMyIsIm9yaWdpbmF0aW9uIjoicmVnaXN0cmF0aW9uIn0=',
updated_at: new Date(),
uuid: '58e3322b-269a-4be3-a658-b035dffcd70f',
},
identifier,
)
/**
* Note that this storage contains "sync.standardnotes.org" as the API Host param.
*/
await application.deviceInterface.setRawStorageValue(
'standardnotes-storage',
JSON.stringify({
wrapped: {
uuid: '15af096f-4e9d-4cde-8d67-f132218fa757',
content_type: 'SN|EncryptedStorage',
enc_item_key:
'003:2fb0c55859ddf0c16982b91d6202a6fb8174f711d820f8b785c558538cda5048:15af096f-4e9d-4cde-8d67-f132218fa757:09a4da52d5214e76642f0363246daa99:zt5fnmxYSZOqC+uA08oAKdtjfTdAoX1lPnbTe98CYQSlIvaePIpG5c9tAN5QzZbECkj4Lm9txwSA2O6Y4Y25rqO4lIerKjxxNqPwDze9mtPOGeoR48csUPiMIHiH78bLGZZs4VoBwYKAP+uEygXEFYRuscGnDOrFV7fnwGDL/nkhr6xpM159OTUKBgiBpVMS:eyJwd19ub25jZSI6IjRjYjEwM2FhODljZmY0NTYzYTkxMWQzZjM5NjU4M2NlZmM2ODMzYzY2Zjg4MGZiZWUwNmJkYTk0YzMxZjg2OGIiLCJwd19jb3N0IjoxMTAwMDAsImlkZW50aWZpZXIiOiJub3YyMzIyQGJpdGFyLmlvIiwidmVyc2lvbiI6IjAwMyIsIm9yaWdpbmF0aW9uIjoicmVnaXN0cmF0aW9uIn0=',
content:
'003:70a02948696e09211cfd34cd312dbbf85751397189da06d7acc7c46dafa9aeeb:15af096f-4e9d-4cde-8d67-f132218fa757:b92fb4b030ac51f4d3eef0ada35f3d5f:r3gdrawyd069qOQQotD5EtabTwjs4IiLFWotK0Ygbt9oAT09xILx7v92z8YALJ6i6EKHOT7zyCytR5l2B9b1J7Tls00uVgfEKs3zX7n3F6ne+ju0++WsJuy0Gre5+Olov6lqQrY3I8hWQShxaG84huZaFTIPU5+LP0JAseWWDENqUQ+Vxr+w0wqNYO6TLtr/YAqk2yOY7DLQ0WhGzK+WH9JfvS8MCccJVeBD99ebM8lKVVfTaUfrk2AlbMv47TFSjTeCDblQuU68joE45HV8Y0g2CF4nkTvdr3wn0HhdDp07YuXditX9NGtBhI8oFkstwKEksblyX9dGpn7of4ctdvNOom3Vjw/m4x9mE0lCIbjxQVAiDyy+Hg0HDtVt1j205ycg1RS7cT7+Sn746Z06S8TixcVUUUQh+MGRIulIE5utOE81Lv/p+jb2vmv+TGHUV4kZJPluG7A9IEphMZrMWwiU56FdSlSDD82qd9iG+C3Pux+X/GYCMiWS2T/BoyI6a9OERSARuTUuom2bv59hqD1yUoj7VQXhqXmverSwLE1zDeF+dc0tMwuTNCNOTk08A6wRKTR9ZjuFlLcxHsg/VZyfIdCkElFh1FrliMbW2ZsgsPFaZAI+YN8pid1tTw+Ou1cOfyD85aki98DDvg/cTi8ahrrm8UvxRQwhIW17Cm1RnKxhIvaq5HRjEN76Y46ubkZv7/HjhNwJt9vPEr9wyOrMH6XSxCnSIFD1kbVHI33q444xyUWa/EQju8SoEGGU92HhpMWd1kIz37SJRJTC7u2ah2Xg60JGcUcCNtHG3IHMPVP+UKUjx5nKP6t/NVSa+xsjIvM/ZkSL37W0TMZykC1cKfzeUmlZhGQPCIqad3b4ognZ48LGCgwBP87rWn8Ln8Cqcz7X0Ze22HoouKBPAtWlYJ8fmvg2HiW6nX/L9DqoxK4OXt/LnC2BTEvtP4PUzBqx8WoqmVNNnYp+FgYptLcgxmgckle41w1eMr6NYGeaaC1Jk3i/e9Piw0w0XjV/lB+yn03gEMYPTT2yiXMQrfPmkUNYNN7/xfhY3bqqwfER7iXdr/80Lc+x9byywChXLvg8VCjHWGd+Sky3NHyMdxLY8IqefyyZWMeXtt1aNYH6QW9DeK5KvK3DI+MK3kWwMCySe51lkE9jzcqrxpYMZjb2Za9VDZNBgdwQYXfOlxFEje0so0LlMJmmxRfbMU06bYt0vszT2szAkOnVuyi6TBRiGLyjMxYI0csM0SHZWZUQK0z7ZoQAWR5D+adX29tOvrKc2kJA8Lrzgeqw/rJIh6zPg3kmsd2rFbo+Qfe3J6XrlZU+J+N96I98i0FU0quI6HwG1zFg6UOmfRjaCML8rSAPtMaNhlO7M2sgRmDCtsNcpU06Fua6F2fEHPiXs4+9:eyJwd19ub25jZSI6IjRjYjEwM2FhODljZmY0NTYzYTkxMWQzZjM5NjU4M2NlZmM2ODMzYzY2Zjg4MGZiZWUwNmJkYTk0YzMxZjg2OGIiLCJwd19jb3N0IjoxMTAwMDAsImlkZW50aWZpZXIiOiJub3YyMzIyQGJpdGFyLmlvIiwidmVyc2lvbiI6IjAwMyIsIm9yaWdpbmF0aW9uIjoicmVnaXN0cmF0aW9uIn0=',
created_at: '2020-11-24T00:53:42.057Z',
updated_at: '1970-01-01T00:00:00.000Z',
},
nonwrapped: {
ROOT_KEY_PARAMS: {
pw_nonce: '4cb103aa89cff4563a911d3f396583cefc6833c66f880fbee06bda94c31f868b',
pw_cost: 110000,
identifier: 'nov2322@bitar.io',
version: '003',
},
},
}),
)
const password = 'password'
await application.prepareForLaunch({
receiveChallenge: (challenge) => {
if (challenge.prompts.length === 2) {
application.submitValuesForChallenge(
challenge,
challenge.prompts.map(
(prompt) =>
CreateChallengeValue(
prompt,
prompt.validation !== ChallengeValidation.ProtectionSessionDuration
? password
: UnprotectedAccessSecondsDuration.OneMinute,
),
),
)
} else {
const prompt = challenge.prompts[0]
application.submitValuesForChallenge(challenge, [CreateChallengeValue(prompt, password)])
}
},
})
await application.launch(false)
await application.setHost(Factory.getDefaultHost())
const backupFile = {
items: [
{
uuid: '11204d02-5a8b-47c0-ab94-ae0727d656b5',
content_type: 'Note',
created_at: '2020-11-23T17:11:06.322Z',
enc_item_key:
'003:111edcff9ed3432b9e11c4a64bef9e810ed2b9147790963caf6886511c46bbc4:11204d02-5a8b-47c0-ab94-ae0727d656b5:62de2b95cca4d7948f70516d12f5cb3a:lhUF/EoQP2DC8CSVrXyLp1yXsiJUXxwmtkwXtLUJ5sm4E0+ZNzMCO9U9ho+q6i9V+777dSbfTqODz4ZSt6hj3gtYxi9ZlOM/VrTtmJ2YcxiMaRTVl5sVZPG+YTpQPMuugN5/0EfuT/SJ9IqVbjgYhKA5xt/lMgw4JSbiW8ZkVQ5tVDfgt0omhDRLlkh758ou:eyJwd19ub25jZSI6IjNlMzU3YzQxZmI1YWU2MTUyYmZmMzY2ZjBhOGE3ZjRmZDk2NDQxZDZhNWViYzY3MDA4OTk2ZWY2YzU1YTg3ZjIiLCJwd19jb3N0IjoxMTAwMDAsImlkZW50aWZpZXIiOiJub3YyMzVAYml0YXIuaW8iLCJ2ZXJzaW9uIjoiMDAzIn0=',
content:
'003:d43c6d2dc9465796e01145843cf1b95031030c15cc79a73f14d941d15e28147a:11204d02-5a8b-47c0-ab94-ae0727d656b5:84a2b760019a62d7ad9c314bc7a5564a:G8Mm9fy9ybuo92VbV4NUERruJ1VA7garv1+fBg4KRDRjsRGoLvORhHldQHRfUQmSR6PkrG6ol/jOn1gjIH5gtgGczB5NgbKau7amYZHsQJPr1UleJVsLrjMJgiYGqbEDmXPtJSX2tLGFhAbYcVX4xrHKbkiuLQnu9bZp9zbR6txB1NtLoNFvwDZTMko7Q+28fM4TKBbQCCw3NufLHVUnfEwS7tLLFFPdEyyMXOerKP93u8X+7NG2eDmsUetPsPOq:eyJwd19ub25jZSI6IjNlMzU3YzQxZmI1YWU2MTUyYmZmMzY2ZjBhOGE3ZjRmZDk2NDQxZDZhNWViYzY3MDA4OTk2ZWY2YzU1YTg3ZjIiLCJwd19jb3N0IjoxMTAwMDAsImlkZW50aWZpZXIiOiJub3YyMzVAYml0YXIuaW8iLCJ2ZXJzaW9uIjoiMDAzIn0=',
auth_hash: null,
updated_at: '2020-11-23T17:11:40.399Z',
},
],
auth_params: {
pw_nonce: '3e357c41fb5ae6152bff366f0a8a7f4fd96441d6a5ebc67008996ef6c55a87f2',
pw_cost: 110000,
identifier: 'nov235@bitar.io',
version: '003',
},
}
const result = await application.mutator.importData(backupFile, false)
expect(result.errorCount).to.equal(0)
await Factory.safeDeinit(application)
})
it('importing another accounts notes/tags should correctly keep relationships', async function () {
this.timeout(Factory.TwentySecondTimeout)
await setup({ fakeCrypto: true })
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
Factory.handlePasswordChallenges(application, password)
const pair = createRelatedNoteTagPairPayload()
await application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
await application.sync.sync()
const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups()
await Factory.safeDeinit(application)
application = await Factory.createInitAppWithFakeCrypto()
Factory.handlePasswordChallenges(application, password)
await Factory.registerUserToApplication({
application: application,
email: `${Math.random()}`,
password: password,
})
await application.mutator.importData(backupData, true)
expect(application.itemManager.getDisplayableNotes().length).to.equal(1)
expect(application.itemManager.getDisplayableTags().length).to.equal(1)
const importedNote = application.itemManager.getDisplayableNotes()[0]
const importedTag = application.itemManager.getDisplayableTags()[0]
expect(application.itemManager.referencesForItem(importedTag).length).to.equal(1)
expect(application.itemManager.itemsReferencingItem(importedNote).length).to.equal(1)
})
})

View File

@@ -0,0 +1,220 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('items', () => {
const BASE_ITEM_COUNT = 2 /** Default items key, user preferences */
const syncOptions = {
checkIntegrity: true,
awaitAll: true,
}
beforeEach(async function () {
this.expectedItemCount = BASE_ITEM_COUNT
this.application = await Factory.createInitAppWithFakeCrypto()
})
afterEach(async function () {
await Factory.safeDeinit(this.application)
})
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)
const item = this.application.itemManager.items[0]
const prevDate = item.userModifiedDate.getTime()
await Factory.sleep(0.1)
await this.application.itemManager.setItemDirty(item, true)
const refreshedItem = this.application.itemManager.findItem(item.uuid)
const newDate = refreshedItem.userModifiedDate.getTime()
expect(prevDate).to.not.equal(newDate)
})
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)
const item = this.application.itemManager.items[0]
const prevDate = item.userModifiedDate.getTime()
await Factory.sleep(0.1)
await this.application.itemManager.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)
const item = this.application.itemManager.items[0]
expect(item.pinned).to.not.be.ok
const refreshedItem = await this.application.mutator.changeAndSaveItem(
item,
(mutator) => {
mutator.pinned = true
mutator.archived = true
mutator.locked = true
},
undefined,
undefined,
syncOptions,
)
expect(refreshedItem.pinned).to.equal(true)
expect(refreshedItem.archived).to.equal(true)
expect(refreshedItem.locked).to.equal(true)
})
it('properly compares item equality', async function () {
const params1 = Factory.createNotePayload()
const params2 = Factory.createNotePayload()
await this.application.itemManager.emitItemsFromPayloads([params1, params2], PayloadEmitSource.LocalChanged)
let item1 = this.application.itemManager.getDisplayableNotes()[0]
let item2 = this.application.itemManager.getDisplayableNotes()[1]
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
// items should ignore this field when checking for equality
item1 = await this.application.mutator.changeAndSaveItem(
item1,
(mutator) => {
mutator.userModifiedDate = new Date()
},
undefined,
undefined,
syncOptions,
)
item2 = await this.application.mutator.changeAndSaveItem(
item2,
(mutator) => {
mutator.userModifiedDate = undefined
},
undefined,
undefined,
syncOptions,
)
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
item1 = await this.application.mutator.changeAndSaveItem(
item1,
(mutator) => {
mutator.mutableContent.foo = 'bar'
},
undefined,
undefined,
syncOptions,
)
expect(item1.isItemContentEqualWith(item2)).to.equal(false)
item2 = await this.application.mutator.changeAndSaveItem(
item2,
(mutator) => {
mutator.mutableContent.foo = 'bar'
},
undefined,
undefined,
syncOptions,
)
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
expect(item2.isItemContentEqualWith(item1)).to.equal(true)
item1 = await this.application.mutator.changeAndSaveItem(
item1,
(mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
},
undefined,
undefined,
syncOptions,
)
item2 = await this.application.mutator.changeAndSaveItem(
item2,
(mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
},
undefined,
undefined,
syncOptions,
)
expect(item1.content.references.length).to.equal(1)
expect(item2.content.references.length).to.equal(1)
expect(item1.isItemContentEqualWith(item2)).to.equal(false)
item1 = await this.application.mutator.changeAndSaveItem(
item1,
(mutator) => {
mutator.removeItemAsRelationship(item2)
},
undefined,
undefined,
syncOptions,
)
item2 = await this.application.mutator.changeAndSaveItem(
item2,
(mutator) => {
mutator.removeItemAsRelationship(item1)
},
undefined,
undefined,
syncOptions,
)
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
expect(item1.content.references.length).to.equal(0)
expect(item2.content.references.length).to.equal(0)
})
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)
let item1 = this.application.itemManager.getDisplayableNotes()[0]
const item2 = this.application.itemManager.getDisplayableNotes()[1]
item1 = await this.application.mutator.changeAndSaveItem(
item1,
(mutator) => {
mutator.mutableContent.foo = 'bar'
},
undefined,
undefined,
syncOptions,
)
expect(item1.content.foo).to.equal('bar')
item1.contentKeysToIgnoreWhenCheckingEquality = () => {
return ['foo']
}
item2.contentKeysToIgnoreWhenCheckingEquality = () => {
return ['foo']
}
// calling isItemContentEqualWith should not have side effects
// There was an issue where calling that function would modify values directly to omit keys
// in contentKeysToIgnoreWhenCheckingEquality.
await this.application.itemManager.setItemsDirty([item1, item2])
expect(item1.userModifiedDate).to.be.ok
expect(item2.userModifiedDate).to.be.ok
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
expect(item2.isItemContentEqualWith(item1)).to.equal(true)
expect(item1.userModifiedDate).to.be.ok
expect(item2.userModifiedDate).to.be.ok
expect(item1.content.foo).to.equal('bar')
})
})

View File

@@ -0,0 +1,127 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import * as Factory from '../lib/factory.js'
import { createNoteParams } from '../lib/Items.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('model manager mapping', () => {
const BASE_ITEM_COUNT = 2 /** Default items key, user preferences */
beforeEach(async function () {
this.expectedItemCount = BASE_ITEM_COUNT
this.application = await Factory.createInitAppWithFakeCrypto()
})
afterEach(async function () {
await Factory.safeDeinit(this.application)
})
it('mapping nonexistent item creates it', async function () {
const payload = Factory.createNotePayload()
await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged)
this.expectedItemCount++
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
})
it('mapping nonexistent deleted item doesnt create it', async function () {
const payload = new DeletedPayload({
...createNoteParams(),
dirty: false,
deleted: true,
})
await this.application.itemManager.emitItemFromPayload(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)
this.expectedItemCount++
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
const changedParams = new DeletedPayload({
...payload,
dirty: false,
deleted: true,
})
this.expectedItemCount--
await this.application.itemManager.emitItemsFromPayloads([changedParams], PayloadEmitSource.LocalChanged)
expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount)
})
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)
this.expectedItemCount++
await this.application.itemManager.emitItemFromPayload(new DeleteItemMutator(item).getDeletedResult())
const payload2 = new DeletedPayload(this.application.payloadManager.findOne(payload.uuid).ejected())
await this.application.itemManager.emitItemsFromPayloads([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)
const newTitle = 'updated title'
const mutated = new DecryptedPayload({
...payload,
content: {
...payload.content,
title: newTitle,
},
})
await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged)
const item = this.application.itemManager.getDisplayableNotes()[0]
expect(item.content.title).to.equal(newTitle)
})
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)
const note = this.application.itemManager.getDisplayableNotes()[0]
await this.application.itemManager.setItemDirty(note)
const dirtyItems = this.application.itemManager.getDirtyItems()
expect(dirtyItems.length).to.equal(1)
})
it('set all items dirty', async function () {
const count = 10
this.expectedItemCount += count
const payloads = []
for (let i = 0; i < count; i++) {
payloads.push(Factory.createNotePayload())
}
await this.application.itemManager.emitItemsFromPayloads(payloads, PayloadEmitSource.LocalChanged)
await this.application.syncService.markAllItemsAsNeedingSyncAndPersist()
const dirtyItems = this.application.itemManager.getDirtyItems()
expect(dirtyItems.length).to.equal(this.expectedItemCount)
})
it('sync observers should be notified of changes', async function () {
const payload = Factory.createNotePayload()
await this.application.itemManager.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)
})
})
})

View File

@@ -0,0 +1,75 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
const generateLongString = (minLength = 600) => {
const BASE = 'Lorem ipsum dolor sit amet. '
const repeatCount = Math.ceil(minLength / BASE.length)
return BASE.repeat(repeatCount)
}
const getFilteredNotes = (application, { views }) => {
const criteria = {
views,
includePinned: true,
}
application.items.setPrimaryItemDisplayOptions(criteria)
const notes = application.items.getDisplayableNotes()
return notes
}
const titles = (items) => {
return items.map((item) => item.title).sort()
}
describe('notes and smart views', () => {
beforeEach(async function () {
this.application = await Factory.createInitAppWithFakeCrypto()
})
afterEach(async function () {
await Factory.safeDeinit(this.application)
})
it('lets me create a smart view and use it', async function () {
// ## The user creates 3 notes
const [note_1, note_2, note_3] = await Promise.all([
Factory.createMappedNote(this.application, 'long & pinned', generateLongString()),
Factory.createMappedNote(this.application, 'long & !pinned', generateLongString()),
Factory.createMappedNote(this.application, 'pinned', 'this is a pinned note'),
])
// The user pin 2 notes
await Promise.all([Factory.pinNote(this.application, note_1), Factory.pinNote(this.application, note_3)])
// ## The user creates smart views (long & pinned)
const not_pinned = '!["Not Pinned", "pinned", "=", false]'
const long = '!["Long", "text.length", ">", 500]'
const tag_not_pinned = await this.application.mutator.createTagOrSmartView(not_pinned)
const tag_long = await this.application.mutator.createTagOrSmartView(long)
// ## The user can filter and see the pinned notes
const notes_not_pinned = getFilteredNotes(this.application, {
views: [tag_not_pinned],
})
expect(titles(notes_not_pinned)).to.eql(['long & !pinned'])
// ## The user can filter and see the long notes
const notes_long = getFilteredNotes(this.application, { views: [tag_long] })
expect(titles(notes_long)).to.eql(['long & !pinned', 'long & pinned'])
// ## The user creates a new long note
await Factory.createMappedNote(this.application, 'new long', generateLongString())
// ## The user can filter and see the new long note
const notes_long2 = getFilteredNotes(this.application, {
views: [tag_long],
})
expect(titles(notes_long2)).to.eql(['long & !pinned', 'long & pinned', 'new long'])
})
})

View File

@@ -0,0 +1,846 @@
/* eslint-disable no-undef */
import * as Factory from '../lib/factory.js'
import * as Utils from '../lib/Utils.js'
import { createRelatedNoteTagPairPayload } from '../lib/Items.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('notes and tags', () => {
const BASE_ITEM_COUNT = 2 /** Default items key, user preferences */
const syncOptions = {
checkIntegrity: true,
awaitAll: true,
}
beforeEach(async function () {
this.expectedItemCount = BASE_ITEM_COUNT
this.application = await Factory.createInitAppWithFakeCrypto()
})
afterEach(async function () {
await Factory.safeDeinit(this.application)
})
it('uses proper class for note', async function () {
const payload = Factory.createNotePayload()
await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
const note = this.application.itemManager.getItems([ContentType.Note])[0]
expect(note.constructor === SNNote).to.equal(true)
})
it('properly constructs syncing params', async function () {
const title = 'Foo'
const text = 'Bar'
const note = await this.application.mutator.createTemplateItem(ContentType.Note, {
title,
text,
})
expect(note.content.title).to.equal(title)
expect(note.content.text).to.equal(text)
const tag = await this.application.mutator.createTemplateItem(ContentType.Tag, {
title,
})
expect(tag.title).to.equal(title)
})
it('properly handles legacy relationships', async function () {
// legacy relationships are when a note has a reference to a tag
const pair = createRelatedNoteTagPairPayload()
const notePayload = pair[0]
const tagPayload = pair[1]
const mutatedTag = new DecryptedPayload({
...tagPayload,
content: {
...tagPayload.content,
references: null,
},
})
const mutatedNote = new DecryptedPayload({
...notePayload,
content: {
references: [
{
uuid: tagPayload.uuid,
content_type: tagPayload.content_type,
},
],
},
})
await this.application.itemManager.emitItemsFromPayloads([mutatedNote, mutatedTag], PayloadEmitSource.LocalChanged)
const note = this.application.itemManager.getItems([ContentType.Note])[0]
const tag = this.application.itemManager.getItems([ContentType.Tag])[0]
expect(note.content.references.length).to.equal(1)
expect(this.application.itemManager.itemsReferencingItem(tag).length).to.equal(1)
})
it('creates relationship between note and tag', async function () {
const pair = createRelatedNoteTagPairPayload({ dirty: false })
const notePayload = pair[0]
const tagPayload = pair[1]
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)
let note = this.application.itemManager.getDisplayableNotes()[0]
let tag = this.application.itemManager.getDisplayableTags()[0]
expect(note.dirty).to.not.be.ok
expect(tag.dirty).to.not.be.ok
expect(note.content.references.length).to.equal(0)
expect(tag.content.references.length).to.equal(1)
expect(note.isReferencingItem(tag)).to.equal(false)
expect(tag.isReferencingItem(note)).to.equal(true)
expect(this.application.itemManager.itemsReferencingItem(note).length).to.equal(1)
expect(note.payload.references.length).to.equal(0)
expect(tag.noteCount).to.equal(1)
await this.application.itemManager.setItemToBeDeleted(note)
tag = this.application.itemManager.getDisplayableTags()[0]
const deletedNotePayload = this.application.payloadManager.findOne(note.uuid)
expect(deletedNotePayload.dirty).to.be.true
expect(tag.dirty).to.be.true
await this.application.syncService.sync(syncOptions)
expect(tag.content.references.length).to.equal(0)
expect(this.application.itemManager.itemsReferencingItem(note).length).to.equal(0)
expect(tag.noteCount).to.equal(0)
tag = this.application.itemManager.getDisplayableTags()[0]
expect(this.application.itemManager.getDisplayableNotes().length).to.equal(0)
expect(tag.dirty).to.be.false
})
it('handles remote deletion of relationship', async function () {
const pair = createRelatedNoteTagPairPayload()
const notePayload = pair[0]
const tagPayload = pair[1]
await this.application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged)
let 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)
await this.application.syncService.sync(syncOptions)
const mutatedTag = new DecryptedPayload({
...tagPayload,
dirty: false,
content: {
...tagPayload.content,
references: [],
},
})
await this.application.itemManager.emitItemsFromPayloads([mutatedTag], PayloadEmitSource.LocalChanged)
note = this.application.itemManager.findItem(note.uuid)
tag = this.application.itemManager.findItem(tag.uuid)
expect(tag.content.references.length).to.equal(0)
expect(this.application.itemManager.itemsReferencingItem(note).length).to.equal(0)
expect(tag.noteCount).to.equal(0)
// expect to be false
expect(note.dirty).to.not.be.ok
expect(tag.dirty).to.not.be.ok
})
it('creating basic note should have text set', async function () {
const note = await Factory.createMappedNote(this.application)
expect(note.title).to.be.ok
expect(note.text).to.be.ok
})
it('creating basic tag should have title', async function () {
const tag = await Factory.createMappedTag(this.application)
expect(tag.title).to.be.ok
})
it('handles removing relationship between note and tag', async function () {
const pair = createRelatedNoteTagPairPayload()
const notePayload = pair[0]
const tagPayload = pair[1]
await this.application.itemManager.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,
(mutator) => {
mutator.removeItemAsRelationship(note)
},
undefined,
undefined,
syncOptions,
)
expect(this.application.itemManager.itemsReferencingItem(note).length).to.equal(0)
expect(tag.noteCount).to.equal(0)
})
it('properly handles tag duplication', async function () {
const pair = createRelatedNoteTagPairPayload()
await this.application.itemManager.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)
await this.application.syncService.sync(syncOptions)
note = this.application.itemManager.findItem(note.uuid)
tag = this.application.itemManager.findItem(tag.uuid)
expect(tag.uuid).to.not.equal(duplicateTag.uuid)
expect(tag.content.references.length).to.equal(1)
expect(tag.noteCount).to.equal(1)
expect(duplicateTag.content.references.length).to.equal(1)
expect(duplicateTag.noteCount).to.equal(1)
const noteTags = this.application.itemManager.itemsReferencingItem(note)
expect(noteTags.length).to.equal(2)
const noteTag1 = noteTags[0]
const noteTag2 = noteTags[1]
expect(noteTag1.uuid).to.not.equal(noteTag2.uuid)
// expect to be false
expect(note.dirty).to.not.be.ok
expect(tag.dirty).to.not.be.ok
})
it('duplicating a note should maintain its tag references', async function () {
const pair = createRelatedNoteTagPairPayload()
const notePayload = pair[0]
const tagPayload = pair[1]
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
const note = this.application.itemManager.getItems([ContentType.Note])[0]
const duplicateNote = await this.application.itemManager.duplicateItem(note, true)
expect(note.uuid).to.not.equal(duplicateNote.uuid)
expect(this.application.itemManager.itemsReferencingItem(duplicateNote).length).to.equal(
this.application.itemManager.itemsReferencingItem(note).length,
)
})
it('deleting a note should update tag references', async function () {
const pair = createRelatedNoteTagPairPayload()
const notePayload = pair[0]
const tagPayload = pair[1]
await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged)
const note = this.application.itemManager.getItems([ContentType.Note])[0]
let tag = this.application.itemManager.getItems([ContentType.Tag])[0]
expect(tag.content.references.length).to.equal(1)
expect(tag.noteCount).to.equal(1)
expect(note.content.references.length).to.equal(0)
expect(this.application.itemManager.itemsReferencingItem(note).length).to.equal(1)
await this.application.itemManager.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)
let note = this.application.itemManager.getItems([ContentType.Note])[0]
note = await this.application.mutator.changeAndSaveItem(
note,
(mutator) => {
mutator.mutableContent.title = Math.random()
},
undefined,
undefined,
syncOptions,
)
expect(note.content.title).to.not.equal(notePayload.content.title)
})
it('deleting a tag should not dirty notes', async function () {
// Tags now reference notes, but it used to be that tags referenced notes and notes referenced tags.
// After the change, there was an issue where removing an old tag relationship from a note would only
// remove one way, and thus keep it intact on the visual level.
const pair = createRelatedNoteTagPairPayload()
const notePayload = pair[0]
const tagPayload = pair[1]
await this.application.itemManager.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)
note = this.application.itemManager.findItem(note.uuid)
this.application.itemManager.findItem(tag.uuid)
expect(note.dirty).to.not.be.ok
})
it('should sort notes', async function () {
await Promise.all(
['Y', 'Z', 'A', 'B'].map(async (title) => {
return this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, { title }),
)
}),
)
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'dsc',
})
const titles = this.application.items.getDisplayableNotes().map((note) => note.title)
/** setPrimaryItemDisplayOptions inverses sort for title */
expect(titles).to.deep.equal(['A', 'B', 'Y', 'Z'])
})
it('setting a note dirty should collapse its properties into content', async function () {
let note = await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'Foo',
})
await this.application.mutator.insertItem(note)
note = this.application.itemManager.findItem(note.uuid)
expect(note.content.title).to.equal('Foo')
})
describe('Tags', function () {
it('should sort tags in ascending alphabetical order by default', async function () {
const titles = ['1', 'A', 'b', '2']
const sortedTitles = titles.sort((a, b) => a.localeCompare(b))
await Promise.all(titles.map((title) => this.application.mutator.findOrCreateTag(title)))
expect(this.application.items.tagDisplayController.items().map((t) => t.title)).to.deep.equal(sortedTitles)
})
it('should match a tag', async function () {
const taggedNote = await Factory.createMappedNote(this.application)
const tag = await this.application.mutator.findOrCreateTag('A')
await this.application.mutator.changeItem(tag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(taggedNote)
})
await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'dsc',
tags: [tag],
})
const displayedNotes = this.application.items.getDisplayableNotes()
expect(displayedNotes.length).to.equal(1)
expect(displayedNotes[0].uuid).to.equal(taggedNote.uuid)
})
it('should not show trashed notes when displaying a tag', async function () {
const taggedNote = await Factory.createMappedNote(this.application)
const trashedNote = await Factory.createMappedNote(this.application)
const tag = await this.application.mutator.findOrCreateTag('A')
await this.application.mutator.changeItem(tag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(taggedNote)
mutator.e2ePendingRefactor_addItemAsRelationship(trashedNote)
})
await this.application.mutator.changeItem(trashedNote, (mutator) => {
mutator.trashed = true
})
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'dsc',
tags: [tag],
includeTrashed: false,
})
const displayedNotes = this.application.items.getDisplayableNotes()
expect(displayedNotes.length).to.equal(1)
expect(displayedNotes[0].uuid).to.equal(taggedNote.uuid)
})
it('should sort notes when displaying tag', async function () {
await Promise.all(
['Y', 'Z', 'A', 'B'].map(async (title) => {
return this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title,
}),
)
}),
)
const pinnedNote = this.application.itemManager.getDisplayableNotes().find((note) => note.title === 'B')
await this.application.mutator.changeItem(pinnedNote, (mutator) => {
mutator.pinned = true
})
const tag = await this.application.mutator.findOrCreateTag('A')
await this.application.mutator.changeItem(tag, (mutator) => {
for (const note of this.application.itemManager.getDisplayableNotes()) {
mutator.e2ePendingRefactor_addItemAsRelationship(note)
}
})
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'dsc',
tags: [tag],
})
const displayedNotes = this.application.items.getDisplayableNotes()
expect(displayedNotes).to.have.length(4)
/** setPrimaryItemDisplayOptions inverses sort for title */
expect(displayedNotes[0].title).to.equal('B')
expect(displayedNotes[1].title).to.equal('A')
})
})
describe('Smart views', function () {
it('"title", "startsWith", "Foo"', async function () {
const note = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'Foo 🎲',
}),
)
await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'Not Foo 🎲',
}),
)
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
title: 'Foo Notes',
predicate: {
keypath: 'title',
operator: 'startsWith',
value: 'Foo',
},
}),
)
const matches = this.application.items.notesMatchingSmartView(view)
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'asc',
views: [view],
})
const displayedNotes = this.application.items.getDisplayableNotes()
expect(displayedNotes).to.deep.equal(matches)
expect(matches.length).to.equal(1)
expect(matches[0].uuid).to.equal(note.uuid)
})
it('"pinned", "=", true', async function () {
const note = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
await this.application.mutator.changeItem(note, (mutator) => {
mutator.pinned = true
})
await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'B',
pinned: false,
}),
)
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
title: 'Pinned',
predicate: {
keypath: 'pinned',
operator: '=',
value: true,
},
}),
)
const matches = this.application.items.notesMatchingSmartView(view)
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'asc',
views: [view],
})
const displayedNotes = this.application.items.getDisplayableNotes()
expect(displayedNotes).to.deep.equal(matches)
expect(matches.length).to.equal(1)
expect(matches[0].uuid).to.equal(note.uuid)
})
it('"pinned", "=", false', async function () {
const pinnedNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
await this.application.mutator.changeItem(pinnedNote, (mutator) => {
mutator.pinned = true
})
const unpinnedNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'B',
}),
)
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
title: 'Not pinned',
predicate: {
keypath: 'pinned',
operator: '=',
value: false,
},
}),
)
const matches = this.application.items.notesMatchingSmartView(view)
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'asc',
views: [view],
})
const displayedNotes = this.application.items.getDisplayableNotes()
expect(displayedNotes).to.deep.equal(matches)
expect(matches.length).to.equal(1)
expect(matches[0].uuid).to.equal(unpinnedNote.uuid)
})
it('"text.length", ">", 500', async function () {
const longNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'A',
text: Array(501).fill(0).join(''),
}),
)
await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'B',
text: 'b',
}),
)
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
title: 'Long',
predicate: {
keypath: 'text.length',
operator: '>',
value: 500,
},
}),
)
const matches = this.application.items.notesMatchingSmartView(view)
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'asc',
views: [view],
})
const displayedNotes = this.application.items.getDisplayableNotes()
expect(displayedNotes).to.deep.equal(matches)
expect(matches.length).to.equal(1)
expect(matches[0].uuid).to.equal(longNote.uuid)
})
it('"updated_at", ">", "1.days.ago"', async function () {
await Factory.registerUserToApplication({
application: this.application,
email: Utils.generateUuid(),
password: Utils.generateUuid(),
})
const recentNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
await this.application.sync.sync()
const olderNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'B',
text: 'b',
}),
)
const threeDays = 3 * 24 * 60 * 60 * 1000
await Factory.changePayloadUpdatedAt(this.application, olderNote.payload, new Date(Date.now() - threeDays))
/** Create an unsynced note which shouldn't get an updated_at */
await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'B',
text: 'b',
}),
)
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
title: 'One day ago',
predicate: {
keypath: 'serverUpdatedAt',
operator: '>',
value: '1.days.ago',
},
}),
)
const matches = this.application.items.notesMatchingSmartView(view)
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'asc',
views: [view],
})
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, {
title: 'A',
}),
)
const taggedNote = await Factory.createMappedNote(this.application)
const tag = await this.application.mutator.findOrCreateTag('A')
await this.application.mutator.changeItem(tag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(taggedNote)
})
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
title: 'Untagged',
predicate: {
keypath: 'tags.length',
operator: '=',
value: 0,
},
}),
)
const matches = this.application.items.notesMatchingSmartView(view)
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'asc',
views: [view],
})
const displayedNotes = this.application.items.getDisplayableNotes()
expect(displayedNotes).to.deep.equal(matches)
expect(matches.length).to.equal(1)
expect(matches[0].uuid).to.equal(untaggedNote.uuid)
})
it('"tags", "includes", ["title", "startsWith", "b"]', async function () {
const taggedNote = await Factory.createMappedNote(this.application)
const tag = await this.application.mutator.findOrCreateTag('B')
await this.application.mutator.changeItem(tag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(taggedNote)
})
await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
title: 'B-tags',
predicate: {
keypath: 'tags',
operator: 'includes',
value: { keypath: 'title', operator: 'startsWith', value: 'B' },
},
}),
)
const matches = this.application.items.notesMatchingSmartView(view)
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'asc',
views: [view],
})
const displayedNotes = this.application.items.getDisplayableNotes()
expect(displayedNotes).to.deep.equal(matches)
expect(matches.length).to.equal(1)
expect(matches[0].uuid).to.equal(taggedNote.uuid)
})
it('"ignored", "and", [["pinned", "=", true], ["locked", "=", true]]', async function () {
const pinnedAndLockedNote = await Factory.createMappedNote(this.application)
await this.application.mutator.changeItem(pinnedAndLockedNote, (mutator) => {
mutator.pinned = true
mutator.locked = true
})
const pinnedNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
await this.application.mutator.changeItem(pinnedNote, (mutator) => {
mutator.pinned = true
})
const lockedNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
await this.application.mutator.changeItem(lockedNote, (mutator) => {
mutator.locked = true
})
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
title: 'Pinned & Locked',
predicate: {
operator: 'and',
value: [
{ keypath: 'pinned', operator: '=', value: true },
{ keypath: 'locked', operator: '=', value: true },
],
},
}),
)
const matches = this.application.items.notesMatchingSmartView(view)
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'asc',
views: [view],
})
const displayedNotes = this.application.items.getDisplayableNotes()
expect(displayedNotes).to.deep.equal(matches)
expect(matches.length).to.equal(1)
expect(matches[0].uuid).to.equal(pinnedAndLockedNote.uuid)
})
it('"ignored", "or", [["content.protected", "=", true], ["pinned", "=", true]]', async function () {
const protectedNote = await Factory.createMappedNote(this.application)
await this.application.mutator.changeItem(protectedNote, (mutator) => {
mutator.protected = true
})
const pinnedNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
await this.application.mutator.changeItem(pinnedNote, (mutator) => {
mutator.pinned = true
})
const pinnedAndProtectedNote = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
await this.application.mutator.changeItem(pinnedAndProtectedNote, (mutator) => {
mutator.pinned = true
mutator.protected = true
})
await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.Note, {
title: 'A',
}),
)
const view = await this.application.mutator.insertItem(
await this.application.mutator.createTemplateItem(ContentType.SmartView, {
title: 'Protected or Pinned',
predicate: {
operator: 'or',
value: [
{ keypath: 'content.protected', operator: '=', value: true },
{ keypath: 'pinned', operator: '=', value: true },
],
},
}),
)
const matches = this.application.items.notesMatchingSmartView(view)
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'created_at',
sortDirection: 'asc',
views: [view],
})
const displayedNotes = this.application.items.getDisplayableNotes()
expect(displayedNotes.length).to.equal(matches.length)
expect(matches.length).to.equal(3)
expect(matches.find((note) => note.uuid === protectedNote.uuid)).to.exist
expect(matches.find((note) => note.uuid === pinnedNote.uuid)).to.exist
expect(matches.find((note) => note.uuid === pinnedAndProtectedNote.uuid)).to.exist
})
})
it('include notes that have tag titles that match search query', async function () {
const [notePayload1, tagPayload1] = createRelatedNoteTagPairPayload({
noteTitle: 'A simple note',
noteText: 'This is just a note.',
tagTitle: 'Test',
})
const notePayload2 = Factory.createNotePayload('Foo')
const notePayload3 = Factory.createNotePayload('Bar')
const notePayload4 = Factory.createNotePayload('Testing')
await this.application.itemManager.emitItemsFromPayloads(
[notePayload1, notePayload2, notePayload3, notePayload4, tagPayload1],
PayloadEmitSource.LocalChanged,
)
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'dsc',
searchQuery: {
query: 'Test',
},
})
const displayedNotes = this.application.items.getDisplayableNotes()
expect(displayedNotes.length).to.equal(2)
/** setPrimaryItemDisplayOptions inverses sort for title */
expect(displayedNotes[0].uuid).to.equal(notePayload1.uuid)
expect(displayedNotes[1].uuid).to.equal(notePayload4.uuid)
})
it('search query should be case insensitive and match notes and tags title', async function () {
const [notePayload1, tagPayload1] = createRelatedNoteTagPairPayload({
noteTitle: 'A simple note',
noteText: 'Just a note. Nothing to see.',
tagTitle: 'Foo',
})
const notePayload2 = Factory.createNotePayload('Another bar (foo)')
const notePayload3 = Factory.createNotePayload('Testing FOO (Bar)')
const notePayload4 = Factory.createNotePayload('This should not match')
await this.application.itemManager.emitItemsFromPayloads(
[notePayload1, notePayload2, notePayload3, notePayload4, tagPayload1],
PayloadEmitSource.LocalChanged,
)
this.application.items.setPrimaryItemDisplayOptions({
sortBy: 'title',
sortDirection: 'dsc',
searchQuery: {
query: 'foo',
},
})
const displayedNotes = this.application.items.getDisplayableNotes()
expect(displayedNotes.length).to.equal(3)
/** setPrimaryItemDisplayOptions inverses sort for title */
expect(displayedNotes[0].uuid).to.equal(notePayload1.uuid)
expect(displayedNotes[1].uuid).to.equal(notePayload2.uuid)
expect(displayedNotes[2].uuid).to.equal(notePayload3.uuid)
})
})

View File

@@ -0,0 +1,86 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('tags as folders', () => {
beforeEach(async function () {
this.application = await Factory.createInitAppWithFakeCrypto()
})
afterEach(async function () {
await Factory.safeDeinit(this.application)
})
it('lets me create a tag, add relationships, move a note to a children, and query data all along', async function () {
// ## The user creates four tags
let tagChildren = await Factory.createMappedTag(this.application, {
title: 'children',
})
let tagParent = await Factory.createMappedTag(this.application, {
title: 'parent',
})
let tagGrandParent = await Factory.createMappedTag(this.application, {
title: 'grandparent',
})
let tagGrandParent2 = await Factory.createMappedTag(this.application, {
title: 'grandparent2',
})
// ## Now the users moves the tag children into the parent
await this.application.mutator.setTagParent(tagParent, tagChildren)
expect(this.application.items.getTagParent(tagChildren)).to.equal(tagParent)
expect(Uuids(this.application.items.getTagChildren(tagParent))).deep.to.equal(Uuids([tagChildren]))
// ## Now the user moves the tag parent into the grand parent
await this.application.mutator.setTagParent(tagGrandParent, tagParent)
expect(this.application.items.getTagParent(tagParent)).to.equal(tagGrandParent)
expect(Uuids(this.application.items.getTagChildren(tagGrandParent))).deep.to.equal(Uuids([tagParent]))
// ## Now the user moves the tag parent into another grand parent
await this.application.mutator.setTagParent(tagGrandParent2, tagParent)
expect(this.application.items.getTagParent(tagParent)).to.equal(tagGrandParent2)
expect(this.application.items.getTagChildren(tagGrandParent)).deep.to.equal([])
expect(Uuids(this.application.items.getTagChildren(tagGrandParent2))).deep.to.equal(Uuids([tagParent]))
// ## Now the user tries to move the tag into one of its children
await expect(this.application.mutator.setTagParent(tagChildren, tagParent)).to.eventually.be.rejected
expect(this.application.items.getTagParent(tagParent)).to.equal(tagGrandParent2)
expect(this.application.items.getTagChildren(tagGrandParent)).deep.to.equal([])
expect(Uuids(this.application.items.getTagChildren(tagGrandParent2))).deep.to.equal(Uuids([tagParent]))
// ## Now the user move the tag outside any hierarchy
await this.application.mutator.unsetTagParent(tagParent)
expect(this.application.items.getTagParent(tagParent)).to.equal(undefined)
expect(this.application.items.getTagChildren(tagGrandParent2)).deep.to.equals([])
})
it('lets me add a note to a tag hierarchy', async function () {
// ## The user creates four tags hierarchy
const tags = await Factory.createTags(this.application, {
grandparent: { parent: { child: true } },
another: true,
})
const note1 = await Factory.createMappedNote(this.application, 'my first note')
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)
// ## The note has been added to other tags
const note1Tags = await this.application.items.getSortedTagsForNote(note1)
const note2Tags = await this.application.items.getSortedTagsForNote(note2)
expect(note1Tags.length).to.equal(3)
expect(note2Tags.length).to.equal(1)
})
})

View File

@@ -0,0 +1,140 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('mapping performance', () => {
it('shouldnt take a long time', async () => {
/*
There was an issue with mapping where we were using arrays for everything instead of hashes (like items, missedReferences),
which caused searching to be really expensive and caused a huge slowdown.
*/
const application = await Factory.createInitAppWithFakeCrypto()
// create a bunch of notes and tags, and make sure mapping doesn't take a long time
const noteCount = 1500
const tagCount = 10
const tags = []
const notes = []
for (let i = 0; i < tagCount; i++) {
var tag = {
uuid: UuidGenerator.GenerateUuid(),
content_type: ContentType.Tag,
content: {
title: `${Math.random()}`,
references: [],
},
}
tags.push(tag)
}
for (let i = 0; i < noteCount; i++) {
const note = {
uuid: UuidGenerator.GenerateUuid(),
content_type: ContentType.Note,
content: {
title: `${Math.random()}`,
text: `${Math.random()}`,
references: [],
},
}
const randomTag = Factory.randomArrayValue(tags)
randomTag.content.references.push({
content_type: ContentType.Note,
uuid: note.uuid,
})
notes.push(note)
}
const payloads = Factory.shuffleArray(tags.concat(notes)).map((item) => {
return new DecryptedPayload(item)
})
const t0 = performance.now()
// process items in separate batches, so as to trigger missed references
let currentIndex = 0
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)
currentIndex += batchSize
}
const t1 = performance.now()
const seconds = (t1 - t0) / 1000
const expectedRunTime = 3 // seconds
expect(seconds).to.be.at.most(expectedRunTime)
for (const note of application.itemManager.getItems(ContentType.Note)) {
expect(application.itemManager.itemsReferencingItem(note).length).to.be.above(0)
}
await Factory.safeDeinit(application)
}).timeout(20000)
it('mapping a tag with thousands of notes should be quick', async () => {
/*
There was an issue where if you have a tag with thousands of notes, it will take minutes to resolve.
Fixed now. The issue was that we were looping around too much. I've consolidated some of the loops
so that things require less loops in payloadManager, regarding missedReferences.
*/
const application = await Factory.createInitAppWithFakeCrypto()
const noteCount = 10000
const notes = []
const tag = {
uuid: UuidGenerator.GenerateUuid(),
content_type: ContentType.Tag,
content: {
title: `${Math.random()}`,
references: [],
},
}
for (let i = 0; i < noteCount; i++) {
const note = {
uuid: UuidGenerator.GenerateUuid(),
content_type: ContentType.Note,
content: {
title: `${Math.random()}`,
text: `${Math.random()}`,
references: [],
},
}
tag.content.references.push({
content_type: ContentType.Note,
uuid: note.uuid,
})
notes.push(note)
}
const payloads = [tag].concat(notes).map((item) => new DecryptedPayload(item))
const t0 = performance.now()
// process items in separate batches, so as to trigger missed references
let currentIndex = 0
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)
currentIndex += batchSize
}
const t1 = performance.now()
const seconds = (t1 - t0) / 1000
/** Expected run time depends on many different factors,
* like how many other tests you're running and overall system capacity.
* Locally, best case should be around 3.3s and worst case should be 5s.
* However on CI this can sometimes take up to 10s.
*/
const MAX_RUN_TIME = 15.0 // seconds
expect(seconds).to.be.at.most(MAX_RUN_TIME)
application.itemManager.getItems(ContentType.Tag)[0]
for (const note of application.itemManager.getItems(ContentType.Note)) {
expect(application.itemManager.itemsReferencingItem(note).length).to.equal(1)
}
await Factory.safeDeinit(application)
}).timeout(20000)
})