1import assert from 'assert'; 2import nock from 'nock'; 3import { FetchError } from 'node-fetch'; 4import { URLSearchParams } from 'url'; 5 6import { getExpoApiBaseUrl } from '../../endpoint'; 7import UserSettings from '../../user/UserSettings'; 8import { ApiV2Error, fetchAsync } from '../client'; 9 10jest.mock('../../user/UserSettings'); 11 12const asMock = (fn: any): jest.Mock => fn; 13 14it('converts Expo APIv2 error to ApiV2Error', async () => { 15 const scope = nock(getExpoApiBaseUrl()) 16 .post('/v2/test') 17 .reply(400, { 18 errors: [ 19 { 20 message: 'hellomessage', 21 code: 'TEST_CODE', 22 stack: 'line 1: hello', 23 details: { who: 'world' }, 24 metadata: { an: 'object' }, 25 }, 26 ], 27 }); 28 29 expect.assertions(6); 30 31 try { 32 await fetchAsync('test', { 33 method: 'POST', 34 }); 35 } catch (error: any) { 36 assert(error instanceof ApiV2Error); 37 38 expect(error.message).toEqual('hellomessage'); 39 expect(error.expoApiV2ErrorCode).toEqual('TEST_CODE'); 40 expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' }); 41 expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' }); 42 expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello'); 43 } 44 expect(scope.isDone()).toBe(true); 45}); 46 47it('converts Expo APIv2 error to ApiV2Error (invalid password)', async () => { 48 const scope = nock(getExpoApiBaseUrl()) 49 .post('/v2/test') 50 .reply(401, { 51 errors: [ 52 { 53 code: 'AUTHENTICATION_ERROR', 54 message: 'Your username, email, or password was incorrect.', 55 isTransient: false, 56 }, 57 ], 58 }); 59 60 expect.assertions(3); 61 62 try { 63 await fetchAsync('test', { 64 method: 'POST', 65 }); 66 } catch (error: any) { 67 assert(error instanceof ApiV2Error); 68 69 expect(error.message).toEqual('Your username, email, or password was incorrect.'); 70 expect(error.expoApiV2ErrorCode).toEqual('AUTHENTICATION_ERROR'); 71 } 72 expect(scope.isDone()).toBe(true); 73}); 74 75it('does not convert non-APIv2 error to ApiV2Error', async () => { 76 const scope = nock(getExpoApiBaseUrl()).post('/v2/test').reply(500, 'Something went wrong'); 77 78 expect.assertions(1); 79 80 try { 81 await fetchAsync('test', { 82 method: 'POST', 83 }); 84 } catch (error: any) { 85 expect(error).toBeInstanceOf(FetchError); 86 expect(error).not.toBeInstanceOf(ApiV2Error); 87 } 88 expect(scope.isDone()).toBe(true); 89}); 90 91it('makes a get request', async () => { 92 nock(getExpoApiBaseUrl()).get('/v2/get-me?foo=bar').reply(200, 'Hello World'); 93 const res = await fetchAsync('get-me', { 94 method: 'GET', 95 // Ensure our custom support for URLSearchParams works... 96 searchParams: new URLSearchParams({ 97 foo: 'bar', 98 }), 99 }); 100 expect(res.status).toEqual(200); 101 expect(await res.text()).toEqual('Hello World'); 102}); 103 104// This test ensures that absolute URLs are allowed with the abstraction. 105it('makes a request using an absolute URL', async () => { 106 nock('http://example').get('/get-me?foo=bar').reply(200, 'Hello World'); 107 const res = await fetchAsync('http://example/get-me', { 108 searchParams: new URLSearchParams({ 109 foo: 'bar', 110 }), 111 }); 112 expect(res.status).toEqual(200); 113 expect(await res.text()).toEqual('Hello World'); 114}); 115 116it('makes an authenticated request with access token', async () => { 117 asMock(UserSettings.getAccessToken).mockClear().mockReturnValue('my-access-token'); 118 119 nock(getExpoApiBaseUrl()) 120 .matchHeader('authorization', (val) => val.length === 1 && val[0] === 'Bearer my-access-token') 121 .get('/v2/get-me') 122 .reply(200, 'Hello World'); 123 const res = await fetchAsync('get-me', { 124 method: 'GET', 125 }); 126 expect(res.status).toEqual(200); 127}); 128 129it('makes an authenticated request with session secret', async () => { 130 asMock(UserSettings.getSession).mockClear().mockReturnValue({ sessionSecret: 'my-secret-token' }); 131 asMock(UserSettings.getAccessToken).mockReturnValue(null); 132 133 nock(getExpoApiBaseUrl()) 134 .matchHeader('expo-session', (val) => val.length === 1 && val[0] === 'my-secret-token') 135 .get('/v2/get-me') 136 .reply(200, 'Hello World'); 137 const res = await fetchAsync('get-me', { 138 method: 'GET', 139 }); 140 expect(res.status).toEqual(200); 141}); 142 143it('only uses access token when both authentication methods are available', async () => { 144 asMock(UserSettings.getAccessToken).mockClear().mockReturnValue('my-access-token'); 145 asMock(UserSettings.getSession).mockClear().mockReturnValue({ sessionSecret: 'my-secret-token' }); 146 147 nock(getExpoApiBaseUrl()) 148 .matchHeader('authorization', (val) => val.length === 1 && val[0] === 'Bearer my-access-token') 149 .matchHeader('expo-session', (val) => !val) 150 .get('/v2/get-me') 151 .reply(200, 'Hello World'); 152 const res = await fetchAsync('get-me', { 153 method: 'GET', 154 }); 155 expect(res.status).toEqual(200); 156}); 157