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