Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the testing for Collabora Online #63

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions packages/k6-tests/src/clients/cool/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { sleep } from 'k6'
import { ErrorEvent } from 'k6/experimental/websockets'

import { FileInfo, UserAuth } from './info'
import { EngineType, MessageType, Session, SocketType } from './io'
import { docsWorker } from './worker'

export class Client {
// @ts-ignore
private session: Session

private token: string

private documentId: string

private userAuth: UserAuth

private fileInfo: FileInfo

private wopiSrc: string

constructor(p: { url: string, access_token: string, documentId: string, userAuth: UserAuth, fileInfo: FileInfo }) {

// eslint-disable-next-line @typescript-eslint/naming-convention
const { access_token, access_token_ttl } = p

const appUrl = new URL(p.url)
const wopiSrc = app_url.searchParams.get('WOPISrc')

const wopiUrl = new URL(wopiSrc)
wopiUrl.searchParams.set('access_token', access_token)
wopiUrl.searchParams.set('access_token_ttl', access_token_ttl)

const wssUrl = new URL(`wss://${appUrl.hostname}:${app_url.port}/cool/${encodeURIComponent(wopiUrl)}/ws`)
wssUrl.searchParams.set('WOPISrc', wopiSrc)
wssUrl.searchParams.set('compat', '/ws')

this.token = p.token
this.documentId = p.documentId
this.userAuth = p.userAuth
this.fileInfo = p.fileInfo
this.wopiSrc = wopiSrc
this.session = new Session({ url: wss_url })
}

onError(h: (event?: ErrorEvent) => void) {
this.session.onError(h)
}

async establishSession(): Promise<void> {
await docsWorker({ session: this.session })
await this.session.waitFor({ engineType: EngineType.enum.open })
await this.session.publish({ data: `load url=${this.wopiSrc} accessibilityState=false` +
' deviceFormFactor=desktop darkTheme=false timezone=America/Montreal' })
await this.session.waitFor({ engineType: EngineType.enum.message, socketType: SocketType.enum.connect })
}

async makeChanges(p: { changes: Array<string> }) {
await this.session.publish({ data: isSaveLockMessage() })
const { messageData: { saveLock: isLocked } } = await this.session.waitFor({
engineType: EngineType.enum.message,
socketType: SocketType.enum.event,
messageType: MessageType.enum.saveLock
})

if (isLocked) {
sleep(0.5)
await this.makeChanges(p)
return
}

p.changes.forEach(async (change) => {
await this.session.publish({ data: change })
})

await this.session.publish({ data: 'save dontTerminateEdit=0 dontSaveIfUnmodified=0' })

await this.session.waitFor({
engineType: EngineType.enum.message,
socketType: SocketType.enum.event,
messageType: MessageType.enum.unSaveLock
})
}

disconnect() {
this.session.disconnect()
}
}
2 changes: 2 additions & 0 deletions packages/k6-tests/src/clients/cool/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Client } from './client'
export { obtainDocumentInformation } from './info'
67 changes: 67 additions & 0 deletions packages/k6-tests/src/clients/cool/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Client } from '@ownclouders/k6-tdk/lib/client'
import { objectToQueryString } from '@ownclouders/k6-tdk/lib/utils'
import { z } from 'zod'

const UserAuth = z.object({
wopiSrc: z.string(),
access_token: z.string(),
access_token_ttl: z.number(),
userSessionId: z.string(),
mode: z.string()
})
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type UserAuth = z.infer<typeof UserAuth>

