Replace mocha integration tests by cucumber tests

This commit is contained in:
Clement Michaud 2017-07-26 23:45:26 +02:00
parent e45ac39c8f
commit c12a085f8e
39 changed files with 517 additions and 330 deletions

View File

@ -1,23 +1,37 @@
dist: trusty
language: node_js language: node_js
sudo: required
node_js: node_js:
- node - "7"
services: services:
- docker - docker
- ntp - ntp
addons: addons:
chrome: stable
apt: apt:
sources:
- google-chrome
packages: packages:
- libgif-dev - libgif-dev
- google-chrome-stable
hosts: hosts:
- auth.test.local - auth.test.local
- home.test.local - home.test.local
- public.test.local
- secret.test.local - secret.test.local
- secret1.test.local - secret1.test.local
- secret2.test.local - secret2.test.local
- mx1.mail.test.local - mx1.mail.test.local
- mx2.mail.test.local - mx2.mail.test.local
before_install: npm install -g npm@'>=2.13.5' before_install:
- npm install -g npm@'>=2.13.5'
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- sleep 3
script: script:
- ./scripts/travis.sh - ./scripts/travis.sh

View File

@ -12,17 +12,13 @@ module.exports = function (grunt) {
cmd: "./node_modules/.bin/tslint", cmd: "./node_modules/.bin/tslint",
args: ['-c', 'tslint.json', '-p', 'tsconfig.json'] args: ['-c', 'tslint.json', '-p', 'tsconfig.json']
}, },
"test": { "unit-tests": {
cmd: "./node_modules/.bin/mocha", cmd: "./node_modules/.bin/mocha",
args: ['--compilers', 'ts:ts-node/register', '--recursive', 'test/unit'] args: ['--compilers', 'ts:ts-node/register', '--recursive', 'test/unit']
}, },
"test-int": { "integration-tests": {
cmd: "./node_modules/.bin/mocha", cmd: "./node_modules/.bin/cucumber-js",
args: ['--compilers', 'ts:ts-node/register', '--recursive', 'test/integration'] args: ["--compiler", "ts:ts-node/register", "./test/features"]
},
"test-system": {
cmd: "./node_modules/.bin/mocha",
args: ['--compilers', 'ts:ts-node/register', '--recursive', 'test/system']
}, },
"docker-build": { "docker-build": {
cmd: "docker", cmd: "docker",
@ -165,5 +161,8 @@ module.exports = function (grunt) {
grunt.registerTask('docker-build', ['run:docker-build']); grunt.registerTask('docker-build', ['run:docker-build']);
grunt.registerTask('docker-restart', ['run:docker-restart']); grunt.registerTask('docker-restart', ['run:docker-restart']);
grunt.registerTask('test', ['run:test']); grunt.registerTask('unit-tests', ['run:unit-tests']);
grunt.registerTask('integration-tests', ['run:unit-tests']);
grunt.registerTask('test', ['unit-tests']);
}; };

View File

@ -91,6 +91,7 @@ Make sure you don't have anything listening on port 8080.
Add the following lines to your **/etc/hosts** to alias multiple subdomains so that nginx can redirect request to the correct virtual host. Add the following lines to your **/etc/hosts** to alias multiple subdomains so that nginx can redirect request to the correct virtual host.
127.0.0.1 public.test.local
127.0.0.1 secret.test.local 127.0.0.1 secret.test.local
127.0.0.1 secret1.test.local 127.0.0.1 secret1.test.local
127.0.0.1 secret2.test.local 127.0.0.1 secret2.test.local

View File

@ -48,7 +48,7 @@ ldap:
# beginning of the pattern. # beginning of the pattern.
access_control: access_control:
default: default:
- home.test.local - public.test.local
groups: groups:
admin: admin:
- '*.test.local' - '*.test.local'

View File

@ -45,3 +45,10 @@ mail: bob.dylan@example.com
sn: Bob Dylan sn: Bob Dylan
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g= userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
dn: cn=james,ou=users,dc=example,dc=com
cn: james
objectclass: inetOrgPerson
objectclass: top
mail: james.dean@example.com
sn: James Dean
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=

View File

@ -9,6 +9,9 @@
You need to log in to access the secret!<br/><br/> You need to log in to access the secret!<br/><br/>
Try to access it via one of the following links.<br/> Try to access it via one of the following links.<br/>
<ul> <ul>
<li>
<a href="https://public.test.local:8080/secret.html">public.test.local</a>
</li>
<li> <li>
<a href="https://secret.test.local:8080/secret.html">secret.test.local</a> <a href="https://secret.test.local:8080/secret.html">secret.test.local</a>
</li> </li>
@ -18,9 +21,6 @@
<li> <li>
<a href="https://secret2.test.local:8080/secret.html">secret2.test.local</a> <a href="https://secret2.test.local:8080/secret.html">secret2.test.local</a>
</li> </li>
<li>
<a href="https://home.test.local:8080/secret.html">home.test.local</a>
</li>
<li> <li>
<a href="https://mx1.mail.test.local:8080/secret.html">mx1.mail.test.local</a> <a href="https://mx1.mail.test.local:8080/secret.html">mx1.mail.test.local</a>
</li> </li>
@ -45,7 +45,7 @@
<ul> <ul>
<li><strong>Default policy</strong> <li><strong>Default policy</strong>
<ul> <ul>
<li>home.test.local</li> <li>public.test.local</li>
</ul> </ul>
</li> </li>
<li><strong>Groups policy</strong> <li><strong>Groups policy</strong>

View File

@ -53,7 +53,8 @@ http {
root /usr/share/nginx/html; root /usr/share/nginx/html;
server_name secret1.test.local secret2.test.local secret.test.local server_name secret1.test.local secret2.test.local secret.test.local
home.test.local mx1.mail.test.local mx2.mail.test.local; home.test.local mx1.mail.test.local mx2.mail.test.local
public.test.local;
ssl on; ssl on;
ssl_certificate /etc/ssl/server.crt; ssl_certificate /etc/ssl/server.crt;

View File

@ -7,7 +7,7 @@
"authelia": "dist/src/server/index.js" "authelia": "dist/src/server/index.js"
}, },
"scripts": { "scripts": {
"test": "./node_modules/.bin/grunt test", "test": "./node_modules/.bin/grunt unit-tests",
"cover": "NODE_ENV=test nyc npm t", "cover": "NODE_ENV=test nyc npm t",
"serve": "node dist/server/index.js" "serve": "node dist/server/index.js"
}, },
@ -48,6 +48,7 @@
"@types/body-parser": "^1.16.3", "@types/body-parser": "^1.16.3",
"@types/connect-redis": "0.0.6", "@types/connect-redis": "0.0.6",
"@types/cors": "^2.8.1", "@types/cors": "^2.8.1",
"@types/cucumber": "^2.0.1",
"@types/ejs": "^2.3.33", "@types/ejs": "^2.3.33",
"@types/express": "^4.0.35", "@types/express": "^4.0.35",
"@types/express-session": "0.0.32", "@types/express-session": "0.0.32",
@ -64,6 +65,7 @@
"@types/query-string": "^4.3.1", "@types/query-string": "^4.3.1",
"@types/randomstring": "^1.1.5", "@types/randomstring": "^1.1.5",
"@types/request": "0.0.46", "@types/request": "0.0.46",
"@types/selenium-webdriver": "^3.0.4",
"@types/sinon": "^2.2.1", "@types/sinon": "^2.2.1",
"@types/speakeasy": "^2.0.1", "@types/speakeasy": "^2.0.1",
"@types/tmp": "0.0.33", "@types/tmp": "0.0.33",
@ -71,6 +73,8 @@
"@types/yamljs": "^0.2.30", "@types/yamljs": "^0.2.30",
"apidoc": "^0.17.6", "apidoc": "^0.17.6",
"browserify": "^14.3.0", "browserify": "^14.3.0",
"chromedriver": "^2.31.0",
"cucumber": "^2.3.1",
"grunt": "^1.0.1", "grunt": "^1.0.1",
"grunt-browserify": "^5.0.0", "grunt-browserify": "^5.0.0",
"grunt-contrib-concat": "^1.0.1", "grunt-contrib-concat": "^1.0.1",
@ -82,7 +86,7 @@
"jquery": "^3.2.1", "jquery": "^3.2.1",
"js-logger": "^1.3.0", "js-logger": "^1.3.0",
"jsdom": "^11.0.0", "jsdom": "^11.0.0",
"mocha": "^3.2.0", "mocha": "^3.4.2",
"mockdate": "^2.0.1", "mockdate": "^2.0.1",
"notifyjs-browser": "^0.4.2", "notifyjs-browser": "^0.4.2",
"nyc": "^10.3.2", "nyc": "^10.3.2",
@ -90,11 +94,12 @@
"proxyquire": "^1.8.0", "proxyquire": "^1.8.0",
"query-string": "^4.3.4", "query-string": "^4.3.4",
"request": "^2.81.0", "request": "^2.81.0",
"selenium-webdriver": "^3.5.0",
"should": "^11.1.1", "should": "^11.1.1",
"sinon": "^2.3.8", "sinon": "^2.3.8",
"sinon-promise": "^0.1.3", "sinon-promise": "^0.1.3",
"tmp": "0.0.31", "tmp": "0.0.31",
"ts-node": "^3.0.4", "ts-node": "^3.3.0",
"tslint": "^5.2.0", "tslint": "^5.2.0",
"typescript": "^2.3.2", "typescript": "^2.3.2",
"u2f-api": "0.0.9", "u2f-api": "0.0.9",

