18d307f52SEvan Baconimport { getUserStatePath } from '@expo/config/build/getUserState';
28d307f52SEvan Baconimport { fs, vol } from 'memfs';
38d307f52SEvan Baconimport nock from 'nock';
48d307f52SEvan Bacon
5e377ff85SWill Schurmanimport {
6e377ff85SWill Schurman  DevelopmentCodeSigningInfoFile,
7e377ff85SWill Schurman  getDevelopmentCodeSigningDirectory,
8e377ff85SWill Schurman} from '../../../utils/codesigning';
98d307f52SEvan Baconimport { getExpoApiBaseUrl } from '../../endpoint';
108d307f52SEvan Baconimport UserSettings from '../UserSettings';
11*d88ac65dSlzkbimport { getSessionUsingBrowserAuthFlowAsync } from '../expoSsoLauncher';
12*d88ac65dSlzkbimport {
13*d88ac65dSlzkb  Actor,
14*d88ac65dSlzkb  getActorDisplayName,
15*d88ac65dSlzkb  getUserAsync,
16*d88ac65dSlzkb  loginAsync,
17*d88ac65dSlzkb  logoutAsync,
18*d88ac65dSlzkb  ssoLoginAsync,
19*d88ac65dSlzkb} from '../user';
20*d88ac65dSlzkb
21*d88ac65dSlzkbjest.mock('../expoSsoLauncher', () => ({
22*d88ac65dSlzkb  getSessionUsingBrowserAuthFlowAsync: jest.fn(),
23*d88ac65dSlzkb}));
248d307f52SEvan Bacon
254c50faceSEvan Baconjest.mock('../../../log');
268d307f52SEvan Baconjest.unmock('../UserSettings');
278d307f52SEvan Baconjest.mock('../../graphql/client', () => ({
288d307f52SEvan Bacon  graphqlClient: {
298d307f52SEvan Bacon    query: () => {
308d307f52SEvan Bacon      return {
318d307f52SEvan Bacon        toPromise: () =>
32*d88ac65dSlzkb          Promise.resolve({ data: { meUserActor: { id: 'USER_ID', username: 'USERNAME' } } }),
338d307f52SEvan Bacon      };
348d307f52SEvan Bacon    },
358d307f52SEvan Bacon  },
368d307f52SEvan Bacon}));
378d307f52SEvan Baconjest.mock('../../graphql/queries/UserQuery', () => ({
388d307f52SEvan Bacon  UserQuery: {
398d307f52SEvan Bacon    currentUserAsync: async () => ({ __typename: 'User', username: 'USERNAME', id: 'USER_ID' }),
408d307f52SEvan Bacon  },
418d307f52SEvan Bacon}));
428d307f52SEvan Bacon
438d307f52SEvan BaconbeforeEach(() => {
448d307f52SEvan Bacon  vol.reset();
458d307f52SEvan Bacon});
468d307f52SEvan Bacon
478d307f52SEvan Baconconst userStub: Actor = {
488d307f52SEvan Bacon  __typename: 'User',
498d307f52SEvan Bacon  id: 'userId',
508d307f52SEvan Bacon  username: 'username',
518d307f52SEvan Bacon  accounts: [],
528d307f52SEvan Bacon  isExpoAdmin: false,
538d307f52SEvan Bacon};
548d307f52SEvan Bacon
55*d88ac65dSlzkbconst ssoUserStub: Actor = {
56*d88ac65dSlzkb  __typename: 'SSOUser',
57*d88ac65dSlzkb  id: 'userId',
58*d88ac65dSlzkb  username: 'username',
59*d88ac65dSlzkb  accounts: [],
60*d88ac65dSlzkb  isExpoAdmin: false,
61*d88ac65dSlzkb};
62*d88ac65dSlzkb
638d307f52SEvan Baconconst robotStub: Actor = {
648d307f52SEvan Bacon  __typename: 'Robot',
658d307f52SEvan Bacon  id: 'userId',
668d307f52SEvan Bacon  firstName: 'GLaDOS',
678d307f52SEvan Bacon  accounts: [],
688d307f52SEvan Bacon  isExpoAdmin: false,
698d307f52SEvan Bacon};
708d307f52SEvan Bacon
718d307f52SEvan Baconfunction mockLoginRequest() {
728d307f52SEvan Bacon  nock(getExpoApiBaseUrl())
738d307f52SEvan Bacon    .post('/v2/auth/loginAsync')
748d307f52SEvan Bacon    .reply(200, { data: { sessionSecret: 'SESSION_SECRET' } });
758d307f52SEvan Bacon}
768d307f52SEvan Bacon
77e32ccf9fSEvan Baconfunction resetEnv() {
78e32ccf9fSEvan Bacon  beforeEach(() => {
79e32ccf9fSEvan Bacon    delete process.env.EXPO_OFFLINE;
80e32ccf9fSEvan Bacon    delete process.env.EXPO_TOKEN;
81e32ccf9fSEvan Bacon  });
82e32ccf9fSEvan Bacon  afterAll(() => {
83e32ccf9fSEvan Bacon    delete process.env.EXPO_OFFLINE;
84e32ccf9fSEvan Bacon    delete process.env.EXPO_TOKEN;
85e32ccf9fSEvan Bacon  });
86e32ccf9fSEvan Bacon}
87e32ccf9fSEvan Bacon
88e32ccf9fSEvan BaconresetEnv();
89e32ccf9fSEvan Bacon
908d307f52SEvan Bacondescribe(getUserAsync, () => {
91e32ccf9fSEvan Bacon  resetEnv();
928d307f52SEvan Bacon  it('skips fetching user without access token or session secret', async () => {
938d307f52SEvan Bacon    expect(await getUserAsync()).toBeUndefined();
948d307f52SEvan Bacon  });
958d307f52SEvan Bacon
968d307f52SEvan Bacon  it('fetches user when access token is defined', async () => {
978d307f52SEvan Bacon    process.env.EXPO_TOKEN = 'accesstoken';
988d307f52SEvan Bacon    expect(await getUserAsync()).toMatchObject({ __typename: 'User' });
998d307f52SEvan Bacon  });
1008d307f52SEvan Bacon
1018d307f52SEvan Bacon  it('fetches user when session secret is defined', async () => {
1028d307f52SEvan Bacon    mockLoginRequest();
1038d307f52SEvan Bacon
1048d307f52SEvan Bacon    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
1058d307f52SEvan Bacon    expect(await getUserAsync()).toMatchObject({ __typename: 'User' });
1068d307f52SEvan Bacon  });
10774e3651eSCedric van Putten
10874e3651eSCedric van Putten  it('skips fetching user when running in offline mode', async () => {
10974e3651eSCedric van Putten    jest.resetModules();
110e32ccf9fSEvan Bacon    process.env.EXPO_OFFLINE = '1';
11174e3651eSCedric van Putten    const { getUserAsync } = require('../user');
11274e3651eSCedric van Putten
11374e3651eSCedric van Putten    process.env.EXPO_TOKEN = 'accesstoken';
11474e3651eSCedric van Putten    await expect(getUserAsync()).resolves.toBeUndefined();
11574e3651eSCedric van Putten  });
1168d307f52SEvan Bacon});
1178d307f52SEvan Bacon
1188d307f52SEvan Bacondescribe(loginAsync, () => {
119e32ccf9fSEvan Bacon  resetEnv();
1208d307f52SEvan Bacon  it('saves user data to ~/.expo/state.json', async () => {
1218d307f52SEvan Bacon    mockLoginRequest();
1228d307f52SEvan Bacon    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
1238d307f52SEvan Bacon
1248d307f52SEvan Bacon    expect(await fs.promises.readFile(getUserStatePath(), 'utf8')).toMatchInlineSnapshot(`
1258d307f52SEvan Bacon      "{
126e1bb5bdfSKudo Chien        "auth": {
127e1bb5bdfSKudo Chien          "sessionSecret": "SESSION_SECRET",
128e1bb5bdfSKudo Chien          "userId": "USER_ID",
129e1bb5bdfSKudo Chien          "username": "USERNAME",
130e1bb5bdfSKudo Chien          "currentConnection": "Username-Password-Authentication"
1318d307f52SEvan Bacon        }
1328d307f52SEvan Bacon      }
1338d307f52SEvan Bacon      "
1348d307f52SEvan Bacon    `);
1358d307f52SEvan Bacon  });
1368d307f52SEvan Bacon});
1378d307f52SEvan Bacon
138*d88ac65dSlzkbdescribe(ssoLoginAsync, () => {
139*d88ac65dSlzkb  it('saves user data to ~/.expo/state.json', async () => {
140*d88ac65dSlzkb    jest.mocked(getSessionUsingBrowserAuthFlowAsync).mockResolvedValue('SESSION_SECRET');
141*d88ac65dSlzkb
142*d88ac65dSlzkb    await ssoLoginAsync();
143*d88ac65dSlzkb
144*d88ac65dSlzkb    expect(await fs.promises.readFile(getUserStatePath(), 'utf8')).toMatchInlineSnapshot(`
145*d88ac65dSlzkb      "{
146*d88ac65dSlzkb        "auth": {
147*d88ac65dSlzkb          "sessionSecret": "SESSION_SECRET",
148*d88ac65dSlzkb          "userId": "USER_ID",
149*d88ac65dSlzkb          "username": "USERNAME",
150*d88ac65dSlzkb          "currentConnection": "Browser-Flow-Authentication"
151*d88ac65dSlzkb        }
152*d88ac65dSlzkb      }
153*d88ac65dSlzkb      "
154*d88ac65dSlzkb    `);
155*d88ac65dSlzkb  });
156*d88ac65dSlzkb});
157*d88ac65dSlzkb
1588d307f52SEvan Bacondescribe(logoutAsync, () => {
159e32ccf9fSEvan Bacon  resetEnv();
1608d307f52SEvan Bacon  it('removes the session secret', async () => {
1618d307f52SEvan Bacon    mockLoginRequest();
1628d307f52SEvan Bacon    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
1638d307f52SEvan Bacon    expect(UserSettings.getSession()?.sessionSecret).toBe('SESSION_SECRET');
1648d307f52SEvan Bacon
1658d307f52SEvan Bacon    await logoutAsync();
1668d307f52SEvan Bacon    expect(UserSettings.getSession()?.sessionSecret).toBeUndefined();
1678d307f52SEvan Bacon  });
168e377ff85SWill Schurman
169e377ff85SWill Schurman  it('removes code signing data', async () => {
170e377ff85SWill Schurman    mockLoginRequest();
171e377ff85SWill Schurman    await loginAsync({ username: 'USERNAME', password: 'PASSWORD' });
172e377ff85SWill Schurman
173e377ff85SWill Schurman    await DevelopmentCodeSigningInfoFile.setAsync('test', {});
174e377ff85SWill Schurman    expect(fs.existsSync(getDevelopmentCodeSigningDirectory())).toBe(true);
175e377ff85SWill Schurman
176e377ff85SWill Schurman    await logoutAsync();
177e377ff85SWill Schurman    expect(fs.existsSync(getDevelopmentCodeSigningDirectory())).toBe(false);
178e377ff85SWill Schurman  });
1798d307f52SEvan Bacon});
1808d307f52SEvan Bacon
1818d307f52SEvan Bacondescribe(getActorDisplayName, () => {
182e32ccf9fSEvan Bacon  resetEnv();
1838d307f52SEvan Bacon  it('returns anonymous for unauthenticated users', () => {
1848d307f52SEvan Bacon    expect(getActorDisplayName()).toBe('anonymous');
1858d307f52SEvan Bacon  });
1868d307f52SEvan Bacon
1878d307f52SEvan Bacon  it('returns username for user actors', () => {
1888d307f52SEvan Bacon    expect(getActorDisplayName(userStub)).toBe(userStub.username);
1898d307f52SEvan Bacon  });
1908d307f52SEvan Bacon
191*d88ac65dSlzkb  it('returns username for SSO user actors', () => {
192*d88ac65dSlzkb    expect(getActorDisplayName(userStub)).toBe(ssoUserStub.username);
193*d88ac65dSlzkb  });
194*d88ac65dSlzkb
1958d307f52SEvan Bacon  it('returns firstName with robot prefix for robot actors', () => {
1968d307f52SEvan Bacon    expect(getActorDisplayName(robotStub)).toBe(`${robotStub.firstName} (robot)`);
1978d307f52SEvan Bacon  });
1988d307f52SEvan Bacon
1998d307f52SEvan Bacon  it('returns robot prefix only for robot actors without firstName', () => {
2008d307f52SEvan Bacon    expect(getActorDisplayName({ ...robotStub, firstName: undefined })).toBe('robot');
2018d307f52SEvan Bacon  });
2028d307f52SEvan Bacon});
203