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