diff --git a/.env.test b/.env.test
index 99805fd..623afd8 100644
--- a/.env.test
+++ b/.env.test
@@ -1 +1,7 @@
MONGODB_CONNECTION_STRING=mongodb://127.0.0.1:27017/mongoose_test
+STRIPE_SECRET_KEY=sk_test_123
+STRIPE_WEBHOOK_SECRET=whsec_test_123
+GITHUB_REDIRECT_URI=http://localhost:7777/oauth-github-login
+MAILGUN_FROM_EMAIL=noreply@example.com
+MAILGUN_DOMAIN=example.com
+MAILGUN_PRIVATE_KEY=key-test
diff --git a/netlify/functions/getMyAccount.js b/netlify/functions/getMyAccount.js
new file mode 100644
index 0000000..dfbb917
--- /dev/null
+++ b/netlify/functions/getMyAccount.js
@@ -0,0 +1,5 @@
+'use strict';
+
+const extrovert = require('extrovert');
+
+module.exports = extrovert.toNetlifyFunction(require('../../src/actions/getMyAccount'));
diff --git a/netlify/functions/github.js b/netlify/functions/github.js
index 533443c..3002021 100644
--- a/netlify/functions/github.js
+++ b/netlify/functions/github.js
@@ -30,7 +30,8 @@ module.exports = extrovert.toNetlifyFunction(async function github(params) {
const { access_token: token } = await githubOAuth.getAccessToken(code);
const userData = await githubOAuth.getUser(token);
- const { id: githubUserId, notification_email: email, avatar_url: picture, name, login: githubUsername } = userData;
+ const { id: githubUserId, notification_email: notificationEmail, avatar_url: picture, name, login: githubUsername } = userData;
+ const email = notificationEmail || await githubOAuth.getPrimaryEmail(token) || `${githubUsername}@users.noreply.github.com`;
const $set = {
githubUserId,
@@ -55,9 +56,12 @@ module.exports = extrovert.toNetlifyFunction(async function github(params) {
let roles = null;
if (member == null) {
const invitation = await Invitation.findOne({
- githubUsername,
workspaceId,
- status: 'pending'
+ status: 'pending',
+ $or: [
+ { githubUsername },
+ { email: email.toLowerCase() }
+ ]
});
if (invitation != null) {
workspace.members.push({ userId: user._id, roles: invitation.roles });
@@ -73,6 +77,15 @@ module.exports = extrovert.toNetlifyFunction(async function github(params) {
const seats = users.length;
await stripe.updateSubscriptionSeats(workspace.stripeSubscriptionId, seats);
}
+ } else if (
+ workspace.members.length === 0 &&
+ workspace.stripeCustomerEmail &&
+ workspace.stripeCustomerEmail.toLowerCase() === email.toLowerCase()
+ ) {
+ workspace.members.push({ userId: user._id, roles: ['owner'] });
+ workspace.stripeCustomerEmail = null;
+ await workspace.save();
+ roles = ['owner'];
} else if (workspace.subscriptionTier === 'free') {
workspace.members.push({ userId: user._id, roles: ['readonly'] });
await workspace.save();
diff --git a/netlify/functions/google.js b/netlify/functions/google.js
index 09db416..bef5426 100644
--- a/netlify/functions/google.js
+++ b/netlify/functions/google.js
@@ -73,6 +73,14 @@ module.exports = extrovert.toNetlifyFunction(async function googleLogin(params)
const seats = users.length;
await stripe.updateSubscriptionSeats(workspace.stripeSubscriptionId, seats);
}
+ } else if (
+ workspace.members.length === 0 &&
+ workspace.stripeCustomerEmail &&
+ workspace.stripeCustomerEmail.toLowerCase() === email.toLowerCase()
+ ) {
+ workspace.members.push({ userId: user._id, roles: ['owner'] });
+ await workspace.save();
+ roles = ['owner'];
} else if (workspace.subscriptionTier === 'free') {
workspace.members.push({ userId: user._id, roles: ['readonly'] });
await workspace.save();
diff --git a/netlify/functions/updateWorkspace.js b/netlify/functions/updateWorkspace.js
new file mode 100644
index 0000000..7699823
--- /dev/null
+++ b/netlify/functions/updateWorkspace.js
@@ -0,0 +1,5 @@
+'use strict';
+
+const extrovert = require('extrovert');
+
+module.exports = extrovert.toNetlifyFunction(require('../../src/actions/updateWorkspace'));
diff --git a/package.json b/package.json
index f7607ff..02b9218 100644
--- a/package.json
+++ b/package.json
@@ -11,11 +11,13 @@
"cheerio": "1.0.0",
"extrovert": "0.2.0",
"googleapis": "100.0.0",
- "mongoose": "^9.0.0-0",
+ "mongoose": "9.x",
"ramda": "0.28.0",
+ "random-word-slugs": "^0.1.7",
"stripe": "9.9.0",
"time-commando": "1.0.1",
- "vue": "3.x"
+ "vue": "3.x",
+ "xss": "^1.0.15"
},
"devDependencies": {
"@masteringjs/eslint-config": "0.1.1",
diff --git a/public/login.html b/public/login.html
new file mode 100644
index 0000000..58d82ad
--- /dev/null
+++ b/public/login.html
@@ -0,0 +1,157 @@
+
+
+
+
+
+ Mongoose Studio Account Setup
+
+
+
+
+
+ Set up your account
+ Sign in to connect your account and access your API key.
+
+
+
+
+
+
+
+ Get Help on Discord
+
+
+
+ Already signed in? Go to My Account
+
+
+
+
+
+
diff --git a/public/my-account.html b/public/my-account.html
new file mode 100644
index 0000000..e842c92
--- /dev/null
+++ b/public/my-account.html
@@ -0,0 +1,251 @@
+
+
+
+
+
+ My Account - Mongoose Studio
+
+
+
+
+
+
+
+
My Account
+
Loading account...
+
+
+
+
+
+
+
+
+ Go to login
+
+
+
+
+ No workspaces found for this account.
+
+
+
+
+
+
+
+
+
diff --git a/src/actions/getMyAccount.js b/src/actions/getMyAccount.js
new file mode 100644
index 0000000..7d4a9f9
--- /dev/null
+++ b/src/actions/getMyAccount.js
@@ -0,0 +1,41 @@
+'use strict';
+
+const Archetype = require('archetype');
+const connect = require('../../src/db');
+
+const GetMyAccountParams = new Archetype({
+ authorization: {
+ $type: 'string',
+ $required: true
+ }
+}).compile('GetMyAccountParams');
+
+module.exports = async function getMyAccount(params) {
+ const { authorization } = new GetMyAccountParams(params);
+
+ const db = await connect();
+ const { AccessToken, User, Workspace } = db.models;
+
+ const accessToken = await AccessToken.findById(authorization).orFail(new Error('Invalid or expired access token'));
+ if (accessToken.expiresAt < new Date()) {
+ throw new Error('Access token has expired');
+ }
+
+ const user = await User.findById(accessToken.userId).orFail(new Error('User not found'));
+ const workspaces = await Workspace.find({ 'members.userId': user._id }).sort({ name: 1, createdAt: 1 });
+
+ const workspaceSummaries = workspaces.map(workspace => {
+ const member = workspace.members.find(member => member.userId.toString() === user._id.toString());
+ return {
+ _id: workspace._id,
+ name: workspace.name,
+ apiKey: workspace.apiKey,
+ subscriptionTier: workspace.subscriptionTier,
+ stripeCustomerId: workspace.stripeCustomerId,
+ stripeSubscriptionId: workspace.stripeSubscriptionId,
+ roles: member?.roles ?? []
+ };
+ });
+
+ return { user, workspaces: workspaceSummaries };
+};
diff --git a/src/actions/inviteToWorkspace.js b/src/actions/inviteToWorkspace.js
index 2fad8e2..82cee56 100644
--- a/src/actions/inviteToWorkspace.js
+++ b/src/actions/inviteToWorkspace.js
@@ -19,7 +19,8 @@ const InviteToWorkspaceParams = new Archetype({
$required: true
},
email: {
- $type: 'string'
+ $type: 'string',
+ $transform: v => v == null ? null : v.toLowerCase()
},
roles: {
$type: ['string'],
diff --git a/src/actions/stripeWebhook.js b/src/actions/stripeWebhook.js
index 616f65d..0147528 100644
--- a/src/actions/stripeWebhook.js
+++ b/src/actions/stripeWebhook.js
@@ -2,9 +2,16 @@
const Archetype = require('archetype');
const assert = require('assert');
+const cheerio = require('cheerio');
const connect = require('../../src/db');
+const crypto = require('crypto');
+const fs = require('fs');
const mongoose = require('mongoose');
+const mailgun = require('../integrations/mailgun');
+const path = require('path');
+const { generateSlug } = require('random-word-slugs');
const stripe = require('../integrations/stripe');
+const xss = require('xss');
const StripeWebhookParams = new Archetype({
type: {
@@ -25,6 +32,8 @@ const StripeWebhookParams = new Archetype({
}
}).compile('StripeWebhookParams');
+const newWorkspaceTemplate = fs.readFileSync(path.resolve(__dirname, '..', 'emailTemplates', 'newWorkspace.html'), 'utf8');
+
module.exports = async function stripeWebhook(params, event) {
try {
stripe.client.webhooks.constructEvent(event.body, event.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET);
@@ -41,31 +50,70 @@ module.exports = async function stripeWebhook(params, event) {
if (type === 'checkout.session.completed') {
const workspaceId = data?.object?.client_reference_id;
- // If no workspace ID provided, create a new workspace with just API key and subscription details
- if (!workspaceId && data?.object?.customer && data?.object?.subscription) {
- // Get customer email from Stripe
- const customer = await stripe.client.customers.retrieve(data.object.customer);
- const customerEmail = customer.email ?? 'Auto-created workspace';
+ const stripeCustomerId = data?.object?.customer;
+ const stripeSubscriptionId = data?.object?.subscription;
+ assert.ok(stripeCustomerId, 'no customer found in webhook');
+ assert.ok(stripeSubscriptionId, 'no subscription found in webhook');
+ const customer = await stripe.client.customers.retrieve(stripeCustomerId);
+ const customerEmail = customer?.email?.toLowerCase?.() ?? null;
+ const newApiKey = crypto.randomBytes(48).toString('hex');
+ const publicAppBaseUrl = new URL(process.env.GITHUB_REDIRECT_URI).origin;
+ const randomWorkspaceName = generateSlug(2, { format: 'kebab' });
+
+ // If no workspace ID provided, create a new workspace with just API key and subscription details
+ if (!workspaceId) {
const newWorkspace = new Workspace({
- name: customerEmail,
- stripeCustomerId: data.object.customer,
- stripeSubscriptionId: data.object.subscription,
- subscriptionTier: 'pro'
+ name: randomWorkspaceName,
+ stripeCustomerId,
+ stripeSubscriptionId,
+ subscriptionTier: 'pro',
+ stripeCustomerEmail: customerEmail,
+ apiKey: newApiKey
});
await newWorkspace.save();
+ if (customerEmail) {
+ const setupUrl = new URL('/login.html', publicAppBaseUrl);
+ setupUrl.searchParams.set('workspaceId', newWorkspace._id.toString());
+ const $ = cheerio.load(newWorkspaceTemplate);
+ $('#workspace-name').text(newWorkspace.name);
+ $('#setup-link').attr('href', setupUrl.toString());
+ $('#setup-link-fallback').attr('href', setupUrl.toString()).text(setupUrl.toString());
+ await mailgun.sendEmail({
+ to: customerEmail,
+ from: process.env.MAILGUN_FROM_EMAIL,
+ subject: 'Set up your Mongoose Studio Workspace',
+ html: $.html()
+ });
+ }
return { workspace: newWorkspace };
} else {
const workspace = await Workspace.findById(workspaceId).orFail();
assert.ok(!workspace.stripeSubscriptionId, 'workspace already has a subscription');
- assert.ok(data.object.customer, 'no customer found in webhook');
- assert.ok(data.object.subscription, 'no subscription found in webhook');
- workspace.stripeCustomerId = data.object.customer;
- workspace.stripeSubscriptionId = data.object.subscription;
+ workspace.stripeCustomerId = stripeCustomerId;
+ workspace.stripeSubscriptionId = stripeSubscriptionId;
workspace.subscriptionTier = 'pro';
+ workspace.apiKey = newApiKey;
+ if (workspace.members.length === 0 && customerEmail) {
+ workspace.stripeCustomerEmail = customerEmail;
+ }
await workspace.save();
+ if (customerEmail) {
+ const setupUrl = new URL('/login.html', publicAppBaseUrl);
+ setupUrl.searchParams.set('workspaceId', workspace._id.toString());
+ const $ = cheerio.load(newWorkspaceTemplate);
+ $('#workspace-name').text(xss(workspace.name || randomWorkspaceName));
+ $('#setup-link').attr('href', setupUrl.toString());
+ $('#setup-link-fallback').attr('href', setupUrl.toString()).text(setupUrl.toString());
+ await mailgun.sendEmail({
+ to: customerEmail,
+ from: process.env.MAILGUN_FROM_EMAIL,
+ subject: 'Set up your Mongoose Studio Workspace',
+ html: $.html()
+ });
+ }
return { workspace };
}
diff --git a/src/actions/updateWorkspace.js b/src/actions/updateWorkspace.js
new file mode 100644
index 0000000..a3a9060
--- /dev/null
+++ b/src/actions/updateWorkspace.js
@@ -0,0 +1,52 @@
+'use strict';
+
+const Archetype = require('archetype');
+const connect = require('../../src/db');
+const mongoose = require('mongoose');
+
+const UpdateWorkspaceParams = new Archetype({
+ authorization: {
+ $type: 'string',
+ $required: true
+ },
+ workspaceId: {
+ $type: mongoose.Types.ObjectId,
+ $required: true
+ },
+ name: {
+ $type: 'string'
+ }
+}).compile('UpdateWorkspaceParams');
+
+module.exports = async function updateWorkspace(params) {
+ const { authorization, workspaceId, name } = new UpdateWorkspaceParams(params);
+
+ const db = await connect();
+ const { AccessToken, Workspace } = db.models;
+
+ const accessToken = await AccessToken.findById(authorization).orFail(new Error('Invalid or expired access token'));
+ if (accessToken.expiresAt < new Date()) {
+ throw new Error('Access token has expired');
+ }
+
+ const workspace = await Workspace.findById(workspaceId).orFail(new Error('Workspace not found'));
+ const roles = workspace.members.find(member => member.userId.toString() === accessToken.userId.toString())?.roles || [];
+ if (!roles.includes('owner') && !roles.includes('admin')) {
+ throw new Error('Forbidden');
+ }
+
+ if (name != null) {
+ const trimmed = name.trim();
+ if (!trimmed) {
+ throw new Error('Workspace name is required');
+ }
+ if (trimmed.length > 120) {
+ throw new Error('Workspace name is too long');
+ }
+ workspace.name = trimmed;
+ }
+
+ await workspace.save();
+
+ return { workspace };
+};
diff --git a/src/db/invitation.js b/src/db/invitation.js
index c8c26af..9451e71 100644
--- a/src/db/invitation.js
+++ b/src/db/invitation.js
@@ -36,7 +36,7 @@ const invitationSchema = new mongoose.Schema({
}
}, { timestamps: true, id: false });
-invitationSchema.post('validate', function () {
+invitationSchema.post('validate', function() {
if (!this.email && !this.githubUsername) {
throw new Error('Either email or githubUsername is required');
}
diff --git a/src/db/user.js b/src/db/user.js
index 9b3c16e..870a7a0 100644
--- a/src/db/user.js
+++ b/src/db/user.js
@@ -30,7 +30,7 @@ const userSchema = new mongoose.Schema({
userSchema.post('validate', function() {
if (!this.githubUserId && !this.googleUserId) {
- throw new Error('Either githubUserId or googleUserId must be set.')
+ throw new Error('Either githubUserId or googleUserId must be set.');
}
});
diff --git a/src/db/workspace.js b/src/db/workspace.js
index 7718536..feda508 100644
--- a/src/db/workspace.js
+++ b/src/db/workspace.js
@@ -41,6 +41,10 @@ const workspaceSchema = new mongoose.Schema({
subscriptionTier: {
type: String,
enum: ['', 'free', 'pro']
+ },
+ stripeCustomerEmail: {
+ type: String,
+ lowercase: true
}
}, { timestamps: true, id: false });
diff --git a/src/emailTemplates/newWorkspace.html b/src/emailTemplates/newWorkspace.html
new file mode 100644
index 0000000..50a82c6
--- /dev/null
+++ b/src/emailTemplates/newWorkspace.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+ Mongoose Studio Setup
+
+
+
+
+
+
+
+
+ Welcome to Mongoose Studio
+ Thank you for your support. Your new workspace is ready and we appreciate you being here.
+ Workspace:
+ Use the link below to sign in and finish setup:
+ Finish Setup
+ If the button does not work, copy and paste this link into your browser:
+ Need help? We're here for you on Discord: Get Help on Discord
+ |
+
+
+ |
+
+
+
+
diff --git a/src/integrations/githubOAuth.js b/src/integrations/githubOAuth.js
index 5ba1dea..04c2846 100644
--- a/src/integrations/githubOAuth.js
+++ b/src/integrations/githubOAuth.js
@@ -52,6 +52,22 @@ module.exports = {
}
return body;
},
+ async getPrimaryEmail(token) {
+ const headers = {
+ authorization: `token ${token}`,
+ accept: 'application/vnd.github.v3+json'
+ };
+ const response = await fetch('https://api.github.com/user/emails', { headers });
+ const body = await response.json();
+ if (response.status >= 400) {
+ throw new Error(`Request failed with status ${response.status}: ${require('util').inspect(body)}`);
+ }
+ if (!Array.isArray(body)) {
+ return null;
+ }
+ const primary = body.find(email => email.primary && email.verified) || body.find(email => email.primary) || body[0];
+ return primary?.email || null;
+ },
getAccessToken(code) {
const body = {
client_id: githubOAuthClientId,
diff --git a/src/integrations/mailgun.js b/src/integrations/mailgun.js
new file mode 100644
index 0000000..01a314d
--- /dev/null
+++ b/src/integrations/mailgun.js
@@ -0,0 +1,59 @@
+'use strict';
+
+// Node.js has built-in form-data, but our version of Axios isn't compatible with it
+const FormData = require('form-data');
+const { IntegrationError } = require('../util/error');
+const assert = require('assert');
+const axios = require('axios');
+
+const from = process.env.MAILGUN_FROM_EMAIL;
+assert.ok(from);
+
+const domain = process.env.MAILGUN_DOMAIN;
+assert.ok(domain);
+
+const privateKey = process.env.MAILGUN_PRIVATE_KEY;
+assert.ok(privateKey);
+
+exports.sendEmail = async function sendEmail(params) {
+ const url = `https://api.mailgun.net/v3/${domain}/messages`;
+ let form = null;
+ if (params.attachment) {
+ form = new FormData();
+ for (const [key, value] of Object.entries(params)) {
+ form.append(key, value);
+ }
+ }
+
+ try {
+ const { data } = await axios(
+ url,
+ {
+ method: 'post',
+ ...(form ? { headers: form.getHeaders(), data: form } : { params }),
+ auth: {
+ username: 'api',
+ password: privateKey
+ }
+ }
+ );
+ if (data && data.message) {
+ return data.message;
+ }
+ } catch (error) {
+ if (error instanceof axios.AxiosError) {
+ throw new IntegrationError(
+ `Error sending email: ${error.response?.data?.message ?? error.response?.data} (status code ${error.response?.status})`,
+ 'mailgun',
+ error.response?.status,
+ {
+ responseData: error.response?.data,
+ message: error.response?.data?.message,
+ status: error.response?.status,
+ params
+ }
+ );
+ }
+ throw error;
+ }
+};
diff --git a/src/util/error.js b/src/util/error.js
new file mode 100644
index 0000000..eb069ff
--- /dev/null
+++ b/src/util/error.js
@@ -0,0 +1,13 @@
+'use strict';
+
+class IntegrationError extends Error {
+ constructor(message, integration, statusCode, extra) {
+ super(message);
+ this.name = 'IntegrationError';
+ this.integration = integration;
+ this.statusCode = statusCode;
+ this.extra = extra;
+ }
+}
+
+module.exports = { IntegrationError };
diff --git a/test/stripeWebhook.test.js b/test/stripeWebhook.test.js
new file mode 100644
index 0000000..9d48035
--- /dev/null
+++ b/test/stripeWebhook.test.js
@@ -0,0 +1,98 @@
+'use strict';
+
+const assert = require('assert');
+const connect = require('../src/db');
+const { afterEach, beforeEach, describe, it } = require('mocha');
+const mailgun = require('../src/integrations/mailgun');
+const sinon = require('sinon');
+const stripe = require('../src/integrations/stripe');
+const stripeWebhook = require('../src/actions/stripeWebhook');
+
+describe('stripeWebhook', function() {
+ let db;
+ let Workspace;
+
+ beforeEach(async function() {
+ db = await connect();
+ ({ Workspace } = db.models);
+ await Workspace.deleteMany({});
+ });
+
+ afterEach(function() {
+ sinon.restore();
+ });
+
+ it('creates a workspace, rotates api key, and sends setup email for checkout without workspaceId', async function() {
+ sinon.stub(stripe.client.webhooks, 'constructEvent').returns({});
+ sinon.stub(stripe.client.customers, 'retrieve').resolves({ email: 'customer@example.com' });
+ const sendEmailStub = sinon.stub(mailgun, 'sendEmail').resolves('Queued');
+
+ const res = await stripeWebhook(
+ {
+ type: 'checkout.session.completed',
+ data: {
+ object: {
+ customer: 'cus_123',
+ subscription: 'sub_123'
+ }
+ }
+ },
+ { body: '{}', headers: { 'stripe-signature': 'sig_123' } }
+ );
+
+ assert.ok(res.workspace);
+ assert.ok(/^[a-z]+-[a-z]+$/.test(res.workspace.name), `expected slug name, got "${res.workspace.name}"`);
+ assert.equal(res.workspace.stripeCustomerEmail, 'customer@example.com');
+ assert.equal(res.workspace.stripeCustomerId, 'cus_123');
+ assert.equal(res.workspace.stripeSubscriptionId, 'sub_123');
+ assert.equal(res.workspace.subscriptionTier, 'pro');
+ assert.ok(/^[a-f0-9]{96}$/.test(res.workspace.apiKey), 'expected hex api key');
+
+ assert.equal(sendEmailStub.callCount, 1);
+ const emailParams = sendEmailStub.firstCall.args[0];
+ assert.equal(emailParams.to, 'customer@example.com');
+ assert.ok(
+ /^Set up your Mongoose Studio/.test(emailParams.subject),
+ `unexpected email subject "${emailParams.subject}"`
+ );
+ assert.ok(!emailParams.html.includes(res.workspace.apiKey));
+ assert.ok(emailParams.html.includes(`workspaceId=${res.workspace._id}`));
+ });
+
+ it('updates existing workspace and rotates api key', async function() {
+ sinon.stub(stripe.client.webhooks, 'constructEvent').returns({});
+ sinon.stub(stripe.client.customers, 'retrieve').resolves({ email: 'owner@example.com' });
+ const sendEmailStub = sinon.stub(mailgun, 'sendEmail').resolves('Queued');
+
+ const workspace = await Workspace.create({
+ name: 'existing-workspace',
+ apiKey: 'a'.repeat(96),
+ subscriptionTier: 'free',
+ members: []
+ });
+
+ const res = await stripeWebhook(
+ {
+ type: 'checkout.session.completed',
+ data: {
+ object: {
+ client_reference_id: workspace._id.toString(),
+ customer: 'cus_456',
+ subscription: 'sub_456'
+ }
+ }
+ },
+ { body: '{}', headers: { 'stripe-signature': 'sig_456' } }
+ );
+
+ assert.ok(res.workspace);
+ assert.equal(res.workspace._id.toString(), workspace._id.toString());
+ assert.equal(res.workspace.subscriptionTier, 'pro');
+ assert.equal(res.workspace.stripeCustomerId, 'cus_456');
+ assert.equal(res.workspace.stripeSubscriptionId, 'sub_456');
+ assert.equal(res.workspace.stripeCustomerEmail, 'owner@example.com');
+ assert.notEqual(res.workspace.apiKey, 'a'.repeat(96));
+ assert.ok(/^[a-f0-9]{96}$/.test(res.workspace.apiKey), 'expected rotated hex api key');
+ assert.equal(sendEmailStub.callCount, 1);
+ });
+});
diff --git a/test/updateWorkspaceMember.test.js b/test/updateWorkspaceMember.test.js
index 6cdd19f..d875752 100644
--- a/test/updateWorkspaceMember.test.js
+++ b/test/updateWorkspaceMember.test.js
@@ -60,7 +60,7 @@ describe('updateWorkspaceMember', function() {
authorization: accessToken._id.toString(),
workspaceId: workspace._id,
userId: memberUser._id,
- role: 'admin'
+ roles: ['admin']
});
assert.ok(result.workspace);
diff --git a/test/verifyGithubAccessToken.test.js b/test/verifyGithubAccessToken.test.js
index 97d2ff5..552914b 100644
--- a/test/verifyGithubAccessToken.test.js
+++ b/test/verifyGithubAccessToken.test.js
@@ -17,7 +17,7 @@ describe('verifyGithubAccessToken', function() {
user = await User.create({ email: 'test@example.com', githubUsername: 'test', githubUserId: '5678' });
accessToken = await AccessToken.create({ userId: user._id });
- const task = { sideEffect: async (fn, params) => fn(params) };
+ const task = { sideEffect: async(fn, params) => fn(params) };
verifyGithubAccessToken = verifyGithubAccessTokenFactory({ task, conn: db });
});