From 1df40e5882b963ccaa44ff5e4c586a50a8517cb8 Mon Sep 17 00:00:00 2001 From: alanv Date: Wed, 10 Jun 2026 12:05:10 -0500 Subject: [PATCH 1/6] LabKeySiteWrapper: add attemptReauth --- src/org/labkey/test/LabKeySiteWrapper.java | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/org/labkey/test/LabKeySiteWrapper.java b/src/org/labkey/test/LabKeySiteWrapper.java index 90a67cf466..d84c3952eb 100644 --- a/src/org/labkey/test/LabKeySiteWrapper.java +++ b/src/org/labkey/test/LabKeySiteWrapper.java @@ -388,10 +388,17 @@ public void attemptSignIn(String email) public void attemptSignIn(String email, String password) { - if (isSignedIn()) + attemptSignIn(email, password, false); + } + + public void attemptSignIn(String email, String password, boolean isReAuth) + { + if (!isReAuth && isSignedIn()) throw new IllegalStateException("You need to be logged out to log in. Please log out to log in."); - if (!getDriver().getTitle().contains("Sign In")) + boolean onLoginPage = getDriver().getTitle().contains("Sign In"); + + if (!onLoginPage && !isReAuth) { try { @@ -403,13 +410,17 @@ public void attemptSignIn(String email, String password) throw new IllegalStateException("Unable to find \"Sign In\" link on current page.", error); } } + else if (!onLoginPage) + { + throw new IllegalStateException("Unable to Sign In, not already on the Sign In page"); + } assertTitleContains("Sign In"); assertElementPresent(Locator.tagWithName("form", "login")); setFormElement(Locator.id("email"), email); setFormElement(Locator.id("password"), password); - WebElement signInButton = Locator.button("Sign In").findElement(getDriver()); + WebElement signInButton = Locator.byClass("signin-btn").findElement(getDriver()); doAndMaybeWaitForPageToLoad(10_000, () -> { signInButton.click(); shortWait().until(ExpectedConditions.invisibilityOfElementLocated(Locator.byClass("signing-in-msg"))); @@ -420,6 +431,11 @@ public void attemptSignIn(String email, String password) }); } + public void attemptReauth() + { + attemptSignIn(PasswordUtil.getUsername(), PasswordUtil.getPassword(), true); + } + public void signInShouldFail(String email, String password, String... expectedMessages) { attemptSignIn(email, password); From d7ebf6578c47eabad16e83200e27d86fd112e53d Mon Sep 17 00:00:00 2001 From: alanv Date: Wed, 10 Jun 2026 16:34:11 -0500 Subject: [PATCH 2/6] Add SignInPage, use in LabKeySiteWrapper. Remove attemptReauth. --- src/org/labkey/test/LabKeySiteWrapper.java | 36 ++-------- .../test/pages/core/login/SignInPage.java | 70 +++++++++++++++++++ 2 files changed, 74 insertions(+), 32 deletions(-) create mode 100644 src/org/labkey/test/pages/core/login/SignInPage.java diff --git a/src/org/labkey/test/LabKeySiteWrapper.java b/src/org/labkey/test/LabKeySiteWrapper.java index d84c3952eb..41b752d1e6 100644 --- a/src/org/labkey/test/LabKeySiteWrapper.java +++ b/src/org/labkey/test/LabKeySiteWrapper.java @@ -54,6 +54,7 @@ import org.labkey.test.components.ui.navigation.UserMenu; import org.labkey.test.pages.core.admin.CustomizeSitePage; import org.labkey.test.pages.core.admin.ShowAdminPage; +import org.labkey.test.pages.core.login.SignInPage; import org.labkey.test.pages.user.UserDetailsPage; import org.labkey.test.util.APIUserHelper; import org.labkey.test.util.ApiPermissionsHelper; @@ -388,17 +389,10 @@ public void attemptSignIn(String email) public void attemptSignIn(String email, String password) { - attemptSignIn(email, password, false); - } - - public void attemptSignIn(String email, String password, boolean isReAuth) - { - if (!isReAuth && isSignedIn()) + if (isSignedIn()) throw new IllegalStateException("You need to be logged out to log in. Please log out to log in."); - boolean onLoginPage = getDriver().getTitle().contains("Sign In"); - - if (!onLoginPage && !isReAuth) + if (!getDriver().getTitle().contains("Sign In")) { try { @@ -410,30 +404,8 @@ public void attemptSignIn(String email, String password, boolean isReAuth) throw new IllegalStateException("Unable to find \"Sign In\" link on current page.", error); } } - else if (!onLoginPage) - { - throw new IllegalStateException("Unable to Sign In, not already on the Sign In page"); - } - assertTitleContains("Sign In"); - - assertElementPresent(Locator.tagWithName("form", "login")); - setFormElement(Locator.id("email"), email); - setFormElement(Locator.id("password"), password); - WebElement signInButton = Locator.byClass("signin-btn").findElement(getDriver()); - doAndMaybeWaitForPageToLoad(10_000, () -> { - signInButton.click(); - shortWait().until(ExpectedConditions.invisibilityOfElementLocated(Locator.byClass("signing-in-msg"))); - shortWait().until(ExpectedConditions.or( - ExpectedConditions.stalenessOf(signInButton), // Successful login - ExpectedConditions.presenceOfElementLocated(Locators.labkeyError.withText()))); // Error during sign-in - return ExpectedConditions.stalenessOf(signInButton).apply(null); - }); - } - - public void attemptReauth() - { - attemptSignIn(PasswordUtil.getUsername(), PasswordUtil.getPassword(), true); + new SignInPage(getDriver()).signIn(email, password); } public void signInShouldFail(String email, String password, String... expectedMessages) diff --git a/src/org/labkey/test/pages/core/login/SignInPage.java b/src/org/labkey/test/pages/core/login/SignInPage.java new file mode 100644 index 0000000000..df3c77156c --- /dev/null +++ b/src/org/labkey/test/pages/core/login/SignInPage.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.test.pages.core.login; + +import org.labkey.test.Locator; +import org.labkey.test.Locators; +import org.labkey.test.pages.LabKeyPage; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; + +/** + * Page object for the LabKey "Sign In" page. This object assumes the browser is already on the Sign In page; it does + * not handle navigating to it (e.g. clicking a "Sign In" link). + */ +public class SignInPage extends LabKeyPage +{ + public SignInPage(WebDriver driver) + { + super(driver); + } + + @Override + protected void waitForPage() + { + waitFor(() -> getDriver().getTitle().contains("Sign In") && isElementPresent(Locator.tagWithName("form", "login")), + "Sign In page did not load in time.", WAIT_FOR_PAGE); + } + + public void signIn(String userName, String password) + { + setFormElement(elementCache().emailInput, userName); + setFormElement(elementCache().passwordInput, password); + WebElement signInButton = elementCache().signInButton; + doAndMaybeWaitForPageToLoad(10_000, () -> { + signInButton.click(); + shortWait().until(ExpectedConditions.invisibilityOfElementLocated(Locator.byClass("signing-in-msg"))); + shortWait().until(ExpectedConditions.or( + ExpectedConditions.stalenessOf(signInButton), // Successful login + ExpectedConditions.presenceOfElementLocated(Locators.labkeyError.withText()))); // Error during sign-in + return ExpectedConditions.stalenessOf(signInButton).apply(null); + }); + } + + @Override + protected ElementCache newElementCache() + { + return new ElementCache(); + } + + protected class ElementCache extends LabKeyPage.ElementCache + { + WebElement emailInput = Locator.id("email").findWhenNeeded(getDriver()); + WebElement passwordInput = Locator.id("password").findWhenNeeded(getDriver()); + WebElement signInButton = Locator.byClass("signin-btn").findWhenNeeded(getDriver()); + } +} From 856da77059d37bb4cf35ddc15c319447c24365d6 Mon Sep 17 00:00:00 2001 From: alanv Date: Tue, 16 Jun 2026 21:09:38 -0500 Subject: [PATCH 3/6] ReactDateTimePicker: use refindWhenNeeded in order to prevent issues with stale elements --- src/org/labkey/test/components/react/ReactDateTimePicker.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/labkey/test/components/react/ReactDateTimePicker.java b/src/org/labkey/test/components/react/ReactDateTimePicker.java index 782fb6cdb4..c02b87ef93 100644 --- a/src/org/labkey/test/components/react/ReactDateTimePicker.java +++ b/src/org/labkey/test/components/react/ReactDateTimePicker.java @@ -201,8 +201,8 @@ protected ElementCache newElementCache() protected class ElementCache extends Component.ElementCache { WebElement inputContainer = Locator.tagWithClass("div", "react-datepicker__input-container") - .findElement(this); - public Input input = new Input(Locator.tag("input").findWhenNeeded(inputContainer), getDriver()); + .refindWhenNeeded(this); + public Input input = new Input(Locator.tag("input").refindWhenNeeded(inputContainer), getDriver()); WebElement popup = Locator.xpath(".").followingSibling("div").withClass("react-datepicker__tab-loop") .refindWhenNeeded(this); From 10ff78f6e3b5364b009b4ff2894e5296ec6e13fc Mon Sep 17 00:00:00 2001 From: alanv Date: Wed, 17 Jun 2026 09:07:43 -0500 Subject: [PATCH 4/6] SignInPage: PR Feedback --- src/org/labkey/test/pages/core/login/SignInPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/labkey/test/pages/core/login/SignInPage.java b/src/org/labkey/test/pages/core/login/SignInPage.java index df3c77156c..3454790bee 100644 --- a/src/org/labkey/test/pages/core/login/SignInPage.java +++ b/src/org/labkey/test/pages/core/login/SignInPage.java @@ -61,7 +61,7 @@ protected ElementCache newElementCache() return new ElementCache(); } - protected class ElementCache extends LabKeyPage.ElementCache + protected class ElementCache extends LabKeyPage.ElementCache { WebElement emailInput = Locator.id("email").findWhenNeeded(getDriver()); WebElement passwordInput = Locator.id("password").findWhenNeeded(getDriver()); From 5f6e43383ba9d69ba1ba2c7406a0b2c718109a50 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Wed, 17 Jun 2026 20:51:38 -0700 Subject: [PATCH 5/6] Fix button text --- src/org/labkey/test/LabKeySiteWrapper.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/org/labkey/test/LabKeySiteWrapper.java b/src/org/labkey/test/LabKeySiteWrapper.java index 41b752d1e6..02a3c82713 100644 --- a/src/org/labkey/test/LabKeySiteWrapper.java +++ b/src/org/labkey/test/LabKeySiteWrapper.java @@ -157,7 +157,7 @@ public void simpleSignIn() } else { - fillSignInFormAndSubmit(); + fillSignInFormAndSubmit("Sign In"); // verify we're signed in now if (!waitFor(() -> @@ -198,14 +198,14 @@ else if (errors.contains("log in and approve the terms of use.")) WebTestHelper.saveSession(PasswordUtil.getUsername(), getDriver()); } - public void fillSignInFormAndSubmit() + public void fillSignInFormAndSubmit(String buttonText) { - log("Signing in as " + PasswordUtil.getUsername()); + log(buttonText + " as " + PasswordUtil.getUsername()); assertElementPresent(Locator.tagWithName("form", "login")); setFormElement(Locator.name("email"), PasswordUtil.getUsername()); setFormElement(Locator.name("password"), PasswordUtil.getPassword()); acceptTermsOfUse(null, false); - clickButton("Sign In", 0); + clickButton(buttonText, 0); } /** From a97013c7405ebb4f5bc57226d652744f1c6e1ef8 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 19 Jun 2026 16:47:14 -0700 Subject: [PATCH 6/6] Add AbstractReauthTest and implement for DB auth --- src/org/labkey/test/LabKeySiteWrapper.java | 16 +- .../test/pages/test/TestReauthPage.java | 103 +++++++++++++ .../labkey/test/tests/AbstractReauthTest.java | 139 ++++++++++++++++++ .../test/tests/core/login/DbReauthTest.java | 57 +++++++ 4 files changed, 311 insertions(+), 4 deletions(-) create mode 100644 src/org/labkey/test/pages/test/TestReauthPage.java create mode 100644 src/org/labkey/test/tests/AbstractReauthTest.java create mode 100644 src/org/labkey/test/tests/core/login/DbReauthTest.java diff --git a/src/org/labkey/test/LabKeySiteWrapper.java b/src/org/labkey/test/LabKeySiteWrapper.java index 02a3c82713..26d3eb52f3 100644 --- a/src/org/labkey/test/LabKeySiteWrapper.java +++ b/src/org/labkey/test/LabKeySiteWrapper.java @@ -200,12 +200,20 @@ else if (errors.contains("log in and approve the terms of use.")) public void fillSignInFormAndSubmit(String buttonText) { - log(buttonText + " as " + PasswordUtil.getUsername()); + fillSignInFormAndSubmit(buttonText, PasswordUtil.getUsername(), PasswordUtil.getPassword()); + } + + public void fillSignInFormAndSubmit(String buttonText, String username, String password) + { + log(buttonText + " as " + username); assertElementPresent(Locator.tagWithName("form", "login")); - setFormElement(Locator.name("email"), PasswordUtil.getUsername()); - setFormElement(Locator.name("password"), PasswordUtil.getPassword()); + setFormElement(Locator.name("email"), username); + setFormElement(Locator.name("password"), password); acceptTermsOfUse(null, false); - clickButton(buttonText, 0); + WebElement signInButton = Locator.byClass("signin-btn").findElement(getDriver()); + if (buttonText != null) + assertEquals("Wrong sign-in button text", buttonText, signInButton.getText()); + signInButton.click(); } /** diff --git a/src/org/labkey/test/pages/test/TestReauthPage.java b/src/org/labkey/test/pages/test/TestReauthPage.java new file mode 100644 index 0000000000..a9ef1a9b71 --- /dev/null +++ b/src/org/labkey/test/pages/test/TestReauthPage.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2018-2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.test.pages.test; + +import org.apache.hc.core5.http.HttpStatus; +import org.labkey.test.Locator; +import org.labkey.test.WebDriverWrapper; +import org.labkey.test.WebTestHelper; +import org.labkey.test.pages.LabKeyPage; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import java.util.Map; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +public class TestReauthPage extends LabKeyPage +{ + public TestReauthPage(WebDriver driver) + { + super(driver); + } + + public static TestReauthPage beginAt(WebDriverWrapper webDriverWrapper) + { + webDriverWrapper.beginAt(WebTestHelper.buildURL("test", "home", "testReauth")); + return new TestReauthPage(webDriverWrapper.getDriver()); + } + + public static TestReauthPage beginAt(WebDriverWrapper webDriverWrapper, String reauthToken) + { + webDriverWrapper.beginAt(WebTestHelper.buildURL("test", "home", "testReauth", Map.of("reauthToken", reauthToken))); + return new TestReauthPage(webDriverWrapper.getDriver()); + } + + public String getDescription() + { + if (elementCache().description.isDisplayed()) + return elementCache().description.getText(); + else + return ""; + } + + public void clickReauth() + { + clickAndWait(elementCache().reauthLink); + clearCache(); + } + + public String getReauthToken() + { + return elementCache().reauthTokenInput() + .map(el -> el.getDomProperty("value")).orElse(""); + } + + public void validateToken() + { + clickAndWait(elementCache().validateButton); + clearCache(); + assertNoLabKeyErrors(); + assertEquals("Response code", HttpStatus.SC_OK, getResponseCode()); + } + + public void validateTokenExpectingError() + { + clickAndWait(elementCache().validateButton); + clearCache(); + assertNotEquals("Response code", HttpStatus.SC_OK, getResponseCode()); + } + + + @Override + protected ElementCache newElementCache() + { + return new ElementCache(); + } + + protected class ElementCache extends LabKeyPage.ElementCache + { + final WebElement description = Locator.id("description").findWhenNeeded(this); + final WebElement reauthLink = Locator.id("link").findWhenNeeded(this); + final Optional reauthTokenInput() + { + return Locator.name("reauthToken").findOptionalElement(this); + } + final WebElement validateButton = Locator.tagWithAttribute("input", "value", "Sign!").findWhenNeeded(this); + } +} diff --git a/src/org/labkey/test/tests/AbstractReauthTest.java b/src/org/labkey/test/tests/AbstractReauthTest.java new file mode 100644 index 0000000000..508f70b3bf --- /dev/null +++ b/src/org/labkey/test/tests/AbstractReauthTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2018-2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.test.tests; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.labkey.test.BaseWebDriverTest; +import org.labkey.test.Locator; +import org.labkey.test.Locators; +import org.labkey.test.pages.test.TestReauthPage; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +public abstract class AbstractReauthTest extends BaseWebDriverTest +{ + public record User(String email, String password) { } + + private final User user1; + private final User user2; + + protected AbstractReauthTest(User user1, User user2) + { + this.user1 = user1; + this.user2 = user2; + } + + protected abstract void clickSignIn(); + protected abstract void authenticate(String email, String password); + protected abstract void authenticateExpectingError(String email, String password); + + @Test + public void testReauth() + { + signInAs(user1); + + TestReauthPage testReauthPage = TestReauthPage.beginAt(this); + testReauthPage.clickReauth(); + authenticate(user1.email, user1.password); + testReauthPage.validateToken(); + + // Reauth again as the same user to ensure newer token is recognized + testReauthPage = TestReauthPage.beginAt(this); + testReauthPage.clickReauth(); + authenticate(user1.email, user1.password); + testReauthPage.validateToken(); + } + + @Test + public void testReuseReauthToken() + { + signInAs(user1); + + TestReauthPage testReauthPage = TestReauthPage.beginAt(this); + testReauthPage.clickReauth(); + authenticate(user1.email, user1.password); + String reauthToken = testReauthPage.getReauthToken(); + testReauthPage.validateToken(); + + testReauthPage = TestReauthPage.beginAt(this, reauthToken); + testReauthPage.validateTokenExpectingError(); + assertElementPresent(Locator.byClass("labkey-error-heading").withText("Reauthentication validation failed!")); + } + + @Test + public void testReauthAsWrongUser() + { + signInAs(user1); + + TestReauthPage testReauthPage = TestReauthPage.beginAt(this); + testReauthPage.clickReauth(); + authenticate(user2.email, user2.password); + assertElementPresent(Locators.labkeyError.containing("wrong user reauthenticated")); + + testReauthPage.clickReauth(); // Try again + authenticate(user1.email, user1.password); + testReauthPage.validateToken(); + } + + /** + * Test that reauth works when fixing the password after logging in with the wrong password + */ + @Test + public void testReauthWithBadPassword() + { + signInAs(user1); + + TestReauthPage testReauthPage = TestReauthPage.beginAt(this); + testReauthPage.clickReauth(); + authenticateExpectingError(user1.email, user1.password + "wrong"); + + authenticate(user1.email, user1.password); + testReauthPage.validateToken(); + } + + private void signInAs(User user) + { + signOut(); + clickSignIn(); + authenticate(user.email, user.password); + assertSignedInAs(user); + } + + private void assertSignedInAs(User user) + { + Assert.assertEquals("Signed in as", user.email, getCurrentUser()); + } + + @Override + protected String getProjectName() + { + return null; + } + + @Override + public List getAssociatedModules() + { + return Arrays.asList(); + } +} diff --git a/src/org/labkey/test/tests/core/login/DbReauthTest.java b/src/org/labkey/test/tests/core/login/DbReauthTest.java new file mode 100644 index 0000000000..40d0ac4567 --- /dev/null +++ b/src/org/labkey/test/tests/core/login/DbReauthTest.java @@ -0,0 +1,57 @@ +package org.labkey.test.tests.core.login; + +import org.junit.BeforeClass; +import org.labkey.test.Locator; +import org.labkey.test.tests.AbstractReauthTest; +import org.labkey.test.util.PasswordUtil; +import org.labkey.test.util.TestUser; + +public class DbReauthTest extends AbstractReauthTest +{ + private static final TestUser USER1 = new TestUser("db_user1@reauth.test"); + private static final TestUser USER2 = new TestUser("db_user2@reauth.test"); + + public DbReauthTest() + { + super(new User(USER1.getEmail(), PasswordUtil.getPassword()), new User(USER2.getEmail(), PasswordUtil.getPassword())); + } + + @Override + protected void clickSignIn() + { + clickAndWait(Locator.tagWithClass("a", "header-link").withText("Sign In")); + } + + @Override + protected void authenticate(String email, String password) + { + doAndWaitForPageToLoad(() -> fillSignInFormAndSubmit(null, email, password)); + } + + @Override + protected void authenticateExpectingError(String email, String password) + { + fillSignInFormAndSubmit(null, email, password); + } + + @Override + protected void doCleanup(boolean afterTest) + { + _userHelper.deleteUsers(afterTest, USER1, USER2); + } + + @BeforeClass + public static void setupProject() throws Exception + { + DbReauthTest init = getCurrentTest(); + + init.doSetup(); + } + + private void doSetup() + { + USER1.create(this).setInitialPassword(); + USER2.create(this).setInitialPassword(); + } + +}