diff --git a/src/routes/test/cloudron-test.js b/src/routes/test/cloudron-test.js index 2c8163dea..021c94906 100644 --- a/src/routes/test/cloudron-test.js +++ b/src/routes/test/cloudron-test.js @@ -39,6 +39,15 @@ describe('Cloudron API (pre-activation)', function () { }); }); + it('device is in first time mode', function (done) { + superagent.get(SERVER_URL + '/api/v1/cloudron/status') + .end(function (err, res) { + expect(res.statusCode).to.equal(200); + expect(res.body.activated).to.not.be.ok(); + done(err); + }); + }); + it('fails due to missing setupToken', function (done) { superagent.post(SERVER_URL + '/api/v1/cloudron/activate') .send({ username: '', password: 'somepassword', email: 'admin@foo.bar' }) @@ -117,6 +126,15 @@ describe('Cloudron API (pre-activation)', function () { done(); }); }); + + it('device left first time mode', function (done) { + superagent.get(SERVER_URL + '/api/v1/cloudron/status') + .end(function (err, res) { + expect(res.statusCode).to.equal(200); + expect(res.body.activated).to.be.ok(); + done(); + }); + }); }); describe('Cloudron API (post activation)', function () { diff --git a/src/routes/test/common.js b/src/routes/test/common.js index f8f4a7570..b8dd34c67 100644 --- a/src/routes/test/common.js +++ b/src/routes/test/common.js @@ -3,7 +3,9 @@ const async = require('async'), constants = require('../../constants.js'), database = require('../../database.js'), + delay = require('delay'), expect = require('expect.js'), + mailer = require('../../mailer.js'), server = require('../../server.js'), settings = require('../../settings.js'), superagent = require('superagent'), @@ -12,6 +14,8 @@ const async = require('async'), exports = module.exports = { setup, cleanup, + clearMailQueue, + checkMails, owner: { id: null, @@ -33,7 +37,7 @@ exports = module.exports = { DASHBOARD_DOMAIN: 'test.example.com', DASHBOARD_FQDN: 'my.test.example.com', - serverUrl: `http://localhost:${constants.PORT}` + serverUrl: `http://localhost:${constants.PORT}`, }; function setup(done) { @@ -71,7 +75,6 @@ function setup(done) { expect(result.statusCode).to.equal(201); user.id = result.body.id; - user.token = 'usertoken'; // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) const token = await tokens.add({ identifier: user.id, clientId: 'test-client-id', expires: Date.now() + 10000, name: 'fromtest' }); @@ -90,3 +93,13 @@ function cleanup(done) { server.stop(done); }); } + +function clearMailQueue() { + mailer._mailQueue = []; +} + +async function checkMails(number) { + await delay(1000); + expect(mailer._mailQueue.length).to.equal(number); + clearMailQueue(); +} diff --git a/src/routes/test/profile-test.js b/src/routes/test/profile-test.js index 814ad3b79..8110d3b74 100644 --- a/src/routes/test/profile-test.js +++ b/src/routes/test/profile-test.js @@ -257,4 +257,66 @@ describe('Profile API', function () { expect(response.body.accessToken).to.be.a('string'); }); }); + + describe('app password', function () { + it('cannot add app password with invalid token', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/app_passwords`) + .query({ access_token: user.token + 'xx' }) + .send({ name: 'my-device', identifier: 'someapp' }) + .ok(() => true); + + expect(response.statusCode).to.equal(401); + }); + + it('cannot add app password without name', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/app_passwords`) + .query({ access_token: user.token }) + .send({ identifier: 'someapp' }) + .ok(() => true); + + expect(response.statusCode).to.equal(400); + }); + + let pwd; + it('can add app password', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/app_passwords`) + .query({ access_token: user.token }) + .send({ name: 'my-device', identifier: 'someapp' }); + + expect(response.statusCode).to.equal(201); + expect(response.body.password).to.be.a('string'); + pwd = response.body; + }); + + it('can get app passwords', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/app_passwords`) + .query({ access_token: user.token }); + + expect(response.statusCode).to.equal(200); + expect(response.body.appPasswords).to.be.an(Array); + expect(response.body.appPasswords.length).to.be(1); + expect(response.body.appPasswords[0].name).to.be('my-device'); + expect(response.body.appPasswords[0].identifier).to.be('someapp'); + expect(response.body.appPasswords[0].hashedPassword).to.be(undefined); + expect(response.body.appPasswords[0].password).to.be(undefined); + }); + + it('can get app password', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/app_passwords/${pwd.id}`) + .query({ access_token: user.token }); + + expect(response.statusCode).to.equal(200); + expect(response.body.name).to.be('my-device'); + expect(response.body.identifier).to.be('someapp'); + expect(response.body.hashedPassword).to.be(undefined); + expect(response.body.password).to.be(undefined); + }); + + it('can del app password', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/app_passwords/${pwd.id}`) + .query({ access_token: user.token }); + + expect(response.statusCode).to.equal(204); + }); + }); }); diff --git a/src/routes/test/users-test.js b/src/routes/test/users-test.js index 3bd1acf38..6c923f8ba 100644 --- a/src/routes/test/users-test.js +++ b/src/routes/test/users-test.js @@ -5,970 +5,534 @@ 'use strict'; -var async = require('async'), - constants = require('../../constants.js'), - database = require('../../database.js'), - domains = require('../../domains.js'), - tokendb = require('../../tokendb.js'), +const { execContainer } = require('../../docker.js'); +const common = require('./common.js'), expect = require('expect.js'), - hat = require('../../hat.js'), - groups = require('../../groups.js'), - mailer = require('../../mailer.js'), superagent = require('superagent'), - server = require('../../server.js'), users = require('../../users.js'); -const SERVER_URL = 'http://localhost:' + constants.PORT; - -const DOMAIN_0 = { - domain: 'example-user-test.com', - zoneName: 'example-user-test.com', - config: {}, - provider: 'noop', - fallbackCertificate: null, - tlsConfig: { provider: 'fallback' } -}; - -let AUDIT_SOURCE = { ip: '1.2.3.4' }; - -const USERNAME_0 = 'superaDmIn', PASSWORD = 'Foobar?1337', EMAIL_0 = 'silLY@me.com', EMAIL_0_NEW = 'stupID@me.com', DISPLAY_NAME_0_NEW = 'New Name'; -const USERNAME_1 = 'userTheFirst', EMAIL_1 = 'taO@zen.mac'; -const USERNAME_2 = 'userTheSecond', EMAIL_2 = 'USER@foo.bar', EMAIL_2_NEW = 'happy@ME.com'; -const USERNAME_3 = 'ut', EMAIL_3 = 'user3@FOO.bar'; -const USERNAME_4 = 'importedUser', EMAIL_4 = 'import@external.com'; - -var groupObject; - -function setup(done) { - mailer._mailQueue = []; - - async.series([ - server.start, - database._clear, - domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE), - ], function (error) { - expect(error).to.not.be.ok(); - - groups.create('somegroupname', '', function (error, result) { - expect(error).to.not.be.ok(); - - groupObject = result; - - done(); - }); - }); -} - -function cleanup(done) { - database._clear(function (error) { - expect(!error).to.be.ok(); - - mailer._mailQueue = []; - - server.stop(done); - }); -} - -function checkMails(number, done) { - // mails are enqueued async - setTimeout(function () { - expect(mailer._mailQueue.length).to.equal(number); - mailer._mailQueue = []; - done(); - }, 500); -} - describe('Users API', function () { - var user_0, user_1, user_2, user_4; - var token = null, userToken = null; - var token_1 = hat(8 * 32); + const { setup, cleanup, serverUrl, owner, user } = common; + + const user2 = { + id: null, + username: 'User2', + password: 'Foobar?1339', + email: 'uSer2@cloudron.LoCal', + token: null + }; + + const unnamedUser = { + id: null, + email: 'unnameduser@cloudron.local', + }; + + const userWithPassword = { + id: null, + username: 'userwithpassword', + password: 'Secret123#', + email: 'userwithpassword@cloudron.local', + token: null + }; before(setup); after(cleanup); - describe('owner', function () { - it('device is in first time mode', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/status') - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.activated).to.not.be.ok(); - done(err); - }); + describe('user info', async function () { + it('cannot get userInfo of random user', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/users/baduserid`) + .query({ access_token: owner.token }) + .ok(() => true); + + expect(response.statusCode).to.equal(404); }); - it('create admin fails due to missing parameters', function (done) { - superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: USERNAME_0 }) - .end(function (err, res) { - expect(res.statusCode).to.equal(400); - done(); - }); + it('can get userInfo with token', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/users/${user.id}`) + .query({ access_token: owner.token }); + + expect(response.statusCode).to.equal(200); + expect(response.body.username).to.equal(user.username.toLowerCase()); + expect(response.body.email).to.equal(user.email.toLowerCase()); + expect(response.body.groupIds).to.eql([]); + expect(response.body.role).to.be(users.ROLE_USER); }); - it('create admin fails because only POST is allowed', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/activate') - .end(function (err, res) { - expect(res.statusCode).to.equal(404); - done(); - }); - }); + it('cannot get userInfo with normal user token', async function () { + const reponse = await superagent.get(`${serverUrl}/api/v1/users/${user.id}`) + .query({ access_token: user.token }) + .ok(() => true); - it('create owner', function (done) { - superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: USERNAME_0, password: PASSWORD, email: EMAIL_0 }) - .end(function (err, res) { - expect(err).to.eql(null); - expect(res.statusCode).to.equal(201); - - // stash for later use - token = res.body.token; - - superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) { - expect(error).to.eql(null); - expect(result.status).to.equal(200); - - // stash for further use - user_0 = result.body; - - done(); - }); - }); - }); - - it('device left first time mode', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/status') - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.activated).to.be.ok(); - done(); - }); - }); - }); - - describe('user info', function () { - it('cannot get userInfo by username', function (done) { - superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(404); - - done(); - }); - }); - - it('can get userInfo with token', function (done) { - superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.username).to.equal(USERNAME_0.toLowerCase()); - expect(res.body.email).to.equal(EMAIL_0.toLowerCase()); - expect(res.body.groupIds).to.eql([]); - expect(res.body.role).to.be(users.ROLE_OWNER); - - done(); - }); - }); - - it('cannot get userInfo with expired token', function (done) { - var expires = Date.now() + 2000; // 1 sec - - let token = { - id: 'tid-0', - accessToken: hat(8 * 32), - identifier: user_0.id, - clientId: null, - expires: expires, - scope: 'unused', - name: 'tokenname' - }; - - tokendb.add(token, function (error) { - expect(error).to.not.be.ok(); - - setTimeout(function () { - superagent.get(SERVER_URL + '/api/v1/users/' + user_0.username) - .query({ access_token: token.accessToken }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }, 2000); - }); - }); - - it('can get userInfo with token', function (done) { - superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.username).to.equal(USERNAME_0.toLowerCase()); - expect(res.body.email).to.equal(EMAIL_0.toLowerCase()); - expect(res.body.groupIds).to.eql([]); - expect(res.body.role).to.be(users.ROLE_OWNER); - - done(); - }); - }); - - it('cannot get userInfo only with basic auth', function (done) { - superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id) - .auth(USERNAME_0, PASSWORD) - .end(function (err, res) { - expect(res.statusCode).to.equal(401); - done(); - }); - }); - - it('cannot get userInfo with invalid token (token length)', function (done) { - superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id) - .query({ access_token: 'x' + token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(401); - done(); - }); - }); - - it('cannot get userInfo with invalid token (wrong token)', function (done) { - superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id) - .query({ access_token: token.toUpperCase() }) - .end(function (err, res) { - expect(res.statusCode).to.equal(401); - done(); - }); - }); - - it('can get userInfo with token in auth header', function (done) { - superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id) - .set('Authorization', 'Bearer ' + token) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.username).to.equal(USERNAME_0.toLowerCase()); - expect(res.body.email).to.equal(EMAIL_0.toLowerCase()); - expect(res.body.groupIds).to.eql([]); - expect(res.body.role).to.be(users.ROLE_OWNER); - expect(res.body.displayName).to.be.a('string'); - expect(res.body.password).to.not.be.ok(); - expect(res.body.salt).to.not.be.ok(); - done(); - }); - }); - - it('cannot get userInfo with invalid token in auth header', function (done) { - superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id) - .set('Authorization', 'Bearer ' + 'x' + token) - .end(function (err, res) { - expect(res.statusCode).to.equal(401); - done(); - }); - }); - - it('cannot get userInfo with invalid token (wrong token)', function (done) { - superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id) - .set('Authorization', 'Bearer ' + 'x' + token.toUpperCase()) - .end(function (err, res) { - expect(res.statusCode).to.equal(401); - done(); - }); + expect(reponse.statusCode).to.equal(403); }); }); describe('create user', function () { + it('cannot create user without email', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users`) + .query({ access_token: owner.token }) + .send({ username: user2.username }) + .ok(() => true); - it('cannot create user without email', function (done) { - superagent.post(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .send({ username: USERNAME_1 }) - .end(function (error, result) { - expect(error).to.be.ok(); - expect(result.statusCode).to.equal(400); - done(); - }); + expect(response.statusCode).to.equal(400); }); - it('create second user succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .send({ username: USERNAME_1, email: EMAIL_1 }) - .end(function (error, result) { - expect(error).to.not.be.ok(); - expect(result.statusCode).to.equal(201); + it('create second user succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users`) + .query({ access_token: owner.token }) + .send({ username: user2.username, email: user2.email }); - user_1 = result.body; + expect(response.statusCode).to.equal(201); - // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - tokendb.add({ id: 'tid-3', accessToken: token_1, identifier: user_1.id, clientId: 'test-client-id', expires: Date.now() + 10000, scope: 'unused', name: 'fromtest' }, done); - }); + user2.id = response.body.id; }); - it('create user missing username succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .send({ email: `unnamed${EMAIL_2}` }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - done(); - }); + it('get userInfo succeeds for second user', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/users/${user2.id}`) + .query({ access_token: owner.token }); + + expect(response.statusCode).to.equal(200); + expect(response.body.username).to.equal(user2.username.toLowerCase()); + expect(response.body.email).to.equal(user2.email.toLowerCase()); + expect(response.body.groupIds).to.eql([]); }); - it('create user missing email fails', function (done) { - superagent.post(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .send({ username: USERNAME_2 }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + it('create user missing username succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users`) + .query({ access_token: owner.token }) + .send({ email: unnamedUser.email }); + + expect(response.statusCode).to.equal(201); + unnamedUser.id = response.body.id; }); - it('create user reserved name fails', function (done) { - superagent.post(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .send({ username: 'no-reply' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + it('create user missing email fails', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users`) + .query({ access_token: owner.token }) + .send({ username: 'someusername' }) + .ok(() => true); + + expect(response.statusCode).to.equal(400); }); - it('create user with short name fails', function (done) { - superagent.post(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .send({ username: 'n' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + it('create user reserved name fails', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users`) + .query({ access_token: owner.token }) + .send({ username: 'no-reply', email: 'reserved@cloudron.local' }) + .ok(() => true); + + expect(response.statusCode).to.equal(400); }); - it('create second and third user', function (done) { - mailer._mailQueue = []; - superagent.post(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .send({ username: USERNAME_2, email: EMAIL_2 }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); + it('create user with short name succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users`) + .query({ access_token: owner.token }) + .send({ username: 'n', email: 'reserved@cloudron.local' }); - user_2 = result.body; - - superagent.post(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .send({ username: USERNAME_3, email: EMAIL_3 }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - - done(); - }); - }); + expect(response.statusCode).to.equal(201); }); - it('get userInfo succeeds for second user', function (done) { - superagent.get(SERVER_URL + '/api/v1/users/' + user_2.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body.username).to.equal(USERNAME_2.toLowerCase()); - expect(result.body.email).to.equal(EMAIL_2.toLowerCase()); - expect(result.body.groupIds).to.eql([]); + it('create user with same username should fail', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users`) + .query({ access_token: owner.token }) + .send({ username: user2.username, email: user2.email }) + .ok(() => true); - done(); - }); + expect(response.statusCode).to.equal(409); }); - it('create user with same username should fail', function (done) { - superagent.post(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .send({ username: USERNAME_2, email: EMAIL_0, invite: false }) - .end(function (err, res) { - expect(res.statusCode).to.equal(409); - done(); - }); + it('cannot create user with bad password', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users`) + .query({ access_token: owner.token }) + .send({ username: 'badpassworduser', email: 'badpass@cloudron.local', password:'tooweak' }) + .ok(() => true); + + expect(response.statusCode).to.equal(400); }); - it('cannot create user with bad password', function (done) { - mailer._mailQueue = []; + it('can create user with a password', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users`) + .query({ access_token: owner.token }) + .send({ username: userWithPassword.username, email: userWithPassword.email, password: userWithPassword.password }); - superagent.post(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .send({ username: USERNAME_4, email: EMAIL_4, password: 'tooweak' }) - .end(function (error, result) { - expect(error).to.be.ok(); - expect(result.statusCode).to.equal(400); - done(); - }); + expect(response.statusCode).to.equal(201); + userWithPassword.id = response.body.id; }); - it('can create user with a password', function (done) { - superagent.post(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .send({ username: USERNAME_4, email: EMAIL_4, password: 'Secret1#' }) - .end(function (error, result) { - expect(error).to.not.be.ok(); - expect(result.statusCode).to.equal(201); - - user_4 = result.body; - - userToken = hat(8 * 32); - var expires = Date.now() + 2000; // 1 sec - - tokendb.add({ id: 'tid-2', accessToken: userToken, identifier: user_4.id, clientId: null, expires: expires, scope: 'unused', name: '' }, done); - }); - }); - - it('can get profile of user with pre-set password', function (done) { - superagent.get(SERVER_URL + '/api/v1/profile') - .query({ access_token: userToken }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - - expect(res.body.email).to.be(EMAIL_4); - - done(); - }); - }); - }); - - describe('invite', function () { - - it('reinvite unknown user fails', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1+USERNAME_1 + '/create_invite') - .query({ access_token: token }) - .send({}) - .end(function (err, res) { - expect(err).to.be.an(Error); - expect(res.statusCode).to.equal(404); - done(); - }); - }); - - it('send invite without creating invite fails succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/send_invite') - .query({ access_token: token }) - .send({}) - .end(function (err, res) { - expect(err).to.be.an(Error); - expect(res.statusCode).to.equal(409); - done(); - }); - }); - - it('create invite second user succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/create_invite') - .query({ access_token: token }) - .send({}) - .end(function (err, res) { - expect(err).to.not.be.ok(); - expect(res.statusCode).to.equal(200); - expect(res.body.resetToken).to.be.ok(); - done(); - }); - }); - - it('can send invite', function (done) { - mailer._mailQueue = []; - - superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/send_invite') - .query({ access_token: token }) - .send({}) - .end(function (err, res) { - expect(err).to.be(null); - expect(res.statusCode).to.equal(200); - checkMails(1, done); - }); - }); - }); - - describe('admin status', function () { - it('set second user as admin succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id) - .query({ access_token: token }) - .send({ role: users.ROLE_ADMIN }) - .end(function (err, res) { - expect(res.statusCode).to.equal(204); - - superagent.get(SERVER_URL + '/api/v1/users/' + user_1.id) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.role).to.be(users.ROLE_ADMIN); - - done(); - }); - }); - }); - - it('make self as admin fails', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id) - .query({ access_token: token }) - .send({ role: users.ROLE_ADMIN }) - .end(function (err, res) { - expect(res.statusCode).to.equal(409); - done(); - }); - }); - - it('make self as normal user fails', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id) - .query({ access_token: token }) - .send({ role: users.ROLE_USER }) - .end(function (err, res) { - expect(res.statusCode).to.equal(409); - done(); - }); - }); - - it('remove second user as admin succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id) - .query({ access_token: token }) - .send({ role: users.ROLE_USER }) - .end(function (err, res) { - expect(res.statusCode).to.equal(204); - done(); - }); - }); - }); - - describe('groups', function () { - it('does not list groupIds when listing users', function (done) { - superagent.get(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .end(function (error, res) { - expect(error).to.be(null); - expect(res.statusCode).to.equal(200); - expect(res.body.users).to.be.an('array'); - - res.body.users.forEach(function (user) { - expect('groupIds' in user).to.be(false); - }); - done(); - }); - }); - - it('remove second user from group succeeds', function (done) { - superagent.put(SERVER_URL + '/api/v1/users/' + user_1.id + '/groups') - .query({ access_token: token }) - .send({ groupIds: [ groupObject.id ] }) - .end(function (err, res) { - expect(res.statusCode).to.equal(204); - - superagent.get(SERVER_URL + '/api/v1/users/' + user_1.id) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.groupIds).to.eql([ groupObject.id ]); - - done(); - }); - }); - }); - }); - - describe('list users', function () { - - it('list users fails for normal user', function (done) { - superagent.get(SERVER_URL + '/api/v1/users') - .query({ access_token: token_1 }) - .end(function (error, res) { - expect(res.statusCode).to.equal(403); - done(); - }); - }); - - it('list users succeeds for admin', function (done) { - superagent.get(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .end(function (error, res) { - expect(error).to.be(null); - expect(res.statusCode).to.equal(200); - expect(res.body.users).to.be.an('array'); - expect(res.body.users.length).to.equal(6); - - res.body.users.forEach(function (user) { - expect(user).to.be.an('object'); - expect(user.id).to.be.ok(); - expect(user.email).to.be.ok(); - if (!user.email.startsWith('unnamed')) expect(user.username).to.be.ok(); - expect(user.password).to.not.be.ok(); - expect(user.salt).to.not.be.ok(); - expect(user.groupIds).to.not.be.ok(); - }); - - done(); - }); - }); - }); - - describe('update', function () { - // Change email - it('change email fails due to missing token', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id) - .send({ email: EMAIL_0_NEW }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - it('change email fails due to invalid email', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id) - .query({ access_token: token }) - .send({ email: 'foo@bar' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); - }); - - it('change user succeeds without email nor displayName', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id) - .query({ access_token: token }) - .send({}) - .end(function (error, result) { - expect(result.statusCode).to.equal(204); - done(); - }); - }); - - it('change email succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_2.id) - .query({ access_token: token }) - .send({ email: EMAIL_2_NEW }) - .end(function (error, result) { - expect(result.statusCode).to.equal(204); - - superagent.get(SERVER_URL + '/api/v1/users/' + user_2.id) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.username).to.equal(USERNAME_2.toLowerCase()); - expect(res.body.email).to.equal(EMAIL_2_NEW.toLowerCase()); - expect(res.body.displayName).to.equal(''); - - done(); - }); - }); - }); - - it('change email as admin for other user succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_2.id) - .query({ access_token: token }) - .send({ email: EMAIL_2 }) - .end(function (error, result) { - expect(result.statusCode).to.equal(204); - - superagent.get(SERVER_URL + '/api/v1/users/' + user_2.id) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.username).to.equal(USERNAME_2.toLowerCase()); - expect(res.body.email).to.equal(EMAIL_2.toLowerCase()); - expect(res.body.displayName).to.equal(''); - - done(); - }); - }); - }); - - it('change displayName succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id) - .query({ access_token: token }) - .send({ displayName: DISPLAY_NAME_0_NEW }) - .end(function (error, result) { - expect(result.statusCode).to.equal(204); - - superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.username).to.equal(USERNAME_0.toLowerCase()); - expect(res.body.email).to.equal(EMAIL_0.toLowerCase()); - expect(res.body.displayName).to.equal(DISPLAY_NAME_0_NEW); - - done(); - }); - }); - }); - }); - - describe('password', function () { - // Change password - it('change password fails due to missing token', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id + '/password') - .send({ password: 'youdontsay' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - it('change password fails due to small password', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id + '/password') - .query({ access_token: token }) - .send({ password: 'small' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); - }); - - it('change password succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id + '/password') - .query({ access_token: token }) - .send({ password: 'bigenough' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(204); - done(); - }); - }); - - it('did change the user password', function (done) { - users.verify(user_0.id, 'bigenough', users.AP_WEBADMIN, function (error) { + it('did set password of created user', function (done) { + users.verify(userWithPassword.id, userWithPassword.password, users.AP_WEBADMIN, function (error) { expect(error).to.be(null); done(); }); }); }); - describe('app password', function () { + describe('invite', function () { + it('reinvite unknown user fails', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/randomuserid/create_invite`) + .query({ access_token: owner.token }) + .send({}) + .ok(() => true); - it('cannot add app password with invalid token', function (done) { - superagent.post(SERVER_URL + '/api/v1/app_passwords') - .query({ access_token: token + 'xx' }) - .send({ name: 'my-device', identifier: 'someapp' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); + expect(response.statusCode).to.equal(404); }); - it('cannot add app password without name', function (done) { - superagent.post(SERVER_URL + '/api/v1/app_passwords') - .query({ access_token: token }) - .send({ identifier: 'someapp' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + it('send invite without creating invite fails succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}/send_invite`) + .query({ access_token: owner.token }) + .send({}) + .ok(() => true); + + expect(response.statusCode).to.equal(409); }); - let pwd; - it('can add app password', function (done) { - superagent.post(SERVER_URL + '/api/v1/app_passwords') - .query({ access_token: token }) - .send({ name: 'my-device', identifier: 'someapp' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - expect(result.body.password).to.be.a('string'); - pwd = result.body; - done(); - }); + it('create invite second user succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}/create_invite`) + .query({ access_token: owner.token }) + .send({}); + + expect(response.statusCode).to.equal(200); + expect(response.body.resetToken).to.be.ok(); }); - it('can get app passwords', function (done) { - superagent.get(SERVER_URL + '/api/v1/app_passwords') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body.appPasswords).to.be.an(Array); - expect(result.body.appPasswords.length).to.be(1); - expect(result.body.appPasswords[0].name).to.be('my-device'); - expect(result.body.appPasswords[0].identifier).to.be('someapp'); - expect(result.body.appPasswords[0].hashedPassword).to.be(undefined); - expect(result.body.appPasswords[0].password).to.be(undefined); - done(); - }); - }); + it('can send invite', async function () { + common.clearMailQueue(); - it('can get app password', function (done) { - superagent.get(SERVER_URL + '/api/v1/app_passwords/' + pwd.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body.name).to.be('my-device'); - expect(result.body.identifier).to.be('someapp'); - expect(result.body.hashedPassword).to.be(undefined); - expect(result.body.password).to.be(undefined); - done(); - }); - }); + const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}/send_invite`) + .query({ access_token: owner.token }) + .send({}); - it('can del app password', function (done) { - superagent.del(SERVER_URL + '/api/v1/app_passwords/' + pwd.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(204); - done(); - }); + expect(response.statusCode).to.equal(200); + await common.checkMails(1); }); }); + describe('admin status', function () { + it('set second user as admin succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}`) + .query({ access_token: owner.token }) + .send({ role: users.ROLE_ADMIN }); + + expect(response.statusCode).to.equal(204); + }); + + it('did set second user as admin', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/users/${user.id}`) + .query({ access_token: owner.token }); + + expect(response.statusCode).to.equal(200); + expect(response.body.role).to.be(users.ROLE_ADMIN); + }); + + it('make self as admin fails', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${owner.id}`) + .query({ access_token: owner.token }) + .send({ role: users.ROLE_ADMIN }) + .ok(() => true); + + expect(response.statusCode).to.equal(409); + }); + + it('make self as normal user fails', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${owner.id}`) + .query({ access_token: owner.token }) + .send({ role: users.ROLE_USER }) + .ok(() => true); + + expect(response.statusCode).to.equal(409); + }); + + it('remove second user as admin succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}`) + .query({ access_token: owner.token }) + .send({ role: users.ROLE_USER }); + + expect(response.statusCode).to.equal(204); + }); + + it('normal user cannot change role of admin', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${owner.id}`) + .query({ access_token: user.token }) + .send({ role: users.ROLE_USER }) + .ok(() => true); + + expect(response.statusCode).to.equal(403); + }); + }); + + describe('groups', function () { + it('does not list groupIds when listing users', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/users`) + .query({ access_token: owner.token }); + + expect(response.statusCode).to.equal(200); + expect(response.body.users).to.be.an('array'); + + response.body.users.forEach(function (user) { + expect('groupIds' in user).to.be(false); + }); + }); + }); + + describe('list users', function () { + it('list users fails for normal user', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/users`) + .query({ access_token: user.token }) + .ok(() => true); + + expect(response.statusCode).to.equal(403); + }); + + it('list users succeeds for admin', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/users`) + .query({ access_token: owner.token }); + + expect(response.statusCode).to.equal(200); + expect(response.body.users).to.be.an('array'); + expect(response.body.users.length).to.be.greaterThan(3); + + response.body.users.forEach(function (user) { + expect(user).to.be.an('object'); + expect(user.id).to.be.ok(); + expect(user.email).to.be.ok(); + if (!user.email.startsWith('unnamed')) expect(user.username).to.be.ok(); + expect(user.password).to.not.be.ok(); + expect(user.salt).to.not.be.ok(); + expect(user.groupIds).to.not.be.ok(); + }); + }); + }); + + describe('update', function () { + it('change email fails due to missing token', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user2.id}`) + .send({ email: 'newemail@cloudron.local' }) + .ok(() => true); + + expect(response.statusCode).to.equal(401); + }); + + it('change email fails due to invalid email', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user2.id}`) + .query({ access_token: owner.token }) + .send({ email: 'newemail@cloudron' }) + .ok(() => true); + + expect(response.statusCode).to.equal(400); + }); + + it('change user succeeds without email nor displayName', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user2.id}`) + .query({ access_token: owner.token }) + .send({}); + + expect(response.statusCode).to.equal(204); + }); + + it('change email succeeds', async function () { + user2.email = 'NewEmail@cloudron.local'; + const response = await superagent.post(`${serverUrl}/api/v1/users/${user2.id}`) + .query({ access_token: owner.token }) + .send({ email: user2.email }); + + expect(response.statusCode).to.equal(204); + + const response2 = await superagent.get(`${serverUrl}/api/v1/users/${user2.id}`) + .query({ access_token: owner.token }); + + expect(response2.statusCode).to.equal(200); + expect(response2.body.username).to.equal(user2.username.toLowerCase()); + expect(response2.body.email).to.equal(user2.email.toLowerCase()); + expect(response2.body.displayName).to.equal(''); + }); + + it('cannot change email to existing one', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user2.id}`) + .query({ access_token: owner.token }) + .send({ email: owner.email }) + .ok(() => true); + + expect(response.statusCode).to.equal(409); + }); + + it('can change display name', async function () { + const displayName = 'New name'; + + const response = await superagent.post(`${serverUrl}/api/v1/users/${user2.id}`) + .query({ access_token: owner.token }) + .send({ displayName: displayName }); + + expect(response.statusCode).to.equal(204); + + const response2 = await superagent.get(`${serverUrl}/api/v1/users/${user2.id}`) + .query({ access_token: owner.token }); + + expect(response2.statusCode).to.equal(200); + expect(response2.body.displayName).to.equal(displayName); + }); + }); + + describe('password', function () { + it('change password fails due to missing token', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}/password`) + .send({ password: 'youdontsay' }) + .ok(() => true); + + expect(response.statusCode).to.equal(401); + }); + + it('change password fails due to small password', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}/password`) + .query({ access_token: owner.token }) + .send({ password: 'small' }) + .ok(() => true); + + expect(response.statusCode).to.equal(400); + }); + + it('change password succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}/password`) + .query({ access_token: owner.token }) + .send({ password: 'bigenough' }); + + expect(response.statusCode).to.equal(204); + }); + + it('did change the user password', function (done) { + users.verify(user.id, 'bigenough', users.AP_WEBADMIN, function (error) { + expect(error).to.be(null); + done(); + }); + }); + }); describe('role - user manager', function () { - it('can make second user a usermanager', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id) - .query({ access_token: token }) - .send({ role: users.ROLE_USER_MANAGER }) - .end(function (err, res) { - expect(res.statusCode).to.equal(204); - done(); - }); + it('can make second user a usermanager', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}`) + .query({ access_token: owner.token }) + .send({ role: users.ROLE_USER_MANAGER }); + + expect(response.statusCode).to.equal(204); }); - it('can list users as usermanager', function (done) { - superagent.get(SERVER_URL + '/api/v1/users') - .query({ access_token: token_1 }) - .end(function (error, res) { - expect(res.statusCode).to.equal(200); - done(); - }); + it('can list users as usermanager', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/users`) + .query({ access_token: user.token }); + + expect(response.statusCode).to.equal(200); + expect(response.body.users).to.be.an(Array); + expect(response.body.users.length).to.be.greaterThan(3); }); - it('cannot set password of admin', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id + '/password') - .query({ access_token: token_1 }) + it('cannot set password of admin', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${owner.id}/password`) + .query({ access_token: user.token }) .send({ password: 'bigenough' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(403); - done(); - }); + .ok(() => true); + + expect(response.statusCode).to.equal(403); }); - it('can set password of another', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_2.id + '/password') - .query({ access_token: token_1 }) - .send({ password: 'bigenough' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(204); - done(); - }); + it('can set password of another', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user2.id}/password`) + .query({ access_token: user.token }) + .send({ password: 'bigenough' }); + + expect(response.statusCode).to.equal(204); }); - it('cannot create invite for admin', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id + '/create_invite') - .query({ access_token: token_1 }) - .send({}) - .end(function (err, result) { - expect(result.statusCode).to.equal(403); - done(); - }); + it('cannot create invite for admin', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${owner.id}/create_invite`) + .query({ access_token: user.token }) + .ok(() => true); + + expect(response.statusCode).to.equal(403); }); - it('cannot change admin bit of another', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_2.id) - .query({ access_token: token_1 }) + it('can create invite for another user', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user2.id}/create_invite`) + .query({ access_token: user.token }); + + expect(response.statusCode).to.equal(200); + expect(response.body.resetToken).to.be.a('string'); + expect(response.body.inviteLink).to.be.a('string'); + }); + + it('cannot change admin bit of another', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${owner.id}`) + .query({ access_token: user.token }) .send({ role: users.ROLE_ADMIN }) - .end(function (err, result) { - expect(result.statusCode).to.equal(403); - done(); - }); + .ok(() => true); + + expect(response.statusCode).to.equal(403); }); - it('cannot change admin bit of self', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id) - .query({ access_token: token_1 }) + it('cannot change admin bit of self', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}`) + .query({ access_token: user.token }) .send({ role: users.ROLE_ADMIN }) - .end(function (err, result) { - expect(result.statusCode).to.equal(409); - done(); - }); + .ok(() => true); + + expect(response.statusCode).to.equal(409); }); - it('cannot remove admin', function (done) { - superagent.del(SERVER_URL + '/api/v1/users/' + user_0.id) - .query({ access_token: token_1 }) - .end(function (err, res) { - expect(res.statusCode).to.equal(403); - done(); - }); + it('cannot remove admin', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/users/${owner.id}`) + .query({ access_token: user.token }) + .ok(() => true); + + expect(response.statusCode).to.equal(403); }); - it('can remove normal user', function (done) { - superagent.del(SERVER_URL + '/api/v1/users/' + user_2.id) - .query({ access_token: token_1 }) - .end(function (err, res) { - expect(res.statusCode).to.equal(204); - done(); - }); + it('can remove normal user', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/users/${unnamedUser.id}`) + .query({ access_token: user.token }); + + expect(response.statusCode).to.equal(204); }); }); describe('remove', function () { + it('remove random user fails', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/users/randomid`) + .query({ access_token: owner.token }) + .ok(() => true); - it('remove random user fails', function (done) { - superagent.del(SERVER_URL + '/api/v1/users/randomid') - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(404); - done(); - }); + expect(response.statusCode).to.equal(404); }); - it('user removes himself is not allowed', function (done) { - superagent.del(SERVER_URL + '/api/v1/users/' + user_0.id) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(409); - done(); - }); + it('user cannot removes himself', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/users/${owner.id}`) + .query({ access_token: owner.token }) + .ok(() => true); + + expect(response.statusCode).to.equal(409); }); - it('admin removes normal user', function (done) { - superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(204); - done(); - }); + it('admin removes normal user', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/users/${user2.id}`) + .query({ access_token: owner.token }) + .ok(() => true); + + expect(response.statusCode).to.equal(204); }); - it('admin removes himself should not be allowed', function (done) { - superagent.del(SERVER_URL + '/api/v1/users/' + user_0.id) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(409); - done(); - }); + it('admin removes himself should not be allowed', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/users/${owner.id}`) + .query({ access_token: owner.token }) + .ok(() => true); + + expect(response.statusCode).to.equal(409); }); }); describe('transfer ownership', function () { + it('succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}/make_owner`) + .query({ access_token: owner.token }) + .send({}); - before(function (done) { - superagent.post(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .send({ username: USERNAME_1, email: EMAIL_1 }) - .end(function (error, result) { - expect(error).to.not.be.ok(); - expect(result.statusCode).to.equal(201); - - user_1 = result.body; - token_1 = hat(8 * 32); - - // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - tokendb.add({ id: 'tid-3', accessToken: token_1, identifier: user_1.id, clientId: 'test-client-id', expires: Date.now() + 10000, scope: 'unused', name: 'fromtest' }, done); - }); - }); - - it('succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/make_owner') - .query({ access_token: token }) - .send({}) - .end(function (error, result) { - expect(error).to.not.be.ok(); - expect(result.statusCode).to.equal(204); - - superagent.get(SERVER_URL + '/api/v1/users/' + user_0.id) - .query({ access_token: token_1 }) - .end(function (error, result) { - expect(error).to.not.be.ok(); - expect(result.statusCode).to.equal(200); - expect(result.body.role).to.equal('user'); - - done(); - }); - }); + expect(response.statusCode).to.equal(204); }); }); });