View File

@ -3,11 +3,10 @@
set -e set -e
docker-compose \ docker-compose \
-f docker-compose.base.yml \ -f docker-compose.base.yml \
-f docker-compose.yml \ -f docker-compose.yml \
-f docker-compose.dev.yml \ -f docker-compose.dev.yml \
-f example/mongo/docker-compose.yml \ -f example/mongo/docker-compose.yml \
-f example/redis/docker-compose.yml \ -f example/redis/docker-compose.yml \
-f example/nginx/docker-compose.yml \ -f example/nginx/docker-compose.yml \
-f example/ldap/docker-compose.yml \ -f example/ldap/docker-compose.yml $*
-f test/integration/docker-compose.yml $*

View File

@ -8,5 +8,4 @@ docker-compose \
-f example/mongo/docker-compose.yml \ -f example/mongo/docker-compose.yml \
-f example/redis/docker-compose.yml \ -f example/redis/docker-compose.yml \
-f example/nginx/docker-compose.yml \ -f example/nginx/docker-compose.yml \
-f example/ldap/docker-compose.yml \ -f example/ldap/docker-compose.yml $*
-f test/integration/docker-compose.yml $*

View File

@ -1,10 +1,10 @@
#!/bin/bash #!/bin/bash
DC_SCRIPT=./scripts/example/dc-example.sh DC_SCRIPT=./scripts/example/dc-example.sh
EXPECTED_SERVICES_COUNT=6 EXPECTED_SERVICES_COUNT=5
start_services() { start_services() {
$DC_SCRIPT up -d mongo redis openldap authelia nginx nginx-tests $DC_SCRIPT up -d mongo redis openldap authelia nginx
sleep 3 sleep 3
} }
@ -27,39 +27,12 @@ expect_services_count() {
} }
run_integration_tests() { run_integration_tests() {
echo "Prepare nginx-test configuration"
cat example/nginx/nginx.conf | sed 's/listen 443 ssl/listen 8080 ssl/g' | dd of="test/integration/nginx.conf"
echo "Build services images..."
$DC_SCRIPT build
echo "Start services..."
start_services
docker ps -a
echo "Display services logs..."
$DC_SCRIPT logs redis
$DC_SCRIPT logs openldap
$DC_SCRIPT logs nginx
$DC_SCRIPT logs nginx-tests
$DC_SCRIPT logs authelia
echo "Check number of services"
expect_services_count $EXPECTED_SERVICES_COUNT
echo "Run integration tests..."
$DC_SCRIPT run --rm integration-tests
echo "Shutdown services..."
shut_services
}
run_system_tests() {
echo "Start services..." echo "Start services..."
start_services start_services
expect_services_count $EXPECTED_SERVICES_COUNT expect_services_count $EXPECTED_SERVICES_COUNT
./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/system sleep 5
./node_modules/.bin/grunt run:integration-tests
shut_services shut_services
} }
@ -80,11 +53,8 @@ set -e
echo "Make sure services are not already running" echo "Make sure services are not already running"
shut_services shut_services
# Prepare & run integration tests
run_integration_tests
# Prepare & test example from end user perspective # Prepare & test example from end user perspective
run_system_tests run_integration_tests
# Other tests like executing the deployment script # Other tests like executing the deployment script
run_other_tests run_other_tests