const FileInfo = z.object({
BaseFileName: z.string(),
BreadcrumbDocName: z.string(),
HostEditUrl: z.string(),
HostViewUrl: z.string(),
BreadcrumbFolderUrl: z.string(),
IsAnonymousUser: z.boolean(),
UserFriendlyName: z.string(),
BreadcrumbFolderName: z.string(),
DownloadUrl: z.string(),
FileUrl: z.string(),
BreadcrumbBrandName: z.string(),
BreadcrumbBrandUrl: z.string(),
OwnerId: z.string(),
UserId: z.string(),
Size: z.number(),
Version: z.string(),
SupportsExtendedLockLength: z.boolean(),
SupportsGetLock: z.boolean(),
SupportsUpdate: z.boolean(),
UserCanWrite: z.boolean(),
SupportsLocks: z.boolean(),
SupportsDeleteFile: z.boolean(),
UserCanNotWriteRelative: z.boolean(),
SupportsRename: z.boolean(),
UserCanRename: z.boolean(),
SupportsContainers: z.boolean(),
SupportsUserInfo: z.boolean()
})
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type FileInfo = z.infer<typeof FileInfo>

export const obtainDocumentInformation = async (p: {
client: Client,
resourceId: string,
appName: string
}) => {
const openRequestParams = { file_id: p.resourceId, lang: 'de', app_name: p.appName }
const appOpenResponse = p.client.httpClient<'text'>(
'POST',
`/app/open?${objectToQueryString(openRequestParams)}`,
JSON.stringify(openRequestParams)
)

// eslint-disable-next-line @typescript-eslint/naming-convention
const { app_url, form_parameters: { access_token, access_token_ttl } } = JSON.parse(appOpenResponse.body)

return {
app_url,
access_token,
access_token_ttl
}
}
2 changes: 2 additions & 0 deletions packages/k6-tests/src/clients/cool/io/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { EngineType, MessageType, SocketType } from '../../onlyoffice/io/io'
export { Session } from '../../onlyoffice/io/session'
9 changes: 9 additions & 0 deletions packages/k6-tests/src/clients/cool/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { EngineType, SocketType } from './io'

export const encodeData = (p: { engineType: EngineType, socketType?: SocketType, data?: unknown }) => {
return [p.engineType, p.socketType, JSON.stringify(p.data)]
.filter((v) => {
return v !== undefined
})
.join('')
}
22 changes: 22 additions & 0 deletions packages/k6-tests/src/clients/cool/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { EngineType, MessageType, Session, SocketType } from './io'

export const docsWorker = async (p: { session: Session }): Promise<void> => {

p.session.subscribe({
engineType: EngineType.enum.message,
socketType: SocketType.enum.event,
messageType: MessageType.enum.error,
fn: async ({ messageData }) => {
console.error(messageData)
}
})

p.session.subscribe({
engineType: EngineType.enum.message,
socketType: SocketType.enum.event,
messageType: MessageType.enum.drop,
fn: async ({ messageData }) => {
console.error(messageData)
}
})
}
8 changes: 8 additions & 0 deletions packages/k6-tests/src/values/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,14 @@ export const envValues = () => {
get app_name() {
return ENV('ONLY_OFFICE_APP_NAME', 'OnlyOffice')
}
},
cool: {
get wss_url() {
return ENV('COOL_WSS_URL', 'wss://localhost:9980/')
},
get app_name() {
return ENV('COOL_APP_NAME', 'Collabora')
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Options } from 'k6/options'

import { open_change_save_010, open_change_save_010_setup, open_change_save_010_teardown, options as inherited_options } from './simple.k6'

export const options: Options = {
...inherited_options,
iterations: 10,
duration: '7d',
teardownTimeout: '1h'
}

export const setup = open_change_save_010_setup
export default open_change_save_010
export const teardown = open_change_save_010_teardown
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Options } from 'k6/options'
import { omit } from 'lodash'

import { options as inherited_options } from './baseline.k6'

export { open_change_save_010, open_change_save_010_setup as setup, open_change_save_010_teardown as teardown } from './simple.k6'

