1import { getUserStatePath } from '@expo/config/build/getUserState';
2import { fs, vol } from 'memfs';
3import nock from 'nock';
4
5import {
6  DevelopmentCodeSigningInfoFile,
7  getDevelopmentCodeSigningDirectory,
8} from '../../../utils/codesigning';
9import { getExpoApiBaseUrl } from '../../endpoint';
10import UserSettings from '../UserSettings';
11import { Actor, getActorDisplayName, getUserAsync, loginAsync, logoutAsync } from '../user';
12
13jest.mock('../../../log');
14jest.unmock('../UserSettings');
15jest.mock('../../graphql/client', () => ({
16  graphqlClient: {
17    query: () => {
18      return {
19        toPromise: () =>
20          Promise.resolve({ data: { viewer: { id: 'USER_ID', username: 'USERNAME' } } }),
21      };
22    },
23  },
24}));
25jest.mock('../../graphql/queries/UserQuery', () => ({
26  UserQuery: {
27    currentUserAsync: async () => ({ __typename: 'User', username: 'USERNAME', id: 'USER_ID' }),
28  },
29}));
30
31beforeEach(() => {
32  vol.reset();
33});
34
35const userStub: Actor = {
36  __typename: 'User',
37  id: 'userId',
38  username: 'username',
39  accounts: [],
40  isExpoAdmin: false,
41};
42
43const robotStub: Actor = {
44  __typename: 'Robot',
45  id: 'userId',
46  firstName: 'GLaDOS',
47  accounts: [],
48  isExpoAdmin: false,
49};
50
51function mockLoginRequest() {
52  nock(getExpoApiBaseUrl())
53    .post('/v2/auth/loginAsync')
54    .reply(200, { data: { sessionSecret: 'SESSION_SECRET' } });
55}
56
57describe(getUserAsync, () => {
58  it('skips fetching user without access token or session secret', async () => {
59    expect(await getUserAsync()).toBeUndefined();
60  });
61
62  it('fetches user when access token is defined', async () => {
63    process.env.EXPO_TOKEN = 'accesstoken';
64    expect(await getUserAsync()).toMatchObject({ __typename: 'User' });
65  });
66
67  it('fetches user when session secret is defined', async () => {
68    mockLoginRequest();
69
70    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
71    expect(await getUserAsync()).toMatchObject({ __typename: 'User' });
72  });
73
74  it('skips fetching user when running in offline mode', async () => {
75    jest.resetModules();
76    jest.mock('../../settings', () => ({ APISettings: { isOffline: true } }));
77    const { getUserAsync } = require('../user');
78
79    process.env.EXPO_TOKEN = 'accesstoken';
80    await expect(getUserAsync()).resolves.toBeUndefined();
81  });
82});
83
84describe(loginAsync, () => {
85  it('saves user data to ~/.expo/state.json', async () => {
86    mockLoginRequest();
87    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
88
89    expect(await fs.promises.readFile(getUserStatePath(), 'utf8')).toMatchInlineSnapshot(`
90      "{
91        "auth": {
92          "sessionSecret": "SESSION_SECRET",
93          "userId": "USER_ID",
94          "username": "USERNAME",
95          "currentConnection": "Username-Password-Authentication"
96        }
97      }
98      "
99    `);
100  });
101});
102
103describe(logoutAsync, () => {
104  it('removes the session secret', async () => {
105    mockLoginRequest();
106    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
107    expect(UserSettings.getSession()?.sessionSecret).toBe('SESSION_SECRET');
108
109    await logoutAsync();
110    expect(UserSettings.getSession()?.sessionSecret).toBeUndefined();
111  });
112
113  it('removes code signing data', async () => {
114    mockLoginRequest();
115    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
116
117    await DevelopmentCodeSigningInfoFile.setAsync('test', {});
118    expect(fs.existsSync(getDevelopmentCodeSigningDirectory())).toBe(true);
119
120    await logoutAsync();
121    expect(fs.existsSync(getDevelopmentCodeSigningDirectory())).toBe(false);
122  });
123});
124
125describe(getActorDisplayName, () => {
126  it('returns anonymous for unauthenticated users', () => {
127    expect(getActorDisplayName()).toBe('anonymous');
128  });
129
130  it('returns username for user actors', () => {
131    expect(getActorDisplayName(userStub)).toBe(userStub.username);
132  });
133
134  it('returns firstName with robot prefix for robot actors', () => {
135    expect(getActorDisplayName(robotStub)).toBe(`${robotStub.firstName} (robot)`);
136  });
137
138  it('returns robot prefix only for robot actors without firstName', () => {
139    expect(getActorDisplayName({ ...robotStub, firstName: undefined })).toBe('robot');
140  });
141});
142