View File

@ -13,7 +13,7 @@ export function validate(username: string, password: string, $: JQueryStatic): B
}) })
.fail(function (xhr: JQueryXHR, textStatus: string) { .fail(function (xhr: JQueryXHR, textStatus: string) {
if (xhr.status == 401) if (xhr.status == 401)
reject(new Error("Authetication failed. Please check your credentials")); reject(new Error("Authetication failed. Please check your credentials."));
reject(new Error(textStatus)); reject(new Error(textStatus));
}); });
}); });

View File

@ -30,7 +30,7 @@ export default function(window: Window, $: JQueryStatic) {
requestPasswordReset(username) requestPasswordReset(username)
.then(function () { .then(function () {
$.notify("An email has been sent. Click on the link to change your password", "success"); $.notify("An email has been sent. Click on the link to change your password.", "success");
setTimeout(function () { setTimeout(function () {
window.location.replace(Endpoints.FIRST_FACTOR_GET); window.location.replace(Endpoints.FIRST_FACTOR_GET);
}, 1000); }, 1000);

View File

@ -2,7 +2,7 @@
import * as BluebirdPromise from "bluebird"; import * as BluebirdPromise from "bluebird";
import exceptions = require("./Exceptions"); import exceptions = require("./Exceptions");
import { UserDataStore } from "./storage/UserDataStore"; import { UserDataStore } from "./storage/UserDataStore";
import {AuthenticationTraceDocument} from "./storage/AuthenticationTraceDocument"; import { AuthenticationTraceDocument } from "./storage/AuthenticationTraceDocument";
const MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE = 3; const MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE = 3;
@ -22,19 +22,19 @@ export class AuthenticationRegulator {
regulate(userId: string): BluebirdPromise<void> { regulate(userId: string): BluebirdPromise<void> {
return this.userDataStore.retrieveLatestAuthenticationTraces(userId, false, 3) return this.userDataStore.retrieveLatestAuthenticationTraces(userId, false, 3)
.then((docs: AuthenticationTraceDocument[]) => { .then((docs: AuthenticationTraceDocument[]) => {
if (docs.length < MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE) { if (docs.length < MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE) {
// less than the max authorized number of authentication in time range, thus authorizing access // less than the max authorized number of authentication in time range, thus authorizing access
return BluebirdPromise.resolve();
}
const oldestDocument = docs[MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE - 1];
const noLockMinDate = new Date(new Date().getTime() - this.lockTimeInSeconds * 1000);
if (oldestDocument.date > noLockMinDate) {
throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes.");
}
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
} });
const oldestDocument = docs[MAX_AUTHENTICATION_COUNT_IN_TIME_RANGE - 1];
const noLockMinDate = new Date(new Date().getTime() - this.lockTimeInSeconds * 1000);
if (oldestDocument.date > noLockMinDate) {
throw new exceptions.AuthenticationRegulationError("Max number of authentication. Please retry in few minutes.");
}
return BluebirdPromise.resolve();
});
} }
} }

View File

@ -41,10 +41,10 @@ export class Client {
reconnect: true reconnect: true
}); });
const clientLogger = (ldapClient as any).log; /*const clientLogger = (ldapClient as any).log;
if (clientLogger) { if (clientLogger) {
clientLogger.level("trace"); clientLogger.level("trace");
} }*/
this.client = BluebirdPromise.promisifyAll(ldapClient) as ldapjs.ClientAsync; this.client = BluebirdPromise.promisifyAll(ldapClient) as ldapjs.ClientAsync;
} }

