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