feat: add snjs package
This commit is contained in:
374
packages/snjs/mocha/model_tests/appmodels.test.js
Normal file
374
packages/snjs/mocha/model_tests/appmodels.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
871
packages/snjs/mocha/model_tests/importing.test.js
Normal file
871
packages/snjs/mocha/model_tests/importing.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
220
packages/snjs/mocha/model_tests/items.test.js
Normal file
220
packages/snjs/mocha/model_tests/items.test.js
Normal 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')
|
||||
})
|
||||
})
|
||||
127
packages/snjs/mocha/model_tests/mapping.test.js
Normal file
127
packages/snjs/mocha/model_tests/mapping.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
75
packages/snjs/mocha/model_tests/notes_smart_tags.test.js
Normal file
75
packages/snjs/mocha/model_tests/notes_smart_tags.test.js
Normal 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'])
|
||||
})
|
||||
})
|
||||
846
packages/snjs/mocha/model_tests/notes_tags.test.js
Normal file
846
packages/snjs/mocha/model_tests/notes_tags.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
86
packages/snjs/mocha/model_tests/notes_tags_folders.test.js
Normal file
86
packages/snjs/mocha/model_tests/notes_tags_folders.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
140
packages/snjs/mocha/model_tests/performance.test.js
Normal file
140
packages/snjs/mocha/model_tests/performance.test.js
Normal 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)
|
||||
})
|
||||
Reference in New Issue
Block a user