Skip to content

Commit

Permalink
Clarify in email hint text that shared inbox is not allowed (#1746)
Browse files Browse the repository at this point in the history
* Clarify in email hint text that shared inbox is not allowed

- Changed the password field's validation message to clarify what a hard to guess password is
- Fixed the aria-invalid attribute on text boxes
- Add cypress tests for registration page
- Use jsrsasign to generate JWTs for admin api access in cypress tests

* Various cleanups

* Remove unused import

* Add missing translation

* Undo unneeded changes to cypress.config.js

* Clear cookies after login and register test suites

* Refactor and clean up tests a bit

- Pulled the wait for confirmation email code into it's own action and out of CreateAccount
- Added nanoid and use it for unique aliases on the email address
- Fetch the confirmation link in a more sane way

* Installed nanoid in the wrong package.json...
  • Loading branch information
whabanks authored Jan 29, 2024
1 parent a3afbf6 commit 3e2c41a
Show file tree
Hide file tree
Showing 16 changed files with 511 additions and 277 deletions.
8 changes: 6 additions & 2 deletions app/main/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,12 @@ def password(label=_l("Password")):
label,
validators=[
DataRequired(message=_l("This cannot be empty")),
Length(8, 255, message=_l("Must be at least 8 characters and hard to guess")),
Blocklist(message=_l("Choose a password that’s harder to guess")),
Length(8, 255, message=_l("Must be at least 8 characters")),
Blocklist(
message=_l(
"A password that is hard to guess contains:<li>Uppercase and lowercase letters.</li><li>Numbers and special characters.</li><li>Words separated by a space.</li>"
)
),
],
)

Expand Down
6 changes: 3 additions & 3 deletions app/templates/components/textbox.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@ <h1><label class="form-label heading-large" for="{{ field.name }}" >
class=field_class,
data_module='highlight-tags' if highlight_tags else '',
rows=rows|string if field.type == 'TextAreaField' else false,
invalid='' if field.errors else false,
aria_invalid='true' if field.errors else false,
required='' if required != false else false,
aria_required='true' if required != false else false,
aria_describedby=describedby|trim if describedby != '' else false,
**kwargs
) }}

{% if suffix %}
<span>{{ suffix }}</span>
{% endif %}
Expand Down Expand Up @@ -119,7 +119,7 @@ <h1><label class="form-label heading-large" for="{{ field.name }}" >
</span>
{% endif %}
</div>

{% set field_class = '{}'.format(width) %}

