Skip to content

Instantly share code, notes, and snippets.

@p2k
Created February 11, 2020 11:59
Show Gist options
  • Save p2k/d8f51a64067cf86b7da1aa9bef0d1aba to your computer and use it in GitHub Desktop.
Save p2k/d8f51a64067cf86b7da1aa9bef0d1aba to your computer and use it in GitHub Desktop.
Collection of Scripts to Verify Couchbase Sync Gateway Integrity

Steps to reproduce

Initialize:

yarn install

Build config.json:

node build_config.js

Start containers:

docker-compose up -d

Point your browser to http://<docker-machine-ip>:8091/ui/index.html

During cluster setup, create an arbitrary admin user and set the index memory quota to 256 MB.

Create a Couchbase bucket with name sync-test and create a Couchbase user sync-test with password sync-test. Make sure to disable replicas under "Advanced bucket settings".

Wait a few moments until the sync gateway has restarted (it constantly restarts itself until you finish configuring the bucket).

Create 1500 users:

node create_users.js http://<docker-machine-ip>:4985/sync-test 1500

Create test group with 100 users:

node create_group.js http://<docker-machine-ip>:4985/sync-test 100

Verify the above group:

node verify_group.js http://<docker-machine-ip>:4985/sync-test 100

Create 10 test assets for the above group:

node create_assets.js http://<docker-machine-ip>:4985/sync-test 100 10

Verify assets for the above group (via admin API, checks all at once):

node verify_assets.js http://<docker-machine-ip>:4985/sync-test 100 10

Verify assets for the above group (via public API, will check for each user):

node verify_assets.js http://<docker-machine-ip>:4984/sync-test 100 10

Verify users for all groups (checks list of channels):

node verify_users.js http://<docker-machine-ip>:4985/sync-test

Repeat the above for various numbers. Couchbase breaks at about 1000 users per group.

