synctos
Version:
The Syncmaker. A tool to build comprehensive sync functions for Couchbase Sync Gateway.
737 lines (644 loc) • 38.5 kB
JavaScript
const assert = require('assert');
const fs = require('fs');
const syncFunctionLoader = require('../loading/sync-function-loader');
const testEnvironmentMaker = require('./test-environment-maker');
const validationErrorFormatter = require('./validation-error-formatter');
/**
* Initializes a test fixture for the sync function at the specified file path.
*
* @param {string} filePath The path to the sync function to load
*/
exports.initFromSyncFunction = function(filePath) {
const rawSyncFunction = fs.readFileSync(filePath, 'utf8').toString();
return init(rawSyncFunction, filePath, true);
};
/**
* Initializes a test fixture for the document definitions at the specified file path.
*
* @param {string} filePath The path to the document definitions to load
*/
exports.initFromDocumentDefinitions = function(filePath) {
const rawSyncFunction = syncFunctionLoader.load(filePath);
return init(rawSyncFunction);
};
function init(rawSyncFunction, syncFunctionFile, unescapeBackticks) {
const testEnvironment = testEnvironmentMaker.create(rawSyncFunction, syncFunctionFile, unescapeBackticks);
const fixture = {
/**
* An object that contains functions that are used to format expected validation error messages in specifications. Documentation can be
* found in the "validation-error-formatter" module.
*/
validationErrorFormatter,
/**
* Resets the test fixture's environment to its initial state. Should be called after each test case to ensure the
* environment is in a pristine state at all times.
*/
resetTestEnvironment,
/**
* Attempts to write the specified doc and then verifies that it completed successfully with the expected channels.
*
* @param {Object} doc The document to write. May include property "_deleted=true" to simulate a delete operation.
* @param {Object} oldDoc The document to replace or delete. May be null or undefined or include property "_deleted=true" to simulate a
* create operation.
* @param {(Object|string[])} expectedAuthorization Either an object that specifies the separate channels/roles/users or a list of channels
* that are authorized to perform the operation. If it is an object, the following fields are
* available:
* - expectedChannels: an optional list of channels that are authorized
* - expectedRoles: an optional list of roles that are authorized
* - expectedUsers: an optional list of users that are authorized
* @param {Object[]} [expectedAccessAssignments] An optional list of expected user and role channel assignments. Each entry is an object
* that contains the following fields:
* - expectedType: an optional string that indicates whether this is a "channel" (default) or "role" assignment
* - expectedChannels: an optional list of channels to assign to the users and roles
* - expectedUsers: an optional list of users to which to assign the channels
* - expectedRoles: an optional list of roles to which to assign the channels
* @param {string|number|Date} [expectedExpiry] The date/time or offset at which the document is expected to expire
*/
verifyDocumentAccepted,
/**
* Attempts to create the specified doc and then verifies that it completed successfully with the expected channels.
*
* @param {Object} doc The new document
* @param {(Object|string[])} [expectedAuthorization] Either an optional object that specifies the channels/roles/users or an optional list
* of channels that are authorized to perform the operation. If omitted, then the channel
* "write" is assumed. If it is an object, the following fields are available:
* - expectedChannels: an optional list of channels that are authorized
* - expectedRoles: an optional list of roles that are authorized
* - expectedUsers: an optional list of users that are authorized
* @param {Object[]} [expectedAccessAssignments] An optional list of expected user and role channel assignments. Each entry is an object
* that contains the following fields:
* - expectedType: an optional string that indicates whether this is a "channel" (default) or "role" assignment
* - expectedChannels: an optional list of channels to assign to the users and roles
* - expectedUsers: an optional list of users to which to assign the channels
* - expectedRoles: an optional list of roles to which to assign the channels
* @param {string|number|Date} [expectedExpiry] The date/time or offset at which the document is expected to expire
*/
verifyDocumentCreated,
/**
* Attempts to replace the specified doc and then verifies that it completed successfully with the expected channels.
*
* @param {Object} doc The updated document
* @param {Object} oldDoc The document to replace
* @param {(Object|string[])} [expectedAuthorization] Either an optional object that specifies the channels/roles/users or an optional list
* of channels that are authorized to perform the operation. If omitted, then the channel
* "write" is assumed. If it is an object, the following fields are available:
* - expectedChannels: an optional list of channels that are authorized
* - expectedRoles: an optional list of roles that are authorized
* - expectedUsers: an optional list of users that are authorized
* @param {Object[]} [expectedAccessAssignments] An optional list of expected user and role channel assignments. Each entry is an object
* that contains the following fields:
* - expectedType: an optional string that indicates whether this is a "channel" (default) or "role" assignment
* - expectedChannels: an optional list of channels to assign to the users and roles
* - expectedUsers: an optional list of users to which to assign the channels
* - expectedRoles: an optional list of roles to which to assign the channels
* @param {string|number|Date} [expectedExpiry] The date/time or offset at which the document is expected to expire
*/
verifyDocumentReplaced,
/**
* Attempts to delete the specified doc and then verifies that it completed successfully with the expected channels.
*
* @param {Object} oldDoc The document to delete
* @param {(Object|string[])} [expectedAuthorization] Either an optional object that specifies the channels/roles/users or an optional list
* of channels that are authorized to perform the operation. If omitted, then the channel
* "write" is assumed. If it is an object, the following fields are available:
* - expectedChannels: an optional list of channels that are authorized
* - expectedRoles: an optional list of roles that are authorized
* - expectedUsers: an optional list of users that are authorized
*/
verifyDocumentDeleted,
/**
* Attempts to write the specified doc and then verifies that it failed validation with the expected channels.
*
* @param {Object} doc The document to write. May include property "_deleted=true" to simulate a delete operation.
* @param {Object} oldDoc The document to replace or delete. May be null or undefined or include property "_deleted=true" to simulate a
* create operation.
* @param {string} docType The document's type as specified in the document definition
* @param {string[]} expectedErrorMessages The list of validation error messages that should be generated by the operation. May be a string
* if only one validation error is expected.
* @param {(Object|string[])} expectedAuthorization Either an object that specifies the separate channels/roles/users or a list of channels
* that are authorized to perform the operation. If it is an object, the following fields are
* available:
* - expectedChannels: an optional list of channels that are authorized
* - expectedRoles: an optional list of roles that are authorized
* - expectedUsers: an optional list of users that are authorized
*/
verifyDocumentRejected,
/**
* Attempts to create the specified doc and then verifies that it failed validation with the expected channels.
*
* @param {Object} doc The new document
* @param {string} docType The document's type as specified in the document definition
* @param {string[]} expectedErrorMessages The list of validation error messages that should be generated by the operation. May be a string
* if only one validation error is expected.
* @param {(Object|string[])} [expectedAuthorization] Either an optional object that specifies the channels/roles/users or an optional list
* of channels that are authorized to perform the operation. If omitted, then the channel
* "write" is assumed. If it is an object, the following fields are available:
* - expectedChannels: an optional list of channels that are authorized
* - expectedRoles: an optional list of roles that are authorized
* - expectedUsers: an optional list of users that are authorized
*/
verifyDocumentNotCreated,
/**
* Attempts to replace the specified doc and then verifies that it failed validation with the expected channels.
*
* @param {Object} doc The updated document
* @param {Object} oldDoc The document to replace
* @param {string} docType The document's type as specified in the document definition
* @param {string[]} expectedErrorMessages The list of validation error messages that should be generated by the operation. May be a string
* if only one validation error is expected.
* @param {(Object|string[])} [expectedAuthorization] Either an optional object that specifies the channels/roles/users or an optional list
* of channels that are authorized to perform the operation. If omitted, then the channel
* "write" is assumed. If it is an object, the following fields are available:
* - expectedChannels: an optional list of channels that are authorized
* - expectedRoles: an optional list of roles that are authorized
* - expectedUsers: an optional list of users that are authorized
*/
verifyDocumentNotReplaced,
/**
* Attempts to delete the specified doc and then verifies that it failed validation with the expected channels.
*
* @param {Object} oldDoc The document to delete
* @param {string} docType The document's type as specified in the document definition
* @param {string[]} expectedErrorMessages The list of validation error messages that should be generated by the operation. May be a string
* if only one validation error is expected.
* @param {(Object|string[])} [expectedAuthorization] Either an optional object that specifies the channels/roles/users or an optional list
* of channels that are authorized to perform the operation. If omitted, then the channel
* "write" is assumed. If it is an object, the following fields are available:
* - expectedChannels: an optional list of channels that are authorized
* - expectedRoles: an optional list of roles that are authorized
* - expectedUsers: an optional list of users that are authorized
*/
verifyDocumentNotDeleted,
/**
* Verifies that the given exception result of a document write operation includes the specified validation error messages.
*
* @param {Object} docType The document's type as specified in the document definition
* @param {string[]} expectedErrorMessages The list of validation error messages that should be contained in the exception. May be a string
* if only one validation error is expected.
* @param {Object} exception The exception that was thrown by the sync function. Should include a "forbidden" property of type string.
*/
verifyValidationErrors,
/**
* Verifies that the specified document that was created, replaced or deleted required the specified channels for access.
*
* @param {string[]} expectedChannels The list of all channels that are authorized to perform the operation. May be a string if only one channel
* is expected.
*/
verifyRequireAccess,
/**
* Verifies that the specified document that was created, replaced or deleted required the specified roles for access.
*
* @param {string[]} expectedRoles The list of all roles that are authorized to perform the operation. May be a string if only one role is
* expected.
*/
verifyRequireRole,
/**
* Verifies that the specified document that was created, replaced or deleted required the specified users for access.
*
* @param {string[]} expectedUsers The list of all users that are authorized to perform the operation. May be a string if only one user is
* expected.
*/
verifyRequireUser,
/**
* Verifies that the specified channels were all assigned to a document that was created, replaced or deleted.
*
* @param {string[]} expectedChannels The list of channels that should have been assigned to the document. May be a string if only one
* channel is expected.
*/
verifyChannelAssignment,
/**
* Verifies that the sync function throws an error when authorization is denied to create/replace/delete a document.
*
* @param {Object} doc The document to attempt to write. May include property "_deleted=true" to simulate a delete operation.
* @param {Object} oldDoc The document to replace or delete. May be null or undefined or include property "_deleted=true" to simulate a
* create operation.
* @param {(Object|string[])} expectedAuthorization Either an object that specifies the separate channels/roles/users or a list of channels
* that are authorized to perform the operation. If it is an object, the following fields are
* available:
* - expectedChannels: an optional list of channels that are authorized
* - expectedRoles: an optional list of roles that are authorized
* - expectedUsers: an optional list of users that are authorized
*/
verifyAccessDenied,
/**
* Verifies that the given document's type is unknown/invalid.
*
* @param {Object} doc The document to attempt to write. May include property "_deleted=true" to simulate a delete operation.
* @param {Object} oldDoc The document to replace or delete. May be null or undefined or include property "_deleted=true" to simulate a
* create operation.
*/
verifyUnknownDocumentType,
/**
* The test environment that the test fixture uses to simulate execution of the sync function. Exposes the sync
* function itself via the "syncFunction" property along with several simple-mock stubs for the following Sync
* Gateway API functions:
*
* - requireAccess: https://developer.couchbase.com/documentation/mobile/current/guides/sync-gateway/sync-function-api-guide/index.html#requireaccess-channels
* - requireAdmin: https://github.com/couchbase/sync_gateway/issues/3276
* - requireRole: https://developer.couchbase.com/documentation/mobile/current/guides/sync-gateway/sync-function-api-guide/index.html#requirerole-rolename
* - requireUser: https://developer.couchbase.com/documentation/mobile/current/guides/sync-gateway/sync-function-api-guide/index.html#requireuser-username
* - channel: https://developer.couchbase.com/documentation/mobile/current/guides/sync-gateway/sync-function-api-guide/index.html#channel-name
* - access: https://developer.couchbase.com/documentation/mobile/current/guides/sync-gateway/sync-function-api-guide/index.html#access-username-channelname
* - role: https://developer.couchbase.com/documentation/mobile/current/guides/sync-gateway/sync-function-api-guide/index.html#role-username-rolename
* - expiry: https://developer.couchbase.com/documentation/mobile/current/guides/sync-gateway/sync-function-api-guide/index.html#expiry-value
*/
testEnvironment
};
const defaultWriteChannel = 'write';
function resetTestEnvironment() {
const newEnvironment = testEnvironmentMaker.create(rawSyncFunction, syncFunctionFile, unescapeBackticks);
Object.assign(testEnvironment, newEnvironment);
return testEnvironment;
}
function verifyRequireAccess(expectedChannels) {
assert.ok(
testEnvironment.requireAccess.callCount > 0,
`Document does not specify required channels. Expected: ${expectedChannels}`);
checkAuthorizations(expectedChannels, testEnvironment.requireAccess.calls[0].arg, 'channel');
}
function verifyRequireRole(expectedRoles) {
assert.ok(
testEnvironment.requireRole.callCount > 0,
`Document does not specify required roles. Expected: ${expectedRoles}`);
checkAuthorizations(expectedRoles, testEnvironment.requireRole.calls[0].arg, 'role');
}
function verifyRequireUser(expectedUsers) {
assert.ok(
testEnvironment.requireUser.callCount > 0,
`Document does not specify required users. Expected: ${expectedUsers}`);
checkAuthorizations(expectedUsers, testEnvironment.requireUser.calls[0].arg, 'user');
}
function verifyChannelAssignment(expectedChannels) {
assert.equal(
testEnvironment.channel.callCount,
1,
`Document was not assigned to any channels. Expected: ${expectedChannels}`);
checkAuthorizations(expectedChannels, testEnvironment.channel.calls[0].arg, 'channel');
}
function checkAuthorizations(expectedAuthorizations, actualAuthorizations, authorizationType) {
if (!Array.isArray(expectedAuthorizations)) {
expectedAuthorizations = [ expectedAuthorizations ];
}
// Rather than compare the sizes of the two lists, which leads to an obtuse error message on failure (e.g. "expected 2 to be 3"), ensure
// that neither list of channels/roles/users contains an element that does not exist in the other
expectedAuthorizations.forEach((expectedAuth) => {
if (!actualAuthorizations.includes(expectedAuth)) {
assert.fail(`Expected ${authorizationType} was not encountered: ${expectedAuth}. Actual ${authorizationType}s: ${actualAuthorizations}`);
}
});
actualAuthorizations.forEach((actualAuth) => {
if (!expectedAuthorizations.includes(actualAuth)) {
assert.fail(`Unexpected ${authorizationType} encountered: ${actualAuth}. Expected ${authorizationType}s: ${expectedAuthorizations}`);
}
});
}
function areUnorderedListsEqual(list1, list2) {
return list1.length === list2.length &&
list1.every((element) => list2.includes(element)) &&
list2.every((element) => list1.includes(element));
}
function accessAssignmentCallExists(accessFunction, expectedAssignees, expectedPermissions) {
// Try to find an actual channel/role access assignment call that matches the expected call
return accessFunction.calls.some((accessCall) => {
return areUnorderedListsEqual(accessCall.args[0], expectedAssignees) && areUnorderedListsEqual(accessCall.args[1], expectedPermissions);
});
}
function prefixRoleName(role) {
return `role:${role}`;
}
function verifyChannelAccessAssignment(expectedAssignment) {
const expectedUsersAndRoles = [ ];
if (expectedAssignment.expectedUsers) {
if (Array.isArray(expectedAssignment.expectedUsers)) {
expectedUsersAndRoles.push(...expectedAssignment.expectedUsers);
} else {
expectedUsersAndRoles.push(expectedAssignment.expectedUsers);
}
}
if (expectedAssignment.expectedRoles) {
// The prefix "role:" must be applied to roles when calling the access function, as specified by
// http://developer.couchbase.com/documentation/mobile/current/guides/sync-gateway/sync-function-api-guide/index.html#access-username-channelname
if (Array.isArray(expectedAssignment.expectedRoles)) {
expectedAssignment.expectedRoles.forEach((expectedRole) => {
expectedUsersAndRoles.push(prefixRoleName(expectedRole));
});
} else {
expectedUsersAndRoles.push(prefixRoleName(expectedAssignment.expectedRoles));
}
}
const expectedChannels = [ ];
if (expectedAssignment.expectedChannels) {
if (Array.isArray(expectedAssignment.expectedChannels)) {
expectedChannels.push(...expectedAssignment.expectedChannels);
} else {
expectedChannels.push(expectedAssignment.expectedChannels);
}
}
if (!accessAssignmentCallExists(testEnvironment.access, expectedUsersAndRoles, expectedChannels)) {
assert.fail(`Missing expected call to assign channel access (${JSON.stringify(expectedChannels)}) to users and roles (${JSON.stringify(expectedUsersAndRoles)})`);
}
}
function verifyRoleAccessAssignment(expectedAssignment) {
const expectedUsers = [ ];
if (expectedAssignment.expectedUsers) {
if (Array.isArray(expectedAssignment.expectedUsers)) {
expectedUsers.push(...expectedAssignment.expectedUsers);
} else {
expectedUsers.push(expectedAssignment.expectedUsers);
}
}
const expectedRoles = [ ];
if (expectedAssignment.expectedRoles) {
// The prefix "role:" must be applied to roles when calling the role function, as specified by
// http://developer.couchbase.com/documentation/mobile/current/guides/sync-gateway/sync-function-api-guide/index.html#role-username-rolename
if (Array.isArray(expectedAssignment.expectedRoles)) {
expectedAssignment.expectedRoles.forEach((expectedRole) => {
expectedRoles.push(prefixRoleName(expectedRole));
});
} else {
expectedRoles.push(prefixRoleName(expectedAssignment.expectedRoles));
}
}
if (!accessAssignmentCallExists(testEnvironment.role, expectedUsers, expectedRoles)) {
assert.fail(`Missing expected call to assign role access (${JSON.stringify(expectedRoles)}) to users (${JSON.stringify(expectedUsers)})`);
}
}
function verifyAccessAssignments(expectedAccessAssignments) {
let expectedAccessCalls = 0;
let expectedRoleCalls = 0;
expectedAccessAssignments.forEach((expectedAssignment) => {
if (expectedAssignment.expectedType === 'role') {
verifyRoleAccessAssignment(expectedAssignment);
expectedRoleCalls++;
} else if (expectedAssignment.expectedType === 'channel' || !expectedAssignment.expectedType) {
verifyChannelAccessAssignment(expectedAssignment);
expectedAccessCalls++;
} else {
assert.fail(`Unrecognized expected access assignment type ("${expectedAssignment.expectedType}")`);
}
});
if (testEnvironment.access.callCount !== expectedAccessCalls) {
assert.fail(`Number of calls to assign channel access (${testEnvironment.access.callCount}) does not match expected (${expectedAccessCalls})`);
}
if (testEnvironment.role.callCount !== expectedRoleCalls) {
assert.fail(`Number of calls to assign role access (${testEnvironment.role.callCount}) does not match expected (${expectedRoleCalls})`);
}
}
function verifyDocumentExpiry(rawExpectedExpiry) {
const expectedExpiry =
(rawExpectedExpiry instanceof Date) ? Math.floor(rawExpectedExpiry.getTime() / 1000) : rawExpectedExpiry;
assert.equal(testEnvironment.expiry.callCount, 1, 'Document expiry was not set');
const actualArgs = testEnvironment.expiry.calls[0].args;
assert.equal(actualArgs.length, 1, 'The expiry function received the wrong number of arguments');
assert.deepEqual(
actualArgs,
[ expectedExpiry ],
`Document expiry was not set with the expected value (${expectedExpiry}). Actual value: ${actualArgs[0]}`);
}
function verifyOperationChannelsAssigned(doc, expectedChannels) {
if (testEnvironment.channel.callCount !== 1) {
assert.fail('Document channels were not assigned');
}
const actualChannels = testEnvironment.channel.calls[0].arg;
if (Array.isArray(expectedChannels)) {
expectedChannels.forEach((expectedChannel) => {
assert.ok(
actualChannels.includes(expectedChannel),
`Document was not assigned to expected channel: ${expectedChannel}. Actual: ${actualChannels}`);
});
} else {
assert.ok(
actualChannels.includes(expectedChannels),
`Document was not assigned to expected channel: "${expectedChannels}. Actual: ${actualChannels}`);
}
}
function verifyAuthorization(expectedAuthorization) {
let expectedOperationChannels = [ ];
if (typeof expectedAuthorization === 'string' || Array.isArray(expectedAuthorization)) {
// For backward compatibility, if the authorization parameter is not an object, treat it as the collection of channels that are required
// for authorization
expectedOperationChannels = expectedAuthorization;
verifyRequireAccess(expectedAuthorization);
assert.equal(
testEnvironment.requireRole.callCount,
0,
`Unexpected document roles assigned: ${JSON.stringify(testEnvironment.requireRole.calls)}`);
assert.equal(
testEnvironment.requireUser.callCount,
0,
`Unexpected document users assigned: ${JSON.stringify(testEnvironment.requireUser.calls)}`);
} else {
if (expectedAuthorization.expectedChannels) {
expectedOperationChannels = expectedAuthorization.expectedChannels;
verifyRequireAccess(expectedAuthorization.expectedChannels);
}
if (expectedAuthorization.expectedRoles) {
verifyRequireRole(expectedAuthorization.expectedRoles);
} else {
assert.equal(
testEnvironment.requireRole.callCount,
0,
`Unexpected document roles assigned: ${JSON.stringify(testEnvironment.requireRole.calls)}`);
}
if (expectedAuthorization.expectedUsers) {
verifyRequireUser(expectedAuthorization.expectedUsers);
} else {
assert.equal(
testEnvironment.requireUser.callCount,
0,
`Unexpected document users assigned: ${JSON.stringify(testEnvironment.requireUser.calls)}`);
}
if (!expectedAuthorization.expectedChannels && !expectedAuthorization.expectedRoles && !expectedAuthorization.expectedUsers) {
verifyRequireAccess([ ]);
}
}
return expectedOperationChannels;
}
function verifyDocumentAccepted(doc, oldDoc, expectedAuthorization, expectedAccessAssignments, expectedExpiry) {
testEnvironment.syncFunction(doc, oldDoc || null);
if (expectedAccessAssignments) {
verifyAccessAssignments(expectedAccessAssignments);
}
if (expectedExpiry) {
verifyDocumentExpiry(expectedExpiry);
}
const expectedOperationChannels = verifyAuthorization(expectedAuthorization);
verifyOperationChannelsAssigned(doc, expectedOperationChannels);
}
function verifyDocumentCreated(doc, expectedAuthorization, expectedAccessAssignments, expectedExpiry) {
verifyDocumentAccepted(
doc,
null,
expectedAuthorization || defaultWriteChannel,
expectedAccessAssignments,
expectedExpiry);
}
function verifyDocumentReplaced(doc, oldDoc, expectedAuthorization, expectedAccessAssignments, expectedExpiry) {
verifyDocumentAccepted(
doc,
oldDoc,
expectedAuthorization || defaultWriteChannel,
expectedAccessAssignments,
expectedExpiry);
}
function verifyDocumentDeleted(oldDoc, expectedAuthorization) {
verifyDocumentAccepted({ _id: oldDoc._id, _deleted: true }, oldDoc, expectedAuthorization || defaultWriteChannel);
}
function verifyDocumentRejected(doc, oldDoc, docType, expectedErrorMessages, expectedAuthorization) {
let syncFuncError = null;
try {
testEnvironment.syncFunction(doc, oldDoc || null);
} catch (ex) {
syncFuncError = ex;
}
if (syncFuncError) {
verifyValidationErrors(docType, expectedErrorMessages, syncFuncError);
verifyAuthorization(expectedAuthorization);
assert.equal(
testEnvironment.channel.callCount,
0,
`Document was erroneously assigned to channels: ${JSON.stringify(testEnvironment.channel.calls)}`);
} else {
assert.fail('Document validation succeeded when it was expected to fail');
}
}
function verifyDocumentNotCreated(doc, docType, expectedErrorMessages, expectedAuthorization) {
verifyDocumentRejected(doc, null, docType, expectedErrorMessages, expectedAuthorization || defaultWriteChannel);
}
function verifyDocumentNotReplaced(doc, oldDoc, docType, expectedErrorMessages, expectedAuthorization) {
verifyDocumentRejected(doc, oldDoc, docType, expectedErrorMessages, expectedAuthorization || defaultWriteChannel);
}
function verifyDocumentNotDeleted(oldDoc, docType, expectedErrorMessages, expectedAuthorization) {
verifyDocumentRejected({ _id: oldDoc._id, _deleted: true }, oldDoc, docType, expectedErrorMessages, expectedAuthorization || defaultWriteChannel);
}
function verifyValidationErrors(docType, expectedErrorMessages, exception) {
if (!Array.isArray(expectedErrorMessages)) {
expectedErrorMessages = [ expectedErrorMessages ];
}
// Used to split the leading component (e.g. "Invalid foobar document") from the validation error messages, which are separated by a colon
const validationErrorRegex = /^([^:]+):\s*(.+)$/;
const exceptionMessageMatches = validationErrorRegex.exec(exception.forbidden);
let actualErrorMessages;
if (exceptionMessageMatches) {
assert.equal(exceptionMessageMatches.length, 3, `Unrecognized document validation error message format: "${exception.forbidden}"`);
const invalidDocMessage = exceptionMessageMatches[1].trim();
assert.equal(
invalidDocMessage,
`Invalid ${docType} document`,
`Unrecognized document validation error message format: "${exception.forbidden}"`);
actualErrorMessages = exceptionMessageMatches[2].trim().split(/;\s*/);
} else {
actualErrorMessages = [ exception.forbidden ];
}
// Rather than compare the sizes of the two lists, which leads to an obtuse error message on failure (e.g. "expected 2 to be 3"), verify
// that neither list of validation errors contains an element that does not exist in the other
expectedErrorMessages.forEach((expectedErrorMsg) => {
assert.ok(
actualErrorMessages.includes(expectedErrorMsg),
`Document validation errors do not include expected error message: "${expectedErrorMsg}". Actual error: ${exception.forbidden}`);
});
actualErrorMessages.forEach((errorMessage) => {
if (!expectedErrorMessages.includes(errorMessage)) {
assert.fail(`Unexpected document validation error: "${errorMessage}". Expected error: Invalid ${docType} document: ${expectedErrorMessages.join('; ')}`);
}
});
}
function countAuthorizationTypes(expectedAuthorization) {
let count = 0;
if (expectedAuthorization.expectedChannels) {
count++;
}
if (expectedAuthorization.expectedRoles) {
count++;
}
if (expectedAuthorization.expectedUsers) {
count++;
}
return count;
}
function verifyAccessDenied(doc, oldDoc, expectedAuthorization) {
const channelAccessDeniedError = new Error('Channel access denied!');
const roleAccessDeniedError = new Error('Role access denied!');
const userAccessDeniedError = new Error('User access denied!');
const generalAuthFailedMessage = 'missing channel access';
testEnvironment.requireAccess.throwWith(channelAccessDeniedError);
testEnvironment.requireRole.throwWith(roleAccessDeniedError);
testEnvironment.requireUser.throwWith(userAccessDeniedError);
let syncFuncError = null;
try {
testEnvironment.syncFunction(doc, oldDoc || null);
} catch (ex) {
syncFuncError = ex;
}
if (syncFuncError) {
if (typeof expectedAuthorization === 'string' || Array.isArray(expectedAuthorization)) {
assert.equal(
syncFuncError,
channelAccessDeniedError,
`Document authorization error does not indicate channel access was denied. Actual: ${JSON.stringify(syncFuncError)}`);
} else if (countAuthorizationTypes(expectedAuthorization) === 0) {
verifyRequireAccess([ ]);
} else if (countAuthorizationTypes(expectedAuthorization) > 1) {
assert.equal(
syncFuncError.forbidden,
generalAuthFailedMessage,
`Document authorization error does not indicate that channel, role and user access were all denied. Actual: ${JSON.stringify(syncFuncError)}`);
} else if (expectedAuthorization.expectedChannels) {
assert.equal(
syncFuncError,
channelAccessDeniedError,
`Document authorization error does not indicate channel access was denied. Actual: ${JSON.stringify(syncFuncError)}`);
} else if (expectedAuthorization.expectedRoles) {
assert.equal(
syncFuncError,
roleAccessDeniedError,
`Document authorization error does not indicate role access was denied. Actual: ${JSON.stringify(syncFuncError)}`);
} else {
assert.ok(
syncFuncError,
userAccessDeniedError,
`Document authorization error does not indicate user access was denied. Actual: ${JSON.stringify(syncFuncError)}`);
}
verifyAuthorization(expectedAuthorization);
} else {
assert.fail('Document authorization succeeded when it was expected to fail');
}
}
function verifyUnknownDocumentType(doc, oldDoc) {
let syncFuncError = null;
try {
testEnvironment.syncFunction(doc, oldDoc || null);
} catch (ex) {
syncFuncError = ex;
}
if (syncFuncError) {
assert.equal(
syncFuncError.forbidden,
'Unknown document type',
`Document validation error does not indicate the document type is unrecognized. Actual: ${JSON.stringify(syncFuncError)}`);
assert.equal(
testEnvironment.channel.callCount,
0,
`Document was erroneously assigned to channels: ${JSON.stringify(testEnvironment.channel.calls)}`);
assert.equal(
testEnvironment.requireAccess.callCount,
0,
`Unexpected attempt to specify required channels: ${JSON.stringify(testEnvironment.requireAccess.calls)}`);
assert.equal(
testEnvironment.requireAdmin.callCount,
0,
`Unexpected attempt to specify an admin is required: ${JSON.stringify(testEnvironment.requireAdmin.calls)}`);
assert.equal(
testEnvironment.requireRole.callCount,
0,
`Unexpected attempt to specify required roles: ${JSON.stringify(testEnvironment.requireRole.calls)}`);
assert.equal(
testEnvironment.requireUser.callCount,
0,
`Unexpected attempt to specify required users: ${JSON.stringify(testEnvironment.requireUser.calls)}`);
} else {
assert.fail('Document type was successfully identified when it was expected to be unknown');
}
}
return fixture;
}