diff --git a/src/index.js b/src/index.js index 1105592..d69b27c 100644 --- a/src/index.js +++ b/src/index.js @@ -5,7 +5,6 @@ const createKeys = require('./keys') const createPlans = require('./plans') module.exports = ({ serialize = JSONB.stringify, deserialize = JSONB.parse, redis = new Map() } = {}) => { - if (!redis) throw TypeError('The argument `store` is required.') const plans = createPlans({ serialize, deserialize, redis }) const keys = createKeys({ serialize, deserialize, redis, plans }) return { keys, plans } diff --git a/src/keys.js b/src/keys.js index 4f0a9b9..0a590bd 100644 --- a/src/keys.js +++ b/src/keys.js @@ -1,6 +1,6 @@ 'use strict' -const { pick, uid, validateKey, assert } = require('./util') +const { pick, uid, validateKey, assert, assertMetadata } = require('./util') const KEY_PREFIX = 'key_' const KEY_FIELDS = ['name', 'description', 'enabled', 'value', 'plan'] @@ -22,6 +22,7 @@ module.exports = ({ serialize, deserialize, plans, redis } = {}) => { */ const create = async (opts = {}) => { assert(typeof opts.name === 'string' && opts.name.length > 0, 'The argument `name` is required.') + opts.metadata = assertMetadata(opts.metadata) const key = pick(opts, KEY_FIELDS.concat(KEY_FIELDS_OBJECT)) key.id = await uid({ redis, prefix: KEY_PREFIX, size: 5 }) key.createdAt = key.updatedAt = Date.now() @@ -66,7 +67,8 @@ module.exports = ({ serialize, deserialize, plans, redis } = {}) => { assert(plan === null, `The key \`${keyId}\` is associated with the plan \`${getKey.plan}\``) } const isDeleted = (await redis.del(getKey(keyId, { verify: true }))) === 1 - return assert(isDeleted, `The key \`${keyId}\` does not exist.`) || isDeleted + assert(isDeleted, `The key \`${keyId}\` does not exist.`) + return isDeleted } /** @@ -84,7 +86,7 @@ module.exports = ({ serialize, deserialize, plans, redis } = {}) => { */ const update = async (keyId, opts) => { const currentKey = await retrieve(keyId, { throwError: true }) - const metadata = Object.assign({}, currentKey.metadata, opts.metadata) + const metadata = Object.assign({}, currentKey.metadata, assertMetadata(opts.metadata)) const key = Object.assign(currentKey, pick(opts, KEY_FIELDS), { updatedAt: Date.now() }) diff --git a/src/plans.js b/src/plans.js index b329db2..dfb8a7e 100644 --- a/src/plans.js +++ b/src/plans.js @@ -66,7 +66,8 @@ module.exports = ({ serialize, deserialize, redis } = {}) => { */ const del = async planId => { const isDeleted = (await redis.del(getKey(planId, { validate: true }))) === 1 - return assert(isDeleted, `The plan \`${planId}\` does not exist.`) || isDeleted + assert(isDeleted, `The plan \`${planId}\` does not exist.`) + return isDeleted } /** diff --git a/test/keys.js b/test/keys.js index fcba50b..abd33f9 100644 --- a/test/keys.js +++ b/test/keys.js @@ -13,7 +13,6 @@ const { keys, plans } = openkey({ redis: new Redis() }) test.beforeEach(async () => { const keys = await redis.keys(`${KEY_PREFIX}*`) - if (keys.length > 0) await redis.del(keys) }) @@ -24,6 +23,19 @@ test('.create # `name` is required', async t => { t.is(error.name, 'TypeError') }) +test('.create # `metadata` must be a flat object', async t => { + const error = await t.throwsAsync(keys.create({ name: 'hello@microlink.io', metadata: { tier: { type: 'new' } } })) + + t.is(error.message, "The metadata field 'tier' can't be an object.") + t.is(error.name, 'TypeError') +}) + +test('.create # `metadata` as undefined is omitted', async t => { + const key = keys.create({ name: 'hello@microlink.io', metadata: { cc: undefined } }) + + t.is(key.metadata, undefined) +}) + test('.create # error if plan is invalid', async t => { const error = await t.throwsAsync(keys.create({ name: 'hello@microlink.io', plan: 123 })) @@ -48,7 +60,22 @@ test('.create', async t => { t.true(key.enabled) }) -test('.retrieve', async t => { +test('.create # associate a plan', async t => { + const plan = await plans.create({ + name: 'free tier', + quota: { limit: 3000, period: 'day' } + }) + + const key = await keys.create({ name: 'hello@microlink.io', plan: plan.id }) + + t.true(key.id.startsWith('key_')) + t.truthy(key.createdAt) + t.is(key.createdAt, key.updatedAt) + t.is(key.value.length, 16) + t.true(key.enabled) +}) + +test('.retrieve # a key previously created', async t => { const { id, value } = await keys.create({ name: 'hello@microlink.io' }) const { createdAt, updatedAt, ...key } = await keys.retrieve(id) @@ -61,6 +88,10 @@ test('.retrieve', async t => { }) }) +test('.retrieve # a key not previously created', async t => { + t.is(await keys.retrieve('key_1'), null) +}) + test('.update', async t => { const { id, value, createdAt } = await keys.create({ name: 'hello@microlink.io' @@ -86,47 +117,30 @@ test('.update', async t => { t.deepEqual(await keys.retrieve(id), { ...key, updatedAt }) }) -test('.update # error if plan is invalid', async t => { - const { id } = await keys.create({ name: 'hello@microlink.io' }) - - const error = await t.throwsAsync( - keys.update(id, { - description: 'new description', - enabled: false, - plan: 123 - }) - ) +test('.update # error if key is invalid', async t => { + const error = await t.throwsAsync(keys.update('id', { foo: 'bar' })) + t.is(error.message, 'The id `id` must to start with `key_`.') + t.is(error.name, 'TypeError') +}) - t.is(error.message, 'The id `123` must to start with `plan_`.') +test('.update # error if key does not exist', async t => { + const error = await t.throwsAsync(keys.update('key_id')) + t.is(error.message, 'The key `key_id` does not exist.') t.is(error.name, 'TypeError') }) -test('.update # error if plan does not exist', async t => { +test('.update # error if plan is invalid', async t => { const { id } = await keys.create({ name: 'hello@microlink.io' }) - - const error = await t.throwsAsync( - keys.update(id, { - description: 'new description', - enabled: false, - plan: 'plan_123' - }) - ) - - t.is(error.message, 'The plan `plan_123` does not exist.') + const error = await t.throwsAsync(keys.update(id, { plan: 'id' })) + t.is(error.message, 'The id `id` must to start with `plan_`.') t.is(error.name, 'TypeError') }) -test('.update # error if key does not exist', async t => { - { - const error = await t.throwsAsync(keys.update('id', { foo: 'bar' })) - t.is(error.message, 'The id `id` must to start with `key_`.') - t.is(error.name, 'TypeError') - } - { - const error = await t.throwsAsync(keys.update('key_id', { foo: 'bar' })) - t.is(error.message, 'The key `key_id` does not exist.') - t.is(error.name, 'TypeError') - } +test('.update # error if plan does not exist', async t => { + const { id } = await keys.create({ name: 'hello@microlink.io' }) + const error = await t.throwsAsync(keys.update(id, { plan: 'plan_id' })) + t.is(error.message, 'The plan `plan_id` does not exist.') + t.is(error.name, 'TypeError') }) test('.update # add a plan', async t => { @@ -142,10 +156,39 @@ test('.update # add a plan', async t => { }) test('.update # add metadata', async t => { + { + const { id } = await keys.create({ name: 'hello@microlink.io' }) + const key = await keys.update(id, { metadata: { cc: 'hello@microlink.io' } }) + t.is(key.metadata.cc, 'hello@microlink.io') + } + { + const { id } = await keys.create({ name: 'hello@microlink.io' }) + await keys.update(id, { metadata: { cc: 'hello@microlink.io' } }) + const key = await keys.update(id, { metadata: { cc: 'hello@microlink.io', version: 2 } }) + + t.is(key.metadata.cc, 'hello@microlink.io') + t.is(key.metadata.version, 2) + } +}) + +test('.update # metadata must be a flat object', async t => { const { id } = await keys.create({ name: 'hello@microlink.io' }) - const key = await keys.update(id, { metadata: { cc: 'hello@microlink.io' } }) + const error = await t.throwsAsync(keys.update(id, { metadata: { email: { cc: 'hello@microlink.io' } } })) + t.is(error.message, "The metadata field 'email' can't be an object.") + t.is(error.name, 'TypeError') +}) - t.is(key.metadata.cc, 'hello@microlink.io') +test('.update # metadata as undefined is omitted', async t => { + { + const { id } = await keys.create({ name: 'hello@microlink.io' }) + const key = await keys.update(id, { metadata: { email: undefined } }) + t.is(key.metadata, undefined) + } + { + const { id } = await keys.create({ name: 'hello@microlink.io' }) + const key = await keys.update(id, { metadata: { cc: 'hello@microlink.io', bcc: undefined } }) + t.deepEqual(Object.keys(key.metadata), ['cc']) + } }) test('.update # prevent to add random data', async t => { @@ -155,6 +198,13 @@ test('.update # prevent to add random data', async t => { t.is(key.foo, undefined) }) +test('.update # prevent to modify the key id', async t => { + const { id } = await keys.create({ name: 'hello@microlink.io' }) + const key = await keys.update(id, { id: 'foo' }) + + t.is(key.id, id) +}) + test.serial('.list', async t => { const { id: id1 } = await keys.create({ name: 'hello@microlink.io' }) const { id: id2 } = await keys.create({ name: 'hello@microlink.io' }) diff --git a/test/plans.js b/test/plans.js index f84b343..19a0008 100644 --- a/test/plans.js +++ b/test/plans.js @@ -113,11 +113,11 @@ test('.create', async t => { t.deepEqual(plan.throttle, { burstLimit: 1000, rateLimit: 10 }) }) -test('.retrieve # a plan not previosuly declared', async t => { +test('.retrieve # a plan not previously created', async t => { t.is(await plans.retrieve('plan_1'), null) }) -test('.retrieve # a plan previosuly declared', async t => { +test('.retrieve # a plan previously created', async t => { const { id } = await plans.create({ name: 'free tier', quota: { limit: 3000, period: 'day' } @@ -232,17 +232,16 @@ test('.update # prevent to modify the plan id', async t => { t.is(plan.id, id) }) +test('.update # error if plan is invalid', async t => { + const error = await t.throwsAsync(plans.update('id', { foo: 'bar' })) + t.is(error.message, 'The id `id` must to start with `plan_`.') + t.is(error.name, 'TypeError') +}) + test('.update # error if plan does not exist', async t => { - { - const error = await t.throwsAsync(plans.update('id', { foo: 'bar' })) - t.is(error.message, 'The id `id` must to start with `plan_`.') - t.is(error.name, 'TypeError') - } - { - const error = await t.throwsAsync(plans.update('plan_id', { foo: 'bar' })) - t.is(error.message, 'The plan `plan_id` does not exist.') - t.is(error.name, 'TypeError') - } + const error = await t.throwsAsync(plans.update('plan_id', { foo: 'bar' })) + t.is(error.message, 'The plan `plan_id` does not exist.') + t.is(error.name, 'TypeError') }) test.serial('.list', async t => { @@ -277,6 +276,7 @@ test('.del', async t => { test('.del # error if plan does not exist', async t => { const error = await t.throwsAsync(plans.del('plan_id')) + t.is(error.message, 'The plan `plan_id` does not exist.') t.is(error.name, 'TypeError') })