Skip to content

Commit

Permalink
Add integration test for plugin discovery and loading
Browse files Browse the repository at this point in the history
  • Loading branch information
ShishKabab committed Apr 12, 2020
1 parent 242573e commit 357de8c
Show file tree
Hide file tree
Showing 21 changed files with 249 additions and 644 deletions.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
"@types/bcrypt": "^3.0.0",
"@types/chai": "^4.0.6",
"@types/events": "^1.2.0",
"@types/expect": "^1.20.4",
"@types/koa": "^2.11.0",
"@types/koa-bodyparser": "^4.3.0",
"@types/koa-router": "^7.4.0",
Expand All @@ -60,15 +59,17 @@
"@types/yargs": "^11.0.0",
"chai": "^4.1.2",
"del": "^5.1.0",
"expect": "^23.5.0",
"expect": "^25.3.0",
"fake-fs": "^0.5.0",
"mocha": "^4.0.1",
"nyc": "^13.3.0",
"sinon": "^4.1.2",
"source-map-support": "0.5.16",
"storex-hub-plugin-internal-selftest": "^0.0.1",
"ts-node": "^7.0.1",
"typescript": "^3.7.5",
"@types/prompt-sync": "^4.1.0"
"@types/prompt-sync": "^4.1.0",
"@worldbrain/storex-hub-interfaces": "^0.1.1"
},
"resolutions": {
"**/graphql": "^14.0.0"
Expand Down
17 changes: 17 additions & 0 deletions ts/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { main } from "./main"
import expect from "expect"

describe('Main entry point', () => {
it('should correctly discover plugins', async () => {
const { application, pluginManager } = await main({
runtimeConfig: { discoverPlugins: 'true' },
confirmPluginInstall: async () => 'install',
withoutServer: true,
})
expect(pluginManager.loadedPlugins).toEqual({
'io.worldbrain.storex-hub.internal.self-test': expect.objectContaining({
running: true
})
})
})
})
52 changes: 37 additions & 15 deletions ts/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,56 @@ import { BcryptAccessTokenManager } from "./access-tokens";
import { createHttpServer } from "./server";
import { PluginManager } from './plugins/manager';
import { discoverInstalledPlugins } from './plugins/discovery/main';
import { PluginInstallConfirmer } from './plugins/discovery';

export async function main() {
const application = await setupApplication()
await maybeRunPluginDiscovery(application)
await startServer(application)
await loadPlugins(application)
export interface RuntimeConfig {
dbFilePath?: string
discoverPlugins?: string
}

export async function setupApplication() {
const application = new Application(getApplicationDependencies({
export async function main(options?: {
runtimeConfig?: RuntimeConfig,
confirmPluginInstall?: PluginInstallConfirmer,
withoutServer?: boolean
}) {
const runtimeConfig = options?.runtimeConfig ?? getRuntimeConfig()
const application = await setupApplication(runtimeConfig)
await maybeRunPluginDiscovery(application, { runtimeConfig, confirmPluginInstall: options?.confirmPluginInstall })
if (!options?.withoutServer) {
await startServer(application)
}
const pluginManager = await loadPlugins(application)
return { application, pluginManager }
}

export function getRuntimeConfig(): RuntimeConfig {
return {
dbFilePath: process.env.DB_PATH,
discoverPlugins: process.env.STOREX_HUB_DISCOVER_PLUGINS,
}
}

export async function setupApplication(runtimeConfig?: RuntimeConfig) {
const application = new Application(getApplicationDependencies({
dbFilePath: runtimeConfig?.dbFilePath,
}))
await application.setup()
return application
}

async function maybeRunPluginDiscovery(application: Application) {
if (!process.env.STOREX_HUB_DISCOVER_PLUGINS) {
return
}

const patternOrTrue = process.env.STOREX_HUB_DISCOVER_PLUGINS
if (patternOrTrue.toLowerCase() === 'false') {
async function maybeRunPluginDiscovery(application: Application, options?: {
runtimeConfig?: RuntimeConfig
confirmPluginInstall?: PluginInstallConfirmer,
}) {
const patternOrTrue = options?.runtimeConfig?.discoverPlugins
if (!patternOrTrue || patternOrTrue.toLowerCase() === 'false') {
return
}

await discoverInstalledPlugins(application, {
nodeModulesPath: join(process.cwd(), 'node_modules'),
pluginDirGlob: patternOrTrue.toLowerCase() !== 'true' ? patternOrTrue : undefined
pluginDirGlob: patternOrTrue.toLowerCase() !== 'true' ? patternOrTrue : undefined,
confirmPluginInstall: options?.confirmPluginInstall,
})

// TODO: Don't know why yet, but added plugins do not immediately get stored
Expand All @@ -50,6 +71,7 @@ async function loadPlugins(application: Application) {
pluginManagementStorage: storage.systemModules.plugins,
})
await pluginManager.setup(() => application.api())
return pluginManager
}

export async function startServer(application: Application) {
Expand Down
4 changes: 2 additions & 2 deletions ts/plugins/discovery/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import expect from "expect"
import { PluginInfo } from "@worldbrain/storex-hub-interfaces/lib/plugins"
import { withTestApplication } from "../../tests/api/index.tests"
import { discoverPlugins } from "."
import { PluginInfo } from "../types"
import expect from "expect"
import { Application } from "../../application"

const PLUGINS: { [path: string]: PluginInfo } = {
Expand Down
6 changes: 4 additions & 2 deletions ts/plugins/discovery/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { PluginInfo } from "../types";
import { PluginInfo } from "@worldbrain/storex-hub-interfaces/lib/plugins";
import { PluginManagementStorage } from "../storage";

export type PluginInstallConfirmer = (pluginInfo: PluginInfo, options: { pluginDir: string }) => Promise<'install' | 'skip' | 'abort'>

export async function discoverPlugins(pluginDirs: string[], options: {
getPluginInfo: (pluginDir: string) => Promise<PluginInfo | null>
confirmPluginInstall: (pluginInfo: PluginInfo, options: { pluginDir: string }) => Promise<'install' | 'skip' | 'abort'>
confirmPluginInstall: PluginInstallConfirmer
pluginManagementStorage: PluginManagementStorage
}) {
for (const pluginDir of pluginDirs) {
Expand Down
71 changes: 37 additions & 34 deletions ts/plugins/discovery/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import path from 'path'
import { existsSync, readFileSync } from 'fs'
import fastGlob from 'fast-glob'
import createPrompt from 'prompt-sync'
import { discoverPlugins } from '.'
import { PluginInfo } from '../types'
import { PluginInfo } from "@worldbrain/storex-hub-interfaces/lib/plugins";
import { discoverPlugins, PluginInstallConfirmer } from '.'
import { Application } from '../../application'
const prompt = createPrompt()

Expand All @@ -14,6 +14,7 @@ function validatePluginInfo(untrusted: any): PluginInfo | null {
export async function discoverInstalledPlugins(application: Application, options: {
pluginDirGlob?: string
nodeModulesPath: string
confirmPluginInstall?: PluginInstallConfirmer
}) {
const pluginDirGlob = (
options.pluginDirGlob || '<node_modules>/storex-hub-plugin-*'
Expand Down Expand Up @@ -42,38 +43,40 @@ export async function discoverInstalledPlugins(application: Application, options
const pluginInfo = validatePluginInfo(packageInfo['storexHub'])
return pluginInfo
},
async confirmPluginInstall(pluginInfo: PluginInfo, { pluginDir }) {
let result: string | undefined
do {
console.log([
`Found new plugin in directory ${pluginDir}:`,
`- Description: ${pluginInfo.description}`,
`- Website: ${pluginInfo.siteUrl}`,
].join('\n'))
result = prompt(
`Only install plugins you trust. Do you want to install this one? [y/N] `,
'N'
)
if (!result) {
return 'abort'
}
confirmPluginInstall: options.confirmPluginInstall ?? confirmPluginInstallByPrompt,
})
}

if (result) {
result = result.toLowerCase()
}
if (['y', 'n', 'yes', 'no'].indexOf(result) === -1) {
console.error(`Invalid response: ${result}`)
result = undefined
}
} while (!result)
const confirmPluginInstallByPrompt: PluginInstallConfirmer = async (pluginInfo: PluginInfo, { pluginDir }) => {
let result: string | undefined
do {
console.log([
`Found new plugin in directory ${pluginDir}:`,
`- Description: ${pluginInfo.description}`,
`- Website: ${pluginInfo.siteUrl}`,
].join('\n'))
result = prompt(
`Only install plugins you trust. Do you want to install this one? [y/N] `,
'N'
)
if (!result) {
return 'abort'
}

if (result.startsWith('y')) {
console.log('Installing plugin...')
return 'install'
} else {
console.log('OK, skipping this plugin...')
return 'skip'
}
},
})
if (result) {
result = result.toLowerCase()
}
if (['y', 'n', 'yes', 'no'].indexOf(result) === -1) {
console.error(`Invalid response: ${result}`)
result = undefined
}
} while (!result)

if (result.startsWith('y')) {
console.log('Installing plugin...')
return 'install'
} else {
console.log('OK, skipping this plugin...')
return 'skip'
}
}
9 changes: 5 additions & 4 deletions ts/plugins/manager.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { PluginManagementStorage } from "./storage";
import { PluginInfo, PluginEntryFunction, PluginInterface } from "./types";
import { PluginInfo, PluginEntryFunction, PluginInterface } from "@worldbrain/storex-hub-interfaces/lib/plugins";
import { StorexHubApi_v0 } from "../public-api";

export class PluginManager {
loadedPlugins: { [identifier: string]: PluginInterface } = {}

constructor(private options: {
pluginManagementStorage: PluginManagementStorage
}) {
Expand Down Expand Up @@ -39,15 +41,14 @@ export class PluginManager {
return null
}

let plugin: PluginInterface
try {
plugin = await entryFunction({ api: await createAPI() })
this.loadedPlugins[pluginInfo.identifier] = await entryFunction({ api: await createAPI() })
} catch (e) {
console.error(`ERROR during initialization plugin '${pluginInfo.identifier}':`)
console.error(e)
return null
}

return plugin
return this.loadedPlugins[pluginInfo.identifier]
}
}
2 changes: 1 addition & 1 deletion ts/plugins/storage/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as path from 'path'
import { StorageModule, StorageModuleConfig } from "@worldbrain/storex-pattern-modules";
import STORAGE_VERSIONS from "../../storage/versions";
import { PluginInfo } from "../types";
import { PluginInfo } from '@worldbrain/storex-hub-interfaces/lib/plugins';

export class PluginManagementStorage extends StorageModule {
getConfig = (): StorageModuleConfig => ({
Expand Down
9 changes: 0 additions & 9 deletions ts/plugins/test-plugins/one/main.ts

This file was deleted.

11 changes: 0 additions & 11 deletions ts/plugins/test-plugins/one/package.json

This file was deleted.

18 changes: 0 additions & 18 deletions ts/plugins/types.ts

This file was deleted.

40 changes: 0 additions & 40 deletions ts/public-api/client.ts

This file was deleted.

25 changes: 0 additions & 25 deletions ts/public-api/common.ts

This file was deleted.

4 changes: 1 addition & 3 deletions ts/public-api/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
export * from './server'
export * from './client'
export * from './common'
export * from '@worldbrain/storex-hub-interfaces/lib/api'
Loading

0 comments on commit 357de8c

Please sign in to comment.