diff --git a/package.json b/package.json index af86e02..29d3ab8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@internxt/sdk", "author": "Internxt ", - "version": "1.17.2", + "version": "1.17.3", "description": "An sdk for interacting with Internxt's services", "repository": { "type": "git", diff --git a/src/drive/index.ts b/src/drive/index.ts index 8cbb4bf..3550068 100644 --- a/src/drive/index.ts +++ b/src/drive/index.ts @@ -6,3 +6,4 @@ export * from './payments'; export * from './payments/object-storage'; export * from './backups'; export * from './trash'; +export * from './photos'; diff --git a/src/drive/photos/index.ts b/src/drive/photos/index.ts new file mode 100644 index 0000000..19ba9a7 --- /dev/null +++ b/src/drive/photos/index.ts @@ -0,0 +1,53 @@ +import { ApiSecurity, ApiUrl, AppDetails } from '../../shared'; +import { headersWithToken } from '../../shared/headers'; +import { HttpClient } from '../../shared/http/client'; +import { PhotoDevice } from './types'; + +export class Photos { + private readonly client: HttpClient; + private readonly appDetails: AppDetails; + private readonly apiSecurity: ApiSecurity; + + public static client(apiUrl: ApiUrl, appDetails: AppDetails, apiSecurity: ApiSecurity) { + return new Photos(apiUrl, appDetails, apiSecurity); + } + + private constructor(apiUrl: ApiUrl, appDetails: AppDetails, apiSecurity: ApiSecurity) { + this.client = HttpClient.create(apiUrl, apiSecurity.unauthorizedCallback, apiSecurity.retryOptions); + this.appDetails = appDetails; + this.apiSecurity = apiSecurity; + } + + public listDevices(): Promise { + return this.client.get('/photos/devices', this.headers()); + } + + public createDevice(deviceName: string): Promise { + return this.client.post('/photos/devices', { deviceName }, this.headers()); + } + + public getDevice(uuid: string): Promise { + return this.client.get(`/photos/devices/${uuid}`, this.headers()); + } + + public deleteDevice(uuid: string): Promise { + return this.client.delete(`/photos/devices/${uuid}`, this.headers()); + } + + public renameDevice(uuid: string, deviceName: string): Promise { + return this.client.patch(`/photos/devices/${uuid}`, { deviceName }, this.headers()); + } + + private headers() { + return headersWithToken({ + clientName: this.appDetails.clientName, + clientVersion: this.appDetails.clientVersion, + token: this.apiSecurity.token, + workspaceToken: this.apiSecurity.workspaceToken, + desktopToken: this.appDetails.desktopHeader, + customHeaders: this.appDetails.customHeaders, + }); + } +} + +export * from './types'; diff --git a/src/drive/photos/types.ts b/src/drive/photos/types.ts new file mode 100644 index 0000000..63dd87b --- /dev/null +++ b/src/drive/photos/types.ts @@ -0,0 +1,6 @@ +export type PhotoDevice = { + uuid: string; + plainName: string; + bucket: string; + status: 'EXISTS' | 'TRASHED' | 'DELETED'; +}; diff --git a/test/drive/photos/index.test.ts b/test/drive/photos/index.test.ts new file mode 100644 index 0000000..af25990 --- /dev/null +++ b/test/drive/photos/index.test.ts @@ -0,0 +1,126 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { Photos } from '../../../src/drive/photos'; +import { PhotoDevice } from '../../../src/drive/photos/types'; +import { ApiSecurity, AppDetails } from '../../../src/shared'; +import { headersWithToken } from '../../../src/shared/headers'; +import { HttpClient } from '../../../src/shared/http/client'; + +const existingDevice: PhotoDevice = { + uuid: 'device-uuid-1', + plainName: 'Laura iPhone', + bucket: 'photos-bucket', + status: 'EXISTS', +}; + +function clientAndHeaders() { + const appDetails: AppDetails = { clientName: 'c-name', clientVersion: '0.1' }; + const apiSecurity: ApiSecurity = { token: 'my-token' }; + const client = Photos.client('', appDetails, apiSecurity); + const headers = headersWithToken({ clientName: 'c-name', clientVersion: '0.1', token: 'my-token' }); + return { client, headers }; +} + +beforeEach(() => { + vi.restoreAllMocks(); +}); + +describe('listDevices', () => { + test('when the server returns a list of devices, then they are parsed and returned', async () => { + const devices = [existingDevice]; + vi.spyOn(HttpClient.prototype, 'get').mockResolvedValue(devices); + const { client, headers } = clientAndHeaders(); + + const result = await client.listDevices(); + + expect(result).toEqual(devices); + expect(HttpClient.prototype.get).toHaveBeenCalledWith('/photos/devices', headers); + }); + + test('when the server throws, then the error is propagated', async () => { + vi.spyOn(HttpClient.prototype, 'get').mockRejectedValue(new Error('server error')); + const { client } = clientAndHeaders(); + + await expect(client.listDevices()).rejects.toThrow('server error'); + }); +}); + +describe('createDevice', () => { + test('when a device is created successfully, then the created device is returned', async () => { + vi.spyOn(HttpClient.prototype, 'post').mockResolvedValue(existingDevice); + const { client, headers } = clientAndHeaders(); + + const result = await client.createDevice('Laura iPhone'); + + expect(result).toEqual(existingDevice); + expect(HttpClient.prototype.post).toHaveBeenCalledWith('/photos/devices', { deviceName: 'Laura iPhone' }, headers); + }); + + test('when the server throws, then the error is propagated', async () => { + vi.spyOn(HttpClient.prototype, 'post').mockRejectedValue(new Error('conflict')); + const { client } = clientAndHeaders(); + + await expect(client.createDevice('Laura iPhone')).rejects.toThrow('conflict'); + }); +}); + +describe('getDevice', () => { + test('when the device is found, then it is returned', async () => { + vi.spyOn(HttpClient.prototype, 'get').mockResolvedValue(existingDevice); + const { client, headers } = clientAndHeaders(); + + const result = await client.getDevice('device-uuid-1'); + + expect(result).toEqual(existingDevice); + expect(HttpClient.prototype.get).toHaveBeenCalledWith('/photos/devices/device-uuid-1', headers); + }); + + test('when the server throws, then the error is propagated', async () => { + vi.spyOn(HttpClient.prototype, 'get').mockRejectedValue(new Error('not found')); + const { client } = clientAndHeaders(); + + await expect(client.getDevice('device-uuid-missing')).rejects.toThrow('not found'); + }); +}); + +describe('deleteDevice', () => { + test('when the device is deleted successfully, then void is returned', async () => { + vi.spyOn(HttpClient.prototype, 'delete').mockResolvedValue(undefined); + const { client, headers } = clientAndHeaders(); + + const result = await client.deleteDevice('device-uuid-1'); + + expect(result).toBe(undefined); + expect(HttpClient.prototype.delete).toHaveBeenCalledWith('/photos/devices/device-uuid-1', headers); + }); + + test('when the server throws, then the error is propagated', async () => { + vi.spyOn(HttpClient.prototype, 'delete').mockRejectedValue(new Error('server error')); + const { client } = clientAndHeaders(); + + await expect(client.deleteDevice('device-uuid-1')).rejects.toThrow('server error'); + }); +}); + +describe('renameDevice', () => { + test('when the device is renamed successfully, then the updated device is returned', async () => { + const updatedDevice: PhotoDevice = { ...existingDevice, plainName: 'New Name' }; + vi.spyOn(HttpClient.prototype, 'patch').mockResolvedValue(updatedDevice); + const { client, headers } = clientAndHeaders(); + + const result = await client.renameDevice('device-uuid-1', 'New Name'); + + expect(result).toEqual(updatedDevice); + expect(HttpClient.prototype.patch).toHaveBeenCalledWith( + '/photos/devices/device-uuid-1', + { deviceName: 'New Name' }, + headers, + ); + }); + + test('when the server throws, then the error is propagated', async () => { + vi.spyOn(HttpClient.prototype, 'patch').mockRejectedValue(new Error('server error')); + const { client } = clientAndHeaders(); + + await expect(client.renameDevice('device-uuid-1', 'New Name')).rejects.toThrow('server error'); + }); +});