View File

@ -1,7 +1,7 @@
import * as BluebirdPromise from "bluebird"; import * as BluebirdPromise from "bluebird";
import * as util from "util"; import * as util from "util";
import * as fs from "fs"; import * as Fs from "fs";
import { INotifier } from "./INotifier"; import { INotifier } from "./INotifier";
import { Identity } from "../../../types/Identity"; import { Identity } from "../../../types/Identity";
@ -17,7 +17,7 @@ export class FileSystemNotifier implements INotifier {
notify(identity: Identity, subject: string, link: string): BluebirdPromise<void> { notify(identity: Identity, subject: string, link: string): BluebirdPromise<void> {
const content = util.format("Date: %s\nUser: %s\nSubject: %s\nLink: %s", new Date().toString(), identity.userid, const content = util.format("Date: %s\nUser: %s\nSubject: %s\nLink: %s", new Date().toString(), identity.userid,
subject, link); subject, link);
const writeFilePromised = BluebirdPromise.promisify<void, string, string>(fs.writeFile); const writeFilePromised: any = BluebirdPromise.promisify(Fs.writeFile);
return writeFilePromised(this.filename, content); return writeFilePromised(this.filename, content);
} }
} }

View File

@ -8,4 +8,4 @@ block form-header
<img class="header-img" src="/img/warning.png" alt=""> <img class="header-img" src="/img/warning.png" alt="">
block content block content
<p>You are not authorized.</p> <p>You are either not authorized or not <a href="/">logged in</a>.</p>

View File

@ -8,4 +8,4 @@ block form-header
<img class="header-img" src="/img/warning.png" alt=""> <img class="header-img" src="/img/warning.png" alt="">
block content block content
<p>You are not authorized.</p> <p>You are either not authorized or not <a href="/">logged in</a>.</p>

View File

@ -12,7 +12,7 @@ block content
</div> </div>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
<!-- <label class="checkbox pull-left"><input type="checkbox" value="remember-me">Remember me</label> --> <!-- <label class="checkbox pull-left"><input type="checkbox" value="remember-me">Remember me</label> -->
a(href=reset_password_request_endpoint, class="pull-right link") Forgot password? a(href=reset_password_request_endpoint, class="pull-right link forgot-password") Forgot password?
<span class="clearfix"></span> <span class="clearfix"></span>
</form> </form>

View File

@ -20,7 +20,7 @@ html
</div> </div>
</div> </div>
<div class="row poweredby-block"> <div class="row poweredby-block">
<div class="poweredby col-xs-10 col-xs-offset-1 col-sm-2 col-sm-offset-8 col-md-6 col-md-offset-4">Powered by <a class="authelia-brand" href="https://github.com/clems4ever/authelia">Authelia</a></div> <div class="poweredby col-xs-6 col-xs-offset-4 col-sm-6 col-sm-offset-4 col-md-6 col-md-offset-4">Powered by <a class="authelia-brand" href="https://github.com/clems4ever/authelia">Authelia</a></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -9,14 +9,14 @@ block content
<div class="form-inputs"> <div class="form-inputs">
<input type="text" class="form-control" id="token" placeholder="Token" required autofocus> <input type="text" class="form-control" id="token" placeholder="Token" required autofocus>
</div> </div>
<button class="btn btn-lg btn-primary btn-block" type="submit">TOTP</button> <button class="btn btn-lg btn-primary btn-block totp-button" type="submit">TOTP</button>
a(href=totp_identity_start_endpoint, class="pull-right link") Need to register? a(href=totp_identity_start_endpoint, class="pull-right link register-totp") Need to register?
<span class="clearfix"></span> <span class="clearfix"></span>
</form> </form>
<hr> <hr>
<form class="form-signin u2f"> <form class="form-signin u2f">
<button class="btn btn-lg btn-primary btn-block" type="submit">U2F</button> <button class="btn btn-lg btn-primary btn-block u2f-button" type="submit">U2F</button>
a(href=u2f_identity_start_endpoint, class="pull-right link") Need to register? a(href=u2f_identity_start_endpoint, class="pull-right link register-u2f") Need to register?
<span class="clearfix"></span> <span class="clearfix"></span>
</form> </form>

View File

@ -0,0 +1,38 @@
Feature: User has access restricted access to domains
Scenario: User john has admin access
When I register TOTP and login with user "john" and password "password"
Then I have access to:
| url |
| https://public.test.local:8080/secret.html |
| https://secret.test.local:8080/secret.html |
| https://secret1.test.local:8080/secret.html |
| https://secret2.test.local:8080/secret.html |
| https://mx1.mail.test.local:8080/secret.html |
| https://mx2.mail.test.local:8080/secret.html |
Scenario: User bob has restricted access
When I register TOTP and login with user "bob" and password "password"
Then I have access to:
| url |
| https://public.test.local:8080/secret.html |
| https://secret.test.local:8080/secret.html |
| https://secret2.test.local:8080/secret.html |
| https://mx1.mail.test.local:8080/secret.html |
| https://mx2.mail.test.local:8080/secret.html |
And I have no access to:
| url |
| https://secret1.test.local:8080/secret.html |
Scenario: User harry has restricted access
When I register TOTP and login with user "harry" and password "password"
Then I have access to:
| url |
| https://public.test.local:8080/secret.html |
| https://secret1.test.local:8080/secret.html |
And I have no access to:
| url |
| https://secret.test.local:8080/secret.html |
| https://secret2.test.local:8080/secret.html |
| https://mx1.mail.test.local:8080/secret.html |
| https://mx2.mail.test.local:8080/secret.html |

