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