diff --git a/package.json b/package.json index 833ee91e9..c5a011a16 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "packages/*" ], "scripts": { - "test": "c8 --reporter=lcov yarn workspaces run test:ci", + "build": "yarn workspace @keyv/test-suite run build", + "test": " yarn build && c8 --reporter=lcov yarn workspaces run test:ci", "test:services:start": "docker-compose -f ./docker-compose.yaml up -d", "test:services:stop": "docker-compose -f ./docker-compose.yaml down -v", "clean": "rm -rf node_modules && rm -rf yarn.lock && yarn workspaces run clean" diff --git a/packages/test-suite/package.json b/packages/test-suite/package.json index 75d3d2f83..f2cdf7d1d 100644 --- a/packages/test-suite/package.json +++ b/packages/test-suite/package.json @@ -2,17 +2,23 @@ "name": "@keyv/test-suite", "version": "1.8.9", "description": "Test suite for Keyv API compliancy", - "main": "src/index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", "type": "commonjs", "scripts": { - "test": "xo && c8 ava --serial", - "test:ci": "xo && ava --serial", + "build": "tsc", + "prepare": "yarn build", + "test": "yarn build && xo && c8 ava --serial", + "test:ci": "yarn build && xo && ava --serial", "clean": "rm -rf node_modules && rm -rf ./coverage && rm -rf dist && rm -rf ./test/testdb.sqlite" }, "xo": { "rules": { "unicorn/prefer-module": 0, - "unicorn/prefer-node-protocol": 0 + "unicorn/prefer-node-protocol": 0, + "@typescript-eslint/no-unsafe-assignment": 0, + "@typescript-eslint/no-confusing-void-expression": 0, + "import/extensions": 0 } }, "repository": { @@ -39,13 +45,26 @@ "bignumber.js": "^9.1.1", "delay": "^5.0.0", "json-bigint": "^1.0.0", + "sqlite3": "^5.1.4", "timekeeper": "^2.2.0" }, "devDependencies": { + "@ava/typescript": "^3.0.1", + "ts-node": "^10.9.1", "@keyv/compress-brotli": "*", + "@types/json-bigint": "^1.0.1", + "@types/json-buffer": "^3.0.0", "ava": "^5.1.0", "c8": "^7.12.0", "keyv": "*", - "xo": "^0.53.1" + "typescript": "^4.9.5" + }, + "ava": { + "extensions": [ + "ts" + ], + "require": [ + "ts-node/register" + ] } } diff --git a/packages/test-suite/src/api.js b/packages/test-suite/src/api.ts similarity index 94% rename from packages/test-suite/src/api.js rename to packages/test-suite/src/api.ts index c404ed321..38ccf7e3b 100644 --- a/packages/test-suite/src/api.js +++ b/packages/test-suite/src/api.ts @@ -1,6 +1,10 @@ -const tk = require('timekeeper'); +import tk from 'timekeeper'; -const keyvApiTests = (test, Keyv, store) => { +import type {TestFn} from 'ava'; +import type KeyvModule from 'keyv'; +import type {KeyvStoreFn} from './types'; + +const keyvApiTests = (test: TestFn, Keyv: typeof KeyvModule, store: KeyvStoreFn) => { test.beforeEach(async () => { const keyv = new Keyv({store: store()}); await keyv.clear(); @@ -67,7 +71,7 @@ const keyvApiTests = (test, Keyv, store) => { await keyv.set('foo', 'bar'); await keyv.set('foo1', 'bar1', 1); await keyv.set('foo2', 'bar2'); - await new Promise(resolve => { + await new Promise(resolve => { setTimeout(() => { // Simulate database latency resolve(); @@ -179,4 +183,4 @@ const keyvApiTests = (test, Keyv, store) => { }); }; -module.exports = keyvApiTests; +export default keyvApiTests; diff --git a/packages/test-suite/src/compression.js b/packages/test-suite/src/compression.ts similarity index 88% rename from packages/test-suite/src/compression.js rename to packages/test-suite/src/compression.ts index 2c680083c..e19d9b45a 100644 --- a/packages/test-suite/src/compression.js +++ b/packages/test-suite/src/compression.ts @@ -1,6 +1,8 @@ -const Keyv = require('keyv'); +import Keyv, {type CompressionAdapter} from 'keyv'; -const keyvCompressionTests = (test, compression) => { +import type {TestFn} from 'ava'; + +const keyvCompressionTests = (test: TestFn, compression: CompressionAdapter) => { let keyv; test.beforeEach(async () => { keyv = new Keyv({ @@ -52,4 +54,4 @@ const keyvCompressionTests = (test, compression) => { }); }; -module.exports = keyvCompressionTests; +export default keyvCompressionTests; diff --git a/packages/test-suite/src/index.js b/packages/test-suite/src/index.js deleted file mode 100644 index 8adafe96d..000000000 --- a/packages/test-suite/src/index.js +++ /dev/null @@ -1,21 +0,0 @@ -const keyvApiTests = require('./api.js'); -const keyvValueTests = require('./values.js'); -const keyvNamepsaceTests = require('./namespace.js'); -const keyvOfficialTests = require('./official.js'); -const keyvIteratorTests = require('./iterator.js'); -const keyvCompresstionTests = require('./compression.js'); - -const keyvTestSuite = (test, Keyv, store) => { - keyvApiTests(test, Keyv, store); - keyvValueTests(test, Keyv, store); - keyvNamepsaceTests(test, Keyv, store); -}; - -exports.keyvApiTests = keyvApiTests; -exports.keyvValueTests = keyvValueTests; -exports.keyvNamepsaceTests = keyvNamepsaceTests; -exports.keyvOfficialTests = keyvOfficialTests; -exports.keyvIteratorTests = keyvIteratorTests; -exports.keyvCompresstionTests = keyvCompresstionTests; - -exports.default = keyvTestSuite; diff --git a/packages/test-suite/src/index.ts b/packages/test-suite/src/index.ts new file mode 100644 index 000000000..89f4b20e0 --- /dev/null +++ b/packages/test-suite/src/index.ts @@ -0,0 +1,24 @@ +import type KeyvModule from 'keyv'; +import type {TestFn} from 'ava'; +import type {KeyvStoreFn} from './types'; + +import keyvApiTests from './api'; +import keyvValueTests from './values'; +import keyvNamepsaceTests from './namespace'; + +const keyvTestSuite = (test: TestFn, Keyv: typeof KeyvModule, store: KeyvStoreFn) => { + keyvApiTests(test, Keyv, store); + keyvValueTests(test, Keyv, store); + keyvNamepsaceTests(test, Keyv, store); +}; + +export { + keyvTestSuite as default, +}; +export {default as keyvOfficialTests} from './official'; +export {default as keyvIteratorTests} from './iterator'; +export {default as keyvCompresstionTests} from './compression'; + +export {default as keyvApiTests} from './api'; +export {default as keyvValueTests} from './values'; +export {default as keyvNamepsaceTests} from './namespace'; diff --git a/packages/test-suite/src/iterator.js b/packages/test-suite/src/iterator.ts similarity index 77% rename from packages/test-suite/src/iterator.js rename to packages/test-suite/src/iterator.ts index 55cbbc81e..721af3c54 100644 --- a/packages/test-suite/src/iterator.js +++ b/packages/test-suite/src/iterator.ts @@ -1,7 +1,10 @@ -'use strict'; -const delay = require('delay'); +import delay from 'delay'; -const keyvIteratorTests = (test, Keyv, store) => { +import type {TestFn} from 'ava'; +import type KeyvModule from 'keyv'; +import type {KeyvStoreFn} from './types'; + +const keyvIteratorTests = (test: TestFn, Keyv: typeof KeyvModule, store: KeyvStoreFn) => { test.beforeEach(async () => { const keyv = new Keyv({store: store()}); await keyv.clear(); @@ -17,7 +20,7 @@ const keyvIteratorTests = (test, Keyv, store) => { const map = new Map( Array.from({length: 5}) .fill(0) - .map((x, i) => [String(i), String(i + 10)]), + .map((_x, i) => [String(i), String(i + 10)]), ); const toResolve = []; for (const [key, value] of map) { @@ -36,13 +39,13 @@ const keyvIteratorTests = (test, Keyv, store) => { test.serial( 'iterator() doesn\'t yield values from other namespaces', async t => { - const KeyvStore = store(); + const keyvStore = store(); - const keyv1 = new Keyv({store: KeyvStore, namespace: 'keyv1'}); + const keyv1 = new Keyv({store: keyvStore, namespace: 'keyv1'}); const map1 = new Map( Array.from({length: 5}) .fill(0) - .map((x, i) => [String(i), String(i + 10)]), + .map((_x, i) => [String(i), String(i + 10)]), ); const toResolve = []; for (const [key, value] of map1) { @@ -51,11 +54,11 @@ const keyvIteratorTests = (test, Keyv, store) => { await Promise.all(toResolve); - const keyv2 = new Keyv({store: KeyvStore, namespace: 'keyv2'}); + const keyv2 = new Keyv({store: keyvStore, namespace: 'keyv2'}); const map2 = new Map( Array.from({length: 5}) .fill(0) - .map((x, i) => [String(i), String(i + 11)]), + .map((_x, i) => [String(i), String(i + 11)]), ); toResolve.length = 0; for (const [key, value] of map2) { @@ -80,7 +83,7 @@ const keyvIteratorTests = (test, Keyv, store) => { const map = new Map( Array.from({length: 5}) .fill(0) - .map((x, i) => [String(i), String(i + 10)]), + .map((_x, i) => [String(i), String(i + 10)]), ); const toResolve = []; for (const [key, value] of map) { @@ -102,4 +105,4 @@ const keyvIteratorTests = (test, Keyv, store) => { ); }; -module.exports = keyvIteratorTests; +export default keyvIteratorTests; diff --git a/packages/test-suite/src/namespace.js b/packages/test-suite/src/namespace.ts similarity index 87% rename from packages/test-suite/src/namespace.js rename to packages/test-suite/src/namespace.ts index 0ab9b613d..c8e9df621 100644 --- a/packages/test-suite/src/namespace.js +++ b/packages/test-suite/src/namespace.ts @@ -1,4 +1,8 @@ -const keyvNamepsaceTests = (test, Keyv, store) => { +import type {TestFn} from 'ava'; +import type KeyvModule from 'keyv'; +import type {KeyvStoreFn} from './types'; + +const keyvNamepsaceTests = (test: TestFn, Keyv: typeof KeyvModule, store: KeyvStoreFn) => { test.beforeEach(async () => { const keyv1 = new Keyv({store: store(), namespace: 'keyv1'}); const keyv2 = new Keyv({store: store(), namespace: 'keyv2'}); @@ -47,4 +51,4 @@ const keyvNamepsaceTests = (test, Keyv, store) => { }); }; -module.exports = keyvNamepsaceTests; +export default keyvNamepsaceTests; diff --git a/packages/test-suite/src/official.js b/packages/test-suite/src/official.js deleted file mode 100644 index c61d6d44c..000000000 --- a/packages/test-suite/src/official.js +++ /dev/null @@ -1,26 +0,0 @@ -const {promisify} = require('util'); - -const keyvOfficialTests = (test, Keyv, goodUri, badUri, options = {}) => { // eslint-disable-line max-params - test.serial('connection string automatically requires storage adapter', async t => { - const keyv = new Keyv(goodUri, options); - await keyv.clear(); - t.is(await keyv.get('foo'), undefined); - await keyv.set('foo', 'bar'); - t.is(await keyv.get('foo'), 'bar'); - await keyv.clear(); - }); - - const withCallback = fn => async t => { - await promisify(fn)(t); - }; - - test.serial('connection errors are emitted', withCallback((t, end) => { - const keyv = new Keyv(badUri, options); - keyv.on('error', () => { - t.pass(); - end(); - }); - })); -}; - -module.exports = keyvOfficialTests; diff --git a/packages/test-suite/src/official.ts b/packages/test-suite/src/official.ts new file mode 100644 index 000000000..c06307ba1 --- /dev/null +++ b/packages/test-suite/src/official.ts @@ -0,0 +1,28 @@ +import {promisify} from 'util'; +import type {ExecutionContext, TestFn} from 'ava'; +import type KeyvModule from 'keyv'; + +const keyvOfficialTests = (test: TestFn, Keyv: typeof KeyvModule, goodUri: string, badUri: string, options = {}) => { // eslint-disable-line max-params + test.serial('connection string automatically requires storage adapter', async t => { + const keyv = new Keyv(goodUri, options); + await keyv.clear(); + t.is(await keyv.get('foo'), undefined); + await keyv.set('foo', 'bar'); + t.is(await keyv.get('foo'), 'bar'); + await keyv.clear(); + }); + + const withCallback = (fn: (t: ExecutionContext, end: () => void) => void) => async (t: ExecutionContext) => { + await promisify(fn)(t); + }; + + test.serial('connection errors are emitted', withCallback((t: ExecutionContext, end) => { + const keyv = new Keyv(badUri, options); + keyv.on('error', () => { + t.pass(); + end(); + }); + })); +}; + +export default keyvOfficialTests; diff --git a/packages/test-suite/src/types.ts b/packages/test-suite/src/types.ts new file mode 100644 index 000000000..60b5974ab --- /dev/null +++ b/packages/test-suite/src/types.ts @@ -0,0 +1,3 @@ +import type {Store} from 'keyv'; + +export type KeyvStoreFn = () => Store; diff --git a/packages/test-suite/src/values.js b/packages/test-suite/src/values.ts similarity index 69% rename from packages/test-suite/src/values.js rename to packages/test-suite/src/values.ts index c5e323b08..b35b99004 100644 --- a/packages/test-suite/src/values.js +++ b/packages/test-suite/src/values.ts @@ -1,7 +1,13 @@ -const JSONbig = require('json-bigint'); -const bigNumber = require('bignumber.js'); +import {Buffer} from 'buffer'; +import type {TestFn} from 'ava'; +import type KeyvModule from 'keyv'; +import type bigNumber from 'bignumber.js'; -const keyvValueTests = (test, Keyv, store) => { +import JSONbig from 'json-bigint'; +import {BigNumber} from 'bignumber.js'; +import type {KeyvStoreFn} from './types'; + +const keyvValueTests = (test: TestFn, Keyv: typeof KeyvModule, store: KeyvStoreFn) => { test.beforeEach(async () => { const keyv = new Keyv({store: store()}); await keyv.clear(); @@ -40,14 +46,14 @@ const keyvValueTests = (test, Keyv, store) => { test.serial('value can be a buffer', async t => { const keyv = new Keyv({store: store()}); - const buf = require('buffer').Buffer.from('bar'); + const buf = Buffer.from('bar'); await keyv.set('foo', buf); t.true(buf.equals(await keyv.get('foo'))); }); test.serial('value can be an object containing a buffer', async t => { const keyv = new Keyv({store: store()}); - const value = {buff: require('buffer').Buffer.from('buzz')}; + const value = {buff: Buffer.from('buzz')}; await keyv.set('foo', value); t.deepEqual(await keyv.get('foo'), value); }); @@ -59,34 +65,44 @@ const keyvValueTests = (test, Keyv, store) => { t.deepEqual(await keyv.get('foo'), value); }); + test.serial('value can be a string', async t => { + const keyv = new Keyv({store: store()}); + await keyv.set('foo', 'bar'); + t.is(await keyv.get('foo'), 'bar'); + }); + test.serial('value can not be symbol', async t => { const keyv = new Keyv({store: store()}); const value = Symbol('value'); - try { - await keyv.set('foo', value); - } catch (error) { - t.is(error.context, 'symbol cannot be serialized'); - } + + const error = await (new Promise(resolve => { + keyv.set('foo', value).catch(error => { + resolve(error.context); + }); + })); + t.is(error, 'symbol cannot be serialized'); }); test.serial('value can be BigInt using other serializer/deserializer', async t => { + // @ts-expect-error TS doesn't know that store().opts is a KeyvStoreOpts store().opts.deserialize = JSONbig.parse; const keyv = new Keyv({store: store(), serialize: JSONbig.stringify, deserialize: JSONbig.parse}); - const value = BigInt('9223372036854775807'); + const value = BigInt('9223372036854775807') as unknown as bigNumber.Value; await keyv.set('foo', value); - t.deepEqual(await keyv.get('foo'), bigNumber(value)); + // eslint-disable-next-line new-cap + t.deepEqual(await keyv.get('foo'), BigNumber(value)); }); test.serial('single quotes value should be saved', async t => { const keyv = new Keyv({store: store()}); - // eslint-disable-next-line quotes - let value = "'"; + + let value = '\''; await keyv.set('key', value); t.is(await keyv.get('key'), value); - // eslint-disable-next-line quotes - value = "''"; + + value = '\'\''; await keyv.set('key1', value); t.is(await keyv.get('key1'), value); value = '"'; @@ -96,10 +112,10 @@ const keyvValueTests = (test, Keyv, store) => { test.serial('single quotes key should be saved', async t => { const keyv = new Keyv({store: store()}); - // eslint-disable-next-line quotes - const value = "'"; - // eslint-disable-next-line quotes - const key = "'"; + + const value = '\''; + + const key = '\''; await keyv.set(key, value); t.is(await keyv.get(key), value); }); @@ -110,4 +126,4 @@ const keyvValueTests = (test, Keyv, store) => { }); }; -module.exports = keyvValueTests; +export default keyvValueTests; diff --git a/packages/test-suite/test/unit.js b/packages/test-suite/test/unit.ts similarity index 52% rename from packages/test-suite/test/unit.js rename to packages/test-suite/test/unit.ts index 8c5fb74b1..710b36e72 100644 --- a/packages/test-suite/test/unit.js +++ b/packages/test-suite/test/unit.ts @@ -1,14 +1,15 @@ -const test = require('ava'); -const Keyv = require('keyv'); -const KeyvBrotli = require('@keyv/compress-brotli'); -const keyvTestSuite = require('../src/index.js').default; -const {keyvOfficialTests, keyvIteratorTests, keyvCompresstionTests} = require('../src/index.js'); +import test from 'ava'; +import Keyv from 'keyv'; +import KeyvBrotli from '@keyv/compress-brotli'; +import keyvTestSuite, {keyvOfficialTests, keyvIteratorTests, keyvCompresstionTests} from '../src/index'; keyvOfficialTests(test, Keyv, 'sqlite://test/testdb.sqlite', 'sqlite://non/existent/database.sqlite'); const storeExtended = () => { class MapExtend extends Map { - constructor(map, options) { + private readonly opts: {timeout: number}; + constructor(map: Map, options: {timeout: number}) { + // @ts-expect-error - super don't accept arguments super(map); this.opts = options; } diff --git a/packages/test-suite/tsconfig.json b/packages/test-suite/tsconfig.json new file mode 100644 index 000000000..0271ba1f8 --- /dev/null +++ b/packages/test-suite/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + + /* Interop Constraints */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + + /* Completeness */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "lib": [ + "es2016" + ] + }, + "include": [ + "src/**/*" + ] +}