View File

@ -0,0 +1,53 @@
Feature: User validate first factor
Scenario: User succeeds first factor
Given I visit "https://auth.test.local:8080/"
When I set field "username" to "bob"
And I set field "password" to "password"
And I click on "Sign in"
Then I'm redirected to "https://auth.test.local:8080/secondfactor"
Scenario: User fails first factor
Given I visit "https://auth.test.local:8080/"
When I set field "username" to "john"
And I set field "password" to "bad-password"
And I click on "Sign in"
Then I get a notification with message "Error during authentication: Authetication failed. Please check your credentials."
Scenario: User succeeds TOTP second factor
Given I visit "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I register a TOTP secret called "Sec0"
When I visit "https://secret.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I use "Sec0" as TOTP token handle
And I click on "TOTP"
Then I'm redirected to "https://secret.test.local:8080/secret.html"
Scenario: User fails TOTP second factor
When I visit "https://secret.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I use "BADTOKEN" as TOTP token
And I click on "TOTP"
Then I get a notification with message "Error while validating TOTP token. Cause: error"
Scenario: User logs out
Given I visit "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I register a TOTP secret called "Sec0"
And I visit "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I use "Sec0" as TOTP token handle
When I visit "https://auth.test.local:8080/logout?redirect=https://www.google.fr"
And I visit "https://secret.test.local:8080/secret.html"
Then I'm redirected to "https://auth.test.local:8080/"
Scenario: Logout redirects user
Given I visit "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I register a TOTP secret called "Sec0"
And I visit "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I use "Sec0" as TOTP token handle
When I visit "https://auth.test.local:8080/logout?redirect=https://www.google.fr"
Then I'm redirected to "https://www.google.fr"

View File

@ -0,0 +1,7 @@
Feature: User is redirected to authelia when he is not authenticated
Scenario: User is redirected to authelia
Given I'm on https://home.test.local:8080
When I click on the link to secret.test.local
Then I'm redirected to "https://auth.test.local:8080/"

View File

@ -0,0 +1,33 @@
Feature: User is able to reset his password
Scenario: User is redirected to password reset page
Given I'm on https://auth.test.local:8080
When I click on the link "Forgot password?"
Then I'm redirected to "https://auth.test.local:8080/password-reset/request"
Scenario: User get an email with a link to reset password
Given I'm on https://auth.test.local:8080/password-reset/request
When I set field "username" to "james"
And I click on "Reset Password"
Then I get a notification with message "An email has been sent. Click on the link to change your password."
Scenario: User resets his password
Given I'm on https://auth.test.local:8080/password-reset/request
And I set field "username" to "james"
And I click on "Reset Password"
When I click on the link of the email
And I set field "password1" to "newpassword"
And I set field "password2" to "newpassword"
And I click on "Reset Password"
Then I'm redirected to "https://auth.test.local:8080/"
Scenario: User does not confirm new password
Given I'm on https://auth.test.local:8080/password-reset/request
And I set field "username" to "james"
And I click on "Reset Password"
When I click on the link of the email
And I set field "password1" to "newpassword"
And I set field "password2" to "newpassword2"
And I click on "Reset Password"
Then I get a notification with message "The passwords are different"

View File

@ -0,0 +1,13 @@
Feature: Authelia keeps user sessions despite the application restart
Scenario: Session is still valid after Authelia restarts
When I register TOTP and login with user "john" and password "password"
And the application restarts
Then I have access to:
| url |
| https://public.test.local:8080/secret.html |
| https://secret.test.local:8080/secret.html |
| https://secret1.test.local:8080/secret.html |
| https://secret2.test.local:8080/secret.html |
| https://mx1.mail.test.local:8080/secret.html |
| https://mx2.mail.test.local:8080/secret.html |

View File

@ -0,0 +1,17 @@
Feature: Non authenticated users have no access to certain pages
Scenario Outline: User has no access to protected pages
When I visit "<url>"
Then I get an error <error code>
Examples:
| url | error code |
| https://auth.test.local:8080/secondfactor | 401 |
| https://auth.test.local:8080/verify | 401 |
| https://auth.test.local:8080/secondfactor/u2f/identity/start | 401 |
| https://auth.test.local:8080/secondfactor/u2f/identity/finish | 403 |
| https://auth.test.local:8080/secondfactor/totp/identity/start | 401 |
| https://auth.test.local:8080/secondfactor/totp/identity/finish | 403 |
| https://auth.test.local:8080/password-reset/identity/start | 403 |
| https://auth.test.local:8080/password-reset/identity/finish | 403 |

View File

@ -0,0 +1,7 @@
import Cucumber = require("cucumber");
Cucumber.defineSupportCode(function({After}) {
After(function() {
return this.driver.quit();
});
});

View File

