From d345a439e35b4a55b32706b391a5afa750e2d37e Mon Sep 17 00:00:00 2001
From: Chris Smith <1979423+chris13524@users.noreply.github.com>
Date: Mon, 19 Aug 2024 14:19:14 -0400
Subject: [PATCH] chore: Verify API tests (#2467)
---
apps/laboratory/package.json | 2 +-
.../pages/library/verify-domain-mismatch.tsx | 47 +++++++
.../src/pages/library/verify-evil.tsx | 55 ++++++++
.../src/pages/library/verify-valid.tsx | 56 ++++++++
.../pages/library/wagmi-permissions-async.tsx | 2 +-
.../pages/library/wagmi-permissions-sync.tsx | 2 +-
apps/laboratory/src/utils/WagmiConstants.ts | 8 +-
.../tests/shared/fixtures/w3m-fixture.ts | 6 -
.../w3m-verify-domain-mismatch-fixture.ts | 12 ++
.../fixtures/w3m-verify-evil-fixture.ts | 12 ++
.../fixtures/w3m-verify-valid-fixture.ts | 12 ++
.../tests/shared/pages/ModalPage.ts | 23 +++-
apps/laboratory/tests/shared/utils/verify.ts | 38 ++++++
apps/laboratory/tests/verify.spec.ts | 125 ++++++++++++++++++
14 files changed, 384 insertions(+), 16 deletions(-)
create mode 100644 apps/laboratory/src/pages/library/verify-domain-mismatch.tsx
create mode 100644 apps/laboratory/src/pages/library/verify-evil.tsx
create mode 100644 apps/laboratory/src/pages/library/verify-valid.tsx
create mode 100644 apps/laboratory/tests/shared/fixtures/w3m-verify-domain-mismatch-fixture.ts
create mode 100644 apps/laboratory/tests/shared/fixtures/w3m-verify-evil-fixture.ts
create mode 100644 apps/laboratory/tests/shared/fixtures/w3m-verify-valid-fixture.ts
create mode 100644 apps/laboratory/tests/shared/utils/verify.ts
create mode 100644 apps/laboratory/tests/verify.spec.ts
diff --git a/apps/laboratory/package.json b/apps/laboratory/package.json
index 2a78e67758..62023f71d0 100644
--- a/apps/laboratory/package.json
+++ b/apps/laboratory/package.json
@@ -11,7 +11,7 @@
"playwright:start": "pnpm start",
"playwright:install": "playwright install --with-deps",
"synpress": "synpress ./tests/wallet-setup",
- "playwright:test": "playwright test --grep-invert 'metamask.spec.ts'",
+ "playwright:test": "playwright test",
"playwright:test:metamask": "playwright test --grep 'metamask.spec.ts'",
"playwright:test:basic": "playwright test --grep 'basic-tests.spec.ts'",
"playwright:test:wallet": "playwright test --grep 'wallet.spec.ts'",
diff --git a/apps/laboratory/src/pages/library/verify-domain-mismatch.tsx b/apps/laboratory/src/pages/library/verify-domain-mismatch.tsx
new file mode 100644
index 0000000000..2c2c097b38
--- /dev/null
+++ b/apps/laboratory/src/pages/library/verify-domain-mismatch.tsx
@@ -0,0 +1,47 @@
+import { createWeb3Modal } from '@web3modal/wagmi/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { useEffect, useState } from 'react'
+import { WagmiProvider } from 'wagmi'
+import { AppKitButtons } from '../../components/AppKitButtons'
+import { WagmiTests } from '../../components/Wagmi/WagmiTests'
+import { ThemeStore } from '../../utils/StoreUtil'
+import { ConstantsUtil } from '../../utils/ConstantsUtil'
+import { WagmiModalInfo } from '../../components/Wagmi/WagmiModalInfo'
+import { getWagmiConfig } from '../../utils/WagmiConstants'
+
+// Special project ID with verify enabled on localhost
+const projectId = 'e4eae1aad4503db9966a04fd045a7e4d'
+
+const queryClient = new QueryClient()
+
+const wagmiConfig = getWagmiConfig('default', {
+ projectId
+})
+
+const modal = createWeb3Modal({
+ wagmiConfig,
+ projectId,
+ metadata: ConstantsUtil.Metadata,
+ termsConditionsUrl: 'https://walletconnect.com/terms',
+ privacyPolicyUrl: 'https://walletconnect.com/privacy'
+})
+
+ThemeStore.setModal(modal)
+
+export default function Wagmi() {
+ const [ready, setReady] = useState(false)
+
+ useEffect(() => {
+ setReady(true)
+ }, [])
+
+ return ready ? (
+
+
+
+
+
+
+
+ ) : null
+}
diff --git a/apps/laboratory/src/pages/library/verify-evil.tsx b/apps/laboratory/src/pages/library/verify-evil.tsx
new file mode 100644
index 0000000000..f9d7905a2c
--- /dev/null
+++ b/apps/laboratory/src/pages/library/verify-evil.tsx
@@ -0,0 +1,55 @@
+import { createWeb3Modal } from '@web3modal/wagmi/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { useEffect, useState } from 'react'
+import { WagmiProvider } from 'wagmi'
+import { AppKitButtons } from '../../components/AppKitButtons'
+import { WagmiTests } from '../../components/Wagmi/WagmiTests'
+import { ThemeStore } from '../../utils/StoreUtil'
+import { WagmiModalInfo } from '../../components/Wagmi/WagmiModalInfo'
+import { getWagmiConfig } from '../../utils/WagmiConstants'
+
+const metadata = {
+ name: 'Evil Web3Modal',
+ description: 'Evil Web3Modal Laboratory',
+ url: 'https://malicious-app-verify-simulation.vercel.app/',
+ icons: ['https://avatars.githubusercontent.com/u/37784886'],
+ verifyUrl: ''
+}
+
+// Special project ID with https://malicious-app-verify-simulation.vercel.app/ as the verified domain and this domain is marked as a scam
+const projectId = '9d176efa3150a1df0a76c8c138b6b657'
+
+const queryClient = new QueryClient()
+
+const wagmiConfig = getWagmiConfig('default', {
+ projectId,
+ metadata
+})
+
+const modal = createWeb3Modal({
+ wagmiConfig,
+ projectId,
+ metadata,
+ termsConditionsUrl: 'https://walletconnect.com/terms',
+ privacyPolicyUrl: 'https://walletconnect.com/privacy'
+})
+
+ThemeStore.setModal(modal)
+
+export default function Wagmi() {
+ const [ready, setReady] = useState(false)
+
+ useEffect(() => {
+ setReady(true)
+ }, [])
+
+ return ready ? (
+
+
+
+
+
+
+
+ ) : null
+}
diff --git a/apps/laboratory/src/pages/library/verify-valid.tsx b/apps/laboratory/src/pages/library/verify-valid.tsx
new file mode 100644
index 0000000000..7253dfcd41
--- /dev/null
+++ b/apps/laboratory/src/pages/library/verify-valid.tsx
@@ -0,0 +1,56 @@
+import { createWeb3Modal } from '@web3modal/wagmi/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { useEffect, useState } from 'react'
+import { WagmiProvider } from 'wagmi'
+import { AppKitButtons } from '../../components/AppKitButtons'
+import { WagmiTests } from '../../components/Wagmi/WagmiTests'
+import { ThemeStore } from '../../utils/StoreUtil'
+import { WagmiModalInfo } from '../../components/Wagmi/WagmiModalInfo'
+import { getWagmiConfig } from '../../utils/WagmiConstants'
+
+const metadata = {
+ name: 'Web3Modal',
+ description: 'Web3Modal Laboratory',
+ // Allow localhost
+ url: 'http://localhost:3000',
+ icons: ['https://avatars.githubusercontent.com/u/37784886'],
+ verifyUrl: ''
+}
+
+// Special project ID with verify enabled on localhost
+const projectId = 'e4eae1aad4503db9966a04fd045a7e4d'
+
+const queryClient = new QueryClient()
+
+const wagmiConfig = getWagmiConfig('default', {
+ projectId,
+ metadata
+})
+
+const modal = createWeb3Modal({
+ wagmiConfig,
+ projectId,
+ metadata,
+ termsConditionsUrl: 'https://walletconnect.com/terms',
+ privacyPolicyUrl: 'https://walletconnect.com/privacy'
+})
+
+ThemeStore.setModal(modal)
+
+export default function Wagmi() {
+ const [ready, setReady] = useState(false)
+
+ useEffect(() => {
+ setReady(true)
+ }, [])
+
+ return ready ? (
+
+
+
+
+
+
+
+ ) : null
+}
diff --git a/apps/laboratory/src/pages/library/wagmi-permissions-async.tsx b/apps/laboratory/src/pages/library/wagmi-permissions-async.tsx
index 1007d3e578..88f9bfe1f6 100644
--- a/apps/laboratory/src/pages/library/wagmi-permissions-async.tsx
+++ b/apps/laboratory/src/pages/library/wagmi-permissions-async.tsx
@@ -20,7 +20,7 @@ const connectors = [
optionalMethods: [...OPTIONAL_METHODS, 'wallet_grantPermissions']
})
]
-const wagmiEmailConfig = getWagmiConfig('email', connectors)
+const wagmiEmailConfig = getWagmiConfig('email', { connectors })
const modal = createWeb3Modal({
wagmiConfig: wagmiEmailConfig,
projectId: ConstantsUtil.ProjectId,
diff --git a/apps/laboratory/src/pages/library/wagmi-permissions-sync.tsx b/apps/laboratory/src/pages/library/wagmi-permissions-sync.tsx
index e28c634f0c..d320566bab 100644
--- a/apps/laboratory/src/pages/library/wagmi-permissions-sync.tsx
+++ b/apps/laboratory/src/pages/library/wagmi-permissions-sync.tsx
@@ -20,7 +20,7 @@ const connectors = [
optionalMethods: [...OPTIONAL_METHODS, 'wallet_grantPermissions']
})
]
-const wagmiEmailConfig = getWagmiConfig('email', connectors)
+const wagmiEmailConfig = getWagmiConfig('email', { connectors })
const modal = createWeb3Modal({
wagmiConfig: wagmiEmailConfig,
projectId: ConstantsUtil.ProjectId,
diff --git a/apps/laboratory/src/utils/WagmiConstants.ts b/apps/laboratory/src/utils/WagmiConstants.ts
index b02e82914c..361a2e620e 100644
--- a/apps/laboratory/src/utils/WagmiConstants.ts
+++ b/apps/laboratory/src/utils/WagmiConstants.ts
@@ -18,7 +18,6 @@ import {
type Chain
} from 'wagmi/chains'
import { ConstantsUtil } from './ConstantsUtil'
-import type { CreateConnectorFn } from 'wagmi'
export const WagmiConstantsUtil = {
chains: [
@@ -40,21 +39,20 @@ export const WagmiConstantsUtil = {
] as [Chain, ...Chain[]]
}
-export function getWagmiConfig(type: 'default' | 'email', connectors: CreateConnectorFn[] = []) {
+export function getWagmiConfig(type: 'default' | 'email', override = {}) {
const config = {
chains: WagmiConstantsUtil.chains,
projectId: ConstantsUtil.ProjectId,
metadata: ConstantsUtil.Metadata,
ssr: true,
- connectors
+ ...override
}
const emailConfig = {
...config,
auth: {
socials: ['google', 'x', 'discord', 'farcaster', 'github', 'apple', 'facebook']
- },
- connectors
+ }
}
const wagmiConfig = defaultWagmiConfig(type === 'email' ? emailConfig : config)
diff --git a/apps/laboratory/tests/shared/fixtures/w3m-fixture.ts b/apps/laboratory/tests/shared/fixtures/w3m-fixture.ts
index 4e4b7613d5..2ae08d7c79 100644
--- a/apps/laboratory/tests/shared/fixtures/w3m-fixture.ts
+++ b/apps/laboratory/tests/shared/fixtures/w3m-fixture.ts
@@ -1,14 +1,12 @@
/* eslint no-console: 0 */
import { ModalPage } from '../pages/ModalPage'
-import { ModalValidator } from '../validators/ModalValidator'
import { timeStart, timeEnd } from '../utils/logs'
import { timingFixture } from './timing-fixture'
// Declare the types of fixtures to use
export interface ModalFixture {
modalPage: ModalPage
- modalValidator: ModalValidator
library: string
}
@@ -32,10 +30,6 @@ export const testMSiwe = timingFixture.extend({
const modalPage = new ModalPage(page, library, 'siwe')
await modalPage.load()
await use(modalPage)
- },
- modalValidator: async ({ modalPage }, use) => {
- const modalValidator = new ModalValidator(modalPage.page)
- await use(modalValidator)
}
})
diff --git a/apps/laboratory/tests/shared/fixtures/w3m-verify-domain-mismatch-fixture.ts b/apps/laboratory/tests/shared/fixtures/w3m-verify-domain-mismatch-fixture.ts
new file mode 100644
index 0000000000..e87f4a3707
--- /dev/null
+++ b/apps/laboratory/tests/shared/fixtures/w3m-verify-domain-mismatch-fixture.ts
@@ -0,0 +1,12 @@
+import type { ModalFixture } from './w3m-fixture'
+import { ModalPage } from '../pages/ModalPage'
+import { timingFixture } from './timing-fixture'
+
+export const testMVerifyDomainMismatch = timingFixture.extend({
+ library: ['wagmi', { option: true }],
+ modalPage: async ({ page, library }, use) => {
+ const modalPage = new ModalPage(page, library, 'verify-domain-mismatch')
+ await modalPage.load()
+ await use(modalPage)
+ }
+})
diff --git a/apps/laboratory/tests/shared/fixtures/w3m-verify-evil-fixture.ts b/apps/laboratory/tests/shared/fixtures/w3m-verify-evil-fixture.ts
new file mode 100644
index 0000000000..9e65c6e300
--- /dev/null
+++ b/apps/laboratory/tests/shared/fixtures/w3m-verify-evil-fixture.ts
@@ -0,0 +1,12 @@
+import type { ModalFixture } from './w3m-fixture'
+import { ModalPage } from '../pages/ModalPage'
+import { timingFixture } from './timing-fixture'
+
+export const testMVerifyEvil = timingFixture.extend({
+ library: ['wagmi', { option: true }],
+ modalPage: async ({ page, library }, use) => {
+ const modalPage = new ModalPage(page, library, 'verify-evil')
+ await modalPage.load()
+ await use(modalPage)
+ }
+})
diff --git a/apps/laboratory/tests/shared/fixtures/w3m-verify-valid-fixture.ts b/apps/laboratory/tests/shared/fixtures/w3m-verify-valid-fixture.ts
new file mode 100644
index 0000000000..8b42c63bb0
--- /dev/null
+++ b/apps/laboratory/tests/shared/fixtures/w3m-verify-valid-fixture.ts
@@ -0,0 +1,12 @@
+import type { ModalFixture } from './w3m-fixture'
+import { ModalPage } from '../pages/ModalPage'
+import { timingFixture } from './timing-fixture'
+
+export const testMVerifyValid = timingFixture.extend({
+ library: ['wagmi', { option: true }],
+ modalPage: async ({ page, library }, use) => {
+ const modalPage = new ModalPage(page, library, 'verify-valid')
+ await modalPage.load()
+ await use(modalPage)
+ }
+})
diff --git a/apps/laboratory/tests/shared/pages/ModalPage.ts b/apps/laboratory/tests/shared/pages/ModalPage.ts
index 81a02a4a35..4cc2aa2658 100644
--- a/apps/laboratory/tests/shared/pages/ModalPage.ts
+++ b/apps/laboratory/tests/shared/pages/ModalPage.ts
@@ -8,13 +8,28 @@ import { DeviceRegistrationPage } from './DeviceRegistrationPage'
import type { TimingRecords } from '../fixtures/timing-fixture'
import { WalletPage } from './WalletPage'
import { WalletValidator } from '../validators/WalletValidator'
+import { routeInterceptUrl } from '../utils/verify'
-export type ModalFlavor = 'default' | 'siwe' | 'email' | 'wallet' | 'external' | 'all'
+const maliciousUrl = 'https://malicious-app-verify-simulation.vercel.app'
+
+export type ModalFlavor =
+ | 'default'
+ | 'siwe'
+ | 'email'
+ | 'wallet'
+ | 'external'
+ | 'verify-valid'
+ | 'verify-domain-mismatch'
+ | 'verify-evil'
+ | 'all'
function getUrlByFlavor(baseUrl: string, library: string, flavor: ModalFlavor) {
const urlsByFlavor: Partial> = {
default: `${baseUrl}library/${library}/`,
- external: `${baseUrl}library/external/`
+ external: `${baseUrl}library/external/`,
+ 'verify-valid': `${baseUrl}library/verify-valid/`,
+ 'verify-domain-mismatch': `${baseUrl}library/verify-domain-mismatch/`,
+ 'verify-evil': maliciousUrl
}
return urlsByFlavor[flavor] || `${baseUrl}library/${library}-${flavor}/`
@@ -37,6 +52,10 @@ export class ModalPage {
}
async load() {
+ if (this.flavor === 'verify-evil') {
+ await routeInterceptUrl(this.page, maliciousUrl, this.baseURL, '/library/verify-evil/')
+ }
+
await this.page.goto(this.url)
}
diff --git a/apps/laboratory/tests/shared/utils/verify.ts b/apps/laboratory/tests/shared/utils/verify.ts
new file mode 100644
index 0000000000..eaccb4dee0
--- /dev/null
+++ b/apps/laboratory/tests/shared/utils/verify.ts
@@ -0,0 +1,38 @@
+import type { Page } from '@playwright/test'
+
+/*
+ * This function makes requests to the intercept URL be handled by the base URL
+ * This allows the browser APIs to think interceptUrl is the URL the page is on
+ */
+// eslint-disable-next-line max-params
+export async function routeInterceptUrl(
+ page: Page,
+ interceptUrl: string,
+ baseUrl: string,
+ path: string
+) {
+ await page.route(`${interceptUrl}/**/*`, async (route, request) => {
+ // eslint-disable-next-line init-declarations
+ let url: string
+ if (request.url() === `${interceptUrl}/`) {
+ url = `${baseUrl}${path}`
+ } else {
+ url = request.url().replace(interceptUrl, baseUrl)
+ }
+ const response = await fetch(url, {
+ method: request.method(),
+ headers: request.headers(),
+ body: request.postData()
+ })
+ const headers: Record = {}
+ response.headers.forEach((value: string, key: string) => {
+ headers[key] = value
+ })
+ const body = Buffer.from(await response.arrayBuffer())
+ await route.fulfill({
+ status: response.status,
+ headers,
+ body
+ })
+ })
+}
diff --git a/apps/laboratory/tests/verify.spec.ts b/apps/laboratory/tests/verify.spec.ts
new file mode 100644
index 0000000000..4452f33ec2
--- /dev/null
+++ b/apps/laboratory/tests/verify.spec.ts
@@ -0,0 +1,125 @@
+import { DEFAULT_CHAIN_NAME, DEFAULT_SESSION_PARAMS } from './shared/constants'
+import { testM } from './shared/fixtures/w3m-fixture'
+import { testMVerifyDomainMismatch } from './shared/fixtures/w3m-verify-domain-mismatch-fixture'
+import { testMVerifyEvil } from './shared/fixtures/w3m-verify-evil-fixture'
+import { testMVerifyValid } from './shared/fixtures/w3m-verify-valid-fixture'
+import { WalletPage } from './shared/pages/WalletPage'
+import { ModalValidator } from './shared/validators/ModalValidator'
+import { WalletValidator } from './shared/validators/WalletValidator'
+import { expect } from '@playwright/test'
+
+testM(
+ 'connection and signature requests from non-verified project should show as cannot verify',
+ async ({ modalPage, context }) => {
+ const modalValidator = new ModalValidator(modalPage.page)
+ const walletPage = new WalletPage(await context.newPage())
+ await walletPage.load()
+ const walletValidator = new WalletValidator(walletPage.page)
+
+ const uri = await modalPage.getConnectUri()
+ await walletPage.connectWithUri(uri)
+ await expect(walletPage.page.getByText('Cannot Verify')).toBeVisible()
+ await walletPage.handleSessionProposal(DEFAULT_SESSION_PARAMS)
+ await modalValidator.expectConnected()
+ await walletValidator.expectConnected()
+
+ await modalPage.sign()
+ const chainName = modalPage.library === 'solana' ? 'Solana' : DEFAULT_CHAIN_NAME
+ await walletValidator.expectReceivedSign({ chainName })
+ await expect(walletPage.page.getByText('Cannot Verify')).toBeVisible()
+ await walletPage.handleRequest({ accept: true })
+ await modalValidator.expectAcceptedSign()
+
+ await modalPage.disconnect()
+ await modalValidator.expectDisconnected()
+ await walletValidator.expectDisconnected()
+ }
+)
+
+testMVerifyValid(
+ 'connection and signature requests from non-scam verified domain should show as domain match',
+ async ({ modalPage, context }) => {
+ const modalValidator = new ModalValidator(modalPage.page)
+ const walletPage = new WalletPage(await context.newPage())
+ await walletPage.load()
+ const walletValidator = new WalletValidator(walletPage.page)
+
+ const uri = await modalPage.getConnectUri()
+ await walletPage.connectWithUri(uri)
+ await expect(walletPage.page.getByTestId('session-info-verified')).toBeVisible()
+ await walletPage.handleSessionProposal(DEFAULT_SESSION_PARAMS)
+ await modalValidator.expectConnected()
+ await walletValidator.expectConnected()
+
+ await modalPage.sign()
+ const chainName = modalPage.library === 'solana' ? 'Solana' : DEFAULT_CHAIN_NAME
+ await walletValidator.expectReceivedSign({ chainName })
+ await expect(walletPage.page.getByTestId('session-info-verified')).toBeVisible()
+ await walletPage.handleRequest({ accept: true })
+ await modalValidator.expectAcceptedSign()
+
+ await modalPage.disconnect()
+ await modalValidator.expectDisconnected()
+ await walletValidator.expectDisconnected()
+ }
+)
+
+testMVerifyDomainMismatch(
+ 'connection and signature requests from non-scam verified domain but on localhost should show as invalid domain',
+ async ({ modalPage, context }) => {
+ const modalValidator = new ModalValidator(modalPage.page)
+ const walletPage = new WalletPage(await context.newPage())
+ await walletPage.load()
+ const walletValidator = new WalletValidator(walletPage.page)
+
+ const uri = await modalPage.getConnectUri()
+ await walletPage.connectWithUri(uri)
+ await expect(walletPage.page.getByText('Invalid Domain')).toBeVisible()
+ await walletPage.handleSessionProposal(DEFAULT_SESSION_PARAMS)
+ await modalValidator.expectConnected()
+ await walletValidator.expectConnected()
+
+ await modalPage.sign()
+ const chainName = modalPage.library === 'solana' ? 'Solana' : DEFAULT_CHAIN_NAME
+ await walletValidator.expectReceivedSign({ chainName })
+ await expect(walletPage.page.getByText('Invalid Domain')).toBeVisible()
+ await walletPage.handleRequest({ accept: true })
+ await modalValidator.expectAcceptedSign()
+
+ await modalPage.disconnect()
+ await modalValidator.expectDisconnected()
+ await walletValidator.expectDisconnected()
+ }
+)
+
+testMVerifyEvil(
+ 'connection and signature requests from scam verified domain should show as scam domain',
+ async ({ modalPage, context }) => {
+ const modalValidator = new ModalValidator(modalPage.page)
+ const walletPage = new WalletPage(await context.newPage())
+ await walletPage.load()
+ const walletValidator = new WalletValidator(walletPage.page)
+
+ const uri = await modalPage.getConnectUri()
+ await walletPage.connectWithUri(uri)
+ await expect(walletPage.page.getByText('Website flagged')).toBeVisible()
+ await walletPage.page.getByText('Proceed anyway').click()
+ await expect(walletPage.page.getByText('Potential threat')).toBeVisible()
+ await walletPage.handleSessionProposal(DEFAULT_SESSION_PARAMS)
+ await modalValidator.expectConnected()
+ await walletValidator.expectConnected()
+
+ await modalPage.sign()
+ const chainName = modalPage.library === 'solana' ? 'Solana' : DEFAULT_CHAIN_NAME
+ await expect(walletPage.page.getByText('Website flagged')).toBeVisible()
+ await walletPage.page.getByText('Proceed anyway').click()
+ await walletValidator.expectReceivedSign({ chainName })
+ await expect(walletPage.page.getByText('Potential threat')).toBeVisible()
+ await walletPage.handleRequest({ accept: true })
+ await modalValidator.expectAcceptedSign()
+
+ await modalPage.disconnect()
+ await modalValidator.expectDisconnected()
+ await walletValidator.expectDisconnected()
+ }
+)