text:
var _ = require('underscore');
var assert = require('assert');
var fs = require('fs');

var ActivityTestsUtil = require('oae-activity/lib/test/util');
var AuthzUtil = require('oae-authz/lib/util');
var EmailTestsUtil = require('oae-email/lib/test/util');
var log = require('oae-logger').logger('test-activity');
var PreviewConstants = require('oae-preview-processor/lib/constants');
var RestAPI = require('oae-rest');
var RestContext = require('oae-rest/lib/model').RestContext;
var TestsUtil = require('oae-tests');

var Etherpad = require('oae-content/lib/internal/etherpad');

describe('Content Activity', function() {

    // Rest contexts that can be used performing rest requests
    var anonymousCamRestContext = null;
    var camAdminRestContext = null;
    var globalAdminRestContext = null;

    var suitable_files = null;
    var suitable_sizes = null;

    /**
     * Function that will fill up the tenant admin and anymous rest context
     */
    before(function(callback) {
        // Fill up the anonymous cam rest context
        anonymousCamRestContext = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host);
        // Fill up global admin rest context
        camAdminRestContext = TestsUtil.createTenantAdminRestContext(global.oaeTests.tenants.cam.host);
        globalAdminRestContext = TestsUtil.createGlobalAdminRestContext();

        // An object that adheres to the RestAPI.Content.setPreviewItems.files parameter.
        // We need 4 different files here as request.js mixes up the filenames.
        suitable_files = {
            'file.small.jpg': getFunctionThatReturnsFileStream('apereo-conference-2013.jpeg'),
            'file.medium.jpg': getFunctionThatReturnsFileStream('apereo.jpg'),
            'thumbnail.png': getFunctionThatReturnsFileStream('oae-logo.png'),
            'wide.png': getFunctionThatReturnsFileStream('oae-video.png')
        };
        suitable_sizes = {
            'file.small.jpg': 'small',
            'file.medium.jpg': 'medium',
            'thumbnail.png': 'thumbnail',
            'wide.png': 'wide'
        };

        callback();
    });

    /*!
     * Get the activity from the stream with the given criteria.
     *
     * @param  {ActivityStream}    activityStream      The stream to search
     * @param  {String}            activityType        The type of activity to find
     * @param  {String}            entityType          The type of entity to apply the criteria (one of actor, object or target)
     * @param  {String}            entityOaeId         The oae:id of the entity to search
     * @return {Activity}                              An activity from the stream that matches the provided criteria
     */
    var _getActivity = function(activityStream, activityType, entityType, entityOaeId) {
        if (!activityStream || !activityStream.items) {
            return null;
        }

        for (var i = 0; i < activityStream.items.length; i++) {
            var activity = activityStream.items[i];
            if (activity['oae:activityType'] === activityType && activity[entityType] && activity[entityType]['oae:id'] === entityOaeId) {
                return activity;
            }
        }
        return null;
    };

    /**
     * Returns a function that will return a stream that points to the specified file.
     * That function can then be passed into those RestAPI methods which need to upload a file
     *
     * @param  {String}     filename    The file in the tests/data directory that should be returned as a stream.
     * @return {Function}               A function that returns a stream when executed.
     */
    var getFunctionThatReturnsFileStream = function(filename) {
        return function() {
            var file = __dirname + '/data/' + filename;
            return fs.createReadStream(file);
        };
    };

    describe('Routes', function() {

        /**
         * Test that verifies a content resource routes activities to its members when created, updated and shared
         */
        it('verify routing to content members', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');
            var janeUsername = TestsUtil.generateTestUserId('jane');
            var managerGroupMemberUsername = TestsUtil.generateTestUserId('managerGroupMember');

            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                RestAPI.User.createUser(camAdminRestContext, janeUsername, 'password', 'Jane', null, function(err, jane) {
                    assert.ok(!err);

                    RestAPI.User.createUser(camAdminRestContext, managerGroupMemberUsername, 'password', 'Jane', null, function(err, managerGroupMember) {
                        assert.ok(!err);

                        // Create the group that will be a viewer of the content
                        RestAPI.Group.createGroup(camAdminRestContext, 'Viewer Group displayName', 'Viewer Group Description', 'public', 'no', [], [], function(err, viewerGroup) {
                            assert.ok(!err);

                            // Create a group that will be a manager of the content
                            RestAPI.Group.createGroup(camAdminRestContext, 'Manager Group displayName', 'Manager Group Description',  'public', 'no', [], [], function(err, managerGroup) {
                                assert.ok(!err);

                                // managerGroupMember should be a member of the manager group to verify indirect group member routing
                                var membership = {};
                                membership[managerGroupMember.id] = 'manager';
                                RestAPI.Group.setGroupMembers(camAdminRestContext, managerGroup.id, membership, function(err) {
                                    assert.ok(!err);

                                    // Create a content item with manager group and viewer group as members.
                                    RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [managerGroup.id], [viewerGroup.id], function(err, link) {
                                        assert.ok(!err);

                                        // Share the content item with jane
                                        RestAPI.Content.shareContent(jackCtx, link.id, [jane.id], function(err) {
                                            assert.ok(!err);

                                            // Update the content item
                                            RestAPI.Content.updateContent(jackCtx, link.id, {'description': 'Super awesome link'}, function(err) {
                                                assert.ok(!err);

                                                // Verify Jack got the create, share and update as he was the actor for all of them
                                                ActivityTestsUtil.collectAndGetActivityStream(jackCtx, jack.id, null, function(err, activityStream) {
                                                    assert.ok(!err);
                                                    assert.ok(_getActivity(activityStream, 'content-create', 'object', link.id));
                                                    assert.ok(_getActivity(activityStream, 'content-share', 'target', jane.id));
                                                    assert.ok(_getActivity(activityStream, 'content-update', 'object', link.id));

                                                    // Verify the manager group received the create, share and update as they are a content member
                                                    ActivityTestsUtil.collectAndGetActivityStream(camAdminRestContext, managerGroup.id, null, function(err, activityStream) {
                                                        assert.ok(!err);
                                                        assert.ok(_getActivity(activityStream, 'content-create', 'object', link.id));
                                                        assert.ok(_getActivity(activityStream, 'content-share', 'target', jane.id));
                                                        assert.ok(_getActivity(activityStream, 'content-update', 'object', link.id));

                                                        // Verify the viewer group received only the create and update. only managers care about the sharing of the "object"
                                                        ActivityTestsUtil.collectAndGetActivityStream(camAdminRestContext, viewerGroup.id, null, function(err, activityStream) {
                                                            assert.ok(!err);
                                                            assert.ok(_getActivity(activityStream, 'content-create', 'object', link.id));
                                                            assert.ok(!_getActivity(activityStream, 'content-share', 'target', jane.id));
                                                            assert.ok(_getActivity(activityStream, 'content-update', 'object', link.id));

                                                            // Verify the manager group *member* got the same activities as the manager group, as they are a member
                                                            ActivityTestsUtil.collectAndGetActivityStream(camAdminRestContext, managerGroupMember.id, null, function(err, activityStream) {
                                                                assert.ok(!err);
                                                                assert.ok(_getActivity(activityStream, 'content-create', 'object', link.id));
                                                                assert.ok(_getActivity(activityStream, 'content-share', 'target', jane.id));
                                                                assert.ok(_getActivity(activityStream, 'content-update', 'object', link.id));
                                                                callback();
                                                            });
                                                        });
                                                    });
                                                });
                                            });
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies when a an activity is routed to a user that isn't in the routes of a private content item (e.g., content-share
         * when non-manager), the content item is still propagated to the route appropriately.
         */
        it('verify content propagation to non-route activity feeds', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);

                // Create a private content item
                RestAPI.Content.createLink(camAdminRestContext, 'Google', 'Google', 'private', 'http://www.google.ca', [], [], function(err, link) {
                    assert.ok(!err);

                    // Share the content item with Jack. Jack will get the activity in his feed because he is the target, but he will not be in
                    // the routes of the content item because he is not a manager. Despite this, we need to verify that Jack has the full content
                    // item propagated to him as he does have access to it.
                    RestAPI.Content.shareContent(camAdminRestContext, link.id, [jack.id], function(err) {
                        assert.ok(!err);

                        ActivityTestsUtil.collectAndGetActivityStream(camAdminRestContext, jack.id, null, function(err, activityStream) {
                            assert.ok(!err);
                            assert.ok(activityStream);

                            // Ensure that the sensitive content info is available in jack's feed
                            var object = activityStream.items[0].object;
                            assert.equal(object['oae:visibility'], 'private');
                            assert.equal(object['oae:resourceSubType'], 'link');
                            assert.equal(object['oae:profilePath'], '/content/' + link.tenant.alias + '/' + AuthzUtil.getResourceFromId(link.id).resourceId);
                            assert.equal(object['displayName'], 'Google');
                            callback();
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies a comment activity is routed to recent commenters of a content item.
         */
        it('verify comment activity is routed to the recent commenters of a content item', function(callback) {
            TestsUtil.generateTestUsers(camAdminRestContext, 3, function(err, users) {
                var simong = _.values(users)[0];
                var mrvisser = _.values(users)[1];
                var bert = _.values(users)[2];

                // Create a content item to be commented on
                RestAPI.Content.createLink(simong.restContext, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, link) {
                    assert.ok(!err);

                    // mrvisser is not a member, but he will comment on it
                    RestAPI.Content.createComment(mrvisser.restContext, link.id, 'This link clearly goes to Google.', null, function(err, mrvisserComment) {
                        assert.ok(!err);

                        // Bert retorts!
                        RestAPI.Content.createComment(bert.restContext, link.id, 'You\'re wrong and you smell bad!', null, function(err, bertComment) {
                            assert.ok(!err);

                            // mrvisser should have a notification and an activity about this because he was a recent commenter
                            ActivityTestsUtil.collectAndGetActivityStream(mrvisser.restContext, mrvisser.user.id, null, function(err, activityStream) {
                                assert.ok(!err);

                                // Should have exactly 1 activity, 2 aggregated comments
                                assert.equal(activityStream.items.length, 1);
                                assert.equal(activityStream.items[0].object['oae:collection'].length, 2);
                                callback();
                            });
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies that a comment activity is not routed to a recent commenter if they no longer have
         * access to the content item (e.g., it becomes private after they commented).
         */
        it('verify a comment activity is not routed to a recent commenter if they no longer have access to the content item', function(callback) {
            TestsUtil.generateTestUsers(camAdminRestContext, 3, function(err, users) {
                var simong = _.values(users)[0];
                var mrvisser = _.values(users)[1];
                var bert = _.values(users)[2];

                // Create a content item to be commented on, bert is a member
                RestAPI.Content.createLink(simong.restContext, 'Google', 'Google', 'public', 'http://www.google.ca', [], [bert.user.id], function(err, link) {
                    assert.ok(!err);

                    // mrvisser is not a member, but he will comment on it
                    RestAPI.Content.createComment(mrvisser.restContext, link.id, 'This link clearly goes to Google.', null, function(err, mrvisserComment) {
                        assert.ok(!err);

                        // Force a collection before the content item goes private
                        ActivityTestsUtil.collectAndGetActivityStream(mrvisser.restContext, mrvisser.user.id, null, function(err) {
                            assert.ok(!err);

                            // simong has had enough of mrvisser's tom-foolery and makes the content item private
                            RestAPI.Content.updateContent(simong.restContext, link.id, {'visibility': 'private'}, function(err) {
                                assert.ok(!err);

                                // Bert retorts!
                                RestAPI.Content.createComment(bert.restContext, link.id, 'You\'re wrong and you smell bad!', null, function(err, bertComment) {
                                    assert.ok(!err);

                                    // mrvisser should only have the activity for the comment he made, not Bert's
                                    ActivityTestsUtil.collectAndGetActivityStream(mrvisser.restContext, mrvisser.user.id, null, function(err, activityStream) {
                                        assert.ok(!err);
                                        assert.equal(activityStream.items.length, 1);
                                        assert.equal(activityStream.items[0].object['oae:id'], mrvisserComment.id);
                                        callback();
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });

        /**
         * Verifies that a notification gets sent out to all the managers of a collaborative document.
         */
        it('verify that publishing a collaborative document generates a notification', function(callback) {
            TestsUtil.generateTestUsers(camAdminRestContext, 3, function(err, users) {
                assert.ok(!err);
                var simonCtx = _.values(users)[0].restContext;
                var brandenCtx = _.values(users)[1].restContext;
                var brandenId = _.keys(users)[1];
                var nicoCtx = _.values(users)[2].restContext;
                var nicoId = _.keys(users)[2];

                // Create a collaborative document where both Simon and Branden are managers and Nico as a viewer.
                var name = TestsUtil.generateTestUserId('collabdoc');
                RestAPI.Content.createCollabDoc(simonCtx, name, 'description', 'public', [brandenId], [nicoId], function(err, contentObj) {
                    assert.ok(!err);

                    // Branden edits a couple of things and publishes the document.
                    var etherpadClient = Etherpad.getClient(contentObj.id);
                    var args = {
                        'padID': contentObj.etherpadPadId,
                        'text': 'Ehrmagod that document!'
                    };
                    etherpadClient.setText(args, function(err) {
                        assert.ok(!err);
                        RestAPI.Content.publishCollabDoc(brandenCtx, contentObj.id, function(err, data) {
                            assert.ok(!err);
                            assert.ok(data);

                            // There should be a notification in Simon's stream as he is a manager.
                            ActivityTestsUtil.collectAndGetNotificationStream(simonCtx, null, function(err, data) {
                                assert.ok(!err);
                                var notificationSimon = _getActivity(data, 'content-revision', 'object', contentObj.id);
                                assert.ok(notificationSimon);
                                assert.equal(notificationSimon['oae:activityType'], 'content-revision');
                                assert.equal(notificationSimon['actor']['oae:id'], brandenId);
                                assert.equal(notificationSimon['object']['oae:id'], contentObj.id);

                                // There should be no notification in Nico's stream as he is not a manager.
                                ActivityTestsUtil.collectAndGetNotificationStream(nicoCtx, null, function(err, data) {
                                    assert.ok(!err);
                                    var notificationNico = _getActivity(data, 'content-revision', 'object', contentObj.id);
                                    assert.ok(!notificationNico);
                                    callback();
                                });
                            });
                        });
                    });
                });
            });
        });

        /**
         * Verifies that a notification gets sent out to all the managers of a piece of content when restoring an older version.
         */
        it('verify that restoring a piece of content generates a notification', function(callback) {
            TestsUtil.generateTestUsers(camAdminRestContext, 3, function(err, users) {
                assert.ok(!err);
                var simonCtx = _.values(users)[0].restContext;
                var brandenCtx = _.values(users)[1].restContext;
                var brandenId = _.keys(users)[1];
                var nicoCtx = _.values(users)[2].restContext;
                var nicoId = _.keys(users)[2];

                // Create a file where both Simon and Branden are managers and Nico as a viewer
                var name = TestsUtil.generateTestUserId('file');
                RestAPI.Content.createFile(simonCtx, name, 'description', 'public', getFunctionThatReturnsFileStream('oae-video.png'),  [brandenId], [nicoId], function(err, content) {
                    assert.ok(!err);

                    // Create a new revision
                    RestAPI.Content.updateFileBody(simonCtx, content.id, getFunctionThatReturnsFileStream('apereo.jpg'), function(err) {
                        assert.ok(!err);

                        // Restore the original revision
                        RestAPI.Content.restoreRevision(brandenCtx, content.id, content.latestRevisionId, function(err, revisionObj) {
                            assert.ok(!err);

                            // Verify the activity streams. All users should have received an activity
                            ActivityTestsUtil.collectAndGetActivityStream(brandenCtx, null, null, function(err, data) {
                                assert.ok(!err);
                                var activity = _getActivity(data, 'content-restored-revision', 'object', content.id);
                                assert.ok(activity);
                                assert.equal(activity['oae:activityType'], 'content-restored-revision');
                                assert.equal(activity['actor']['oae:id'], brandenId);
                                assert.equal(activity['object']['oae:id'], content.id);

                                ActivityTestsUtil.collectAndGetActivityStream(simonCtx, null, null, function(err, data) {
                                    assert.ok(!err);
                                    var activity = _getActivity(data, 'content-restored-revision', 'object', content.id);
                                    assert.ok(activity);
                                    assert.equal(activity['oae:activityType'], 'content-restored-revision');
                                    assert.equal(activity['actor']['oae:id'], brandenId);
                                    assert.equal(activity['object']['oae:id'], content.id);

                                    ActivityTestsUtil.collectAndGetActivityStream(nicoCtx, null, null, function(err, data) {
                                        assert.ok(!err);
                                        var notificationSimon = _getActivity(data, 'content-restored-revision', 'object', content.id);
                                        assert.ok(notificationSimon);
                                        assert.equal(notificationSimon['oae:activityType'], 'content-restored-revision');
                                        assert.equal(notificationSimon['actor']['oae:id'], brandenId);
                                        assert.equal(notificationSimon['object']['oae:id'], content.id);

                                        // There should also be a notification in Simon's stream as he is a manager
                                        ActivityTestsUtil.collectAndGetNotificationStream(simonCtx, null, function(err, data) {
                                            assert.ok(!err);
                                            var notificationSimon = _getActivity(data, 'content-restored-revision', 'object', content.id);
                                            assert.ok(notificationSimon);
                                            assert.equal(notificationSimon['oae:activityType'], 'content-restored-revision');
                                            assert.equal(notificationSimon['actor']['oae:id'], brandenId);
                                            assert.equal(notificationSimon['object']['oae:id'], content.id);

                                            // There should be no notification in Nico's stream as he is not a manager
                                            ActivityTestsUtil.collectAndGetNotificationStream(nicoCtx, null, function(err, data) {
                                                assert.ok(!err);
                                                var notificationNico = _getActivity(data, 'content-restored-revision', 'object', content.id);
                                                assert.ok(!notificationNico);
                                                callback();
                                            });
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });
    });

    describe('Activity Entity Models', function() {

        /**
         * Test that verifies the properties of the content entity
         */
        it('verify the content entity model contains the correct content information', function(callback) {

            /*!
             * Function used to verify the status of the "static" link content item in this test case. This basically means
             * everything except the preview items.
             */
            var _assertStandardLinkModel = function(entity, contentId) {
                var resourceId = AuthzUtil.getResourceFromId(contentId).resourceId;
                assert.equal(entity['oae:visibility'], 'public');
                assert.equal(entity['oae:resourceSubType'], 'link');
                assert.equal(entity['oae:profilePath'], '/content/camtest/' + resourceId);
                assert.equal(entity['displayName'], 'Google');
                assert.equal(entity['objectType'], 'content');
                assert.equal(entity['oae:id'], contentId);
                assert.equal(entity['url'], 'http://' + global.oaeTests.tenants.cam.host + '/content/camtest/' + resourceId);
                assert.ok(entity['id'].indexOf(contentId) !== -1);
            };

            var jackUsername = TestsUtil.generateTestUserId('jack');
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Generate an activity with the content
                RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, link) {
                    assert.ok(!err);

                    // Verify model with no preview state
                    ActivityTestsUtil.collectAndGetActivityStream(jackCtx, jack.id, null, function(err, activityStream) {
                        assert.ok(!err);
                        var entity = activityStream.items[0].object;
                        _assertStandardLinkModel(entity, link.id);
                        assert.ok(!entity.image);
                        assert.ok(!entity['oae:wideImage']);

                        // Get the global admin context on the camtest tenant
                        RestAPI.Admin.loginOnTenant(globalAdminRestContext, global.oaeTests.tenants.localhost.alias, function(err, globalTenantAdminRestContext) {
                            assert.ok(!err);

                            // Get the revision ID.
                            RestAPI.Content.getRevisions(globalTenantAdminRestContext, link.id, null, 1, function(err, revisions) {
                                assert.ok(!err);
                                var revisionId = revisions.results[0].revisionId;

                                // Set the preview to error status
                                RestAPI.Content.setPreviewItems(globalTenantAdminRestContext, link.id, revisionId, 'error', {}, {}, {}, {}, function(err) {
                                    assert.ok(!err);

                                    // Verify that the preview does not display
                                    ActivityTestsUtil.collectAndGetActivityStream(jackCtx, jack.id, null, function(err, activityStream) {
                                        assert.ok(!err);

                                        var entity = activityStream.items[0].object;
                                        _assertStandardLinkModel(entity, link.id);
                                        assert.ok(!entity.image);
                                        assert.ok(!entity['oae:wideImage']);

                                        // Set the preview to ignored status with no files
                                        RestAPI.Content.setPreviewItems(globalTenantAdminRestContext, link.id, revisionId, 'ignored', {}, {}, {}, {}, function(err) {
                                            assert.ok(!err);

                                            // Verify that the preview still does not display
                                            ActivityTestsUtil.collectAndGetActivityStream(jackCtx, jack.id, null, function(err, activityStream) {
                                                assert.ok(!err);

                                                var entity = activityStream.items[0].object;
                                                _assertStandardLinkModel(entity, link.id);
                                                assert.ok(!entity.image);
                                                assert.ok(!entity['oae:wideImage']);

                                                // Set the preview to done status with files
                                                RestAPI.Content.setPreviewItems(globalTenantAdminRestContext, link.id, revisionId, 'done', suitable_files, suitable_sizes, {}, {}, function(err) {
                                                    assert.ok(!err);

                                                    // Verify that the previews are returned in the activity
                                                    ActivityTestsUtil.collectAndGetActivityStream(jackCtx, jack.id, null, function(err, activityStream) {
                                                        assert.ok(!err);

                                                        var entity = activityStream.items[0].object;
                                                        _assertStandardLinkModel(entity, link.id);
                                                        assert.ok(entity.image);
                                                        assert.equal(entity.image.width, PreviewConstants.SIZES.IMAGE.THUMBNAIL);
                                                        assert.equal(entity.image.height, PreviewConstants.SIZES.IMAGE.THUMBNAIL);
                                                        assert.ok(entity.image.url);
                                                        assert.ok(entity['oae:wideImage']);
                                                        assert.equal(entity['oae:wideImage'].width, PreviewConstants.SIZES.IMAGE.WIDE_WIDTH);
                                                        assert.equal(entity['oae:wideImage'].height, PreviewConstants.SIZES.IMAGE.WIDE_HEIGHT);
                                                        assert.ok(entity['oae:wideImage'].url);
                                                        callback();
                                                    });
                                                });
                                            });
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies the properties of a comment entity
         */
        it('verify the comment entity model contains the correct comment information', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Generate an activity with the content
                RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, link) {
                    assert.ok(!err);

                    // Create 3 comments, including one reply. We want to make sure the context is properly aggregated in these comments as their activities are delivered.
                    RestAPI.Content.createComment(jackCtx, link.id, 'Comment A', null, function(err, commentA) {
                        assert.ok(!err);

                        RestAPI.Content.createComment(jackCtx, link.id, 'Comment B', null, function(err, commentB) {
                            assert.ok(!err);

                            RestAPI.Content.createComment(jackCtx, link.id, 'Reply Comment A', commentA.created, function(err, replyCommentA) {
                                assert.ok(!err);

                                ActivityTestsUtil.collectAndGetActivityStream(jackCtx, jack.id, null, function(err, activityStream) {
                                    assert.ok(!err);
                                    assert.ok(activityStream);

                                    // The first in the list (most recent) is the aggregated comment activity
                                    var activity = activityStream.items[0];
                                    var hadCommentA = false;
                                    var hadCommentB = false;
                                    var hadReplyCommentA = false;

                                    assert.ok(activity.object['oae:collection']);
                                    assert.equal(activity.object['oae:collection'].length, 3);

                                    /*!
                                     * Verifies the model of a comment and its context.
                                     *
                                     * @param  {ActivityEntity}    entity                      The comment entity to verify
                                     * @param  {Comment}           comment                     The comment with which to verify the entity
                                     * @param  {Comment}           [replyToComment]            Indicates the entity should have this comment as its inReplyTo. If unspecified, the entity should have no parent.
                                     */
                                    var _validateComment = function(entity, comment, replyToComment) {
                                        assert.equal(entity['objectType'], 'comment');
                                        assert.equal(entity['content'], comment.body);
                                        assert.equal(entity['oae:id'], comment.id);
                                        assert.equal(entity['url'], '/content/camtest/' + AuthzUtil.getResourceFromId(comment.messageBoxId).resourceId);
                                        assert.ok(entity['id'].indexOf('content/' + link.id + '/messages/' + comment.created) !== -1);
                                        assert.equal(entity['published'], comment.created);
                                        assert.equal(entity['oae:messageBoxId'], comment.messageBoxId);
                                        assert.equal(entity['oae:threadKey'], comment.threadKey);

                                        assert.ok(entity['author']);
                                        assert.ok(entity['author']['objectType'], 'user');
                                        assert.equal(entity['author']['oae:id'], comment.createdBy.id);

                                        if (replyToComment) {
                                            _validateComment(entity['inReplyTo'], replyToComment);
                                        } else {
                                            assert.ok(!entity['inReplyTo']);
                                        }
                                    };

                                    // Verify that the collection contains all comments, and their models are correct.
                                    activity.object['oae:collection'].forEach(function(entity) {
                                        if (entity.content === 'Comment A') {
                                            hadCommentA = true;

                                            // Ensures that comment A has correct data, and no parents
                                            _validateComment(entity, commentA);
                                        } else if (entity.content === 'Comment B') {
                                            hadCommentB = true;

                                            // Ensures that comment B has correct data, and no parents
                                            _validateComment(entity, commentB);
                                        } else if (entity.content === 'Reply Comment A') {
                                            hadReplyCommentA = true;

                                            // Verify that the reply to comment A has the right data and the parent (comment A)
                                            _validateComment(entity, replyCommentA, commentA);
                                        }
                                    });

                                    assert.ok(hadCommentA);
                                    assert.ok(hadCommentB);
                                    assert.ok(hadReplyCommentA);

                                    callback();
                                });
                            });
                        });
                    });
                });
            });
        });
    });

    describe('Posting Activities', function() {

        /**
         * Test that verifies that a content-create and content-update activity are generated when a content item is created and updated.
         */
        it('verify content-create and content-update activities are posted when content is created and updated', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Generate an activity with the content
                RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, link) {
                    assert.ok(!err);

                    RestAPI.Content.updateContent(jackCtx, link.id, {'description': 'Super awesome link'}, function(err) {
                        assert.ok(!err);

                        ActivityTestsUtil.collectAndGetActivityStream(jackCtx, jack.id, null, function(err, activityStream) {
                            assert.ok(!err);
                            var createActivity = _getActivity(activityStream, 'content-create', 'object', link.id);
                            var updateActivity = _getActivity(activityStream, 'content-update', 'object', link.id);
                            assert.ok(createActivity);
                            assert.equal(createActivity.verb, 'create');
                            assert.ok(updateActivity);
                            assert.equal(updateActivity.verb, 'update');
                            callback();
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies that a content-share activity is generated when a content item is shared.
         */
        it('verify a content-share activity is generated when a content item is shared', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);

                RestAPI.Content.createLink(camAdminRestContext, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, link) {
                    assert.ok(!err);

                    // Try and generate a share activity
                    RestAPI.Content.shareContent(camAdminRestContext, link.id, [jack.id], function(err) {
                        assert.ok(!err);

                        ActivityTestsUtil.collectAndGetActivityStream(camAdminRestContext, jack.id, null, function(err, activityStream) {
                            assert.ok(!err);
                            var shareActivity = _getActivity(activityStream, 'content-share', 'object', link.id);
                            assert.ok(shareActivity);
                            assert.equal(shareActivity.verb, 'share');
                            callback();
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies that a content-revision activity is generated when a content item's body has been updated / uploaded.
         */
        it('verify a content-revision activity is generated when a content item\'s body has been updated', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Create a revisable content item
                RestAPI.Content.createFile(jackCtx, 'Test Content 1', 'Test content description 1', 'private', getFunctionThatReturnsFileStream('oae-video.png'),  [], [], function(err, content) {
                    assert.ok(!err);
                    assert.ok(content);

                    // Create a new version
                    RestAPI.Content.updateFileBody(jackCtx, content.id, getFunctionThatReturnsFileStream('apereo.jpg'), function(err) {
                        assert.ok(!err);

                        // Verify the revision activity was created for jack
                        ActivityTestsUtil.collectAndGetActivityStream(camAdminRestContext, jack.id, null, function(err, activityStream) {
                            assert.ok(!err);
                            var revisionActivity = _getActivity(activityStream, 'content-revision', 'object', content.id);
                            assert.ok(revisionActivity);
                            assert.equal(revisionActivity.verb, 'update');

                            // Also verify that a content-update activity *doesn't* get generated. no one will want to see both a revision and a meta-data update
                            assert.ok(!_getActivity(activityStream, 'content-update', 'object', content.id));

                            callback();
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies that a content-add-to-library activity is generated when a user adds a content item to their own library
         */
        it('verify a content-add-to-library activity is generated when a user adds a content item to their own library', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                RestAPI.Content.createLink(camAdminRestContext, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, link) {
                    assert.ok(!err);

                    // Jack adds the content item to his own library
                    RestAPI.Content.shareContent(jackCtx, link.id, [jack.id], function(err) {
                        assert.ok(!err);

                        ActivityTestsUtil.collectAndGetActivityStream(camAdminRestContext, jack.id, null, function(err, activityStream) {
                            assert.ok(!err);
                            var addActivity = _getActivity(activityStream, 'content-add-to-library', 'object', link.id);
                            assert.ok(addActivity);
                            assert.equal(addActivity.verb, 'add');
                            callback();
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies that a content-update-visibility activity is generated when a content's visibility is updated
         */
        it('verify a content-update-visibility activity is generated when a content\'s visibility is updated', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                RestAPI.Content.createLink(camAdminRestContext, 'Google', 'Google', 'public', 'http://www.google.ca', [], [jack.id], function(err, link) {
                    assert.ok(!err);

                    // Jack adds the content item to his own library
                    RestAPI.Content.updateContent(camAdminRestContext, link.id, {'visibility': 'private'}, function(err) {
                        assert.ok(!err);

                        ActivityTestsUtil.collectAndGetActivityStream(camAdminRestContext, jack.id, null, function(err, activityStream) {
                            assert.ok(!err);
                            var updateVisibilityActivity = _getActivity(activityStream, 'content-update-visibility', 'object', link.id);
                            assert.ok(updateVisibilityActivity);
                            assert.equal(updateVisibilityActivity.verb, 'update');
                            callback();
                        });
                    });
                });
            });
        });
    });

    describe('Activity Aggregation', function() {

        ////////////////////
        // CONTENT CREATE //
        ////////////////////

        /**
         * Test that verifies that when multiple content-create activities are done by the same actor, the content items get
         * aggregated into a collection.
         */
        it('verify content-create activities are pivoted by actor', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');

            // Create Jack, the user whose will create content items
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Create a google link
                RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, googleLink) {
                    assert.ok(!err);

                    // Create a Yahoo link
                    RestAPI.Content.createLink(jackCtx, 'Yahoo!', 'Yahoo!', 'public', 'http://www.yahoo.ca', [], [], function(err, yahooLink) {
                        assert.ok(!err);

                        // Verify the activities were aggregated into one, pivoted by actor
                        ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                            assert.ok(!err);
                            assert.ok(activityStream);
                            assert.equal(activityStream.items.length, 1);

                            var aggregate = _getActivity(activityStream, 'content-create', 'actor', jack.id);
                            assert.ok(aggregate.object);
                            assert.ok(aggregate.object['oae:collection']);
                            assert.equal(aggregate.object['oae:collection'].length, 2);

                            if (aggregate.object['oae:collection'][0]['oae:id'] === googleLink.id && aggregate.object['oae:collection'][1]['oae:id'] === yahooLink.id) {
                                // Don't fail, we want one to be google and the other to be yahoo
                            } else if (aggregate.object['oae:collection'][0]['oae:id'] === yahooLink.id && aggregate.object['oae:collection'][1]['oae:id'] === googleLink.id) {
                                // Don't fail, we want one to be google and the other to be yahoo
                            } else {
                                assert.fail('Expected the collection of created content items to be one yahoo link and one google link.');
                            }

                            callback();
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies when a content create activity is aggregated and re-delivered, the activity that it is replacing is
         * deleted properly
         */
        it('verify when a content-create activity is redelivered, it deletes the previous one', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');

            // Create Jack, the user whose will create content items
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Create a google link
                RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, googleLink) {
                    assert.ok(!err);

                    // Force a collection of activities so that the individual activity is delivered
                    ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                        assert.ok(!err);
                        assert.ok(activityStream);
                        assert.ok(activityStream.items.length, 1);

                        // Create a Yahoo link
                        RestAPI.Content.createLink(jackCtx, 'Yahoo!', 'Yahoo!', 'public', 'http://www.yahoo.ca', [], [], function(err, yahooLink) {
                            assert.ok(!err);

                            // Collect again and ensure we still only have one activity
                            ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                assert.ok(!err);
                                assert.ok(activityStream);
                                assert.ok(activityStream.items.length, 1);

                                // Rinse and repeat once to ensure that the aggregates are removed properly as well
                                RestAPI.Content.createLink(jackCtx, 'Apereo!', 'Apereo!', 'public', 'http://www.apereo.org', [], [], function(err, apereoLink) {
                                    assert.ok(!err);

                                    // Collect again and ensure we still only have one activity
                                    ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                        assert.ok(!err);
                                        assert.ok(activityStream);
                                        assert.ok(activityStream.items.length, 1);
                                        callback();
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });

        ////////////////////
        // CONTENT UPDATE //
        ////////////////////

        /**
         * Test that verifies when a content item is updated multiple times, the actors that updated it are aggregated into a collection.
         */
        it('verify content-update activities are pivoted by object', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');

            // Create Jack, the user whose will create content items
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Create a google link
                RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, googleLink) {
                    assert.ok(!err);

                    // Update the content once as jack
                    RestAPI.Content.updateContent(jackCtx, googleLink.id, {'displayName': 'The Google'}, function(err) {
                        assert.ok(!err);

                        // Update it a second time as jack, we use this to make sure we don't get duplicates in the aggregation
                        RestAPI.Content.updateContent(jackCtx, googleLink.id, {'displayName': 'Google'}, function(err) {
                            assert.ok(!err);

                            // Update it with a different user, this should be a second entry in the collection
                            RestAPI.Content.updateContent(camAdminRestContext, googleLink.id, {'displayName': 'Google'}, function(err) {
                                assert.ok(!err);

                                // Verify we get the 2 actors in the stream
                                ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                    assert.ok(!err);
                                    assert.ok(activityStream);

                                    var activity = activityStream.items[0];
                                    assert.ok(activity);
                                    assert.equal(activity['oae:activityType'], 'content-update');

                                    var actors = activity.actor['oae:collection'];
                                    assert.ok(actors);
                                    assert.equal(actors.length, 2);
                                    callback();
                                });
                            });
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies two duplicate content updates do not result in an aggregation, but simply an activity with an updated
         * timestamp.
         */
        it('verify duplicate content-update activities are re-released with a more recent date, with no aggregations', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');

            // Create Jack, the user whose will create content items
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Create a google link
                RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, googleLink) {
                    assert.ok(!err);

                    // Update the content once as jack
                    RestAPI.Content.updateContent(jackCtx, googleLink.id, {'displayName': 'The Google'}, function(err) {
                        assert.ok(!err);

                        // Add something to the activity feed that happened later than the previous update
                        RestAPI.Content.createLink(jackCtx, 'Yahoo!', 'Yahoo!', 'public', 'http://www.yahoo.ca', [], [], function(err, yahooLink) {
                            assert.ok(!err);

                            // Update it a second time as jack, we use this to make sure we don't get duplicates in the aggregation, and ensure the update jumps ahead of the last create activity in the feed
                            RestAPI.Content.updateContent(jackCtx, googleLink.id, {'displayName': 'Google'}, function(err) {
                                assert.ok(!err);

                                // Verify that the activity is still a non-aggregated activity, it just jumped to the front of the feed
                                ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                    assert.ok(!err);
                                    assert.ok(activityStream);

                                    // One for the "content-create" aggregation, one for the "update content" duplicates
                                    assert.ok(activityStream.items.length, 2);

                                    // Ensures that the actor is not a collection, but still an individual entity
                                    var activity = activityStream.items[0];
                                    assert.equal(activity['oae:activityType'], 'content-update');
                                    assert.ok(activity.actor['oae:id'], jack.id);
                                    assert.equal(activity.actor['oae:profilePath'], '/user/' + jack.tenant.alias + '/' + AuthzUtil.getResourceFromId(jack.id).resourceId);
                                    assert.ok(activity.object['oae:id'], googleLink.id);
                                    assert.equal(activity.object['oae:profilePath'], '/content/' + googleLink.tenant.alias + '/' +  AuthzUtil.getResourceFromId(googleLink.id).resourceId);

                                    // Send a new activity into the feed so it is the most recent
                                    RestAPI.Content.createLink(jackCtx, 'Apereo', 'Apereo', 'public', 'http://www.apereo.org', [], [], function(err, apereoLink) {
                                        assert.ok(!err);

                                        // Force a collection so that the most recent activity is in the feed
                                        ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                            assert.ok(!err);
                                            assert.ok(activityStream);
                                            assert.equal(activityStream.items.length, 2);

                                            // Jump the update activity to the top again
                                            RestAPI.Content.updateContent(jackCtx, googleLink.id, {'displayName': 'Google'}, function(err) {
                                                assert.ok(!err);

                                                // Verify update activity is at the top and still an individual activity
                                                ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                                    assert.ok(activityStream);
                                                    assert.equal(activityStream.items.length, 2);

                                                    // content-update activity should be the first in the list, it should still be a single activity
                                                    var activity = activityStream.items[0];
                                                    assert.equal(activity['oae:activityType'], 'content-update');
                                                    assert.equal(activity.actor['oae:id'], jack.id);
                                                    assert.equal(activity.actor['oae:profilePath'], '/user/' + jack.tenant.alias + '/' +  AuthzUtil.getResourceFromId(jack.id).resourceId);
                                                    assert.ok(activity.object['oae:id'], googleLink.id);
                                                    assert.equal(activity.object['oae:profilePath'], '/content/' + googleLink.tenant.alias + '/' +  AuthzUtil.getResourceFromId(googleLink.id).resourceId);
                                                    callback();
                                                });
                                            });
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });

        ///////////////////////////////
        // CONTENT UPDATE VISIBILITY //
        ///////////////////////////////

        /*
         * The "content-update-visibility" activity demonstrates an activity that has no pivot points, therefore it should be
         * treated as though it pivots on all three entities (actor, object, target) such that duplicate activities within the
         * aggregation period are not posted multiple times. Duplicates are instead pushed to the top of the feed.
         */

        /**
         * Test that verifies when a content-update-visibility activity is posted duplicate times, it does not result in multiple entries in the
         * activity feed. Instead, the activity should be updated and reposted as a recent item.
         */
        it('verify duplicate content-update-visibility activities are not duplicated in the feed', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');

            // Create Jack, the user whose will create content items
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Create a google link
                RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, googleLink) {
                    assert.ok(!err);

                    // Update the content once as jack
                    RestAPI.Content.updateContent(jackCtx, googleLink.id, {'visibility': 'loggedin'}, function(err) {
                        assert.ok(!err);

                        // Add something to the activity feed that happened later than the previous update
                        RestAPI.Content.createLink(jackCtx, 'Yahoo!', 'Yahoo!', 'public', 'http://www.yahoo.ca', [], [], function(err, yahooLink) {
                            assert.ok(!err);

                            // Update it a second time as jack, we use this to make sure we don't get duplicates in the aggregation, and ensure the update jumps ahead of the last create activity in the feed
                            RestAPI.Content.updateContent(jackCtx, googleLink.id, {'visibility': 'private'}, function(err) {
                                assert.ok(!err);

                                // Verify that the activity is still a non-aggregated activity, it just jumped to the front of the feed
                                ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                    assert.ok(!err);
                                    assert.ok(activityStream);

                                    // One for the "content-create" aggregation, one for the "update content" duplicates
                                    assert.ok(activityStream.items.length, 2);

                                    // Ensures that the actor is not a collection, but still an individual entity
                                    var activity = activityStream.items[0];
                                    assert.equal(activity['oae:activityType'], 'content-update-visibility');
                                    assert.ok(activity.actor['oae:id'], jack.id);
                                    assert.equal(activity.actor['oae:profilePath'], '/user/' + jack.tenant.alias + '/' +  AuthzUtil.getResourceFromId(jack.id).resourceId);
                                    assert.ok(activity.object['oae:id'], googleLink.id);
                                    assert.equal(activity.object['oae:profilePath'], '/content/' + googleLink.tenant.alias + '/' +  AuthzUtil.getResourceFromId(googleLink.id).resourceId);

                                    // Send a new activity into the feed so it is the most recent
                                    RestAPI.Content.createLink(jackCtx, 'Apereo', 'Apereo', 'public', 'http://www.apereo.org', [], [], function(err, apereoLink) {
                                        assert.ok(!err);

                                        // Force a collection so that the most recent activity is in the feed
                                        ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                            assert.ok(!err);
                                            assert.ok(activityStream);
                                            assert.equal(activityStream.items.length, 2);

                                            // Jump the update activity to the top again
                                            RestAPI.Content.updateContent(jackCtx, googleLink.id, {'visibility': 'public'}, function(err) {
                                                assert.ok(!err);

                                                // Verify update activity is at the top and still an individual activity
                                                ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                                    assert.ok(activityStream);
                                                    assert.equal(activityStream.items.length, 2);

                                                    // content-update activity should be the first in the list, it should still be a single activity
                                                    var activity = activityStream.items[0];
                                                    assert.equal(activity['oae:activityType'], 'content-update-visibility');
                                                    assert.equal(activity.actor['oae:id'], jack.id);
                                                    assert.equal(activity.actor['oae:profilePath'], '/user/' + jack.tenant.alias + '/' + AuthzUtil.getResourceFromId(jack.id).resourceId);
                                                    assert.ok(activity.object['oae:id'], googleLink.id);
                                                    assert.equal(activity.object['oae:profilePath'], '/content/' + googleLink.tenant.alias + '/' + AuthzUtil.getResourceFromId(googleLink.id).resourceId);
                                                    callback();
                                                });
                                            });
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });

        /////////////////////
        // CONTENT-COMMENT //
        /////////////////////

        /*
         * The content-comment activity demonstrates an activity that has all 3 entities, but only aggregates on 1 of them. This means that
         * 2 of the entity types are aggregated as more activities are generated instead of just one.
         */

        /**
         * Test that verifies that when content-comment activities are aggregated, both actor and object entities are collected into the activity
         * for display.
         */
        it('verify that content-comment activity aggregates both actor and object entities while pivoting on target', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');

            // Create Jack, the user whose will create content items
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Create a google link
                RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, link) {
                    assert.ok(!err);

                    // Post a content as jack
                    RestAPI.Content.createComment(jackCtx, link.id, 'Test Comment A', null, function(err) {
                        assert.ok(!err);

                        // Post a comment as the cambridge admin, we have now aggregated a 2nd comment posting on the same content item
                        RestAPI.Content.createComment(camAdminRestContext, link.id, 'Test Comment B', null, function(err) {
                            assert.ok(!err);

                            // Verify that both actors (camadmin and jack) and both objects (both comments) are available in the activity
                            ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                assert.ok(!err);
                                assert.ok(activityStream);
                                assert.equal(activityStream.items.length, 2);

                                var activity = activityStream.items[0];
                                assert.equal(activity['oae:activityType'], 'content-comment');

                                // Ensure we've aggregated all actors and objects
                                var actors = activity.actor['oae:collection'];
                                var objects = activity.object['oae:collection'];
                                assert.ok(actors);
                                assert.ok(objects);
                                assert.equal(actors.length, 2);
                                assert.equal(objects.length, 2);
                                assert.equal(activity.target['oae:id'], link.id);

                                // Post a 3rd comment as a user who has posted already
                                RestAPI.Content.createComment(jackCtx, link.id, 'Test Comment C', null, function(err) {
                                    assert.ok(!err);

                                    // Verify that the 3rd comment is aggregated into the object collection of the activity, however the actor collection has only the 2 unique actors
                                    ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                        assert.ok(!err);
                                        assert.ok(activityStream);
                                        assert.equal(activityStream.items.length, 2);

                                        var activity = activityStream.items[0];
                                        assert.equal(activity['oae:activityType'], 'content-comment');

                                        // Ensure we now have one additional object, but we should still only have 2 users because it was the same user that posted the 3rd time
                                        var actors = activity.actor['oae:collection'];
                                        var objects = activity.object['oae:collection'];
                                        assert.ok(actors);
                                        assert.ok(objects);
                                        assert.equal(actors.length, 2);
                                        assert.equal(objects.length, 3);
                                        assert.equal(activity.target['oae:id'], link.id);
                                        callback();
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });

        ///////////////////
        // CONTENT-SHARE //
        ///////////////////

        /*
         * The content-share activity demonstrates a case where you have 2 pivots for a single activity.
         *
         * One pivot is actor+object, which enables the aggregation: "Branden Visser shared Mythology with 4 users and groups"
         *
         * The other pivot is actor+target, which enables the aggregation: "Branden Visser shared 5 items with GroupA"
         */

        /**
         * Test that verifies that duplicating an activity that has multiple pivots does not result in redundant data in the
         * activity feed.
         */
        it('verify duplicate content-share activities do not result in redundant activities', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');
            var janeUsername = TestsUtil.generateTestUserId('jane');

            // Create Jack, the user whose will create content items
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Create Jane, the user who will be shared with
                RestAPI.User.createUser(camAdminRestContext, janeUsername, 'password', 'Jane', null, function(err, jane) {
                    assert.ok(!err);
                    var janeCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, janeUsername, 'password');

                    // Create a google link
                    RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, link) {
                        assert.ok(!err);

                        // Share with jack, creates one activity item in cam admin's feed
                        RestAPI.Content.shareContent(jackCtx, link.id, [jane.id], function(err) {
                            assert.ok(!err);

                            var removeJane = {};
                            removeJane[jane.id] = false;

                            // Remove jane so we can duplicate the content share after
                            RestAPI.Content.updateMembers(jackCtx, link.id, removeJane, function(err) {
                                assert.ok(!err);

                                // Create some noise in the feed to ensure that the second share content will jump to the top
                                RestAPI.Content.createLink(jackCtx, 'Yahoo', 'Yahoo', 'public', 'http://www.google.ca', [], [], function(err, yahooLink) {
                                    assert.ok(!err);

                                    // Now re-add Jane
                                    RestAPI.Content.shareContent(jackCtx, link.id, [jane.id], function(err) {
                                        assert.ok(!err);

                                        // Verify that jack only has only one activity in his feed representing the content-share
                                        ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                            assert.ok(!err);
                                            assert.ok(activityStream);
                                            assert.equal(activityStream.items.length, 2);

                                            // The first activity should be the content share, and it should not be an aggregation
                                            var activity = activityStream.items[0];
                                            assert.equal(activity['oae:activityType'], 'content-share');
                                            assert.equal(activity.actor['oae:id'], jack.id);
                                            assert.equal(activity.actor['oae:profilePath'], '/user/' + jack.tenant.alias + '/' + AuthzUtil.getResourceFromId(jack.id).resourceId);
                                            assert.equal(activity.object['oae:id'], link.id);
                                            assert.equal(activity.object['oae:profilePath'], '/content/' + link.tenant.alias + '/' + AuthzUtil.getResourceFromId(link.id).resourceId);
                                            assert.equal(activity.target['oae:id'], jane.id);
                                            assert.equal(activity.target['oae:profilePath'], '/user/' + jane.tenant.alias + '/' + AuthzUtil.getResourceFromId(jane.id).resourceId);

                                            // Repeat once more to ensure we don't duplicate when the aggregate is already active
                                            RestAPI.Content.updateMembers(jackCtx, link.id, removeJane, function(err) {
                                                assert.ok(!err);

                                                // Create some noise in the feed to ensure that the third share content will jump to the top
                                                RestAPI.Content.createLink(jackCtx, 'Apereo', 'Apereo', 'public', 'http://www.apereo.org', [], [], function(err, apereoLink) {
                                                    assert.ok(!err);

                                                    // Re-share with Jane for the 3rd time
                                                    RestAPI.Content.shareContent(jackCtx, link.id, [jane.id], function(err) {
                                                        assert.ok(!err);

                                                        // Verify that jack still has only one activity in his feed representing the content-share
                                                        ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                                            assert.ok(!err);
                                                            assert.ok(activityStream);
                                                            assert.equal(activityStream.items.length, 2);

                                                            // The first activity should be the content share, and it should still not be an aggregation
                                                            var activity = activityStream.items[0];
                                                            assert.equal(activity['oae:activityType'], 'content-share');
                                                            assert.equal(activity.actor['oae:id'], jack.id);
                                                            assert.equal(activity.actor['oae:profilePath'], '/user/camtest/' + AuthzUtil.getResourceFromId(jack.id).resourceId);
                                                            assert.equal(activity.object['oae:id'], link.id);
                                                            assert.equal(activity.object['oae:profilePath'], '/content/camtest/' + AuthzUtil.getResourceFromId(link.id).resourceId);
                                                            assert.equal(activity.target['oae:id'], jane.id);
                                                            assert.equal(activity.target['oae:profilePath'], '/user/camtest/' + AuthzUtil.getResourceFromId(jane.id).resourceId);
                                                            callback();
                                                        });
                                                    });
                                                });
                                            });
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies an activity with two aggregates will create 2 aggregate activities correctly when collected all at once
         * from the activity bucket.
         */
        it('verify content-share activities aggregate and are branched properly when all collected at once', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');
            var janeUsername = TestsUtil.generateTestUserId('jane');
            var brandenUsername = TestsUtil.generateTestUserId('mrvisser');

            // Create Jack, the user whose will create content items
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Create Jane, the user who will be shared with
                RestAPI.User.createUser(camAdminRestContext, janeUsername, 'password', 'Jane', null, function(err, jane) {
                    assert.ok(!err);

                    // Create Branden, a second user to be shared with
                    RestAPI.User.createUser(camAdminRestContext, brandenUsername, 'password', 'Branden Visser', null, function(err, branden) {
                        assert.ok(!err);

                        // Create a google link and yahoo link to be shared around
                        RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, googleLink) {
                            assert.ok(!err);

                            RestAPI.Content.createLink(jackCtx, 'Yahoo', 'Yahoo', 'public', 'http://www.yahoo.ca', [], [], function(err, yahooLink) {
                                assert.ok(!err);

                                // Share google link with jane and branden
                                RestAPI.Content.shareContent(jackCtx, googleLink.id, [jane.id, branden.id], function(err) {
                                    assert.ok(!err);

                                    // Share Yahoo link with jane only
                                    RestAPI.Content.shareContent(jackCtx, yahooLink.id, [jane.id], function(err) {
                                        assert.ok(!err);

                                        // Verify that the share activities aggregated in both pivot points
                                        ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                            assert.ok(!err);
                                            assert.ok(activityStream);
                                            assert.equal(activityStream.items.length, 3);

                                            // 1. actor+target should have jack+(google,yahoo)+jane, and it would be most recent
                                            var activity = activityStream.items[0];
                                            assert.ok(activity.object['oae:collection']);
                                            assert.equal(activity.object['oae:collection'].length, 2);

                                            // 2. actor+object aggregate should have: jack+google+(jane,branden)
                                            activity = activityStream.items[1];
                                            assert.ok(activity.target['oae:collection']);
                                            assert.equal(activity.target['oae:collection'].length, 2);

                                            callback();
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies an activity with two aggregates will create 2 aggregate activities correctly when one single activity
         * currently exists in the activity stream.
         */
        it('verify content-share activities aggregate and are branched properly when collected after first share', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');
            var janeUsername = TestsUtil.generateTestUserId('jane');
            var brandenUsername = TestsUtil.generateTestUserId('mrvisser');

            // Create Jack, the user whose will create content items
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Create Jane, the user who will be shared with
                RestAPI.User.createUser(camAdminRestContext, janeUsername, 'password', 'Jane', null, function(err, jane) {
                    assert.ok(!err);

                    // Create Branden, a second user to be shared with
                    RestAPI.User.createUser(camAdminRestContext, brandenUsername, 'password', 'Branden Visser', null, function(err, branden) {
                        assert.ok(!err);

                        // Create a google link and yahoo link to be shared around
                        RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, googleLink) {
                            assert.ok(!err);

                            RestAPI.Content.createLink(jackCtx, 'Yahoo', 'Yahoo', 'public', 'http://www.yahoo.ca', [], [], function(err, yahooLink) {
                                assert.ok(!err);

                                // Share google link with jane
                                RestAPI.Content.shareContent(jackCtx, googleLink.id, [jane.id], function(err) {
                                    assert.ok(!err);

                                    // Perform a collection to activate some aggregates ahead of time
                                    ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                        assert.ok(!err);

                                        // Share google now with branden, should aggregate with the previous
                                        RestAPI.Content.shareContent(jackCtx, googleLink.id, [branden.id], function(err) {
                                            assert.ok(!err);

                                            // Share Yahoo link with jane only
                                            RestAPI.Content.shareContent(jackCtx, yahooLink.id, [jane.id], function(err) {
                                                assert.ok(!err);

                                                // Verify that the share activities aggregated in both pivot points
                                                ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                                    assert.ok(!err);
                                                    assert.ok(activityStream);
                                                    assert.equal(activityStream.items.length, 3);

                                                    // 1. actor+target should have jack+(google,yahoo)+jane, and it would be most recent
                                                    var activity = activityStream.items[0];
                                                    assert.ok(activity.object['oae:collection']);
                                                    assert.equal(activity.object['oae:collection'].length, 2);

                                                    // 2. actor+object aggregate should have: jack+google+(jane,branden)
                                                    activity = activityStream.items[1];
                                                    assert.ok(activity.target['oae:collection']);
                                                    assert.equal(activity.target['oae:collection'].length, 2);

                                                    callback();
                                                });
                                            });
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies an activity with two aggregates will create 2 aggregate activities correctly when one aggregate exists
         * in the feed before a third is collected.
         */
        it('verify content-share activities aggregate and are branched properly when collected before last share', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');
            var janeUsername = TestsUtil.generateTestUserId('jane');
            var brandenUsername = TestsUtil.generateTestUserId('mrvisser');

            // Create Jack, the user whose will create content items
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Create Jane, the user who will be shared with
                RestAPI.User.createUser(camAdminRestContext, janeUsername, 'password', 'Jane', null, function(err, jane) {
                    assert.ok(!err);

                    // Create Branden, a second user to be shared with
                    RestAPI.User.createUser(camAdminRestContext, brandenUsername, 'password', 'Branden Visser', null, function(err, branden) {
                        assert.ok(!err);

                        // Create a google link and yahoo link to be shared around
                        RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, googleLink) {
                            assert.ok(!err);

                            RestAPI.Content.createLink(jackCtx, 'Yahoo', 'Yahoo', 'public', 'http://www.yahoo.ca', [], [], function(err, yahooLink) {
                                assert.ok(!err);

                                // Share google link with jane and branden
                                RestAPI.Content.shareContent(jackCtx, googleLink.id, [jane.id, branden.id], function(err) {
                                    assert.ok(!err);

                                    // Perform a collection to activate some aggregates ahead of time
                                    ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                        assert.ok(!err);

                                        // Share Yahoo link with jane only
                                        RestAPI.Content.shareContent(jackCtx, yahooLink.id, [jane.id], function(err) {
                                            assert.ok(!err);

                                            // Verify that the share activities aggregated in both pivot points
                                            ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                                assert.ok(!err);
                                                assert.ok(activityStream);
                                                assert.equal(activityStream.items.length, 3);

                                                // 1. actor+target should have jack+(google,yahoo)+jane, and it would be most recent
                                                var activity = activityStream.items[0];
                                                assert.ok(activity.object['oae:collection']);
                                                assert.equal(activity.object['oae:collection'].length, 2);

                                                // 2. actor+object aggregate should have: jack+google+(jane,branden)
                                                activity = activityStream.items[1];
                                                assert.ok(activity.target['oae:collection']);
                                                assert.equal(activity.target['oae:collection'].length, 2);

                                                callback();
                                            });
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies an activity with two aggregates will create 2 aggregate activities correctly when each activity is collected
         * and delivered to the feed one by one.
         */
        it('verify content-share activities aggregate and are branched properly when collected after each share', function(callback) {
            var jackUsername = TestsUtil.generateTestUserId('jack');
            var janeUsername = TestsUtil.generateTestUserId('jane');
            var brandenUsername = TestsUtil.generateTestUserId('mrvisser');

            // Create Jack, the user whose will create content items
            RestAPI.User.createUser(camAdminRestContext, jackUsername, 'password', 'Jack McJackerson', null, function(err, jack) {
                assert.ok(!err);
                var jackCtx = TestsUtil.createTenantRestContext(global.oaeTests.tenants.cam.host, jackUsername, 'password');

                // Create Jane, the user who will be shared with
                RestAPI.User.createUser(camAdminRestContext, janeUsername, 'password', 'Jane', null, function(err, jane) {
                    assert.ok(!err);

                    // Create Branden, a second user to be shared with
                    RestAPI.User.createUser(camAdminRestContext, brandenUsername, 'password', 'Branden Visser', null, function(err, branden) {
                        assert.ok(!err);

                        // Create a google link and yahoo link to be shared around
                        RestAPI.Content.createLink(jackCtx, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, googleLink) {
                            assert.ok(!err);

                            RestAPI.Content.createLink(jackCtx, 'Yahoo', 'Yahoo', 'public', 'http://www.yahoo.ca', [], [], function(err, yahooLink) {
                                assert.ok(!err);

                                // Share google link with jane
                                RestAPI.Content.shareContent(jackCtx, googleLink.id, [jane.id], function(err) {
                                    assert.ok(!err);

                                    // Perform a collection to activate some aggregates ahead of time
                                    ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                        assert.ok(!err);

                                        // Share google now with branden, should aggregate with the previous
                                        RestAPI.Content.shareContent(jackCtx, googleLink.id, [branden.id], function(err) {
                                            assert.ok(!err);

                                            // Perform a collection to activate some aggregates ahead of time
                                            ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                                assert.ok(!err);

                                                // Share Yahoo link with jane only
                                                RestAPI.Content.shareContent(jackCtx, yahooLink.id, [jane.id], function(err) {
                                                    assert.ok(!err);

                                                    // Verify that the share activities aggregated in both pivot points
                                                    ActivityTestsUtil.collectAndGetActivityStream(jackCtx, null, null, function(err, activityStream) {
                                                        assert.ok(!err);
                                                        assert.ok(activityStream);
                                                        assert.equal(activityStream.items.length, 3);

                                                        // 1. actor+target should have jack+(google,yahoo)+jane, and it would be most recent
                                                        var activity = activityStream.items[0];
                                                        assert.ok(activity.object['oae:collection']);
                                                        assert.equal(activity.object['oae:collection'].length, 2);

                                                        // 2. actor+object aggregate should have: jack+google+(jane,branden)
                                                        activity = activityStream.items[1];
                                                        assert.ok(activity.target['oae:collection']);
                                                        assert.equal(activity.target['oae:collection'].length, 2);

                                                        callback();
                                                    });
                                                });
                                            });
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });
    });

    describe('Email', function() {

        /**
         * Test that verifies an email is sent to the recent commenters, and that private users are appropriately
         * scrubbed.
         */
        it('verify content-comment email and privacy', function(callback) {
            TestsUtil.generateTestUsers(camAdminRestContext, 3, function(err, users) {
                assert.ok(!err);

                var mrvisser = _.values(users)[0];
                var simong = _.values(users)[1];
                var nicolaas = _.values(users)[2];

                mrvisser.user.email = 'mrvisser' + Math.random() + '@email.com';
                simong.user.email = 'simong' + Math.random() + '@email.com';

                var mrvisserUpdate = {'email': mrvisser.user.email};
                var simongUpdate = {
                    'email': simong.user.email,
                    'visibility': 'private',
                    'publicAlias': 'swappedFromPublicAlias'
                };

                RestAPI.User.updateUser(mrvisser.restContext, mrvisser.user.id, mrvisserUpdate, function(err) {
                    assert.ok(!err);

                    RestAPI.User.updateUser(simong.restContext, simong.user.id, simongUpdate, function(err) {
                        assert.ok(!err);

                        RestAPI.Content.createLink(mrvisser.restContext, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, link) {
                            assert.ok(!err);

                            RestAPI.Content.createComment(simong.restContext, link.id, 'Nice link, would click again', null, function(err, simongComment) {
                                assert.ok(!err);

                                EmailTestsUtil.collectAndFetchEmails(function(messages) {
                                    // There should be exactly one message, the one sent to mrvisser (manager of content item receives content-comment notification)
                                    assert.equal(messages.length, 1);

                                    var stringMessage = JSON.stringify(messages[0]);
                                    var message = messages[0]._message;

                                    // Sanity check that the message is to mrvisser
                                    assert.equal(message.to, mrvisser.user.email);

                                    // Ensure some data expected to be in the email is there
                                    assert.notEqual(stringMessage.indexOf(simong.restContext.hostHeader), -1);
                                    assert.notEqual(stringMessage.indexOf(link.profilePath), -1);
                                    assert.notEqual(stringMessage.indexOf(link.displayName), -1);

                                    // Ensure simong's private info is *nowhere* to be found
                                    assert.equal(stringMessage.indexOf(simong.user.displayName), -1);
                                    assert.equal(stringMessage.indexOf(simong.user.email), -1);
                                    assert.equal(stringMessage.indexOf(simong.user.locale), -1);
                                    assert.equal(stringMessage.indexOf(simong.user.timezone), -1);

                                    // The message probably contains the public alias, though
                                    assert.notEqual(stringMessage.indexOf('swappedFromPublicAlias'), -1);

                                    // Post a comment as nicolaas and ensure the recent commenter, simong receives an email about it
                                    RestAPI.Content.createComment(nicolaas.restContext, link.id, 'It 404d', null, function(err, nicolaasComment) {
                                        assert.ok(!err);

                                        EmailTestsUtil.collectAndFetchEmails(function(emails) {
                                            // There should be 2 emails this time, one to the manager and one to the recent commenter, simong
                                            assert.equal(emails.length, 2);

                                            var emailAddresses = [emails[0]._message.to, emails[1]._message.to];
                                            assert.ok(_.contains(emailAddresses, simong.user.email));
                                            assert.ok(_.contains(emailAddresses, mrvisser.user.email));
                                            return callback();
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies an email is sent to the members when a content item is created, and that private users are
         * appropriately scrubbed.
         */
        it('verify content-create email and privacy', function(callback) {
            TestsUtil.generateTestUsers(camAdminRestContext, 2, function(err, users) {
                assert.ok(!err);

                var mrvisser = _.values(users)[0];
                var simong = _.values(users)[1];

                mrvisser.user.email = 'mrvisser' + Math.random() + '@email.com';
                simong.user.email = 'simong' + Math.random() + '@email.com';

                // Simon is private and mrvisser is public
                var mrvisserUpdate = {'email': mrvisser.user.email};
                var simongUpdate = {
                    'email': simong.user.email,
                    'visibility': 'private',
                    'publicAlias': 'swappedFromPublicAlias'
                };

                RestAPI.User.updateUser(mrvisser.restContext, mrvisser.user.id, mrvisserUpdate, function(err) {
                    assert.ok(!err);

                    RestAPI.User.updateUser(simong.restContext, simong.user.id, simongUpdate, function(err) {
                        assert.ok(!err);

                        // Create the link, sharing it with mrvisser during the creation step. We will ensure he gets an email about it
                        RestAPI.Content.createLink(simong.restContext, 'Google', 'Google', 'public', 'http://www.google.ca', [], [mrvisser.user.id], function(err, link) {
                            assert.ok(!err);

                            // Mrvisser should get an email, with simong's information scrubbed
                            EmailTestsUtil.collectAndFetchEmails(function(messages) {
                                // There should be exactly one message, the one sent to mrvisser
                                assert.equal(messages.length, 1);

                                var stringMessage = JSON.stringify(messages[0]);
                                var message = messages[0]._message;

                                // Sanity check that the message is to mrvisser
                                assert.equal(message.to, mrvisser.user.email);

                                // Ensure some data expected to be in the email is there
                                assert.notEqual(stringMessage.indexOf(simong.restContext.hostHeader), -1);
                                assert.notEqual(stringMessage.indexOf(link.profilePath), -1);
                                assert.notEqual(stringMessage.indexOf(link.displayName), -1);

                                // Ensure simong's private info is *nowhere* to be found
                                assert.equal(stringMessage.indexOf(simong.user.displayName), -1);
                                assert.equal(stringMessage.indexOf(simong.user.email), -1);
                                assert.equal(stringMessage.indexOf(simong.user.locale), -1);
                                assert.equal(stringMessage.indexOf(simong.user.timezone), -1);

                                // The message probably contains the public alias, though
                                assert.notEqual(stringMessage.indexOf('swappedFromPublicAlias'), -1);

                                callback();
                            });
                        });
                    });
                });
            });
        });

        /**
         * Test that verifies an email is sent to the target users when content is shared, and that private users are
         * appropriately scrubbed.
         */
        it('verify content-share email and privacy', function(callback) {
            TestsUtil.generateTestUsers(camAdminRestContext, 2, function(err, users) {
                assert.ok(!err);

                var mrvisser = _.values(users)[0];
                var simong = _.values(users)[1];

                mrvisser.user.email = 'mrvisser' + Math.random() + '@email.com';
                simong.user.email = 'simong' + Math.random() + '@email.com';

                // Simon is private and mrvisser is public
                var mrvisserUpdate = {'email': mrvisser.user.email};
                var simongUpdate = {
                    'email': simong.user.email,
                    'visibility': 'private',
                    'publicAlias': 'swappedFromPublicAlias'
                };

                RestAPI.User.updateUser(mrvisser.restContext, mrvisser.user.id, mrvisserUpdate, function(err) {
                    assert.ok(!err);

                    RestAPI.User.updateUser(simong.restContext, simong.user.id, simongUpdate, function(err) {
                        assert.ok(!err);

                        // Create the link, then share it with mrvisser. We will ensure that mrvisser gets the email about the share
                        RestAPI.Content.createLink(simong.restContext, 'Google', 'Google', 'public', 'http://www.google.ca', [], [], function(err, link) {
                            assert.ok(!err);

                            // Collect the createLink activity
                            EmailTestsUtil.collectAndFetchEmails(function(messages) {

                                RestAPI.Content.shareContent(simong.restContext, link.id, [mrvisser.user.id], function(err) {
                                    assert.ok(!err);

                                    // Mrvisser should get an email, with simong's information scrubbed
                                    EmailTestsUtil.collectAndFetchEmails(function(messages) {
                                        // There should be exactly one message, the one sent to mrvisser
                                        assert.equal(messages.length, 1);

                                        var stringMessage = JSON.stringify(messages[0]);
                                        var message = messages[0]._message;

                                        // Sanity check that the message is to mrvisser
                                        assert.equal(message.to, mrvisser.user.email);

                                        // Ensure some data expected to be in the email is there
                                        assert.notEqual(stringMessage.indexOf(simong.restContext.hostHeader), -1);
                                        assert.notEqual(stringMessage.indexOf(link.profilePath), -1);
                                        assert.notEqual(stringMessage.indexOf(link.displayName), -1);

                                        // Ensure simong's private info is *nowhere* to be found
                                        assert.equal(stringMessage.indexOf(simong.user.displayName), -1);
                                        assert.equal(stringMessage.indexOf(simong.user.email), -1);
                                        assert.equal(stringMessage.indexOf(simong.user.locale), -1);
                                        assert.equal(stringMessage.indexOf(simong.user.timezone), -1);

                                        // The message probably contains the public alias, though
                                        assert.notEqual(stringMessage.indexOf('swappedFromPublicAlias'), -1);
                                        callback();
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });
    });
});