// Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; import { Proto, StorageState } from '@signalapp/mock-server'; import type { PrimaryDevice } from '@signalapp/mock-server'; import createDebug from 'debug'; import * as durations from '../../util/durations'; import { uuidToBytes } from '../../util/uuidToBytes'; import { MY_STORY_ID } from '../../types/Stories'; import { Bootstrap } from '../bootstrap'; import type { App } from '../bootstrap'; export const debug = createDebug('mock:test:username'); const IdentifierType = Proto.ManifestRecord.Identifier.Type; const USERNAME = 'signalapp.55'; const NICKNAME = 'signalapp'; const CARL_USERNAME = 'carl.84'; describe('pnp/username', function needsName() { this.timeout(durations.MINUTE); let bootstrap: Bootstrap; let app: App; let usernameContact: PrimaryDevice; beforeEach(async () => { bootstrap = new Bootstrap({ contactCount: 0 }); await bootstrap.init(); const { server, phone } = bootstrap; usernameContact = await server.createPrimaryDevice({ profileName: 'ACI Contact', }); let state = StorageState.getEmpty(); state = state.updateAccount({ profileKey: phone.profileKey.serialize(), e164: phone.device.number, }); state = state.addContact(usernameContact, { username: USERNAME, serviceE164: undefined, }); // Put contact into left pane state = state.pin(usernameContact); // Add my story state = state.addRecord({ type: IdentifierType.STORY_DISTRIBUTION_LIST, record: { storyDistributionList: { allowsReplies: true, identifier: uuidToBytes(MY_STORY_ID), isBlockList: true, name: MY_STORY_ID, recipientUuids: [], }, }, }); await phone.setStorageState(state); app = await bootstrap.link(); }); afterEach(async function after() { if (this.currentTest?.state !== 'passed') { await bootstrap.saveLogs(app); } await app.close(); await bootstrap.teardown(); }); for (const type of ['profile', 'system']) { // eslint-disable-next-line no-loop-func it(`drops username when contact's ${type} name becomes known`, async () => { const { phone } = bootstrap; const window = await app.getWindow(); const leftPane = window.locator('.left-pane-wrapper'); debug('find username in the left pane'); await leftPane .locator( `[data-testid="${usernameContact.device.uuid}"] >> "${USERNAME}"` ) .waitFor(); let state = await phone.expectStorageState('consistency check'); if (type === 'profile') { debug('adding profile key for username contact'); state = state.updateContact(usernameContact, { profileKey: usernameContact.profileKey.serialize(), }); } else { debug('adding nickname for username contact'); state = state.updateContact(usernameContact, { systemNickname: usernameContact.profileName, }); } await phone.setStorageState(state); await phone.sendFetchStorage({ timestamp: bootstrap.getTimestamp(), }); debug('find profile name in the left pane'); await leftPane .locator( `[data-testid="${usernameContact.device.uuid}"] >> ` + `"${usernameContact.profileName}"` ) .waitFor(); debug('verify that storage service state is updated'); { const newState = await phone.waitForStorageState({ after: state, }); const { added, removed } = newState.diff(state); assert.strictEqual(added.length, 1, 'only one record must be added'); assert.strictEqual( removed.length, 1, 'only one record must be removed' ); assert.strictEqual( added[0].contact?.serviceUuid, usernameContact.device.uuid ); assert.strictEqual(added[0].contact?.username, ''); assert.strictEqual( removed[0].contact?.serviceUuid, usernameContact.device.uuid ); assert.strictEqual(removed[0].contact?.username, USERNAME); } }); } it('reserves/confirms/deletes username', async () => { const { phone, server } = bootstrap; const window = await app.getWindow(); debug('opening avatar context menu'); await window .locator('.module-main-header .module-Avatar__contents') .click(); debug('opening profile editor'); await window .locator('.module-avatar-popup .module-avatar-popup__profile') .click(); debug('opening username editor'); const profileEditor = window.locator('.ProfileEditor'); await profileEditor.locator('.ProfileEditor__row >> "Username"').click(); debug('skipping onboarding'); await profileEditor.locator('.module-Button >> "Continue"').click(); debug('entering new username'); const usernameField = profileEditor.locator('.Input__input'); await usernameField.type(NICKNAME); debug('waiting for generated discriminator'); const discriminator = profileEditor.locator( '.EditUsernameModalBody__discriminator:not(:empty)' ); await discriminator.waitFor(); const discriminatorValue = await discriminator.innerText(); assert.match(discriminatorValue, /^\.\d+$/); const username = `${NICKNAME}${discriminatorValue}`; debug('saving username'); let state = await phone.expectStorageState('consistency check'); await profileEditor.locator('.module-Button >> "Save"').click(); debug('checking the username is saved'); { await profileEditor .locator(`.ProfileEditor__row >> "${username}"`) .waitFor(); const uuid = await server.lookupByUsername(username); assert.strictEqual(uuid, phone.device.uuid); const newState = await phone.waitForStorageState({ after: state, }); const { added, removed } = newState.diff(state); assert.strictEqual(added.length, 1, 'only one record must be added'); assert.strictEqual(removed.length, 1, 'only one record must be removed'); assert.strictEqual(added[0]?.account?.username, username); state = newState; } debug('deleting username'); await profileEditor .locator('button[aria-label="Copy or delete username"]') .click(); await profileEditor.locator('button[aria-label="Delete"]').click(); await window .locator('.module-Modal .module-Modal__button-footer button >> "Delete"') .click(); await profileEditor.locator('.ProfileEditor__row >> "Username"').waitFor(); debug('confirming username deletion'); { const uuid = await server.lookupByUsername(username); assert.strictEqual(uuid, undefined); const newState = await phone.waitForStorageState({ after: state, }); const { added, removed } = newState.diff(state); assert.strictEqual(added.length, 1, 'only one record must be added'); assert.strictEqual(removed.length, 1, 'only one record must be removed'); assert.strictEqual(added[0]?.account?.username, ''); state = newState; } }); it('looks up contacts by username', async () => { const { desktop, server } = bootstrap; debug('creating a contact with username'); const carl = await server.createPrimaryDevice({ profileName: 'Carl', }); await server.setUsername(carl.device.uuid, CARL_USERNAME); const window = await app.getWindow(); debug('entering username into search field'); await window.locator('button[aria-label="New conversation"]').click(); const searchInput = window.locator('.module-SearchInput__container input'); await searchInput.type(`@${CARL_USERNAME}`); debug('starting lookup'); await window.locator(`button >> "${CARL_USERNAME}"`).click(); debug('sending a message'); { const composeArea = window.locator( '.composition-area-wrapper, .conversation .ConversationView' ); const compositionInput = composeArea.locator( '[data-testid=CompositionInput]' ); await compositionInput.type('Hello Carl'); await compositionInput.press('Enter'); const { body, source } = await carl.waitForMessage(); assert.strictEqual(body, 'Hello Carl'); assert.strictEqual(source, desktop); } }); });