{% set field_class = 'form-control input focus:shadow-outline min-h-target ' + field_class + (
Expand Down
2 changes: 1 addition & 1 deletion app/templates/views/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<h1 class="heading-large">{{ _('Create an account') }}</h1>
{% call form_wrapper(autocomplete=True) %}
{{ textbox(form.name, width='w-full md:w-3/4', autocomplete='name') }}
{% set hint_txt = _('Must be a federal government email address') %}
{% set hint_txt = _('Use a government email address only you can access, not a shared inbox') %}
{{ textbox(form.email_address, hint=hint_txt, width='w-full md:w-3/4', safe_error_message=True, autocomplete='email') }}
<div class="extra-tracking">
{% set hint_txt = _('We’ll send you a security code by text message') %}
Expand Down
4 changes: 3 additions & 1 deletion app/translations/csv/fr.csv
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
"Must be a valid https URL","L’URL https doit être valide"
"Bearer token","Jeton 'Bearer'"
"Must be at least 10 characters","Doit contenir au moins 10 caractères"
"Must be at least 8 characters","Doit contenir au moins 8 caractères"
"Send text messages to international phone numbers","Envoyer des messages texte à des numéros de téléphone internationaux"
"Folder name","Nom du dossier"
"Current folder path","Chemin du dossier actuel"
Expand Down Expand Up @@ -579,7 +580,7 @@
"Your account will be created with this email address:","Votre compte sera créé avec cette adresse courriel&nbsp;:"
"We’ll send you a security code by text message","Nous vous enverrons un code de sécurité par message texte."
"Your account will be created with this email:","Votre compte sera créé avec cette adresse courriel&nbsp;:"
"Must be a federal government email address","Veuillez entrer une adresse courriel du gouvernement fédéral."
"Use a government email address only you can access, not a shared inbox","Utilisez une adresse e-mail gouvernementale à laquelle vous seul avez accès, pas une boîte de réception partagée."
"By creating an account, you agree to the <a href='{}'>terms of use</a>.","En créant un compte, vous acceptez de vous soumettre aux <a href='{}'>conditions d’utilisation</a>."
"Click the link in the email to continue creating an account.","Cliquez sur le lien dans le courriel pour continuer à créer un compte."
"A new confirmation email has been sent to","Un nouveau courriel de confirmation a été envoyé à"
Expand Down Expand Up @@ -1637,6 +1638,7 @@
"If you don't receive a password reset link in your inbox,","Si vous ne recevez pas de courriel pour réinitialiser votre mot de passe,"
"please contact our support team.","contactez notre équipe de soutien technique."
"A password that is hard to guess contains:","Un mot de passe difficile à deviner contient les caractéristiques suivantes:"
"A password that is hard to guess contains:<li>Uppercase and lowercase letters.</li><li>Numbers and special characters.</li><li>Words separated by a space.</li>","Un mot de passe difficile à deviner contient les caractéristiques suivantes:<li>Lettres majuscules et minuscules.</li><li>Des chiffres et des caractères spéciaux.</li><li>Mots séparés par des espaces.</li>"
"Uppercase and lowercase characters.","Lettres majuscules et minuscules."
"Letters and numbers.","Lettres et chiffres."
"Special characters.","Caractères spéciaux."
Expand Down
6 changes: 5 additions & 1 deletion tests/app/main/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ def test_should_raise_validation_error_for_password(
form.password.data = password

form.validate()
assert "Choose a password that’s harder to guess" in form.errors["password"]

assert (
"A password that is hard to guess contains:<li>Uppercase and lowercase letters.</li><li>Numbers and special characters.</li><li>Words separated by a space.</li>"
in form.errors["password"]
)


def test_valid_email_not_in_valid_domains(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def test_registration_from_org_invite_404s_if_user_not_in_session(
"mobile_number": "+4966921809",
"password": "password",
},
"Choose a password that’s harder to guess",
"A password that is hard to guess contains:<li>Uppercase and lowercase letters.</li><li>Numbers and special characters.</li><li>Words separated by a space.</li>",
],
],
)
Expand Down
5 changes: 4 additions & 1 deletion tests/app/main/views/test_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,10 @@ def test_should_return_200_if_password_is_blocked(
)

response.status_code == 200
assert "Choose a password that’s harder to guess" in response.get_data(as_text=True)
assert (
"A password that is hard to guess contains:<li>Uppercase and lowercase letters.</li><li>Numbers and special characters.</li><li>Words separated by a space.</li>"
in response.get_data(as_text=True)
)


def test_register_with_existing_email_sends_emails(
Expand Down
4 changes: 2 additions & 2 deletions tests_cypress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ let STAGING = {
Default: '24e5288d-8bfa-4ad4-93aa-592c11a694cd',
Second: '797865c4-788b-4184-91ae-8e45eb07e40b'
},
viewports: [320,375,640,768]
viewports: [320, 375, 640, 768]
};

let LOCAL = {
Expand Down Expand Up @@ -63,7 +63,7 @@ let LOCAL = {
Default: '1bc45a34-f4de-4635-b36f-7da2e2d248ed',
Second: 'aaa58593-fc0a-46b0-82b8-b303ae662a41'
},
viewports: [320,375,640,768]
viewports: [320, 375, 640, 768]
};

const config = {
Expand Down
64 changes: 64 additions & 0 deletions tests_cypress/cypress/Notify/Admin/Pages/CreateAccountPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const { recurse } = require('cypress-recurse')


let Components = {
FullName: () => cy.get("#name"),
EmailAddress: () => cy.get('#email_address'),
MobileNumber: () => cy.get('#mobile_number'),
Password: () => cy.get('#password'),
TwoFactorCode: () => cy.get('#two_factor_code'),
SubmitButton: () => cy.get('button[type="submit"]'),
}


let Actions = {
VisitPage: () => {
cy.visit(CreateAccountPage.URL)
},

EmptySubmit: () => {
Components.SubmitButton().click();
},

Submit: (name, email, mobile, password) => {
Components.FullName().type(name);
Components.EmailAddress().type(email);
Components.MobileNumber().type(mobile);
Components.Password().type(password);
Components.SubmitButton().click();
},

WaitForConfirmationEmail: () => {
recurse(
() => cy.task('getLastEmail', {}), // Cypress commands to retry
Cypress._.isObject,
{
log: true,
limit: 250, // max number of iterations
timeout: 120000, // time limit in ms
delay: 500, // delay before next iteration, ms
},
)
.its('html')
.then((html) => (
cy.document({ log: false }).invoke({ log: false }, 'write', html)
));
},

CreateAccount: (name, email, mobile, password) => {
cy.task('deleteAllEmails', {});
Components.FullName().type(name);
Components.EmailAddress().type(email);
Components.MobileNumber().type(mobile);
Components.Password().type(password);
Components.SubmitButton().click();
}
};

let CreateAccountPage = {
URL: '/register',
Components,
...Actions
};

export default CreateAccountPage;
8 changes: 5 additions & 3 deletions tests_cypress/cypress/Notify/Admin/Pages/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import LoginPage from "./LoginPage";
import TwoFactorPage from "./TwoFactorPage";
import DashboardPage from "./DashboardPage";
import AddRecipientsPage from "./AddRecipientsPage";
import Navigation from "./Navigation";
import Navigation from "./Navigation";
import TemplatesPage from "./TemplatesPage";
import AccountsPage from "./AccountsPage";
import AccountsPage from "./AccountsPage";
import CreateAccountPage from "./CreateAccountPage";

export default {
LoginPage,
Expand All @@ -13,5 +14,6 @@ export default {
AddRecipientsPage,
Navigation,
TemplatesPage,
AccountsPage
AccountsPage,
CreateAccountPage,
}
42 changes: 36 additions & 6 deletions tests_cypress/cypress/Notify/NotifyAPI.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import jwt from "jsonwebtoken";
import config from "../../config";
import { customAlphabet } from "nanoid";

const BASE_URL = config.Hostnames.API

const Utilities = {
CreateJWT: () => {
const jwt = require('jsrsasign');
const claims = {
'iss': Cypress.env('ADMIN_USERNAME'),
'iat': Math.round(Date.now() / 1000)
}

var token = jwt.sign(claims, Cypress.env('ADMIN_SECRET'));

return token;
const headers = { alg: "HS256", typ: "JWT" };
return jwt.jws.JWS.sign("HS256", JSON.stringify(headers), JSON.stringify(claims), Cypress.env('ADMIN_SECRET'));
},
GenerateID: (length = 10) => {
const nanoid = customAlphabet('1234567890abcdef-_', length)
return nanoid()
}
};
const Admin = {
SendOneOff: ({to, template_id}) => {
SendOneOff: ({ to, template_id }) => {

var token = Utilities.CreateJWT();
return cy.request({
Expand All @@ -29,6 +35,31 @@ const Admin = {
'created_by': Cypress.env('NOTIFY_USER_ID'),
}
});
},
ArchiveUser: ({ userId }) => {
var token = Utilities.CreateJWT();
return cy.request({
failOnStatusCode: false,
url: `${BASE_URL}/user/${userId}/archive`,
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": 'application/json'
},
})
},
GetUserByEmail: ({ email }) => {
var token = Utilities.CreateJWT();
return cy.request({
failOnStatusCode: true,
retryOnstatusCodeFailure: true,
url: `${BASE_URL}/user/email?email=${encodeURIComponent(email)}`,
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": 'application/json'
}
})
}
}
// const Admin = {
Expand Down Expand Up @@ -174,7 +205,6 @@ const API = {
}
});
},

}

