From 6045004dc5ccac15621e61133f93a6be1b2bb3de Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Mon, 16 May 2022 00:18:24 +0300 Subject: [PATCH] Backported API tests from the Hapi branch --- lib/api/addresses.js | 9 +- lib/api/certs.js | 2 +- lib/api/users.js | 3 +- lib/message-handler.js | 3 +- lib/user-handler.js | 2 +- package.json | 47 ++-- test/api/addresses-test.js | 214 +++++++++++++++++ test/api/certs-test.js | 111 +++++++++ test/api/dkim-test.js | 62 +++++ test/api/domainaliases-test.js | 59 +++++ test/api/users-test.js | 408 +++++++++++++++++++++++++++++++++ 11 files changed, 890 insertions(+), 30 deletions(-) create mode 100644 test/api/addresses-test.js create mode 100644 test/api/certs-test.js create mode 100644 test/api/dkim-test.js create mode 100644 test/api/domainaliases-test.js create mode 100644 test/api/users-test.js diff --git a/lib/api/addresses.js b/lib/api/addresses.js index 561b4164..6c5064af 100644 --- a/lib/api/addresses.js +++ b/lib/api/addresses.js @@ -148,6 +148,7 @@ module.exports = (db, server, userHandler, settingsHandler) => { name: true, user: true, tags: true, + tagsview: true, targets: true, forwardedDisabled: true } @@ -437,7 +438,7 @@ module.exports = (db, server, userHandler, settingsHandler) => { if (result.value.tags) { addressData.tags = result.value.tags; - addressData.tagsview = result.value.tags; + addressData.tagsview = result.value.tags.map(tag => tag.toLowerCase()); } if (result.value.metaData) { @@ -1073,8 +1074,10 @@ module.exports = (db, server, userHandler, settingsHandler) => { } if (addressData.address === userData.address) { + res.status(400); res.json({ - error: 'Trying to delete main address. Set a new main address first' + error: 'Can not delete main address', + code: 'NotPermitted' }); return next(); } @@ -1509,7 +1512,7 @@ module.exports = (db, server, userHandler, settingsHandler) => { if (result.value.tags) { addressData.tags = result.value.tags; - addressData.tagsview = result.value.tags; + addressData.tagsview = result.value.tags.map(tag => tag.toLowerCase()); } if (result.value.metaData) { diff --git a/lib/api/certs.js b/lib/api/certs.js index 4e47abdf..d2cff05e 100644 --- a/lib/api/certs.js +++ b/lib/api/certs.js @@ -357,7 +357,7 @@ module.exports = (db, server) => { // permissions check req.validate(roles.can(req.role).readAny('certs')); - let cert = new ObjectId(result.value.certs); + let cert = new ObjectId(result.value.cert); let response; try { diff --git a/lib/api/users.js b/lib/api/users.js index cdd9e243..1c089642 100644 --- a/lib/api/users.js +++ b/lib/api/users.js @@ -1629,7 +1629,8 @@ async function getKeyInfo(pubKeyArmored) { let ciphertext = await openpgp.encrypt({ message: await openpgp.createMessage({ text: 'Hello, World!' }), encryptionKeys: pubKey, // for encryption - format: 'armored' + format: 'armored', + config: { minRSABits: 1024 } }); if (/^-----BEGIN PGP MESSAGE/.test(ciphertext)) { diff --git a/lib/message-handler.js b/lib/message-handler.js index d87a33e3..6ce22a3d 100644 --- a/lib/message-handler.js +++ b/lib/message-handler.js @@ -1767,7 +1767,8 @@ class MessageHandler { ciphertext = await openpgp.encrypt({ message: await openpgp.createMessage({ binary: Buffer.concat([Buffer.from(bodyHeaders + '\r\n\r\n'), body]) }), encryptionKeys: pubKey, - format: 'armored' + format: 'armored', + config: { minRSABits: 1024 } }); } catch (err) { return false; diff --git a/lib/user-handler.js b/lib/user-handler.js index f6847ce7..f99c6c58 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -726,7 +726,7 @@ class UserHandler { throw err; } if (success) { - if (userData.validAfter > now) { + if (userData.tempPassword.validAfter && userData.tempPassword.validAfter > now) { let err = new Error('Temporary password is not yet activated'); err.responseCode = 403; err.code = 'TempPasswordNotYetValid'; diff --git a/package.json b/package.json index f3200ff5..196add2b 100644 --- a/package.json +++ b/package.json @@ -19,23 +19,23 @@ }, "license": "EUPL-1.2", "devDependencies": { - "ajv": "8.10.0", + "ajv": "8.11.0", "chai": "4.3.6", - "docsify-cli": "4.4.3", - "eslint": "8.10.0", + "docsify-cli": "4.4.4", + "eslint": "8.15.0", "eslint-config-nodemailer": "1.2.0", "eslint-config-prettier": "8.5.0", - "grunt": "1.4.1", + "grunt": "1.5.3", "grunt-cli": "1.4.3", "grunt-eslint": "24.0.0", "grunt-mocha-test": "0.13.3", "grunt-shell-spawn": "0.4.0", "grunt-wait": "0.3.0", - "imapflow": "1.0.85", - "mailparser": "3.4.0", - "mocha": "9.2.1", + "imapflow": "1.0.95", + "mailparser": "3.5.0", + "mocha": "10.0.0", "request": "2.88.2", - "supertest": "6.2.2" + "supertest": "6.2.3" }, "dependencies": { "@fidm/x509": "1.2.1", @@ -44,36 +44,36 @@ "@root/csr": "0.8.1", "accesscontrol": "2.2.1", "argon2-browser": "1.18.0", - "axios": "0.26.0", + "axios": "0.27.2", "base32.js": "0.1.0", "bcryptjs": "2.4.3", "bull": "3.29.3", - "fido2-lib": "^2.8.1", + "fido2-lib": "3.1.4", "gelf": "2.0.1", "generate-password": "1.7.0", "he": "1.2.0", - "html-to-text": "8.1.0", + "html-to-text": "8.2.0", "humanname": "0.2.2", "iconv-lite": "0.6.3", "ioredfour": "1.2.0-ioredis-06", - "ioredis": "4.28.5", + "ioredis": "5.0.4", "ipaddr.js": "2.0.1", "isemail": "3.2.0", "joi": "17.6.0", "js-yaml": "4.1.0", "key-fingerprint": "1.1.0", "libbase64": "1.2.1", - "libmime": "5.0.0", + "libmime": "5.1.0", "libqp": "1.1.0", - "mailsplit": "5.3.1", + "mailsplit": "5.3.2", "mobileconfig": "2.4.0", "mongo-cursor-pagination": "7.6.1", - "mongodb": "4.4.1", + "mongodb": "4.6.0", "mongodb-extended-json": "1.11.1", - "node-forge": "1.2.1", - "nodemailer": "6.7.2", - "npmlog": "6.0.1", - "openpgp": "5.2.0", + "node-forge": "1.3.1", + "nodemailer": "6.7.5", + "npmlog": "6.0.2", + "openpgp": "5.2.1", "pem-jwk": "2.0.0", "punycode": "2.1.1", "pwnedpasswords": "1.0.6", @@ -83,18 +83,19 @@ "restify-logger": "2.0.1", "saslprep": "1.0.3", "seq-index": "1.1.0", - "smtp-server": "3.10.0", + "smtp-server": "3.11.0", "speakeasy": "2.0.0", - "unixcrypt": "1.0.13", + "unix-crypt-td-js": "1.1.4", + "unixcrypt": "1.1.0", "uuid": "8.3.2", "wild-config": "1.6.0", - "yargs": "17.3.1" + "yargs": "17.5.0" }, "repository": { "type": "git", "url": "git://github.com/wildduck-email/wildduck.git" }, "engines": { - "node": ">=12.0.0" + "node": ">=12.0.0 <17" } } diff --git a/test/api/addresses-test.js b/test/api/addresses-test.js new file mode 100644 index 00000000..26da6155 --- /dev/null +++ b/test/api/addresses-test.js @@ -0,0 +1,214 @@ +/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0, no-console:0 */ + +/* globals before: false, after: false */ + +'use strict'; + +const supertest = require('supertest'); +const chai = require('chai'); + +const expect = chai.expect; +chai.config.includeStack = true; + +const server = supertest.agent('http://localhost:8080'); + +describe('API Users', function () { + this.timeout(10000); // eslint-disable-line no-invalid-this + + let user, user2, forwarded; + + before(async () => { + // ensure that we have an existing user account + const response = await server + .post('/users') + .send({ + username: 'addressuser', + password: 'secretvalue', + address: 'addressuser.addrtest@example.com', + name: 'address user' + }) + .expect(200); + expect(response.body.success).to.be.true; + expect(response.body.id).to.exist; + + user = response.body.id; + + const response2 = await server + .post('/users') + .send({ + username: 'addressuser2', + password: 'secretvalue', + address: 'addressuser2.addrtest@example.com', + name: 'address user 2' + }) + .expect(200); + expect(response2.body.success).to.be.true; + expect(response2.body.id).to.exist; + + user2 = response2.body.id; + }); + + after(async () => { + if (!user) { + return; + } + + const response = await server.delete(`/users/${user}`).expect(200); + expect(response.body.success).to.be.true; + + user = false; + }); + + it('should POST /users/{user}/addresses', async () => { + const response = await server + .post(`/users/${user}/addresses`) + .send({ + address: `user1.1.addrtest@example.com`, + tags: ['TAG1', 'tag2'] + }) + .expect(200); + expect(response.body.success).to.be.true; + + const response2 = await server + .post(`/users/${user2}/addresses`) + .send({ + address: `user2.1.addrtest@example.com` + }) + .expect(200); + expect(response2.body.success).to.be.true; + + const response3 = await server + .post(`/users/${user}/addresses`) + .send({ + address: `user1.2.addrtest@example.com`, + tags: ['TAG2', 'tag3'] + }) + .expect(200); + + expect(response3.body.success).to.be.true; + }); + + it('should GET /addresses', async () => { + const addressListResponse = await server.get(`/addresses`).expect(200); + expect(addressListResponse.body.success).to.be.true; + expect(addressListResponse.body.total).to.gt(3); + }); + + it('should GET /addresses with tags', async () => { + const addressListResponse = await server.get(`/addresses?tags=tag2,tag3`).expect(200); + expect(addressListResponse.body.success).to.be.true; + expect(addressListResponse.body.total).to.equal(2); + }); + + it('should GET /addresses with required tags', async () => { + const addressListResponse = await server.get(`/addresses?requiredTags=tag2,tag3`).expect(200); + expect(addressListResponse.body.success).to.be.true; + expect(addressListResponse.body.total).to.equal(1); + }); + + it('should GET /addresses with a user token', async () => { + const authResponse = await server + .post('/authenticate') + .send({ + username: 'addressuser', + password: 'secretvalue', + token: true + }) + .expect(200); + + expect(authResponse.body.success).to.be.true; + expect(authResponse.body.token).to.exist; + + let token = authResponse.body.token; + + const userListResponse = await server.get(`/addresses?accessToken=${token}`).expect(200); + expect(userListResponse.body.success).to.be.true; + + expect(userListResponse.body.total).to.equal(3); + }); + + it('should GET /users/{user}/addresses', async () => { + const addressListResponse = await server.get(`/users/${user}/addresses`).expect(200); + expect(addressListResponse.body.success).to.be.true; + + expect(addressListResponse.body.results.length).to.equal(3); + expect(addressListResponse.body.results.filter(addr => addr.main).length).to.equal(1); + expect(addressListResponse.body.results.find(addr => addr.main).address).to.equal('addressuser.addrtest@example.com'); + }); + + it('should PUT /users/{user}/addresses/{address}', async () => { + let addressListResponse = await server.get(`/users/${user}/addresses`).expect(200); + expect(addressListResponse.body.success).to.be.true; + let addresses = addressListResponse.body.results; + let address = addresses.find(addr => addr.address === 'user1.1.addrtest@example.com').id; + + const response = await server + .put(`/users/${user}/addresses/${address}`) + .send({ + main: true + }) + .expect(200); + expect(response.body.success).to.be.true; + + addressListResponse = await server.get(`/users/${user}/addresses`).expect(200); + expect(addressListResponse.body.success).to.be.true; + + expect(addressListResponse.body.results.length).to.equal(3); + expect(addressListResponse.body.results.filter(addr => addr.main).length).to.equal(1); + expect(addressListResponse.body.results.find(addr => addr.main).address).to.equal('user1.1.addrtest@example.com'); + }); + + it('should DELETE /users/{user}/addresses/{address} and fail', async () => { + let addressListResponse = await server.get(`/users/${user}/addresses`).expect(200); + expect(addressListResponse.body.success).to.be.true; + let addresses = addressListResponse.body.results; + let address = addresses.find(addr => addr.main).id; + + // trying to delete a main address should fail + const response = await server.delete(`/users/${user}/addresses/${address}`).expect(400); + expect(response.body.code).to.equal('NotPermitted'); + }); + + it('should DELETE /users/{user}/addresses/{address}', async () => { + let addressListResponse = await server.get(`/users/${user}/addresses`).expect(200); + expect(addressListResponse.body.success).to.be.true; + let addresses = addressListResponse.body.results; + let address = addresses.find(addr => addr.address === 'user1.2.addrtest@example.com').id; + + const response = await server.delete(`/users/${user}/addresses/${address}`).expect(200); + expect(response.body.success).to.be.true; + }); + + it('should POST /addresses/forwarded', async () => { + const response = await server + .post(`/addresses/forwarded`) + .send({ + address: `forwarded.1.addrtest@example.com`, + targets: ['andris@ethereal.email'], + tags: ['TAG1', 'tag2'] + }) + .expect(200); + expect(response.body.success).to.be.true; + forwarded = response.body.id; + }); + + it('should GET /addresses with query', async () => { + const addressListResponse = await server.get(`/addresses?query=forwarded.1.addrtest`).expect(200); + expect(addressListResponse.body.success).to.be.true; + expect(addressListResponse.body.total).to.equal(1); + expect(forwarded).to.exist; + }); + + it('should PUT /addresses/forwarded/{address}', async () => { + const response = await server + .put(`/addresses/forwarded/${forwarded}`) + .send({ + tags: ['tAG2', 'tAg3'] + }) + .expect(200); + expect(response.body.success).to.be.true; + + const addressListResponse = await server.get(`/addresses?query=forwarded.1.addrtest`).expect(200); + expect(addressListResponse.body.total).to.equal(1); + }); +}); diff --git a/test/api/certs-test.js b/test/api/certs-test.js new file mode 100644 index 00000000..b00102b0 --- /dev/null +++ b/test/api/certs-test.js @@ -0,0 +1,111 @@ +/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0, no-console:0 */ + +'use strict'; + +const supertest = require('supertest'); +const chai = require('chai'); + +const expect = chai.expect; +chai.config.includeStack = true; + +const server = supertest.agent('http://localhost:8080'); + +describe('API Certs', function () { + let cert; + + this.timeout(10000); // eslint-disable-line no-invalid-this + + it('should POST /certs', async () => { + const response = await server + .post('/certs') + .send({ + servername: 'example.com', + privateKey: `-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUwsIjU4ItLme5 +8bhvsdU3ifpYxCA0sz1GaDnUIeH62JW1XLR5dM9yh1vNCZNeMnZ2YASA7dh1+SNT +J8O46OWaIaDMt3PqBeYs8seuJDiHc1kZtsXWaoKQpeA0LkQsDMTpHm/m2j4lpMeI ++c0GjEcYYYupnbJPxfMW9Xzxmer7p1SduKl5g9x0Izicv6ZvuBnQSpaYQUruqIXU +CdK7RAVUinNU2Bjik/bg773CXvCgEf4QpJQdOMaGB45MWs61x3yv+qoIE7oSE36/ +RsstJC9NbnavFhheitvc8rPJchq7LWMdADD/C/OdYXM3j9AyVdAbCMYpgcOLNdmG ++mGZUKexAgMBAAECggEBAMsH/9NOQY90FS/wZ5zPCzUwymIi5sjjsrmZhHXWz5td +S6ACk4bD3aLhYM1NMgBWD43vGt0eG86YrQkRjUjLly96n8Q73LWaY4jJNZwMnJVF +keVj8W8nvOjkIgwpioyussnzbb3SzjOGB5PDLc/t1XqCu5BlGF/f+pYSNeUoiIET +8xCMLQ7yWyTA7b8mL3Lx+ZJsW4nbugK7FwOnRktK+RQlAPiJsLCzFL+9AZPRrMom +Tq0z5F7iYGet5vdc/3IiEDG0sH7H51Gtjbc0sLT901Faw+fw2Ca9C6tuR30SFuNY +8SPt4ETViVdueSjuAzCnDAqjeHm7H9lWb8GGjXBfjHECgYEA92lxgXb6xwNSQ9Rx +1bCjlpoLNPvbxUYiKBeCNGUor5i1aLWd8hbF6nMCx6/2AzOFCzK1WxKEFfreY3bG +IyiJxxFYCWNoS2+dCM0IbbC4oY1VQzbagv3V8gGCh8to47dUDBV8nNE8Iqi03Hpk +WDVqk3jnzUQ77IYTcPjHCm+uw2sCgYEA3CVlxKiyRV6f/TboSfio8+jCm2Z/eYP1 +UoaWBOwdpFzsOMn74MXtfwQhDm6tf0vjnEDFWWrS0d8lxGV4rSCMJT0o+sgZrs/2 +D+MSZxLyqq+NewsqaEU6Hl13/Ic09xIP2Gz8Fk36ddl6f/MO9j/pAdhxF21jNSIW +/dlxnPvU5FMCgYEAt5TpIUyctlEzmJspwIsqR5SUHkOIBnCM5bzT43bwYqNocILa +6QiW4OloNa3OWP/Ah9eflC1AD2Mv4xP935az7R9keMrnV5pBJoek6meICG/rxU0N +hMc/Giyeo45+jQG6fqDu7xmeioUudq7miEFSjIzZS4mHAXFXOauPXaITRnMCgYA5 +JSwJpJDCGRIGtN4PdZDF38HEfRLSBEMGLRF8LZ50L/rRsvzDGB3SPswl5uz6gkSP +JvETiPs4p2gyVvTAXBaFBB9DGfYwvqLs9NCuGOkNDYz4R6m2b2Hqx/CBiMdi6zlZ +wNCfKZa+SLnXxMw5d9WQORMCNc7u1+6H7o3jZiuZKQKBgF92xcje7ROjMas6bLru +XzoNjcESSn09LuY0Jmm6eq927QPWvr7HGpvHZJCtsoPhSAqoVVL2f4SlDfxko+NG +5RD9W3AE6jSBumZSpGD+3Pm1p/3fRAbrfOcKJai9O9/K3ZQi3aSQgRQgAhAUZ1C4 +gWkJtB9ZKR6nboyDYCFNjfYw +-----END PRIVATE KEY----- +`, + cert: `-----BEGIN CERTIFICATE----- +MIIDADCCAegCCQCPXSqvTzty/zANBgkqhkiG9w0BAQsFADBCMRQwEgYDVQQDDAtl +eGFtcGxlLmNvbTEdMBsGA1UECgwUTXkgQ29tcGFueSBOYW1lIExURC4xCzAJBgNV +BAYTAlVTMB4XDTIxMTEzMDEzMjUyOFoXDTIyMTEzMDEzMjUyOFowQjEUMBIGA1UE +AwwLZXhhbXBsZS5jb20xHTAbBgNVBAoMFE15IENvbXBhbnkgTmFtZSBMVEQuMQsw +CQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANTCwiNT +gi0uZ7nxuG+x1TeJ+ljEIDSzPUZoOdQh4frYlbVctHl0z3KHW80Jk14ydnZgBIDt +2HX5I1Mnw7jo5ZohoMy3c+oF5izyx64kOIdzWRm2xdZqgpCl4DQuRCwMxOkeb+ba +PiWkx4j5zQaMRxhhi6mdsk/F8xb1fPGZ6vunVJ24qXmD3HQjOJy/pm+4GdBKlphB +Su6ohdQJ0rtEBVSKc1TYGOKT9uDvvcJe8KAR/hCklB04xoYHjkxazrXHfK/6qggT +uhITfr9Gyy0kL01udq8WGF6K29zys8lyGrstYx0AMP8L851hczeP0DJV0BsIximB +w4s12Yb6YZlQp7ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAxpun+z6fLOW8xlWx +ej7XUmaI5emFC6wFSaGh3022ASvqS8TOR9qnY9yN+a1notLyqIiKUvoY4uvjPpk8 +OAcMa6e7NRjsBQ/Zry3dxC88CCs4oR0SHeKy/4d3VmqUax5Ufn+X1+in+Sb4FDBD +rDnBTi9TJnAo8JMQ7FwkBFnMsieelX9IXLSsFE0yhz0U97r9B0JFcUEP0OsY9Tz0 +NbFXanIpFENKxoXRzAvq0XlE3p446wIiUlIle/PXQpOx8s5Ae0eEmX0/2DY+1MZs +nBhCzyAvD7Z2TQjrszlbekiIeqTgN/D+r7WWJ3Urpf2NdfLOGNWTDe9cVgZlR85n +rp+tEw== +-----END CERTIFICATE----- +`, + description: 'Some text about this certificate', + sess: '12345', + ip: '127.0.0.1' + }) + .expect(200); + expect(response.body.success).to.be.true; + expect(/^[0-9a-f]{24}$/.test(response.body.id)).to.be.true; + expect(response.body.servername).to.equal('example.com'); + expect(response.body.altNames).to.deep.equal(['example.com']); + cert = response.body.id; + }); + + it('should GET /certs/:cert', async () => { + const response = await server.get(`/certs/${cert}`).expect(200); + + expect(response.body.success).to.be.true; + expect(response.body.id).to.equal(cert); + }); + + it('should GET /certs/resolve/:servername', async () => { + const response = await server.get(`/certs/resolve/example.com`).expect(200); + + expect(response.body.success).to.be.true; + expect(response.body.id).to.equal(cert); + }); + + it('should GET /certs', async () => { + const response = await server.get(`/certs`).expect(200); + + expect(response.body.success).to.be.true; + expect(response.body.results.length).to.gte(1); + expect(response.body.results.find(entry => entry.id === cert)).to.exist; + }); + + it('should DELETE /certs/:cert', async () => { + const response = await server.delete(`/certs/${cert}`).expect(200); + + expect(response.body.success).to.be.true; + }); +}); diff --git a/test/api/dkim-test.js b/test/api/dkim-test.js new file mode 100644 index 00000000..ca83ae1d --- /dev/null +++ b/test/api/dkim-test.js @@ -0,0 +1,62 @@ +/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0, no-console:0 */ + +'use strict'; + +const supertest = require('supertest'); +const chai = require('chai'); + +const expect = chai.expect; +chai.config.includeStack = true; + +const server = supertest.agent('http://localhost:8080'); + +describe('API DKIM', function () { + let dkim; + + this.timeout(10000); // eslint-disable-line no-invalid-this + + it('should POST /dkim', async () => { + const response = await server + .post('/dkim') + .send({ + domain: 'example.com', + selector: 'wildduck', + privateKey: + '-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQDFCPszabID2MLAzzfja3/TboKp4dHUGSkl6hNSly7IRdAhfh6J\nh6vNa+2Y7pyNagX00ukycZ/03O/93X3UxjzX/NpLESo3GwSjp39R4AgdW91nKt7X\nzGoz4ZQELAao+AH1QhJ8vumXFLFc6sS9l7Eu3+cZcAdWij2TCPKrB56tMQIDAQAB\nAoGAAQNfz07e1Hg74CPwpKG74Yly8I6xtoZ+mKxQdx9B5VO+kz2DyK9C6eaBLUUk\n1vFRoIWpH1JIQUkVjtehuwNd8rgPacPZRjSJrGuvwtP/bjzA8m/z/lI0+rfQW7L7\nRfPoi2fl6MJ3KkjNypmVPPNvtJA42aPUDW6SFcXFvSv43gECQQD12RFLlZ5H3W6z\n2ncJXiZha508LoyABkYeb+veCFwicoNEreQrToDgC3GuBRkODsUgRZaVu2sa4tlv\nzO0rwkXRAkEAzSvmAxTvkSf/gMy5mO+sZKeUEtMHibF4LKxw7Men2oADgVTnS38r\nf8uYJteLt3lkfHfV5ezEOERvQutKnMfpYQJBAL7apceUvkyyBWfQWIrIMWl9vpHi\n3SXiOPsWDfjPap8/YNKnYDOSfQ/xMm5S/NFh+/yCqVVSKuKzavOVFiXbapECQQDC\nhWdK7rN/xRNaUz93/2xL9hHOkyNnacoNWOSrqVO8NnicSxoLmyNrw2SbFusRZdde\npuM2XfdffYqbQKd545OhAkBiCm/hUl5+hCJI6xl4wh3aR4h8j/TA6/u4ohPjqYco\nLUPpKBaWeKdwQRbkkpMsVz6lFtpyZlV6V8joGEd8OLMO\n-----END RSA PRIVATE KEY-----', + description: 'Some text about this DKIM certificate', + sess: '12345', + ip: '127.0.0.1' + }) + .expect(200); + expect(response.body.success).to.be.true; + expect(/^[0-9a-f]{24}$/.test(response.body.id)).to.be.true; + dkim = response.body.id; + }); + + it('should GET /dkim/:dkim', async () => { + const response = await server.get(`/dkim/${dkim}`).expect(200); + + expect(response.body.success).to.be.true; + expect(response.body.id).to.equal(dkim); + }); + + it('should GET /dkim/resolve/:domain', async () => { + const response = await server.get(`/dkim/resolve/example.com`).expect(200); + expect(response.body.success).to.be.true; + expect(response.body.id).to.equal(dkim); + }); + + it('should GET /dkim', async () => { + const response = await server.get(`/dkim`).expect(200); + + expect(response.body.success).to.be.true; + expect(response.body.results.length).to.equal(1); + expect(response.body.results.find(entry => entry.id === dkim)).to.exist; + }); + + it('should DELETE /dkim/:dkim', async () => { + const response = await server.delete(`/dkim/${dkim}`).expect(200); + + expect(response.body.success).to.be.true; + }); +}); diff --git a/test/api/domainaliases-test.js b/test/api/domainaliases-test.js new file mode 100644 index 00000000..304e029f --- /dev/null +++ b/test/api/domainaliases-test.js @@ -0,0 +1,59 @@ +/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0, no-console:0 */ + +'use strict'; + +const supertest = require('supertest'); +const chai = require('chai'); + +const expect = chai.expect; +chai.config.includeStack = true; + +const server = supertest.agent('http://localhost:8080'); + +describe('API DomainAliases', function () { + let domainalias; + + this.timeout(10000); // eslint-disable-line no-invalid-this + + it('should POST /domainaliases', async () => { + const response = await server + .post('/domainaliases') + .send({ + domain: 'example.com', + alias: 'alias.example.com', + sess: '12345', + ip: '127.0.0.1' + }) + .expect(200); + expect(response.body.success).to.be.true; + expect(/^[0-9a-f]{24}$/.test(response.body.id)).to.be.true; + domainalias = response.body.id; + }); + + it('should GET /domainaliases/:domainalias', async () => { + const response = await server.get(`/domainaliases/${domainalias}`).expect(200); + + expect(response.body.success).to.be.true; + expect(response.body.id).to.equal(domainalias); + }); + + it('should GET /domainaliases/resolve/:domain', async () => { + const response = await server.get(`/domainaliases/resolve/alias.example.com`).expect(200); + expect(response.body.success).to.be.true; + expect(response.body.id).to.equal(domainalias); + }); + + it('should GET /domainaliases', async () => { + const response = await server.get(`/domainaliases?query=alias.example.com`).expect(200); + + expect(response.body.success).to.be.true; + expect(response.body.results.length).to.gte(1); + expect(response.body.results.find(entry => entry.id === domainalias)).to.exist; + }); + + it('should DELETE /domainaliases/:domainalias', async () => { + const response = await server.delete(`/domainaliases/${domainalias}`).expect(200); + + expect(response.body.success).to.be.true; + }); +}); diff --git a/test/api/users-test.js b/test/api/users-test.js new file mode 100644 index 00000000..e8b60485 --- /dev/null +++ b/test/api/users-test.js @@ -0,0 +1,408 @@ +/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0, no-console:0 */ + +'use strict'; + +const supertest = require('supertest'); +const chai = require('chai'); + +const expect = chai.expect; +chai.config.includeStack = true; + +const server = supertest.agent('http://localhost:8080'); + +describe('API Users', function () { + this.timeout(10000); // eslint-disable-line no-invalid-this + + let user, user2, token; + + it('should POST /users', async () => { + const response = await server + .post('/users') + .send({ + username: 'myuser2', + name: 'John Smith', + address: 'john@example.com', + password: 'secretvalue', + hashedPassword: false, + emptyAddress: false, + language: 'et', + retention: 0, + targets: ['user@example.com', 'https://example.com/upload/email'], + spamLevel: 50, + quota: 1073741824, + recipients: 2000, + forwards: 2000, + requirePasswordChange: false, + imapMaxUpload: 5368709120, + imapMaxDownload: 21474836480, + pop3MaxDownload: 21474836480, + pop3MaxMessages: 300, + imapMaxConnections: 15, + receivedMax: 60, + fromWhitelist: ['user@alternative.domain', '*@example.com'], + tags: ['status:user', 'account:example.com'], + addTagsToAddress: false, + uploadSentMessages: false, + mailboxes: { + sent: 'Saadetud kirjad', + trash: 'Prügikast', + junk: 'Praht', + drafts: 'Mustandid' + }, + disabledScopes: ['imap', 'pop3', 'smtp'], + metaData: { + accountIcon: 'avatar.png' + }, + internalData: { + inTrial: true + }, + pubKey: '-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: Keybase OpenPGP v1.0.0\nComment: https://keybase.io/crypto\n\nxo0EYb0PqAEEANJtI/ivwudfCMmxm+a77Fll5YwSzaaI2nqhcp6pMRJ4l0aafsX3\nBcXUQpsyyELelt2xFtwTNygR4RFWVTn4OoXmO5zFtWCSegAwSyUNK7R/GXi2GTKk\nkYtxUwGcNKBkfY7yAn5KsaeuZL1feDXUGt0YHUmBds5i+6ylI+i4tNbRABEBAAHN\nH1dpbGQgRHVjayA8dGVzdEB3aWxkZHVjay5lbWFpbD7CrQQTAQoAFwUCYb0PqAIb\nLwMLCQcDFQoIAh4BAheAAAoJEJVLs8wf5gSCzBoD/3gz32OfJM1D4IrmKVwyLKxC\n1P81kL7E6ICWD2A0JF9EkojsMHl+/zagwoJejBQhmzTNkFmui5zwmdLGforKl303\ntB0l9vCTb5+eDDHOTUatJrvlw76Fz2ZjIhQTqD4xEM7MWx4xwTGY8bC5roIpdZJD\n9+vr81MXxiq9LZJDBXIyzo0EYb0PqAEEAL/uCTOrAncTRC/3cOQz+kLIzF4A9OTe\n6yxdNWWmx+uo9yJxnBv59Xz9qt8OT8Ih7SD/A4kFCuQqlyd0OFVhyd3KTAQ3CEml\nYOgL5jOE11YrEQjr36xPqO646JZuZIorKDf9PoIyipAMG89BlAoAjSXB1oeQADYn\n5fFLFVm1S7pLABEBAAHCwIMEGAEKAA8FAmG9D6gFCQ8JnAACGy4AqAkQlUuzzB/m\nBIKdIAQZAQoABgUCYb0PqAAKCRBhR/oKY9pg/YqnA/0Szmy4q4TnTBby+j57oXtn\nX/7H/xiaqlCd6bA3lbj3cPK4ybn/gnI4ECsfZfmSFG3T5C9EcZU0e9ByzimH6sxi\nOwPgKFWeJzpl5o8toR7m4wQVhv2NZRUukHe+2JH7nITS0gKeIBHMq2TbufcH6do1\n8s2G7XyLSd5Kkljxx7YmNiKoA/9CQ4l2WkARAFByyEJT9BEE4NBO0m0bI8sg0HRK\nGuP3FKcUu0Pz9R8AExEecofh8s4kaxofa2sbrTcK+L0p0hdR/39JWNuTJbxwEU3C\nA0mZKthjzL7seiRTG7Eny5gGenejRp2x0ziyMEaTgkvf44LPi06XiuE6FGnhElOc\nC7JoIc6NBGG9D6gBBADzW30GOysnqYkexL+bY9o+ai1mL+X58GPLilXJ5WXgEEdf\n8Pg/9jlEOzOnWTTgJAQDGHtwm0duKmK7EJGozLEY94QGOzRjAir6tMF2OYDQIDgj\nAoXavPAc5chFABEVUS12hUPPLoW6YgvaIb3AAZbIM8603BLXTaLGbtZ0z7eYxwAR\nAQABwsCDBBgBCgAPBQJhvQ+oBQkPCZwAAhsuAKgJEJVLs8wf5gSCnSAEGQEKAAYF\nAmG9D6gACgkQ58zrS0TNGbAiVAP/UIxYiSdoHDnBW5qB7onEiUVL5ZFk1Xk+NB0z\n7jOm1oAV0RH8I5NRQBtZ+75xar0vPTX122IdkgpaiNT0wy5Kd/2vz4LKVK9apyJI\neaZ+D7dt5Ipu1p0lWtglqL0xtjOSWuwHFwHuiRYg6eyhGN1RylFpuiKi5KykhrBS\nuBL/BHrk6AP/boRA+KIlb6s19KHNt54Kl8n8G4ZApCwZbUc2jzvbP5DZL5rcjlHd\ns4i4XE+uIJxsiX3iJZtVXzhTKuQlaoEljlhPs/TZYUmxeJ3TdV4o7emWiZ4gE8EQ\nhfxV37ew/GoYm6yME3tAZLIXbv2+bj6HZ4eE8bAMmPvpcQ+UwNJXvnk=\n=dR+x\n-----END PGP PUBLIC KEY BLOCK-----', + encryptMessages: false, + encryptForwarded: false + }) + .expect(200); + + expect(response.body.success).to.be.true; + expect(/^[0-9a-f]{24}$/.test(response.body.id)).to.be.true; + + user = response.body.id; + }); + + it('should POST /authenticate', async () => { + const authResponse = await server + .post('/authenticate') + .send({ + username: 'myuser2', + password: 'secretvalue' + }) + .expect(200); + + expect(authResponse.body.success).to.be.true; + expect(authResponse.body).to.deep.equal({ + success: true, + id: user, + username: 'myuser2', + scope: 'master', + require2fa: false, + requirePasswordChange: false + }); + }); + + it('should POST /authenticate and fail', async () => { + const authResponse = await server + .post('/authenticate') + .send({ + username: 'myuser2', + password: 'invalidpass' + }) + .expect(403); + expect(authResponse.body.code).to.equal('AuthFailed'); + }); + + it('should POST /users and fail - invalid username', async () => { + const response = await server + .post('/users') + .send({ + username: 'ömyuser2', + name: 'John Smith', + password: 'secretvalue' + }) + .expect(400); + + expect(response.body.details.username).to.exist; + }); + + it('should POST /authenticate and request a token', async () => { + const authResponse = await server + .post('/authenticate') + .send({ + username: 'myuser2', + password: 'secretvalue', + token: true + }) + .expect(200); + + expect(authResponse.body.success).to.be.true; + expect(authResponse.body.token).to.exist; + + token = authResponse.body.token; + }); + + it('should POST /users with hashed password', async () => { + const response = await server + .post('/users') + .send({ + username: 'myuser2hash', + name: 'John Smith', + // password: 'test', + password: '$argon2i$v=19$m=16,t=2,p=1$SFpGczI1bWV1RVRpYjNYaw$EBE/WnOGeWint3eQ+SQ7Sg', + hashedPassword: true + }) + .expect(200); + + expect(response.body.success).to.be.true; + user2 = response.body.id; + + const authResponse = await server + .post('/authenticate') + .send({ + username: 'myuser2hash', + password: 'test' + }) + .expect(200); + + expect(authResponse.body.success).to.be.true; + expect(authResponse.body).to.deep.equal({ + success: true, + id: user2, + username: 'myuser2hash', + scope: 'master', + require2fa: false, + requirePasswordChange: false + }); + }); + + it('should GET /users/resolve/{username}', async () => { + const response = await server.get('/users/resolve/myuser2').expect(200); + + expect(response.body).to.deep.equal({ + success: true, + id: user + }); + }); + + it('should GET /users/resolve/{username} and fail', async () => { + const response = await server.get('/users/resolve/myuser2invalid').expect(404); + expect(response.body.code).to.equal('UserNotFound'); + }); + + it('should GET /users', async () => { + const response = await server.get('/users?query=myuser2').expect(200); + + expect(response.body.success).to.be.true; + expect(response.body.results.find(entry => entry.id === user)).to.exist; + }); + + it('should GET /users/{user}', async () => { + let response = await server.get(`/users/${user}`).expect(200); + expect(response.body.success).to.be.true; + expect(response.body.id).to.equal(user); + }); + + it('should GET /users/{user} using a token', async () => { + let response = await server.get(`/users/${user}?accessToken=${token}`).expect(200); + expect(response.body.success).to.be.true; + expect(response.body.id).to.equal(user); + }); + + it('should GET /users/me using a token', async () => { + let response = await server.get(`/users/me?accessToken=${token}`).expect(200); + expect(response.body.success).to.be.true; + expect(response.body.id).to.equal(user); + }); + + it('should GET /users/{user} using a token and fail against other user', async () => { + let response = await server.get(`/users/${user2}?accessToken=${token}`); + expect(response.body.code).to.equal('MissingPrivileges'); + }); + + it('should DELETE /authenticate', async () => { + let response = await server.delete(`/authenticate?accessToken=${token}`).expect(200); + expect(response.body.success).to.be.true; + }); + + it('should DELETE /authenticate with false', async () => { + // token is not valid anymore + await server.delete(`/authenticate?accessToken=${token}`).expect(403); + }); + + it('should PUT /users/{user}', async () => { + const name = 'John Smith 2'; + + // update user data + const response = await server + .put(`/users/${user}`) + .send({ + name + }) + .expect(200); + + expect(response.body.success).to.be.true; + + // request and verify + let getResponse = await server.get(`/users/${user}`); + expect(getResponse.body.success).to.be.true; + expect(getResponse.body.id).to.equal(user); + expect(getResponse.body.name).to.equal(name); + }); + + it('should PUT /users/{user} and renew a token', async () => { + const authResponse1 = await server + .post('/authenticate') + .send({ + username: 'myuser2', + password: 'secretvalue', + token: true + }) + .expect(200); + + expect(authResponse1.body.success).to.be.true; + expect(authResponse1.body.token).to.exist; + + let token1 = authResponse1.body.token; + + const authResponse2 = await server + .post('/authenticate') + .send({ + username: 'myuser2', + password: 'secretvalue', + token: true + }) + .expect(200); + + expect(authResponse2.body.success).to.be.true; + expect(authResponse2.body.token).to.exist; + + let token2 = authResponse2.body.token; + + // try out token 1 + let getResponse1 = await server.get(`/users/me?accessToken=${token1}`).expect(200); + expect(getResponse1.body.success).to.be.true; + expect(getResponse1.body.id).to.equal(user); + + // try out token 2 + let getResponse2 = await server.get(`/users/me?accessToken=${token2}`).expect(200); + expect(getResponse2.body.success).to.be.true; + expect(getResponse2.body.id).to.equal(user); + + // update password using a token + const response = await server + .put(`/users/me?accessToken=${token1}`) + .send({ + password: 'secretvalue' + }) + .expect(200); + + expect(response.body.success).to.be.true; + + // try out token 1, should have been renewed + let getResponse3 = await server.get(`/users/me?accessToken=${token1}`).expect(200); + expect(getResponse3.body.success).to.be.true; + expect(getResponse3.body.id).to.equal(user); + + // try out token 2, should fail as it was not renewed + await server.get(`/users/me?accessToken=${token2}`).expect(403); + }); + + it('should PUT /users/{user}/logout', async () => { + // request logout + const response = await server.put(`/users/${user}/logout`).send({ reason: 'Just because' }).expect(200); + expect(response.body.success).to.be.true; + }); + + it('should POST /users/{user}/quota/reset', async () => { + const response = await server.post(`/users/${user}/quota/reset`).send({}).expect(200); + expect(response.body.success).to.be.true; + + expect(response.body.storageUsed).to.exist; + expect(response.body.previousStorageUsed).to.exist; + }); + + it('should POST /quota/reset', async () => { + const response = await server.post(`/quota/reset`).send({}).expect(200); + expect(response.body.success).to.be.true; + expect(response.body.task).to.exist; + }); + + it('should POST /users/{user}/password/reset', async () => { + const response = await server.post(`/users/${user}/password/reset`).send({}).expect(200); + expect(response.body.success).to.be.true; + + expect(response.body.password).to.exist; + + const authResponse = await server + .post('/authenticate') + .send({ + username: 'myuser2', + password: response.body.password + }) + .expect(200); + + expect(authResponse.body.success).to.be.true; + expect(authResponse.body).to.deep.equal({ + success: true, + id: user, + username: 'myuser2', + scope: 'master', + require2fa: false, + // using a temporary password requires a password change + requirePasswordChange: true + }); + }); + + it('should POST /users/{user}/password/reset using a future date', async () => { + const response = await server + .post(`/users/${user}/password/reset`) + .send({ + validAfter: new Date(Date.now() + 1 * 3600 * 1000).toISOString() + }) + .expect(200); + expect(response.body.success).to.be.true; + + expect(response.body.password).to.exist; + + // password not yet valid + await server + .post('/authenticate') + .send({ + username: 'myuser2', + password: response.body.password + }) + .expect(403); + }); + + it('should DELETE /users/{user}', async () => { + // first set the user password + const passwordUpdateResponse = await server + .put(`/users/${user}`) + .send({ + password: 'secretvalue', + ip: '1.2.3.5' + }) + .expect(200); + + expect(passwordUpdateResponse.body.success).to.be.true; + + // Delete user + const response = await server.delete(`/users/${user}?deleteAfter=${encodeURIComponent(new Date(Date.now() + 3600 * 1000).toISOString())}`).expect(200); + expect(response.body.success).to.be.true; + + expect(response.body.addresses.deleted).to.gte(1); + expect(response.body.task).to.exist; + + // Try to authenticate, should fail + await server + .post('/authenticate') + .send({ + username: 'myuser2', + password: 'secretvalue' + }) + .expect(403); + }); + + it('should GET /users/{user}/restore', async () => { + const response = await server.get(`/users/${user}/restore`).expect(200); + expect(response.body.success).to.be.true; + + expect(response.body.username).to.equal('myuser2'); + expect(response.body.recoverableAddresses).to.deep.equal(['john@example.com']); + }); + + it('should POST /users/{user}/restore', async () => { + const response = await server.post(`/users/${user}/restore`).send({}).expect(200); + expect(response.body.success).to.be.true; + + expect(response.body.addresses.recovered).to.gte(1); + expect(response.body.addresses.main).to.equal('john@example.com'); + }); +});