@ -0,0 +1,92 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
import Assert = require("assert");
import Fs = require("fs");
import Speakeasy = require("speakeasy");
import CustomWorld = require("../support/world");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
When(/^I visit "(https:\/\/[a-z0-9:.\/=?-]+)"$/, function (link: string) {
return this.visit(link);
});
When("I set field {stringInDoubleQuotes} to {stringInDoubleQuotes}", function (fieldName: string, content: string) {
return this.setFieldTo(fieldName, content);
});
When("I click on {stringInDoubleQuotes}", function (text: string) {
return this.clickOnButton(text);
});
Given("I login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}", function (username: string, password: string) {
return this.loginWithUserPassword(username, password);
});
Given("I register a TOTP secret called {stringInDoubleQuotes}", function (handle: string) {
return this.registerTotpSecret(handle);
});
Given("I use {stringInDoubleQuotes} as TOTP token", function (token: string) {
return this.useTotpToken(token);
});
Given("I use {stringInDoubleQuotes} as TOTP token handle", function (handle) {
return this.useTotpTokenHandle(handle);
});
Then("I get a notification with message {stringInDoubleQuotes}", function (notificationMessage: string) {
const that = this;
that.driver.sleep(500);
return this.driver
.findElement(seleniumWebdriver.By.className("notifyjs-corner"))
.findElement(seleniumWebdriver.By.tagName("span"))
.findElement(seleniumWebdriver.By.xpath("//span[contains(.,'" + notificationMessage + "')]"));
});
When("I visit {stringInDoubleQuotes} and get redirected {stringInDoubleQuotes}", function (url: string, redirectUrl: string) {
const that = this;
return this.driver.get(url)
.then(function () {
return that.driver.wait(seleniumWebdriver.until.urlIs(redirectUrl), 2000);
});
});
Given("I register TOTP and login with user {stringInDoubleQuotes} and password {stringInDoubleQuotes}", function (username: string, password: string) {
return this.registerTotpAndSignin(username, password);
});
function hasAccessToSecret(link: string, driver: any) {
return driver.get(link)
.then(function () {
return driver.findElement(seleniumWebdriver.By.tagName("body")).getText()
.then(function (body: string) {
Assert(body.indexOf("This is a very important secret!") > -1);
});
});
}
function hasNoAccessToSecret(link: string, driver: any) {
return driver.get(link)
.then(function () {
return driver.wait(seleniumWebdriver.until.urlIs("https://auth.test.local:8080/"));
});
}
Then("I have access to:", function (dataTable: Cucumber.TableDefinition) {
const promises = [];
for (let i = 0; i < dataTable.rows().length; i++) {
const url = (dataTable.hashes() as any)[i].url;
promises.push(hasAccessToSecret(url, this.driver));
}
return Promise.all(promises);
});
Then("I have no access to:", function (dataTable: Cucumber.TableDefinition) {
const promises = [];
for (let i = 0; i < dataTable.rows().length; i++) {
const url = (dataTable.hashes() as any)[i].url;
promises.push(hasNoAccessToSecret(url, this.driver));
}
return Promise.all(promises);
});
});

View File

@ -0,0 +1,17 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
import Assert = require("assert");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
Given("I'm on https://{string}", function (link: string) {
return this.driver.get("https://" + link);
});
When("I click on the link to {string}", function (link: string) {
return this.driver.findElement(seleniumWebdriver.By.linkText(link)).click();
});
Then("I'm redirected to {stringInDoubleQuotes}", function (link: string) {
return this.driver.wait(seleniumWebdriver.until.urlContains(link), 5000);
});
});

View File

@ -0,0 +1,20 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
import Assert = require("assert");
import Fs = require("fs");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
When("I click on the link {stringInDoubleQuotes}", function (text: string) {
return this.driver.findElement(seleniumWebdriver.By.linkText(text)).click();
});
When("I click on the link of the email", function () {
const notif = Fs.readFileSync("./notifications/notification.txt").toString();
const regexp = new RegExp(/Link: (.+)/);
const match = regexp.exec(notif);
const link = match[1];
const that = this;
return this.driver.get(link);
});
});

View File

@ -0,0 +1,12 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
import Assert = require("assert");
import ChildProcess = require("child_process");
import BluebirdPromise = require("bluebird");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
When(/^the application restarts$/, {timeout: 15 * 1000}, function () {
const exec = BluebirdPromise.promisify(ChildProcess.exec);
return exec("./scripts/example/dc-example.sh restart authelia && sleep 2");
});
});

View File

@ -0,0 +1,11 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
import Assert = require("assert");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
Then("I get an error {number}", function (code: number) {
return this.driver
.findElement(seleniumWebdriver.By.tagName("h1"))
.findElement(seleniumWebdriver.By.xpath("//h1[contains(.,'Error " + code + "')]"));
});
});

View File

