Skip to content

Commit 861f97e

Browse files
committed
Implement the test for Collabora Online
Code was adapted from the OnlyOffice tests. Signed-off-by: Hubert Figuière <hub@collabora.com>
1 parent 706bb5a commit 861f97e

10 files changed

Lines changed: 367 additions & 0 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { sleep } from 'k6'
2+
import { ErrorEvent } from 'k6/experimental/websockets'
3+
4+
import { FileInfo, UserAuth } from './info'
5+
import { EngineType, MessageType, Session, SocketType } from './io'
6+
import { docsWorker } from './worker'
7+
8+
export class Client {
9+
// @ts-ignore
10+
private session: Session
11+
12+
private token: string
13+
14+
private documentId: string
15+
16+
private userAuth: UserAuth
17+
18+
private fileInfo: FileInfo
19+
20+
private wopiSrc: string
21+
22+
constructor(p: { url: string, access_token: string, documentId: string, userAuth: UserAuth, fileInfo: FileInfo }) {
23+
24+
// eslint-disable-next-line @typescript-eslint/naming-convention
25+
const { access_token, access_token_ttl } = p
26+
27+
const appUrl = new URL(p.url)
28+
const wopiSrc = app_url.searchParams.get('WOPISrc')
29+
30+
const wopiUrl = new URL(wopiSrc)
31+
wopiUrl.searchParams.set('access_token', access_token)
32+
wopiUrl.searchParams.set('access_token_ttl', access_token_ttl)
33+
34+
const wssUrl = new URL(`wss://${appUrl.hostname}:${app_url.port}/cool/${encodeURIComponent(wopiUrl)}/ws`)
35+
wssUrl.searchParams.set('WOPISrc', wopiSrc)
36+
wssUrl.searchParams.set('compat', '/ws')
37+
38+
this.token = p.token
39+
this.documentId = p.documentId
40+
this.userAuth = p.userAuth
41+
this.fileInfo = p.fileInfo
42+
this.wopiSrc = wopiSrc
43+
this.session = new Session({ url: wss_url })
44+
}
45+
46+
onError(h: (event?: ErrorEvent) => void) {
47+
this.session.onError(h)
48+
}
49+
50+
async establishSession(): Promise<void> {
51+
await docsWorker({ session: this.session })
52+
await this.session.waitFor({ engineType: EngineType.enum.open })
53+
await this.session.publish({ data: `load url=${this.wopiSrc} accessibilityState=false` +
54+
' deviceFormFactor=desktop darkTheme=false timezone=America/Montreal' })
55+
await this.session.waitFor({ engineType: EngineType.enum.message, socketType: SocketType.enum.connect })
56+
}
57+
58+
async makeChanges(p: { changes: Array<string> }) {
59+
await this.session.publish({ data: isSaveLockMessage() })
60+
const { messageData: { saveLock: isLocked } } = await this.session.waitFor({
61+
engineType: EngineType.enum.message,
62+
socketType: SocketType.enum.event,
63+
messageType: MessageType.enum.saveLock
64+
})
65+
66+
if (isLocked) {
67+
sleep(0.5)
68+
await this.makeChanges(p)
69+
return
70+
}
71+
72+
p.changes.forEach(async (change) => {
73+
await this.session.publish({ data: change })
74+
})
75+
76+
await this.session.publish({ data: 'save dontTerminateEdit=0 dontSaveIfUnmodified=0' })
77+
78+
await this.session.waitFor({
79+
engineType: EngineType.enum.message,
80+
socketType: SocketType.enum.event,
81+
messageType: MessageType.enum.unSaveLock
82+
})
83+
}
84+
85+
disconnect() {
86+
this.session.disconnect()
87+
}
88+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { Client } from './client'
2+
export { obtainDocumentInformation } from './info'
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Client } from '@ownclouders/k6-tdk/lib/client'
2+
import { objectToQueryString } from '@ownclouders/k6-tdk/lib/utils'
3+
import { z } from 'zod'
4+
5+
const UserAuth = z.object({
6+
wopiSrc: z.string(),
7+
access_token: z.string(),
8+
access_token_ttl: z.number(),
9+
userSessionId: z.string(),
10+
mode: z.string()
11+
})
12+
// eslint-disable-next-line @typescript-eslint/no-redeclare
13+
export type UserAuth = z.infer<typeof UserAuth>
14+
15+
const FileInfo = z.object({
16+
BaseFileName: z.string(),
17+
BreadcrumbDocName: z.string(),
18+
HostEditUrl: z.string(),
19+
HostViewUrl: z.string(),
20+
BreadcrumbFolderUrl: z.string(),
21+
IsAnonymousUser: z.boolean(),
22+
UserFriendlyName: z.string(),
23+
BreadcrumbFolderName: z.string(),
24+
DownloadUrl: z.string(),
25+
FileUrl: z.string(),
26+
BreadcrumbBrandName: z.string(),
27+
BreadcrumbBrandUrl: z.string(),
28+
OwnerId: z.string(),
29+
UserId: z.string(),
30+
Size: z.number(),
31+
Version: z.string(),
32+
SupportsExtendedLockLength: z.boolean(),
33+
SupportsGetLock: z.boolean(),
34+
SupportsUpdate: z.boolean(),
35+
UserCanWrite: z.boolean(),
36+
SupportsLocks: z.boolean(),
37+
SupportsDeleteFile: z.boolean(),
38+
UserCanNotWriteRelative: z.boolean(),
39+
SupportsRename: z.boolean(),
40+
UserCanRename: z.boolean(),
41+
SupportsContainers: z.boolean(),
42+
SupportsUserInfo: z.boolean()
43+
})
44+
// eslint-disable-next-line @typescript-eslint/no-redeclare
45+
export type FileInfo = z.infer<typeof FileInfo>
46+
47+
export const obtainDocumentInformation = async (p: {
48+
client: Client,
49+
resourceId: string,
50+
appName: string
51+
}) => {
52+
const openRequestParams = { file_id: p.resourceId, lang: 'de', app_name: p.appName }
53+
const appOpenResponse = p.client.httpClient<'text'>(
54+
'POST',
55+
`/app/open?${objectToQueryString(openRequestParams)}`,
56+
JSON.stringify(openRequestParams)
57+
)
58+
59+
// eslint-disable-next-line @typescript-eslint/naming-convention
60+
const { app_url, form_parameters: { access_token, access_token_ttl } } = JSON.parse(appOpenResponse.body)
61+
62+
return {
63+
app_url,
64+
access_token,
65+
access_token_ttl
66+
}
67+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { EngineType, MessageType, SocketType } from '../../onlyoffice/io/io'
2+
export { Session } from '../../onlyoffice/io/session'
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { EngineType, SocketType } from './io'
2+
3+
export const encodeData = (p: { engineType: EngineType, socketType?: SocketType, data?: unknown }) => {
4+
return [p.engineType, p.socketType, JSON.stringify(p.data)]
5+
.filter((v) => {
6+
return v !== undefined
7+
})
8+
.join('')
9+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { EngineType, MessageType, Session, SocketType } from './io'
2+
3+
export const docsWorker = async (p: { session: Session }): Promise<void> => {
4+
5+
p.session.subscribe({
6+
engineType: EngineType.enum.message,
7+
socketType: SocketType.enum.event,
8+
messageType: MessageType.enum.error,
9+
fn: async ({ messageData }) => {
10+
console.error(messageData)
11+
}
12+
})
13+
14+
p.session.subscribe({
15+
engineType: EngineType.enum.message,
16+
socketType: SocketType.enum.event,
17+
messageType: MessageType.enum.drop,
18+
fn: async ({ messageData }) => {
19+
console.error(messageData)
20+
}
21+
})
22+
}

packages/k6-tests/src/values/env.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,14 @@ export const envValues = () => {
153153
get app_name() {
154154
return ENV('ONLY_OFFICE_APP_NAME', 'OnlyOffice')
155155
}
156+
},
157+
cool: {
158+
get wss_url() {
159+
return ENV('COOL_WSS_URL', 'wss://localhost:9980/')
160+
},
161+
get app_name() {
162+
return ENV('COOL_APP_NAME', 'Collabora')
163+
}
156164
}
157165
}
158166

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Options } from 'k6/options'
2+
3+
import { open_change_save_010, open_change_save_010_setup, open_change_save_010_teardown, options as inherited_options } from './simple.k6'
4+
5+
export const options: Options = {
6+
...inherited_options,
7+
iterations: 10,
8+
duration: '7d',
9+
teardownTimeout: '1h'
10+
}
11+
12+
export const setup = open_change_save_010_setup
13+
export default open_change_save_010
14+
export const teardown = open_change_save_010_teardown
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Options } from 'k6/options'
2+
import { omit } from 'lodash'
3+
4+
import { options as inherited_options } from './baseline.k6'
5+
6+
export { open_change_save_010, open_change_save_010_setup as setup, open_change_save_010_teardown as teardown } from './simple.k6'
7+
8+
export const options: Options = {
9+
...omit(inherited_options, 'iterations', 'duration'),
10+
scenarios: {
11+
open_change_save_010: {
12+
executor: 'ramping-vus',
13+
startVUs: 0,
14+
exec: 'open_change_save_010',
15+
stages: [
16+
{ target: 10, duration: '20s' },
17+
{ target: 10, duration: '30s' },
18+
{ target: 0, duration: '10s' }
19+
]
20+
}
21+
}
22+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { ENV, queryXml, store } from '@ownclouders/k6-tdk/lib/utils'
2+
import { sleep } from 'k6'
3+
import exec from 'k6/execution'
4+
import { Counter } from 'k6/metrics'
5+
import { Options } from 'k6/options'
6+
7+
import { Client, obtainDocumentInformation } from '@/clients/cool'
8+
import { userPool } from '@/pools'
9+
import { clientFor } from '@/shortcuts'
10+
import { getTestRoot } from '@/test'
11+
import { getPoolItem } from '@/utils'
12+
import { envValues } from '@/values'
13+
14+
export interface Environment {
15+
testRoot: string;
16+
}
17+
18+
// eslint-disable-next-line no-restricted-globals
19+
const docX = open('../data/sample.docx', 'b')
20+
21+
export const options: Options = {
22+
vus: 1,
23+
iterations: 1,
24+
insecureSkipTLSVerify: true
25+
}
26+
27+
const settings = {
28+
...envValues(),
29+
get docx() {
30+
return ENV('DOCX', [settings.seed.resource.root, 'sample.docx'].join('/'))
31+
}
32+
}
33+
34+
const coolClientErrors = new Counter('cool_client_errors')
35+
36+
export const open_change_save_010_setup = async (): Promise<Environment> => {
37+
const adminClient = clientFor({ userLogin: settings.admin.login, userPassword: settings.admin.password })
38+
const rootInfo = await getTestRoot({
39+
client: adminClient,
40+
userLogin: settings.admin.login,
41+
platform: settings.platform.type,
42+
resourceName: settings.seed.container.name,
43+
resourceType: settings.seed.container.type,
44+
isOwner: false
45+
})
46+
47+
const testRoot = [rootInfo.root, rootInfo.path].join('/')
48+
49+
adminClient.resource.uploadResource({
50+
root: testRoot,
51+
resourcePath: settings.docx,
52+
resourceBytes: docX
53+
})
54+
55+
return {
56+
testRoot
57+
}
58+
}
59+
60+
export const open_change_save_010 = async ({ testRoot }: Environment): Promise<void> => {
61+
const user = getPoolItem({ pool: userPool, n: exec.vu.idInTest })
62+
const userStore = store(user.userLogin)
63+
const documentInformation = await userStore.setOrGet('root', async () => {
64+
const ocisClient = clientFor(user)
65+
66+
const getResourcePropertiesResponse = await ocisClient.resource.getResourceProperties({
67+
root: testRoot,
68+
resourcePath: settings.docx
69+
})
70+
sleep(settings.sleep.after_request)
71+
72+
const [resourceId] = queryXml("$..['oc:fileid']", getResourcePropertiesResponse.body)
73+
return obtainDocumentInformation({ client: ocisClient, appName: settings.cool.app_name, resourceId })
74+
})
75+
76+
const { app_url } = documentInformation
77+
78+
const coolClient = new Client({
79+
...documentInformation,
80+
url: app_url
81+
})
82+
83+
coolClient.onError((err) => {
84+
console.error(err?.error)
85+
coolClientErrors.add(1, { errorType: err?.error || 'unknown' })
86+
})
87+
88+
await coolClient.establishSession()
89+
sleep(settings.sleep.after_request)
90+
91+
// todo: move into pool
92+
const changes = [
93+
'textinput id=0 text=H',
94+
'textinput id=0 text=e',
95+
'textinput id=0 text=l',
96+
'textinput id=0 text=l',
97+
'textinput id=0 text=o',
98+
'key type=input char=32 key=0',
99+
'textinput id=0 text=w',
100+
'textinput id=0 text=o',
101+
'textinput id=0 text=r',
102+
'textinput id=0 text=l',
103+
'textinput id=0 text=d',
104+
'key type=input char=33 key=0'
105+
]
106+
107+
await coolClient.makeChanges({ changes })
108+
sleep(settings.sleep.after_request)
109+
110+
coolClient.disconnect()
111+
sleep(settings.sleep.after_iteration)
112+
}
113+
114+
export const open_change_save_010_teardown = ({ testRoot }: Environment): void => {
115+
const adminClient = clientFor({ userLogin: settings.admin.login, userPassword: settings.admin.password })
116+
117+
const waitForUnlock = () => {
118+
const { body } = adminClient.resource.getResourceProperties({ root: testRoot, resourcePath: settings.docx })
119+
120+
if(queryXml("$..['d:activelock']", body).length !== 0){
121+
sleep(1)
122+
waitForUnlock()
123+
}
124+
}
125+
126+
waitForUnlock()
127+
128+
adminClient.resource.deleteResource({ root: testRoot, resourcePath: settings.docx })
129+
}
130+
131+
export const setup = open_change_save_010_setup
132+
export default open_change_save_010
133+
export const teardown = open_change_save_010_teardown

0 commit comments

Comments
 (0)