diff --git a/client/go/outline/config/module_test.go b/client/go/outline/config/module_test.go index 09df7f9bfe..cba1551e21 100644 --- a/client/go/outline/config/module_test.go +++ b/client/go/outline/config/module_test.go @@ -57,3 +57,17 @@ func TestRegisterParseURL(t *testing.T) { require.Equal(t, "example.com:4321", d.FirstHop) require.Equal(t, ConnTypeTunneled, d.ConnType) } + +func TestRegisterParseURLInQuotes(t *testing.T) { + providers := RegisterDefaultProviders(NewProviderContainer()) + + node, err := ParseConfigYAML(`"ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpaTXJSMW92ZmRBaEQ@example.com:4321/#My%20Server"`) + require.NoError(t, err) + + d, err := providers.StreamDialers.NewInstance(context.Background(), node) + require.NoError(t, err) + + require.NotNil(t, d.Dial) + require.Equal(t, "example.com:4321", d.FirstHop) + require.Equal(t, ConnTypeTunneled, d.ConnType) +} diff --git a/client/src/www/app/outline_server_repository/config.spec.ts b/client/src/www/app/outline_server_repository/config.spec.ts index 14616e5f1a..9c6bcc528b 100644 --- a/client/src/www/app/outline_server_repository/config.spec.ts +++ b/client/src/www/app/outline_server_repository/config.spec.ts @@ -11,11 +11,11 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -/* -import {makeConfig, SIP002_URI} from 'ShadowsocksConfig'; import * as config from './config'; +import * as method_channel from '../method_channel'; +/* describe('getAddressFromTransport', () => { it('extracts address', () => { expect( @@ -146,24 +146,55 @@ describe('parseTunnelConfig', () => { }); }); }); +*/ -describe('serviceNameFromAccessKey', () => { - it('extracts name from ss:// key', () => { - expect( - config.TEST_ONLY.serviceNameFromAccessKey('ss://anything#My%20Server') - ).toEqual('My Server'); +describe('parseAccessKey', () => { + method_channel.installDefaultMethodChannel({ + async invokeMethod(methodName: string, params: string): Promise { + if (!params) { + throw Error('empty transport config'); + } + if (params.indexOf('invalid') > -1) { + throw Error('fake invalid config'); + } + return 'first-hop:4321'; + }, }); - it('extracts name from ssconf:// key', () => { - expect( - config.TEST_ONLY.serviceNameFromAccessKey('ssconf://anything#My%20Server') - ).toEqual('My Server'); + + it('extracts name from ss:// key', async () => { + const transportConfig = `ss://${encodeURIComponent( + btoa('chacha20-ietf-poly1305:SECRET') + )}@example.com:4321`; + const accessKey = `${transportConfig}#My%20Server`; + expect(await config.TEST_ONLY.parseAccessKey(accessKey)).toEqual( + new config.StaticServiceConfig('My Server', { + firstHop: 'first-hop:4321', + transport: transportConfig, + }) + ); }); - it('ignores parameters', () => { + + it('extracts name from ssconf:// key', async () => { expect( - config.TEST_ONLY.serviceNameFromAccessKey( - 'ss://anything#foo=bar&My%20Server&baz=boo' + await config.TEST_ONLY.parseAccessKey( + 'ssconf://example.com:4321/path#My%20Server' + ) + ).toEqual( + new config.DynamicServiceConfig( + 'My Server', + new URL('https://example.com:4321/path') ) - ).toEqual('My Server'); + ); + }); + + it('name extraction ignores parameters', async () => { + const transportConfig = 'ss://anything'; + const accessKey = `${transportConfig}#foo=bar&My%20Server&baz=boo`; + expect(await config.TEST_ONLY.parseAccessKey(accessKey)).toEqual( + new config.StaticServiceConfig('My Server', { + firstHop: 'first-hop:4321', + transport: transportConfig, + }) + ); }); }); -*/ diff --git a/client/src/www/app/outline_server_repository/config.ts b/client/src/www/app/outline_server_repository/config.ts index 019adea401..e6f61e1aa1 100644 --- a/client/src/www/app/outline_server_repository/config.ts +++ b/client/src/www/app/outline_server_repository/config.ts @@ -17,6 +17,7 @@ import * as method_channel from '@outline/client/src/www/app/method_channel'; import * as errors from '../../model/errors'; export const TEST_ONLY = { + parseAccessKey: parseAccessKey, getAddressFromTransportConfig: getAddressFromTransportConfig, serviceNameFromAccessKey: serviceNameFromAccessKey, }; @@ -32,10 +33,7 @@ export type ServiceConfig = StaticServiceConfig | DynamicServiceConfig; * It's the structured representation of a Static Access Key. */ export class StaticServiceConfig { - constructor( - readonly name: string, - readonly tunnelConfig: TunnelConfigJson - ) {} + constructor(readonly name: string, readonly tunnelConfig: TunnelConfigJson) {} } /** @@ -43,10 +41,7 @@ export class StaticServiceConfig { * It's the structured representation of a Dynamic Access Key. */ export class DynamicServiceConfig { - constructor( - readonly name: string, - readonly transportConfigLocation: URL - ) {} + constructor(readonly name: string, readonly transportConfigLocation: URL) {} } /** @@ -105,84 +100,35 @@ export async function parseTunnelConfig( firstHop: await getAddressFromTransportConfig(tunnelConfigText), transport: tunnelConfigText, }; - - // // const firstHop = validateTunnelConfig(tunnelConfigText) - // // return {firstHop: firstHop, tunnelConfig: tunnelConfigText} - // tunnelConfigText = tunnelConfigText.trim(); - // if (tunnelConfigText.startsWith('ss://')) { - // return staticKeyToTunnelConfig(tunnelConfigText); - // } - - // const responseJson = JSON.parse(tunnelConfigText); - - // if ('error' in responseJson) { - // throw new errors.SessionProviderError( - // responseJson.error.message, - // responseJson.error.details - // ); - // } - - // // TODO(fortuna): stop converting to the Go format. Let the Go code convert. - // // We don't validate the method because that's already done in the Go code as - // // part of the Dynamic Key connection flow. - // const transport: TransportConfigJson = { - // host: responseJson.server, - // port: responseJson.server_port, - // method: responseJson.method, - // password: responseJson.password, - // }; - // if (responseJson.prefix) { - // (transport as {prefix?: string}).prefix = responseJson.prefix; - // } - // return { - // transport, - // firstHop: getAddressFromTransportConfig(transport), - // }; } -/** Parses an access key string into a TunnelConfig object. */ -// function staticKeyToTunnelConfig(staticKey: string): TunnelConfigJson { -// const config = SHADOWSOCKS_URI.parse(staticKey); -// if (!isShadowsocksCipherSupported(config.method.data)) { -// throw new errors.ShadowsocksUnsupportedCipher( -// config.method.data || 'unknown' -// ); -// } -// const transport: TransportConfigJson = { -// host: config.host.data, -// port: config.port.data, -// method: config.method.data, -// password: config.password.data, -// }; -// if (config.extra?.['prefix']) { -// (transport as {prefix?: string}).prefix = config.extra?.['prefix']; -// } -// return { -// transport, -// firstHop: getAddressFromTransportConfig(transport), -// }; -// } - -export async function parseAccessKey( - accessKey: string -): Promise { +export async function parseAccessKey(accessKeyText: string): Promise { try { - accessKey = accessKey.trim(); + const accessKeyUrl = new URL(accessKeyText.trim()); // The default service name is extracted from the URL fragment of the access key. - const name = serviceNameFromAccessKey(accessKey); + const name = serviceNameFromAccessKey(accessKeyUrl); + // The hash only encodes service config, not tunnel config or config location. + const noHashAccessKey = new URL(accessKeyUrl); + noHashAccessKey.hash = ''; // Static ss:// keys. It encodes the full service config. - if (accessKey.startsWith('ss://')) { - return new StaticServiceConfig(name, await parseTunnelConfig(accessKey)); + if (noHashAccessKey.protocol === 'ss:') { + return new StaticServiceConfig( + name, + await parseTunnelConfig(noHashAccessKey.toString()) + ); } // Dynamic ssconf:// keys. It encodes the location of the service config. - if (accessKey.startsWith('ssconf://') || accessKey.startsWith('https://')) { + if ( + noHashAccessKey.protocol === 'ssconf:' || + noHashAccessKey.protocol === 'https:' + ) { try { // URL does not parse the hostname (treats as opaque string) if the protocol is non-standard (e.g. non-http). const configLocation = new URL( - accessKey.replace(/^ssconf:\/\//, 'https://') + noHashAccessKey.toString().replace(/^ssconf:\/\//, 'https://') ); return new DynamicServiceConfig(name, configLocation); } catch (error) { @@ -202,32 +148,17 @@ export function validateAccessKey(accessKey: string) { getAddressFromTransportConfig(accessKey); } -// // We only support AEAD ciphers for Shadowsocks. -// // See https://shadowsocks.org/en/spec/AEAD-Ciphers.html -// const SUPPORTED_SHADOWSOCKS_CIPHERS = [ -// 'chacha20-ietf-poly1305', -// 'aes-128-gcm', -// 'aes-192-gcm', -// 'aes-256-gcm', -// ]; - -// function isShadowsocksCipherSupported(cipher?: string): boolean { -// return SUPPORTED_SHADOWSOCKS_CIPHERS.includes(cipher); -// } - /** * serviceNameFromAccessKey extracts the service name from the access key. * This is done by getting parsing the fragment hash in the URL and returning the * entry that is not a key=value pair. * This is used to name the service card in the UI when the service is added. */ -function serviceNameFromAccessKey(accessKey: string): string | undefined { - const {hash} = new URL(accessKey.replace(/^ss(?:conf)?:\/\//, 'https://')); - - if (!hash) return; +function serviceNameFromAccessKey(accessKey: URL): string | undefined { + if (!accessKey.hash) return; return decodeURIComponent( - hash + accessKey.hash .slice(1) .split('&') .find(keyValuePair => !keyValuePair.includes('=')) diff --git a/client/src/www/app/outline_server_repository/index.ts b/client/src/www/app/outline_server_repository/index.ts index ce10a3f407..4c899e4c6a 100644 --- a/client/src/www/app/outline_server_repository/index.ts +++ b/client/src/www/app/outline_server_repository/index.ts @@ -23,6 +23,15 @@ import * as events from '../../model/events'; import {ServerRepository} from '../../model/server'; import {Server} from '../../model/server'; +// Name by which servers are saved to storage. +const SERVERS_STORAGE_KEY_V0 = 'servers'; +const SERVERS_STORAGE_KEY = 'servers_v1'; + +export const TEST_ONLY = { + SERVERS_STORAGE_KEY_V0: SERVERS_STORAGE_KEY_V0, + SERVERS_STORAGE_KEY: SERVERS_STORAGE_KEY, +}; + // DEPRECATED: V0 server persistence format. interface ServersStorageV0Config { host?: string; @@ -31,6 +40,7 @@ interface ServersStorageV0Config { method?: string; name?: string; } + export interface ServersStorageV0 { [serverId: string]: ServersStorageV0Config; } @@ -63,12 +73,18 @@ type ServerEntry = {accessKey: string; server: Server}; export async function newOutlineServerRepository( vpnApi: VpnApi, - eventQueue: events.EventQueue, - storage: Storage, - localize: Localizer): Promise { + eventQueue: events.EventQueue, + storage: Storage, + localize: Localizer +): Promise { console.debug('OutlineServerRepository is initializing'); - const repo = new OutlineServerRepository(vpnApi, eventQueue, storage, localize); + const repo = new OutlineServerRepository( + vpnApi, + eventQueue, + storage, + localize + ); await loadServers(storage, repo); console.debug('OutlineServerRepository loaded servers'); @@ -104,9 +120,6 @@ export async function newOutlineServerRepository( // Maintains a persisted set of servers and liaises with the core. class OutlineServerRepository implements ServerRepository { - // Name by which servers are saved to storage. - static readonly SERVERS_STORAGE_KEY_V0 = 'servers'; - static readonly SERVERS_STORAGE_KEY = 'servers_v1'; private lastForgottenServer: ServerEntry | null = null; private serverById = new Map(); @@ -115,15 +128,14 @@ class OutlineServerRepository implements ServerRepository { private eventQueue: events.EventQueue, private storage: Storage, private localize: Localizer - ) { - } + ) {} getAll() { return Array.from(this.serverById.values()).map(e => e.server); } getById(serverId: string) { - return this.serverById.get(serverId).server; + return this.serverById.get(serverId)?.server; } async add(accessKey: string) { @@ -131,7 +143,11 @@ class OutlineServerRepository implements ServerRepository { if (alreadyAddedServer) { throw new errors.ServerAlreadyAdded(alreadyAddedServer); } - const server = await this.internalCreateServer(uuidv4(), accessKey, undefined); + const server = await this.internalCreateServer( + uuidv4(), + accessKey, + undefined + ); this.storeServers(); this.eventQueue.enqueue(new events.ServerAdded(server)); @@ -204,7 +220,7 @@ class OutlineServerRepository implements ServerRepository { }); } const json = JSON.stringify(servers); - this.storage.setItem(OutlineServerRepository.SERVERS_STORAGE_KEY, json); + this.storage.setItem(SERVERS_STORAGE_KEY, json); } async internalCreateServer( @@ -224,10 +240,9 @@ class OutlineServerRepository implements ServerRepository { } } - // Loads servers from storage, raising an error if there is any problem loading. async function loadServers(storage: Storage, repo: OutlineServerRepository) { - if (storage.getItem(OutlineServerRepository.SERVERS_STORAGE_KEY)) { + if (storage.getItem(SERVERS_STORAGE_KEY)) { console.debug('server storage migrated to V1'); await loadServersV1(storage, repo); return; @@ -236,9 +251,7 @@ async function loadServers(storage: Storage, repo: OutlineServerRepository) { } async function loadServersV0(storage: Storage, repo: OutlineServerRepository) { - const serversJson = storage.getItem( - OutlineServerRepository.SERVERS_STORAGE_KEY_V0 - ); + const serversJson = storage.getItem(SERVERS_STORAGE_KEY_V0); if (!serversJson) { console.debug('no V0 servers found in storage'); return; @@ -257,7 +270,7 @@ async function loadServersV0(storage: Storage, repo: OutlineServerRepository) { serverId, serversStorageV0ConfigToAccessKey(v0Config), v0Config.name - ) + ); } catch (e) { // Don't propagate so other stored servers can be created. console.error(e); @@ -266,9 +279,7 @@ async function loadServersV0(storage: Storage, repo: OutlineServerRepository) { } async function loadServersV1(storage: Storage, repo: OutlineServerRepository) { - const serversStorageJson = storage.getItem( - OutlineServerRepository.SERVERS_STORAGE_KEY - ); + const serversStorageJson = storage.getItem(SERVERS_STORAGE_KEY); if (!serversStorageJson) { console.debug('no servers found in storage'); return; diff --git a/client/src/www/app/outline_server_repository/outline_server_repository.spec.ts b/client/src/www/app/outline_server_repository/outline_server_repository.spec.ts index eda01ec090..07d704944b 100644 --- a/client/src/www/app/outline_server_repository/outline_server_repository.spec.ts +++ b/client/src/www/app/outline_server_repository/outline_server_repository.spec.ts @@ -16,13 +16,13 @@ import {InMemoryStorage} from '@outline/infrastructure/memory_storage'; import {makeConfig, SIP002_URI} from 'ShadowsocksConfig'; import { - OutlineServerRepository, + newOutlineServerRepository, ServersStorageV0, ServersStorageV1, serversStorageV0ConfigToAccessKey, + TEST_ONLY, } from '.'; import * as config from './config'; -import {OutlineServer} from './server'; import {FakeVpnApi} from './vpn.fake'; import {ServerAccessKeyInvalid} from '../../model/errors'; import { @@ -32,6 +32,7 @@ import { ServerForgotten, ServerRenamed, } from '../../model/events'; +import {ServerRepository, Server} from '../../model/server'; // TODO(alalama): unit tests for OutlineServer. @@ -52,33 +53,22 @@ describe('OutlineServerRepository', () => { name: 'fake server 1', }; - it('loads V0 servers', () => { + it('loads V0 servers', async () => { const storageV0: ServersStorageV0 = { 'server-0': CONFIG_0_V0, 'server-1': CONFIG_1_V0, }; const storage = new InMemoryStorage( - new Map([ - [ - OutlineServerRepository.SERVERS_STORAGE_KEY_V0, - JSON.stringify(storageV0), - ], - ]) + new Map([[TEST_ONLY.SERVERS_STORAGE_KEY_V0, JSON.stringify(storageV0)]]) ); - const repo = newTestRepo(new EventQueue(), storage); + const repo = await newTestRepo(new EventQueue(), storage); const server0 = repo.getById('server-0'); - expect(server0?.accessKey).toEqual( - serversStorageV0ConfigToAccessKey(CONFIG_0_V0) - ); expect(server0?.name).toEqual(CONFIG_0_V0.name); const server1 = repo.getById('server-1'); - expect(server1?.accessKey).toEqual( - serversStorageV0ConfigToAccessKey(CONFIG_1_V0) - ); expect(server1?.name).toEqual(CONFIG_1_V0.name); }); - it('loads V1 servers', () => { + it('loads V1 servers', async () => { // Store V0 servers with different ids. const storageV0: ServersStorageV0 = { 'v0-server-0': CONFIG_0_V0, @@ -98,50 +88,33 @@ describe('OutlineServerRepository', () => { ]; const storage = new InMemoryStorage( new Map([ - [ - OutlineServerRepository.SERVERS_STORAGE_KEY_V0, - JSON.stringify(storageV0), - ], - [ - OutlineServerRepository.SERVERS_STORAGE_KEY, - JSON.stringify(storageV1), - ], + [TEST_ONLY.SERVERS_STORAGE_KEY_V0, JSON.stringify(storageV0)], + [TEST_ONLY.SERVERS_STORAGE_KEY, JSON.stringify(storageV1)], ]) ); - const repo = newTestRepo(new EventQueue(), storage); + const repo = await newTestRepo(new EventQueue(), storage); const server0 = repo.getById('server-0'); - expect(server0?.accessKey).toEqual( - serversStorageV0ConfigToAccessKey(CONFIG_0_V0) - ); expect(server0?.name).toEqual(CONFIG_0_V0.name); const server1 = repo.getById('server-1'); - expect(server1?.accessKey).toEqual( - serversStorageV0ConfigToAccessKey(CONFIG_1_V0) - ); expect(server1?.name).toEqual('renamed server'); }); - it('stores V1 servers', () => { + it('stores V1 servers', async () => { const storageV0: ServersStorageV0 = { 'server-0': {...CONFIG_0_V0, name: CONFIG_0_V0.name}, 'server-1': {...CONFIG_1_V0, name: CONFIG_1_V0.name}, }; const storage = new InMemoryStorage( - new Map([ - [ - OutlineServerRepository.SERVERS_STORAGE_KEY_V0, - JSON.stringify(storageV0), - ], - ]) + new Map([[TEST_ONLY.SERVERS_STORAGE_KEY_V0, JSON.stringify(storageV0)]]) ); - const repo = newTestRepo(new EventQueue(), storage); + const repo = await newTestRepo(new EventQueue(), storage); // Trigger storage change. repo.forget('server-1'); repo.undoForget('server-1'); - const serversJson = JSON.parse( - storage.getItem(OutlineServerRepository.SERVERS_STORAGE_KEY) - ); + const item = storage.getItem(TEST_ONLY.SERVERS_STORAGE_KEY) ?? ''; + expect(item).toBeTruthy; + const serversJson = JSON.parse(item); expect(serversJson).toContain({ id: 'server-0', name: 'fake server 0', @@ -154,16 +127,16 @@ describe('OutlineServerRepository', () => { }); }); - it('add stores servers', () => { + it('add stores servers', async () => { const storage = new InMemoryStorage(); - const repo = newTestRepo(new EventQueue(), storage); + const repo = await newTestRepo(new EventQueue(), storage); const accessKey0 = serversStorageV0ConfigToAccessKey(CONFIG_0_V0); const accessKey1 = serversStorageV0ConfigToAccessKey(CONFIG_1_V0); - repo.add(accessKey0); - repo.add(accessKey1); - const servers: ServersStorageV1 = JSON.parse( - storage.getItem(OutlineServerRepository.SERVERS_STORAGE_KEY) - ); + await repo.add(accessKey0); + await repo.add(accessKey1); + const item = storage.getItem(TEST_ONLY.SERVERS_STORAGE_KEY) ?? ''; + expect(item).toBeTruthy; + const servers: ServersStorageV1 = JSON.parse(item); expect(servers.length).toEqual(2); expect(servers[0].accessKey).toEqual(accessKey0); expect(servers[0].name).toEqual(CONFIG_0_V0.name); @@ -171,15 +144,14 @@ describe('OutlineServerRepository', () => { expect(servers[1].name).toEqual(CONFIG_1_V0.name); }); - it('add emits ServerAdded event', () => { + it('add emits ServerAdded event', async () => { const eventQueue = new EventQueue(); - const repo = newTestRepo(eventQueue, new InMemoryStorage()); + const repo = await newTestRepo(eventQueue, new InMemoryStorage()); const accessKey = serversStorageV0ConfigToAccessKey(CONFIG_0_V0); - repo.add(accessKey); + await repo.add(accessKey); let didEmitServerAddedEvent = false; eventQueue.subscribe(ServerAdded, (event: ServerAdded) => { - const server = event.server as OutlineServer; - expect(server.accessKey).toEqual(accessKey); + const server = event.server as Server; expect(server.name).toEqual(CONFIG_0_V0.name); didEmitServerAddedEvent = true; }); @@ -187,68 +159,66 @@ describe('OutlineServerRepository', () => { expect(didEmitServerAddedEvent).toBeTruthy(); }); - it('add throws on invalid access keys', () => { - const repo = newTestRepo(new EventQueue(), new InMemoryStorage()); - expect(() => repo.add('ss://invalid')).toThrowError(ServerAccessKeyInvalid); - expect(() => repo.add('')).toThrowError(ServerAccessKeyInvalid); + it('add throws on invalid access keys', async () => { + const repo = await newTestRepo(new EventQueue(), new InMemoryStorage()); + expect(async () => await repo.add('ss://invalid')).toThrowError( + ServerAccessKeyInvalid + ); + expect(async () => await repo.add('')).toThrowError(ServerAccessKeyInvalid); }); - it('getAll returns added servers', () => { - const repo = newTestRepo(new EventQueue(), new InMemoryStorage()); + it('getAll returns added servers', async () => { + const repo = await newTestRepo(new EventQueue(), new InMemoryStorage()); expect(repo.getAll()).toEqual([]); const accessKey0 = serversStorageV0ConfigToAccessKey(CONFIG_0_V0); const accessKey1 = serversStorageV0ConfigToAccessKey(CONFIG_1_V0); - repo.add(accessKey0); - repo.add(accessKey1); + await repo.add(accessKey0); + await repo.add(accessKey1); const servers = repo.getAll(); expect(servers.length).toEqual(2); - const accessKeys = servers.map(s => s.accessKey); - expect(accessKeys).toContain(accessKey0); - expect(accessKeys).toContain(accessKey1); const serverNames = servers.map(s => s.name); expect(serverNames).toContain(CONFIG_0_V0.name); expect(serverNames).toContain(CONFIG_1_V0.name); }); - it('getById retrieves added servers', () => { - const repo = newTestRepo(new EventQueue(), new InMemoryStorage()); + it('getById retrieves added servers', async () => { + const repo = await newTestRepo(new EventQueue(), new InMemoryStorage()); const accessKey = serversStorageV0ConfigToAccessKey(CONFIG_0_V0); - repo.add(accessKey); + await repo.add(accessKey); const serverId = repo.getAll()[0].id; const server = repo.getById(serverId); expect(server?.id).toEqual(serverId); - expect(server?.accessKey).toEqual(accessKey); expect(server?.name).toEqual(CONFIG_0_V0.name); }); - it('getById returns undefined for nonexistent servers', () => { - const repo = newTestRepo(new EventQueue(), new InMemoryStorage()); + it('getById returns undefined for nonexistent servers', async () => { + const repo = await newTestRepo(new EventQueue(), new InMemoryStorage()); expect(repo.getById('server-does-not-exist')).toBeUndefined(); expect(repo.getById('')).toBeUndefined(); }); - it('renames servers', () => { + it('renames servers', async () => { const NEW_SERVER_NAME = 'new server name'; const storage = new InMemoryStorage(); - const repo = newTestRepo(new EventQueue(), storage); - repo.add(serversStorageV0ConfigToAccessKey(CONFIG_0_V0)); + const repo = await newTestRepo(new EventQueue(), storage); + await repo.add(serversStorageV0ConfigToAccessKey(CONFIG_0_V0)); const server = repo.getAll()[0]; repo.rename(server.id, NEW_SERVER_NAME); expect(server.name).toEqual(NEW_SERVER_NAME); - const serversStorage: ServersStorageV1 = JSON.parse( - storage.getItem(OutlineServerRepository.SERVERS_STORAGE_KEY) - ); + const item = storage.getItem(TEST_ONLY.SERVERS_STORAGE_KEY) ?? ''; + expect(item).toBeTruthy; + const serversStorage: ServersStorageV1 = JSON.parse(item); const serverNames = serversStorage.map(s => s.name); expect(serverNames).toContain(NEW_SERVER_NAME); }); - it('rename emits ServerRenamed event', () => { + it('rename emits ServerRenamed event', async () => { const NEW_SERVER_NAME = 'new server name'; const eventQueue = new EventQueue(); eventQueue.subscribe(ServerAdded, () => {}); // Silence dropped event warnings. - const repo = newTestRepo(eventQueue, new InMemoryStorage()); + const repo = await newTestRepo(eventQueue, new InMemoryStorage()); const accessKey = serversStorageV0ConfigToAccessKey(CONFIG_0_V0); - repo.add(accessKey); + await repo.add(accessKey); const server = repo.getAll()[0]; repo.rename(server.id, NEW_SERVER_NAME); let didEmitServerRenamedEvent = false; @@ -260,30 +230,30 @@ describe('OutlineServerRepository', () => { expect(didEmitServerRenamedEvent).toBeTruthy(); }); - it('forgets servers', () => { + it('forgets servers', async () => { const storage = new InMemoryStorage(); - const repo = newTestRepo(new EventQueue(), storage); - repo.add(serversStorageV0ConfigToAccessKey(CONFIG_0_V0)); - repo.add(serversStorageV0ConfigToAccessKey(CONFIG_1_V0)); + const repo = await newTestRepo(new EventQueue(), storage); + await repo.add(serversStorageV0ConfigToAccessKey(CONFIG_0_V0)); + await repo.add(serversStorageV0ConfigToAccessKey(CONFIG_1_V0)); const forgottenServerId = repo.getAll()[0].id; repo.forget(forgottenServerId); expect(repo.getById(forgottenServerId)).toBeUndefined(); const serverIds = repo.getAll().map(s => s.id); expect(serverIds.length).toEqual(1); expect(serverIds).not.toContain(forgottenServerId); - const serversStorage: ServersStorageV1 = JSON.parse( - storage.getItem(OutlineServerRepository.SERVERS_STORAGE_KEY) - ); + const item = storage.getItem(TEST_ONLY.SERVERS_STORAGE_KEY) ?? ''; + expect(item).toBeTruthy; + const serversStorage: ServersStorageV1 = JSON.parse(item); const serverIdsStorage = serversStorage.map(s => s.id); expect(serverIdsStorage).not.toContain(forgottenServerId); }); - it('forget emits ServerForgotten events', () => { + it('forget emits ServerForgotten events', async () => { const eventQueue = new EventQueue(); eventQueue.subscribe(ServerAdded, () => {}); // Silence dropped event warnings. - const repo = newTestRepo(eventQueue, new InMemoryStorage()); - repo.add(serversStorageV0ConfigToAccessKey(CONFIG_0_V0)); - repo.add(serversStorageV0ConfigToAccessKey(CONFIG_1_V0)); + const repo = await newTestRepo(eventQueue, new InMemoryStorage()); + await repo.add(serversStorageV0ConfigToAccessKey(CONFIG_0_V0)); + await repo.add(serversStorageV0ConfigToAccessKey(CONFIG_1_V0)); const forgottenServerId = repo.getAll()[0].id; repo.forget(forgottenServerId); let didEmitServerForgottenEvent = false; @@ -295,11 +265,11 @@ describe('OutlineServerRepository', () => { expect(didEmitServerForgottenEvent).toBeTruthy(); }); - it('undoes forgetting servers', () => { + it('undoes forgetting servers', async () => { const storage = new InMemoryStorage(); - const repo = newTestRepo(new EventQueue(), storage); - repo.add(serversStorageV0ConfigToAccessKey(CONFIG_0_V0)); - repo.add(serversStorageV0ConfigToAccessKey(CONFIG_1_V0)); + const repo = await newTestRepo(new EventQueue(), storage); + await repo.add(serversStorageV0ConfigToAccessKey(CONFIG_0_V0)); + await repo.add(serversStorageV0ConfigToAccessKey(CONFIG_1_V0)); const forgottenServerId = repo.getAll()[0].id; repo.forget(forgottenServerId); repo.undoForget(forgottenServerId); @@ -308,21 +278,21 @@ describe('OutlineServerRepository', () => { const serverIds = repo.getAll().map(s => s.id); expect(serverIds.length).toEqual(2); expect(serverIds).toContain(forgottenServerId); - const serversStorage: ServersStorageV1 = JSON.parse( - storage.getItem(OutlineServerRepository.SERVERS_STORAGE_KEY) - ); + const item = storage.getItem(TEST_ONLY.SERVERS_STORAGE_KEY) ?? ''; + expect(item).toBeTruthy; + const serversStorage: ServersStorageV1 = JSON.parse(item); const serverIdsStorage = serversStorage.map(s => s.id); expect(serverIdsStorage).toContain(forgottenServerId); }); - it('undoForget emits ServerForgetUndone events', () => { + it('undoForget emits ServerForgetUndone events', async () => { const eventQueue = new EventQueue(); // Silence dropped event warnings. eventQueue.subscribe(ServerAdded, () => {}); eventQueue.subscribe(ServerForgotten, () => {}); - const repo = newTestRepo(eventQueue, new InMemoryStorage()); - repo.add(serversStorageV0ConfigToAccessKey(CONFIG_0_V0)); - repo.add(serversStorageV0ConfigToAccessKey(CONFIG_1_V0)); + const repo = await newTestRepo(eventQueue, new InMemoryStorage()); + await repo.add(serversStorageV0ConfigToAccessKey(CONFIG_0_V0)); + await repo.add(serversStorageV0ConfigToAccessKey(CONFIG_1_V0)); const forgottenServerId = repo.getAll()[0].id; repo.forget(forgottenServerId); repo.undoForget(forgottenServerId); @@ -335,60 +305,63 @@ describe('OutlineServerRepository', () => { expect(didEmitServerForgetUndoneEvent).toBeTruthy(); }); - it('validates static access keys', () => { + it('validates static access keys', async () => { // Invalid access keys. - expect(() => config.validateAccessKey('')).toThrowError( - ServerAccessKeyInvalid - ); - expect(() => config.validateAccessKey('ss://invalid')).toThrowError( + expect(async () => await config.validateAccessKey('')).toThrowError( ServerAccessKeyInvalid ); + expect( + async () => await config.validateAccessKey('ss://invalid') + ).toThrowError(ServerAccessKeyInvalid); // IPv6 host. - expect(() => - config.validateAccessKey( - SIP002_URI.stringify( - makeConfig({ - host: '2001:0:ce49:7601:e866:efff:62c3:fffe', - port: 443, - password: 'test', - method: 'chacha20-ietf-poly1305', - }) + expect( + async () => + await config.validateAccessKey( + SIP002_URI.stringify( + makeConfig({ + host: '2001:0:ce49:7601:e866:efff:62c3:fffe', + port: 443, + password: 'test', + method: 'chacha20-ietf-poly1305', + }) + ) ) - ) ).toBeTruthy(); // Unsupported ciphers. - expect(() => - config.validateAccessKey( - SIP002_URI.stringify( - makeConfig({ - host: '127.0.0.1', - port: 443, - password: 'test', - method: 'aes-256-ctr', - }) + expect( + async () => + await config.validateAccessKey( + SIP002_URI.stringify( + makeConfig({ + host: '127.0.0.1', + port: 443, + password: 'test', + method: 'aes-256-ctr', + }) + ) ) - ) ).toThrowError(ServerAccessKeyInvalid); - expect(() => - config.validateAccessKey( - SIP002_URI.stringify( - makeConfig({ - host: '127.0.0.1', - port: 443, - password: 'test', - method: 'chacha20', - }) + expect( + async () => + await config.validateAccessKey( + SIP002_URI.stringify( + makeConfig({ + host: '127.0.0.1', + port: 443, + password: 'test', + method: 'chacha20', + }) + ) ) - ) ).toThrowError(ServerAccessKeyInvalid); }); }); -function newTestRepo( +async function newTestRepo( eventQueue: EventQueue, storage: Storage -): OutlineServerRepository { - return new OutlineServerRepository( +): Promise { + return await newOutlineServerRepository( new FakeVpnApi(), eventQueue, storage, diff --git a/client/src/www/views/contact_view/support_form/index.spec.ts b/client/src/www/views/contact_view/support_form/index.spec.ts index c0b3df860a..0fcdc21f7c 100644 --- a/client/src/www/views/contact_view/support_form/index.spec.ts +++ b/client/src/www/views/contact_view/support_form/index.spec.ts @@ -40,6 +40,7 @@ describe('SupportForm', () => { accessKeySource: 'a friend', subject: 'Test Subject', description: 'Test Description', + outreachConsent: false, }; const el = await fixture(html` `);