Skip to content

Commit

Permalink
[feature] Added support for password expiration feature #713
Browse files Browse the repository at this point in the history
Closes #713
  • Loading branch information
pandafy authored Nov 15, 2023
1 parent cd481ec commit 4507596
Show file tree
Hide file tree
Showing 18 changed files with 399 additions and 19 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ jobs:
run: yarn start &

- name: Running OpenWISP Radius
run: cd openwisp-radius && ./tests/manage.py runserver &
run: |
cp browser-test/local_settings.py openwisp-radius/tests/openwisp2/local_settings.py \
&& cd openwisp-radius && ./tests/manage.py runserver &
- name: geckodriver/firefox
run: |
Expand Down
20 changes: 19 additions & 1 deletion browser-test/initialize_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def load_test_data():
# do not initialize data for registration tests
registration_tests = 'register' in sys.argv
create_mobile_verification_org = 'mobileVerification' in sys.argv
expired_password_tests = 'expiredPassword' in sys.argv

sys.path.insert(0, os.path.join(OPENWISP_RADIUS_PATH, 'tests'))
sys.argv.insert(1, 'browser-test')
Expand All @@ -37,6 +38,7 @@ def load_test_data():
sys.exit(1)

from django.contrib.auth import get_user_model
from django.utils.timezone import now, timedelta
from swapper import load_model

User = get_user_model()
Expand Down Expand Up @@ -86,6 +88,22 @@ def load_test_data():
sys.exit(2)

user = User.objects.create_user(
username=test_user_email, password=test_user_password, email=test_user_email
username=test_user_email,
password=test_user_password,
email=test_user_email
)
OrganizationUser.objects.create(organization=org, user=user)
User.objects.update(password_updated=now().date())

if expired_password_tests:
data = test_data['expiredPasswordUser']
expired_password_user = User.objects.create_user(
username=data['email'],
password=data['password'],
email=data['email'],
password_updated=now().date()-timedelta(days=180)
)
OrganizationUser.objects.create(
organization=org,
user=expired_password_user
)
2 changes: 2 additions & 0 deletions browser-test/local_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
OPENWISP_USERS_USER_PASSWORD_EXPIRATION = 1
OPENWISP_USERS_STAFF_USER_PASSWORD_EXPIRATION = 1
91 changes: 91 additions & 0 deletions browser-test/password-expired.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {until} from "selenium-webdriver";
import {
getDriver,
getElementByCss,
urls,
initialData,
initializeData,
tearDown,
successToastSelector,
} from "./utils";

