@ldapjs/change
Version:
API for handling LDAP change objects
423 lines (373 loc) • 10.3 kB
JavaScript
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const Attribute = require('@ldapjs/attribute')
const Change = require('./index')
tap.test('constructor', t => {
t.test('throws for bad operation', async t => {
t.throws(
() => new Change({ operation: 'bad' }),
Error('invalid operation type: bad')
)
})
t.test('throws for bad modification', async t => {
t.throws(
() => new Change({ modification: 'bad' }),
Error('modification must be an Attribute')
)
})
t.test('creates an instance', async t => {
const change = new Change({
modification: new Attribute()
})
t.equal(change.operation, 'add')
t.type(change.modification, Attribute)
t.equal(Object.prototype.toString.call(change), '[object LdapChange]')
})
t.end()
})
tap.test('modification', t => {
t.test('gets', async t => {
const attr = new Attribute()
const change = new Change({ modification: attr })
t.equal(change.modification, attr)
})
t.test('sets', async t => {
const attr1 = new Attribute()
const attr2 = new Attribute()
const change = new Change({ modification: attr1 })
t.equal(change.modification, attr1)
change.modification = attr2
t.equal(change.modification, attr2)
t.not(attr1, attr2)
})
t.test('throws if value is not attribute-like', async t => {
const change = new Change({ modification: new Attribute() })
t.throws(
() => { change.modification = { foo: 'foo' } },
Error('modification must be an Attribute')
)
})
t.test('converts attribute-like to Attribute', async t => {
const change = new Change({
modification: {
type: 'dn=foo,dc=example,dc=com',
values: []
}
})
t.equal(
Object.prototype.toString.call(change.modification),
'[object LdapAttribute]'
)
})
t.end()
})
tap.test('.operation', t => {
const attr = new Attribute()
const change = new Change({ modification: attr })
t.test('throws for unrecognized operation', async t => {
t.throws(
() => { change.operation = 'bad' },
Error('invalid operation type: bad')
)
t.throws(
() => { change.operation = 0xff },
Error('invalid operation type: 0xff')
)
})
t.test('sets and gets', async t => {
change.operation = 0
t.equal(change.operation, 'add')
change.operation = 'add'
t.equal(change.operation, 'add')
change.operation = 1
t.equal(change.operation, 'delete')
change.operation = 'delete'
t.equal(change.operation, 'delete')
change.operation = 2
t.equal(change.operation, 'replace')
change.operation = 'replace'
t.equal(change.operation, 'replace')
change.operation = 'Replace'
t.equal(change.operation, 'replace')
})
t.end()
})
tap.test('.pojo', t => {
t.test('returns a plain object', async t => {
const change = new Change({
modification: new Attribute()
})
const expected = {
operation: 'add',
modification: {
type: '',
values: []
}
}
t.strictSame(change.pojo, expected)
t.strictSame(change.toJSON(), expected)
})
t.end()
})
tap.test('toBer', t => {
t.test('serializes to ber', async t => {
const expected = Buffer.from([
0x30, 0x15, // sequence, 21 bytes
0x0a, 0x01, 0x00, // enumerated value 0
0x30, 0x10, // sequence, 16 bytes
0x04, 0x02, // string, 2 bytes
0x63, 0x6e, // 'cn'
0x31, 0x0a, // sequence of strings, 10 bytes
0x04, 0x03, // string, 3 bytes
0x66, 0x6f, 0x6f, // 'foo'
0x04, 0x03, // string 3 bytes
0x62, 0x61, 0x72
])
const change = new Change({
modification: {
type: 'cn',
values: ['foo', 'bar']
}
})
const ber = change.toBer()
t.equal(expected.compare(ber.buffer), 0)
})
t.end()
})
tap.test('#apply', t => {
t.test('throws if change is not a Change', async t => {
t.throws(
() => Change.apply({}, {}),
Error('change must be an instance of Change')
)
})
t.test('applies to a target with no type', async t => {
const attr = new Attribute({
type: 'cn',
values: ['new']
})
const change = new Change({ modification: attr })
const target = {}
Change.apply(change, target)
t.strictSame(target, {
cn: ['new']
})
})
t.test('applies to a target with a scalar type', async t => {
const attr = new Attribute({
type: 'cn',
values: ['new']
})
const change = new Change({ modification: attr })
const target = { cn: 'old' }
Change.apply(change, target)
t.strictSame(target, {
cn: ['old', 'new']
})
})
t.test('applies to a target with an array type', async t => {
const attr = new Attribute({
type: 'cn',
values: ['new']
})
const change = new Change({ modification: attr })
const target = { cn: ['old'] }
Change.apply(change, target)
t.strictSame(target, {
cn: ['old', 'new']
})
})
t.test('add operation adds only new values', async t => {
const attr = new Attribute({
type: 'cn',
values: ['new', 'foo']
})
const change = new Change({ modification: attr })
const target = { cn: ['old', 'new'] }
Change.apply(change, target)
t.strictSame(target, {
cn: ['old', 'new', 'foo']
})
})
t.test('delete operation removes property', async t => {
const attr = new Attribute({
type: 'cn',
values: ['new']
})
const change = new Change({
operation: 'delete',
modification: attr
})
const target = { cn: ['new'] }
Change.apply(change, target)
t.strictSame(target, {})
})
t.test('delete operation removes values', async t => {
const attr = new Attribute({
type: 'cn',
values: ['remove_me']
})
const change = new Change({
operation: 'delete',
modification: attr
})
const target = { cn: ['remove_me', 'keep_me'] }
Change.apply(change, target)
t.strictSame(target, {
cn: ['keep_me']
})
})
t.test('replace removes empty set', async t => {
const attr = new Attribute({
type: 'cn',
values: []
})
const change = new Change({
operation: 'replace',
modification: attr
})
const target = { cn: ['old'] }
Change.apply(change, target)
t.strictSame(target, {})
})
t.test('replace removes values', async t => {
const attr = new Attribute({
type: 'cn',
values: ['new_set']
})
const change = new Change({
operation: 'replace',
modification: attr
})
const target = { cn: ['old_set'] }
Change.apply(change, target)
t.strictSame(target, {
cn: ['new_set']
})
})
t.test('scalar option works for new single values', async t => {
const attr = new Attribute({
type: 'cn',
values: ['new']
})
const change = new Change({ modification: attr })
const target = {}
Change.apply(change, target, true)
t.strictSame(target, {
cn: 'new'
})
})
t.test('scalar option is ignored for multiple values', async t => {
const attr = new Attribute({
type: 'cn',
values: ['new']
})
const change = new Change({ modification: attr })
const target = {
cn: ['old']
}
Change.apply(change, target, true)
t.strictSame(target, {
cn: ['old', 'new']
})
})
t.end()
})
tap.test('#isChange', t => {
t.test('true for instance', async t => {
const change = new Change({ modification: new Attribute() })
t.equal(Change.isChange(change), true)
})
t.test('false for non-object', async t => {
t.equal(Change.isChange([]), false)
})
t.test('true for shape match', async t => {
const change = {
operation: 'add',
modification: {
type: '',
values: []
}
}
t.equal(Change.isChange(change), true)
change.operation = 0
change.modification = new Attribute()
t.equal(Change.isChange(change), true)
})
t.test('false for shape mis-match', async t => {
const change = {
operation: 'add',
mod: {
type: '',
values: []
}
}
t.equal(Change.isChange(change), false)
})
t.end()
})
tap.test('#compare', t => {
t.test('throws if params are not changes', async t => {
const change = new Change({ modification: new Attribute() })
const expected = Error('can only compare Change instances')
t.throws(
() => Change.compare({}, change),
expected
)
t.throws(
() => Change.compare(change, {}),
expected
)
})
t.test('orders add first', async t => {
const change1 = new Change({ modification: new Attribute() })
const change2 = new Change({
operation: 'delete',
modification: new Attribute()
})
t.equal(Change.compare(change1, change2), -1)
change2.operation = 'replace'
t.equal(Change.compare(change1, change2), -1)
})
t.test('orders delete above add', async t => {
const change1 = new Change({ modification: new Attribute() })
const change2 = new Change({
operation: 'delete',
modification: new Attribute()
})
t.equal(Change.compare(change2, change1), 1)
})
t.test('orders by attribute for same operation', async t => {
const change1 = new Change({ modification: new Attribute() })
const change2 = new Change({ modification: new Attribute() })
t.equal(Change.compare(change1, change2), 0)
})
t.end()
})
tap.test('#fromBer', t => {
t.test('creates instance', async t => {
const bytes = [
0x30, 0x15, // sequence, 21 bytes
0x0a, 0x01, 0x00, // enumerated value 0
0x30, 0x10, // sequence, 16 bytes
0x04, 0x02, // string, 2 bytes
0x63, 0x6e, // 'cn'
0x31, 0x0a, // sequence of strings, 10 bytes
0x04, 0x03, // string, 3 bytes
0x66, 0x6f, 0x6f, // 'foo'
0x04, 0x03, // string 3 bytes
0x62, 0x61, 0x72
]
const reader = new BerReader(Buffer.from(bytes))
const change = Change.fromBer(reader)
t.strictSame(change.pojo, {
operation: 'add',
modification: {
type: 'cn',
values: ['foo', 'bar']
}
})
})
t.end()
})