tutanota/test/api/rest/EntityRestClientTest.js

490 lines
20 KiB
JavaScript
Raw Normal View History

// @flow
import o from "ospec"
2021-12-15 16:07:07 +01:00
import {ContactTypeRef, 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"
2021-12-15 16:07:07 +01:00
import {createCustomer, CustomerTypeRef} from "../../../src/api/entities/sys/Customer"
import {EntityRestClient, typeRefToPath} 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"
2021-12-15 16:07:07 +01:00
const accessToken = "My cool access token"
const authHeader = {accessToken: accessToken}
2021-12-15 16:07:07 +01:00
function createEntityRestClientWithMocks(requestMock: Function): {
entityRestClient: EntityRestClient,
cryptoFacadeMock: CryptoFacade,
instanceMapperMock: InstanceMapper,
requestSpy: OspecSpy<typeof RestClient.prototype.request>
} {
2021-12-15 16:07:07 +01:00
const cryptoFacadeMock: CryptoFacade = {
applyMigrations: o.spy(async <T>(typeRef: TypeRef<T>, data: any): Promise<T> => resolveTypeReference(typeRef).then(model => create(model, typeRef))),
applyMigrationsForInstance: o.spy(async <T>(decryptedInstance: T): Promise<T> => decryptedInstance),
setNewOwnerEncSessionKey: o.spy((model: TypeModel, entity: Object) => []),
resolveServiceSessionKey: o.spy(async (typeModel: TypeModel, instance: Object) => []),
encryptBucketKeyForInternalRecipient: o.spy(async (bucketKey: Aes128Key, recipientMailAddress: string, notFoundRecipients: Array<string>) => createInternalRecipientKeyData()),
resolveSessionKey: o.spy(async (typeModel: TypeModel, instance: Object) => []),
}
2021-12-15 16:07:07 +01:00
const instanceMapperMock: InstanceMapper = downcast({
encryptAndMapToLiteral: o.spy(async (model, instance, sk) => {
return {
dummyMessage: "encrypted"
}
}),
decryptAndMapToInstance: o.spy(async (model, instance, sk) => {
return {
dummyMessage: "decrypted"
}
})
})
const requestSpy = o.spy(requestMock)
2021-12-15 16:07:07 +01:00
const entityRestClient = new EntityRestClient(
2021-12-15 16:07:07 +01:00
() => authHeader, // Entity rest client doesn't allow requests without authorization
downcast<RestClient>({request: async (...args) => requestSpy(...args)}),
() => cryptoFacadeMock,
instanceMapperMock,
)
return {
2021-12-15 16:07:07 +01:00
instanceMapperMock,
cryptoFacadeMock,
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 () {
2021-12-15 16:07:07 +01:00
o.spec("Load", function () {
o("loading a list element", async function () {
const {entityRestClient, requestSpy, instanceMapperMock, cryptoFacadeMock} = createEntityRestClientWithMocks(
() => JSON.stringify("The element that was returned from the server")
)
const calendarListId = "calendarListId"
const id1 = "id1"
const result = await entityRestClient.load(CalendarEventTypeRef, [calendarListId, id1])
o(requestSpy.callCount).equals(1)
o(requestSpy.args[0]).equals(`${typeRefToPath(CalendarEventTypeRef)}/${calendarListId}/${id1}`)("path is correct")
o(requestSpy.args[1]).equals(HttpMethod.GET)("Method is GET")
o(instanceMapperMock.decryptAndMapToInstance.callCount).equals(1)
o(cryptoFacadeMock.applyMigrationsForInstance.callCount).equals(1)
})
o("loading an element ", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
2021-12-15 16:07:07 +01:00
() => JSON.stringify("The element that was returned from the server")
)
2021-12-15 16:07:07 +01:00
const id1 = "id1"
const result = await entityRestClient.load(CustomerTypeRef, id1)
o(requestSpy.callCount).equals(1)
2021-12-15 16:07:07 +01:00
o(requestSpy.args[0]).equals(`${typeRefToPath(CustomerTypeRef)}/${id1}`)("path is correct")
o(requestSpy.args[1]).equals(HttpMethod.GET)("Method is GET")
2021-12-15 16:07:07 +01:00
o(result).deepEquals({dummyMessage: "decrypted"})("decrypts and returns from the client")
})
o("query parameters and additional headers + access token and version are always passed to the rest client", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => JSON.stringify("The element that was returned from the server")
)
const id1 = "id1"
const queryParameters = {"foo": "bar"}
const headers = {"baz": "quux"}
const result = await entityRestClient.load(CustomerTypeRef, id1, queryParameters, headers)
2021-12-15 16:07:07 +01:00
const {version} = await resolveTypeReference(CustomerTypeRef)
o(requestSpy.args[2]).deepEquals(queryParameters)("query parameters are passed")
o(requestSpy.args[3]).deepEquals({accessToken: accessToken, v: version, baz: "quux"})("headers are passed")
})
})
2021-12-15 16:07:07 +01:00
o.spec("Load Range", function () {
o("Loads a range of entities in a single request", async function () {
const {entityRestClient, requestSpy, instanceMapperMock, cryptoFacadeMock} = createEntityRestClientWithMocks(
() => JSON.stringify(["e1", "e2", "e3"])
)
const startId = "42"
const count = 5
await entityRestClient.loadRange(CalendarEventTypeRef, "listId", startId, count, false)
const {version} = await resolveTypeReference(CalendarEventTypeRef)
o(requestSpy.callCount).equals(1)
o(requestSpy.args[1]).equals(HttpMethod.GET)("Method is GET")
o(requestSpy.args[2]).deepEquals({
start: `${startId}`,
count: `${count}`,
reverse: "false"
})("Range is passed in as query params")
o(requestSpy.args[3]).deepEquals({accessToken, v: version})("access token and version are passed")
})
})
o.spec("Load multiple", function () {
o("Less than 100 entities requested should result in a single rest request", async function () {
2021-12-15 16:07:07 +01:00
const {entityRestClient, requestSpy, instanceMapperMock, cryptoFacadeMock} = createEntityRestClientWithMocks(
() => JSON.stringify(["e1", "e2", "e3"])
)
const ids = range(0, 5)
const result = await entityRestClient.loadMultiple(CustomerTypeRef, null, ids)
2021-12-15 16:07:07 +01:00
const {version} = await resolveTypeReference(CustomerTypeRef)
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")
2021-12-15 16:07:07 +01:00
o(requestSpy.args[3]).deepEquals({accessToken, v: version})("access token and version are passed")
o(instanceMapperMock.decryptAndMapToInstance.callCount).equals(3)
o(cryptoFacadeMock.applyMigrationsForInstance.callCount).equals(3)
})
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")
})
})
2021-12-15 16:07:07 +01:00
o.spec("Setup", async function () {
o("Setup list entity", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => JSON.stringify({generatedId: "id"})
)
const newContact = createContact()
const result = await entityRestClient.setup("listId", newContact)
const {version} = await resolveTypeReference(ContactTypeRef)
o(result).deepEquals("id")
o(requestSpy.callCount).equals(1)
o(requestSpy.args[1]).equals(HttpMethod.POST)("The method is POST")
o(requestSpy.args[3]).deepEquals({accessToken, v: version})("access token and version are passed")
o(requestSpy.args[4]).deepEquals(JSON.stringify({dummyMessage: "encrypted"}))("Contact were sent")
})
o("Setup list entity throws when no listid is passed", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => JSON.stringify({generatedId: "id"})
)
const newContact = createContact()
const result = await assertThrows(Error, async () => await entityRestClient.setup(null, newContact))
o(result.message).equals("List id must be defined for LETs")
})
o("Setup entity", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => JSON.stringify({generatedId: "id"})
)
const newCustomer = createCustomer()
const result = await entityRestClient.setup(null, newCustomer)
const {version} = await resolveTypeReference(CustomerTypeRef)
o(result).deepEquals("id")
o(requestSpy.callCount).equals(1)
o(requestSpy.args[1]).equals(HttpMethod.POST)("The method is POST")
o(requestSpy.args[3]).deepEquals({accessToken, v: version})("access token and version are passed")
o(requestSpy.args[4]).deepEquals(JSON.stringify({dummyMessage: "encrypted"}))("Contact were sent")
})
o("Setup entity throws when listid is passed", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => JSON.stringify({generatedId: "id"})
)
const newCustomer = createCustomer()
const result = await assertThrows(Error, async () => await entityRestClient.setup("listId", newCustomer))
o(result.message).equals("List id must not be defined for ETs")
})
})
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)
2021-12-15 16:07:07 +01:00
const {version} = await resolveTypeReference(ContactTypeRef)
o(result).deepEquals(["someReturnedId"])
o(requestSpy.callCount).equals(1)
o(requestSpy.args[1]).equals(HttpMethod.POST)("The method is POST")
2021-12-15 16:07:07 +01:00
o(requestSpy.args[3]).deepEquals({accessToken, v: version})("access token and version are passed")
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,
2021-12-28 13:53:11 +01:00
queryParams: Dict,
headers: Dict,
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]])
})
})
2021-12-15 16:07:07 +01:00
o.spec("Update", function () {
o("Update entity", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => {}
)
const newCustomer = createCustomer({_id: "id"})
await entityRestClient.update(newCustomer)
const {version} = await resolveTypeReference(CustomerTypeRef)
o(requestSpy.callCount).equals(1)
o(requestSpy.args[1]).equals(HttpMethod.PUT)("The method is PUT")
o(requestSpy.args[3]).deepEquals({accessToken, v: version})("access token and version are passed")
o(requestSpy.args[4]).deepEquals(JSON.stringify({dummyMessage: "encrypted"}))("Contact were sent")
})
o("Update entity throws if entity does not have an id", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => {}
)
const newCustomer = createCustomer()
const result = await assertThrows(Error, async () => await entityRestClient.update(newCustomer))
o(result.message).equals("Id must be defined")
})
})
o.spec("Delete", function () {
o("Delete entity", async function () {
const {entityRestClient, requestSpy} = createEntityRestClientWithMocks(
() => {}
)
const id = "id"
const newCustomer = createCustomer({_id: id})
await entityRestClient.erase(newCustomer)
const {version} = await resolveTypeReference(CustomerTypeRef)
o(requestSpy.callCount).equals(1)
o(requestSpy.args[0]).equals(`${typeRefToPath(CustomerTypeRef)}/${id}`)("path is correct")
o(requestSpy.args[1]).equals(HttpMethod.DELETE)("The method is DELETE")
o(requestSpy.args[3]).deepEquals({accessToken, v: version})("access token and version are passed")
})
})
})