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
75describe(loginAsync, () => {
76  it('saves user data to ~/.expo/state.json', async () => {
77    mockLoginRequest();
78    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
79
80    expect(await fs.promises.readFile(getUserStatePath(), 'utf8')).toMatchInlineSnapshot(`
81      "{
82        \\"auth\\": {
83          \\"sessionSecret\\": \\"SESSION_SECRET\\",
84          \\"userId\\": \\"USER_ID\\",
85          \\"username\\": \\"USERNAME\\",
86          \\"currentConnection\\": \\"Username-Password-Authentication\\"
87        }
88      }
89      "
90    `);
91  });
92});
93
94describe(logoutAsync, () => {
95  it('removes the session secret', async () => {
96    mockLoginRequest();
97    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
98    expect(UserSettings.getSession()?.sessionSecret).toBe('SESSION_SECRET');
99
100    await logoutAsync();
101    expect(UserSettings.getSession()?.sessionSecret).toBeUndefined();
102  });
103
104  it('removes code signing data', async () => {
105    mockLoginRequest();
106    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
107
108    await DevelopmentCodeSigningInfoFile.setAsync('test', {});
109    expect(fs.existsSync(getDevelopmentCodeSigningDirectory())).toBe(true);
110
111    await logoutAsync();
112    expect(fs.existsSync(getDevelopmentCodeSigningDirectory())).toBe(false);
113  });
114});
115
116describe(getActorDisplayName, () => {
117  it('returns anonymous for unauthenticated users', () => {
118    expect(getActorDisplayName()).toBe('anonymous');
119  });
120
121  it('returns username for user actors', () => {
122    expect(getActorDisplayName(userStub)).toBe(userStub.username);
123  });
124
125  it('returns firstName with robot prefix for robot actors', () => {
126    expect(getActorDisplayName(robotStub)).toBe(`${robotStub.firstName} (robot)`);
127  });
128
129  it('returns robot prefix only for robot actors without firstName', () => {
130    expect(getActorDisplayName({ ...robotStub, firstName: undefined })).toBe('robot');
131  });
132});
133