const fs = require('fs');
const sync = fs.readFileSync('sync.js', { encoding: 'utf8' });
const cfg = {
interface: ':4984',
adminInterface: ':4985',
logging: {
console: {
log_keys: ['*'],
},
},
databases: {
'sync-test': {
server: 'couchbase://server',
bucket: 'sync-test',
username: 'sync-test',
password: 'sync-test',
num_index_replicas: 0,
users: {
GUEST: {
disabled: false,
admin_channels: ['user:public', '!'],
},
},
allow_conflicts: true,
revs_limit: 20,
sync,
},
},
};
fs.writeFileSync('config.json', JSON.stringify(cfg, null, 2), { encoding: 'utf8' });
const path = require('path');
const request = require('request-promise-native');
async function createAssets (url, groupId, acount) {
for (let i = 0; i < acount; i++) {
const assetId = `000${i}`.slice(-4);
await request.put(`${url}/asset:group-${groupId}-asset-${assetId}`, {
json: true,
body: {
_id: `asset:group-${groupId}-asset-${assetId}`,
type: 'asset',
permissions: [
{ group: `test-group-${groupId}`, read: true },
],
},
});
}
}
if (process.argv.length < 5) {
console.log(`usage: node ${path.basename(process.argv[1])} <database-url> <users-count> <assets-count>`);
process.exit(1);
}
let dbURL = process.argv[2];
if (dbURL.endsWith('/')) {
dbURL = dbURL.substr(0, dbURL.length - 1);
}
const groupId = `000${parseInt(process.argv[3], 10)}`.slice(-4);
const assetsCount = parseInt(process.argv[4], 10);
console.log(`Creating ${assetsCount} asset(s) for group test-group-${groupId}...`);
const ts = new Date().valueOf();
createAssets(dbURL, groupId, assetsCount)
.then(() => {
console.log(`Done in ${new Date().valueOf() - ts}ms.`);
process.exit(0);
})
.catch((err) => {
console.log(err);
process.exit(1);
});
const path = require('path');
const request = require('request-promise-native');
if (process.argv.length < 4) {
console.log(`usage: node ${path.basename(process.argv[1])} <database-url> <users-count>`);
process.exit(1);
}
let dbURL = process.argv[2];
if (dbURL.endsWith('/')) {
dbURL = dbURL.substr(0, dbURL.length - 1);
}
const usersCount = parseInt(process.argv[3], 10);
const groupId = `000${usersCount}`.slice(-4);
console.log(`Creating group for ${usersCount} user(s)...`);
if (usersCount >= 500) {
console.log(`Note: This might take a ${usersCount >= 1000 ? 'very long' : 'long'} time.`);
}
const ts = new Date().valueOf();
const members = [];
for (let i = 0; i < usersCount; i++) {
const userId = `000${i}`.slice(-4);
members.push(`test-user-${userId}@example.com`);
}
request.put(`${dbURL}/group:test-group-${groupId}`, {
json: true,
body: {
type: 'group',
name: `test-group-${groupId}`,
members,
},
}).then(() => {
console.log(`Done in ${new Date().valueOf() - ts}ms.`);
process.exit(0);
}).catch((err) => {
console.log(err);
process.exit(1);
});
const path = require('path');
const request = require('request-promise-native');
async function createUsers (url, count) {
let n = count / 10;
for (let i = 0; i < count; i++) {
const userId = `000${i}`.slice(-4);
await request.post(`${url}/_user/`, {
json: true,
body: {
name: `test-user-${userId}@example.com`,
password: `test-user-${userId}`,
},
});
await request.put(`${url}/user:test-user-${userId}@example.com`, {
json: true,
body: {
_id: `user:test-user-${userId}@example.com`,
type: 'user',
name: `test-user-${userId}@example.com`,
},
});
if (i >= n) {
console.log(`${(i * 100 / count) | 0}%`);
n += count / 10;
}
}
}
if (process.argv.length < 4) {
console.log(`usage: node ${path.basename(process.argv[1])} <database-url> <users-count>`);
process.exit(1);
}
let dbURL = process.argv[2];
if (dbURL.endsWith('/')) {
dbURL = dbURL.substr(0, dbURL.length - 1);
}
const usersCount = parseInt(process.argv[3], 10);
console.log(`Creating ${usersCount} user(s)...`);
const ts = new Date().valueOf();
createUsers(dbURL, usersCount)
.then(() => {
console.log(`Done in ${new Date().valueOf() - ts}ms.`);
process.exit(0);
})
.catch((err) => {
console.log(err);
process.exit(1);
});
version: '3.5'
services:
server:
image: couchbase/server:community-6.0.0
ports:
- '8091-8094:8091-8094'
- '11210:11210'
networks:
- sync-test
gateway:
image: couchbase/sync-gateway:2.7.0-community
ports:
- '4984-4985:4984-4985'
networks:
- sync-test
volumes:
- ./config.json:/etc/sync_gateway.json
command: /etc/sync_gateway.json
restart: on-failure
networks:
sync-test:
name: sync-test
{
"name": "sync-test",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"request": "^2.88.0",
"request-promise-native": "^1.0.8"
}
}
function (doc) {
// Note: This minimal version of our sync function does not contain any kind of validation or authentication
if (doc._deleted) {
return;
}
if (doc.type === 'user') {
var userChannel = 'user:' + doc.name;
channel(userChannel);
access(doc.name, userChannel);
}
else if (doc.type === 'group') {
var groupChannel = 'group:' + doc.name;
channel(groupChannel);
access(doc.members, groupChannel);
}
else {
var channels = [];
for (var i = 0; i < doc.permissions.length; i++) {
var permission = doc.permissions[i];
if (permission.read === true) {
if (permission.user != null) {
channels.push('user:' + permission.user);
}
if (permission.group != null) {
if (permission.group === 'public') {
channels.push('!');
}
else {
channels.push('group:' + permission.group);
}
}
}
}
channel(channels);
}
}
const path = require('path');
const request = require('request-promise-native');
async function verifyAssetsAdmin (url, groupId, expect) {
const res = await request.get(url, { json: true });
if (res.rows.length !== expect) {
throw new Error(`Did not see the expected number of assets (${res.rows.length} != ${expect})`);
}
for (const row of res.rows) {
if (row.value.channels == null || row.value.channels.length !== 1 || row.value.channels[0] !== `group:test-group-${groupId}`) {
throw new Error(`Did not see the expected channels in asset ${row.id}`);
}
}
}
async function verifyAssets (url, groupId, expect, usersCount) {
let n = usersCount / 10;
const opts = { json: true };
for (let i = 0; i < usersCount; i++) {
const userId = `000${i}`.slice(-4);
opts.auth = {
user: `test-user-${userId}@example.com`,
pass: `test-user-${userId}`,
};
const res = await request.get(url, opts);
if (res.rows.length !== expect) {
console.log(`Did not see the expected number of assets (${res.rows.length} != ${expect}) for user test-user-${userId}`);
}
for (const row of res.rows) {
if (row.value.channels == null || row.value.channels.length !== 1 || row.value.channels[0] !== `group:test-group-${groupId}`) {
console.log(`Did not see the expected channels in asset ${row.id} for user test-user-${userId}`);
}
}
if (i >= n) {
console.log(`${(i * 100 / usersCount) | 0}%`);
n += usersCount / 10;
}
}
}
if (process.argv.length < 5) {
console.log(`usage: node ${path.basename(process.argv[1])} <database-url> <users-count> <assets-count>`);
process.exit(1);
}
let dbURL = process.argv[2];
if (dbURL.endsWith('/')) {
dbURL = dbURL.substr(0, dbURL.length - 1);
}
const lslash = dbURL.lastIndexOf('/');
if (lslash === -1) {
console.log('Error: Invalid URL');
process.exit(1);
}
const rootURL = dbURL.substr(0, lslash);
const usersCount = parseInt(process.argv[3], 10);
const groupId = `000${usersCount}`.slice(-4);
const assetsCount = parseInt(process.argv[4], 10);
const reqURL = `${dbURL}/_all_docs?channels=true&startkey=asset:group-${groupId}-asset-&endkey=asset:group-${groupId}-asset-z`;
request.get(rootURL, { json: true })
.then((ret) => {
const isAdmin = !!ret.ADMIN;
console.log(`Verifying ${assetsCount} asset(s) for group test-group-${groupId} on ${isAdmin ? 'admin' : 'public'} API...`);
const ts = new Date().valueOf();
return (isAdmin ? verifyAssetsAdmin(reqURL, groupId, assetsCount) : verifyAssets(reqURL, groupId, assetsCount, usersCount))
.then(() => {
console.log(`Done in ${new Date().valueOf() - ts}ms.`);
process.exit(0);
});
})
.catch((err) => {
console.log(err);
process.exit(1);
});
const path = require('path');
const request = require('request-promise-native');
if (process.argv.length < 4) {
console.log(`usage: node ${path.basename(process.argv[1])} <database-url> <users-count>`);
process.exit(1);
}
let dbURL = process.argv[2];
if (dbURL.endsWith('/')) {
dbURL = dbURL.substr(0, dbURL.length - 1);
}
const usersCount = parseInt(process.argv[3], 10);
const groupId = `000${usersCount}`.slice(-4);
console.log(`Verifying test-group-${groupId}...`);
const ts = new Date().valueOf();
const mset = new Set();
for (let i = 0; i < usersCount; i++) {
const userId = `000${i}`.slice(-4);
mset.add(`test-user-${userId}@example.com`);
}
const groupName = `group:test-group-${groupId}`;
request.get(`${dbURL}/_raw/${groupName}`, {
json: true,
}).then((doc) => {
let channels = Object.keys(doc._sync.channels);
if (channels.length !== 1) {
throw new Error('Unexpected number of channels');
}
if (channels[0] !== groupName) {
throw new Error(`Unexpected channel: ${channels[0]}`);
}
const access = doc._sync.access;
for (const user of Object.keys(access)) {
if (!mset.has(user)) {
throw new Error(`Unexpected access for user ${user}`);
}
channels = Object.keys(access[user]);
if (channels.length !== 1) {
throw new Error(`Unexpected number of channels for user ${user}`);
}
if (channels[0] !== groupName) {
throw new Error(`Unexpected channel for user ${user}: ${channels[0]}`);
}
mset.delete(user);
}
if (mset.size !== 0) {
throw new Error(`Missing access for: ${Array.from(mset).join(', ')}`);
}
console.log(`Done in ${new Date().valueOf() - ts}ms.`);
process.exit(0);
}).catch((err) => {
console.log(err);
process.exit(1);
});
const path = require('path');
const request = require('request-promise-native');
async function verifyUsers (url, users, userGroups) {
let n = users.length / 10;
for (let i = 0; i < users.length; i++) {
const user = users[i];
const { all_channels: channels } = await request.get(`${url}/_user/${user}`, { json: true });
const channelSet = new Set(channels);
const groups = userGroups.get(user) || [];
const missing = [];
for (const group of groups) {
if (!channelSet.has(group)) {
missing.push(group);
}
}
if (missing.length > 0) {
console.log(`User ${user} is missing channels: ${missing.join(', ')}`);
}
if (i >= n) {
console.log(`${(i * 100 / users.length) | 0}%`);
n += users.length / 10;
}
}
}
if (process.argv.length < 3) {
console.log(`usage: node ${path.basename(process.argv[1])} <database-url>`);
process.exit(1);
}
let dbURL = process.argv[2];
if (dbURL.endsWith('/')) {
dbURL = dbURL.substr(0, dbURL.length - 1);
}
request.get(`${dbURL}/_user/`, { json: true })
.then((users) => {
return request.get(`${dbURL}/_all_docs?include_docs=true&startkey=group:test-group-&endkey=group:test-group-z`, { json: true })
.then((ret) => {
const userGroups = new Map();
for (const { id: groupId, doc } of ret.rows) {
for (const user of doc.members) {
const groups = userGroups.get(user);
if (groups == null) {
userGroups.set(user, [groupId]);
}
else {
groups.push(groupId);
}
}
}
console.log(`Verifying ${users.length} user(s) against ${ret.rows.length} group(s)...`);
const ts = new Date().valueOf();
return verifyUsers(dbURL, users, userGroups)
.then(() => {
console.log(`Done in ${new Date().valueOf() - ts}ms.`);
process.exit(0);
});
});
})
.catch((err) => {
console.log(err);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment