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