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