mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-05-03 20:42:28 +00:00
Add UI E2E tests for the login page for OIDC being enabled and when it is disabled
This commit is contained in:
parent
1ed15b3dd9
commit
529c84f0fd
@ -1,5 +1,5 @@
|
||||
<div class="page-header">
|
||||
<h1 class="page-title"><%- i18n('dashboard', 'title', {name: getUserName()}) %></h1>
|
||||
<h1 class="page-title" data-cy="page-title"><%- i18n('dashboard', 'title', {name: getUserName()}) %></h1>
|
||||
</div>
|
||||
|
||||
<% if (columns) { %>
|
||||
|
@ -17,19 +17,19 @@
|
||||
<div class="card-title"><%- i18n('login', 'title') %></div>
|
||||
<div class="form-group">
|
||||
<label class="form-label"><%- i18n('str', 'email-address') %></label>
|
||||
<input name="identity" type="email" class="form-control" placeholder="<%- i18n('str', 'email-address') %>" required autofocus>
|
||||
<input name="identity" type="email" class="form-control" placeholder="<%- i18n('str', 'email-address') %>" data-cy="identity" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label"><%- i18n('str', 'password') %></label>
|
||||
<input name="secret" type="password" class="form-control" placeholder="<%- i18n('str', 'password') %>" required>
|
||||
<div class="invalid-feedback secret-error"></div>
|
||||
<input name="secret" type="password" class="form-control" placeholder="<%- i18n('str', 'password') %>" data-cy="password" required>
|
||||
<div class="invalid-feedback secret-error" data-cy="password-error"></div>
|
||||
</div>
|
||||
<div class="form-footer">
|
||||
<button type="submit" class="btn btn-teal btn-block"><%- i18n('str', 'sign-in') %></button>
|
||||
<button type="submit" class="btn btn-teal btn-block" data-cy="sign-in"><%- i18n('str', 'sign-in') %></button>
|
||||
</div>
|
||||
<div class="form-footer login-oidc">
|
||||
<div class="separator"><slot>OR</slot></div>
|
||||
<button type="button" id="login-oidc" class="btn btn-teal btn-block">
|
||||
<button type="button" id="login-oidc" class="btn btn-teal btn-block" data-cy="oidc-login">
|
||||
<%- i18n('str', 'sign-in-with') %> <span class="oidc-provider"></span>
|
||||
</button>
|
||||
<div class="invalid-feedback oidc-error"></div>
|
||||
|
185
test/cypress/e2e/ui/Login.cy.js
Normal file
185
test/cypress/e2e/ui/Login.cy.js
Normal file
@ -0,0 +1,185 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import {TEST_USER_EMAIL, TEST_USER_NICKNAME, TEST_USER_PASSWORD} from "../../support/constants";
|
||||
|
||||
describe('Login', () => {
|
||||
beforeEach(() => {
|
||||
// Clear all cookies and local storage so we start fresh
|
||||
cy.clearCookies();
|
||||
cy.clearLocalStorage();
|
||||
});
|
||||
|
||||
describe('when OIDC is not enabled', () => {
|
||||
beforeEach(() => {
|
||||
cy.configureOidc(false);
|
||||
cy.visit('/');
|
||||
})
|
||||
|
||||
it('should show the login form', () => {
|
||||
cy.get('input[data-cy="identity"]').should('exist');
|
||||
cy.get('input[data-cy="password"]').should('exist');
|
||||
cy.get('button[data-cy="sign-in"]').should('exist');
|
||||
});
|
||||
|
||||
it('should NOT show the button to sign in with an identity provider', () => {
|
||||
cy.get('button[data-cy="oidc-login"]').should('not.exist');
|
||||
});
|
||||
|
||||
describe('logging in with a username and password', () => {
|
||||
// These tests are duplicated below. The difference is that OIDC is disabled here.
|
||||
beforeEach(() => {
|
||||
// Delete and recreate the test user
|
||||
cy.deleteTestUser();
|
||||
cy.createTestUser();
|
||||
});
|
||||
|
||||
it('should log the user in when the credentials are correct', () => {
|
||||
// Fill in the form with the test user's email and the correct password
|
||||
cy.get('input[data-cy="identity"]').type(TEST_USER_EMAIL);
|
||||
cy.get('input[data-cy="password"]').type(TEST_USER_PASSWORD);
|
||||
|
||||
// Intercept the POST request to /api/tokens, so we can wait for it to complete before proceeding
|
||||
cy.intercept('POST', '/api/tokens').as('login');
|
||||
|
||||
// Click the sign-in button
|
||||
cy.get('button[data-cy="sign-in"]').click();
|
||||
cy.wait('@login');
|
||||
|
||||
// Expect a 200 from the backend
|
||||
cy.get('@login').its('response.statusCode').should('eq', 200);
|
||||
|
||||
// Expect the user to be redirected to the dashboard with a welcome message
|
||||
cy.get('h1[data-cy="page-title"]').should('contain.text', `Hi ${TEST_USER_NICKNAME}`);
|
||||
});
|
||||
|
||||
it('should show an error message if the password is incorrect', () => {
|
||||
// Fill in the form with the test user's email and an incorrect password
|
||||
cy.get('input[data-cy="identity"]').type(TEST_USER_EMAIL);
|
||||
cy.get('input[data-cy="password"]').type(`${TEST_USER_PASSWORD}_obviously_not_correct`);
|
||||
|
||||
// Intercept the POST request to /api/tokens, so we can wait for it to complete before checking the error message
|
||||
cy.intercept('POST', '/api/tokens').as('login');
|
||||
|
||||
// Click the sign-in button
|
||||
cy.get('button[data-cy="sign-in"]').click();
|
||||
cy.wait('@login');
|
||||
|
||||
// Expect a 401 from the backend
|
||||
cy.get('@login').its('response.statusCode').should('eq', 401);
|
||||
// Expect an error message on the UI
|
||||
cy.get('div[data-cy="password-error"]').should('contain.text', 'Invalid password');
|
||||
});
|
||||
|
||||
it('should show an error message if the email is incorrect', () => {
|
||||
// Fill in the form with the test user's email and an incorrect password
|
||||
cy.get('input[data-cy="identity"]').type(`definitely_not_${TEST_USER_EMAIL}`);
|
||||
cy.get('input[data-cy="password"]').type(TEST_USER_PASSWORD);
|
||||
|
||||
// Intercept the POST request to /api/tokens, so we can wait for it to complete before checking the error message
|
||||
cy.intercept('POST', '/api/tokens').as('login');
|
||||
|
||||
// Click the sign-in button
|
||||
cy.get('button[data-cy="sign-in"]').click();
|
||||
cy.wait('@login');
|
||||
|
||||
// Expect a 401 from the backend
|
||||
cy.get('@login').its('response.statusCode').should('eq', 401);
|
||||
// Expect an error message on the UI
|
||||
cy.get('div[data-cy="password-error"]').should('contain.text', 'No relevant user found');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when OIDC is enabled', () => {
|
||||
beforeEach(() => {
|
||||
cy.configureOidc(true);
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('should show the login form', () => {
|
||||
cy.get('input[data-cy="identity"]').should('exist');
|
||||
cy.get('input[data-cy="password"]').should('exist');
|
||||
cy.get('button[data-cy="sign-in"]').should('exist');
|
||||
});
|
||||
|
||||
it('should show the button to sign in with the configured identity provider', () => {
|
||||
cy.get('button[data-cy="oidc-login"]').should('exist');
|
||||
cy.get('button[data-cy="oidc-login"]').should('contain.text', 'Sign in with ACME OIDC Provider');
|
||||
});
|
||||
|
||||
describe('logging in with a username and password', () => {
|
||||
// These tests are the same as the ones above, but we need to repeat them here because the OIDC configuration
|
||||
beforeEach(() => {
|
||||
// Delete and recreate the test user
|
||||
cy.deleteTestUser();
|
||||
cy.createTestUser();
|
||||
});
|
||||
|
||||
it('should log the user in when the credentials are correct', () => {
|
||||
// Fill in the form with the test user's email and the correct password
|
||||
cy.get('input[data-cy="identity"]').type(TEST_USER_EMAIL);
|
||||
cy.get('input[data-cy="password"]').type(TEST_USER_PASSWORD);
|
||||
|
||||
// Intercept the POST request to /api/tokens, so we can wait for it to complete before proceeding
|
||||
cy.intercept('POST', '/api/tokens').as('login');
|
||||
|
||||
// Click the sign-in button
|
||||
cy.get('button[data-cy="sign-in"]').click();
|
||||
cy.wait('@login');
|
||||
|
||||
// Expect a 200 from the backend
|
||||
cy.get('@login').its('response.statusCode').should('eq', 200);
|
||||
|
||||
// Expect the user to be redirected to the dashboard with a welcome message
|
||||
cy.get('h1[data-cy="page-title"]').should('contain.text', `Hi ${TEST_USER_NICKNAME}`);
|
||||
|
||||
});
|
||||
|
||||
it('should show an error message if the password is incorrect', () => {
|
||||
// Fill in the form with the test user's email and an incorrect password
|
||||
cy.get('input[data-cy="identity"]').type(TEST_USER_EMAIL);
|
||||
cy.get('input[data-cy="password"]').type(`${TEST_USER_PASSWORD}_obviously_not_correct`);
|
||||
|
||||
// Intercept the POST request to /api/tokens, so we can wait for it to complete before checking the error message
|
||||
cy.intercept('POST', '/api/tokens').as('login');
|
||||
|
||||
// Click the sign-in button
|
||||
cy.get('button[data-cy="sign-in"]').click();
|
||||
cy.wait('@login');
|
||||
|
||||
// Expect a 401 from the backend
|
||||
cy.get('@login').its('response.statusCode').should('eq', 401);
|
||||
// Expect an error message on the UI
|
||||
cy.get('div[data-cy="password-error"]').should('contain.text', 'Invalid password');
|
||||
});
|
||||
|
||||
it('should show an error message if the email is incorrect', () => {
|
||||
// Fill in the form with the test user's email and an incorrect password
|
||||
cy.get('input[data-cy="identity"]').type(`definitely_not_${TEST_USER_EMAIL}`);
|
||||
cy.get('input[data-cy="password"]').type(TEST_USER_PASSWORD);
|
||||
|
||||
// Intercept the POST request to /api/tokens, so we can wait for it to complete before checking the error message
|
||||
cy.intercept('POST', '/api/tokens').as('login');
|
||||
|
||||
// Click the sign-in button
|
||||
cy.get('button[data-cy="sign-in"]').click();
|
||||
cy.wait('@login');
|
||||
|
||||
// Expect a 401 from the backend
|
||||
cy.get('@login').its('response.statusCode').should('eq', 401);
|
||||
// Expect an error message on the UI
|
||||
cy.get('div[data-cy="password-error"]').should('contain.text', 'No relevant user found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logging in with OIDC', () => {
|
||||
beforeEach(() => {
|
||||
// Delete and recreate the test user
|
||||
cy.deleteTestUser();
|
||||
cy.createTestUser();
|
||||
});
|
||||
|
||||
// TODO: Create a dummy OIDC provider that we can use for testing so we can test this fully.
|
||||
});
|
||||
});
|
||||
});
|
@ -10,6 +10,13 @@
|
||||
//
|
||||
|
||||
import 'cypress-wait-until';
|
||||
import {
|
||||
DEFAULT_ADMIN_EMAIL,
|
||||
DEFAULT_ADMIN_PASSWORD,
|
||||
TEST_USER_EMAIL,
|
||||
TEST_USER_NAME,
|
||||
TEST_USER_NICKNAME, TEST_USER_PASSWORD
|
||||
} from "./constants";
|
||||
|
||||
Cypress.Commands.add('randomString', (length) => {
|
||||
var result = '';
|
||||
@ -40,13 +47,118 @@ Cypress.Commands.add('validateSwaggerSchema', (method, code, path, data) => {
|
||||
}).should('equal', null);
|
||||
});
|
||||
|
||||
/**
|
||||
* Configure OIDC settings in the backend, so we can test scenarios around OIDC being enabled or disabled.
|
||||
*/
|
||||
Cypress.Commands.add('configureOidc', (enabled) => {
|
||||
cy.getToken().then((token) => {
|
||||
if (enabled) {
|
||||
cy.task('backendApiPut', {
|
||||
token: token,
|
||||
path: '/api/settings/oidc-config',
|
||||
data: {
|
||||
meta: {
|
||||
name: 'ACME OIDC Provider',
|
||||
clientID: 'clientID',
|
||||
clientSecret: 'clientSecret',
|
||||
// TODO: Create dummy OIDC provider for testing
|
||||
issuerURL: 'https://oidc.example.com',
|
||||
redirectURL: 'https://redirect.example.com/api/oidc/callback',
|
||||
enabled: true,
|
||||
}
|
||||
},
|
||||
})
|
||||
} else {
|
||||
cy.task('backendApiPut', {
|
||||
token: token,
|
||||
path: '/api/settings/oidc-config',
|
||||
data: {
|
||||
meta: {
|
||||
name: '',
|
||||
clientID: '',
|
||||
clientSecret: '',
|
||||
issuerURL: '',
|
||||
redirectURL: '',
|
||||
enabled: false,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a new user in the backend for testing purposes.
|
||||
*
|
||||
* The created user will have a name, nickname, email, and password as defined in the constants file (TEST_USER_*).
|
||||
*
|
||||
* @param {boolean} withPassword Whether to create the user with a password or not (default: true)
|
||||
*/
|
||||
Cypress.Commands.add('createTestUser', (withPassword) => {
|
||||
if (withPassword === undefined) {
|
||||
withPassword = true;
|
||||
}
|
||||
|
||||
cy.getToken().then((token) => {
|
||||
cy.task('backendApiPost', {
|
||||
token: token,
|
||||
path: '/api/users',
|
||||
data: {
|
||||
name: TEST_USER_NAME,
|
||||
nickname: TEST_USER_NICKNAME,
|
||||
email: TEST_USER_EMAIL,
|
||||
roles: ['admin'],
|
||||
is_disabled: false,
|
||||
auth: withPassword ? {
|
||||
type: 'password',
|
||||
secret: TEST_USER_PASSWORD
|
||||
} : {}
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Delete the test user from the backend.
|
||||
* The test user is identified by the email address defined in the constants file (TEST_USER_EMAIL).
|
||||
*
|
||||
* This command will only attempt to delete the test user if it exists.
|
||||
*/
|
||||
Cypress.Commands.add('deleteTestUser', () => {
|
||||
cy.getToken().then((token) => {
|
||||
cy.task('backendApiGet', {
|
||||
token: token,
|
||||
path: '/api/users',
|
||||
}).then((data) => {
|
||||
// Find the test user
|
||||
const testUser = data.find(user => user.email === TEST_USER_EMAIL);
|
||||
|
||||
// If the test user doesn't exist, we don't need to delete it
|
||||
if (!testUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete the test user
|
||||
cy.task('backendApiDelete', {
|
||||
token: token,
|
||||
path: `/api/users/${testUser.id}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Get a new token from the backend.
|
||||
* The token will be created using the default admin email and password defined in the constants file (DEFAULT_ADMIN_*).
|
||||
*/
|
||||
Cypress.Commands.add('getToken', () => {
|
||||
// login with existing user
|
||||
cy.task('backendApiPost', {
|
||||
path: '/api/tokens',
|
||||
data: {
|
||||
identity: 'admin@example.com',
|
||||
secret: 'changeme'
|
||||
identity: DEFAULT_ADMIN_EMAIL,
|
||||
secret: DEFAULT_ADMIN_PASSWORD
|
||||
}
|
||||
}).then(res => {
|
||||
cy.wrap(res.token);
|
||||
|
16
test/cypress/support/constants.js
Normal file
16
test/cypress/support/constants.js
Normal file
@ -0,0 +1,16 @@
|
||||
// Description: Constants used in the tests.
|
||||
|
||||
/**
|
||||
* The default admin user is used to get tokens from the backend API to make requests.
|
||||
* It is also used to create the test user.
|
||||
*/
|
||||
export const DEFAULT_ADMIN_EMAIL = Cypress.env('DEFAULT_ADMIN_EMAIL') || 'admin@example.com';
|
||||
export const DEFAULT_ADMIN_PASSWORD = Cypress.env('DEFAULT_ADMIN_PASSWORD') || 'changeme';
|
||||
|
||||
/**
|
||||
* The test user is created and deleted by the tests using `cy.createTestUser()` and `cy.deleteTestUser()`.
|
||||
*/
|
||||
export const TEST_USER_NAME = 'Robert Ross';
|
||||
export const TEST_USER_NICKNAME = 'Bob';
|
||||
export const TEST_USER_EMAIL = 'bob@ross.com';
|
||||
export const TEST_USER_PASSWORD = 'changeme';
|
Loading…
x
Reference in New Issue
Block a user