describe("Selenium tests for expired password flow />", () => {
let driver;

beforeAll(async () => {
await initializeData("expiredPassword");
driver = await getDriver();
}, 30000);

afterAll(async () => {
await tearDown(driver);
});

it("should force user to change password before captive portal login", async () => {
// login with original password
await driver.get(urls.login);
const data = initialData();
let username = await getElementByCss(driver, "input#username");
username.sendKeys(data.expiredPasswordUser.email);
let password = await getElementByCss(driver, "input#password");
password.sendKeys(data.expiredPasswordUser.password);
let submitBtn = await getElementByCss(driver, "input[type=submit]");
submitBtn.click();
await driver.wait(until.urlContains("change-password"), 5000);
let successToastDiv = await getElementByCss(driver, "div[role=alert]");
await driver.wait(until.elementIsVisible(successToastDiv));
expect(await successToastDiv.getText()).toEqual("Login successful");
const warningToastMessage = await getElementByCss(
driver,
".Toastify__toast--warning",
);
await driver.wait(until.elementIsVisible(warningToastMessage));
expect(await warningToastMessage.getText()).toEqual(
"Your password has expired, please update it.",
);

// Try visiting the status page, but the user should redirected
// back to change password page
await driver.get(urls.status);
await driver.wait(until.urlContains("change-password"), 5000);

// changing password
await getElementByCss(driver, "div#password-change");
const currPassword = await getElementByCss(
driver,
"input#current-password",
);
currPassword.sendKeys(data.expiredPasswordUser.password);
const newPassword = "newPassword@";
const changePassword = await getElementByCss(driver, "input#new-password");
changePassword.sendKeys(newPassword);
const changePasswordConfirm = await getElementByCss(
driver,
"input#password-confirm",
);
changePasswordConfirm.sendKeys(newPassword);
submitBtn = await getElementByCss(driver, "input[type=submit]");
submitBtn.click();
await getElementByCss(driver, "div#status");
successToastDiv = await getElementByCss(driver, successToastSelector);
await driver.wait(until.elementIsVisible(successToastDiv));
expect(await successToastDiv.getText()).toEqual(
"Password updated successfully",
);

// login with new password
await driver.manage().deleteAllCookies();
await driver.get(urls.login);
await driver.wait(until.urlContains("login"), 5000);
username = await getElementByCss(driver, "input#username");
username.sendKeys(data.expiredPasswordUser.email);
password = await getElementByCss(driver, "input#password");
password.sendKeys(newPassword);
submitBtn = await getElementByCss(driver, "input[type=submit]");
submitBtn.click();
await getElementByCss(driver, "div#status");
successToastDiv = await getElementByCss(driver, "div[role=alert]");
await driver.wait(until.elementIsVisible(successToastDiv));
expect(await successToastDiv.getText()).toEqual("Login successful");
});
});
5 changes: 5 additions & 0 deletions browser-test/testData.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"password": "testuser",
"organization": "default"
},
"expiredPasswordUser": {
"email": "[email protected]",
"password": "testuser",
"organization": "default"
},
"mobileVerificationTestUser": {
"phoneNumber": "+911234567890",
"password": "testuser",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,149 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<PasswordChange /> rendering should not show 'cancel' button if password is expired 1`] = `
<div
className="container content"
id="password-change"
>
<div
className="inner"
>
<form
className="main-column"
onSubmit={[Function]}
>
<div
className="inner"
>
<h1>
Change your password
</h1>
<div
className="row current-password"
>
<label
htmlFor="current-password"
>
Current password
</label>
<input
autoComplete="password"
className="input"
id="current-password"
name="currentPassword"
onChange={[Function]}
pattern=".{6,}"
placeholder="Your current password"
required={true}
title="password must be a minimum of 6 characters"
type="password"
value=""
/>
<PasswordToggleIcon
hidePassword={true}
inputRef={
Object {
"current": null,
}
}
isVisible={false}
parentClassName=""
secondInputRef={Object {}}
toggler={[Function]}
/>
</div>
<div
className="row new-password"
>
<label
htmlFor="new-password"
>
New Password
</label>
<input
autoComplete="password"
className="input"
id="new-password"
name="newPassword1"
onChange={[Function]}
pattern=".{6,}"
placeholder="Your new password"
required={true}
title="password must be a minimum of 6 characters"
type="password"
value=""
/>
<PasswordToggleIcon
hidePassword={true}
inputRef={
Object {
"current": null,
}
}
isVisible={false}
parentClassName=""
secondInputRef={
Object {
"current": null,
}
}
toggler={[Function]}
/>
</div>
<div
className="row password-confirm"
>
<label
htmlFor="password-confirm"
>
Confirm password
</label>
<input
autoComplete="password"
className="input"
id="password-confirm"
name="newPassword2"
onChange={[Function]}
pattern=".{6,}"
placeholder="confirm password"
required={true}
title="password must be a minimum of 6 characters"
type="password"
value=""
/>
<PasswordToggleIcon
hidePassword={true}
inputRef={
Object {
"current": null,
}
}
isVisible={false}
parentClassName=""
secondInputRef={
Object {
"current": null,
}
}
toggler={[Function]}
/>
</div>
<div
className="row submit"
>
<input
className="button full"
type="submit"
value="Change Password"
/>
</div>
</div>
</form>
<Connect(Contact) />
</div>
</div>
`;

exports[`<PasswordChange /> rendering should render correctly 1`] = `
<div
className="container content"
Expand Down
28 changes: 13 additions & 15 deletions client/components/password-change/password-change.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,12 @@ export default class PasswordChange extends React.Component {

async componentDidMount() {
const {setLoading} = this.context;
const {
setTitle,
orgName,
cookies,
userData,
setUserData,
logout,
orgSlug,
language,
} = this.props;
const {setTitle, orgName, cookies, setUserData, logout, orgSlug, language} =
this.props;
let {userData} = this.props;
setLoading(true);
setTitle(t`PWD_CHANGE_TITL`, orgName);
const {mustLogin, mustLogout, repeatLogin} = userData;
await validateToken(
cookies,
orgSlug,
Expand All @@ -59,6 +53,8 @@ export default class PasswordChange extends React.Component {
logout,
language,
);
({userData} = this.props);
setUserData({...userData, mustLogin, mustLogout, repeatLogin});
setLoading(false);
}

Expand Down Expand Up @@ -224,11 +220,13 @@ export default class PasswordChange extends React.Component {
/>
</div>

<div className="row cancel">
<Link className="button full" to={`/${orgSlug}/status`}>
{t`CANCEL`}
</Link>
</div>
{userData.password_expired !== true && (
<div className="row cancel">
<Link className="button full" to={`/${orgSlug}/status`}>
{t`CANCEL`}
</Link>
</div>
)}
</div>
</form>

Expand Down
8 changes: 8 additions & 0 deletions client/components/password-change/password-change.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ describe("<PasswordChange /> rendering", () => {
const wrapper = createShallow(props);
expect(wrapper).toMatchSnapshot();
});

it("should not show 'cancel' button if password is expired", async () => {
props = createTestProps();
props.userData.password_expired = true;
loadTranslation("en", "default");
const wrapper = createShallow(props);
expect(wrapper).toMatchSnapshot();
});
});

describe("<PasswordChange /> interactions", () => {
Expand Down
Loading

0 comments on commit 4507596

Please sign in to comment.