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