Add UI E2E tests for the login page for OIDC being enabled and when it is disabled

This commit is contained in:
Cameron Hutchison 2024-12-11 13:23:31 -06:00
parent 1ed15b3dd9
commit 529c84f0fd
5 changed files with 321 additions and 8 deletions

View File

@ -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) { %>

View File

@ -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>

View 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.
});
});
});

View File

@ -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);

View 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';