18d307f52SEvan Baconimport assert from 'assert';
28d307f52SEvan Baconimport nock from 'nock';
38d307f52SEvan Baconimport { FetchError } from 'node-fetch';
48d307f52SEvan Baconimport { URLSearchParams } from 'url';
58d307f52SEvan Bacon
6*fa5bc561SWill Schurmanimport { asMock } from '../../../__tests__/asMock';
78d307f52SEvan Baconimport { getExpoApiBaseUrl } from '../../endpoint';
88d307f52SEvan Baconimport UserSettings from '../../user/UserSettings';
98d307f52SEvan Baconimport { ApiV2Error, fetchAsync } from '../client';
108d307f52SEvan Bacon
118d307f52SEvan Baconjest.mock('../../user/UserSettings');
128d307f52SEvan Bacon
138d307f52SEvan Baconit('converts Expo APIv2 error to ApiV2Error', async () => {
148d307f52SEvan Bacon  const scope = nock(getExpoApiBaseUrl())
158d307f52SEvan Bacon    .post('/v2/test')
168d307f52SEvan Bacon    .reply(400, {
178d307f52SEvan Bacon      errors: [
188d307f52SEvan Bacon        {
198d307f52SEvan Bacon          message: 'hellomessage',
208d307f52SEvan Bacon          code: 'TEST_CODE',
218d307f52SEvan Bacon          stack: 'line 1: hello',
228d307f52SEvan Bacon          details: { who: 'world' },
238d307f52SEvan Bacon          metadata: { an: 'object' },
248d307f52SEvan Bacon        },
258d307f52SEvan Bacon      ],
268d307f52SEvan Bacon    });
278d307f52SEvan Bacon
288d307f52SEvan Bacon  expect.assertions(6);
298d307f52SEvan Bacon
308d307f52SEvan Bacon  try {
318d307f52SEvan Bacon    await fetchAsync('test', {
328d307f52SEvan Bacon      method: 'POST',
338d307f52SEvan Bacon    });
348d307f52SEvan Bacon  } catch (error: any) {
358d307f52SEvan Bacon    assert(error instanceof ApiV2Error);
368d307f52SEvan Bacon
378d307f52SEvan Bacon    expect(error.message).toEqual('hellomessage');
388d307f52SEvan Bacon    expect(error.expoApiV2ErrorCode).toEqual('TEST_CODE');
398d307f52SEvan Bacon    expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' });
408d307f52SEvan Bacon    expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' });
418d307f52SEvan Bacon    expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello');
428d307f52SEvan Bacon  }
438d307f52SEvan Bacon  expect(scope.isDone()).toBe(true);
448d307f52SEvan Bacon});
458d307f52SEvan Bacon
468d307f52SEvan Baconit('converts Expo APIv2 error to ApiV2Error (invalid password)', async () => {
478d307f52SEvan Bacon  const scope = nock(getExpoApiBaseUrl())
488d307f52SEvan Bacon    .post('/v2/test')
498d307f52SEvan Bacon    .reply(401, {
508d307f52SEvan Bacon      errors: [
518d307f52SEvan Bacon        {
528d307f52SEvan Bacon          code: 'AUTHENTICATION_ERROR',
53b7080537SEvan Bacon          message: 'Your username, email, or password was incorrect.',
548d307f52SEvan Bacon          isTransient: false,
558d307f52SEvan Bacon        },
568d307f52SEvan Bacon      ],
578d307f52SEvan Bacon    });
588d307f52SEvan Bacon
598d307f52SEvan Bacon  expect.assertions(3);
608d307f52SEvan Bacon
618d307f52SEvan Bacon  try {
628d307f52SEvan Bacon    await fetchAsync('test', {
638d307f52SEvan Bacon      method: 'POST',
648d307f52SEvan Bacon    });
658d307f52SEvan Bacon  } catch (error: any) {
668d307f52SEvan Bacon    assert(error instanceof ApiV2Error);
678d307f52SEvan Bacon
68b7080537SEvan Bacon    expect(error.message).toEqual('Your username, email, or password was incorrect.');
698d307f52SEvan Bacon    expect(error.expoApiV2ErrorCode).toEqual('AUTHENTICATION_ERROR');
708d307f52SEvan Bacon  }
718d307f52SEvan Bacon  expect(scope.isDone()).toBe(true);
728d307f52SEvan Bacon});
738d307f52SEvan Bacon
748d307f52SEvan Baconit('does not convert non-APIv2 error to ApiV2Error', async () => {
758d307f52SEvan Bacon  const scope = nock(getExpoApiBaseUrl()).post('/v2/test').reply(500, 'Something went wrong');
768d307f52SEvan Bacon
778d307f52SEvan Bacon  expect.assertions(1);
788d307f52SEvan Bacon
798d307f52SEvan Bacon  try {
808d307f52SEvan Bacon    await fetchAsync('test', {
818d307f52SEvan Bacon      method: 'POST',
828d307f52SEvan Bacon    });
838d307f52SEvan Bacon  } catch (error: any) {
848d307f52SEvan Bacon    expect(error).toBeInstanceOf(FetchError);
858d307f52SEvan Bacon    expect(error).not.toBeInstanceOf(ApiV2Error);
868d307f52SEvan Bacon  }
878d307f52SEvan Bacon  expect(scope.isDone()).toBe(true);
888d307f52SEvan Bacon});
898d307f52SEvan Bacon
908d307f52SEvan Baconit('makes a get request', async () => {
918d307f52SEvan Bacon  nock(getExpoApiBaseUrl()).get('/v2/get-me?foo=bar').reply(200, 'Hello World');
928d307f52SEvan Bacon  const res = await fetchAsync('get-me', {
938d307f52SEvan Bacon    method: 'GET',
948d307f52SEvan Bacon    // Ensure our custom support for URLSearchParams works...
958d307f52SEvan Bacon    searchParams: new URLSearchParams({
968d307f52SEvan Bacon      foo: 'bar',
978d307f52SEvan Bacon    }),
988d307f52SEvan Bacon  });
998d307f52SEvan Bacon  expect(res.status).toEqual(200);
1008d307f52SEvan Bacon  expect(await res.text()).toEqual('Hello World');
1018d307f52SEvan Bacon});
1028d307f52SEvan Bacon
1038d307f52SEvan Bacon// This test ensures that absolute URLs are allowed with the abstraction.
1048d307f52SEvan Baconit('makes a request using an absolute URL', async () => {
1058d307f52SEvan Bacon  nock('http://example').get('/get-me?foo=bar').reply(200, 'Hello World');
1068d307f52SEvan Bacon  const res = await fetchAsync('http://example/get-me', {
1078d307f52SEvan Bacon    searchParams: new URLSearchParams({
1088d307f52SEvan Bacon      foo: 'bar',
1098d307f52SEvan Bacon    }),
1108d307f52SEvan Bacon  });
1118d307f52SEvan Bacon  expect(res.status).toEqual(200);
1128d307f52SEvan Bacon  expect(await res.text()).toEqual('Hello World');
1138d307f52SEvan Bacon});
1148d307f52SEvan Bacon
1158d307f52SEvan Baconit('makes an authenticated request with access token', async () => {
1168d307f52SEvan Bacon  asMock(UserSettings.getAccessToken).mockClear().mockReturnValue('my-access-token');
1178d307f52SEvan Bacon
1188d307f52SEvan Bacon  nock(getExpoApiBaseUrl())
1198d307f52SEvan Bacon    .matchHeader('authorization', (val) => val.length === 1 && val[0] === 'Bearer my-access-token')
1208d307f52SEvan Bacon    .get('/v2/get-me')
1218d307f52SEvan Bacon    .reply(200, 'Hello World');
1228d307f52SEvan Bacon  const res = await fetchAsync('get-me', {
1238d307f52SEvan Bacon    method: 'GET',
1248d307f52SEvan Bacon  });
1258d307f52SEvan Bacon  expect(res.status).toEqual(200);
1268d307f52SEvan Bacon});
1278d307f52SEvan Bacon
1288d307f52SEvan Baconit('makes an authenticated request with session secret', async () => {
129*fa5bc561SWill Schurman  asMock(UserSettings.getSession).mockClear().mockReturnValue({
130*fa5bc561SWill Schurman    sessionSecret: 'my-secret-token',
131*fa5bc561SWill Schurman    userId: '',
132*fa5bc561SWill Schurman    username: '',
133*fa5bc561SWill Schurman    currentConnection: 'Username-Password-Authentication',
134*fa5bc561SWill Schurman  });
1358d307f52SEvan Bacon  asMock(UserSettings.getAccessToken).mockReturnValue(null);
1368d307f52SEvan Bacon
1378d307f52SEvan Bacon  nock(getExpoApiBaseUrl())
1388d307f52SEvan Bacon    .matchHeader('expo-session', (val) => val.length === 1 && val[0] === 'my-secret-token')
1398d307f52SEvan Bacon    .get('/v2/get-me')
1408d307f52SEvan Bacon    .reply(200, 'Hello World');
1418d307f52SEvan Bacon  const res = await fetchAsync('get-me', {
1428d307f52SEvan Bacon    method: 'GET',
1438d307f52SEvan Bacon  });
1448d307f52SEvan Bacon  expect(res.status).toEqual(200);
1458d307f52SEvan Bacon});
1468d307f52SEvan Bacon
1478d307f52SEvan Baconit('only uses access token when both authentication methods are available', async () => {
1488d307f52SEvan Bacon  asMock(UserSettings.getAccessToken).mockClear().mockReturnValue('my-access-token');
149*fa5bc561SWill Schurman  asMock(UserSettings.getSession).mockClear().mockReturnValue({
150*fa5bc561SWill Schurman    sessionSecret: 'my-secret-token',
151*fa5bc561SWill Schurman    userId: '',
152*fa5bc561SWill Schurman    username: '',
153*fa5bc561SWill Schurman    currentConnection: 'Username-Password-Authentication',
154*fa5bc561SWill Schurman  });
1558d307f52SEvan Bacon
1568d307f52SEvan Bacon  nock(getExpoApiBaseUrl())
1578d307f52SEvan Bacon    .matchHeader('authorization', (val) => val.length === 1 && val[0] === 'Bearer my-access-token')
1588d307f52SEvan Bacon    .matchHeader('expo-session', (val) => !val)
1598d307f52SEvan Bacon    .get('/v2/get-me')
1608d307f52SEvan Bacon    .reply(200, 'Hello World');
1618d307f52SEvan Bacon  const res = await fetchAsync('get-me', {
1628d307f52SEvan Bacon    method: 'GET',
1638d307f52SEvan Bacon  });
1648d307f52SEvan Bacon  expect(res.status).toEqual(200);
1658d307f52SEvan Bacon});
166