From 22e41ebbbe1e4508030d7ee5a691d4cdc962d8d0 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Mon, 22 Jun 2026 15:54:42 -0700 Subject: [PATCH 1/3] Add some specimen API tests --- src/org/labkey/test/tests/SpecimenTest.java | 120 +++++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/src/org/labkey/test/tests/SpecimenTest.java b/src/org/labkey/test/tests/SpecimenTest.java index 2afd5888ba..d2f0d4bcc7 100644 --- a/src/org/labkey/test/tests/SpecimenTest.java +++ b/src/org/labkey/test/tests/SpecimenTest.java @@ -18,7 +18,13 @@ import org.apache.commons.lang3.StringUtils; import org.apache.hc.core5.http.HttpStatus; +import org.json.JSONObject; +import org.junit.Assert; import org.junit.experimental.categories.Category; +import org.labkey.remoteapi.CommandException; +import org.labkey.remoteapi.CommandResponse; +import org.labkey.remoteapi.Connection; +import org.labkey.remoteapi.SimplePostCommand; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.Locator; import org.labkey.test.Locators; @@ -36,6 +42,7 @@ import org.labkey.test.util.LogMethod; import org.labkey.test.util.LoggedParam; import org.labkey.test.util.PasswordUtil; +import org.labkey.test.util.PermissionsHelper; import org.labkey.test.util.PortalHelper; import org.labkey.test.util.StudyHelper; import org.labkey.test.util.TextSearcher; @@ -51,6 +58,7 @@ import java.util.HashSet; import java.util.Hashtable; import java.util.List; +import java.util.Map; import java.util.Set; import static org.junit.Assert.assertEquals; @@ -59,6 +67,7 @@ import static org.junit.Assert.assertTrue; import static org.labkey.test.pages.study.specimen.ManageNotificationsPage.SpecimensAttachment; import static org.labkey.test.util.DataRegionTable.DataRegion; +import static org.labkey.test.util.PermissionsHelper.READER_ROLE; @Category({Daily.class, Specimen.class}) @BaseWebDriverTest.ClassTimeout(minutes = 20) @@ -124,11 +133,12 @@ protected void setupRequestabilityRules() @Override @LogMethod - protected void doVerifySteps() throws IOException + protected void doVerifySteps() throws IOException, CommandException { verifyActorDetails(); verifySpecimenEventsRedirect(); createRequest(); + createRequestWithApi(); verifyViews(); verifyAdditionalRequestFields(); verifyNotificationEmails(); @@ -435,6 +445,110 @@ private void createRequest() waitForElement(Locator.css(".labkey-message").withText("Complete")); } + @LogMethod + private void createRequestWithApi() throws IOException, CommandException + { + // Create an empty specimen request as admin user + goToSpecimenData(); + click(Locator.tag("img").withAttributeContaining("alt", "[New Request Icon]")); + selectOptionByText(Locator.name("destinationLocation"), DESTINATION_SITE); + setFormElement(Locator.id("input0"), "Assay Plan"); + setFormElement(Locator.id("input1"), "Shipping"); + setFormElement(Locator.id("input3"), "Comments"); + clickButton("Create and View Details"); + int requestId = Integer.parseInt(getUrlParam("id")); + + // Create commands to add vials, remove vials, add specimens, and cancel the request + JSONObject vialParameters = new JSONObject( + Map.of( + "requestId", requestId, + "idType", "GlobalUniqueId", + "vialIds", new String[]{"ABH00LT8-01"} + ) + ); + SimplePostCommand addVialsCommand = new SimplePostCommand("specimen-api", "addVialsToRequest"); + addVialsCommand.setJsonObject(vialParameters); + SimplePostCommand removeVialsCommand = new SimplePostCommand("specimen-api", "removeVialsFromRequest"); + removeVialsCommand.setJsonObject(vialParameters); + SimplePostCommand addSpecimenCommand = new SimplePostCommand("specimen-api", "addSpecimensToRequest"); + addSpecimenCommand.setJsonObject(new JSONObject( + Map.of( + "requestId", requestId, + "specimenHashes", new String[]{"FakeSpecimenHashThatDoesntMatter"} + ) + )); + SimplePostCommand cancelRequestCommand = new SimplePostCommand("specimen-api", "cancelRequest"); + cancelRequestCommand.setJsonObject(new JSONObject( + Map.of( + "requestId", requestId + ) + )); + + // Assign "Specimen Requester" role to USER1 + String containerPath = getCurrentContainerPath(); + log(containerPath); + ApiPermissionsHelper helper = new ApiPermissionsHelper(containerPath); + helper.uncheckInheritedPermissions(); // Uses UI to un-inherit and save... + clickButton("Cancel"); // ...so click button to return to specimen page + helper.addMemberToRole(USER1, READER_ROLE, PermissionsHelper.MemberType.user, containerPath); + helper.addMemberToRole(USER1, "Specimen Requester", PermissionsHelper.MemberType.user, containerPath); + Connection conn = createDefaultConnection(); + + // Impersonate USER1. Specimen Requester role who doesn't own the request shouldn't be able to modify it. + impersonate(USER1); + Assert.assertThrows("Request " + requestId + " was not found or the current user does not have permissions to access it.", + CommandException.class, + () -> addVialsCommand.execute(conn, containerPath) + ); + Assert.assertThrows("Request " + requestId + " was not found or the current user does not have permissions to access it.", + CommandException.class, + () -> addSpecimenCommand.execute(conn, containerPath) + ); + Assert.assertThrows("Request " + requestId + " was not found or the current user does not have permissions to access it.", + CommandException.class, + () -> cancelRequestCommand.execute(conn, containerPath) + ); + stopImpersonating(false); + + // Verify that the owner can add a vial + impersonateRoles(READER_ROLE, "Specimen Requester"); + CommandResponse response = addVialsCommand.execute(conn, containerPath); + assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + //noinspection unchecked + assertEquals(1, ((List)response.getParsedData().get("vials")).size()); + stopImpersonating(false); + + // Attempt to remove the just-added vial as non-owner + impersonate(USER1); + Assert.assertThrows("Request " + requestId + " was not found or the current user does not have permissions to access it.", + CommandException.class, + () -> removeVialsCommand.execute(conn, containerPath) + ); + stopImpersonating(false); + + // Owner can remove vials and add via specimen hash + impersonateRoles(READER_ROLE, "Specimen Requester"); + response = removeVialsCommand.execute(conn, containerPath); + assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + //noinspection unchecked + assertEquals(0, ((List)response.getParsedData().get("vials")).size()); + response = addSpecimenCommand.execute(conn, containerPath); + assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + stopImpersonating(false); + + // Now give USER1 the "Specimen Coordinator" role and verify they can modify the request, including cancel + helper.addMemberToRole(USER1, "Specimen Coordinator", PermissionsHelper.MemberType.user, containerPath); + impersonate(USER1); + assertEquals(HttpStatus.SC_OK, addVialsCommand.execute(conn, containerPath).getStatusCode()); + assertEquals(HttpStatus.SC_OK, removeVialsCommand.execute(conn, containerPath).getStatusCode()); + assertEquals(HttpStatus.SC_OK, addSpecimenCommand.execute(conn, containerPath).getStatusCode()); + assertEquals(HttpStatus.SC_OK, cancelRequestCommand.execute(conn, containerPath).getStatusCode()); + stopImpersonating(false); + + // Restore permissions back to inherit + helper.checkInheritedPermissions(); + } + @LogMethod private void verifyViews() { @@ -584,8 +698,8 @@ private void verifyNotificationEmails() assertTrue(emailMessages1.getFirst().getBody().contains(_specimen_McMichael)); assertNotNull("No message found", emailMessages1); String messageBody = emailMessages2.getFirst().getBody().replaceFirst("-*=_Part_\\d{3}_\\d*.\\d*\\n",""); - messageBody = messageBody.replaceAll("Content-Type: text\\/html; charset=UTF-8\n",""); - messageBody = messageBody.replaceAll("Content-Transfer-Encoding: 7bit\n", ""); + messageBody = messageBody.replace("Content-Type: text/html; charset=UTF-8\n",""); + messageBody = messageBody.replace("Content-Transfer-Encoding: 7bit\n", ""); assertTrue("Notification was not as expected.\nExpected:\n" + notification + "\n\nActual:\n" + messageBody, messageBody.contains(notification)); String attachment1 = getAttribute(Locator.linkWithText(ATTACHMENT1), "href"); From 771bf7ac108ea55ed42be88550085423d24e373d Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 23 Jun 2026 08:08:51 -0700 Subject: [PATCH 2/3] Reorder test cases --- src/org/labkey/test/tests/SpecimenTest.java | 170 ++++++++++---------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/src/org/labkey/test/tests/SpecimenTest.java b/src/org/labkey/test/tests/SpecimenTest.java index d2f0d4bcc7..fba464cbef 100644 --- a/src/org/labkey/test/tests/SpecimenTest.java +++ b/src/org/labkey/test/tests/SpecimenTest.java @@ -137,8 +137,8 @@ protected void doVerifySteps() throws IOException, CommandException { verifyActorDetails(); verifySpecimenEventsRedirect(); - createRequest(); createRequestWithApi(); + createRequest(); verifyViews(); verifyAdditionalRequestFields(); verifyNotificationEmails(); @@ -332,7 +332,7 @@ private void verifyActorDetails() } // Simulate SpecimenForeignKey redirect behavior - @LogMethod (quiet = true) + @LogMethod private void verifySpecimenEventsRedirect() { String targetStudyId = getContainerId(); @@ -362,89 +362,6 @@ private void verifySpecimenEventsRedirect() clickFolder(getFolderName()); } - @LogMethod - private void createRequest() - { - goToSpecimenData(); - click(Locator.xpath("//span[text()='Vials by Derivative Type']/../img")); - waitForElement(Locator.linkWithText("Plasma, Unknown Processing")); - clickAndWait(Locator.linkWithText("Plasma, Unknown Processing")); - // Verify unavailable sample - assertElementPresent(Locator.xpath("//input[@id='check_" + UNREQUESTABLE_SAMPLE + "' and @disabled]")); - assertElementPresent(Locator.xpath("//input[@id='check_" + UNREQUESTABLE_SAMPLE + "']/../../td[contains(text(), 'This vial is unavailable because it was found in the set called \"" + REQUESTABILITY_QUERY + "\".')]")); -// TODO ONMOUSEOVER assertElementPresent(Locator.xpath("//input[@id='check_" + UNREQUESTABLE_SAMPLE + "']/../a[contains(@onmouseover, 'This vial is unavailable because it was found in the set called \\\"" + REQUESTABILITY_QUERY + "\\\".')]")); - new DataRegionTable.DataRegionFinder(getDriver()).find() - .checkAllOnPage(); - - clickAndWait(Locator.linkContainingText("history")); - assertTextPresent("Vial History"); - goBack(); - - BootstrapMenu.find(getDriver(), "Request Options").clickSubMenu(true, "Create New Request"); - selectOptionByText(Locator.name("destinationLocation"), DESTINATION_SITE); - setFormElement(Locator.id("input0"), "Assay Plan"); - setFormElement(Locator.id("input2"), "Comments"); - setFormElement(Locator.id("input1"), "Shipping"); - clickButton("Create and View Details"); - assertTextPresent("Please provide all required input."); - setFormElement(Locator.id("input3"), "sample last one input"); - clickButton("Create and View Details"); - assertTextPresent("sample last one input", "IRB", "KCMC, Moshi, Tanzania", "Originating IRB Approval", - SOURCE_SITE, "Providing IRB Approval", DESTINATION_SITE, "Receiving IRB Approval", "SLG", - "SLG Approval", "BAA07XNP-01"); - assertTextNotPresent( - UNREQUESTABLE_SAMPLE, - // verify that the swab specimen isn't present yet - "DAA07YGW-01", "Complete"); - - // add additional specimens - goToSpecimenData(); - click(Locator.xpath("//span[text()='Vials by Derivative Type']/../img")); - waitAndClickAndWait(Locator.linkWithText("Swab")); - new DataRegionTable.DataRegionFinder(getDriver()).find() - .checkAllOnPage(); - BootstrapMenu.find(getDriver(), "Request Options").clickSubMenu(false, "Add To Existing Request"); - _extHelper.waitForExtDialog("Request Vial", WAIT_FOR_JAVASCRIPT); - waitForElement(Locator.css("#request-vial-details .x-grid3-row")); - clickButton("Add 8 Vials to Request", 0); - _extHelper.waitForExtDialog("Success", WAIT_FOR_JAVASCRIPT * 5); - clickButton("OK", 0); - BootstrapMenu.find(getDriver(), "Request Options").clickSubMenu(false, "View Existing Requests"); - clickButton("Details"); - assertTextPresent("sample last one input", "IRB", "KCMC, Moshi, Tanzania", "Originating IRB Approval", - SOURCE_SITE, "Providing IRB Approval", DESTINATION_SITE, "Receiving IRB Approval", "SLG", - "SLG Approval", "BAA07XNP-01", "DAA07YGW-01"); - - // submit request - assertTextPresent("Not Yet Submitted"); - assertTextNotPresent("New Request"); - doAndWaitForPageToLoad(() -> - { - clickButton("Submit Request", 0); - assertAlertIgnoreCaseAndSpaces("Once a request is submitted, its specimen list may no longer be modified. Continue?"); - }); - waitForText("New Request"); - assertTextNotPresent("Not Yet Submitted"); - - // Add request attachment - click(Locator.linkWithText("Update Request")); - waitForElement(Locator.name("formFiles[0]")); - setFormElement(Locator.name("formFiles[0]"), REQUEST_ATTACHMENT); - clickButton("Save Changes and Send Notifications"); - waitForElement(Locator.linkContainingText(REQUEST_ATTACHMENT.getName())); - - // modify request - selectOptionByText(Locator.name("newActor"), "SLG"); - setFormElement(Locator.name("newDescription"), "Other SLG Approval"); - clickButton("Add Requirement"); - clickAndWait(Locator.linkWithText("Details")); - checkCheckbox(Locator.checkboxByName("complete")); - checkCheckbox(Locator.checkboxByName("notificationIdPairs")); - checkCheckbox(Locator.checkboxByName("notificationIdPairs").index(1)); - clickButton("Save Changes and Send Notifications"); - waitForElement(Locator.css(".labkey-message").withText("Complete")); - } - @LogMethod private void createRequestWithApi() throws IOException, CommandException { @@ -549,6 +466,89 @@ private void createRequestWithApi() throws IOException, CommandException helper.checkInheritedPermissions(); } + @LogMethod + private void createRequest() + { + goToSpecimenData(); + click(Locator.xpath("//span[text()='Vials by Derivative Type']/../img")); + waitForElement(Locator.linkWithText("Plasma, Unknown Processing")); + clickAndWait(Locator.linkWithText("Plasma, Unknown Processing")); + // Verify unavailable sample + assertElementPresent(Locator.xpath("//input[@id='check_" + UNREQUESTABLE_SAMPLE + "' and @disabled]")); + assertElementPresent(Locator.xpath("//input[@id='check_" + UNREQUESTABLE_SAMPLE + "']/../../td[contains(text(), 'This vial is unavailable because it was found in the set called \"" + REQUESTABILITY_QUERY + "\".')]")); +// TODO ONMOUSEOVER assertElementPresent(Locator.xpath("//input[@id='check_" + UNREQUESTABLE_SAMPLE + "']/../a[contains(@onmouseover, 'This vial is unavailable because it was found in the set called \\\"" + REQUESTABILITY_QUERY + "\\\".')]")); + new DataRegionTable.DataRegionFinder(getDriver()).find() + .checkAllOnPage(); + + clickAndWait(Locator.linkContainingText("history")); + assertTextPresent("Vial History"); + goBack(); + + BootstrapMenu.find(getDriver(), "Request Options").clickSubMenu(true, "Create New Request"); + selectOptionByText(Locator.name("destinationLocation"), DESTINATION_SITE); + setFormElement(Locator.id("input0"), "Assay Plan"); + setFormElement(Locator.id("input2"), "Comments"); + setFormElement(Locator.id("input1"), "Shipping"); + clickButton("Create and View Details"); + assertTextPresent("Please provide all required input."); + setFormElement(Locator.id("input3"), "sample last one input"); + clickButton("Create and View Details"); + assertTextPresent("sample last one input", "IRB", "KCMC, Moshi, Tanzania", "Originating IRB Approval", + SOURCE_SITE, "Providing IRB Approval", DESTINATION_SITE, "Receiving IRB Approval", "SLG", + "SLG Approval", "BAA07XNP-01"); + assertTextNotPresent( + UNREQUESTABLE_SAMPLE, + // verify that the swab specimen isn't present yet + "DAA07YGW-01", "Complete"); + + // add additional specimens + goToSpecimenData(); + click(Locator.xpath("//span[text()='Vials by Derivative Type']/../img")); + waitAndClickAndWait(Locator.linkWithText("Swab")); + new DataRegionTable.DataRegionFinder(getDriver()).find() + .checkAllOnPage(); + BootstrapMenu.find(getDriver(), "Request Options").clickSubMenu(false, "Add To Existing Request"); + _extHelper.waitForExtDialog("Request Vial", WAIT_FOR_JAVASCRIPT); + waitForElement(Locator.css("#request-vial-details .x-grid3-row")); + clickButton("Add 8 Vials to Request", 0); + _extHelper.waitForExtDialog("Success", WAIT_FOR_JAVASCRIPT * 5); + clickButton("OK", 0); + BootstrapMenu.find(getDriver(), "Request Options").clickSubMenu(false, "View Existing Requests"); + clickButton("Details"); + assertTextPresent("sample last one input", "IRB", "KCMC, Moshi, Tanzania", "Originating IRB Approval", + SOURCE_SITE, "Providing IRB Approval", DESTINATION_SITE, "Receiving IRB Approval", "SLG", + "SLG Approval", "BAA07XNP-01", "DAA07YGW-01"); + + // submit request + assertTextPresent("Not Yet Submitted"); + assertTextNotPresent("New Request"); + doAndWaitForPageToLoad(() -> + { + clickButton("Submit Request", 0); + assertAlertIgnoreCaseAndSpaces("Once a request is submitted, its specimen list may no longer be modified. Continue?"); + }); + waitForText("New Request"); + assertTextNotPresent("Not Yet Submitted"); + + // Add request attachment + click(Locator.linkWithText("Update Request")); + waitForElement(Locator.name("formFiles[0]")); + setFormElement(Locator.name("formFiles[0]"), REQUEST_ATTACHMENT); + clickButton("Save Changes and Send Notifications"); + waitForElement(Locator.linkContainingText(REQUEST_ATTACHMENT.getName())); + + // modify request + selectOptionByText(Locator.name("newActor"), "SLG"); + setFormElement(Locator.name("newDescription"), "Other SLG Approval"); + clickButton("Add Requirement"); + clickAndWait(Locator.linkWithText("Details")); + checkCheckbox(Locator.checkboxByName("complete")); + checkCheckbox(Locator.checkboxByName("notificationIdPairs")); + checkCheckbox(Locator.checkboxByName("notificationIdPairs").index(1)); + clickButton("Save Changes and Send Notifications"); + waitForElement(Locator.css(".labkey-message").withText("Complete")); + } + @LogMethod private void verifyViews() { From b29d886d7f614d4377f7ce7529306b1203346a4b Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 23 Jun 2026 12:20:08 -0700 Subject: [PATCH 3/3] Clarify comments --- src/org/labkey/test/tests/SpecimenTest.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/org/labkey/test/tests/SpecimenTest.java b/src/org/labkey/test/tests/SpecimenTest.java index fba464cbef..bfdad9fdeb 100644 --- a/src/org/labkey/test/tests/SpecimenTest.java +++ b/src/org/labkey/test/tests/SpecimenTest.java @@ -411,7 +411,7 @@ private void createRequestWithApi() throws IOException, CommandException helper.addMemberToRole(USER1, "Specimen Requester", PermissionsHelper.MemberType.user, containerPath); Connection conn = createDefaultConnection(); - // Impersonate USER1. Specimen Requester role who doesn't own the request shouldn't be able to modify it. + // Impersonate USER1. Specimen Requester who doesn't own the request shouldn't be able to modify it. impersonate(USER1); Assert.assertThrows("Request " + requestId + " was not found or the current user does not have permissions to access it.", CommandException.class, @@ -427,7 +427,7 @@ private void createRequestWithApi() throws IOException, CommandException ); stopImpersonating(false); - // Verify that the owner can add a vial + // Let's have the "Specimen Requester" owner add a vial so USER1 has something to (attempt to) delete impersonateRoles(READER_ROLE, "Specimen Requester"); CommandResponse response = addVialsCommand.execute(conn, containerPath); assertEquals(HttpStatus.SC_OK, response.getStatusCode()); @@ -435,7 +435,7 @@ private void createRequestWithApi() throws IOException, CommandException assertEquals(1, ((List)response.getParsedData().get("vials")).size()); stopImpersonating(false); - // Attempt to remove the just-added vial as non-owner + // Attempt to remove the just-added vial as "Specimen Requester" non-owner impersonate(USER1); Assert.assertThrows("Request " + requestId + " was not found or the current user does not have permissions to access it.", CommandException.class, @@ -443,7 +443,8 @@ private void createRequestWithApi() throws IOException, CommandException ); stopImpersonating(false); - // Owner can remove vials and add via specimen hash + // "Specimen Requester" owner should be able to remove vials and add via specimen hash. This hash won't match + // any vials, but we're just checking permissions. impersonateRoles(READER_ROLE, "Specimen Requester"); response = removeVialsCommand.execute(conn, containerPath); assertEquals(HttpStatus.SC_OK, response.getStatusCode()); @@ -453,7 +454,8 @@ private void createRequestWithApi() throws IOException, CommandException assertEquals(HttpStatus.SC_OK, response.getStatusCode()); stopImpersonating(false); - // Now give USER1 the "Specimen Coordinator" role and verify they can modify the request, including cancel + // Now give USER1 the (more powerful) "Specimen Coordinator" role and verify they can now modify the request + // that they don't own, including canceling it. helper.addMemberToRole(USER1, "Specimen Coordinator", PermissionsHelper.MemberType.user, containerPath); impersonate(USER1); assertEquals(HttpStatus.SC_OK, addVialsCommand.execute(conn, containerPath).getStatusCode());