@ -0,0 +1,110 @@
require("chromedriver");
import seleniumWebdriver = require("selenium-webdriver");
import Cucumber = require("cucumber");
import Fs = require("fs");
import Speakeasy = require("speakeasy");
function CustomWorld() {
const that = this;
this.driver = new seleniumWebdriver.Builder()
.forBrowser("chrome")
.build();
this.totpSecrets = {};
this.visit = function (link: string) {
return this.driver.get(link);
};
this.setFieldTo = function (fieldName: string, content: string) {
return this.driver.findElement(seleniumWebdriver.By.id(fieldName))
.sendKeys(content);
};
this.clickOnButton = function (buttonText: string) {
return this.driver
.findElement(seleniumWebdriver.By.tagName("button"))
.findElement(seleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]"))
.click();
};
this.loginWithUserPassword = function (username: string, password: string) {
return this.driver
.findElement(seleniumWebdriver.By.id("username"))
.sendKeys(username)
.then(function () {
return that.driver.findElement(seleniumWebdriver.By.id("password"))
.sendKeys(password);
})
.then(function () {
return that.driver.findElement(seleniumWebdriver.By.tagName("button"))
.click();
})
.then(function () {
return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.className("register-totp")), 4000);
});
};
this.registerTotpSecret = function (totpSecretHandle: string) {
return this.driver.findElement(seleniumWebdriver.By.className("register-totp")).click()
.then(function () {
const notif = Fs.readFileSync("./notifications/notification.txt").toString();
const regexp = new RegExp(/Link: (.+)/);
const match = regexp.exec(notif);
const link = match[1];
console.log("Link: " + link);
return that.driver.get(link);
})
.then(function () {
return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.id("secret")), 1000);
})
.then(function () {
return that.driver.findElement(seleniumWebdriver.By.id("secret")).getText();
})
.then(function (secret: string) {
that.totpSecrets[totpSecretHandle] = secret;
});
};
this.useTotpTokenHandle = function (totpSecretHandle: string) {
const token = Speakeasy.totp({
secret: this.totpSecrets[totpSecretHandle],
encoding: "base32"
});
return this.useTotpToken(token);
};
this.useTotpToken = function (totpSecret: string) {
return this.driver.findElement(seleniumWebdriver.By.id("token"))
.sendKeys(totpSecret);
};
this.registerTotpAndSignin = function (username: string, password: string) {
const totpHandle = "HANDLE";
const authUrl = "https://auth.test.local:8080/";
const that = this;
return this.visit(authUrl)
.then(function () {
return that.loginWithUserPassword(username, password);
})
.then(function () {
return that.registerTotpSecret(totpHandle);
})
.then(function () {
return that.visit(authUrl);
})
.then(function () {
return that.loginWithUserPassword(username, password);
})
.then(function () {
return that.useTotpTokenHandle(totpHandle);
})
.then(function () {
return that.clickOnButton("TOTP");
});
};
}
Cucumber.defineSupportCode(function ({ setWorldConstructor }) {
setWorldConstructor(CustomWorld);
});

View File

@ -1,4 +0,0 @@
FROM node:7-alpine
WORKDIR /usr/src

View File

@ -1,30 +0,0 @@
version: '2'
services:
integration-tests:
build: ./test/integration
command: ./node_modules/.bin/mocha --compilers ts:ts-node/register --recursive test/integration
volumes:
- ./:/usr/src
networks:
- example-network
nginx-tests:
image: nginx:alpine
volumes:
- ./example/nginx/html:/usr/share/nginx/html
- ./example/nginx/ssl:/etc/ssl
- ./test/integration/nginx.conf:/etc/nginx/nginx.conf
expose:
- "8080"
depends_on:
- authelia
networks:
example-network:
aliases:
- home.test.local
- secret.test.local
- secret1.test.local
- secret2.test.local
- mx1.mail.test.local
- mx2.mail.test.local
- auth.test.local

View File

@ -1,112 +0,0 @@
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
import Request = require("request");
import Assert = require("assert");
import BluebirdPromise = require("bluebird");
import Util = require("util");
import Redis = require("redis");
import Endpoints = require("../../src/server/endpoints");
const RequestAsync = BluebirdPromise.promisifyAll(Request) as typeof Request;
const DOMAIN = "test.local";
const PORT = 8080;
const HOME_URL = Util.format("https://%s.%s:%d", "home", DOMAIN, PORT);
const SECRET_URL = Util.format("https://%s.%s:%d", "secret", DOMAIN, PORT);
const SECRET1_URL = Util.format("https://%s.%s:%d", "secret1", DOMAIN, PORT);
const SECRET2_URL = Util.format("https://%s.%s:%d", "secret2", DOMAIN, PORT);
const MX1_URL = Util.format("https://%s.%s:%d", "mx1.mail", DOMAIN, PORT);
const MX2_URL = Util.format("https://%s.%s:%d", "mx2.mail", DOMAIN, PORT);
const BASE_AUTH_URL = Util.format("https://%s.%s:%d", "auth", DOMAIN, PORT);
const FIRST_FACTOR_URL = Util.format("%s/api/firstfactor", BASE_AUTH_URL);
const LOGOUT_URL = Util.format("%s/logout", BASE_AUTH_URL);
const redisOptions = {
host: "redis",
port: 6379
};
describe("integration tests", function () {
let redisClient: Redis.RedisClient;
before(function () {
redisClient = Redis.createClient(redisOptions);
});
function str_contains(str: string, pattern: string) {
return str.indexOf(pattern) != -1;
}
function test_homepage_is_correct(body: string) {
Assert(str_contains(body, BASE_AUTH_URL + Endpoints.LOGOUT_GET + "?redirect=" + HOME_URL + "/"));
Assert(str_contains(body, HOME_URL + "/secret.html"));
Assert(str_contains(body, SECRET_URL + "/secret.html"));
Assert(str_contains(body, SECRET1_URL + "/secret.html"));
Assert(str_contains(body, SECRET2_URL + "/secret.html"));
Assert(str_contains(body, MX1_URL + "/secret.html"));
Assert(str_contains(body, MX2_URL + "/secret.html"));
Assert(str_contains(body, "Access the secret"));
}
it("should access the home page", function () {
return RequestAsync.getAsync(HOME_URL)
.then(function (response: Request.RequestResponse) {
Assert.equal(200, response.statusCode);
test_homepage_is_correct(response.body);
});
});
it("should access the authentication page", function () {
return RequestAsync.getAsync(BASE_AUTH_URL)
.then(function (response: Request.RequestResponse) {
Assert.equal(200, response.statusCode);
Assert(response.body.indexOf("Sign in") > -1);
});
});
it("should fail first factor when wrong credentials are provided", function () {
return RequestAsync.postAsync(FIRST_FACTOR_URL, {
json: true,
body: {
username: "john",
password: "wrong password"
}
})
.then(function (response: Request.RequestResponse) {
Assert.equal(401, response.statusCode);
});
});
it("should redirect when correct credentials are provided during first factor", function () {
return RequestAsync.postAsync(FIRST_FACTOR_URL, {
json: true,
body: {
username: "john",
password: "password"
}
})
.then(function (response: Request.RequestResponse) {
Assert.equal(302, response.statusCode);
});
});
it("should have registered four sessions in redis", function (done) {
redisClient.dbsize(function (err: Error, count: number) {
Assert.equal(3, count);
done();
});
});
it("should redirect to home page when logout is called", function () {
return RequestAsync.getAsync(Util.format("%s?redirect=%s", LOGOUT_URL, HOME_URL))
.then(function (response: Request.RequestResponse) {
Assert.equal(200, response.statusCode);
Assert(response.body.indexOf("Access the secret") > -1);
});
});
});

