Chore: break LDAP manager into smaller pieces to improve unit tests (#26994)
* break LDAP manager into smaller pieces to improve unit tests * Fix * Apply suggestions from code review Co-authored-by: Diego Sampaio <chinello@gmail.com> * Apply suggestions from code review Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> * Added few tests Co-authored-by: Diego Sampaio <chinello@gmail.com> Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>pull/26985/head^2
parent
3715e0d109
commit
4afcbf64a2
@ -0,0 +1,209 @@ |
||||
import type { IImportUser, ILDAPEntry } from '@rocket.chat/core-typings'; |
||||
import { expect, spy } from 'chai'; |
||||
|
||||
import { copyCustomFieldsLDAP } from './copyCustomFieldsLDAP'; |
||||
import type { Logger } from '../../../../app/logger/server'; |
||||
|
||||
describe('LDAP copyCustomFieldsLDAP', () => { |
||||
it('should copy custom fields from ldapUser to rcUser', () => { |
||||
const ldapUser = { |
||||
mail: 'test@test.com', |
||||
givenName: 'Test', |
||||
} as unknown as ILDAPEntry; |
||||
|
||||
const userData = { |
||||
name: 'Test', |
||||
username: 'test', |
||||
} as unknown as IImportUser; |
||||
|
||||
copyCustomFieldsLDAP( |
||||
{ |
||||
ldapUser, |
||||
userData, |
||||
syncCustomFields: true, |
||||
customFieldsSettings: JSON.stringify({ |
||||
mappedGivenName: { type: 'text', required: false }, |
||||
}), |
||||
customFieldsMap: JSON.stringify({ |
||||
givenName: 'mappedGivenName', |
||||
}), |
||||
}, |
||||
{ |
||||
debug: () => undefined, |
||||
error: () => undefined, |
||||
} as unknown as Logger, |
||||
); |
||||
|
||||
expect(userData).to.have.property('customFields'); |
||||
expect(userData.customFields).to.be.eql({ mappedGivenName: 'Test' }); |
||||
}); |
||||
|
||||
it('should copy custom fields from ldapUser to rcUser already having other custom fields', () => { |
||||
const ldapUser = { |
||||
mail: 'test@test.com', |
||||
givenName: 'Test', |
||||
} as unknown as ILDAPEntry; |
||||
|
||||
const userData = { |
||||
name: 'Test', |
||||
username: 'test', |
||||
customFields: { |
||||
custom: 'Test', |
||||
}, |
||||
} as unknown as IImportUser; |
||||
|
||||
copyCustomFieldsLDAP( |
||||
{ |
||||
ldapUser, |
||||
userData, |
||||
syncCustomFields: true, |
||||
customFieldsSettings: JSON.stringify({ |
||||
mappedGivenName: { type: 'text', required: false }, |
||||
}), |
||||
customFieldsMap: JSON.stringify({ |
||||
givenName: 'mappedGivenName', |
||||
}), |
||||
}, |
||||
{ |
||||
debug: () => undefined, |
||||
error: () => undefined, |
||||
} as unknown as Logger, |
||||
); |
||||
|
||||
expect(userData).to.have.property('customFields'); |
||||
expect(userData.customFields).to.be.eql({ custom: 'Test', mappedGivenName: 'Test' }); |
||||
}); |
||||
|
||||
it('should not copy custom fields from ldapUser to rcUser if syncCustomFields is false', () => { |
||||
const ldapUser = { |
||||
mail: 'test@test.com', |
||||
givenName: 'Test', |
||||
} as unknown as ILDAPEntry; |
||||
|
||||
const userData = { |
||||
name: 'Test', |
||||
username: 'test', |
||||
} as unknown as IImportUser; |
||||
|
||||
copyCustomFieldsLDAP( |
||||
{ |
||||
ldapUser, |
||||
userData, |
||||
syncCustomFields: false, |
||||
customFieldsSettings: JSON.stringify({ |
||||
mappedGivenName: { type: 'text', required: false }, |
||||
}), |
||||
customFieldsMap: JSON.stringify({ |
||||
givenName: 'mappedGivenName', |
||||
}), |
||||
}, |
||||
{ |
||||
debug: () => undefined, |
||||
error: () => undefined, |
||||
} as unknown as Logger, |
||||
); |
||||
|
||||
expect(userData).to.not.have.property('customFields'); |
||||
}); |
||||
|
||||
it('should call logger.error if customFieldsSettings is not a valid JSON', () => { |
||||
const debug = spy(); |
||||
const error = spy(); |
||||
const ldapUser = { |
||||
mail: 'test@test.com', |
||||
givenName: 'Test', |
||||
} as unknown as ILDAPEntry; |
||||
|
||||
const userData = { |
||||
name: 'Test', |
||||
username: 'test', |
||||
} as unknown as IImportUser; |
||||
|
||||
copyCustomFieldsLDAP( |
||||
{ |
||||
ldapUser, |
||||
userData, |
||||
syncCustomFields: true, |
||||
customFieldsSettings: `${JSON.stringify({ |
||||
mappedGivenName: { type: 'text', required: false }, |
||||
})}}`,
|
||||
customFieldsMap: JSON.stringify({ |
||||
givenName: 'mappedGivenName', |
||||
}), |
||||
}, |
||||
{ |
||||
debug, |
||||
error, |
||||
} as unknown as Logger, |
||||
); |
||||
expect(error).to.have.been.called.exactly(1); |
||||
}); |
||||
it('should call logger.error if customFieldsMap is not a valid JSON', () => { |
||||
const debug = spy(); |
||||
const error = spy(); |
||||
const ldapUser = { |
||||
mail: 'test@test.com', |
||||
givenName: 'Test', |
||||
} as unknown as ILDAPEntry; |
||||
|
||||
const userData = { |
||||
name: 'Test', |
||||
username: 'test', |
||||
} as unknown as IImportUser; |
||||
|
||||
copyCustomFieldsLDAP( |
||||
{ |
||||
ldapUser, |
||||
userData, |
||||
syncCustomFields: true, |
||||
customFieldsSettings: JSON.stringify({ |
||||
mappedGivenName: { type: 'text', required: false }, |
||||
}), |
||||
customFieldsMap: `${JSON.stringify({ |
||||
givenName: 'mappedGivenName', |
||||
})}}`,
|
||||
}, |
||||
{ |
||||
debug, |
||||
error, |
||||
} as unknown as Logger, |
||||
); |
||||
expect(error).to.have.been.called.exactly(1); |
||||
}); |
||||
|
||||
it('should call logger.debug if some custom fields are not mapped but still mapping the other fields', () => { |
||||
const debug = spy(); |
||||
const error = spy(); |
||||
const ldapUser = { |
||||
mail: 'test@test.com', |
||||
givenName: 'Test', |
||||
} as unknown as ILDAPEntry; |
||||
|
||||
const userData = { |
||||
name: 'Test', |
||||
username: 'test', |
||||
} as unknown as IImportUser; |
||||
|
||||
copyCustomFieldsLDAP( |
||||
{ |
||||
ldapUser, |
||||
userData, |
||||
syncCustomFields: true, |
||||
customFieldsSettings: JSON.stringify({ |
||||
mappedGivenName: { type: 'text', required: false }, |
||||
}), |
||||
customFieldsMap: JSON.stringify({ |
||||
givenName: 'mappedGivenName', |
||||
test: 'test', |
||||
}), |
||||
}, |
||||
{ |
||||
debug, |
||||
error, |
||||
} as unknown as Logger, |
||||
); |
||||
expect(debug).to.have.been.called.exactly(1); |
||||
expect(userData).to.have.property('customFields'); |
||||
expect(userData.customFields).to.be.eql({ mappedGivenName: 'Test' }); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,71 @@ |
||||
import type { IImportUser, ILDAPEntry } from '@rocket.chat/core-typings'; |
||||
|
||||
import type { Logger } from '../../../../app/logger/server'; |
||||
import { templateVarHandler } from '../../../../app/utils/lib/templateVarHandler'; |
||||
import { getNestedProp } from './getNestedProp'; |
||||
import { replacesNestedValues } from './replacesNestedValues'; |
||||
|
||||
export const copyCustomFieldsLDAP = ( |
||||
{ |
||||
ldapUser, |
||||
userData, |
||||
customFieldsSettings, |
||||
customFieldsMap, |
||||
syncCustomFields, |
||||
}: { |
||||
ldapUser: ILDAPEntry; |
||||
userData: IImportUser; |
||||
syncCustomFields: boolean; |
||||
customFieldsSettings: string; |
||||
customFieldsMap: string; |
||||
}, |
||||
logger: Logger, |
||||
): void => { |
||||
if (!syncCustomFields) { |
||||
return; |
||||
} |
||||
|
||||
if (!customFieldsMap || !customFieldsSettings) { |
||||
if (customFieldsMap) { |
||||
logger.debug('Skipping LDAP custom fields because there are no custom map fields configured.'); |
||||
return; |
||||
} |
||||
logger.debug('Skipping LDAP custom fields because there are no custom fields configured.'); |
||||
return; |
||||
} |
||||
|
||||
const map: Record<string, string> = (() => { |
||||
try { |
||||
return JSON.parse(customFieldsMap); |
||||
} catch (err) { |
||||
logger.error({ msg: 'Error parsing LDAP custom fields map.', err }); |
||||
} |
||||
})(); |
||||
|
||||
if (!map) { |
||||
return; |
||||
} |
||||
|
||||
let customFields: Record<string, any>; |
||||
try { |
||||
customFields = JSON.parse(customFieldsSettings) as Record<string, any>; |
||||
} catch (err) { |
||||
logger.error({ msg: 'Failed to parse Custom Fields', err }); |
||||
return; |
||||
} |
||||
|
||||
Object.entries(map).forEach(([ldapField, userField]) => { |
||||
if (!getNestedProp(customFields, userField)) { |
||||
logger.debug(`User attribute does not exist: ${userField}`); |
||||
return; |
||||
} |
||||
|
||||
const value = templateVarHandler(ldapField, ldapUser); |
||||
|
||||
if (!value) { |
||||
return; |
||||
} |
||||
|
||||
userData.customFields = replacesNestedValues({ ...userData.customFields }, userField, value); |
||||
}); |
||||
}; |
||||
@ -0,0 +1,23 @@ |
||||
import { expect } from 'chai'; |
||||
|
||||
import { getNestedProp } from './getNestedProp'; |
||||
|
||||
describe('LDAP getNestedProp', () => { |
||||
it('should find shallow values', () => { |
||||
const customFields = { |
||||
foo: 'bar', |
||||
}; |
||||
|
||||
expect(getNestedProp(customFields, 'foo')).to.equal('bar'); |
||||
}); |
||||
|
||||
it('should find deep values', () => { |
||||
const customFields = { |
||||
foo: { |
||||
bar: 'baz', |
||||
}, |
||||
}; |
||||
|
||||
expect(getNestedProp(customFields, 'foo.bar')).to.equal('baz'); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,7 @@ |
||||
export const getNestedProp = (customFields: Record<string, any>, property: string): unknown => { |
||||
try { |
||||
return property.split('.').reduce((acc, el) => acc[el], customFields); |
||||
} catch { |
||||
// ignore errors
|
||||
} |
||||
}; |
||||
@ -0,0 +1,87 @@ |
||||
import { expect } from 'chai'; |
||||
|
||||
import { replacesNestedValues } from './replacesNestedValues'; |
||||
|
||||
describe('LDAP replacesNestedValues', () => { |
||||
it('should replace shallow values', () => { |
||||
const result = replacesNestedValues( |
||||
{ |
||||
a: 1, |
||||
}, |
||||
'a', |
||||
2, |
||||
); |
||||
expect(result).to.eql({ |
||||
a: 2, |
||||
}); |
||||
}); |
||||
|
||||
it('should replace undefined values', () => { |
||||
const result = replacesNestedValues({}, 'a', 2); |
||||
expect(result).to.eql({ |
||||
a: 2, |
||||
}); |
||||
}); |
||||
|
||||
it('should replace nested values', () => { |
||||
const result = replacesNestedValues( |
||||
{ |
||||
a: { |
||||
b: 1, |
||||
}, |
||||
}, |
||||
'a.b', |
||||
2, |
||||
); |
||||
expect(result).to.eql({ |
||||
a: { |
||||
b: 2, |
||||
}, |
||||
}); |
||||
}); |
||||
it('should replace undefined nested values', () => { |
||||
const result = replacesNestedValues( |
||||
{ |
||||
a: {}, |
||||
}, |
||||
'a.b', |
||||
2, |
||||
); |
||||
expect(result).to.eql({ |
||||
a: { |
||||
b: 2, |
||||
}, |
||||
}); |
||||
}); |
||||
|
||||
it('should fail if the value being replaced is not an object', () => { |
||||
expect(() => |
||||
replacesNestedValues( |
||||
{ |
||||
a: [], |
||||
}, |
||||
'a.b', |
||||
2, |
||||
), |
||||
).to.throw(); |
||||
expect(() => |
||||
replacesNestedValues( |
||||
{ |
||||
a: 1, |
||||
}, |
||||
'a.b', |
||||
2, |
||||
), |
||||
).to.throw(); |
||||
|
||||
expect(() => |
||||
replacesNestedValues( |
||||
{ |
||||
a: { b: [] }, |
||||
}, |
||||
'a.b', |
||||
2, |
||||
), |
||||
).to.throw(); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,26 @@ |
||||
export const replacesNestedValues = (obj: Record<string, unknown>, key: string, value: unknown): Record<string, unknown> => { |
||||
const keys = key.split('.'); |
||||
const lastKey = keys.shift(); |
||||
|
||||
if (!lastKey) { |
||||
throw new Error(`Failed to assign custom field: ${key}`); |
||||
} |
||||
|
||||
if (keys.length && obj[lastKey] !== undefined && (typeof obj[lastKey] !== 'object' || Array.isArray(obj[lastKey]))) { |
||||
throw new Error(`Failed to assign custom field: ${key}`); |
||||
} |
||||
|
||||
if (keys.length === 0 && typeof obj[lastKey] === 'object') { |
||||
throw new Error(`Failed to assign custom field: ${key}`); |
||||
} |
||||
|
||||
return { |
||||
...obj, |
||||
...(keys.length === 0 && { |
||||
[lastKey]: value, |
||||
}), |
||||
...(keys.length > 0 && { |
||||
[lastKey]: replacesNestedValues(obj[lastKey] as Record<string, unknown>, keys.join('.'), value), |
||||
}), |
||||
}; |
||||
}; |
||||
Loading…
Reference in new issue