export const options: Options = {
...omit(inherited_options, 'iterations', 'duration'),
scenarios: {
open_change_save_010: {
executor: 'ramping-vus',
startVUs: 0,
exec: 'open_change_save_010',
stages: [
{ target: 10, duration: '20s' },
{ target: 10, duration: '30s' },
{ target: 0, duration: '10s' }
]
}
}
}
133 changes: 133 additions & 0 deletions packages/k6-tests/tests/koko/cool/010-open-change-save/simple.k6.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { ENV, queryXml, store } from '@ownclouders/k6-tdk/lib/utils'
import { sleep } from 'k6'
import exec from 'k6/execution'
import { Counter } from 'k6/metrics'
import { Options } from 'k6/options'

import { Client, obtainDocumentInformation } from '@/clients/cool'
import { userPool } from '@/pools'
import { clientFor } from '@/shortcuts'
import { getTestRoot } from '@/test'
import { getPoolItem } from '@/utils'
import { envValues } from '@/values'

export interface Environment {
testRoot: string;
}

// eslint-disable-next-line no-restricted-globals
const docX = open('../data/sample.docx', 'b')

export const options: Options = {
vus: 1,
iterations: 1,
insecureSkipTLSVerify: true
}

const settings = {
...envValues(),
get docx() {
return ENV('DOCX', [settings.seed.resource.root, 'sample.docx'].join('/'))
}
}

const coolClientErrors = new Counter('cool_client_errors')

export const open_change_save_010_setup = async (): Promise<Environment> => {
const adminClient = clientFor({ userLogin: settings.admin.login, userPassword: settings.admin.password })
const rootInfo = await getTestRoot({
client: adminClient,
userLogin: settings.admin.login,
platform: settings.platform.type,
resourceName: settings.seed.container.name,
resourceType: settings.seed.container.type,
isOwner: false
})

const testRoot = [rootInfo.root, rootInfo.path].join('/')

adminClient.resource.uploadResource({
root: testRoot,
resourcePath: settings.docx,
resourceBytes: docX
})

return {
testRoot
}
}

export const open_change_save_010 = async ({ testRoot }: Environment): Promise<void> => {
const user = getPoolItem({ pool: userPool, n: exec.vu.idInTest })
const userStore = store(user.userLogin)
const documentInformation = await userStore.setOrGet('root', async () => {
const ocisClient = clientFor(user)

const getResourcePropertiesResponse = await ocisClient.resource.getResourceProperties({
root: testRoot,
resourcePath: settings.docx
})
sleep(settings.sleep.after_request)

const [resourceId] = queryXml("$..['oc:fileid']", getResourcePropertiesResponse.body)
return obtainDocumentInformation({ client: ocisClient, appName: settings.cool.app_name, resourceId })
})

const { app_url } = documentInformation

const coolClient = new Client({
...documentInformation,
url: app_url
})

coolClient.onError((err) => {
console.error(err?.error)
coolClientErrors.add(1, { errorType: err?.error || 'unknown' })
})

await coolClient.establishSession()
sleep(settings.sleep.after_request)

// todo: move into pool
const changes = [
'textinput id=0 text=H',
'textinput id=0 text=e',
'textinput id=0 text=l',
'textinput id=0 text=l',
'textinput id=0 text=o',
'key type=input char=32 key=0',
'textinput id=0 text=w',
'textinput id=0 text=o',
'textinput id=0 text=r',
'textinput id=0 text=l',
'textinput id=0 text=d',
'key type=input char=33 key=0'
]

await coolClient.makeChanges({ changes })
sleep(settings.sleep.after_request)

coolClient.disconnect()
sleep(settings.sleep.after_iteration)
}

export const open_change_save_010_teardown = ({ testRoot }: Environment): void => {
const adminClient = clientFor({ userLogin: settings.admin.login, userPassword: settings.admin.password })

const waitForUnlock = () => {
const { body } = adminClient.resource.getResourceProperties({ root: testRoot, resourcePath: settings.docx })

if(queryXml("$..['d:activelock']", body).length !== 0){
sleep(1)
waitForUnlock()
}
}

waitForUnlock()

adminClient.resource.deleteResource({ root: testRoot, resourcePath: settings.docx })
}

export const setup = open_change_save_010_setup
export default open_change_save_010
export const teardown = open_change_save_010_teardown