View File

@ -1,92 +0,0 @@
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
import Request = require("request");
import Assert = require("assert");
import BluebirdPromise = require("bluebird");
import Util = require("util");
import Endpoints = require("../../src/server/endpoints");
const RequestAsync = BluebirdPromise.promisifyAll(Request) as typeof Request;
const DOMAIN = "test.local";
const PORT = 8080;
const HOME_URL = Util.format("https://%s.%s:%d", "home", DOMAIN, PORT);
const SECRET_URL = Util.format("https://%s.%s:%d", "secret", DOMAIN, PORT);
const SECRET1_URL = Util.format("https://%s.%s:%d", "secret1", DOMAIN, PORT);
const SECRET2_URL = Util.format("https://%s.%s:%d", "secret2", DOMAIN, PORT);
const MX1_URL = Util.format("https://%s.%s:%d", "mx1.mail", DOMAIN, PORT);
const MX2_URL = Util.format("https://%s.%s:%d", "mx2.mail", DOMAIN, PORT);
const BASE_AUTH_URL = Util.format("https://%s.%s:%d", "auth", DOMAIN, PORT);
const FIRST_FACTOR_URL = Util.format("%s/api/firstfactor", BASE_AUTH_URL);
const LOGOUT_URL = Util.format("%s/logout", BASE_AUTH_URL);
describe("test example environment", function () {
function str_contains(str: string, pattern: string) {
return str.indexOf(pattern) != -1;
}
function test_homepage_is_correct(body: string) {
Assert(str_contains(body, BASE_AUTH_URL + Endpoints.LOGOUT_GET + "?redirect=" + HOME_URL + "/"));
Assert(str_contains(body, HOME_URL + "/secret.html"));
Assert(str_contains(body, SECRET_URL + "/secret.html"));
Assert(str_contains(body, SECRET1_URL + "/secret.html"));
Assert(str_contains(body, SECRET2_URL + "/secret.html"));
Assert(str_contains(body, MX1_URL + "/secret.html"));
Assert(str_contains(body, MX2_URL + "/secret.html"));
Assert(str_contains(body, "Access the secret"));
}
it("should access the home page", function () {
return RequestAsync.getAsync(HOME_URL)
.then(function (response: Request.RequestResponse) {
Assert.equal(200, response.statusCode);
test_homepage_is_correct(response.body);
});
});
it("should access the authentication page", function () {
return RequestAsync.getAsync(BASE_AUTH_URL)
.then(function (response: Request.RequestResponse) {
Assert.equal(200, response.statusCode);
Assert(response.body.indexOf("Sign in") > -1);
});
});
it("should fail first factor when wrong credentials are provided", function () {
return RequestAsync.postAsync(FIRST_FACTOR_URL, {
json: true,
body: {
username: "john",
password: "wrong password"
}
})
.then(function (response: Request.RequestResponse) {
Assert.equal(401, response.statusCode);
});
});
it("should redirect when correct credentials are provided during first factor", function () {
return RequestAsync.postAsync(FIRST_FACTOR_URL, {
json: true,
body: {
username: "john",
password: "password"
}
})
.then(function (response: Request.RequestResponse) {
Assert.equal(302, response.statusCode);
});
});
it("should redirect to home page when logout is called", function () {
return RequestAsync.getAsync(Util.format("%s?redirect=%s", LOGOUT_URL, HOME_URL))
.then(function (response: Request.RequestResponse) {
Assert.equal(200, response.statusCode);
Assert(response.body.indexOf("Access the secret") > -1);
});
});
});

View File

@ -42,7 +42,7 @@ describe("test FirstFactorValidator", function () {
}); });
it("should fail with error 401", () => { it("should fail with error 401", () => {
return should_fail_first_factor_validation(401, "Authetication failed. Please check your credentials"); return should_fail_first_factor_validation(401, "Authetication failed. Please check your credentials.");
}); });
}); });
}); });