export default { API, Utilities, Admin };
1 change: 1 addition & 0 deletions tests_cypress/cypress/e2e/admin/all.cy.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// import './gca_pages.cy';
import './login.cy';
import './register.cy';
import './qualtrics.cy';
5 changes: 5 additions & 0 deletions tests_cypress/cypress/e2e/admin/login.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ describe('Basic login', () => {
cy.intercept('GET', '**/dashboard.json', {});
});

// Clear cookies for the next suite of tests
after(() => {
cy.clearCookie('notify_admin_session');
});

it('succeeds and ADMIN displays accounts page', () => {
cy.visit("/accounts");

Expand Down
71 changes: 71 additions & 0 deletions tests_cypress/cypress/e2e/admin/register.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/// <reference types="cypress" />

import { CreateAccountPage } from "../../Notify/Admin/Pages/all";
import { Utilities, Admin } from "../../Notify/NotifyAPI";

describe('Create Account Page', () => {

beforeEach(() => {
CreateAccountPage.VisitPage();
});

// Clear cookies for the next suite of tests
after(() => {
cy.clearCookie('notify_admin_session');
});

it('Display the correct title', () => {
cy.contains('h1', 'Create an account').should('be.visible');
});

it('Display error when submitting an empty form', () => {
CreateAccountPage.EmptySubmit();
cy.contains('span', 'This cannot be empty').should('be.visible');
cy.contains('span', 'Enter an email address').should('be.visible');
});

it('Display an error when submitting and invalid phone number and non-government email', () => {
CreateAccountPage.Submit('John Doe', '[email protected]', '123', 'password123')
cy.contains('span', "not-a-gov-email.ca is not on our list of government domains").should('be.visible');
cy.contains('span', "Not a valid phone number").should('be.visible');
});

it('Display an error if password is shorter than 8 characters', () => {
CreateAccountPage.Submit("john doe", "[email protected]", "1234567890", "1234567");
cy.contains('span', 'Must be at least 8 characters').should('be.visible');
});

it('Display an error if password is not strong enough', () => {
CreateAccountPage.Submit("john doe", "[email protected]", "1234567890", "123456789");
cy.contains('span', 'A password that is hard to guess contains:').should('be.visible');
cy.contains('li', 'Uppercase and lowercase letters.').should('be.visible');
cy.contains('li', 'Numbers and special characters.').should('be.visible');
cy.contains('li', 'Words separated by a space.').should('be.visible');
});

it('Succeeds with a valid form', () => {
let email = `notify-ui-tests+${Utilities.GenerateID()}@cds-snc.ca`;

CreateAccountPage.CreateAccount("john doe", email, "16132532223", "BestPassword123!");
cy.contains('h1', 'Check your email').should('be.visible');
cy.contains('p', `An email has been sent to ${email}.`).should('be.visible');

CreateAccountPage.WaitForConfirmationEmail();
// Ensure registration email is received and click registration link
cy.contains('p', 'To complete your registration for GC Notify, please click the link:').should('be.visible');
cy.get('a')
.should('have.attr', 'href')
.and('include', 'verify-email')
.then((href) => {
cy.visit(href);
});

cy.contains('h1', 'Check your phone messages').should('be.visible')

Admin.GetUserByEmail({ email: email }).then((user) => {
// TODO: Ideally we'd want the ability to completely delete the test user rather than archive it.
Admin.ArchiveUser({ userId: user.body.data.id })
});
});

});
Loading

0 comments on commit 3e2c41a

Please sign in to comment.