Fix e2e test with minimal configuration.

This commit is contained in:
Clement Michaud 2019-01-30 16:47:03 +01:00
parent 561578dffc
commit c5eb86e0fd
72 changed files with 2065 additions and 1945 deletions

View File

@ -12,7 +12,7 @@ RUN apk --update add --no-cache --virtual \
COPY dist/server /usr/src/server COPY dist/server /usr/src/server
COPY dist/shared /usr/src/shared COPY dist/shared /usr/src/shared
EXPOSE 8080 EXPOSE 9091
VOLUME /etc/authelia VOLUME /etc/authelia
VOLUME /var/lib/authelia VOLUME /var/lib/authelia

View File

@ -1,4 +1,5 @@
import React, { Component } from "react"; import React, { Component } from "react";
import classnames from 'classnames';
import styles from '../../assets/scss/components/AlreadyAuthenticated/AlreadyAuthenticated.module.scss'; import styles from '../../assets/scss/components/AlreadyAuthenticated/AlreadyAuthenticated.module.scss';
import Button from "@material/react-button"; import Button from "@material/react-button";
@ -17,7 +18,7 @@ export type Props = OwnProps & DispatchProps;
class AlreadyAuthenticated extends Component<Props> { class AlreadyAuthenticated extends Component<Props> {
render() { render() {
return ( return (
<div className={styles.container}> <div className={classnames(styles.container, 'already-authenticated-step')}>
<div className={styles.successContainer}> <div className={styles.successContainer}>
<div className={styles.messageContainer}> <div className={styles.messageContainer}>
<span className={styles.username}>{this.props.username}</span> <span className={styles.username}>{this.props.username}</span>

View File

@ -65,7 +65,7 @@ class FirstFactorForm extends Component<Props, State> {
render() { render() {
return ( return (
<div> <div className='first-factor-step'>
<Notification <Notification
show={this.props.error != null} show={this.props.error != null}
className={styles.notification}> className={styles.notification}>
@ -103,8 +103,9 @@ class FirstFactorForm extends Component<Props, State> {
<div className={styles.buttons}> <div className={styles.buttons}>
<Button <Button
onClick={this.onLoginClicked} onClick={this.onLoginClicked}
color="primary" color='primary'
raised={true} raised={true}
id='login-button'
disabled={this.props.formDisabled}> disabled={this.props.formDisabled}>
Login Login
</Button> </Button>

View File

@ -11,7 +11,7 @@ interface Props {
class Notification extends Component<Props> { class Notification extends Component<Props> {
render() { render() {
return (this.props.show) return (this.props.show)
? (<div className={classnames(styles.container, this.props.className)}> ? (<div className={classnames(styles.container, this.props.className, 'notification')}>
{this.props.children} {this.props.children}
</div>) </div>)
: null; : null;

View File

@ -1,4 +1,5 @@
import React, { Component, KeyboardEvent, ChangeEvent, FormEvent } from 'react'; import React, { Component, KeyboardEvent, FormEvent } from 'react';
import classnames from 'classnames';
import TextField, { Input } from '@material/react-text-field'; import TextField, { Input } from '@material/react-text-field';
import Button from '@material/react-button'; import Button from '@material/react-button';
@ -63,7 +64,7 @@ class SecondFactorView extends Component<Props, State> {
<CircleLoader status={u2fStatus}></CircleLoader> <CircleLoader status={u2fStatus}></CircleLoader>
</div> </div>
<div className={styles.registerDeviceContainer}> <div className={styles.registerDeviceContainer}>
<a className={styles.registerDevice} href="#" <a className={classnames(styles.registerDevice, 'register-u2f')} href="#"
onClick={this.props.onRegisterSecurityKeyClicked}> onClick={this.props.onRegisterSecurityKeyClicked}>
Register device Register device
</a> </a>
@ -89,7 +90,7 @@ class SecondFactorView extends Component<Props, State> {
private renderTotp(n: number) { private renderTotp(n: number) {
return ( return (
<div className={styles.methodTotp} key='totp-method'> <div className={classnames(styles.methodTotp, 'second-factor-step')} key='totp-method'>
<div className={styles.methodName}>Option {n} - One-Time Password</div> <div className={styles.methodName}>Option {n} - One-Time Password</div>
<Notification show={this.props.oneTimePasswordVerificationError !== null}> <Notification show={this.props.oneTimePasswordVerificationError !== null}>
{this.props.oneTimePasswordVerificationError} {this.props.oneTimePasswordVerificationError}
@ -99,14 +100,14 @@ class SecondFactorView extends Component<Props, State> {
label="One-Time Password" label="One-Time Password"
outlined={true}> outlined={true}>
<Input <Input
name="totp-token" name='totp-token'
id="totp-token" id='totp-token'
onChange={this.onOneTimePasswordChanged as any} onChange={this.onOneTimePasswordChanged as any}
onKeyPress={this.onTotpKeyPressed} onKeyPress={this.onTotpKeyPressed}
value={this.state.oneTimePassword} /> value={this.state.oneTimePassword} />
</TextField> </TextField>
<div className={styles.registerDeviceContainer}> <div className={styles.registerDeviceContainer}>
<a className={styles.registerDevice} href="#" <a className={classnames(styles.registerDevice, 'register-totp')} href="#"
onClick={this.props.onRegisterOneTimePasswordClicked}> onClick={this.props.onRegisterOneTimePasswordClicked}>
Register device Register device
</a> </a>
@ -115,6 +116,7 @@ class SecondFactorView extends Component<Props, State> {
<Button <Button
color="primary" color="primary"
raised={true} raised={true}
id='totp-button'
onClick={this.onOneTimePasswordValidationRequested} onClick={this.onOneTimePasswordValidationRequested}
disabled={this.props.oneTimePasswordVerificationInProgress}> disabled={this.props.oneTimePasswordVerificationInProgress}>
OK OK

View File

@ -51,9 +51,10 @@ class ForgotPasswordView extends Component<Props, State> {
<TextField <TextField
className={styles.field} className={styles.field}
outlined={true} outlined={true}
id="username"
label="Username"> label="Username">
<Input <Input
id="username"
name="username"
onChange={this.onUsernameChanged} onChange={this.onUsernameChanged}
onKeyPress={this.onKeyPressed} onKeyPress={this.onKeyPressed}
value={this.state.username} value={this.state.username}
@ -64,6 +65,7 @@ class ForgotPasswordView extends Component<Props, State> {
<Button <Button
onClick={this.onPasswordResetRequested} onClick={this.onPasswordResetRequested}
color="primary" color="primary"
id="next-button"
raised={true} raised={true}
className={styles.buttonConfirm} className={styles.buttonConfirm}
disabled={this.props.disabled}> disabled={this.props.disabled}>
@ -75,6 +77,7 @@ class ForgotPasswordView extends Component<Props, State> {
onClick={this.props.onCancelClicked} onClick={this.props.onCancelClicked}
color="primary" color="primary"
raised={true} raised={true}
id="cancel-button"
className={styles.buttonCancel}> className={styles.buttonCancel}>
Cancel Cancel
</Button> </Button>

View File

@ -1,4 +1,5 @@
import React, { Component } from "react"; import React, { Component } from "react";
import classnames from 'classnames';
import Button from "@material/react-button"; import Button from "@material/react-button";
@ -54,10 +55,10 @@ class OneTimePasswordRegistrationView extends Component<Props> {
Register your device by scanning the barcode or adding the key. Register your device by scanning the barcode or adding the key.
</div> </div>
<div className={styles.secretContainer}> <div className={styles.secretContainer}>
<div className={styles.qrcodeContainer}> <div className={classnames(styles.qrcodeContainer, 'qrcode')}>
<QRCode value={secret.otpauth_url} size={180} level="Q"></QRCode> <QRCode value={secret.otpauth_url} size={180} level="Q"></QRCode>
</div> </div>
<div className={styles.base32Container}>{secret.base32_secret}</div> <div className={classnames(styles.base32Container, 'base32-secret')}>{secret.base32_secret}</div>
</div> </div>
<div className={styles.loginButtonContainer}> <div className={styles.loginButtonContainer}>
<Button <Button

View File

@ -92,6 +92,7 @@ class ResetPasswordView extends Component<Props, State> {
<Input <Input
type="password" type="password"
key="password1" key="password1"
name="password1"
value={this.state.password1} value={this.state.password1}
onChange={this.onPassword1Changed} onChange={this.onPassword1Changed}
disabled={this.props.disabled}/> disabled={this.props.disabled}/>
@ -104,6 +105,7 @@ class ResetPasswordView extends Component<Props, State> {
<Input <Input
type="password" type="password"
key="password2" key="password2"
name="password2"
value={this.state.password2} value={this.state.password2}
onKeyPress={this.onKeyPressed} onKeyPress={this.onKeyPressed}
onChange={this.onPassword2Changed} onChange={this.onPassword2Changed}
@ -114,6 +116,7 @@ class ResetPasswordView extends Component<Props, State> {
<Button <Button
onClick={this.onResetClicked} onClick={this.onResetClicked}
color="primary" color="primary"
id="reset-button"
raised={true} raised={true}
disabled={this.props.disabled} disabled={this.props.disabled}
className={classnames(styles.button, styles.buttonReset)}> className={classnames(styles.button, styles.buttonReset)}>
@ -124,6 +127,7 @@ class ResetPasswordView extends Component<Props, State> {
<Button <Button
onClick={this.props.onCancelClicked} onClick={this.props.onCancelClicked}
color="primary" color="primary"
id="cancel-button"
raised={true} raised={true}
className={classnames(styles.button, styles.buttonCancel)}> className={classnames(styles.button, styles.buttonCancel)}>
Cancel Cancel

View File

@ -2,9 +2,11 @@
# Authelia minimal configuration # # Authelia minimal configuration #
############################################################### ###############################################################
port: 9091
authentication_backend: authentication_backend:
file: file:
path: /etc/authelia/users_database.yml path: ./users_database.yml
session: session:
secret: unsecure_session_secret secret: unsecure_session_secret
@ -96,7 +98,7 @@ notifier:
username: test username: test
password: password password: password
secure: false secure: false
host: 'smtp' host: 127.0.0.1
port: 1025 port: 1025
sender: admin@example.com sender: admin@example.com

View File

@ -3,7 +3,7 @@
############################################################### ###############################################################
# The port to listen on # The port to listen on
port: 8080 port: 9091
# Log level # Log level
# #
@ -42,7 +42,7 @@ authentication_backend:
# production. # production.
ldap: ldap:
# The url of the ldap server # The url of the ldap server
url: ldap://openldap url: ldap://127.0.0.1
# The base dn for every entries # The base dn for every entries
base_dn: dc=example,dc=com base_dn: dc=example,dc=com
@ -198,7 +198,7 @@ session:
# The redis connection details # The redis connection details
redis: redis:
host: redis host: 127.0.0.1
port: 6379 port: 6379
password: authelia password: authelia
@ -229,7 +229,7 @@ storage:
# Settings to connect to mongo server # Settings to connect to mongo server
mongo: mongo:
url: mongodb://mongo url: mongodb://127.0.0.1
database: authelia database: authelia
auth: auth:
username: authelia username: authelia
@ -258,6 +258,6 @@ notifier:
username: test username: test
password: password password: password
secure: false secure: false
host: 'smtp' host: 127.0.0.1
port: 1025 port: 1025
sender: admin@example.com sender: admin@example.com

View File

@ -1,5 +1,9 @@
version: '2' version: '2'
services: {} # services: {}
networks: networks:
authelianet: authelianet:
external: true driver: bridge
ipam:
config:
- subnet: 192.168.240.0/24
gateway: 192.168.240.1

View File

@ -111,7 +111,7 @@ http {
server_name public.example.com; server_name public.example.com;
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://authelia:8080/api/verify; set $upstream_verify http://192.168.240.1:9091/api/verify;
set $upstream_endpoint http://nginx-backend; set $upstream_endpoint http://nginx-backend;
set $upstream_headers http://httpbin:8000/headers; set $upstream_headers http://httpbin:8000/headers;
@ -179,7 +179,7 @@ http {
server_name admin.example.com; server_name admin.example.com;
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://authelia:8080/api/verify; set $upstream_verify http://192.168.240.1:9091/api/verify;
set $upstream_endpoint http://nginx-backend; set $upstream_endpoint http://nginx-backend;
ssl_certificate /etc/ssl/server.crt; ssl_certificate /etc/ssl/server.crt;
@ -229,7 +229,7 @@ http {
server_name dev.example.com; server_name dev.example.com;
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://authelia:8080/api/verify; set $upstream_verify http://192.168.240.1:9091/api/verify;
set $upstream_endpoint http://nginx-backend; set $upstream_endpoint http://nginx-backend;
ssl_certificate /etc/ssl/server.crt; ssl_certificate /etc/ssl/server.crt;
@ -279,7 +279,7 @@ http {
server_name mx1.mail.example.com mx2.mail.example.com; server_name mx1.mail.example.com mx2.mail.example.com;
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://authelia:8080/api/verify; set $upstream_verify http://192.168.240.1:9091/api/verify;
set $upstream_endpoint http://nginx-backend; set $upstream_endpoint http://nginx-backend;
ssl_certificate /etc/ssl/server.crt; ssl_certificate /etc/ssl/server.crt;
@ -329,7 +329,7 @@ http {
server_name single_factor.example.com; server_name single_factor.example.com;
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://authelia:8080/api/verify; set $upstream_verify http://192.168.240.1:9091/api/verify;
set $upstream_endpoint http://nginx-backend; set $upstream_endpoint http://nginx-backend;
set $upstream_headers http://httpbin:8000/headers; set $upstream_headers http://httpbin:8000/headers;
@ -400,7 +400,7 @@ http {
server_name authelia.example.com; server_name authelia.example.com;
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://authelia:8080; set $upstream_endpoint http://192.168.240.1:9091;
ssl_certificate /etc/ssl/server.crt; ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key; ssl_certificate_key /etc/ssl/server.key;

View File

@ -17,6 +17,14 @@ if (program.production) {
options['production'] = true; options['production'] = true;
} }
html = ejs.renderFile(__dirname + '/nginx.conf.ejs', options, (err, conf) => { const templatePath = __dirname + '/nginx.conf.ejs';
fs.writeFileSync(__dirname + '/nginx.conf', conf); const outputPath = __dirname + '/nginx.conf';
html = ejs.renderFile(templatePath, options, (err, conf) => {
try {
var fd = fs.openSync(outputPath, 'w');
fs.writeFileSync(fd, conf);
} catch (e) {
fs.writeFileSync(outputPath, conf);
}
}); });

2898
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -93,7 +93,7 @@
"grunt-run": "^0.8.0", "grunt-run": "^0.8.0",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"jquery": "^3.2.1", "jquery": "^3.2.1",
"mocha": "^5.0.5", "mocha": "^5.2.0",
"mockdate": "^2.0.1", "mockdate": "^2.0.1",
"nodemon": "^1.18.9", "nodemon": "^1.18.9",
"nyc": "^13.1.0", "nyc": "^13.1.0",
@ -107,7 +107,7 @@
"tmp": "0.0.33", "tmp": "0.0.33",
"ts-node": "^6.0.1", "ts-node": "^6.0.1",
"tslint": "^5.2.0", "tslint": "^5.2.0",
"typescript": "^2.3.2", "typescript": "^2.9.2",
"typescript-json-schema": "^0.23.0" "typescript-json-schema": "^0.23.0"
}, },
"nyc": { "nyc": {

View File

@ -4,6 +4,7 @@ var program = require('commander');
program program
.version('0.0.1') .version('0.0.1')
.command('start', 'Start development environment.') .command('start', 'Start development environment.')
.command('build', 'Build production version of Authelia from source.') .command('build', 'Build production version of Authelia from source.')
.command('clean', 'Clean the production version of Authelia.') .command('clean', 'Clean the production version of Authelia.')
@ -12,7 +13,6 @@ program
.command('build-docker', 'Build Docker image containing production version of Authelia.') .command('build-docker', 'Build Docker image containing production version of Authelia.')
.command('publish-docker', 'Publish Docker image containing production version of Authelia to Dockerhub.') .command('publish-docker', 'Publish Docker image containing production version of Authelia to Dockerhub.')
.parse(process.argv);
program.parse(process.argv);

View File

@ -1,8 +1,39 @@
#!/bin/bash #!/usr/bin/env node
# Render the production version of the nginx portal configuration var program = require('commander');
./example/compose/nginx/portal/render.js --production var execSync = require('child_process').execSync;
var spawn = require('child_process').spawn;
./scripts/utils/prepare-environment.sh program
.option('-c, --config <config>', 'Configuration file to run Authelia with.')
.option('--no-watch', 'Disable hot reload.')
.parse(process.argv);
./node_modules/.bin/nodemon -e yml --exec 'node dist/server/src/index.js config.yml' let config = 'config.yml'; // set default config file.
if (program.config) {
config = program.config;
}
// Render the production version of the nginx portal configuration
execSync('./example/compose/nginx/portal/render.js --production');
// Prepare the environment
execSync('./scripts/utils/prepare-environment.sh');
var server;
if (program.watch) {
server = spawn('./node_modules/.bin/nodemon',
['-e', 'yml', '--ignore', './users_database*.yml', '--exec', `node dist/server/src/index.js ${config}`]);
}
else {
server = spawn('/usr/bin/env', ['node', 'dist/server/src/index.js', config]);
}
server.stdout.on('data', (data) => {
process.stdout.write(`${data}`);
});
server.stderr.on('data', (data) => {
process.stderr.write(`${data}`);
});

View File

@ -1,10 +1,20 @@
#!/bin/bash #!/bin/bash
config_path=$1
if [ "$config_path" == "" ];
then
echo "Please provide a configuration file."
exit 1
fi
./example/compose/nginx/portal/render.js ./example/compose/nginx/portal/render.js
./scripts/utils/prepare-environment.sh ./scripts/utils/prepare-environment.sh
server_watcher="./node_modules/.bin/nodemon -e yml,js,ts,json --exec ./scripts/run-dev-server.sh 2>&1 /tmp/authelia-server.log" ./node_modules/.bin/typescript-json-schema -o server/src/lib/configuration/Configuration.schema.json --strictNullChecks --required server/tsconfig.json Configuration
server_watcher="./node_modules/.bin/nodemon -e yml,js,ts,json --exec ./scripts/run-dev-server.sh $config_path 2>&1 /tmp/authelia-server.log"
client_watcher="cd client && BROWSER=none npm run start > /tmp/authelia-client.log" client_watcher="cd client && BROWSER=none npm run start > /tmp/authelia-client.log"
./node_modules/.bin/concurrently "$server_watcher" "$client_watcher" ./node_modules/.bin/concurrently "$server_watcher" "$client_watcher"

View File

@ -1,3 +1,25 @@
#!/bin/bash #!/usr/bin/env node
TS_NODE_PROJECT=server/tsconfig.json ./node_modules/.bin/mocha --colors --require ts-node/register server/src/**/*.spec.ts var program = require('commander');
var spawn = require('child_process').spawn;
program
.option('--with-server', 'Spawn Authelia before running the tests.')
.parse(process.argv);
mocha = spawn('./node_modules/.bin/mocha', ['--exit', '--colors', '--require', 'ts-node/register', ...program.args], {
env: {
...process.env,
TS_NODE_PROJECT: 'test/tsconfig.json',
WITH_SERVER: (program.withServer) ? 'y' : 'n',
}
});
mocha.stdout.on('data', (data) => {
process.stdout.write(`${data}`);
});
mocha.stderr.on('data', (data) => {
process.stderr.write(`${data}`);
});
// TS_NODE_PROJECT=server/tsconfig.json ./node_modules/.bin/mocha --colors --require ts-node/register server/src/**/*.spec.ts

View File

@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
./node_modules/.bin/mocha --colors --require ts-node/register $* WITH_SERVER=n TS_NODE_PROJECT=test/tsconfig.json ./node_modules/.bin/mocha --exit --colors --require ts-node/register $*

View File

@ -1,3 +1,3 @@
#!/bin/sh #!/bin/sh
./node_modules/.bin/ts-node -P ./server/tsconfig.json ./server/src/index.ts ./config.yml ./node_modules/.bin/ts-node -P ./server/tsconfig.json ./server/src/index.ts $*

View File

@ -1,14 +1,4 @@
#!/bin/bash #!/bin/bash
bridge_exists=`docker network ls | grep " authelianet " | wc -l`
if [ "$bridge_exists" != "1" ];
then
docker network create -d bridge --subnet 192.168.240.0/24 --gateway 192.168.240.1 authelianet
else
echo "Bridge authelianet already exist."
fi
./scripts/dc-dev.sh up -d ./scripts/dc-dev.sh up -d
./scripts/dc-dev.sh kill -s SIGHUP nginx-portal ./scripts/dc-dev.sh kill -s SIGHUP nginx-portal

View File

@ -1,9 +1,9 @@
import WithDriver from "../helpers/with-driver"; import WithDriver from "../helpers/context/WithDriver";
import LoginAndRegisterTotp from "../helpers/login-and-register-totp"; import LoginAndRegisterTotp from "../helpers/LoginAndRegisterTotp";
import SeeNotification from "../helpers/see-notification"; import SeeNotification from "../helpers/SeeNotification";
import VisitPage from "../helpers/visit-page"; import VisitPage from "../helpers/VisitPage";
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click'; import FillLoginPageWithUserAndPasswordAndClick from '../helpers/FillLoginPageAndClick';
import ValidateTotp from "../helpers/validate-totp"; import ValidateTotp from "../helpers/ValidateTotp";
import {CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN} from '../../shared/UserMessages'; import {CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN} from '../../shared/UserMessages';
/* /*

View File

@ -1,6 +1,6 @@
import WithDriver from '../helpers/with-driver'; import WithDriver from '../helpers/context/WithDriver';
import fullLogin from '../helpers/full-login'; import fullLogin from '../helpers/FullLogin';
import loginAndRegisterTotp from '../helpers/login-and-register-totp'; import loginAndRegisterTotp from '../helpers/LoginAndRegisterTotp';
describe("Connection retry when mongo fails or restarts", function() { describe("Connection retry when mongo fails or restarts", function() {
this.timeout(30000); this.timeout(30000);

View File

@ -0,0 +1,14 @@
import SeleniumWebdriver from "selenium-webdriver";
export default async function(driver: any) {
const content = await driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.tagName('body')), 5000).getText();
if (content.indexOf('This is a very important secret') > - 1) {
return;
}
else {
throw new Error('Secret page is not accessible.');
}
}

8
test/helpers/ClickOn.ts Normal file
View File

@ -0,0 +1,8 @@
import SeleniumWebdriver, { WebDriver, Locator } from "selenium-webdriver";
export default async function(driver: WebDriver, locator: Locator) {
const el = await driver.wait(
SeleniumWebdriver.until.elementLocated(locator), 5000);
await el.click();
};

View File

@ -0,0 +1,8 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
export default async function(driver: WebDriver, linkText: string) {
const element = await driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.linkText(linkText)), 5000)
await element.click();
};

View File

@ -0,0 +1,9 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
export default async function(driver: WebDriver, fieldName: string, text: string) {
const element = await driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.name(fieldName)), 5000)
await element.sendKeys(text);
};

View File

@ -0,0 +1,17 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
export default async function(
driver: WebDriver,
username: string,
password: string,
keepMeLoggedIn: boolean = false) {
await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.id("username")), 5000)
await driver.findElement(SeleniumWebdriver.By.id("username")).sendKeys(username);
await driver.findElement(SeleniumWebdriver.By.id("password")).sendKeys(password);
if (keepMeLoggedIn) {
await driver.findElement(SeleniumWebdriver.By.id("keep_me_logged_in")).click();
return;
}
await driver.findElement(SeleniumWebdriver.By.tagName("button")).click();
};

13
test/helpers/FullLogin.ts Normal file
View File

@ -0,0 +1,13 @@
import VisitPage from "./VisitPage";
import FillLoginPageWithUserAndPasswordAndClick from "./FillLoginPageAndClick";
import ValidateTotp from "./ValidateTotp";
import WaitRedirected from "./WaitRedirected";
import { WebDriver } from "selenium-webdriver";
// Validate the two factors!
export default async function(driver: WebDriver, url: string, user: string, secret: string) {
await VisitPage(driver, `https://login.example.com:8080/?rd=${url}`);
await FillLoginPageWithUserAndPasswordAndClick(driver, user, 'password');
await ValidateTotp(driver, secret);
await WaitRedirected(driver, "https://admin.example.com:8080/secret.html");
}

View File

@ -0,0 +1,32 @@
import Bluebird = require("bluebird");
import Fs = require("fs");
import Request = require("request-promise");
export async function GetLinkFromFile() {
const data = await Bluebird.promisify(Fs.readFile)("/tmp/authelia/notification.txt")
const regexp = new RegExp(/Link: (.+)/);
const match = regexp.exec(data.toLocaleString());
if (match == null) {
throw new Error('No match');
}
return match[1];
};
export async function GetLinkFromEmail() {
const data = await Request({
method: "GET",
uri: "http://localhost:8085/messages",
json: true
});
const messageId = data[data.length - 1].id;
const data2 = await Request({
method: "GET",
uri: `http://localhost:8085/messages/${messageId}.html`
});
const regexp = new RegExp(/<a href="(.+)" class="button">Continue<\/a>/);
const match = regexp.exec(data2);
if (match == null) {
throw new Error('No match');
}
return match[1];
};

View File

@ -0,0 +1,5 @@
import SeleniumWebDriver, { WebDriver } from "selenium-webdriver";
export default async function(driver: WebDriver) {
await driver.wait(SeleniumWebDriver.until.elementLocated(SeleniumWebDriver.By.className('second-factor-step')));
}

View File

@ -0,0 +1,10 @@
import RegisterTotp from './RegisterTotp';
import LoginAs from './LoginAs';
import { WebDriver } from 'selenium-webdriver';
import IsSecondFactorStage from './IsSecondFactorStage';
export default async function(driver: WebDriver, user: string, email?: boolean) {
await LoginAs(driver, user);
await IsSecondFactorStage(driver);
return await RegisterTotp(driver, email);
}

8
test/helpers/LoginAs.ts Normal file
View File

@ -0,0 +1,8 @@
import VisitPage from "./VisitPage";
import FillLoginPageAndClick from './FillLoginPageAndClick';
import { WebDriver } from "selenium-webdriver";
export default async function(driver: WebDriver, user: string) {
await VisitPage(driver, "https://login.example.com:8080/");
await FillLoginPageAndClick(driver, user, "password");
}

View File

@ -0,0 +1,13 @@
import SeleniumWebdriver = require("selenium-webdriver");
import {GetLinkFromFile, GetLinkFromEmail} from './GetIdentityLink';
export default async function(driver: SeleniumWebdriver.WebDriver, email?: boolean){
await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("register-totp")), 5000)
await driver.findElement(SeleniumWebdriver.By.className("register-totp")).click();
await driver.sleep(500);
const link = (email) ? await GetLinkFromEmail() : await GetLinkFromFile();
await driver.get(link);
await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("base32-secret")), 5000);
return await driver.findElement(SeleniumWebdriver.By.className("base32-secret")).getText();
};

View File

@ -0,0 +1,9 @@
import SeleniumWebdriver, { ThenableWebDriver, WebDriver } from "selenium-webdriver";
import Assert = require("assert");
export default async function(driver: WebDriver, type: string, message: string) {
await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("notification")), 5000)
const notificationEl = driver.findElement(SeleniumWebdriver.By.className("notification"));
const txt = await notificationEl.getText();
Assert.equal(message, txt);
}

View File

@ -0,0 +1,16 @@
import Speakeasy from "speakeasy";
import SeleniumWebdriver, { WebDriver } from 'selenium-webdriver';
export default async function(driver: WebDriver, secret: string) {
const token = Speakeasy.totp({
secret: secret,
encoding: "base32"
});
await driver.wait(SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.id("totp-token")), 5000)
await driver.findElement(SeleniumWebdriver.By.id("totp-token")).sendKeys(token);
const el = await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.id('totp-button')));
el.click();
}

View File

@ -0,0 +1,6 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
export default async function(driver: WebDriver, url: string, timeout: number = 5000) {
await driver.get(url)
await driver.wait(SeleniumWebdriver.until.urlIs(url), timeout);
}

View File

@ -0,0 +1,5 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
export default async function(driver: WebDriver, url: string, timeout: number = 5000) {
await driver.wait(SeleniumWebdriver.until.urlIs(url), timeout);
}

View File

@ -1,12 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
import Bluebird = require("bluebird");
export default function(driver: any) {
return driver.findElement(
SeleniumWebdriver.By.tagName('h1')).getText()
.then(function(content: string) {
return (content.indexOf('Secret') > -1)
? Bluebird.resolve()
: Bluebird.reject(new Error("Secret is not accessible."));
})
}

View File

@ -1,13 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function(driver: any, buttonText: string) {
return driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.tagName("button")), 5000)
.then(function () {
return driver
.findElement(SeleniumWebdriver.By.tagName("button"))
.findElement(SeleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]"))
.click();
});
};

View File

@ -1,10 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function(driver: any, linkText: string) {
return driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.linkText(linkText)), 5000)
.then(function (el) {
return el.click();
});
};

View File

@ -0,0 +1,34 @@
import WithAutheliaRunning from "./WithAutheliaRunning";
import WithDriver from "./WithDriver";
let running = false;
interface AutheliaSuiteType {
(description: string, cb: (this: Mocha.ISuiteCallbackContext) => void): Mocha.ISuite;
only: (description: string, cb: (this: Mocha.ISuiteCallbackContext) => void) => Mocha.ISuite;
}
function AutheliaSuiteBase(description: string,
context: (description: string, ctx: (this: Mocha.ISuiteCallbackContext) => void) => Mocha.ISuite,
cb: (this: Mocha.ISuiteCallbackContext) => void) {
if (!running && process.env['WITH_SERVER'] == 'y') {
WithAutheliaRunning();
running = true;
}
return context('Suite: ' + description, function(this: Mocha.ISuiteCallbackContext) {
WithDriver.call(this);
cb.call(this);
});
}
const AutheliaSuite = <AutheliaSuiteType>function(description: string, cb: (this: Mocha.ISuiteCallbackContext) => void) {
return AutheliaSuiteBase(description, describe, cb);
}
AutheliaSuite.only = function(description: string, cb: (this: Mocha.ISuiteCallbackContext) => void) {
return AutheliaSuiteBase(description, describe.only, cb);
}
export default AutheliaSuite as AutheliaSuiteType;

View File

@ -0,0 +1,23 @@
import ChildProcess from 'child_process';
export default function WithAutheliaRunning(waitTimeout: number = 3000) {
before(function() {
this.timeout(5000);
const authelia = ChildProcess.spawn(
'./scripts/authelia-scripts',
['serve', '--no-watch', '--config', 'config.minimal.yml'],
{detached: true});
this.authelia = authelia;
const waitPromise = new Promise((resolve, reject) => setTimeout(() => resolve(), waitTimeout));
return waitPromise;
});
after(function() {
this.timeout(1000);
// Kill the group of processes.
process.kill(-this.authelia.pid);
});
}

View File

@ -0,0 +1,20 @@
require("chromedriver");
import chrome from 'selenium-webdriver/chrome';
import SeleniumWebdriver from "selenium-webdriver";
export default function() {
const options = new chrome.Options().addArguments('headless');
beforeEach(function() {
const driver = new SeleniumWebdriver.Builder()
.forBrowser("chrome")
.setChromeOptions(
new chrome.Options().headless())
.build();
this.driver = driver;
});
afterEach(function() {
this.driver.quit();
});
}

View File

@ -1,10 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function(driver: any, fieldName: string, text: string) {
return driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.name(fieldName)), 5000)
.then(function (el) {
return el.sendKeys(text);
});
};

View File

@ -1,29 +0,0 @@
import Bluebird = require("bluebird");
import SeleniumWebdriver = require("selenium-webdriver");
export default function(
driver: any,
username: string,
password: string,
keepMeLoggedIn: boolean = false) {
return driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.id("username")), 5000)
.then(() => {
return driver.findElement(SeleniumWebdriver.By.id("username"))
.sendKeys(username);
})
.then(() => {
return driver.findElement(SeleniumWebdriver.By.id("password"))
.sendKeys(password);
})
.then(() => {
if (keepMeLoggedIn) {
return driver.findElement(SeleniumWebdriver.By.id("keep_me_logged_in"))
.click();
}
return Bluebird.resolve();
})
.then(() => {
return driver.findElement(SeleniumWebdriver.By.tagName("button"))
.click();
});
};

View File

@ -1,12 +0,0 @@
import VisitPage from "./visit-page";
import FillLoginPageWithUserAndPasswordAndClick from "./fill-login-page-and-click";
import ValidateTotp from "./validate-totp";
import WaitRedirected from "./wait-redirected";
// Validate the two factors!
export default function(driver: any, url: string, user: string, secret: string) {
return VisitPage(driver, `https://login.example.com:8080/?rd=${url}`)
.then(() => FillLoginPageWithUserAndPasswordAndClick(driver, user, 'password'))
.then(() => ValidateTotp(driver, secret))
.then(() => WaitRedirected(driver, "https://admin.example.com:8080/secret.html"));
}

View File

@ -1,34 +0,0 @@
import Bluebird = require("bluebird");
import Fs = require("fs");
import Request = require("request-promise");
export function GetLinkFromFile(): Bluebird<string> {
return Bluebird.promisify(Fs.readFile)("/tmp/authelia/notification.txt")
.then(function (data: any) {
const regexp = new RegExp(/Link: (.+)/);
const match = regexp.exec(data);
const link = match[1];
return Bluebird.resolve(link);
});
};
export function GetLinkFromEmail(): Bluebird<string> {
return Request({
method: "GET",
uri: "http://localhost:8085/messages",
json: true
})
.then(function (data: any) {
const messageId = data[data.length - 1].id;
return Request({
method: "GET",
uri: `http://localhost:8085/messages/${messageId}.html`
});
})
.then(function (data: any) {
const regexp = new RegExp(/<a href="(.+)" class="button">Continue<\/a>/);
const match = regexp.exec(data);
const link = match[1];
return Bluebird.resolve(link);
});
};

View File

@ -1,10 +0,0 @@
import RegisterTotp from './register-totp';
import WaitRedirected from './wait-redirected';
import LoginAs from './login-as';
import Bluebird = require("bluebird");
export default function(driver: any, user: string, email?: boolean): Bluebird<string> {
return LoginAs(driver, user)
.then(() => WaitRedirected(driver, "https://login.example.com:8080/secondfactor"))
.then(() => RegisterTotp(driver, email));
}

View File

@ -1,7 +0,0 @@
import VisitPage from "./visit-page";
import FillLoginPageAndClick from './fill-login-page-and-click';
export default function(driver: any, user: string) {
return VisitPage(driver, "https://login.example.com:8080/")
.then(() => FillLoginPageAndClick(driver, user, "password"));
}

View File

@ -1,23 +0,0 @@
import Bluebird = require("bluebird");
import SeleniumWebdriver = require("selenium-webdriver");
import {GetLinkFromFile, GetLinkFromEmail} from '../helpers/get-identity-link';
export default function(driver: any, email?: boolean): Bluebird<string> {
return driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("register-totp")), 5000)
.then(function () {
return driver.findElement(SeleniumWebdriver.By.className("register-totp")).click();
})
.then(function () {
if(email) return GetLinkFromEmail();
else return GetLinkFromFile();
})
.then(function (link: string) {
return driver.get(link);
})
.then(function () {
return driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.id("secret")), 5000);
})
.then(function () {
return driver.findElement(SeleniumWebdriver.By.id("secret")).getText();
});
};

View File

@ -1,18 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
import Assert = require("assert");
export default function(driver: any, type: string, message: string) {
const notificationEl = driver.findElement(SeleniumWebdriver.By.className("notification"));
return driver.wait(SeleniumWebdriver.until.elementIsVisible(notificationEl), 5000)
.then(function () {
return notificationEl.getText();
})
.then(function (txt: string) {
Assert.equal(message, txt);
return notificationEl.getAttribute("class");
})
.then(function (classes: string) {
Assert(classes.indexOf(type) > -1, "Class '" + type + "' not found in notification element.");
return driver.sleep(500);
});
}

View File

@ -1,20 +0,0 @@
import Speakeasy = require("speakeasy");
import SeleniumWebdriver = require("selenium-webdriver");
import ClickOnButton from "./click-on-button";
export default function(driver: any, secret: string) {
const token = Speakeasy.totp({
secret: secret,
encoding: "base32"
});
return driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.id("token")), 5000)
.then(function () {
return driver.findElement(SeleniumWebdriver.By.id("token"))
.sendKeys(token);
})
.then(function () {
return ClickOnButton(driver, "Sign in");
});
}

View File

@ -1,8 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function(driver: any, url: string, timeout: number = 5000) {
return driver.get(url)
.then(function () {
return driver.wait(SeleniumWebdriver.until.urlIs(url), timeout);
});
}

View File

@ -1,5 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function(driver: any, url: string, timeout: number = 5000) {
return driver.wait(SeleniumWebdriver.until.urlIs(url), timeout);
}

View File

@ -1,13 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function() {
before(function() {
this.driver = new SeleniumWebdriver.Builder()
.forBrowser("chrome")
.build();
})
after(function() {
this.driver.quit();
});
}

View File

@ -1,10 +1,10 @@
import Bluebird = require("bluebird"); import Bluebird = require("bluebird");
import LoginAndRegisterTotp from "../helpers/login-and-register-totp"; import LoginAndRegisterTotp from "../helpers/LoginAndRegisterTotp";
import VisitPage from "../helpers/visit-page"; import VisitPage from "../helpers/VisitPage";
import FillLoginPageWithUserAndPasswordAndClick from "../helpers/fill-login-page-and-click"; import FillLoginPageWithUserAndPasswordAndClick from "../helpers/FillLoginPageAndClick";
import WithDriver from "../helpers/with-driver"; import WithDriver from "../helpers/context/WithDriver";
import ValidateTotp from "../helpers/validate-totp"; import ValidateTotp from "../helpers/ValidateTotp";
import WaitRedirected from "../helpers/wait-redirected"; import WaitRedirected from "../helpers/WaitRedirected";
describe("Keep me logged in", function() { describe("Keep me logged in", function() {
this.timeout(15000); this.timeout(15000);

View File

@ -1,15 +0,0 @@
require("chromedriver");
import ChildProcess = require('child_process');
import Bluebird = require("bluebird");
const execAsync = Bluebird.promisify(ChildProcess.exec);
before(function() {
this.timeout(1000);
return execAsync("cp users_database.yml users_database.test.yml");
});
after(function() {
this.timeout(1000);
return execAsync("rm users_database.test.yml");
});

View File

@ -1,30 +0,0 @@
import WithDriver from '../helpers/with-driver';
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
import VisitPage from '../helpers/visit-page';
import SeeNotification from '../helpers/see-notification';
import {AUTHENTICATION_FAILED} from '../../shared/UserMessages';
/**
* When user provides bad password,
* Then he gets a notification message.
*/
describe("Provide bad password", function() {
WithDriver();
describe('failed login as john', function() {
before(function() {
this.timeout(10000);
const driver = this.driver;
return VisitPage(driver, "https://login.example.com:8080/")
.then(function() {
return FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'bad_password');
});
});
it('should get a notification message', function() {
this.timeout(10000);
return SeeNotification(this.driver, "error", AUTHENTICATION_FAILED);
});
});
});

View File

@ -1,39 +0,0 @@
require("chromedriver");
import WithDriver from '../helpers/with-driver';
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
import VisitPage from '../helpers/visit-page';
import ValidateTotp from '../helpers/validate-totp';
import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
import seeNotification from "../helpers/see-notification";
import {AUTHENTICATION_TOTP_FAILED} from '../../shared/UserMessages';
/**
* Given john has registered a TOTP secret,
* When he fails the TOTP challenge,
* Then he gets a notification message.
*/
describe('Fail TOTP challenge', function() {
this.timeout(10000);
WithDriver();
describe('successfully login as john', function() {
before(function() {
return LoginAndRegisterTotp(this.driver, "john", true);
});
describe('fail second factor', function() {
before(function() {
const BAD_TOKEN = "125478";
const driver = this.driver;
return VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html")
.then(() => FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password'))
.then(() => ValidateTotp(driver, BAD_TOKEN));
});
it("get a notification message", function() {
return seeNotification(this.driver, "error", AUTHENTICATION_TOTP_FAILED);
});
});
});
});

View File

@ -0,0 +1,23 @@
import ChildProcess from 'child_process';
import Bluebird from "bluebird";
import AutheliaSuite from "../helpers/context/AutheliaSuite";
import BadPassword from "./scenarii/BadPassword";
import RegisterTotp from './scenarii/RegisterTotp';
import ResetPassword from './scenarii/ResetPassword';
import TOTPValidation from './scenarii/TOTPValidation';
const execAsync = Bluebird.promisify(ChildProcess.exec);
AutheliaSuite('Minimal configuration', function() {
this.timeout(10000);
beforeEach(function() {
return execAsync("cp users_database.example.yml users_database.yml");
});
describe('Bad password', BadPassword);
describe('Reset password', ResetPassword);
describe('TOTP Registration', RegisterTotp);
describe('TOTP Validation', TOTPValidation);
});

View File

@ -1,32 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
import WithDriver from '../helpers/with-driver';
import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
/**
* Given the user logs in as john,
* When he register a TOTP token,
* Then he reach a page containing the secret as string an qrcode
*/
describe('Registering TOTP', function() {
this.timeout(10000);
WithDriver();
describe('successfully login as john', function() {
before('register successfully', function() {
this.timeout(10000);
return LoginAndRegisterTotp(this.driver, "john", true);
})
it("should see generated qrcode", function() {
this.driver.findElement(
SeleniumWebdriver.By.id("qrcode"),
5000);
});
it("should see generated secret", function() {
this.driver.findElement(
SeleniumWebdriver.By.id("secret"),
5000);
});
});
});

View File

@ -1,42 +0,0 @@
require("chromedriver");
import Bluebird = require("bluebird");
import ChildProcess = require("child_process");
import WithDriver from '../helpers/with-driver';
import VisitPage from '../helpers/visit-page';
import ClickOnLink from '../helpers/click-on-link';
import ClickOnButton from '../helpers/click-on-button';
import WaitRedirect from '../helpers/wait-redirected';
import FillField from "../helpers/fill-field";
import {GetLinkFromEmail} from "../helpers/get-identity-link";
import FillLoginPageAndClick from "../helpers/fill-login-page-and-click";
const execAsync = Bluebird.promisify(ChildProcess.exec);
describe('Reset password', function() {
this.timeout(10000);
WithDriver();
after(() => {
return execAsync("cp users_database.yml users_database.test.yml");
})
describe('click on reset password', function() {
it("should reset password for john", function() {
return VisitPage(this.driver, "https://login.example.com:8080/")
.then(() => ClickOnLink(this.driver, "Forgot password\?"))
.then(() => WaitRedirect(this.driver, "https://login.example.com:8080/password-reset/request"))
.then(() => FillField(this.driver, "username", "john"))
.then(() => ClickOnButton(this.driver, "Reset Password"))
.then(() => this.driver.sleep(1000)) // Simulate the time to read it from mailbox.
.then(() => GetLinkFromEmail())
.then((link) => VisitPage(this.driver, link))
.then(() => FillField(this.driver, "password1", "newpass"))
.then(() => FillField(this.driver, "password2", "newpass"))
.then(() => ClickOnButton(this.driver, "Reset Password"))
.then(() => WaitRedirect(this.driver, "https://login.example.com:8080/"))
.then(() => FillLoginPageAndClick(this.driver, "john", "newpass"))
.then(() => WaitRedirect(this.driver, "https://login.example.com:8080/secondfactor"))
});
});
});

View File

@ -0,0 +1,23 @@
import FillLoginPageWithUserAndPasswordAndClick from '../../helpers/FillLoginPageAndClick';
import VisitPage from '../../helpers/VisitPage';
import SeeNotification from '../../helpers/SeeNotification';
import {AUTHENTICATION_FAILED} from '../../../shared/UserMessages';
export default function() {
/**
* When user provides bad password,
* Then he gets a notification message.
*/
describe('failed login as john in first factor', function() {
beforeEach(async function() {
this.timeout(10000);
await VisitPage(this.driver, "https://login.example.com:8080/")
await FillLoginPageWithUserAndPasswordAndClick(this.driver, 'john', 'bad_password');
});
it('should get a notification message', async function () {
this.timeout(10000);
await SeeNotification(this.driver, "error", AUTHENTICATION_FAILED);
});
});
}

View File

@ -0,0 +1,30 @@
import SeleniumWebdriver from "selenium-webdriver";
import LoginAndRegisterTotp from '../../helpers/LoginAndRegisterTotp';
/**
* Given the user logs in as john,
* When he register a TOTP token,
* Then he reach a page containing the secret as string an qrcode
*/
export default function() {
describe('successfully login as john', function() {
beforeEach('register successfully', async function() {
this.timeout(10000);
await LoginAndRegisterTotp(this.driver, "john", true);
})
it("should see generated qrcode", async function() {
await this.driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.className("qrcode")),
5000);
});
it("should see generated secret", async function() {
await this.driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.className("base32-secret")),
5000);
});
});
};

View File

@ -0,0 +1,29 @@
import VisitPage from '../../helpers/VisitPage';
import ClickOnLink from '../../helpers/ClickOnLink';
import ClickOn from '../../helpers/ClickOn';
import WaitRedirected from '../../helpers/WaitRedirected';
import FillField from "../../helpers/FillField";
import {GetLinkFromEmail} from "../../helpers/GetIdentityLink";
import FillLoginPageAndClick from "../../helpers/FillLoginPageAndClick";
import SeleniumWebDriver from 'selenium-webdriver';
import IsSecondFactorStage from "../../helpers/IsSecondFactorStage";
export default function() {
it("should reset password for john", async function() {
await VisitPage(this.driver, "https://login.example.com:8080/");
await ClickOnLink(this.driver, "Forgot password\?");
await WaitRedirected(this.driver, "https://login.example.com:8080/forgot-password");
await FillField(this.driver, "username", "john");
await ClickOn(this.driver, SeleniumWebDriver.By.id('next-button'));
await this.driver.sleep(500); // Simulate the time it takes to receive the e-mail.
const link = await GetLinkFromEmail();
await VisitPage(this.driver, link);
await FillField(this.driver, "password1", "newpass");
await FillField(this.driver, "password2", "newpass");
await ClickOn(this.driver, SeleniumWebDriver.By.id('reset-button'));
await WaitRedirected(this.driver, "https://login.example.com:8080/");
await FillLoginPageAndClick(this.driver, "john", "newpass");
await IsSecondFactorStage(this.driver);
});
}

View File

@ -0,0 +1,51 @@
import FillLoginPageWithUserAndPasswordAndClick from '../../helpers/FillLoginPageAndClick';
import WaitRedirected from '../../helpers/WaitRedirected';
import VisitPage from '../../helpers/VisitPage';
import ValidateTotp from '../../helpers/ValidateTotp';
import AccessSecret from "../../helpers/AccessSecret";
import LoginAndRegisterTotp from '../../helpers/LoginAndRegisterTotp';
import SeeNotification from '../../helpers/SeeNotification';
import { AUTHENTICATION_TOTP_FAILED } from '../../../shared/UserMessages';
export default function() {
/**
* Given john has registered a TOTP secret,
* When he validates the TOTP second factor,
* Then he has access to secret page.
*/
describe('Successfully pass second factor with TOTP', function() {
beforeEach(async function() {
const secret = await LoginAndRegisterTotp(this.driver, "john", true);
if (!secret) throw new Error('No secret!');
await VisitPage(this.driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
await FillLoginPageWithUserAndPasswordAndClick(this.driver, 'john', 'password');
await ValidateTotp(this.driver, secret);
await WaitRedirected(this.driver, "https://admin.example.com:8080/secret.html");
});
it("should access the secret", async function() {
await AccessSecret(this.driver);
});
});
/**
* Given john has registered a TOTP secret,
* When he fails the TOTP challenge,
* Then he gets a notification message.
*/
describe('Fail validation of second factor with TOTP', function() {
beforeEach(async function() {
await LoginAndRegisterTotp(this.driver, "john", true);
const BAD_TOKEN = "125478";
await VisitPage(this.driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
await FillLoginPageWithUserAndPasswordAndClick(this.driver, 'john', 'password');
await ValidateTotp(this.driver, BAD_TOKEN);
});
it("get a notification message", async function() {
await SeeNotification(this.driver, "error", AUTHENTICATION_TOTP_FAILED);
});
});
}

View File

@ -1,46 +0,0 @@
require("chromedriver");
import Bluebird = require("bluebird");
import WithDriver from '../helpers/with-driver';
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
import WaitRedirected from '../helpers/wait-redirected';
import VisitPage from '../helpers/visit-page';
import ValidateTotp from '../helpers/validate-totp';
import AccessSecret from "../helpers/access-secret";
import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
/**
* Given john has registered a TOTP secret,
* When he validates the TOTP second factor,
* Then he has access to secret page.
*/
describe('Validate TOTP factor', function() {
this.timeout(10000);
WithDriver();
describe('successfully login as john', function() {
before(function() {
const that = this;
return LoginAndRegisterTotp(this.driver, "john", true)
.then(function(secret: string) {
that.secret = secret;
})
});
describe('validate second factor', function() {
before(function() {
const secret = this.secret;
if(!secret) return Bluebird.reject(new Error("No secret!"));
const driver = this.driver;
return VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html")
.then(() => FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password'))
.then(() => ValidateTotp(driver, secret))
.then(() => WaitRedirected(driver, "https://admin.example.com:8080/secret.html"));
});
it("should access the secret", function() {
return AccessSecret(this.driver);
});
});
});
});

19
test/tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es6",
"allowJs": true,
"skipLibCheck": false,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"lib": [
"esnext"
]
}
}

9
test/types/mocha.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import { WebDriver } from "selenium-webdriver";
/*
declare module 'mocha' {
interface ISuiteCallbackContext {
driver1: WebDriver;
}
}
*/

View File

@ -0,0 +1,29 @@
###############################################################
# Users Database #
###############################################################
# This file can be used if you do not have an LDAP set up.
# List of users
users:
john:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: john.doe@authelia.com
groups:
- admins
- dev
harry:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
emails: harry.potter@authelia.com
groups: []
bob:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: bob.dylan@authelia.com
groups:
- dev
james:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: james.dean@authelia.com