import { Injectable } from '@angular/core' import { FullKeyboard, pauseFor, RPCErrorDetails, SetLanguageParams, } from '@start9labs/shared' import { ApiService } from './embassy-api.service' import { AddOperation, Dump, Operation, PatchOp, pathFromArray, RemoveOperation, ReplaceOperation, Revision, } from 'patch-db-client' import { DataModel, InstallingState, PackageDataEntry, StateInfo, UpdatingState, } from 'src/app/services/patch-db/data-model' import { GetPackageRes, GetPackagesRes } from '@start9labs/marketplace' import { ActionRes, CheckDnsRes, CifsBackupTarget, DiagnosticErrorRes, FollowPackageLogsReq, FollowServerLogsReq, GetActionInputRes, GetPackageLogsReq, GetRegistryPackageReq, GetRegistryPackagesReq, PkgAddPrivateDomainReq, PkgAddPublicDomainReq, PkgBindingSetAddressEnabledReq, PkgRemovePrivateDomainReq, PkgRemovePublicDomainReq, ServerBindingSetAddressEnabledReq, ServerState, WebsocketConfig, } from './api.types' import { Mock } from './api.fixures' import { from, interval, map, shareReplay, startWith, Subject, tap } from 'rxjs' import { mockPatchData } from './mock-patch' import { AuthService } from '../auth.service' import { T } from '@start9labs/start-sdk' import { WebSocketSubject } from 'rxjs/webSocket' import { toAuthorityUrl } from 'src/app/utils/acme' import markdown from './md-sample.md' const PROGRESS: T.FullProgress = { overall: { done: 0, total: 120, units: 'bytes', }, phases: [ { name: 'Downloading', progress: { done: 0, total: 40, units: 'bytes', }, }, { name: 'Validating', progress: null, }, { name: 'Installing', progress: null, }, ], } @Injectable() export class MockApiService extends ApiService { readonly mockWsSource$ = new Subject() private readonly revertTime = 1800 sequence = 0 constructor(private readonly auth: AuthService) { super() this.auth.isVerified$ .pipe( tap(() => { this.sequence = 0 }), ) .subscribe() } async uploadFile(guid: string, body: Blob): Promise { await pauseFor(2000) } async getStatic( urls: string[], params: Record, ): Promise { await pauseFor(2000) return markdown } // websocket openWebsocket$( guid: string, config: WebsocketConfig = {}, ): WebSocketSubject { if (guid === 'db-guid') { return this.mockWsSource$.pipe( shareReplay({ bufferSize: 1, refCount: true }), ) as WebSocketSubject } else if (guid === 'logs-guid') { return interval(50).pipe( map((_, index) => { // mock fire open observer if (index === 0) config.openObserver?.next(new Event('')) if (index === 100) throw new Error('HAAHHA') return Mock.ServerLogs[0] }), ) as WebSocketSubject } else if (guid === 'init-progress-guid') { return from(this.initProgress()).pipe( startWith(PROGRESS), ) as WebSocketSubject } else if (guid === 'sideload-progress-guid') { config.openObserver?.next(new Event('')) return from(this.initProgress()).pipe( startWith(PROGRESS), ) as WebSocketSubject } else if (guid === 'metrics-guid') { return interval(1000).pipe( map(() => Mock.getMetrics()), ) as WebSocketSubject } else { throw new Error('invalid guid type') } } // state async echo(params: T.EchoParams, url: string): Promise { if (url) { const num = Math.floor(Math.random() * 10) + 1 if (num > 8) return params.message throw new Error() } await pauseFor(2000) return params.message } private stateIndex = 0 async getState(): Promise { await pauseFor(1000) this.stateIndex++ return this.stateIndex === 1 ? 'running' : 'running' } // db async subscribeToPatchDB(params: {}): Promise<{ dump: Dump guid: string }> { await pauseFor(2000) return { dump: { id: 1, value: mockPatchData }, guid: 'db-guid', } } async setDbValue( pathArr: Array, value: T, ): Promise { const pointer = pathFromArray(pathArr) const params = { pointer, value } await pauseFor(2000) const patch = [ { op: PatchOp.REPLACE, path: '/ui' + params.pointer, value: params.value, }, ] this.mockRevision(patch) return null } // auth async login(params: T.LoginParams): Promise { await pauseFor(2000) return null } async logout(params: {}): Promise { await pauseFor(2000) return null } async getSessions(params: {}): Promise { await pauseFor(2000) return Mock.Sessions } async killSessions(params: T.KillParams): Promise { await pauseFor(2000) return null } async resetPassword(params: T.ResetPasswordParams): Promise { await pauseFor(2000) return null } // diagnostic async getError(): Promise { await pauseFor(1000) return { code: 15, message: 'Unknown server', data: { details: 'Some details about the error here' }, } } async diagnosticGetError(): Promise { await pauseFor(1000) return { code: 15, message: 'Unknown server', data: { details: 'Some details about the error here' }, } } async diagnosticRestart(): Promise { await pauseFor(1000) } async diagnosticForgetDrive(): Promise { await pauseFor(1000) } async diagnosticRepairDisk(): Promise { await pauseFor(1000) } async diagnosticGetLogs(params: T.LogsParams): Promise { return this.getServerLogs(params) } // init async initFollowProgress(): Promise { await pauseFor(250) return { progress: PROGRESS, guid: 'init-progress-guid', } } async initFollowLogs(): Promise { await pauseFor(2000) return { startCursor: 'start-cursor', guid: 'logs-guid', } } // server async getSystemTime(params: {}): Promise { await pauseFor(2000) return { now: new Date().toUTCString(), uptime: 1234567n, } } async getServerLogs(params: T.LogsParams): Promise { await pauseFor(2000) const entries = this.randomLogs(params.limit) return { entries, startCursor: 'start-cursor', endCursor: 'end-cursor', } } async getKernelLogs(params: T.LogsParams): Promise { await pauseFor(2000) const entries = this.randomLogs(params.limit) return { entries, startCursor: 'start-cursor', endCursor: 'end-cursor', } } async followServerLogs( params: FollowServerLogsReq, ): Promise { await pauseFor(2000) return { startCursor: 'start-cursor', guid: 'logs-guid', } } async followKernelLogs( params: FollowServerLogsReq, ): Promise { await pauseFor(2000) return { startCursor: 'start-cursor', guid: 'logs-guid', } } private randomLogs(limit = 1): T.LogEntry[] { const arrLength = Math.ceil(limit / Mock.ServerLogs.length) const logs = new Array(arrLength) .fill(Mock.ServerLogs) .reduce((acc, val) => acc.concat(val), []) return logs } async followServerMetrics(params: {}): Promise { await pauseFor(2000) return { guid: 'metrics-guid', metrics: Mock.getMetrics(), } } async updateServer(params?: { registry: string targetVersion: string }): Promise<'updating' | 'no-updates'> { await pauseFor(2000) const initialProgress = { size: null, downloaded: 0, } setTimeout(() => { this.updateOSProgress() }, 500) const patch = [ { op: PatchOp.REPLACE, path: '/serverInfo/statusInfo/updateProgress', value: initialProgress, }, ] this.mockRevision(patch) return 'updating' } async restartServer(params: {}): Promise { await pauseFor(2000) const patch = [ { op: PatchOp.REPLACE, path: '/serverInfo/statusInfo/restarting', value: true, }, ] this.mockRevision(patch) setTimeout(() => { const patch2 = [ { op: PatchOp.REPLACE, path: '/serverInfo/statusInfo/restarting', value: false, }, ] this.mockRevision(patch2) }, 2000) return null } async shutdownServer(params: {}): Promise { await pauseFor(2000) const patch = [ { op: PatchOp.REPLACE, path: '/serverInfo/statusInfo/shuttingDown', value: true, }, ] this.mockRevision(patch) setTimeout(() => { const patch2 = [ { op: PatchOp.REPLACE, path: '/serverInfo/statusInfo/shuttingDown', value: false, }, ] this.mockRevision(patch2) }, 2000) return null } async repairDisk(params: {}): Promise { await pauseFor(2000) return null } async toggleKiosk(enable: boolean): Promise { await pauseFor(2000) const patch = [ { op: PatchOp.REPLACE, path: '/serverInfo/kiosk', value: enable, }, ] this.mockRevision(patch) return null } async setHostname(params: { hostname: string }): Promise { await pauseFor(1000) const patch = [ { op: PatchOp.REPLACE, path: '/serverInfo/hostname', value: params.hostname, }, ] this.mockRevision(patch) return null } async setKeyboard(params: FullKeyboard): Promise { await pauseFor(1000) const patch = [ { op: PatchOp.REPLACE, path: '/serverInfo/keyboard', value: params, }, ] this.mockRevision(patch) return null } async setLanguage(params: SetLanguageParams): Promise { await pauseFor(1000) const patch = [ { op: PatchOp.REPLACE, path: '/serverInfo/language', value: params.language, }, ] this.mockRevision(patch) return null } async setDns(params: T.SetStaticDnsParams): Promise { await pauseFor(2000) const patch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path: '/serverInfo/network/dns/staticServers', value: params.servers, }, ] this.mockRevision(patch) return null } async queryDns(params: T.QueryDnsParams): Promise { await pauseFor(2000) return null } async checkPort(params: T.CheckPortParams): Promise { await pauseFor(2000) return { ip: '0.0.0.0', port: params.port, reachable: false } } async checkDns(params: T.CheckDnsParams): Promise { await pauseFor(2000) return false } // marketplace URLs async checkOSUpdate(params: { registry: string serverId: string }): Promise { await pauseFor(2000) return Mock.RegistryOSUpdate } async getRegistryInfo(params: { registry: string }): Promise { await pauseFor(2000) return Mock.RegistryInfo } async getRegistryPackage( params: GetRegistryPackageReq, ): Promise { await pauseFor(2000) const { targetVersion, id } = params if (!targetVersion || targetVersion === '=*') { return Mock.RegistryPackages[id]! } else { return Mock.OtherPackageVersions[id]![targetVersion]! } } async getRegistryPackages( params: GetRegistryPackagesReq, ): Promise { await pauseFor(2000) return Mock.RegistryPackages } // notification async getNotifications( params: T.ListNotificationParams, ): Promise { await pauseFor(2000) return Mock.Notifications } async deleteNotifications(params: T.ModifyNotificationParams): Promise { await pauseFor(2000) return null } async markSeenNotifications( params: T.ModifyNotificationParams, ): Promise { await pauseFor(2000) return null } async markSeenAllNotifications( params: T.ModifyNotificationBeforeParams, ): Promise { await pauseFor(2000) return null } async markUnseenNotifications( params: T.ModifyNotificationParams, ): Promise { await pauseFor(2000) return null } // proxies private proxyId = 0 async addTunnel(params: T.AddTunnelParams): Promise<{ id: string }> { await pauseFor(2000) const id = `wg${this.proxyId++}` const patch: AddOperation[] = [ { op: PatchOp.ADD, path: `/serverInfo/network/gateways/${id}`, value: { name: params.name, secure: false, ipInfo: { name: id, scopeId: 3, deviceType: 'wireguard', subnets: ['192.168.1.10/24'], wanIp: '203.0.113.45', ntpServers: [], lanIp: ['192.168.1.10'], dnsServers: [], }, type: 'inbound-outbound', }, }, ] if (params.setAsDefaultOutbound) { (patch as any[]).push({ op: PatchOp.REPLACE, path: '/serverInfo/network/defaultOutbound', value: id, }) } this.mockRevision(patch) return { id } } async updateTunnel(params: T.RenameGatewayParams): Promise { await pauseFor(2000) const patch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path: `/serverInfo/network/gateways/${params.id}/label`, value: params.name, }, ] this.mockRevision(patch) return null } async removeTunnel(params: T.RemoveTunnelParams): Promise { await pauseFor(2000) const patch: RemoveOperation[] = [ { op: PatchOp.REMOVE, path: `/serverInfo/network/gateways/${params.id}`, }, ] this.mockRevision(patch) return null } async setDefaultOutbound(params: { gateway: string | null }): Promise { await pauseFor(2000) const patch = [ { op: PatchOp.REPLACE, path: '/serverInfo/network/defaultOutbound', value: params.gateway, }, ] this.mockRevision(patch) return null } async setServiceOutbound(params: T.SetOutboundGatewayParams): Promise { await pauseFor(2000) const patch = [ { op: PatchOp.REPLACE, path: `/packageData/${params.package}/outboundGateway`, value: params.gateway, }, ] this.mockRevision(patch) return null } // wifi async enableWifi(params: T.SetWifiEnabledParams): Promise { await pauseFor(2000) const patch = [ { op: PatchOp.REPLACE, path: '/serverInfo/network/wifi/enabled', value: params.enabled, }, ] this.mockRevision(patch) return null } async setWifiCountry(params: T.SetCountryParams): Promise { await pauseFor(2000) return null } async getWifi(params: {}, timeout: number): Promise { await pauseFor(2000) return Mock.Wifi } async addWifi(params: T.WifiAddParams): Promise { await pauseFor(2000) return null } async connectWifi(params: T.WifiSsidParams): Promise { await pauseFor(2000) return null } async deleteWifi(params: T.WifiSsidParams): Promise { await pauseFor(2000) return null } // smtp async setSmtp(params: T.SmtpValue): Promise { await pauseFor(2000) const patch = [ { op: PatchOp.REPLACE, path: '/serverInfo/smtp', value: params, }, ] this.mockRevision(patch) return null } async clearSmtp(params: {}): Promise { await pauseFor(2000) const patch = [ { op: PatchOp.REPLACE, path: '/serverInfo/smtp', value: null, }, ] this.mockRevision(patch) return null } async testSmtp(params: T.TestSmtpParams): Promise { await pauseFor(2000) return null } // ssh async getSshKeys(params: {}): Promise { await pauseFor(2000) return Mock.SshKeys } async addSshKey(params: T.SshAddParams): Promise { await pauseFor(2000) return Mock.SshKey } async deleteSshKey(params: T.SshDeleteParams): Promise { await pauseFor(2000) return null } // backup async getBackupTargets(params: {}): Promise<{ [id: string]: T.BackupTarget }> { await pauseFor(2000) return Mock.BackupTargets } async addBackupTarget( params: T.CifsAddParams, ): Promise<{ [id: string]: CifsBackupTarget }> { await pauseFor(2000) const { hostname, path, username } = params return { latfgvwdbhjsndmk: { type: 'cifs', hostname, path: path.replace(/\\/g, '/'), username, mountable: true, startOs: {}, }, } } async updateBackupTarget( params: T.CifsUpdateParams, ): Promise<{ [id: string]: CifsBackupTarget }> { await pauseFor(2000) const { id, hostname, path, username } = params return { [id]: { ...(Mock.BackupTargets[id] as CifsBackupTarget), hostname, path, username, }, } } async removeBackupTarget(params: T.CifsRemoveParams): Promise { await pauseFor(2000) return null } async getBackupInfo(params: T.InfoParams): Promise { await pauseFor(2000) return Mock.BackupInfo } async createBackup(params: T.BackupParams): Promise { await pauseFor(2000) const serverPath = '/serverInfo/statusInfo/backupProgress' const ids = params.packageIds || [] setTimeout(async () => { for (let i = 0; i < ids.length; i++) { const id = ids[i] const appPath = `/packageData/${id}/statusInfo/desired/main` const appPatch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path: appPath, value: 'backing-up', }, ] this.mockRevision(appPatch) await pauseFor(8000) this.mockRevision([ { ...appPatch[0]!, value: 'stopped', }, ]) const serverPatch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path: `${serverPath}/${id}/complete`, value: true, }, ] this.mockRevision(serverPatch) } await pauseFor(1000) // remove backupProgress const lastPatch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path: serverPath, value: null, }, ] this.mockRevision(lastPatch) }, 500) const originalPatch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path: serverPath, value: ids.reduce((acc, val) => { return { ...acc, [val]: { complete: false }, } }, {}), }, ] this.mockRevision(originalPatch) return null } // async addBackupTarget( // type: BackupTargetType, // params: // | RR.AddCifsBackupTargetReq // | RR.AddCloudBackupTargetReq // | RR.AddDiskBackupTargetReq, // ): Promise { // await pauseFor(2000) // const { path, name } = params // return { // latfgvwdbhjsndmk: { // name, // type: 'cifs', // hostname: 'mockhotname', // path: path.replace(/\\/g, '/'), // username: 'mockusername', // mountable: true, // startOs: {}, // }, // } // } // async updateBackupTarget( // type: BackupTargetType, // params: RR.UpdateCifsBackupTargetReq | RR.UpdateCloudBackupTargetReq, // ): Promise { // await pauseFor(2000) // return { [params.id]: Mock.BackupTargets.saved[params.id] } // } // async removeBackupTarget( // params: RR.RemoveBackupTargetReq, // ): Promise { // await pauseFor(2000) // return null // } // async getBackupJobs( // params: RR.GetBackupJobsReq, // ): Promise { // await pauseFor(2000) // return Mock.BackupJobs // } // async createBackupJob( // params: RR.CreateBackupJobReq, // ): Promise { // await pauseFor(2000) // return { // id: 'hjdfbjsahdbn', // name: params.name, // targetId: Object.keys(Mock.BackupTargets.saved)[0], // cron: params.cron, // packageIds: params.packageIds, // } // } // async updateBackupJob( // params: RR.UpdateBackupJobReq, // ): Promise { // await pauseFor(2000) // return { // ...Mock.BackupJobs[0], // ...params, // } // } // async deleteBackupJob( // params: RR.DeleteBackupJobReq, // ): Promise { // await pauseFor(2000) // return null // } // async getBackupRuns( // params: RR.GetBackupRunsReq, // ): Promise { // await pauseFor(2000) // return Mock.BackupRuns // } // async deleteBackupRuns( // params: RR.DeleteBackupRunsReq, // ): Promise { // await pauseFor(2000) // return null // } // package async getPackageLogs(params: GetPackageLogsReq): Promise { await pauseFor(2000) let entries if (Math.random() < 0.2) { entries = Mock.ServerLogs } else { const arrLength = params.limit ? Math.ceil(params.limit / Mock.ServerLogs.length) : 10 entries = new Array(arrLength) .fill(Mock.ServerLogs) .reduce((acc, val) => acc.concat(val), []) } return { entries, startCursor: 'startCursor', endCursor: 'end-cursor', } } async followPackageLogs( params: FollowPackageLogsReq, ): Promise { await pauseFor(2000) return { startCursor: 'start-cursor', guid: 'logs-guid', } } async installPackage(params: T.InstallParams): Promise { await pauseFor(2000) setTimeout(async () => { this.installProgress(params.id) }, 1000) const patch: AddOperation< PackageDataEntry >[] = [ { op: PatchOp.ADD, path: `/packageData/${params.id}`, value: { ...Mock.LocalPkgs[params.id]!, stateInfo: { // if installing state: 'installing', // if updating // state: 'updating', // manifest: mockPatchData.packageData[params.id]?.stateInfo.manifest!, // both installingInfo: { newManifest: Mock.LocalPkgs[params.id]?.stateInfo.manifest!, progress: PROGRESS, }, }, }, }, ] this.mockRevision(patch) return null } async cancelInstallPackage(params: T.CancelInstallParams): Promise { await pauseFor(500) const patch: RemoveOperation[] = [ { op: PatchOp.REMOVE, path: `/packageData/${params.id}`, }, ] this.mockRevision(patch) return null } async getActionInput( params: T.GetActionInputParams, ): Promise { await pauseFor(2000) if ( params.packageId === 'tor' && params.actionId === 'create-onion-service' ) { return { eventId: 'ANZXNWIFRTTBZ6T52KQPZILIQQODDHXQ', value: null, spec: await Mock.getCreateOnionServiceSpec(), } } return { eventId: 'ANZXNWIFRTTBZ6T52KQPZILIQQODDHXQ', value: Mock.MockConfig, spec: await Mock.getActionInputSpec(), } } async runAction(params: T.RunActionParams): Promise { await pauseFor(2000) const patch: ReplaceOperation<{ [key: string]: T.TaskEntry }>[] = [ { op: PatchOp.REPLACE, path: `/packageData/${params.packageId}/tasks`, value: {}, }, ] this.mockRevision(patch) // return Mock.ActionResGroup return Mock.ActionResMessage // return Mock.ActionResSingle } async clearTask(params: T.ClearTaskParams): Promise { await pauseFor(2000) const patch: RemoveOperation[] = [ { op: PatchOp.REMOVE, path: `/packageData/${params.packageId}/tasks/${params.replayId}`, }, ] this.mockRevision(patch) return null } async restorePackages(params: T.RestorePackageParams): Promise { await pauseFor(2000) const patch: AddOperation[] = params.ids.map(id => { setTimeout(async () => { this.installProgress(id) }, 2000) return { op: PatchOp.ADD, path: `/packageData/${id}`, value: { ...Mock.LocalPkgs[id]!, stateInfo: { state: 'restoring', installingInfo: { newManifest: Mock.LocalPkgs[id]?.stateInfo.manifest!, progress: PROGRESS, }, }, }, } }) this.mockRevision(patch) return null } async startPackage(params: T.ControlParams): Promise { const path = `/packageData/${params.id}/statusInfo` await pauseFor(2000) setTimeout(async () => { const patch2: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path, value: { error: null, desired: { main: 'running' }, started: new Date().toISOString(), health: { 'ephemeral-health-check': { name: 'Ephemeral Health Check', result: 'success', message: null, }, 'unnecessary-health-check': { name: 'Unnecessary Health Check', result: 'disabled', message: 'Custom disabled message', }, 'chain-state': { name: 'Chain State', result: 'loading', message: 'Bitcoin is syncing from genesis', }, 'p2p-interface': { name: 'P2P Interface', result: 'waiting', message: 'Chain State', }, 'rpc-interface': { name: 'RPC Interface', result: 'failure', message: 'Custom failure message', }, }, }, }, ] this.mockRevision(patch2) }, 2000) const originalPatch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path, value: { desired: { main: 'running' }, started: null, error: null, health: {}, }, }, ] this.mockRevision(originalPatch) return null } async restartPackage(params: T.ControlParams): Promise { await pauseFor(2000) const path = `/packageData/${params.id}/statusInfo` setTimeout(async () => { const patch2: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path, value: { desired: { main: 'running' }, error: null, started: new Date().toISOString(), health: { 'ephemeral-health-check': { name: 'Ephemeral Health Check', result: 'starting', message: null, }, 'unnecessary-health-check': { name: 'Unnecessary Health Check', result: 'disabled', message: 'Custom disabled message', }, 'chain-state': { name: 'Chain State', result: 'loading', message: 'Bitcoin is syncing from genesis', }, 'p2p-interface': { name: 'P2P Interface', result: 'success', message: null, }, 'rpc-interface': { name: 'RPC Interface', result: 'failure', message: 'Custom failure message', }, }, }, }, ] this.mockRevision(patch2) }, this.revertTime) const patch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path, value: { desired: { main: 'restarting' }, started: null, error: null, health: {}, }, }, ] this.mockRevision(patch) return null } async stopPackage(params: T.ControlParams): Promise { await pauseFor(2000) const path = `/packageData/${params.id}/statusInfo` setTimeout(() => { const patch2: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path: path, value: { desired: { main: 'stopped' }, error: null, health: {}, started: null, }, }, ] this.mockRevision(patch2) }, this.revertTime) const patch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path: path, value: { desired: { main: 'stopped' }, error: null, health: {}, started: new Date().toISOString(), }, }, ] this.mockRevision(patch) return null } async rebuildPackage(params: T.RebuildParams): Promise { return this.restartPackage(params) } async uninstallPackage(params: T.UninstallParams): Promise { await pauseFor(2000) setTimeout(async () => { const patch2: RemoveOperation[] = [ { op: PatchOp.REMOVE, path: `/packageData/${params.id}`, }, ] this.mockRevision(patch2) }, this.revertTime) const patch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path: `/packageData/${params.id}/stateInfo/state`, value: 'removing', }, ] this.mockRevision(patch) return null } async sideloadPackage(): Promise { await pauseFor(2000) return { upload: 'sideload-upload-guid', // no significance, randomly generated progress: 'sideload-progress-guid', // no significance, randomly generated } } // async setServiceOutboundProxy( // params: RR.SetServiceOutboundTunnelReq, // ): Promise { // await pauseFor(2000) // const patch = [ // { // op: PatchOp.REPLACE, // path: `/packageData/${params.packageId}/outboundProxy`, // value: params.proxy, // }, // ] // this.mockRevision(patch) // return null // } async initAcme(params: T.InitAcmeParams): Promise { await pauseFor(2000) const patch = [ { op: PatchOp.ADD, path: `/serverInfo/acme`, value: { [toAuthorityUrl(params.provider)]: { contact: params.contact }, }, }, ] this.mockRevision(patch) return null } async removeAcme(params: T.RemoveAcmeParams): Promise { await pauseFor(2000) const regex = new RegExp('/', 'g') const patch: RemoveOperation[] = [ { op: PatchOp.REMOVE, path: `/serverInfo/acme/${params.provider.replace(regex, '~1')}`, }, ] this.mockRevision(patch) return null } async serverBindingSetAddressEnabled( params: ServerBindingSetAddressEnabledReq, ): Promise { await pauseFor(2000) const basePath = `/serverInfo/network/host/bindings/${params.internalPort}/addresses` this.mockSetAddressEnabled(basePath, params.address, params.enabled) return null } async osUiAddPublicDomain( params: T.AddPublicDomainParams, ): Promise { await pauseFor(2000) const patch: Operation[] = [ { op: PatchOp.ADD, path: `/serverInfo/network/host/publicDomains`, value: { [params.fqdn]: { gateway: params.gateway, acme: params.acme }, }, }, { op: PatchOp.ADD, path: `/serverInfo/network/host/bindings/80/addresses/available/-`, value: { ssl: true, public: true, host: params.fqdn, port: 443, metadata: { kind: 'public-domain', gateway: params.gateway }, }, }, ] this.mockRevision(patch) return null } async osUiRemovePublicDomain(params: T.RemoveDomainParams): Promise { await pauseFor(2000) const patch: RemoveOperation[] = [ { op: PatchOp.REMOVE, path: `/serverInfo/network/host/publicDomains/${params.fqdn}`, }, ] this.mockRevision(patch) return null } async osUiAddPrivateDomain(params: T.AddPrivateDomainParams): Promise { await pauseFor(2000) const patch: Operation[] = [ { op: PatchOp.ADD, path: `/serverInfo/network/host/privateDomains/${params.fqdn}`, value: ['eth0'], }, { op: PatchOp.ADD, path: `/serverInfo/network/host/bindings/80/addresses/available/-`, value: { ssl: true, public: false, host: params.fqdn, port: 443, metadata: { kind: 'private-domain', gateways: ['eth0'] }, }, }, ] this.mockRevision(patch) return null } async osUiRemovePrivateDomain(params: T.RemoveDomainParams): Promise { await pauseFor(2000) const patch: RemoveOperation[] = [ { op: PatchOp.REMOVE, path: `/serverInfo/network/host/privateDomains/${params.fqdn}`, }, ] this.mockRevision(patch) return null } async pkgBindingSetAddressEnabled( params: PkgBindingSetAddressEnabledReq, ): Promise { await pauseFor(2000) const basePath = `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/addresses` this.mockSetAddressEnabled(basePath, params.address, params.enabled) return null } async pkgAddPublicDomain( params: PkgAddPublicDomainReq, ): Promise { await pauseFor(2000) const patch: Operation[] = [ { op: PatchOp.ADD, path: `/packageData/${params.package}/hosts/${params.host}/publicDomains`, value: { [params.fqdn]: { gateway: params.gateway, acme: params.acme }, }, }, { op: PatchOp.ADD, path: `/packageData/${params.package}/hosts/${params.host}/bindings/80/addresses/available/-`, value: { ssl: true, public: true, host: params.fqdn, port: 443, metadata: { kind: 'public-domain', gateway: params.gateway }, }, }, ] this.mockRevision(patch) return null } async pkgRemovePublicDomain(params: PkgRemovePublicDomainReq): Promise { await pauseFor(2000) const patch: RemoveOperation[] = [ { op: PatchOp.REMOVE, path: `/packageData/${params.package}/hosts/${params.host}/publicDomains/${params.fqdn}`, }, ] this.mockRevision(patch) return null } async pkgAddPrivateDomain(params: PkgAddPrivateDomainReq): Promise { await pauseFor(2000) const patch: Operation[] = [ { op: PatchOp.ADD, path: `/packageData/${params.package}/hosts/${params.host}/privateDomains/${params.fqdn}`, value: ['eth0'], }, { op: PatchOp.ADD, path: `/packageData/${params.package}/hosts/${params.host}/bindings/80/addresses/available/-`, value: { ssl: true, public: false, host: params.fqdn, port: 443, metadata: { kind: 'private-domain', gateways: ['eth0'] }, }, }, ] this.mockRevision(patch) return null } async pkgRemovePrivateDomain( params: PkgRemovePrivateDomainReq, ): Promise { await pauseFor(2000) const patch: RemoveOperation[] = [ { op: PatchOp.REMOVE, path: `/packageData/${params.package}/hosts/${params.host}/privateDomains/${params.fqdn}`, }, ] this.mockRevision(patch) return null } private async initProgress(): Promise { const progress = JSON.parse(JSON.stringify(PROGRESS)) for (let [i, phase] of progress.phases.entries()) { if ( !phase.progress || typeof phase.progress !== 'object' || !phase.progress.total ) { await pauseFor(2000) progress.phases[i]!.progress = true if ( progress.overall && typeof progress.overall === 'object' && progress.overall.total ) { const step = progress.overall.total / progress.phases.length progress.overall.done += step } } else { const step = phase.progress.total / 4 while (phase.progress.done < phase.progress.total) { await pauseFor(200) phase.progress.done += step if ( progress.overall && typeof progress.overall === 'object' && progress.overall.total ) { const step = progress.overall.total / progress.phases.length / 4 progress.overall.done += step } if (phase.progress.done === phase.progress.total) { await pauseFor(250) progress.phases[i]!.progress = true } } } } return progress } private async installProgress(id: string): Promise { const progress = JSON.parse(JSON.stringify(PROGRESS)) for (let [i, phase] of progress.phases.entries()) { if (!phase.progress || phase.progress === true || !phase.progress.total) { await pauseFor(2000) const patches: Operation[] = [ { op: PatchOp.REPLACE, path: `/packageData/${id}/stateInfo/installingInfo/progress/phases/${i}/progress`, value: true, }, ] // overall if ( progress.overall && typeof progress.overall === 'object' && progress.overall.total ) { const step = progress.overall.total / progress.phases.length progress.overall.done += step patches.push({ op: PatchOp.REPLACE, path: `/packageData/${id}/stateInfo/installingInfo/progress/overall/done`, value: progress.overall.done, }) } this.mockRevision(patches) } else { const step = phase.progress.total / 4 while (phase.progress.done < phase.progress.total) { await pauseFor(500) phase.progress.done += step const patches: Operation[] = [ { op: PatchOp.REPLACE, path: `/packageData/${id}/stateInfo/installingInfo/progress/phases/${i}/progress/done`, value: phase.progress.done, }, ] // overall if ( progress.overall && typeof progress.overall === 'object' && progress.overall.total ) { const step = progress.overall.total / progress.phases.length / 4 progress.overall.done += step patches.push({ op: PatchOp.REPLACE, path: `/packageData/${id}/stateInfo/installingInfo/progress/overall/done`, value: progress.overall.done, }) } this.mockRevision(patches) if (phase.progress.done === phase.progress.total) { await pauseFor(250) this.mockRevision([ { op: PatchOp.REPLACE, path: `/packageData/${id}/stateInfo/installingInfo/progress/phases/${i}/progress`, value: true, }, ]) } } } } await pauseFor(1000) this.mockRevision([ { op: PatchOp.REPLACE, path: `/packageData/${id}/stateInfo/installingInfo/progress/overall`, value: true, }, ]) await pauseFor(1000) const patch2: Operation[] = [ { op: PatchOp.REPLACE, path: `/packageData/${id}/stateInfo`, value: { state: 'installed', manifest: Mock.LocalPkgs[id]?.stateInfo.manifest!, }, }, ] this.mockRevision(patch2) } private async updateOSProgress() { let size = 10000 let downloaded = 0 const patch0 = [ { op: PatchOp.REPLACE, path: `/serverInfo/statusInfo/updateProgress/size`, value: size, }, ] this.mockRevision(patch0) while (downloaded < size) { await pauseFor(250) downloaded += 500 const patch = [ { op: PatchOp.REPLACE, path: `/serverInfo/statusInfo/updateProgress/downloaded`, value: downloaded, }, ] this.mockRevision(patch) } const patch2 = [ { op: PatchOp.REPLACE, path: `/serverInfo/statusInfo/updateProgress/downloaded`, value: size, }, ] this.mockRevision(patch2) setTimeout(async () => { const patch3: Operation[] = [ { op: PatchOp.REPLACE, path: '/serverInfo/statusInfo/updated', value: true, }, { op: PatchOp.REMOVE, path: '/serverInfo/statusInfo/updateProgress', }, ] this.mockRevision(patch3) // quickly revert server to "running" for continued testing await pauseFor(100) const patch4 = [ { op: PatchOp.REPLACE, path: '/serverInfo/status', value: 'running', }, ] this.mockRevision(patch4) // set patch indicating update is complete await pauseFor(100) const patch6 = [ { op: PatchOp.REPLACE, path: '/serverInfo/statusInfo', value: Mock.ServerUpdated, }, ] this.mockRevision(patch6) }, 1000) } private mockSetAddressEnabled( basePath: string, addressJson: string, enabled: boolean | null, ): void { const h: T.HostnameInfo = JSON.parse(addressJson) const isPublicIp = h.public && (h.metadata.kind === 'ipv4' || h.metadata.kind === 'ipv6') const current = this.mockData(basePath) as T.DerivedAddressInfo if (isPublicIp) { if (h.port === null) return const sa = h.metadata.kind === 'ipv6' ? `[${h.hostname}]:${h.port}` : `${h.hostname}:${h.port}` const arr = [...current.enabled] if (enabled) { if (!arr.includes(sa)) arr.push(sa) } else { const idx = arr.indexOf(sa) if (idx >= 0) arr.splice(idx, 1) } current.enabled = arr this.mockRevision([ { op: PatchOp.REPLACE, path: `${basePath}/enabled`, value: arr }, ]) } else { const port = h.port ?? 0 const arr = current.disabled.filter( ([dHost, dPort]) => !(dHost === h.hostname && dPort === port), ) if (!enabled) { arr.push([h.hostname, port]) } current.disabled = arr this.mockRevision([ { op: PatchOp.REPLACE, path: `${basePath}/disabled`, value: arr }, ]) } } private mockData(path: string): any { const parts = path.split('/').filter(Boolean) let obj: any = mockPatchData for (const part of parts) { obj = obj[part] } return obj } private async mockRevision(patch: Operation[]): Promise { const revision = { id: ++this.sequence, patch, } this.mockWsSource$.next(revision) } }