2017-08-15 13:54:22 +02:00
|
|
|
// @flow
|
2019-09-13 13:49:11 +02:00
|
|
|
import o from "ospec"
|
2018-10-12 10:50:17 +02:00
|
|
|
import type {BrowserData} from "../../src/misc/ClientConstants"
|
|
|
|
|
import type {Db} from "../../src/api/worker/search/SearchTypes"
|
|
|
|
|
import {aes256RandomKey} from "../../src/api/worker/crypto/Aes"
|
|
|
|
|
import {IndexerCore} from "../../src/api/worker/search/IndexerCore"
|
|
|
|
|
import {EventQueue} from "../../src/api/worker/search/EventQueue"
|
|
|
|
|
import {DbTransaction} from "../../src/api/worker/search/DbFacade"
|
2021-03-25 17:12:17 +01:00
|
|
|
import {fixedIv, uint8ArrayToKey} from "../../src/api/worker/crypto/CryptoUtils"
|
|
|
|
|
import {assertNotNull, downcast, neverNull} from "../../src/api/common/utils/Utils"
|
|
|
|
|
import type {DeviceKeyProvider} from "../../src/desktop/DeviceKeyProviderImpl"
|
2021-06-22 17:36:32 +02:00
|
|
|
import {delay} from "../../src/api/common/utils/PromiseUtils"
|
2017-08-15 13:54:22 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mocks an attribute (function or object) on an object and makes sure that it can be restored to the original attribute by calling unmockAttribute() later.
|
|
|
|
|
* Additionally creates a spy for the attribute if the attribute is a function.
|
|
|
|
|
* @param object The object on which the attribute exists.
|
|
|
|
|
* @param attributeOnObject The attribute to mock.
|
|
|
|
|
* @param attributeMock The attribute mock.
|
|
|
|
|
* @returns An object to be passed to unmockAttribute() in order to restore the original attribute.
|
|
|
|
|
*/
|
2018-09-21 11:17:16 +02:00
|
|
|
export function mockAttribute(object: Object, attributeOnObject: Function | Object, attributeMock: Function | Object): Object {
|
2017-08-15 13:54:22 +02:00
|
|
|
if (attributeOnObject == null) throw new Error("attributeOnObject is undefined")
|
|
|
|
|
let attributeName = Object.getOwnPropertyNames(object).find(key => object[key] === attributeOnObject)
|
|
|
|
|
if (!attributeName) {
|
2018-09-21 11:17:16 +02:00
|
|
|
attributeName = Object.getOwnPropertyNames(Object.getPrototypeOf(object))
|
|
|
|
|
.find(key => object[key] === attributeOnObject)
|
2017-08-15 13:54:22 +02:00
|
|
|
}
|
|
|
|
|
if (!attributeName) {
|
|
|
|
|
throw new Error("attribute not found on object")
|
|
|
|
|
}
|
|
|
|
|
object[attributeName] = (typeof attributeOnObject == "function") ? o.spy(attributeMock) : attributeMock
|
|
|
|
|
return {
|
|
|
|
|
_originalObject: object,
|
|
|
|
|
_originalAttribute: attributeOnObject,
|
|
|
|
|
_attributeName: attributeName
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function unmockAttribute(mock: Object) {
|
|
|
|
|
mock._originalObject[mock._attributeName] = mock._originalAttribute
|
2018-09-21 11:17:16 +02:00
|
|
|
}
|
|
|
|
|
|
2018-10-22 12:02:13 +02:00
|
|
|
export type Spy = ((...any) => any) & {invocations: any[]}
|
|
|
|
|
|
|
|
|
|
export function spy(producer?: (...any) => any): Spy {
|
2018-09-21 11:17:16 +02:00
|
|
|
const invocations = []
|
|
|
|
|
const s = (...args: any[]) => {
|
|
|
|
|
invocations.push(args)
|
|
|
|
|
return producer && producer(...args)
|
|
|
|
|
}
|
|
|
|
|
s.invocations = invocations
|
|
|
|
|
return s
|
2018-10-01 13:29:32 +02:00
|
|
|
}
|
|
|
|
|
|
2018-11-14 13:15:55 +01:00
|
|
|
/**
|
|
|
|
|
* Create partial mock, i.e. allows mocking attributes or functions on actual instances
|
|
|
|
|
* @param obj The base mock object on which mocker may overwrite attributes or functions
|
|
|
|
|
* @param mocker This function receives obj and can overwrite attributes or functions.
|
|
|
|
|
* @returns {T}
|
|
|
|
|
*/
|
2018-10-01 13:29:32 +02:00
|
|
|
export const mock = <T>(obj: T, mocker: any => any): T => {
|
|
|
|
|
mocker(obj)
|
|
|
|
|
return obj
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function mapToObject<K, V>(map: Map<K, V>): {[K]: V} {
|
|
|
|
|
const obj: {[K]: V} = {}
|
|
|
|
|
map.forEach((value, key) => {
|
|
|
|
|
obj[key] = value
|
|
|
|
|
})
|
|
|
|
|
return obj
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function mapObject<K, V, R>(mapper: (V) => R, obj: {[K]: V}): {[K]: R} {
|
|
|
|
|
const newObj = {}
|
|
|
|
|
for (let key of Object.keys(obj)) {
|
|
|
|
|
newObj[key] = mapper(obj[key])
|
|
|
|
|
}
|
|
|
|
|
return newObj
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function replaceAllMaps(toReplace: any): any {
|
|
|
|
|
return toReplace instanceof Map
|
|
|
|
|
? replaceAllMaps(mapToObject(toReplace))
|
|
|
|
|
: toReplace instanceof Array
|
|
|
|
|
? toReplace.map(replaceAllMaps)
|
|
|
|
|
: toReplace != null && Object.getPrototypeOf(toReplace) === (Object: any).prototype // plain object
|
|
|
|
|
? mapObject(replaceAllMaps, toReplace)
|
|
|
|
|
: toReplace
|
2018-10-12 10:50:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2019-04-12 16:16:17 +02:00
|
|
|
export const browserDataStub: BrowserData = {needsMicrotaskHack: false, needsExplicitIDBIds: false, indexedDbSupported: true}
|
2018-10-12 10:50:17 +02:00
|
|
|
|
|
|
|
|
export function makeCore(args?: {
|
|
|
|
|
db?: Db,
|
|
|
|
|
queue?: EventQueue,
|
|
|
|
|
browserData?: BrowserData,
|
|
|
|
|
transaction?: DbTransaction
|
|
|
|
|
}, mocker?: (any) => void): IndexerCore {
|
|
|
|
|
const safeArgs = args || {}
|
|
|
|
|
const {transaction = (null: any)} = safeArgs
|
|
|
|
|
const defaultDb = {
|
|
|
|
|
key: aes256RandomKey(),
|
|
|
|
|
iv: fixedIv,
|
2019-10-02 13:59:34 +02:00
|
|
|
dbFacade: ({createTransaction: () => Promise.resolve(transaction)}: any),
|
2018-10-12 10:50:17 +02:00
|
|
|
initialized: Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
const {db = defaultDb, queue = (null: any), browserData = browserDataStub} = safeArgs
|
|
|
|
|
const core = new IndexerCore(db, queue, browserData)
|
|
|
|
|
mocker && mock(core, mocker)
|
|
|
|
|
return core
|
2019-03-12 11:58:31 +01:00
|
|
|
}
|
2020-01-30 18:58:00 +01:00
|
|
|
|
2021-06-24 11:56:16 +02:00
|
|
|
export interface TimeoutMock {
|
|
|
|
|
(fn: () => mixed, time: number): TimeoutID,
|
|
|
|
|
|
|
|
|
|
next(): void
|
|
|
|
|
|
|
|
|
|
}
|
2019-09-13 13:49:11 +02:00
|
|
|
|
|
|
|
|
export function makeTimeoutMock(): TimeoutMock {
|
2020-01-30 18:58:00 +01:00
|
|
|
let timeoutId = 1
|
2021-03-25 17:12:17 +01:00
|
|
|
let scheduledFn
|
2021-06-24 11:56:16 +02:00
|
|
|
const timeoutMock = function (fn: () => mixed) {
|
2021-03-25 17:12:17 +01:00
|
|
|
scheduledFn = fn
|
2020-01-30 18:58:00 +01:00
|
|
|
timeoutId++
|
2019-08-22 18:24:32 +02:00
|
|
|
return downcast(timeoutId)
|
2020-01-30 18:58:00 +01:00
|
|
|
}
|
2021-05-21 16:01:40 +02:00
|
|
|
const spiedMock = o.spy(timeoutMock)
|
2020-01-30 18:58:00 +01:00
|
|
|
|
2021-05-21 16:01:40 +02:00
|
|
|
spiedMock.next = function () {
|
2021-03-25 17:12:17 +01:00
|
|
|
scheduledFn && scheduledFn()
|
2020-01-30 18:58:00 +01:00
|
|
|
}
|
2021-05-21 16:01:40 +02:00
|
|
|
return spiedMock
|
2020-01-30 18:58:00 +01:00
|
|
|
}
|
|
|
|
|
|
2019-08-22 18:24:32 +02:00
|
|
|
/** Catch error and return either value or error */
|
|
|
|
|
export async function asResult<T>(p: Promise<T>): Promise<T | Error> {
|
|
|
|
|
return p.catch((e) => e)
|
2019-09-13 13:49:11 +02:00
|
|
|
}
|
|
|
|
|
|
2021-06-21 17:47:54 +02:00
|
|
|
export async function assertThrows<T: Error>(expected: Class<T>, fn: () => Promise<mixed>): Promise<T> {
|
2019-09-13 13:49:11 +02:00
|
|
|
try {
|
|
|
|
|
await fn()
|
|
|
|
|
} catch (e) {
|
2021-07-08 18:34:52 +02:00
|
|
|
o(e instanceof expected).equals(true)("AssertThrows failed: Expected a " + downcast(expected) + " to be thrown, but got a "
|
|
|
|
|
+ e.constructor)
|
2019-09-13 13:49:11 +02:00
|
|
|
return e
|
|
|
|
|
}
|
2021-06-21 17:47:54 +02:00
|
|
|
throw new Error("AssertThrows failed: Expected a " + downcast(expected) + " to be thrown, but nothing was")
|
2019-09-13 13:49:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function preTest() {
|
|
|
|
|
browser(() => {
|
|
|
|
|
const p = document.createElement("p")
|
|
|
|
|
p.id = "report"
|
|
|
|
|
p.style.fontWeight = "bold"
|
|
|
|
|
p.style.fontSize = "30px"
|
|
|
|
|
p.style.fontFamily = "sans-serif"
|
|
|
|
|
p.textContent = "Running tests..."
|
|
|
|
|
neverNull(document.body).appendChild(p)
|
|
|
|
|
})()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function reportTest(results: mixed, stats: mixed) {
|
|
|
|
|
const errCount = o.report(results, stats)
|
|
|
|
|
if (typeof process != "undefined" && errCount !== 0) process.exit(1) // eslint-disable-line no-process-exit
|
|
|
|
|
browser(() => {
|
|
|
|
|
const p = assertNotNull(document.getElementById("report"))
|
|
|
|
|
// errCount includes bailCount
|
|
|
|
|
p.textContent = errCount === 0 ? "No errors" : `${errCount} error(s) (see console)`
|
|
|
|
|
p.style.color = errCount === 0 ? "green" : "red"
|
|
|
|
|
})()
|
2021-03-25 17:12:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function makeDeviceKeyProvider(uint8ArrayKey: Uint8Array): DeviceKeyProvider {
|
|
|
|
|
return {
|
|
|
|
|
getDeviceKey() {
|
|
|
|
|
return Promise.resolve(uint8ArrayToKey(uint8ArrayKey))
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-06-22 17:36:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function assertResolvedIn(ms: number, ...promises: $ReadOnlyArray<Promise<*>>): Promise<*> {
|
|
|
|
|
const allP = [delay(ms).then(() => "timeout")]
|
|
|
|
|
.concat(promises.map((p, i) => p.then(() => `promise ${i} is resolved`)))
|
|
|
|
|
const result = await Promise.race(allP)
|
|
|
|
|
o(result).notEquals("timeout")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function assertNotResolvedIn(ms: number, ...promises: $ReadOnlyArray<Promise<*>>): Promise<*> {
|
|
|
|
|
const allP = [delay(ms).then(() => "timeout")]
|
|
|
|
|
.concat(promises.map((p, i) => p.then(() => `promise ${i} is resolved`)))
|
|
|
|
|
const result = await Promise.race(allP)
|
|
|
|
|
o(result).equals("timeout")
|
2019-08-22 18:24:32 +02:00
|
|
|
}
|