549 lines
22 KiB
JavaScript
549 lines
22 KiB
JavaScript
/* jslint node:true */
|
|
|
|
import common from './common.js';
|
|
import expect from 'expect.js';
|
|
import fs from 'node:fs';
|
|
import passkeys from '../../passkeys.js';
|
|
import speakeasy from 'speakeasy';
|
|
import superagent from '@cloudron/superagent';
|
|
import tokens from '../../tokens.js';
|
|
import webauthnHelper from '../../test/webauthn-helper.js';
|
|
|
|
const customAvatarSize = fs.readFileSync('./logo.png').length;
|
|
|
|
/* global it:false */
|
|
/* global describe:false */
|
|
/* global before:false */
|
|
/* global after:false */
|
|
|
|
describe('Profile API', function () {
|
|
const { setup, cleanup, serverUrl, owner, user } = common;
|
|
|
|
before(setup);
|
|
after(cleanup);
|
|
|
|
describe('get profile', function () {
|
|
it('fails without token', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile`)
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(401);
|
|
});
|
|
|
|
it('fails with empty token', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile`)
|
|
.query({ access_token: '' })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(401);
|
|
});
|
|
|
|
it('fails with invalid token', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile`)
|
|
.query({ access_token: 'some token' })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(401);
|
|
});
|
|
|
|
it('succeeds', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile`)
|
|
.query({ access_token: owner.token });
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(response.body.username).to.equal(owner.username.toLowerCase());
|
|
expect(response.body.email).to.equal(owner.email.toLowerCase());
|
|
expect(response.body.fallbackEmail).to.equal('');
|
|
expect(response.body.displayName).to.be.a('string');
|
|
expect(response.body.password).to.not.be.ok();
|
|
expect(response.body.salt).to.not.be.ok();
|
|
expect(response.body.language).to.be(null);
|
|
});
|
|
|
|
it('fails with expired token', async function () {
|
|
const token = await tokens.add({ identifier: '0', clientId: 'clientid-0', expires: Date.now() - 2000, allowedIpRanges: '' });
|
|
expect(token.accessToken).to.be.a('string');
|
|
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile`)
|
|
.query({ access_token: token.accessToken })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(401);
|
|
});
|
|
|
|
it('fails with invalid token in auth header', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile`)
|
|
.set('Authorization', 'Bearer ' + 'x' + owner.token)
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(401);
|
|
});
|
|
|
|
it('succeeds with token in auth header', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile`).set('Authorization', 'Bearer ' + owner.token);
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(response.body.username).to.equal(owner.username.toLowerCase());
|
|
expect(response.body.email).to.equal(owner.email.toLowerCase());
|
|
expect(response.body.displayName).to.be.a('string');
|
|
expect(response.body.password).to.not.be.ok();
|
|
expect(response.body.salt).to.not.be.ok();
|
|
});
|
|
});
|
|
|
|
describe('email', function () {
|
|
it('change email fails due to missing token', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/email`)
|
|
.send({ email: 'newemail@example.com' })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(401);
|
|
});
|
|
|
|
it('change email fails due to missing password', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/email`)
|
|
.query({ access_token: owner.token })
|
|
.send({ email: 'newemail@example.com' })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(400);
|
|
});
|
|
|
|
it('change email fails due to invalid password', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/email`)
|
|
.query({ access_token: owner.token })
|
|
.send({ email: 'foo@bar.com', password: 'this is wrong' })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(412);
|
|
});
|
|
|
|
it('change email fails due to invalid email', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/email`)
|
|
.query({ access_token: owner.token })
|
|
.send({ email: 'foo@bar' })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(400);
|
|
});
|
|
|
|
it('change email succeeds', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/email`)
|
|
.query({ access_token: owner.token })
|
|
.send({ email: 'newemail@example.Com', password: owner.password });
|
|
|
|
expect(response.status).to.equal(204);
|
|
|
|
const response2 = await superagent.get(`${serverUrl}/api/v1/profile`)
|
|
.query({ access_token: owner.token });
|
|
|
|
expect(response2.status).to.equal(200);
|
|
expect(response2.body.username).to.equal(owner.username);
|
|
expect(response2.body.email).to.equal('newemail@example.com'); // lower cased
|
|
expect(response2.body.displayName).to.equal('');
|
|
});
|
|
});
|
|
|
|
describe('fallbackEmail', function () {
|
|
it('change fallback email fails due to missing password', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/fallback_email`)
|
|
.query({ access_token: owner.token })
|
|
.send({ fallbackEmail: 'newemail@example.com' })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(400);
|
|
});
|
|
|
|
it('change fallback email fails due to invalid password', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/fallback_email`)
|
|
.query({ access_token: owner.token })
|
|
.send({ fallbackEmail: 'foo@bar.com', password: 'this is wrong' })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(412);
|
|
});
|
|
|
|
it('change fallback email succeeds', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/fallback_email`)
|
|
.query({ access_token: owner.token })
|
|
.send({ fallbackEmail: 'NewFallbackemail@example.com', password: owner.password });
|
|
|
|
expect(response.status).to.equal(204);
|
|
|
|
const response2 = await superagent.get(`${serverUrl}/api/v1/profile`)
|
|
.query({ access_token: owner.token });
|
|
|
|
expect(response2.status).to.equal(200);
|
|
expect(response2.body.username).to.equal(owner.username);
|
|
expect(response2.body.fallbackEmail).to.equal('newfallbackemail@example.com'); // lowercase
|
|
});
|
|
});
|
|
|
|
describe('displayName', function () {
|
|
it('change displayName succeeds', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/display_name`)
|
|
.query({ access_token: owner.token })
|
|
.send({ displayName: 'Agent Smith' });
|
|
|
|
expect(response.status).to.equal(204);
|
|
|
|
const response2 = await superagent.get(`${serverUrl}/api/v1/profile`)
|
|
.query({ access_token: owner.token });
|
|
expect(response2.status).to.equal(200);
|
|
expect(response2.body.username).to.equal(owner.username);
|
|
expect(response2.body.email).to.equal('newemail@example.com'); // lower cased
|
|
expect(response2.body.displayName).to.equal('Agent Smith');
|
|
});
|
|
});
|
|
|
|
describe('password change', function () {
|
|
it('fails due to missing current password', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/password`)
|
|
.query({ access_token: owner.token })
|
|
.send({ newPassword: 'some wrong password' })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(400);
|
|
});
|
|
|
|
it('fails due to missing new password', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/password`)
|
|
.query({ access_token: owner.token })
|
|
.send({ password: owner.password })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(400);
|
|
});
|
|
|
|
it('fails due to wrong password', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/password`)
|
|
.query({ access_token: owner.token })
|
|
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(412);
|
|
});
|
|
|
|
it('fails due to invalid password', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/password`)
|
|
.query({ access_token: owner.token })
|
|
.send({ password: owner.password, newPassword: 'five' })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(400);
|
|
});
|
|
|
|
it('succeeds', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/password`)
|
|
.query({ access_token: owner.token })
|
|
.send({ password: owner.password, newPassword: 'MOre#$%34' });
|
|
|
|
expect(response.status).to.equal(204);
|
|
});
|
|
});
|
|
|
|
describe('2fa login', function () {
|
|
let secret;
|
|
|
|
it('can get secret', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/twofactorauthentication_secret`)
|
|
.query({ access_token: user.token })
|
|
.send({});
|
|
|
|
secret = response.body.secret;
|
|
});
|
|
|
|
it('can enable 2fa', async function () {
|
|
const totpToken = speakeasy.totp({ secret, encoding: 'base32' });
|
|
|
|
await superagent.post(`${serverUrl}/api/v1/profile/twofactorauthentication_enable`)
|
|
.query({ access_token: user.token })
|
|
.send({ totpToken });
|
|
});
|
|
|
|
it('fails due to missing token', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/auth/login`)
|
|
.send({ username: user.username, password: user.password })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(401);
|
|
});
|
|
|
|
it('fails due to wrong token', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/auth/login`)
|
|
.send({ username: user.username, password: user.password, totpToken: '12345' })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(401);
|
|
});
|
|
|
|
it('succeeds', async function () {
|
|
const totpToken = speakeasy.totp({
|
|
secret: secret,
|
|
encoding: 'base32'
|
|
});
|
|
|
|
const response = await superagent.post(`${serverUrl}/api/v1/auth/login`)
|
|
.send({ username: user.username, password: user.password, totpToken: totpToken });
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(response.body).to.be.an(Object);
|
|
expect(response.body.accessToken).to.be.a('string');
|
|
});
|
|
|
|
it('can disable 2fa', async function () {
|
|
await superagent.post(`${serverUrl}/api/v1/profile/twofactorauthentication_disable`)
|
|
.query({ access_token: user.token })
|
|
.send({ password: user.password });
|
|
});
|
|
|
|
it('did disable 2fa', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/auth/login`)
|
|
.send({ username: user.username, password: user.password });
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(response.body).to.be.an(Object);
|
|
expect(response.body.accessToken).to.be.a('string');
|
|
});
|
|
});
|
|
|
|
describe('passkey', function () {
|
|
const origin = `https://${common.dashboardFqdn}`;
|
|
let authenticator;
|
|
|
|
after(async function () {
|
|
// ensure passkey is cleaned up for subsequent tests
|
|
await passkeys.delAll();
|
|
});
|
|
|
|
it('no passkey by default', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile/passkey`)
|
|
.query({ access_token: user.token });
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(response.body.passkey).to.be(null);
|
|
});
|
|
|
|
it('fails to get registration options without token', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/passkey/register/options`)
|
|
.send({})
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(401);
|
|
});
|
|
|
|
it('can get registration options', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/passkey/register/options`)
|
|
.query({ access_token: user.token })
|
|
.send({});
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(response.body.challenge).to.be.a('string');
|
|
expect(response.body.rp).to.be.an(Object);
|
|
});
|
|
|
|
it('can register passkey', async function () {
|
|
authenticator = webauthnHelper.createVirtualAuthenticator();
|
|
|
|
const optionsResponse = await superagent.post(`${serverUrl}/api/v1/profile/passkey/register/options`)
|
|
.query({ access_token: user.token })
|
|
.send({});
|
|
|
|
const credential = await webauthnHelper.createRegistrationResponse(authenticator, optionsResponse.body, origin);
|
|
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/passkey/register`)
|
|
.query({ access_token: user.token })
|
|
.send({ credential, name: 'Test Passkey' });
|
|
|
|
expect(response.status).to.equal(201);
|
|
expect(response.body.id).to.be.a('string');
|
|
});
|
|
|
|
it('passkey is visible', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile/passkey`)
|
|
.query({ access_token: user.token });
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(response.body.passkey).to.be.an(Object);
|
|
expect(response.body.passkey.name).to.be('Test Passkey');
|
|
expect(response.body.passkey.credentialId).to.be(undefined); // private field stripped
|
|
});
|
|
|
|
it('rejects duplicate registration', async function () {
|
|
const optionsResponse = await superagent.post(`${serverUrl}/api/v1/profile/passkey/register/options`)
|
|
.query({ access_token: user.token })
|
|
.send({})
|
|
.ok(() => true);
|
|
|
|
expect(optionsResponse.status).to.equal(409);
|
|
});
|
|
|
|
it('fails to register without credential', async function () {
|
|
// use owner who has no passkey
|
|
const optionsResponse = await superagent.post(`${serverUrl}/api/v1/profile/passkey/register/options`)
|
|
.query({ access_token: owner.token })
|
|
.send({});
|
|
expect(optionsResponse.status).to.equal(200);
|
|
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/passkey/register`)
|
|
.query({ access_token: owner.token })
|
|
.send({})
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(400);
|
|
});
|
|
|
|
it('can disable passkey', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/passkey/disable`)
|
|
.query({ access_token: user.token })
|
|
.send({ password: user.password });
|
|
|
|
expect(response.status).to.equal(204);
|
|
});
|
|
|
|
it('passkey is gone after disable', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile/passkey`)
|
|
.query({ access_token: user.token });
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(response.body.passkey).to.be(null);
|
|
});
|
|
|
|
it('disable fails when no passkey registered', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/passkey/disable`)
|
|
.query({ access_token: user.token })
|
|
.send({ password: user.password })
|
|
.ok(() => true);
|
|
|
|
expect(response.status).to.equal(404);
|
|
});
|
|
});
|
|
|
|
describe('avatar', function () {
|
|
let customAvatarSize = 0;
|
|
|
|
it('empty by default', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile/avatar/${user.id}`).ok(() => true);
|
|
expect(response.status).to.be(404);
|
|
});
|
|
|
|
it('can set custom avatar', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/avatar`)
|
|
.query({ access_token: user.token })
|
|
.attach('avatar', './logo.png');
|
|
|
|
customAvatarSize = fs.readFileSync('./logo.png').length;
|
|
|
|
expect(response.status).to.be(204);
|
|
});
|
|
|
|
it('did set custom avatar', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile/avatar/${user.id}`)
|
|
.ok(() => true);
|
|
|
|
expect(parseInt(response.headers['content-length'])).to.equal(customAvatarSize);
|
|
expect(response.status).to.equal(200);
|
|
});
|
|
|
|
it('can unset custom avatar', async function () {
|
|
const response = await superagent.del(`${serverUrl}/api/v1/profile/avatar`)
|
|
.query({ access_token: user.token });
|
|
|
|
expect(response.status).to.be(204);
|
|
});
|
|
|
|
it('did unset custom avatar', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile/avatar/${user.id}`).ok(() => true);
|
|
expect(response.status).to.be(404);
|
|
});
|
|
});
|
|
|
|
describe('background', function () {
|
|
it('no default', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile/background_image`)
|
|
.query({ access_token: user.token })
|
|
.ok(() => true);
|
|
expect(response.status).to.be(404);
|
|
});
|
|
|
|
it('can set custom background', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/background_image`)
|
|
.query({ access_token: user.token })
|
|
.attach('backgroundImage', './logo.png');
|
|
|
|
expect(response.status).to.be(200);
|
|
});
|
|
|
|
it('did set custom background', async function () {
|
|
const response2 = await superagent.get(`${serverUrl}/api/v1/profile/background_image`)
|
|
.query({ access_token: user.token })
|
|
.ok(() => true);
|
|
|
|
expect(parseInt(response2.headers['content-length'])).to.equal(customAvatarSize);
|
|
expect(response2.status).to.equal(200);
|
|
});
|
|
|
|
it('can unset background', async function () {
|
|
const response = await superagent.del(`${serverUrl}/api/v1/profile/background_image`)
|
|
.query({ access_token: user.token });
|
|
|
|
expect(response.status).to.be(200);
|
|
|
|
const response2 = await superagent.get(`${serverUrl}/api/v1/profile/background_image`)
|
|
.query({ access_token: user.token })
|
|
.ok(() => true);
|
|
expect(response2.status).to.be(404);
|
|
});
|
|
});
|
|
|
|
describe('language', function () {
|
|
it('fails to set unknown language', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/language`)
|
|
.query({ access_token: user.token })
|
|
.send({ language: 'ta' })
|
|
.ok(() => true);
|
|
expect(response.status).to.be(400);
|
|
});
|
|
|
|
it('fails to set bad language', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/language`)
|
|
.query({ access_token: user.token })
|
|
.send({ language: 123 })
|
|
.ok(() => true);
|
|
expect(response.status).to.be(400);
|
|
});
|
|
|
|
it('fails to set unknown language', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/language`)
|
|
.query({ access_token: user.token })
|
|
.send({ language: 'ta' })
|
|
.ok(() => true);
|
|
expect(response.status).to.be(400);
|
|
});
|
|
|
|
it('set valid language', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/language`)
|
|
.query({ access_token: user.token })
|
|
.send({ language: 'en' });
|
|
expect(response.status).to.be(204);
|
|
});
|
|
|
|
it('did set language', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile`).query({ access_token: user.token });
|
|
expect(response.body.language).to.contain('en');
|
|
});
|
|
|
|
it('reset valid language', async function () {
|
|
const response = await superagent.post(`${serverUrl}/api/v1/profile/language`)
|
|
.query({ access_token: user.token })
|
|
.send({ language: '' });
|
|
expect(response.status).to.be(204);
|
|
});
|
|
|
|
it('did reset language', async function () {
|
|
const response = await superagent.get(`${serverUrl}/api/v1/profile`).query({ access_token: user.token });
|
|
expect(response.body.language).to.be(null);
|
|
});
|
|
});
|
|
});
|