Files
standardnotes-app-web/packages/snjs/mocha/lib/factory.js
2023-01-18 12:41:57 +01:00

503 lines
16 KiB
JavaScript

/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import FakeWebCrypto from './fake_web_crypto.js'
import { AppContext } from './AppContext.js'
import * as Applications from './Applications.js'
import * as Defaults from './Defaults.js'
import * as Utils from './Utils.js'
import { createItemParams, createNoteParams, createTagParams } from './Items.js'
export const TenSecondTimeout = 10_000
export const TwentySecondTimeout = 20_000
export const ThirtySecondTimeout = 30_000
export const syncOptions = {
checkIntegrity: true,
awaitAll: true,
}
export async function createAndInitSimpleAppContext(
{ registerUser, environment } = {
registerUser: false,
environment: Environment.Web,
},
) {
const application = await createInitAppWithFakeCrypto(environment)
const email = UuidGenerator.GenerateUuid()
const password = UuidGenerator.GenerateUuid()
const newPassword = randomString()
if (registerUser) {
await registerUserToApplication({
application,
email,
password,
})
}
return {
application,
email,
password,
newPassword,
}
}
export async function createAppContextWithFakeCrypto(identifier, email, password) {
return createAppContext({ identifier, crypto: new FakeWebCrypto(), email, password })
}
export async function createAppContextWithRealCrypto(identifier) {
return createAppContext({ identifier, crypto: new SNWebCrypto() })
}
export async function createAppContext({ identifier, crypto, email, password, host } = {}) {
const context = new AppContext({ identifier, crypto, email, password, host })
await context.initialize()
return context
}
export function disableIntegrityAutoHeal(application) {
application.syncService.emitOutOfSyncRemotePayloads = () => {
console.warn('Integrity self-healing is disabled for this test')
}
}
export async function safeDeinit(application) {
return Utils.safeDeinit(application)
}
export function getDefaultHost() {
return Defaults.getDefaultHost()
}
export async function publishMockedEvent(eventType, eventPayload) {
const response = await fetch(`${Defaults.getDefaultMockedEventServiceUrl()}/events`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
eventType,
eventPayload,
}),
})
if (!response.ok) {
console.error(`Failed to publish mocked event: ${response.status} ${response.statusText}`)
}
}
export function createApplicationWithFakeCrypto(identifier, environment, platform, host) {
return Applications.createApplicationWithFakeCrypto(identifier, environment, platform, host)
}
export function createApplicationWithRealCrypto(identifier, environment, platform, host) {
return Applications.createApplicationWithRealCrypto(identifier, environment, platform, host)
}
export async function createAppWithRandNamespace(environment, platform) {
return Applications.createAppWithRandNamespace(environment, platform)
}
export async function createInitAppWithFakeCrypto(environment, platform) {
return Applications.createInitAppWithFakeCrypto(environment, platform)
}
export async function createInitAppWithFakeCryptoWithOptions({ environment, platform, identifier }) {
const application = Applications.createApplicationWithOptions({ identifier, environment, platform })
await Applications.initializeApplication(application)
return application
}
export async function createInitAppWithRealCrypto(environment, platform) {
return Applications.createInitAppWithRealCrypto(environment, platform)
}
export async function createAndInitializeApplication(namespace, environment, platform, host, crypto) {
return Applications.createAndInitializeApplication(namespace, environment, platform, host, crypto)
}
export async function initializeApplication(application) {
return Applications.initializeApplication(application)
}
export function registerUserToApplication({ application, email, password, ephemeral, mergeLocal = true }) {
if (!email) email = Utils.generateUuid()
if (!password) password = Utils.generateUuid()
return application.register(email, password, ephemeral, mergeLocal)
}
export async function setOldVersionPasscode({ application, passcode, version }) {
const identifier = await application.protocolService.crypto.generateUUID()
const operator = application.protocolService.operatorManager.operatorForVersion(version)
const key = await operator.createRootKey(identifier, passcode, KeyParamsOrigination.PasscodeCreate)
await application.protocolService.setNewRootKeyWrapper(key)
await application.userService.rewriteItemsKeys()
await application.syncService.sync(syncOptions)
}
/**
* Using application.register will always use latest version of protocol.
* To use older version, use this method.
*/
export async function registerOldUser({ application, email, password, version }) {
if (!email) email = Utils.generateUuid()
if (!password) password = Utils.generateUuid()
const operator = application.protocolService.operatorManager.operatorForVersion(version)
const accountKey = await operator.createRootKey(email, password, KeyParamsOrigination.Registration)
const response = await application.userApiService.register({
email: email,
serverPassword: accountKey.serverPassword,
keyParams: accountKey.keyParams,
})
/** Mark all existing items as dirty. */
await application.itemManager.changeItems(application.itemManager.items, (m) => {
m.dirty = true
})
await application.sessionManager.handleSuccessAuthResponse(response, accountKey)
application.notifyEvent(ApplicationEvent.SignedIn)
await application.syncService.sync({
mode: SyncMode.DownloadFirst,
...syncOptions,
})
await application.protocolService.decryptErroredPayloads()
}
export function createStorageItemPayload(contentType) {
return new DecryptedPayload(createItemParams(contentType))
}
export function createNotePayload(title, text = undefined, dirty = true) {
return new DecryptedPayload(createNoteParams({ title, text, dirty }))
}
export function createNote(title, text = undefined, dirty = true) {
return new SNNote(new DecryptedPayload(createNoteParams({ title, text, dirty })))
}
export function createStorageItemTagPayload(tagParams = {}) {
return new DecryptedPayload(createTagParams(tagParams))
}
export function itemToStoragePayload(item) {
return new DecryptedPayload(item)
}
export function createMappedNote(application, title, text, dirty = true) {
const payload = createNotePayload(title, text, dirty)
return application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
}
export async function createMappedTag(application, tagParams = {}) {
const payload = createStorageItemTagPayload(tagParams)
return application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
}
export async function createSyncedNote(application, title, text) {
const payload = createNotePayload(title, text)
const item = await application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
await application.itemManager.setItemDirty(item)
await application.syncService.sync(syncOptions)
const note = application.items.findItem(payload.uuid)
return note
}
export async function getStoragePayloadsOfType(application, type) {
const rawPayloads = await application.diskStorageService.getAllRawPayloads()
return rawPayloads
.filter((rp) => rp.content_type === type)
.map((rp) => {
return new CreatePayload(rp)
})
}
export async function createManyMappedNotes(application, count) {
const createdNotes = []
for (let i = 0; i < count; i++) {
const note = await createMappedNote(application)
await application.itemManager.setItemDirty(note)
createdNotes.push(note)
}
return createdNotes
}
export async function loginToApplication({
application,
email,
password,
ephemeral,
strict = false,
mergeLocal = true,
awaitSync = true,
}) {
return application.signIn(email, password, strict, ephemeral, mergeLocal, awaitSync)
}
export async function awaitFunctionInvokation(object, functionName) {
return new Promise((resolve) => {
const original = object[functionName]
object[functionName] = async function () {
const result = original.apply(this, arguments)
resolve(result)
return result
}
})
}
/**
* Signing out of an application deinits it.
* A new one must be created.
*/
export async function signOutApplicationAndReturnNew(application) {
const isRealCrypto = application.crypto instanceof SNWebCrypto
await application.user.signOut()
if (isRealCrypto) {
return createInitAppWithRealCrypto()
} else {
return createInitAppWithFakeCrypto()
}
}
export async function signOutAndBackIn(application, email, password) {
const isRealCrypto = application.crypto instanceof SNWebCrypto
await application.user.signOut()
const newApplication = isRealCrypto ? await createInitAppWithRealCrypto() : await createInitAppWithFakeCrypto()
await this.loginToApplication({
application: newApplication,
email,
password,
})
return newApplication
}
export async function restartApplication(application) {
const id = application.identifier
await safeDeinit(application)
const newApplication = await createAndInitializeApplication(id)
return newApplication
}
export async function storagePayloadCount(application) {
const payloads = await application.diskStorageService.getAllRawPayloads()
return payloads.length
}
/**
* The number of seconds between changes before a server creates a new revision.
* Controlled via docker/syncing-server-js.env
*/
export const ServerRevisionFrequency = 5.1
export const ServerRevisionCreationDelay = 1.5
export function yesterday() {
return new Date(new Date().setDate(new Date().getDate() - 1))
}
export function dateToMicroseconds(date) {
return date.getTime() * 1_000
}
export function tomorrow() {
return new Date(new Date().setDate(new Date().getDate() + 1))
}
export async function sleep(seconds, reason) {
console.log('Sleeping for reason', reason)
return Utils.sleep(seconds)
}
export function shuffleArray(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[a[i], a[j]] = [a[j], a[i]]
}
return a
}
export function randomString(length = 10) {
let result = ''
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength))
}
return result
}
export function generateUuidish() {
return this.randomString(32)
}
export function randomArrayValue(array) {
return array[Math.floor(Math.random() * array.length)]
}
export async function expectThrowsAsync(method, errorMessage) {
let error = null
try {
await method()
} catch (err) {
error = err
}
const expect = chai.expect
expect(error).to.be.an('Error')
if (errorMessage) {
expect(error.message)
.to.be.a('string')
.and.satisfy((msg) => msg.startsWith(errorMessage))
}
}
export function ignoreChallenges(application) {
application.setLaunchCallback({
receiveChallenge() {
/** no-op */
},
})
}
export function handlePasswordChallenges(application, password) {
application.setLaunchCallback({
receiveChallenge: (challenge) => {
const values = challenge.prompts.map((prompt) =>
CreateChallengeValue(
prompt,
prompt.validation === ChallengeValidation.ProtectionSessionDuration
? UnprotectedAccessSecondsDuration.OneMinute
: password,
),
)
application.submitValuesForChallenge(challenge, values)
},
})
}
export async function createTags(application, hierarchy, parent = undefined, resultAccumulator = undefined) {
const result = resultAccumulator || {}
const promises = Object.entries(hierarchy).map(async ([key, value]) => {
let tag = await application.mutator.findOrCreateTag(key)
result[key] = tag
if (parent) {
await application.mutator.setTagParent(parent, tag)
}
if (value === true) {
return
}
await createTags(application, value, tag, result)
})
await Promise.all(promises)
return result
}
export function pinNote(application, note) {
return application.mutator.changeItem(note, (mutator) => {
mutator.pinned = true
})
}
export async function insertItemWithOverride(application, contentType, content, needsSync = false, errorDecrypting) {
const item = await application.itemManager.createItem(contentType, content, needsSync)
if (errorDecrypting) {
const encrypted = new EncryptedPayload({
...item.payload.ejected(),
content: '004:...',
errorDecrypting,
})
await application.itemManager.emitItemFromPayload(encrypted)
} else {
const decrypted = new DecryptedPayload({
...item.payload.ejected(),
})
await application.itemManager.emitItemFromPayload(decrypted)
}
return application.itemManager.findAnyItem(item.uuid)
}
export async function alternateUuidForItem(application, uuid) {
const item = application.itemManager.findItem(uuid)
const payload = new DecryptedPayload(item)
const results = await PayloadsByAlternatingUuid(payload, application.payloadManager.getMasterCollection())
await application.payloadManager.emitPayloads(results, PayloadEmitSource.LocalChanged)
await application.syncService.persistPayloads(results)
return application.itemManager.findItem(results[0].uuid)
}
export async function markDirtyAndSyncItem(application, itemToLookupUuidFor) {
const item = application.itemManager.findItem(itemToLookupUuidFor.uuid)
if (!item) {
throw Error('Attempting to save non-inserted item')
}
if (!item.dirty) {
await application.itemManager.changeItem(item, undefined, MutationType.NoUpdateUserTimestamps)
}
await application.sync.sync()
}
export async function changePayloadTimeStampAndSync(application, payload, timestamp, contentOverride, syncOptions) {
await changePayloadTimeStamp(application, payload, timestamp, contentOverride)
await application.sync.sync(syncOptions)
return application.itemManager.findAnyItem(payload.uuid)
}
export async function changePayloadTimeStamp(application, payload, timestamp, contentOverride) {
payload = application.payloadManager.collection.find(payload.uuid)
const changedPayload = new DecryptedPayload({
...payload,
dirty: true,
dirtyIndex: getIncrementedDirtyIndex(),
content: {
...payload.content,
...contentOverride,
},
updated_at_timestamp: timestamp,
})
await application.itemManager.emitItemFromPayload(changedPayload)
return application.itemManager.findAnyItem(payload.uuid)
}
export async function changePayloadUpdatedAt(application, payload, updatedAt) {
const latestPayload = application.payloadManager.collection.find(payload.uuid)
const changedPayload = new DecryptedPayload({
...latestPayload,
dirty: true,
dirtyIndex: getIncrementedDirtyIndex(),
updated_at: updatedAt,
})
await application.itemManager.emitItemFromPayload(changedPayload)
return application.itemManager.findAnyItem(payload.uuid)
}
export async function changePayloadTimeStampDeleteAndSync(application, payload, timestamp, syncOptions) {
payload = application.payloadManager.collection.find(payload.uuid)
const changedPayload = new DeletedPayload({
...payload,
content: undefined,
dirty: true,
dirtyIndex: getIncrementedDirtyIndex(),
deleted: true,
updated_at_timestamp: timestamp,
})
await application.itemManager.emitItemFromPayload(changedPayload)
await application.sync.sync(syncOptions)
}