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
57function resetEnv() {
58  beforeEach(() => {
59    delete process.env.EXPO_OFFLINE;
60    delete process.env.EXPO_TOKEN;
61  });
62  afterAll(() => {
63    delete process.env.EXPO_OFFLINE;
64    delete process.env.EXPO_TOKEN;
65  });
66}
67
68resetEnv();
69
70describe(getUserAsync, () => {
71  resetEnv();
72  it('skips fetching user without access token or session secret', async () => {
73    expect(await getUserAsync()).toBeUndefined();
74  });
75
76  it('fetches user when access token is defined', async () => {
77    process.env.EXPO_TOKEN = 'accesstoken';
78    expect(await getUserAsync()).toMatchObject({ __typename: 'User' });
79  });
80
81  it('fetches user when session secret is defined', async () => {
82    mockLoginRequest();
83
84    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
85    expect(await getUserAsync()).toMatchObject({ __typename: 'User' });
86  });
87
88  it('skips fetching user when running in offline mode', async () => {
89    jest.resetModules();
90    process.env.EXPO_OFFLINE = '1';
91    const { getUserAsync } = require('../user');
92
93    process.env.EXPO_TOKEN = 'accesstoken';
94    await expect(getUserAsync()).resolves.toBeUndefined();
95  });
96});
97
98describe(loginAsync, () => {
99  resetEnv();
100  it('saves user data to ~/.expo/state.json', async () => {
101    mockLoginRequest();
102    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
103
104    expect(await fs.promises.readFile(getUserStatePath(), 'utf8')).toMatchInlineSnapshot(`
105      "{
106        "auth": {
107          "sessionSecret": "SESSION_SECRET",
108          "userId": "USER_ID",
109          "username": "USERNAME",
110          "currentConnection": "Username-Password-Authentication"
111        }
112      }
113      "
114    `);
115  });
116});
117
118describe(logoutAsync, () => {
119  resetEnv();
120  it('removes the session secret', async () => {
121    mockLoginRequest();
122    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
123    expect(UserSettings.getSession()?.sessionSecret).toBe('SESSION_SECRET');
124
125    await logoutAsync();
126    expect(UserSettings.getSession()?.sessionSecret).toBeUndefined();
127  });
128
129  it('removes code signing data', async () => {
130    mockLoginRequest();
131    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
132
133    await DevelopmentCodeSigningInfoFile.setAsync('test', {});
134    expect(fs.existsSync(getDevelopmentCodeSigningDirectory())).toBe(true);
135
136    await logoutAsync();
137    expect(fs.existsSync(getDevelopmentCodeSigningDirectory())).toBe(false);
138  });
139});
140
141describe(getActorDisplayName, () => {
142  resetEnv();
143  it('returns anonymous for unauthenticated users', () => {
144    expect(getActorDisplayName()).toBe('anonymous');
145  });
146
147  it('returns username for user actors', () => {
148    expect(getActorDisplayName(userStub)).toBe(userStub.username);
149  });
150
151  it('returns firstName with robot prefix for robot actors', () => {
152    expect(getActorDisplayName(robotStub)).toBe(`${robotStub.firstName} (robot)`);
153  });
154
155  it('returns robot prefix only for robot actors without firstName', () => {
156    expect(getActorDisplayName({ ...robotStub, firstName: undefined })).toBe('robot');
157  });
158});
159