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