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.unmock('../UserSettings'); 14jest.mock('../../graphql/client', () => ({ 15 graphqlClient: { 16 query: () => { 17 return { 18 toPromise: () => 19 Promise.resolve({ data: { viewer: { id: 'USER_ID', username: 'USERNAME' } } }), 20 }; 21 }, 22 }, 23})); 24jest.mock('../../graphql/queries/UserQuery', () => ({ 25 UserQuery: { 26 currentUserAsync: async () => ({ __typename: 'User', username: 'USERNAME', id: 'USER_ID' }), 27 }, 28})); 29 30beforeEach(() => { 31 vol.reset(); 32}); 33 34const userStub: Actor = { 35 __typename: 'User', 36 id: 'userId', 37 username: 'username', 38 accounts: [], 39 isExpoAdmin: false, 40}; 41 42const robotStub: Actor = { 43 __typename: 'Robot', 44 id: 'userId', 45 firstName: 'GLaDOS', 46 accounts: [], 47 isExpoAdmin: false, 48}; 49 50function mockLoginRequest() { 51 nock(getExpoApiBaseUrl()) 52 .post('/v2/auth/loginAsync') 53 .reply(200, { data: { sessionSecret: 'SESSION_SECRET' } }); 54} 55 56describe(getUserAsync, () => { 57 it('skips fetching user without access token or session secret', async () => { 58 expect(await getUserAsync()).toBeUndefined(); 59 }); 60 61 it('fetches user when access token is defined', async () => { 62 process.env.EXPO_TOKEN = 'accesstoken'; 63 expect(await getUserAsync()).toMatchObject({ __typename: 'User' }); 64 }); 65 66 it('fetches user when session secret is defined', async () => { 67 mockLoginRequest(); 68 69 await loginAsync({ username: 'USERNAME', password: 'PASSWORD' }); 70 expect(await getUserAsync()).toMatchObject({ __typename: 'User' }); 71 }); 72}); 73 74describe(loginAsync, () => { 75 it('saves user data to ~/.expo/state.json', async () => { 76 mockLoginRequest(); 77 await loginAsync({ username: 'USERNAME', password: 'PASSWORD' }); 78 79 expect(await fs.promises.readFile(getUserStatePath(), 'utf8')).toMatchInlineSnapshot(` 80 "{ 81 \\"auth\\": { 82 \\"sessionSecret\\": \\"SESSION_SECRET\\", 83 \\"userId\\": \\"USER_ID\\", 84 \\"username\\": \\"USERNAME\\", 85 \\"currentConnection\\": \\"Username-Password-Authentication\\" 86 } 87 } 88 " 89 `); 90 }); 91}); 92 93describe(logoutAsync, () => { 94 it('removes the session secret', async () => { 95 mockLoginRequest(); 96 await loginAsync({ username: 'USERNAME', password: 'PASSWORD' }); 97 expect(UserSettings.getSession()?.sessionSecret).toBe('SESSION_SECRET'); 98 99 await logoutAsync(); 100 expect(UserSettings.getSession()?.sessionSecret).toBeUndefined(); 101 }); 102 103 it('removes code signing data', async () => { 104 mockLoginRequest(); 105 await loginAsync({ username: 'USERNAME', password: 'PASSWORD' }); 106 107 await DevelopmentCodeSigningInfoFile.setAsync('test', {}); 108 expect(fs.existsSync(getDevelopmentCodeSigningDirectory())).toBe(true); 109 110 await logoutAsync(); 111 expect(fs.existsSync(getDevelopmentCodeSigningDirectory())).toBe(false); 112 }); 113}); 114 115describe(getActorDisplayName, () => { 116 it('returns anonymous for unauthenticated users', () => { 117 expect(getActorDisplayName()).toBe('anonymous'); 118 }); 119 120 it('returns username for user actors', () => { 121 expect(getActorDisplayName(userStub)).toBe(userStub.username); 122 }); 123 124 it('returns firstName with robot prefix for robot actors', () => { 125 expect(getActorDisplayName(robotStub)).toBe(`${robotStub.firstName} (robot)`); 126 }); 127 128 it('returns robot prefix only for robot actors without firstName', () => { 129 expect(getActorDisplayName({ ...robotStub, firstName: undefined })).toBe('robot'); 130 }); 131}); 132