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 75describe(loginAsync, () => { 76 it('saves user data to ~/.expo/state.json', async () => { 77 mockLoginRequest(); 78 await loginAsync({ username: 'USERNAME', password: 'PASSWORD' }); 79 80 expect(await fs.promises.readFile(getUserStatePath(), 'utf8')).toMatchInlineSnapshot(` 81 "{ 82 \\"auth\\": { 83 \\"sessionSecret\\": \\"SESSION_SECRET\\", 84 \\"userId\\": \\"USER_ID\\", 85 \\"username\\": \\"USERNAME\\", 86 \\"currentConnection\\": \\"Username-Password-Authentication\\" 87 } 88 } 89 " 90 `); 91 }); 92}); 93 94describe(logoutAsync, () => { 95 it('removes the session secret', async () => { 96 mockLoginRequest(); 97 await loginAsync({ username: 'USERNAME', password: 'PASSWORD' }); 98 expect(UserSettings.getSession()?.sessionSecret).toBe('SESSION_SECRET'); 99 100 await logoutAsync(); 101 expect(UserSettings.getSession()?.sessionSecret).toBeUndefined(); 102 }); 103 104 it('removes code signing data', async () => { 105 mockLoginRequest(); 106 await loginAsync({ username: 'USERNAME', password: 'PASSWORD' }); 107 108 await DevelopmentCodeSigningInfoFile.setAsync('test', {}); 109 expect(fs.existsSync(getDevelopmentCodeSigningDirectory())).toBe(true); 110 111 await logoutAsync(); 112 expect(fs.existsSync(getDevelopmentCodeSigningDirectory())).toBe(false); 113 }); 114}); 115 116describe(getActorDisplayName, () => { 117 it('returns anonymous for unauthenticated users', () => { 118 expect(getActorDisplayName()).toBe('anonymous'); 119 }); 120 121 it('returns username for user actors', () => { 122 expect(getActorDisplayName(userStub)).toBe(userStub.username); 123 }); 124 125 it('returns firstName with robot prefix for robot actors', () => { 126 expect(getActorDisplayName(robotStub)).toBe(`${robotStub.firstName} (robot)`); 127 }); 128 129 it('returns robot prefix only for robot actors without firstName', () => { 130 expect(getActorDisplayName({ ...robotStub, firstName: undefined })).toBe('robot'); 131 }); 132}); 133