Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add metadata cases #11

Merged
merged 1 commit into from
Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/plans.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const { pick, uid, validateKey, assert } = require('./util')
const { pick, uid, validateKey, assert, assertMetadata } = require('./util')

const PLAN_PREFIX = 'plan_'
const PLAN_QUOTA_PERIODS = ['day', 'week', 'month']
Expand All @@ -22,7 +22,7 @@ module.exports = ({ serialize, deserialize, redis } = {}) => {
* @param {number} [options.throttle.rateLimit] - The rate limit of the plan.
* @param {Object} [options.metadata] - Any extra information can be attached here.
*
* @returns {Object|null} The plan object, null if it doesn't exist.
* @returns {Object} The plan object.
*/
const create = async (opts = {}) => {
assert(typeof opts.name === 'string' && opts.name.length > 0, 'The argument `name` is required.')
Expand All @@ -31,6 +31,7 @@ module.exports = ({ serialize, deserialize, redis } = {}) => {
`The argument \`quota.period\` must be ${PLAN_QUOTA_PERIODS.map(period => `\`${period}\``).join(' or ')}.`
)
assert(opts.quota.limit > 0, 'The argument `quota.limit` must be a positive number.')
opts.metadata = assertMetadata(opts.metadata)
const plan = pick(opts, PLAN_FIELDS.concat(PLAN_FIELDS_OBJECT))
plan.id = await uid({ redis, prefix: PLAN_PREFIX, size: 5 })
plan.createdAt = plan.updatedAt = Date.now()
Expand Down Expand Up @@ -88,7 +89,7 @@ module.exports = ({ serialize, deserialize, redis } = {}) => {
const update = async (planId, opts) => {
const currentPlan = await retrieve(planId, { throwError: true })
const quota = Object.assign(currentPlan.quota, opts.quota)
const metadata = Object.assign({}, currentPlan.metadata, opts.metadata)
const metadata = Object.assign({}, currentPlan.metadata, assertMetadata(opts.metadata))
const plan = Object.assign(currentPlan, pick(opts, PLAN_FIELDS), {
quota,
updatedAt: Date.now()
Expand Down
25 changes: 23 additions & 2 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,30 @@ const validateKey =
return id
}

const assertMetadata = metadata => {
if (metadata) {
assert(isPlainObject(metadata), 'The metadata must be a flat object.')
Object.keys(metadata).forEach(key => {
assert(!isPlainObject(metadata[key]), `The metadata field '${key}' can't be an object.`)
if (metadata[key] === undefined) delete metadata[key]
})
return Object.keys(metadata).length ? metadata : undefined
}
}

const isPlainObject = value => {
if (!value || typeof value !== 'object' || value.toString() !== '[object Object]') {
return false
}

const prototype = Object.getPrototypeOf(value)
return prototype === null || prototype === Object.prototype
}

module.exports = {
uid,
pick,
assert,
assertMetadata,
pick,
uid,
validateKey
}
112 changes: 109 additions & 3 deletions test/plans.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,51 @@ test('.create # `quota` is required', async t => {
}
})

test('.create # `metadata` must be a flat object', async t => {
{
const error = await t.throwsAsync(
plans.create({
name: 'free tier',
quota: { period: 'week', limit: 1000 },
metadata: { tier: { type: 'new' } }
})
)
t.is(error.message, "The metadata field 'tier' can't be an object.")
t.is(error.name, 'TypeError')
}
{
const error = await t.throwsAsync(
plans.create({
name: 'free tier',
quota: { period: 'week', limit: 1000 },
metadata: 'foo'
})
)
t.is(error.message, 'The metadata must be a flat object.')
t.is(error.name, 'TypeError')
}
})

test('.create # `metadata` as undefined is omitted', async t => {
{
const plan = await plans.create({
name: 'free tier',
quota: { period: 'week', limit: 1000 },
metadata: { tier: undefined }
})
t.is(plan.metadata, undefined)
}
{
const plan = await plans.create({
name: 'free tier',
quota: { period: 'week', limit: 1000 },
metadata: { tier: 'free', version: undefined }
})

t.deepEqual(Object.keys(plan.metadata), ['tier'])
}
})

test('.create', async t => {
const plan = await plans.create({
name: 'free tier',
Expand All @@ -68,7 +113,11 @@ test('.create', async t => {
t.deepEqual(plan.throttle, { burstLimit: 1000, rateLimit: 10 })
})

test('.retrieve', async t => {
test('.retrieve # a plan not previosuly declared', async t => {
t.is(await plans.retrieve('plan_1'), null)
})

test('.retrieve # a plan previosuly declared', async t => {
const { id } = await plans.create({
name: 'free tier',
quota: { limit: 3000, period: 'day' }
Expand Down Expand Up @@ -108,13 +157,59 @@ test('.update', async t => {
})

test('.update # add metadata', async t => {
{
const { id } = await plans.create({
name: 'free tier',
quota: { limit: 3000, period: 'day' }
})

const plan = await plans.update(id, { metadata: { tier: 'free' } })
t.is(plan.metadata.tier, 'free')
}
{
const { id } = await plans.create({
name: 'free tier',
quota: { limit: 3000, period: 'day' }
})

await plans.update(id, { metadata: { tier: 'free' } })
const plan = await plans.update(id, { metadata: { tier: 'free', version: 2 } })
t.is(plan.metadata.tier, 'free')
t.is(plan.metadata.version, 2)
}
})

test('.update # metadata must be a flat object', async t => {
const { id } = await plans.create({
name: 'free tier',
quota: { limit: 3000, period: 'day' }
})

const plan = await plans.update(id, { metadata: { tier: 'free' } })
t.is(plan.metadata.tier, 'free')
const error = await t.throwsAsync(plans.update(id, { metadata: { tier: { type: 'new' } } }))
t.is(error.message, "The metadata field 'tier' can't be an object.")
t.is(error.name, 'TypeError')
})

test('.update # metadata as undefined is omitted', async t => {
{
const { id } = await plans.create({
name: 'free tier',
quota: { limit: 3000, period: 'day' }
})

const plan = await plans.update(id, { metadata: { tier: undefined } })
t.is(plan.metadata, undefined)
}

{
const { id } = await plans.create({
name: 'free tier',
quota: { limit: 3000, period: 'day' }
})

const plan = await plans.update(id, { metadata: { tier: 'free', version: undefined } })
t.deepEqual(Object.keys(plan.metadata), ['tier'])
}
})

test('.update # prevent to add random data', async t => {
Expand All @@ -126,6 +221,17 @@ test('.update # prevent to add random data', async t => {
t.is(plan.foo, undefined)
})

test('.update # prevent to modify the plan id', async t => {
const { id } = await plans.create({
name: 'free tier',
quota: { limit: 3000, period: 'day' }
})

const plan = await plans.update(id, { id: 'foo' })

t.is(plan.id, id)
})

test('.update # error if plan does not exist', async t => {
{
const error = await t.throwsAsync(plans.update('id', { foo: 'bar' }))
Expand Down
Loading