From 5a4cf2b257139fcefac13d84f1ec183041c19c58 Mon Sep 17 00:00:00 2001 From: amerah-abdul Date: Thu, 12 Dec 2024 22:12:30 +0800 Subject: [PATCH 1/4] Enhance Loader tests with fixtures --- packages/ingest/tests/Loader.test.ts | 616 ++++++++++++++++++ packages/ingest/tests/Router.test.ts | 4 +- .../tests/fixtures/nested-plugin/plugin.js | 1 + .../fixtures/nested-plugin/sub-plugin.js | 6 + packages/ingest/tests/fixtures/package.json | 3 + .../ingest/tests/fixtures/plugins-config.js | 4 + .../tests/fixtures/test-module-commonjs.js | 3 + .../ingest/tests/fixtures/test-package.json | 5 + 8 files changed, 640 insertions(+), 2 deletions(-) create mode 100644 packages/ingest/tests/Loader.test.ts create mode 100644 packages/ingest/tests/fixtures/nested-plugin/plugin.js create mode 100644 packages/ingest/tests/fixtures/nested-plugin/sub-plugin.js create mode 100644 packages/ingest/tests/fixtures/package.json create mode 100644 packages/ingest/tests/fixtures/plugins-config.js create mode 100644 packages/ingest/tests/fixtures/test-module-commonjs.js create mode 100644 packages/ingest/tests/fixtures/test-package.json diff --git a/packages/ingest/tests/Loader.test.ts b/packages/ingest/tests/Loader.test.ts new file mode 100644 index 0000000..c2f3e42 --- /dev/null +++ b/packages/ingest/tests/Loader.test.ts @@ -0,0 +1,616 @@ +/** + * Test suite for the ConfigLoader and PluginLoader classes + * These classes handle dynamic loading and configuration of plugins in the system + */ + +import { expect } from 'chai'; +import path from 'node:path'; +import NodeFS from '@stackpress/types/dist/system/NodeFS'; +import { ConfigLoader, PluginLoader } from '../src/Loader'; + +/** + * ConfigLoader Test Suite + * Tests the functionality for loading and managing configuration files + */ +describe('ConfigLoader', () => { + let loader: ConfigLoader; + + beforeEach(() => { + loader = new ConfigLoader(); + }); + + /** + * Constructor Tests + * Verifies proper initialization of ConfigLoader with default and custom options + */ + describe('constructor', () => { + it('should initialize with default options', () => { + expect(loader).to.be.instanceOf(ConfigLoader); + }); + + it('should accept custom options', () => { + const fs = new NodeFS(); + const cwd = '/custom/path'; + const customLoader = new ConfigLoader({ fs, cwd }); + expect(customLoader).to.be.instanceOf(ConfigLoader); + }); + }); + + /** + * Import Method Tests + * Tests dynamic import functionality for different module types and scenarios + */ + describe('import', () => { + it('should return defaults when file not found', async () => { + const defaults = { test: true }; + const result = await loader.import('/non/existent/path', defaults); + expect(result).to.deep.equal(defaults); + }); + + it('should throw when file not found and no defaults provided', async () => { + try { + await loader.import('/non/existent/path'); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error).to.exist; + } + }); + + it('should handle package.json with plugins key', async () => { + const pkgPath = path.join(__dirname, 'fixtures', 'test-package.json'); + const result = await loader.import(pkgPath); + expect(result).to.deep.equal({ test: true }); + }); + + it('should handle ES modules with default export', async () => { + const modulePath = path.join(__dirname, 'fixtures', 'test-module-commonjs.js'); + const result = await loader.import(modulePath); + expect(result).to.deep.equal({ test: true }); + }); + + it('should handle non-cached imports', async () => { + const customLoader = new ConfigLoader({ cache: false }); + const modulePath = path.join(__dirname, 'fixtures', 'test-module-commonjs.js'); + const result = await customLoader.import(modulePath); + expect(result).to.deep.equal({ test: true }); + }); + + it('should handle import with non-existent file and no defaults', async () => { + const configLoader = new ConfigLoader({ + cwd: path.join(__dirname, 'fixtures') + }); + + try { + await configLoader.import('non-existent-file'); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error).to.exist; + expect(error.message).to.include('Could not resolve'); + } + }); + + it('should handle import with non-existent file and defaults', async () => { + const configLoader = new ConfigLoader({ + cwd: path.join(__dirname, 'fixtures') + }); + + const result = await configLoader.import('non-existent-file', { default: true }); + expect(result).to.deep.equal({ default: true }); + }); + + it('should handle import with ES module default export', async () => { + const configLoader = new ConfigLoader({ + cwd: path.join(__dirname, 'fixtures') + }); + + const result = await configLoader.import(path.join(__dirname, 'fixtures', 'test-module-commonjs.js')); + expect(result).to.deep.equal({ test: true }); + }); + + it('should handle import with package.json and key', async () => { + const configLoader = new ConfigLoader({ + cwd: path.join(__dirname, 'fixtures'), + key: 'plugins' + }); + + const result = await configLoader.import(path.join(__dirname, 'fixtures', 'package.json')); + expect(result).to.deep.equal(['plugin1', 'plugin2']); + }); + }); + + /** + * Require Method Tests + * Tests synchronous require functionality and cache management + */ + describe('require', () => { + it('should return defaults when file not found', () => { + const defaults = { test: true }; + const result = loader.require('/non/existent/path', defaults); + expect(result).to.deep.equal(defaults); + }); + + it('should throw when file not found and no defaults provided', () => { + try { + loader.require('/non/existent/path'); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error).to.exist; + } + }); + + it('should handle require cache correctly', () => { + // First require + const modulePath = path.join(__dirname, 'fixtures', 'test-module-commonjs.js'); + const result1 = loader.require(modulePath); + expect(result1).to.deep.equal({ test: true }); + + // Modify the module in require.cache to test caching + const cachedModule = require.cache[require.resolve(modulePath)]; + if (cachedModule) { + cachedModule.exports = { test: false }; + } + + // Second require should use cache + const result2 = loader.require(modulePath); + expect(result2).to.deep.equal({ test: false }); + + // Clean up + delete require.cache[require.resolve(modulePath)]; + }); + + it('should bypass require cache when cache is disabled', () => { + const customLoader = new ConfigLoader({ cache: false }); + const modulePath = path.join(__dirname, 'fixtures', 'test-module-commonjs.js'); + + // First require + const result1 = customLoader.require(modulePath); + expect(result1).to.deep.equal({ test: true }); + + // Modify the module in require.cache + const cachedModule = require.cache[require.resolve(modulePath)]; + if (cachedModule) { + cachedModule.exports = { test: false }; + } + + // Second require should not use cache + const result2 = customLoader.require(modulePath); + expect(result2).to.deep.equal({ test: true }); + + // Clean up + delete require.cache[require.resolve(modulePath)]; + }); + + it('should handle require with non-existent file and no defaults', () => { + const configLoader = new ConfigLoader({ + cwd: path.join(__dirname, 'fixtures') + }); + + try { + configLoader.require('non-existent-file'); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error).to.exist; + expect(error.message).to.include('Could not resolve'); + } + }); + + it('should handle require with non-existent file and defaults', () => { + const configLoader = new ConfigLoader({ + cwd: path.join(__dirname, 'fixtures') + }); + + const result = configLoader.require('non-existent-file', { default: true }); + expect(result).to.deep.equal({ default: true }); + }); + + it('should handle require with CommonJS module', () => { + const configLoader = new ConfigLoader({ + cwd: path.join(__dirname, 'fixtures') + }); + + const result = configLoader.require(path.join(__dirname, 'fixtures', 'test-module-commonjs.js')); + expect(result).to.deep.equal({ test: true }); + }); + + it('should handle require with package.json and key', () => { + const configLoader = new ConfigLoader({ + cwd: path.join(__dirname, 'fixtures'), + key: 'plugins' + }); + + const result = configLoader.require(path.join(__dirname, 'fixtures', 'package.json')); + expect(result).to.deep.equal(['plugin1', 'plugin2']); + }); + }); + + /** + * Basepath Method Tests + * Tests the functionality for resolving base paths + */ + describe('basepath', () => { + it('should handle basepath with different extensions', () => { + const configLoader = new ConfigLoader({ + cwd: path.join(__dirname, 'fixtures') + }); + + expect(configLoader.basepath('/path/to/file.js')).to.equal('/path/to/file'); + expect(configLoader.basepath('/path/to/file.ts')).to.equal('/path/to/file'); + expect(configLoader.basepath('/path/to/file.json')).to.equal('/path/to/file.json'); + }); + }); + + /** + * Cache Configuration Tests + * Tests the functionality for cache configuration + */ + describe('cache', () => { + it('should handle cache configuration', () => { + const configLoader = new ConfigLoader({ + cwd: path.join(__dirname, 'fixtures'), + cache: false + }); + + const result1 = configLoader.require(path.join(__dirname, 'fixtures', 'test-module-commonjs.js')); + const result2 = configLoader.require(path.join(__dirname, 'fixtures', 'test-module-commonjs.js')); + expect(result1).to.deep.equal({ test: true }); + expect(result2).to.deep.equal({ test: true }); + }); + }); + + /** + * Filenames Configuration Tests + * Tests the functionality for filenames configuration + */ + describe('filenames', () => { + it('should handle different filenames configuration', () => { + const configLoader = new ConfigLoader({ + cwd: path.join(__dirname, 'fixtures'), + filenames: ['.custom'] + }); + + const result = configLoader.resolve(); + expect(result).to.be.null; + }); + }); +}); + +/** + * PluginLoader Test Suite + * Tests the functionality for loading and managing plugins + */ +describe('PluginLoader', () => { + let loader: PluginLoader; + const fixturesDir = path.join(__dirname, 'fixtures'); + const options = { + modules: path.join(fixturesDir, 'modules'), + plugins: ['test-plugin'] + }; + + beforeEach(() => { + loader = new PluginLoader(options); + }); + + /** + * Constructor Tests + * Verifies proper initialization of PluginLoader with provided options + */ + describe('constructor', () => { + it('should initialize with provided options', () => { + expect(loader).to.be.instanceOf(PluginLoader); + }); + + it('should handle string plugin configuration', () => { + const pluginPath = path.join(fixturesDir, 'test-module-commonjs.js'); + const testLoader = new PluginLoader({ + cwd: fixturesDir, + modules: fixturesDir, + plugins: [pluginPath] + }); + + expect(testLoader.plugins).to.deep.equal([pluginPath]); + }); + }); + + /** + * Plugins Getter Tests + * Tests the functionality for loading plugins from config files + */ + describe('plugins getter', () => { + it('should load plugins from config file', () => { + const configPath = path.join(fixturesDir, 'plugins-config.js'); + const configLoader = new PluginLoader({ + cwd: fixturesDir, + modules: options.modules, + plugins: require(configPath).plugins + }); + expect(configLoader.plugins).to.deep.equal(['plugin1', 'plugin2']); + }); + + it('should handle default export in ES modules', async () => { + const configPath = path.join(fixturesDir, 'test-module-commonjs.js'); + const configLoader = new PluginLoader({ + cwd: fixturesDir, + modules: options.modules + }); + const result = configLoader.plugins; + expect(Array.isArray(result)).to.be.true; + }); + + it('should handle empty or invalid config', () => { + const emptyLoader = new PluginLoader({ + cwd: path.join(fixturesDir, 'empty'), + modules: path.join(fixturesDir, 'empty') + }); + + expect(emptyLoader.plugins).to.deep.equal([]); + }); + + it('should handle plugin loading errors', async () => { + const mockLoader = async (name: string, plugin: unknown): Promise => {}; + const pluginPath = path.join(__dirname, 'fixtures', 'test-module-commonjs.js'); + const testLoader = new PluginLoader({ + plugins: [pluginPath] + }); + + await testLoader.bootstrap(mockLoader); + expect(testLoader['_bootstrapped']).to.be.true; + + try { + await testLoader.bootstrap(mockLoader); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error).to.exist; + } + }); + + it('should handle nested plugin configurations', async () => { + const nestedPluginPath = path.join(fixturesDir, 'nested-plugin', 'plugin.js'); + const loadedPlugins: string[] = []; + const mockLoader = async (name: string, plugin: unknown): Promise => { + loadedPlugins.push(path.basename(name, '.js')); + }; + + const testLoader = new PluginLoader({ + cwd: path.dirname(nestedPluginPath), + plugins: [nestedPluginPath], + modules: path.dirname(nestedPluginPath) + }); + + // First get the plugins + const plugins = testLoader.plugins; + expect(plugins).to.deep.equal([nestedPluginPath]); + + // Then bootstrap them + await testLoader.bootstrap(mockLoader); + expect(testLoader['_bootstrapped']).to.be.true; + expect(loadedPlugins).to.include('sub-plugin'); + }); + + it('should handle non-existent plugin files', async () => { + const invalidLoader = new PluginLoader({ + plugins: ['non-existent-plugin'] + }); + + try { + await invalidLoader.bootstrap(async () => {}); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error).to.exist; + } + }); + + it('should handle plugins from node_modules', async () => { + const pluginPath = path.join(fixturesDir, 'test-module-commonjs.js'); + const testLoader = new PluginLoader({ + plugins: [pluginPath], + modules: fixturesDir + }); + + const loadedPlugins: string[] = []; + await testLoader.bootstrap(async (name, plugin) => { + loadedPlugins.push(name); + }); + + expect(loadedPlugins).to.include(path.basename(pluginPath, '.js')); + }); + + it('should handle plugins with absolute paths', async () => { + const pluginPath = path.join(fixturesDir, 'test-module-commonjs.js'); + const testLoader = new PluginLoader({ + plugins: [pluginPath], + modules: fixturesDir + }); + + const loadedPlugins: string[] = []; + await testLoader.bootstrap(async (name, plugin) => { + loadedPlugins.push(name); + }); + + // Test plugin path starting with modules directory + const modulesLoader = new PluginLoader({ + plugins: [path.join(fixturesDir, 'test-module-commonjs.js')], + modules: fixturesDir + }); + const modulesPlugins: string[] = []; + await modulesLoader.bootstrap(async (name, plugin) => { + modulesPlugins.push(name); + }); + expect(modulesPlugins).to.include(path.basename(pluginPath, '.js')); + + // Test plugin path starting with cwd + const cwdLoader = new PluginLoader({ + plugins: [path.join(fixturesDir, 'test-module-commonjs.js')], + cwd: fixturesDir, + modules: fixturesDir + }); + const cwdPlugins: string[] = []; + await cwdLoader.bootstrap(async (name, plugin) => { + cwdPlugins.push(name); + }); + expect(cwdPlugins).to.include(path.basename(pluginPath, '.js')); + }); + + it('should handle nested array plugins', async () => { + const nestedPluginPath = path.join(fixturesDir, 'nested-plugin', 'plugin.js'); + const loadedPlugins: string[] = []; + const mockLoader = async (name: string, plugin: unknown): Promise => { + loadedPlugins.push(path.basename(name, '.js')); + }; + + const testLoader = new PluginLoader({ + cwd: fixturesDir, + modules: fixturesDir, + plugins: [nestedPluginPath] + }); + + // First get the plugins + const plugins = testLoader.plugins; + expect(plugins).to.deep.equal([nestedPluginPath]); + + // Then bootstrap them + await testLoader.bootstrap(mockLoader); + expect(testLoader['_bootstrapped']).to.be.true; + expect(loadedPlugins).to.include('sub-plugin'); + }); + + it('should handle package.json plugins', () => { + const packageJsonPath = path.join(fixturesDir, 'package.json'); + + const testLoader = new PluginLoader({ + cwd: fixturesDir, + modules: fixturesDir, + key: 'plugins' + }); + + expect(testLoader.plugins).to.deep.equal(['plugin1', 'plugin2']); + }); + + it('should handle string plugin input', () => { + const pluginPath = path.join(fixturesDir, 'test-module.js'); + + const testLoader = new PluginLoader({ + cwd: fixturesDir, + modules: fixturesDir, + plugins: [pluginPath] + }); + + expect(testLoader.plugins).to.deep.equal([pluginPath]); + }); + + it('should handle relative paths in plugins', async () => { + const testLoader = new PluginLoader({ + cwd: path.join(fixturesDir, 'nested-plugin'), + modules: fixturesDir, + plugins: ['../test-module-commonjs.js'] + }); + + const loadedPlugins: string[] = []; + await testLoader.bootstrap(async (name, plugin) => { + loadedPlugins.push(name); + }); + + expect(loadedPlugins).to.include('../test-module-commonjs'); + }); + + it('should handle module paths in plugins', async () => { + const modulePath = path.join(fixturesDir, 'test-module-commonjs.js'); + const testLoader = new PluginLoader({ + cwd: fixturesDir, + modules: fixturesDir, + plugins: [modulePath] + }); + + const loadedPlugins: string[] = []; + await testLoader.bootstrap(async (name, plugin) => { + loadedPlugins.push(name); + }); + + expect(loadedPlugins).to.include('test-module-commonjs'); + }); + + it('should handle absolute paths in plugins', async () => { + const absolutePath = path.resolve(fixturesDir, 'test-module-commonjs.js'); + const testLoader = new PluginLoader({ + cwd: fixturesDir, + modules: fixturesDir, + plugins: [absolutePath] + }); + + const loadedPlugins: string[] = []; + await testLoader.bootstrap(async (name, plugin) => { + loadedPlugins.push(name); + }); + + expect(loadedPlugins).to.include('test-module-commonjs'); + }); + }); + + /** + * Bootstrap Method Tests + * Tests the functionality for bootstrapping plugins + */ + describe('bootstrap', () => { + it('should allow multiple bootstrapping', async () => { + // Mock plugin loader function + const mockLoader = async (name: string, plugin: unknown): Promise => { + // Just simulate loading without returning anything + }; + + const pluginPath = path.join(fixturesDir, 'test-module-commonjs.js'); + const testLoader = new PluginLoader({ + cwd: fixturesDir, + modules: fixturesDir, + plugins: [pluginPath] + }); + + // First bootstrap should work + await testLoader.bootstrap(mockLoader); + expect(testLoader['_bootstrapped']).to.be.true; + + // Second bootstrap should be allowed + await testLoader.bootstrap(mockLoader); + expect(testLoader['_bootstrapped']).to.be.true; + }); + + it('should handle nested plugin configurations', async () => { + // Mock plugin loader function that simulates loading nested plugins + const loadedPlugins: string[] = []; + const mockLoader = async (name: string, plugin: unknown): Promise => { + loadedPlugins.push(path.basename(name, '.js')); + }; + + const nestedPluginPath = path.join(fixturesDir, 'nested-plugin', 'plugin.js'); + const testLoader = new PluginLoader({ + cwd: path.dirname(nestedPluginPath), + plugins: [nestedPluginPath], + modules: path.dirname(nestedPluginPath) + }); + + // Bootstrap should handle nested plugin configurations + await testLoader.bootstrap(mockLoader); + expect(testLoader['_bootstrapped']).to.be.true; + expect(loadedPlugins).to.include('sub-plugin'); + }); + + it('should handle plugin loading errors', async () => { + // Mock plugin loader function that throws an error + const mockLoader = async (name: string, plugin: unknown): Promise => { + throw new Error('Plugin loading error'); + }; + + const pluginPath = path.join(fixturesDir, 'test-module-commonjs.js'); + const testLoader = new PluginLoader({ + plugins: [pluginPath] + }); + + // Bootstrap should handle plugin loading errors + try { + await testLoader.bootstrap(mockLoader); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error).to.exist; + expect(error.message).to.include('Plugin loading error'); + } + }); + }); +}); diff --git a/packages/ingest/tests/Router.test.ts b/packages/ingest/tests/Router.test.ts index 05ef89b..92fc63d 100644 --- a/packages/ingest/tests/Router.test.ts +++ b/packages/ingest/tests/Router.test.ts @@ -2,7 +2,7 @@ import { describe, it } from 'mocha'; import { expect } from 'chai'; import type { Method } from '@stackpress/types/dist/types'; -import type { RouterEntry } from '../src/types'; +import type { EntryAction } from '../src/types'; import path from 'path'; import Router from '../src/Router'; import Request from '../src/Request'; @@ -93,7 +93,7 @@ describe('Router Tests', () => { for (const method of methods) { const route = router[method].bind(router) as ( path: string, - action: RouterEntry, priority?: number + action: EntryAction, priority?: number ) => Router; route('/some/route/path', path.join(__dirname, 'fixtures/any')); const METHOD = method.toUpperCase() as Method; diff --git a/packages/ingest/tests/fixtures/nested-plugin/plugin.js b/packages/ingest/tests/fixtures/nested-plugin/plugin.js new file mode 100644 index 0000000..4e4feda --- /dev/null +++ b/packages/ingest/tests/fixtures/nested-plugin/plugin.js @@ -0,0 +1 @@ +module.exports = ['./sub-plugin.js']; diff --git a/packages/ingest/tests/fixtures/nested-plugin/sub-plugin.js b/packages/ingest/tests/fixtures/nested-plugin/sub-plugin.js new file mode 100644 index 0000000..f8ddea6 --- /dev/null +++ b/packages/ingest/tests/fixtures/nested-plugin/sub-plugin.js @@ -0,0 +1,6 @@ +module.exports = { + name: 'sub-plugin', + setup: () => ({ + subFunction: () => 'sub' + }) +}; diff --git a/packages/ingest/tests/fixtures/package.json b/packages/ingest/tests/fixtures/package.json new file mode 100644 index 0000000..b6e2f01 --- /dev/null +++ b/packages/ingest/tests/fixtures/package.json @@ -0,0 +1,3 @@ +{ + "plugins": ["plugin1", "plugin2"] +} diff --git a/packages/ingest/tests/fixtures/plugins-config.js b/packages/ingest/tests/fixtures/plugins-config.js new file mode 100644 index 0000000..91e9230 --- /dev/null +++ b/packages/ingest/tests/fixtures/plugins-config.js @@ -0,0 +1,4 @@ +// Export the plugins array directly +module.exports = { + plugins: ['plugin1', 'plugin2'] +}; diff --git a/packages/ingest/tests/fixtures/test-module-commonjs.js b/packages/ingest/tests/fixtures/test-module-commonjs.js new file mode 100644 index 0000000..72a060c --- /dev/null +++ b/packages/ingest/tests/fixtures/test-module-commonjs.js @@ -0,0 +1,3 @@ +module.exports = { + test: true +}; diff --git a/packages/ingest/tests/fixtures/test-package.json b/packages/ingest/tests/fixtures/test-package.json new file mode 100644 index 0000000..fd678a1 --- /dev/null +++ b/packages/ingest/tests/fixtures/test-package.json @@ -0,0 +1,5 @@ +{ + "plugins": { + "test": true + } +} From 60581a48fd903514e981565d5343171d2f25cfcb Mon Sep 17 00:00:00 2001 From: amerah-abdul Date: Fri, 13 Dec 2024 00:33:43 +0800 Subject: [PATCH 2/4] Add tests to helpers.test.ts in ingest --- packages/ingest/tests/Loader.test.ts | 113 ++++++--- packages/ingest/tests/helpers.test.ts | 332 +++++++++++++++++++++++++- 2 files changed, 396 insertions(+), 49 deletions(-) diff --git a/packages/ingest/tests/Loader.test.ts b/packages/ingest/tests/Loader.test.ts index c2f3e42..e4311c3 100644 --- a/packages/ingest/tests/Loader.test.ts +++ b/packages/ingest/tests/Loader.test.ts @@ -1,6 +1,7 @@ /** * Test suite for the ConfigLoader and PluginLoader classes - * These classes handle dynamic loading and configuration of plugins in the system + * These classes handle dynamic loading and configuration of plugins + * in the system */ import { expect } from 'chai'; @@ -21,7 +22,8 @@ describe('ConfigLoader', () => { /** * Constructor Tests - * Verifies proper initialization of ConfigLoader with default and custom options + * Verifies proper initialization of ConfigLoader with default + * and custom options */ describe('constructor', () => { it('should initialize with default options', () => { @@ -38,7 +40,8 @@ describe('ConfigLoader', () => { /** * Import Method Tests - * Tests dynamic import functionality for different module types and scenarios + * Tests dynamic import functionality for different module types + * and scenarios */ describe('import', () => { it('should return defaults when file not found', async () => { @@ -47,7 +50,8 @@ describe('ConfigLoader', () => { expect(result).to.deep.equal(defaults); }); - it('should throw when file not found and no defaults provided', async () => { + it('should throw when file not found and no defaults provided', + async () => { try { await loader.import('/non/existent/path'); expect.fail('Should have thrown an error'); @@ -57,25 +61,29 @@ describe('ConfigLoader', () => { }); it('should handle package.json with plugins key', async () => { - const pkgPath = path.join(__dirname, 'fixtures', 'test-package.json'); + const pkgPath = path.join(__dirname, + 'fixtures', 'test-package.json'); const result = await loader.import(pkgPath); expect(result).to.deep.equal({ test: true }); }); it('should handle ES modules with default export', async () => { - const modulePath = path.join(__dirname, 'fixtures', 'test-module-commonjs.js'); + const modulePath = path.join(__dirname, + 'fixtures', 'test-module-commonjs.js'); const result = await loader.import(modulePath); expect(result).to.deep.equal({ test: true }); }); it('should handle non-cached imports', async () => { const customLoader = new ConfigLoader({ cache: false }); - const modulePath = path.join(__dirname, 'fixtures', 'test-module-commonjs.js'); + const modulePath = path.join(__dirname, 'fixtures', + 'test-module-commonjs.js'); const result = await customLoader.import(modulePath); expect(result).to.deep.equal({ test: true }); }); - it('should handle import with non-existent file and no defaults', async () => { + it('should handle import with non-existent file and no defaults', + async () => { const configLoader = new ConfigLoader({ cwd: path.join(__dirname, 'fixtures') }); @@ -89,21 +97,25 @@ describe('ConfigLoader', () => { } }); - it('should handle import with non-existent file and defaults', async () => { + it('should handle import with non-existent file and defaults', + async () => { const configLoader = new ConfigLoader({ cwd: path.join(__dirname, 'fixtures') }); - const result = await configLoader.import('non-existent-file', { default: true }); + const result = await configLoader.import('non-existent-file', + { default: true }); expect(result).to.deep.equal({ default: true }); }); - it('should handle import with ES module default export', async () => { - const configLoader = new ConfigLoader({ + it('should handle import with ES module default export', + async () => { + const configLoader = new ConfigLoader({ cwd: path.join(__dirname, 'fixtures') }); - const result = await configLoader.import(path.join(__dirname, 'fixtures', 'test-module-commonjs.js')); + const result = await configLoader.import(path.join(__dirname, + 'fixtures', 'test-module-commonjs.js')); expect(result).to.deep.equal({ test: true }); }); @@ -113,7 +125,8 @@ describe('ConfigLoader', () => { key: 'plugins' }); - const result = await configLoader.import(path.join(__dirname, 'fixtures', 'package.json')); + const result = await configLoader.import(path.join(__dirname, + 'fixtures', 'package.json')); expect(result).to.deep.equal(['plugin1', 'plugin2']); }); }); @@ -129,7 +142,8 @@ describe('ConfigLoader', () => { expect(result).to.deep.equal(defaults); }); - it('should throw when file not found and no defaults provided', () => { + it('should throw when file not found and no defaults provided', + () => { try { loader.require('/non/existent/path'); expect.fail('Should have thrown an error'); @@ -140,7 +154,8 @@ describe('ConfigLoader', () => { it('should handle require cache correctly', () => { // First require - const modulePath = path.join(__dirname, 'fixtures', 'test-module-commonjs.js'); + const modulePath = path.join(__dirname, + 'fixtures', 'test-module-commonjs.js'); const result1 = loader.require(modulePath); expect(result1).to.deep.equal({ test: true }); @@ -160,7 +175,8 @@ describe('ConfigLoader', () => { it('should bypass require cache when cache is disabled', () => { const customLoader = new ConfigLoader({ cache: false }); - const modulePath = path.join(__dirname, 'fixtures', 'test-module-commonjs.js'); + const modulePath = path.join(__dirname, 'fixtures', + 'test-module-commonjs.js'); // First require const result1 = customLoader.require(modulePath); @@ -180,7 +196,8 @@ describe('ConfigLoader', () => { delete require.cache[require.resolve(modulePath)]; }); - it('should handle require with non-existent file and no defaults', () => { + it('should handle require with non-existent file and no defaults', + () => { const configLoader = new ConfigLoader({ cwd: path.join(__dirname, 'fixtures') }); @@ -194,12 +211,14 @@ describe('ConfigLoader', () => { } }); - it('should handle require with non-existent file and defaults', () => { + it('should handle require with non-existent file and defaults', + () => { const configLoader = new ConfigLoader({ cwd: path.join(__dirname, 'fixtures') }); - const result = configLoader.require('non-existent-file', { default: true }); + const result = configLoader.require('non-existent-file', + { default: true }); expect(result).to.deep.equal({ default: true }); }); @@ -208,7 +227,8 @@ describe('ConfigLoader', () => { cwd: path.join(__dirname, 'fixtures') }); - const result = configLoader.require(path.join(__dirname, 'fixtures', 'test-module-commonjs.js')); + const result = configLoader.require(path.join(__dirname, + 'fixtures', 'test-module-commonjs.js')); expect(result).to.deep.equal({ test: true }); }); @@ -218,7 +238,8 @@ describe('ConfigLoader', () => { key: 'plugins' }); - const result = configLoader.require(path.join(__dirname, 'fixtures', 'package.json')); + const result = configLoader.require(path.join(__dirname, + 'fixtures', 'package.json')); expect(result).to.deep.equal(['plugin1', 'plugin2']); }); }); @@ -233,9 +254,12 @@ describe('ConfigLoader', () => { cwd: path.join(__dirname, 'fixtures') }); - expect(configLoader.basepath('/path/to/file.js')).to.equal('/path/to/file'); - expect(configLoader.basepath('/path/to/file.ts')).to.equal('/path/to/file'); - expect(configLoader.basepath('/path/to/file.json')).to.equal('/path/to/file.json'); + expect(configLoader.basepath + ('/path/to/file.js')).to.equal('/path/to/file'); + expect(configLoader.basepath + ('/path/to/file.ts')).to.equal('/path/to/file'); + expect(configLoader.basepath + ('/path/to/file.json')).to.equal('/path/to/file.json'); }); }); @@ -250,8 +274,10 @@ describe('ConfigLoader', () => { cache: false }); - const result1 = configLoader.require(path.join(__dirname, 'fixtures', 'test-module-commonjs.js')); - const result2 = configLoader.require(path.join(__dirname, 'fixtures', 'test-module-commonjs.js')); + const result1 = configLoader.require(path.join(__dirname, + 'fixtures', 'test-module-commonjs.js')); + const result2 = configLoader.require(path.join(__dirname, + 'fixtures', 'test-module-commonjs.js')); expect(result1).to.deep.equal({ test: true }); expect(result2).to.deep.equal({ test: true }); }); @@ -346,8 +372,10 @@ describe('PluginLoader', () => { }); it('should handle plugin loading errors', async () => { - const mockLoader = async (name: string, plugin: unknown): Promise => {}; - const pluginPath = path.join(__dirname, 'fixtures', 'test-module-commonjs.js'); + const mockLoader = async (name: string, plugin: unknown): + Promise => {}; + const pluginPath = path.join(__dirname, 'fixtures', + 'test-module-commonjs.js'); const testLoader = new PluginLoader({ plugins: [pluginPath] }); @@ -364,9 +392,11 @@ describe('PluginLoader', () => { }); it('should handle nested plugin configurations', async () => { - const nestedPluginPath = path.join(fixturesDir, 'nested-plugin', 'plugin.js'); + const nestedPluginPath = path.join(fixturesDir, + 'nested-plugin', 'plugin.js'); const loadedPlugins: string[] = []; - const mockLoader = async (name: string, plugin: unknown): Promise => { + const mockLoader = async (name: string, plugin: unknown): + Promise => { loadedPlugins.push(path.basename(name, '.js')); }; @@ -451,9 +481,11 @@ describe('PluginLoader', () => { }); it('should handle nested array plugins', async () => { - const nestedPluginPath = path.join(fixturesDir, 'nested-plugin', 'plugin.js'); + const nestedPluginPath = path.join(fixturesDir, + 'nested-plugin', 'plugin.js'); const loadedPlugins: string[] = []; - const mockLoader = async (name: string, plugin: unknown): Promise => { + const mockLoader = + async (name: string, plugin: unknown): Promise => { loadedPlugins.push(path.basename(name, '.js')); }; @@ -552,7 +584,8 @@ describe('PluginLoader', () => { describe('bootstrap', () => { it('should allow multiple bootstrapping', async () => { // Mock plugin loader function - const mockLoader = async (name: string, plugin: unknown): Promise => { + const mockLoader = + async (name: string, plugin: unknown): Promise => { // Just simulate loading without returning anything }; @@ -575,11 +608,13 @@ describe('PluginLoader', () => { it('should handle nested plugin configurations', async () => { // Mock plugin loader function that simulates loading nested plugins const loadedPlugins: string[] = []; - const mockLoader = async (name: string, plugin: unknown): Promise => { + const mockLoader = + async (name: string, plugin: unknown): Promise => { loadedPlugins.push(path.basename(name, '.js')); }; - const nestedPluginPath = path.join(fixturesDir, 'nested-plugin', 'plugin.js'); + const nestedPluginPath = + path.join(fixturesDir, 'nested-plugin', 'plugin.js'); const testLoader = new PluginLoader({ cwd: path.dirname(nestedPluginPath), plugins: [nestedPluginPath], @@ -594,11 +629,13 @@ describe('PluginLoader', () => { it('should handle plugin loading errors', async () => { // Mock plugin loader function that throws an error - const mockLoader = async (name: string, plugin: unknown): Promise => { + const mockLoader = + async (name: string, plugin: unknown): Promise => { throw new Error('Plugin loading error'); }; - const pluginPath = path.join(fixturesDir, 'test-module-commonjs.js'); + const pluginPath = + path.join(fixturesDir, 'test-module-commonjs.js'); const testLoader = new PluginLoader({ plugins: [pluginPath] }); diff --git a/packages/ingest/tests/helpers.test.ts b/packages/ingest/tests/helpers.test.ts index badf7bb..66f578e 100644 --- a/packages/ingest/tests/helpers.test.ts +++ b/packages/ingest/tests/helpers.test.ts @@ -9,17 +9,42 @@ import { routeParams, withUnknownHost } from '../src/helpers'; -import { readableStreamToReadable } from '../src/http/helpers'; -import { readableToReadableStream } from '../src/fetch/helpers'; +import { readableStreamToReadable, imToURL, imQueryToObject } +from '../src/http/helpers'; +import { readableToReadableStream, fetchToURL, fetchQueryToObject } +from '../src/fetch/helpers'; import { Readable } from 'stream'; +import type { NodeRequest } from '../src/types'; +import type { IM } from '../src/types'; + +/** + * Test suite for helper functions used throughout the ingest package + * Tests cover utility functions for data transformation, + * stream handling, and URL manipulation + */ describe('helpers', () => { + /** + * Tests for isHash function + * Verifies object type checking functionality + */ + describe('isHash', () => { + /** + * Tests that isHash correctly identifies plain objects + * Verifies both empty objects and objects with properties + */ + it('should return true for plain objects', () => { expect(isHash({})).to.be.true; expect(isHash({ foo: 'bar' })).to.be.true; }); + /** + * Tests that isHash correctly rejects non-object values + * Checks various primitive types and special values + */ + it('should return false for non-objects', () => { expect(isHash(null)).to.be.false; expect(isHash(undefined)).to.be.false; @@ -30,6 +55,11 @@ describe('helpers', () => { }); }); + /** + * Tests for formDataToObject function + * Validates form data parsing for different content types + */ + describe('formDataToObject', () => { it('should handle JSON content type', () => { const result = formDataToObject( @@ -48,16 +78,24 @@ describe('helpers', () => { }); it('should handle multipart/form-data content type', () => { - const formData = '--boundary\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n--boundary--'; - const result = formDataToObject('multipart/form-data; boundary=boundary', formData); + const formData = + '--boundary\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n--boundary--'; + const result = formDataToObject('multipart/form-data; boundary=boundary', + formData); expect(result).to.deep.equal({ field1: 'value1' }); }); it('should return empty object for unknown content type', () => { - expect(formDataToObject('text/plain', 'some data')).to.deep.equal({}); + expect(formDataToObject + ('text/plain', 'some data')).to.deep.equal({}); }); }); + /** + * Tests for objectFromQuery function + * Ensures proper parsing of URL query strings to objects + */ + describe('objectFromQuery', () => { it('should parse query string with leading ?', () => { const result = objectFromQuery('?foo=bar&baz=qux'); @@ -74,9 +112,15 @@ describe('helpers', () => { }); }); + /** + * Tests for objectFromFormData function + * Verifies form data string parsing to objects + */ + describe('objectFromFormData', () => { it('should parse form data string', () => { - const formData = '--boundary\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n--boundary--'; + const formData = + '--boundary\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n--boundary--'; const result = objectFromFormData(formData); expect(result).to.deep.equal({ field1: 'value1' }); }); @@ -86,6 +130,10 @@ describe('helpers', () => { }); }); + /** + * Tests for objectFromJson function + * Validates JSON string parsing to objects + */ describe('objectFromJson', () => { it('should parse valid JSON object', () => { const result = objectFromJson('{"name":"test","value":123}'); @@ -97,6 +145,11 @@ describe('helpers', () => { }); }); + /** + * Tests for eventParams function + * Checks URL parameter extraction using regex patterns + */ + describe('eventParams', () => { it('should extract parameters from URL with non-global regex', () => { const result = eventParams('/user/(\\d+)/', 'user/123'); @@ -114,18 +167,29 @@ describe('helpers', () => { }); }); + /** + * Tests for routeParams function + * Validates route parameter extraction from URL paths + */ + describe('routeParams', () => { it('should extract named parameters from route', () => { - const result = routeParams('/users/:id/posts/:postId', '/users/123/posts/456'); + const result = routeParams + ('/users/:id/posts/:postId', '/users/123/posts/456'); expect(result.params).to.deep.equal({ id: '123', postId: '456' }); }); - it('should return object with empty params for non-matching route', () => { + it('should return object with empty params for non-matching route', + () => { const result = routeParams('/users/:id', '/posts/123'); expect(result.params).to.deep.equal({}); }); }); + /** + * Tests for withUnknownHost function + * Ensures proper handling of URLs with missing or invalid hosts + */ describe('withUnknownHost', () => { it('should add unknown host to URL without protocol', () => { const result = withUnknownHost('example.com/path'); @@ -134,16 +198,29 @@ describe('helpers', () => { it('should add unknown host to URL with protocol', () => { const result = withUnknownHost('https://example.com/path'); - expect(result).to.equal('http://unknownhost/https://example.com/path'); + expect(result).to.equal + ('http://unknownhost/https://example.com/path'); }); }); + /** + * Tests for readableStreamToReadable function + * Verifies conversion from Web API ReadableStream to Node.js Readable + * Covers empty streams, text data, and binary data + */ + describe('readableStreamToReadable', () => { + /** + * Tests basic conversion of text data + * Verifies that the resulting stream is a Node.js Readable + * and contains the correct data + */ + it('should convert ReadableStream to Node.js Readable', async () => { const data = 'Hello, World!'; const stream = new ReadableStream({ start(controller) { - controller.enqueue(new TextEncoder().encode(data)); + controller.enqueue(new TextEncoder().encode(data) as unknown as any); controller.close(); } }); @@ -157,8 +234,57 @@ describe('helpers', () => { } expect(result).to.equal(data); }); + + /** + * Tests handling of empty streams + * Ensures proper behavior when no data is present + */ + it('should handle empty ReadableStream', async () => { + const stream = new ReadableStream({ + start(controller) { + controller.close(); + } + }); + + const readable = readableStreamToReadable(stream); + expect(readable).to.be.instanceOf(Readable); + + const chunks: Buffer[] = []; + for await (const chunk of readable) { + chunks.push(chunk as Buffer); + } + expect(chunks).to.have.length(0); + }); + + /** + * Tests conversion of binary data + * Verifies that Uint8Array data is properly converted + * and can be read from the resulting stream + */ + it('should handle binary data', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5]); + const stream = new ReadableStream({ + start(controller: ReadableStreamDefaultController) { + controller.enqueue(data); + controller.close(); + } + }); + + const readable = readableStreamToReadable(stream); + const chunks: Uint8Array[] = []; + for await (const chunk of readable) { + chunks.push(chunk); + } + expect(Buffer.concat(chunks)).to.deep.equal(Buffer.from(data)); + }); }); + + /** + * Tests for readableToReadableStream function + * Validates conversion from Node.js Readable to Web API ReadableStream + * Tests empty streams and multiple chunk handling + */ describe('readableToReadableStream', () => { it('should convert Node.js Readable to ReadableStream', async () => { const data = 'Hello, World!'; @@ -178,5 +304,189 @@ describe('helpers', () => { const result = Buffer.concat(chunks).toString(); expect(result).to.equal(data); }); + + it('should handle empty Readable stream', async () => { + const readable = Readable.from([]); + const stream = readableToReadableStream(readable); + const reader = stream.getReader(); + const { done, value } = await reader.read(); + expect(done).to.be.true; + expect(value).to.be.undefined; + }); + + it('should handle multiple chunks', async () => { + const chunks = ['Hello', ' ', 'World', '!'].map(str => Buffer.from(str)); + const readable = Readable.from(chunks); + const stream = readableToReadableStream(readable); + const reader = stream.getReader(); + const receivedChunks: Uint8Array[] = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + receivedChunks.push(value); + } + const result = Buffer.concat(receivedChunks).toString(); + expect(result).to.equal('Hello World!'); + }); + }); + + /** + * Tests for fetchToURL function + * Ensures proper URL object creation from NodeRequest objects + */ + + describe('fetchToURL', () => { + it('should convert NodeRequest to URL', () => { + const request = { + url: 'https://example.com/path?query=value' + }; + const url = fetchToURL(request as NodeRequest); + expect(url).to.be.instanceOf(URL); + expect(url.href).to.equal('https://example.com/path?query=value'); + expect(url.pathname).to.equal('/path'); + expect(url.searchParams.get('query')).to.equal('value'); + }); + + it('should handle URLs without query parameters', () => { + const request = { + url: 'https://example.com/path' + }; + const url = fetchToURL(request as NodeRequest); + expect(url.href).to.equal('https://example.com/path'); + expect(url.search).to.equal(''); + }); + }); + + /** + * Tests for fetchQueryToObject function + * Validates URL query string parsing from NodeRequest objects + */ + describe('fetchQueryToObject', () => { + it('should convert URL query to object', () => { + const request = { + url: 'https://example.com/path?foo=bar&baz=qux' + }; + const query = fetchQueryToObject(request as NodeRequest); + expect(query).to.deep.equal({ + foo: 'bar', + baz: 'qux' + }); + }); + + it('should handle URL without query parameters', () => { + const request = { + url: 'https://example.com/path' + }; + const query = fetchQueryToObject(request as NodeRequest); + expect(query).to.deep.equal({}); + }); + }); + + /** + * Tests for imToURL function + * Verifies URL object creation from IncomingMessage objects + * Tests protocol handling, x-forwarded-proto variations, and invalid URLs + */ + describe('imToURL', () => { + it('should create URL from IM with default protocol', () => { + /** + * Tests default protocol handling + * Verifies that HTTPS is used when no protocol is specified + */ + const im = { + url: '/path', + headers: { host: 'example.com' } + } as unknown as IM; + const url = imToURL(im); + expect(url.href).to.equal('https://example.com/path'); + }); + + /** + * Tests x-forwarded-proto header handling + * Ensures protocol is correctly extracted from header + */ + it('should handle x-forwarded-proto header', () => { + const im = { + url: '/test', + headers: { + host: 'example.com', + 'x-forwarded-proto': 'http' + } + } as unknown as IM; + const url = imToURL(im); + expect(url.protocol).to.equal('http:'); + }); + + /** + * Tests array-format x-forwarded-proto header + * Verifies that first protocol in array is used + */ + it('should handle array x-forwarded-proto header', () => { + const im = { + url: '/test', + headers: { + host: 'example.com', + 'x-forwarded-proto': ['http', 'https'] + } + } as unknown as IM; + const url = imToURL(im); + expect(url.protocol).to.equal('http:'); + }); + + /** + * Tests comma-separated x-forwarded-proto header + * Ensures first protocol in list is used + */ + it('should handle comma-separated x-forwarded-proto', () => { + const im = { + url: '/test', + headers: { + host: 'example.com', + 'x-forwarded-proto': 'http, https' + } + } as unknown as IM; + const url = imToURL(im); + expect(url.protocol).to.equal('http:'); + }); + + /** + * Tests invalid URL handling + * Verifies fallback to unknownhost when URL is invalid + */ + it('should handle invalid URLs with unknownhost', () => { + const im = { + url: 'invalid-url', + headers: { host: undefined } + }; + const url = imToURL(im as IM); + expect(url.href).to.equal('https://undefinedinvalid-url/'); + }); + }); + + /** + * Tests for imQueryToObject function + * Ensures proper query string parsing from IncomingMessage objects + */ + describe('imQueryToObject', () => { + it('should convert IM URL query to object', () => { + const im = { + url: '/path?foo=bar&baz=qux', + headers: { host: 'example.com' } + }; + const query = imQueryToObject(im as IM); + expect(query).to.deep.equal({ + foo: 'bar', + baz: 'qux' + }); + }); + + it('should handle URL without query parameters', () => { + const im = { + url: '/path', + headers: { host: 'example.com' } + }; + const query = imQueryToObject(im as IM); + expect(query).to.deep.equal({}); + }); }); -}); +}); \ No newline at end of file From 4bd528c870ff0ceb1aff6576ad9d8185c215369f Mon Sep 17 00:00:00 2001 From: amerah-abdul Date: Fri, 13 Dec 2024 09:16:13 +0800 Subject: [PATCH 3/4] Add tests to Router.test.ts in ingest --- packages/ingest/tests/Router.test.ts | 111 +++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/packages/ingest/tests/Router.test.ts b/packages/ingest/tests/Router.test.ts index 92fc63d..f5e9390 100644 --- a/packages/ingest/tests/Router.test.ts +++ b/packages/ingest/tests/Router.test.ts @@ -122,4 +122,115 @@ describe('Router Tests', () => { await router.emit('POST /some/route/path', req, res); expect(res.body).to.equal(`POST /some/route/path`); }) + + it('Should handle route parameters correctly', async () => { + const router = new Router(); + router.get('/users/:id/posts/:postId', (req, res) => { + res.setBody('application/json', { + params: req.data.get(), + path: req.url.pathname + }); + }); + + const req = new Request({ + method: 'GET', + url: new URL('http://localhost/users/123/posts/456') + }); + const res = new Response(); + await router.emit('GET /users/123/posts/456', req, res); + + const body = res.body as { params: any, path: string }; + expect(body.params).to.deep.equal({ + id: '123', + postId: '456' + }); + expect(body.path).to.equal('/users/123/posts/456'); + }); + + it('Should handle wildcard routes correctly', async () => { + const router = new Router(); + router.get('/files/**', (req, res) => { + res.setBody('application/json', { + params: req.data.get(), + path: req.url.pathname + }); + }); + + const req = new Request({ + method: 'GET', + url: new URL('http://localhost/files/images/avatar.png') + }); + const res = new Response(); + await router.emit('GET /files/images/avatar.png', req, res); + + const body = res.body as { params: any, path: string }; + expect(body.path).to.equal('/files/images/avatar.png'); + }); + + it('Should handle route priority correctly', async () => { + const router = new Router(); + const calls: string[] = []; + + // Lower priority (1) route + router.get('/api/resource', (req, res) => { + calls.push('low'); + }, 1); + + // Higher priority (2) route + router.get('/api/resource', (req, res) => { + calls.push('high'); + }, 2); + + const req = new Request({ + method: 'GET', + url: new URL('http://localhost/api/resource') + }); + const res = new Response(); + await router.emit('GET /api/resource', req, res); + + // Higher priority should execute first + expect(calls).to.deep.equal(['high', 'low']); + }); + + it('Should handle router chaining with use() correctly', async () => { + const router1 = new Router(); + const router2 = new Router(); + + router1.get('/api/v1/test', (req, res) => { + res.setBody('text/plain', 'router1'); + }); + + router2.use(router1); + + const req = new Request({ + method: 'GET', + url: new URL('http://localhost/api/v1/test') + }); + const res = new Response(); + await router2.emit('GET /api/v1/test', req, res); + + expect(res.body).to.equal('router1'); + }); + + it('Should handle entry file caching correctly', async () => { + const uncachedRouter = new Router(false); + const cachedRouter = new Router(true); + + uncachedRouter.get('/test', path.join(__dirname, 'fixtures/get')); + cachedRouter.get('/test', path.join(__dirname, 'fixtures/get')); + + const req = new Request({ + method: 'GET', + url: new URL('http://localhost/test') + }); + const res1 = new Response(); + const res2 = new Response(); + + await uncachedRouter.emit('GET /test', req, res1); + await cachedRouter.emit('GET /test', req, res2); + + expect(res1.body).to.equal(res2.body); + expect(uncachedRouter.cache).to.be.false; + expect(cachedRouter.cache).to.be.true; + }); }) \ No newline at end of file From 11946bcd4746372b9b9659cbe4d605113de04bc6 Mon Sep 17 00:00:00 2001 From: amerah-abdul Date: Fri, 13 Dec 2024 11:25:03 +0800 Subject: [PATCH 4/4] Add tests to Server.test.ts in ingest --- packages/ingest/tests/Server.test.ts | 290 ++++++++++++++++++++++++++- 1 file changed, 288 insertions(+), 2 deletions(-) diff --git a/packages/ingest/tests/Server.test.ts b/packages/ingest/tests/Server.test.ts index 739305f..d7ee582 100644 --- a/packages/ingest/tests/Server.test.ts +++ b/packages/ingest/tests/Server.test.ts @@ -1,11 +1,21 @@ import { describe, it } from 'mocha'; import { expect } from 'chai'; +import { createServer, IncomingMessage, ServerResponse } from 'node:http'; +import { UnknownNest } from '@stackpress/types'; import Server from '../src/Server'; import Request from '../src/Request'; import Response from '../src/Response'; +import { ServerGateway } from '../src/types'; describe('Server Tests', () => { + /** + * Tests the route handling functionality: + * - Registering GET routes + * - Handling requests with data + * - Handling requests without data + * - Response body formatting + */ it('Should route to', async () => { const server = new Server(); server.get('/some/route/path', (req, res) => { @@ -20,12 +30,19 @@ describe('Server Tests', () => { const res = new Response(); await server.routeTo('GET', '/some/route/path', req, res); expect(res.body).to.equal('- bar'); - const response1 = await server.routeTo('GET', '/some/route/path', { foo: 'baz' }); + const response1 = await server.routeTo('GET', '/some/route/path', + { foo: 'baz' }); expect(response1.results).to.equal('- baz'); const response2 = await server.routeTo('GET', '/some/route/path'); expect(response2.results).to.equal('- undefined'); }); + /** + * Tests the event handling system: + * - Registering event handlers + * - Handling events with data + * - Handling events without data + */ it('Should call', async () => { const server = new Server(); server.on('foo', (req, res) => { @@ -45,4 +62,273 @@ describe('Server Tests', () => { const response2 = await server.call('foo'); expect(response2.results).to.equal('- undefined'); }); -}) \ No newline at end of file + + /** + * Tests the plugin system's registration and retrieval: + * - Registering plugins with configuration + * - Retrieving plugin configurations + * - Type-safe plugin config retrieval + */ + it('Should handle plugin registration and retrieval', () => { + const server = new Server(); + const config = { foo: 'bar' }; + + server.register('test-plugin', config); + expect(server.plugin('test-plugin')).to.deep.equal(config); + + const typedConfig = server.plugin<{ foo: string }>('test-plugin'); + expect(typedConfig?.foo).to.equal('bar'); + }); + + /** + * Tests the plugin bootstrapping process: + * - Plugin initialization + * - Plugin configuration + * - Bootstrap callback execution + */ + it('Should bootstrap plugins', async () => { + const server = new Server(); + const pluginConfig = { initialized: true }; + let called = false; + + const testPlugin = async () => { + called = true; + return pluginConfig; + }; + + server.loader.bootstrap = async (callback) => { + await callback('test-plugin', testPlugin); + return server.loader; + }; + + await server.bootstrap(); + + expect(called).to.be.true; + expect(server.plugin('test-plugin')).to.deep.equal(pluginConfig); + }); + + /** + * Tests HTTP server instance creation: + * - Server creation + * - Basic server methods availability + */ + it('Should create server instance', () => { + const server = new Server(); + const httpServer = server.create(); + + expect(typeof httpServer.listen).to.equal('function'); + expect(typeof httpServer.close).to.equal('function'); + }); + + /** + * Tests custom gateway and handler functionality: + * - Custom gateway implementation + * - Custom request handling + * - Response type safety + */ + it('Should handle custom gateway and handler', async () => { + type CustomResponse = { custom: boolean }; + + const customGateway = (server: Server) => { + return (options: any) => createServer(options); + }; + + const customHandler = async ( + ctx: Server, + req: unknown, + res: unknown + ): Promise => { + return { custom: true }; + }; + + const server = new Server({ + gateway: customGateway, + handler: customHandler + }); + + const result = await server.handle({ custom: true }, { custom: true }); + expect(result).to.deep.equal({ custom: true }); + }); + + /** + * Tests request and response object creation: + * - Request object initialization + * - Response object initialization + * - Status code handling + * - Headers management + */ + it('Should create request and response objects', () => { + const server = new Server(); + + const req = server.request({ + method: 'POST', + url: new URL('http://localhost/test'), + data: { test: true } + }); + + const res = server.response({ + headers: { 'content-type': 'application/json' } + }); + res.setStatus(201); + + expect(req.method).to.equal('POST'); + expect(req.data('test')).to.be.true; + expect(res.status).to.equal('Created'); + expect(res.headers.get('content-type')).to.equal('application/json'); + }); + + /** + * Tests configuration management: + * - Setting configuration values + * - Retrieving configuration values + * - Nested configuration handling + */ + it('Should handle config management', () => { + const server = new Server(); + + server.config.set('database', { url: 'mongodb://localhost' }); + server.config.set('api', { key: '123456' }); + + expect(server.config.get('database')).to.deep.equal + ({ url: 'mongodb://localhost' }); + const api = server.config.get('api') as { key: string }; + expect(api.key).to.equal('123456'); + }); + + /** + * Tests multiple route methods: + * - GET, POST, PUT, DELETE handlers + * - Method-specific behavior + */ + it('Should handle different HTTP methods', async () => { + const server = new Server(); + + server.get('/test', (req, res) => { + res.setBody('text/plain', 'GET'); + }); + server.post('/test', (req, res) => { + res.setBody('text/plain', 'POST'); + }); + server.put('/test', (req, res) => { + res.setBody('text/plain', 'PUT'); + }); + server.delete('/test', (req, res) => { + res.setBody('text/plain', 'DELETE'); + }); + + const getRes = await server.routeTo('GET', '/test'); + const postRes = await server.routeTo('POST', '/test'); + const putRes = await server.routeTo('PUT', '/test'); + const deleteRes = await server.routeTo('DELETE', '/test'); + + expect(getRes.results).to.equal('GET'); + expect(postRes.results).to.equal('POST'); + expect(putRes.results).to.equal('PUT'); + expect(deleteRes.results).to.equal('DELETE'); + }); + + /** + * Tests the server gateway setter: + * - Setting custom gateway + * - Gateway functionality + */ + it('Should set custom gateway', () => { + const server = new Server(); + + // Mock a minimal server implementation + const mockServer = { + listen: () => {}, + close: () => {}, + on: () => {}, + emit: () => {} + }; + + // Create a gateway that returns the mock server directly + const customGateway: ServerGateway = () => mockServer as any; + + server.gateway = customGateway; + const httpServer = server.create(); + + expect(typeof httpServer.listen).to.equal('function'); + }); + + /** + * Tests the server handler setter: + * - Setting custom handler + * - Handler execution + */ + it('Should set custom handler', async () => { + const server = new Server(); + const customResponse = { handled: true }; + + server.handler = async () => customResponse; + const result = await server.handle({}, {}); + + expect(result).to.deep.equal(customResponse); + }); + + /** + * Tests request creation with different initializers: + * - Empty initializer + * - Partial initializer + * - Full initializer + */ + it('Should create requests with different initializers', () => { + const server = new Server(); + + const emptyReq = server.request(); + expect(emptyReq.method).to.equal('GET'); + + const partialReq = server.request({ method: 'POST' }); + expect(partialReq.method).to.equal('POST'); + + const fullReq = server.request({ + method: 'PUT', + url: new URL('http://localhost/test'), + data: { test: true } + }); + expect(fullReq.method).to.equal('PUT'); + expect(fullReq.data('test')).to.be.true; + }); + + /** + * Tests response creation with different initializers: + * - Empty initializer + * - Headers initializer + */ + it('Should create responses with different initializers', () => { + const server = new Server(); + + const emptyRes = server.response(); + emptyRes.setStatus(200); // Set explicit OK status + expect(emptyRes.status).to.equal('OK'); + + const headerRes = server.response({ + headers: { 'content-type': 'application/json' } + }); + expect(headerRes.headers.get('content-type')).to.equal('application/json'); + }); + + /** + * Tests plugin system with async configuration: + * - Async plugin registration + * - Configuration retrieval + */ + it('Should handle async plugin configuration', async () => { + const server = new Server(); + const asyncConfig = { async: true }; + + const asyncPlugin = async () => { + await new Promise(resolve => setTimeout(resolve, 1)); + return asyncConfig; + }; + + server.loader.bootstrap = async (callback) => { + await callback('async-plugin', asyncPlugin); + return server.loader; + }; + + await server.bootstrap(); + expect(server.plugin('async-plugin')).to.deep.equal(asyncConfig); + }); +}); \ No newline at end of file