diff --git a/packages/backend/src/nest/storageServerProxy/storageServerProxy.service.spec.ts b/packages/backend/src/nest/storageServerProxy/storageServerProxy.service.spec.ts new file mode 100644 index 0000000000..25ac291636 --- /dev/null +++ b/packages/backend/src/nest/storageServerProxy/storageServerProxy.service.spec.ts @@ -0,0 +1,70 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { TestModule } from '../common/test.module' +import { ServerProxyServiceModule } from './storageServerProxy.module' +import { ServerProxyService } from './storageServerProxy.service' +import { ServerStoredCommunityMetadata } from './storageServerProxy.types' +import { jest } from '@jest/globals' +import { prepareResponse } from './testUtils' +import { createLibp2pAddress, getValidInvitationUrlTestData, validInvitationDatav1 } from '@quiet/common' + +const mockFetch = async (responseData: Partial[]) => { + /** Mock fetch responses and then initialize nest service */ + const mockedFetch = jest.fn(() => { + return Promise.resolve(prepareResponse(responseData[0])) + }) + + for (const data of responseData) { + mockedFetch.mockResolvedValueOnce(prepareResponse(data)) + } + + global.fetch = mockedFetch + const module = await Test.createTestingModule({ + imports: [ServerProxyServiceModule], + }).compile() + return { service: module.get(ServerProxyService), module } +} + +describe('Server Proxy Service', () => { + let testingModule: TestingModule + let clientMetadata: ServerStoredCommunityMetadata + beforeEach(() => { + const data = getValidInvitationUrlTestData(validInvitationDatav1[0]).data + clientMetadata = { + id: '12345678', + ownerCertificate: 'MIIDeTCCAyCgAwIBAgIGAYv8J0ToMAoGCCqGSM49BAMCMBIxEDAOBgNVBAMTB21hYzIzMT', + rootCa: 'MIIBUjCB+KADAgECAgEBMAoGCCqGSM49BAMCMBIxEDAOBgNVBAM', + ownerOrbitDbIdentity: data.ownerOrbitDbIdentity, + peerList: [createLibp2pAddress(data.pairs[0].onionAddress, data.pairs[0].peerId)], + psk: data.psk, + } + }) + + afterEach(async () => { + jest.clearAllMocks() + await testingModule.close() + }) + + it('downloads data for existing cid and proper server address', async () => { + const { module, service } = await mockFetch([ + { status: 200, json: () => Promise.resolve({ access_token: 'secretToken' }) }, + { status: 200, json: () => Promise.resolve(clientMetadata) }, + ]) + testingModule = module + service.setServerAddress('http://whatever') + const data = await service.downloadData('cid') + expect(data).toEqual(clientMetadata) + expect(global.fetch).toHaveBeenCalledTimes(2) + }) + + it('obtains token', async () => { + const expectedToken = 'verySecretToken' + const { module, service } = await mockFetch([ + { status: 200, json: () => Promise.resolve({ access_token: expectedToken }) }, + ]) + testingModule = module + service.setServerAddress('http://whatever') + const token = await service.auth() + expect(token).toEqual(expectedToken) + expect(global.fetch).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/backend/src/nest/storageServerProxy/storageServerProxy.service.ts b/packages/backend/src/nest/storageServerProxy/storageServerProxy.service.ts index e0829fb98c..7912757f68 100644 --- a/packages/backend/src/nest/storageServerProxy/storageServerProxy.service.ts +++ b/packages/backend/src/nest/storageServerProxy/storageServerProxy.service.ts @@ -3,19 +3,33 @@ import EventEmitter from 'events' import { ServerStoredCommunityMetadata } from './storageServerProxy.types' import fetchRetry from 'fetch-retry' import Logger from '../common/logger' -const fetch = fetchRetry(global.fetch) -// TODO: handle retries + +class HTTPResponseError extends Error { + response: Response + constructor(message: string, response: Response) { + super(`${message}: ${response.status} ${response.statusText}`) + this.response = response + } +} + @Injectable() export class ServerProxyService extends EventEmitter { private readonly logger = Logger(ServerProxyService.name) - serverAddress: string + _serverAddress: string + fetch: any constructor() { super() + this.fetch = fetchRetry(global.fetch) + } + + get serverAddress() { + if (!this._serverAddress) throw new Error('Server address is required') + return this._serverAddress } setServerAddress = (serverAddress: string) => { - this.serverAddress = serverAddress + this._serverAddress = serverAddress } get authUrl() { @@ -33,9 +47,9 @@ export class ServerProxyService extends EventEmitter { return `Bearer ${token}` } - auth = async () => { + auth = async (): Promise => { this.logger('Authenticating') - const authResponse = await fetch(this.authUrl, { + const authResponse = await this.fetch(this.authUrl, { method: 'POST', }) this.logger('Auth response status', authResponse.status) @@ -46,11 +60,15 @@ export class ServerProxyService extends EventEmitter { public downloadData = async (cid: string): Promise => { this.logger('Downloading data', cid) const accessToken = await this.auth() - const dataResponse = await fetch(this.getInviteUrl(cid), { + const dataResponse: Response = await this.fetch(this.getInviteUrl(cid), { method: 'GET', headers: { Authorization: this.getAuthorizationHeader(accessToken) }, retries: 3, }) + this.logger('Download data response status', dataResponse.status) + if (!dataResponse.ok) { + throw new HTTPResponseError('Failed to download data', dataResponse) + } const data: ServerStoredCommunityMetadata = await dataResponse.json() this.logger('Downloaded data', data) return data @@ -59,7 +77,7 @@ export class ServerProxyService extends EventEmitter { public uploadData = async (cid: string, data: ServerStoredCommunityMetadata) => { this.logger('Uploading data', cid, data) const accessToken = await this.auth() - const dataResponsePost = await fetch(this.getInviteUrl(cid), { + const dataResponse: Response = await this.fetch(this.getInviteUrl(cid), { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -68,7 +86,10 @@ export class ServerProxyService extends EventEmitter { body: JSON.stringify(data), retries: 3, }) - this.logger('Upload data response status', dataResponsePost) + this.logger('Upload data response status', dataResponse.status) + if (!dataResponse.ok) { + throw new HTTPResponseError('Failed to upload data', dataResponse) + } return cid } } diff --git a/packages/backend/src/nest/storageServerProxy/testUtils.ts b/packages/backend/src/nest/storageServerProxy/testUtils.ts new file mode 100644 index 0000000000..710be9b126 --- /dev/null +++ b/packages/backend/src/nest/storageServerProxy/testUtils.ts @@ -0,0 +1,34 @@ +export const prepareResponse = (responseData: Partial) => { + const ok = responseData.status ? responseData.status >= 200 && responseData.status < 300 : false + const response: Response = { + headers: new Headers(), + ok, + redirected: false, + status: 200, + statusText: '', + type: 'basic', + url: '', + clone: function (): Response { + throw new Error('Function not implemented.') + }, + body: null, + bodyUsed: false, + arrayBuffer: function (): Promise { + throw new Error('Function not implemented.') + }, + blob: function (): Promise { + throw new Error('Function not implemented.') + }, + formData: function (): Promise { + throw new Error('Function not implemented.') + }, + json: function (): Promise { + throw new Error('Function not implemented.') + }, + text: function (): Promise { + throw new Error('Function not implemented.') + }, + ...responseData, + } + return response +} diff --git a/packages/common/src/tests.ts b/packages/common/src/tests.ts index 6c4791f4bb..603c9342df 100644 --- a/packages/common/src/tests.ts +++ b/packages/common/src/tests.ts @@ -52,12 +52,3 @@ export function getValidInvitationUrlTestData { -// return { -// shareUrl: () => composeInvitationShareUrl(data), -// deepUrl: () => composeInvitationDeepUrl(data), -// code: () => composeInvitationShareUrl(data).split(QUIET_JOIN_PAGE + '#')[1], -// data: data, -// } -// }