tutanota/test/api/rest/EntityRestClientTest.js

327 lines
14 KiB
JavaScript
Raw Normal View History

// @flow
import o from "ospec"
import {createContact} from "../../../src/api/entities/tutanota/Contact"
import {BadRequestError, InternalServerError, PayloadTooLargeError} from "../../../src/api/common/error/RestError"
import {assertThrows} from "@tutao/tutanota-test-utils"
import {SetupMultipleError} from "../../../src/api/common/error/SetupMultipleError"
import {downcast, TypeRef} from "@tutao/tutanota-utils"
import type {HttpMethodEnum, MediaTypeEnum} from "../../../src/api/common/EntityFunctions"
import {HttpMethod, resolveTypeReference} from "../../../src/api/common/EntityFunctions"
import {CustomerTypeRef} from "../../../src/api/entities/sys/Customer"
import {EntityRestClient} from "../../../src/api/worker/rest/EntityRestClient"
import {RestClient} from "../../../src/api/worker/rest/RestClient"
import type {CryptoFacade} from "../../../src/api/worker/crypto/CryptoFacade"
import type {TypeModel} from "../../../src/api/common/EntityTypes"
import {createInternalRecipientKeyData} from "../../../src/api/entities/tutanota/InternalRecipientKeyData"
import {create} from "../../../src/api/common/utils/EntityUtils"
import {InstanceMapper} from "../../../src/api/worker/crypto/InstanceMapper"
import {CalendarEventTypeRef} from "../../../src/api/entities/tutanota/CalendarEvent"
// TODO
// - Test all methods: load, loadRange, update, delete
// - Test more error cases and invariants
// - Invalid entities (e.g. no owner group set)
// - No auth headers set
const cryptoFacadeMock: CryptoFacade = {
applyMigrations: async <T>(typeRef: TypeRef<T>, data: any): Promise<T> => resolveTypeReference(typeRef).then(model => create(model, typeRef)),
applyMigrationsForInstance: async <T>(decryptedInstance: T): Promise<T> => decryptedInstance,
setNewOwnerEncSessionKey: (model: TypeModel, entity: Object) => [],
resolveServiceSessionKey: async (typeModel: TypeModel, instance: Object) => [],
encryptBucketKeyForInternalRecipient: async (bucketKey: Aes128Key, recipientMailAddress: string, notFoundRecipients: Array<string>) => createInternalRecipientKeyData(),
resolveSessionKey: async (typeModel: TypeModel, instance: Object) => [],
}
const instanceMapperMock: InstanceMapper = downcast({
async encryptAndMapToLiteral(model, instance, sk) {
return {
dummyMessage: "encrypted"
}
},
async decryptAndMapToInstance(model, instance, sk) {
return {
dummyMessage: "decrypted"
}
}
})
function createEntityRestClientWithMocks(requestMock: Function): {entityRestClient: EntityRestClient, requestSpy: OspecSpy<typeof RestClient.prototype.request>} {
const requestSpy = o.spy(requestMock)
const entityRestClient = new EntityRestClient(
() => ({accessToken: "My cool access token"}), // Entity rest client doesn't allow requests without authorization
downcast<RestClient>({request: async (...args) => requestSpy(...args)}),
() => cryptoFacadeMock,
instanceMapperMock,
)
return {
entityRestClient,
requestSpy
}
}
function createArrayOf<T>(count: number, factory: (index: number) => T): Array<T> {
return Array(count).fill().map((_, idx) => factory(idx))
}
const range = (start, count) => createArrayOf(count, idx => String(idx + start))
function contacts(count) {
const contactFactory = idx => createContact({
firstName: `Contact${idx}`
})
return createArrayOf(count, contactFactory)
}
o.spec("EntityRestClient", async function () {
o.spec("Load Range", function () {
o("Loads multiple entities in a single request", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => JSON.stringify(["The elements that were returned from the server"])
)
const ids = range(0, 5)
const result = await entityRestClient.loadRange(CalendarEventTypeRef, "listId", ids[2], 5, false)
o(requestSpy.callCount).equals(1)
o(requestSpy.args[1]).equals(HttpMethod.GET)("Method is GET")
o(requestSpy.args[2]).deepEquals({start: ids[2], count: "5", reverse: "false"})("Range is passed in as query params")
o(result).deepEquals([{dummyMessage: "decrypted"}])("decrypts and returns from the client")
})
})
o.spec("Load multiple", function () {
o("Less than 100 entities requested should result in a single rest request", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => JSON.stringify(["The elements that were returned from the server"])
)
const ids = range(0, 5)
const result = await entityRestClient.loadMultiple(CustomerTypeRef, null, ids)
o(requestSpy.callCount).equals(1)
o(requestSpy.args[1]).equals(HttpMethod.GET)("Method is GET")
o(requestSpy.args[2]).deepEquals({ids: ids.join(",")})("Requested IDs are passed in as query params")
o(result).deepEquals([{dummyMessage: "decrypted"}])("decrypts and returns from the client")
})
o("Exactly 100 entities requested should result in a single rest request", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => JSON.stringify(["The elements that were returned from the server"])
)
const ids = range(0, 100)
const result = await entityRestClient.loadMultiple(CustomerTypeRef, null, ids)
o(requestSpy.callCount).equals(1)
o(requestSpy.args[1]).equals(HttpMethod.GET)("Method is GET")
o(requestSpy.args[2]).deepEquals({ids: ids.join(",")})("Requested IDs are passed in as query params")
o(result).deepEquals([{dummyMessage: "decrypted"}])("Returns what was returned by the rest client")
})
o("More than 100 entities requested results in 2 rest requests", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => JSON.stringify(["entities"])
)
const ids = range(0, 101)
const result = await entityRestClient.loadMultiple(CustomerTypeRef, null, ids)
o(requestSpy.callCount).equals(2)
o(requestSpy.calls[0].args[1]).equals(HttpMethod.GET)("Method is GET")
o(requestSpy.calls[0].args[2]).deepEquals({ids: ids.slice(0, 100).join(",")})("The first 100 ids are requested")
o(requestSpy.calls[1].args[1]).equals(HttpMethod.GET)("Method is GET")
o(requestSpy.calls[1].args[2]).deepEquals({ids: ids.slice(100, 101).join(",")})("The remaining 1 id is requested")
o(result).deepEquals([
{dummyMessage: "decrypted"}, {dummyMessage: "decrypted"}
])("Returns what was returned by the rest client")
})
o("More than 200 entities requested results in 3 rest requests", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => JSON.stringify(["entities"])
)
const ids = range(0, 211)
const result = await entityRestClient.loadMultiple(CustomerTypeRef, null, ids)
o(requestSpy.callCount).equals(3)
o(requestSpy.calls[0].args[1]).equals(HttpMethod.GET)("Method is GET")
o(requestSpy.calls[0].args[2]).deepEquals({ids: ids.slice(0, 100).join(",")})("The first 100 ids are requested")
o(requestSpy.calls[1].args[1]).equals(HttpMethod.GET)("Method is GET")
o(requestSpy.calls[1].args[2]).deepEquals({ids: ids.slice(100, 200).join(",")})("The next 100 ids are requested")
o(requestSpy.calls[2].args[1]).equals(HttpMethod.GET)("Method is GET")
o(requestSpy.calls[2].args[2]).deepEquals({ids: ids.slice(200, 211).join(",")})("The remaining 11 ids are requested")
o(result).deepEquals([
{dummyMessage: "decrypted"}, {dummyMessage: "decrypted"}, {dummyMessage: "decrypted"}
])("Returns what was returned by the rest client")
})
})
o.spec("Setup multiple", async function () {
o("Less than 100 entities created should result in a single rest request", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => JSON.stringify([{generatedId: "someReturnedId"}])
)
const newContacts = contacts(1)
const result = await entityRestClient.setupMultiple("listId", newContacts)
o(result).deepEquals(["someReturnedId"])
o(requestSpy.callCount).equals(1)
o(requestSpy.args[1]).equals(HttpMethod.POST)("The method is POST")
o(requestSpy.args[4]).deepEquals(JSON.stringify(newContacts.map(() => ({dummyMessage: "encrypted"}))))("All contacts were sent")
})
o("Exactly 100 entities created should result in a single rest request", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => JSON.stringify([{generatedId: "someReturnedId"}])
)
const newContacts = contacts(100)
const result = await entityRestClient.setupMultiple("listId", newContacts)
o(result).deepEquals(["someReturnedId"])
o(requestSpy.callCount).equals(1)
o(requestSpy.args[1]).equals(HttpMethod.POST)("The method is POST")
o(requestSpy.args[4]).deepEquals(JSON.stringify(newContacts.map(() => ({dummyMessage: "encrypted"}))))("All contacts were sent")
})
o("More than 100 entities created should result in 2 rest requests", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => JSON.stringify([{generatedId: "someReturnedId"}])
)
const newContacts = contacts(101)
const result = await entityRestClient.setupMultiple("listId", newContacts)
o(result).deepEquals(["someReturnedId", "someReturnedId"])
o(requestSpy.callCount).equals(2)
o(requestSpy.calls[0].args[1]).equals(HttpMethod.POST)("The method is POST")
o(requestSpy.calls[0].args[4]).deepEquals(JSON.stringify(newContacts.map(() => ({dummyMessage: "encrypted"})).slice(0, 100)))("First 100 contacts were sent")
o(requestSpy.calls[1].args[1]).equals(HttpMethod.POST)("The method is POST")
o(requestSpy.calls[1].args[4]).deepEquals(JSON.stringify(newContacts.map(() => ({dummyMessage: "encrypted"})).slice(100, 101)))("Remaining contact was sent")
})
o("More than 200 entities created should result in 3 rest requests", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => JSON.stringify([{generatedId: "someReturnedId"}])
)
const newContacts = contacts(211)
const result = await entityRestClient.setupMultiple("listId", newContacts)
o(result).deepEquals(["someReturnedId", "someReturnedId", "someReturnedId"])
o(requestSpy.callCount).equals(3)
o(requestSpy.calls[0].args[1]).equals(HttpMethod.POST)("The method is POST")
o(requestSpy.calls[0].args[4]).deepEquals(JSON.stringify(newContacts.map(() => ({dummyMessage: "encrypted"})).slice(0, 100)))("First 100 contacts were sent")
o(requestSpy.calls[1].args[1]).equals(HttpMethod.POST)("The method is POST")
o(requestSpy.calls[1].args[4]).deepEquals(JSON.stringify(newContacts.map(() => ({dummyMessage: "encrypted"})).slice(100, 200)))("Next 100 contacts were sent")
o(requestSpy.calls[2].args[1]).equals(HttpMethod.POST)("The method is POST")
o(requestSpy.calls[2].args[4]).deepEquals(JSON.stringify(newContacts.map(() => ({dummyMessage: "encrypted"})).slice(200, 211)))("Remaining 11 contacts were sent")
})
o("A single request is made and an error occurs, all entites should be returned as failedInstances", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => { throw new BadRequestError("canny do et") }
)
const newContacts = contacts(100)
const result = await assertThrows(SetupMultipleError, () => entityRestClient.setupMultiple("listId", newContacts))
o(requestSpy.callCount).equals(1)
o(result.failedInstances.length).equals(newContacts.length)
o(result.errors.length).equals(1)
o(result.errors[0] instanceof BadRequestError).equals(true)
o(result.failedInstances).deepEquals(newContacts)
})
o("Post multiple: An error is encountered for part of the request, only failed entities are returned in the result", async function () {
let requestCounter = 0
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => {
requestCounter += 1
if (requestCounter % 2 === 0) {
// Second and Fourth requests are success
return JSON.stringify(range(0, 100))
} else {
// First and Third requests are failure
throw new BadRequestError("It was a bad request")
}
}
)
const newContacts = contacts(400)
const result = await assertThrows(SetupMultipleError,
() => entityRestClient.setupMultiple("listId", newContacts)
)
o(requestSpy.callCount).equals(4)
o(result.failedInstances).deepEquals(newContacts.slice(0, 100).concat(newContacts.slice(200, 300)))
o(result.errors.length).equals(2)
o(result.errors.every(e => e instanceof BadRequestError)).equals(true)
})
o("Post multiple: When a PayloadTooLarge error occurs individual instances are posted", async function () {
const listId = "listId"
const idArray = ["0", null, "2"] // GET fails for id 1
let instances = []
for (let i = 0; i < idArray.length; i++) {
instances.push(createContact())
}
let step = 0
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
function (path: string,
method: HttpMethodEnum,
queryParams: Params,
headers: Params,
body: ?string,
responseType: ?MediaTypeEnum,
progressListener: ?ProgressListener,
baseUrl?: string) {
//post multiple - body is an array
if (body && body.startsWith("[")) {
throw new PayloadTooLargeError("test")
//post single
} else if (step === 1) {
step += 1
throw new InternalServerError("might happen")
} else {
return JSON.stringify(idArray[step++])
}
}
)
const result = await assertThrows(SetupMultipleError, async () => {
return await entityRestClient.setupMultiple(listId, instances)
})
o(requestSpy.callCount).equals(4) //one post multiple and three individual posts
o(result.failedInstances.length).equals(1)//one individual post results in an error
o(result.errors.length).equals(1)
o(result.errors[0] instanceof InternalServerError).equals(true)
o(result.failedInstances).deepEquals([instances[1]])
})
})
})