From 5eaf462569d780c2f07ba4bca2b999158b6fb245 Mon Sep 17 00:00:00 2001 From: srosse <stephane.rosse@frentix.com> Date: Tue, 9 Jun 2020 07:20:55 +0200 Subject: [PATCH] no-jira: add selenium tests for expert rules, learn path --- .../org/olat/selenium/AssessmentTest.java | 255 +++++++++------ .../olat/selenium/CourseLearnPathTest.java | 295 ++++++++++++++++++ .../java/org/olat/selenium/CourseTest.java | 1 + .../page/course/CoursePageFragment.java | 47 ++- .../org/olat/selenium/page/qti/QTI21Page.java | 17 +- .../course_certificates_exrules.zip | Bin 0 -> 27733 bytes 6 files changed, 509 insertions(+), 106 deletions(-) create mode 100644 src/test/java/org/olat/selenium/CourseLearnPathTest.java create mode 100644 src/test/java/org/olat/test/file_resources/course_certificates_exrules.zip diff --git a/src/test/java/org/olat/selenium/AssessmentTest.java b/src/test/java/org/olat/selenium/AssessmentTest.java index 8ba2b269c9e..38ffea5ff45 100644 --- a/src/test/java/org/olat/selenium/AssessmentTest.java +++ b/src/test/java/org/olat/selenium/AssessmentTest.java @@ -34,7 +34,7 @@ import org.jboss.arquillian.test.api.ArquillianResource; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; -import org.olat.course.learningpath.FullyAssessedTrigger; +import org.olat.repository.RepositoryEntryStatusEnum; import org.olat.repository.model.SingleRoleRepositoryEntrySecurity.Role; import org.olat.selenium.page.LoginPage; import org.olat.selenium.page.NavigationPage; @@ -55,6 +55,8 @@ import org.olat.selenium.page.course.MembersPage; import org.olat.selenium.page.graphene.OOGraphene; import org.olat.selenium.page.group.GroupPage; import org.olat.selenium.page.qti.QTI12Page; +import org.olat.selenium.page.qti.QTI21Page; +import org.olat.selenium.page.repository.RepositoryEditDescriptionPage; import org.olat.selenium.page.repository.ScormPage; import org.olat.selenium.page.repository.UserAccess; import org.olat.selenium.page.user.UserToolsPage; @@ -807,6 +809,154 @@ public class AssessmentTest extends Deployments { .selectStatementSegment() .assertOnCourseDetails(testNodeTitle, true); } + + /** + * This tests a course with cascading rules and expert rules + * to calculate if the course is passed and generate a + * certificate. + * + * @param loginPage + */ + @Test + @RunAsClient + public void certificatesGeneratedWithCascadingRules() + throws IOException, URISyntaxException { + + UserVO author = new UserRestClient(deploymentUrl).createRandomAuthor(); + UserVO participant1 = new UserRestClient(deploymentUrl).createRandomUser("Ryomou"); + UserVO participant2 = new UserRestClient(deploymentUrl).createRandomUser("Rei"); + + LoginPage loginPage = LoginPage.load(browser, deploymentUrl); + loginPage.loginAs(author.getLogin(), author.getPassword()); + + URL zipUrl = JunitTestHelper.class.getResource("file_resources/course_certificates_exrules.zip"); + File zipFile = new File(zipUrl.toURI()); + //go the authoring environment to import our course + String zipTitle = "Certif - " + UUID.randomUUID(); + NavigationPage navBar = NavigationPage.load(browser); + navBar + .openAuthoringEnvironment() + .uploadResource(zipTitle, zipFile); + + // publish the course + new RepositoryEditDescriptionPage(browser) + .clickToolbarBack(); + CoursePageFragment course = CoursePageFragment.getCourse(browser) + .edit() + .autoPublish(); + + // add a participant + MembersPage members = course + .members(); + members + .importMembers() + .setMembers(participant1, participant2) + .nextUsers() + .nextOverview() + .nextPermissions() + .finish(); + members + .clickToolbarBack(); + + course + .settings() + .accessConfiguration() + .setUserAccess(UserAccess.registred) + .save() + .clickToolbarBack(); + + course + .changeStatus(RepositoryEntryStatusEnum.published); + + //log out + new UserToolsPage(browser) + .logout(); + + // participant log in and go directly to the first test + LoginPage participantLoginPage = LoginPage.load(browser, deploymentUrl); + + participantLoginPage + .loginAs(participant1.getLogin(), participant1.getPassword()) + .resume(); + + //open the course + NavigationPage participantNavBar = NavigationPage.load(browser); + participantNavBar + .openMyCourses() + .select(zipTitle); + + //go to the test + CoursePageFragment certificationCourse = new CoursePageFragment(browser); + certificationCourse + .clickTree() + .assertWithTitleSelected("Test 1"); + //pass the test + QTI21Page.getQTI21Page(browser) + .passE4() + .assertOnCourseAssessmentTestScore(4); + + OOGraphene.waitingALittleLonger(); + + //open the efficiency statements + String certificateTitle = "Certificates" + zipTitle; + UserToolsPage participantUserTools = new UserToolsPage(browser); + participantUserTools + .openUserToolsMenu() + .openMyEfficiencyStatement() + .assertOnEfficiencyStatmentPage() + .assertOnCertificateAndStatements(certificateTitle) + .selectStatement(certificateTitle) + .selectStatementSegment() + .assertOnCourseDetails("CertificatesCert", true) + .assertOnCourseDetails("Struktur 1", true) + .assertOnCourseDetails("Test 1", true); + + //log out + new UserToolsPage(browser) + .logout(); + + + // participant 2 log in and go directly to the second test + LoginPage participant2LoginPage = LoginPage.load(browser, deploymentUrl); + + participant2LoginPage + .loginAs(participant2.getLogin(), participant2.getPassword()) + .resume(); + + //open the course + NavigationPage participant2NavBar = NavigationPage.load(browser); + participant2NavBar + .openMyCourses() + .select(zipTitle); + + //go to the test + CoursePageFragment certification2Course = new CoursePageFragment(browser); + certification2Course + .clickTree() + .selectWithTitle("Struktur 3") + .assertWithTitleSelected("Struktur 3") + .assertWithTitle("Test 3") + .selectWithTitle("Test 3"); + //pass the test + QTI21Page.getQTI21Page(browser) + .passE4() + .assertOnCourseAssessmentTestScore(4); + + OOGraphene.waitingALittleLonger(); + + //open the efficiency statements + UserToolsPage participant2UserTools = new UserToolsPage(browser); + participant2UserTools + .openUserToolsMenu() + .openMyEfficiencyStatement() + .assertOnEfficiencyStatmentPage() + .assertOnCertificateAndStatements(certificateTitle) + .selectStatement(certificateTitle) + .selectStatementSegment() + .assertOnCourseDetails("CertificatesCert", true) + .assertOnCourseDetails("Struktur 3", true) + .assertOnCourseDetails("Test 3", true); + } /** * An author create a course with an assessment course element with @@ -2114,107 +2264,4 @@ public class AssessmentTest extends Deployments { .openSolutions() .assertSolution("solution_1.txt"); } - - /** - * This is a degenerated form of task but the case exists. - * An author creates a course (learn path) with a task course - * element. The task is configured to only show the solution, - * and the course element to be passed if the task is done. In - * this case, the participant only need to see the solution to - * get the node done. - * - * @param participantBrowser Browser of the participant - * @throws IOException - * @throws URISyntaxException - */ - @Test - @RunAsClient - public void taskLearnPathSolutionsOnly(@Drone @User WebDriver participantBrowser) - throws IOException, URISyntaxException { - - UserVO author = new UserRestClient(deploymentUrl).createRandomAuthor(); - UserVO participant = new UserRestClient(deploymentUrl).createRandomUser("kanu"); - - LoginPage authorLoginPage = LoginPage.load(browser, deploymentUrl); - authorLoginPage.loginAs(author.getLogin(), author.getPassword()); - - //create a course - String courseTitle = "Course-with-auto-task-" + UUID.randomUUID(); - NavigationPage navBar = NavigationPage.load(browser); - navBar - .openAuthoringEnvironment() - .createCourse(courseTitle, true) - .clickToolbarBack(); - - //create a course element of type Test with the test that we create above - String gtaNodeTitle = "Solution 1"; - CourseEditorPageFragment courseEditor = CoursePageFragment.getCourse(browser) - .edit(); - courseEditor - .createNode("ita") - .nodeTitle(gtaNodeTitle); - - courseEditor - .selectTabLearnPath() - .setCompletionCriterion(FullyAssessedTrigger.statusDone) - .save(); - - GroupTaskConfigurationPage gtaConfig = new GroupTaskConfigurationPage(browser); - gtaConfig - .selectWorkflow() - .enableAssignment(false) - .enableSubmission(false) - .enableReview(false) - .enableGrading(false) - .saveWorkflow(); - - URL solutionUrl = JunitTestHelper.class.getResource("file_resources/solution_1.txt"); - File solutionFile = new File(solutionUrl.toURI()); - gtaConfig - .selectSolution() - .uploadSolution("A possible solution", solutionFile); - - courseEditor - .publish() - .quickPublish(UserAccess.membersOnly); - - - MembersPage membersPage = courseEditor - .clickToolbarBack() - .members(); - - membersPage - .importMembers() - .setMembers(participant) - .nextUsers() - .nextOverview() - .nextPermissions() - .finish(); - - //Participant log in - LoginPage participantLoginPage = LoginPage.load(participantBrowser, deploymentUrl); - participantLoginPage - .loginAs(participant) - .resume(); - - //open the course - NavigationPage participantNavBar = NavigationPage.load(participantBrowser); - participantNavBar - .openMyCourses() - .select(courseTitle); - - //go to the group task - CoursePageFragment participantCourse = new CoursePageFragment(participantBrowser); - participantCourse - .clickTree() - .selectWithTitle(gtaNodeTitle); - - GroupTaskPage participantTask = new GroupTaskPage(participantBrowser); - participantTask - .openSolutions() - .assertSolution("solution_1.txt"); - // seeing the solution got the job done - participantCourse - .assertOnLearnPathNodeDone(gtaNodeTitle); - } } diff --git a/src/test/java/org/olat/selenium/CourseLearnPathTest.java b/src/test/java/org/olat/selenium/CourseLearnPathTest.java new file mode 100644 index 00000000000..adcc2e7b104 --- /dev/null +++ b/src/test/java/org/olat/selenium/CourseLearnPathTest.java @@ -0,0 +1,295 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.selenium; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.UUID; + +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.olat.course.learningpath.FullyAssessedTrigger; +import org.olat.selenium.page.LoginPage; +import org.olat.selenium.page.NavigationPage; +import org.olat.selenium.page.User; +import org.olat.selenium.page.core.MenuTreePageFragment; +import org.olat.selenium.page.course.CourseEditorPageFragment; +import org.olat.selenium.page.course.CoursePageFragment; +import org.olat.selenium.page.course.GroupTaskConfigurationPage; +import org.olat.selenium.page.course.GroupTaskPage; +import org.olat.selenium.page.course.MembersPage; +import org.olat.selenium.page.course.SinglePageConfigurationPage; +import org.olat.selenium.page.repository.UserAccess; +import org.olat.test.JunitTestHelper; +import org.olat.test.rest.UserRestClient; +import org.olat.user.restapi.UserVO; +import org.openqa.selenium.WebDriver; + +/** + * Test specifically features of the course in learn path mode. + * + * Initial date: 8 juin 2020<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +@RunWith(Arquillian.class) +public class CourseLearnPathTest extends Deployments { + + @Drone + private WebDriver browser; + @ArquillianResource + private URL deploymentUrl; + + + /** + * This is a course with learn path, three course elements, + * two to confirm, one to visit. The author creates the course + * and a user play it until 100% done. + * + * @param participantBrowser Browser of the participant + * @throws IOException + * @throws URISyntaxException + */ + @Test + @RunAsClient + public void learnPathConfirmationAndVisitedFlow(@Drone @User WebDriver participantBrowser) + throws IOException, URISyntaxException { + + UserVO author = new UserRestClient(deploymentUrl).createRandomAuthor(); + UserVO participant = new UserRestClient(deploymentUrl).createRandomUser("kanu"); + + LoginPage authorLoginPage = LoginPage.load(browser, deploymentUrl); + authorLoginPage.loginAs(author.getLogin(), author.getPassword()); + + //create a course + String courseTitle = "Course-with-auto-task-" + UUID.randomUUID(); + NavigationPage navBar = NavigationPage.load(browser); + navBar + .openAuthoringEnvironment() + .createCourse(courseTitle, true) + .clickToolbarBack(); + + //create a course element of type single page + String firstNodeTitle = "First page"; + CourseEditorPageFragment courseEditor = CoursePageFragment.getCourse(browser) + .edit(); + courseEditor + .createNode("sp") + .nodeTitle(firstNodeTitle); + + String content = "A new single page with some content"; + SinglePageConfigurationPage spConfiguration = new SinglePageConfigurationPage(browser); + spConfiguration + .selectConfiguration() + .newDefaultPage(content) + .assertOnPreview(); + + courseEditor + .selectTabLearnPath() + .setCompletionCriterion(FullyAssessedTrigger.confirmed) + .save(); + + // create a second element, a forum + String forumNodeTitle = "Forum discussion"; + courseEditor + .createNode("fo") + .nodeTitle(forumNodeTitle) + .selectTabLearnPath() + .setCompletionCriterion(FullyAssessedTrigger.confirmed) + .save(); + + // create a third element, an info message + String infosNodeTitle = "Informations"; + courseEditor + .createNode("info") + .nodeTitle(infosNodeTitle) + .selectTabLearnPath() + .setCompletionCriterion(FullyAssessedTrigger.nodeVisited) + .save(); + + courseEditor + .autoPublish() + .publish() + .members() + .importMembers() + .setMembers(participant) + .nextUsers() + .nextOverview() + .nextPermissions() + .finish(); + + //Participant log in + LoginPage participantLoginPage = LoginPage.load(participantBrowser, deploymentUrl); + participantLoginPage + .loginAs(participant) + .resume(); + + //open the course + NavigationPage participantNavBar = NavigationPage.load(participantBrowser); + participantNavBar + .openMyCourses() + .select(courseTitle); + + //go to the group task + CoursePageFragment participantCourse = new CoursePageFragment(participantBrowser); + MenuTreePageFragment menuTree = participantCourse + .clickTree() + .selectWithTitle(firstNodeTitle); + participantCourse + .assertOnLearnPathNodeReady(firstNodeTitle) + .confirmNode() + .assertOnLearnPathNodeDone(firstNodeTitle) + .assertOnLearnPathNodeInProgress(courseTitle) + .assertOnLearnPathNodeReady(forumNodeTitle) + .assertOnLearnPathNodeNotAccessible(infosNodeTitle); + + // confirm second node + menuTree + .selectWithTitle(forumNodeTitle) + .assertWithTitleSelected(forumNodeTitle); + participantCourse + .confirmNode() + .assertOnLearnPathNodeDone(forumNodeTitle) + .assertOnLearnPathNodeInProgress(courseTitle) + .assertOnLearnPathNodeReady(infosNodeTitle); + + // see the third node + menuTree + .selectWithTitle(infosNodeTitle) + .assertWithTitleSelected(infosNodeTitle); + participantCourse + .assertOnLearnPathNodeDone(infosNodeTitle) + .assertOnLearnPathNodeDone(firstNodeTitle) + .assertOnLearnPathNodeDone(forumNodeTitle) + .assertOnLearnPathNodeDone(courseTitle) + .assertOnLearnPathPercent(100); + } + + + /** + * This is a degenerated form of task but the case exists. + * An author creates a course (learn path) with a task course + * element. The task is configured to only show the solution, + * and the course element to be passed if the task is done. In + * this case, the participant only need to see the solution to + * get the node done. + * + * @param participantBrowser Browser of the participant + * @throws IOException + * @throws URISyntaxException + */ + @Test + @RunAsClient + public void taskLearnPathSolutionsOnly(@Drone @User WebDriver participantBrowser) + throws IOException, URISyntaxException { + + UserVO author = new UserRestClient(deploymentUrl).createRandomAuthor(); + UserVO participant = new UserRestClient(deploymentUrl).createRandomUser("kanu"); + + LoginPage authorLoginPage = LoginPage.load(browser, deploymentUrl); + authorLoginPage.loginAs(author.getLogin(), author.getPassword()); + + //create a course + String courseTitle = "Course-with-auto-task-" + UUID.randomUUID(); + NavigationPage navBar = NavigationPage.load(browser); + navBar + .openAuthoringEnvironment() + .createCourse(courseTitle, true) + .clickToolbarBack(); + + //create a course element of type Test with the test that we create above + String gtaNodeTitle = "Solution 1"; + CourseEditorPageFragment courseEditor = CoursePageFragment.getCourse(browser) + .edit(); + courseEditor + .createNode("ita") + .nodeTitle(gtaNodeTitle); + + courseEditor + .selectTabLearnPath() + .setCompletionCriterion(FullyAssessedTrigger.statusDone) + .save(); + + GroupTaskConfigurationPage gtaConfig = new GroupTaskConfigurationPage(browser); + gtaConfig + .selectWorkflow() + .enableAssignment(false) + .enableSubmission(false) + .enableReview(false) + .enableGrading(false) + .saveWorkflow(); + + URL solutionUrl = JunitTestHelper.class.getResource("file_resources/solution_1.txt"); + File solutionFile = new File(solutionUrl.toURI()); + gtaConfig + .selectSolution() + .uploadSolution("A possible solution", solutionFile); + + courseEditor + .publish() + .quickPublish(UserAccess.membersOnly); + + + MembersPage membersPage = courseEditor + .clickToolbarBack() + .members(); + + membersPage + .importMembers() + .setMembers(participant) + .nextUsers() + .nextOverview() + .nextPermissions() + .finish(); + + //Participant log in + LoginPage participantLoginPage = LoginPage.load(participantBrowser, deploymentUrl); + participantLoginPage + .loginAs(participant) + .resume(); + + //open the course + NavigationPage participantNavBar = NavigationPage.load(participantBrowser); + participantNavBar + .openMyCourses() + .select(courseTitle); + + //go to the group task + CoursePageFragment participantCourse = new CoursePageFragment(participantBrowser); + participantCourse + .clickTree() + .selectWithTitle(gtaNodeTitle); + + GroupTaskPage participantTask = new GroupTaskPage(participantBrowser); + participantTask + .openSolutions() + .assertSolution("solution_1.txt"); + // seeing the solution got the job done + participantCourse + .assertOnLearnPathNodeDone(gtaNodeTitle); + } + +} diff --git a/src/test/java/org/olat/selenium/CourseTest.java b/src/test/java/org/olat/selenium/CourseTest.java index d2d6404c62d..b107f1c54c6 100644 --- a/src/test/java/org/olat/selenium/CourseTest.java +++ b/src/test/java/org/olat/selenium/CourseTest.java @@ -1738,4 +1738,5 @@ public class CourseTest extends Deployments { .uploadResource(zipTitle, zipFile) .assertOnResourceType(); } + } diff --git a/src/test/java/org/olat/selenium/page/course/CoursePageFragment.java b/src/test/java/org/olat/selenium/page/course/CoursePageFragment.java index 5700988b382..d229a6416a0 100644 --- a/src/test/java/org/olat/selenium/page/course/CoursePageFragment.java +++ b/src/test/java/org/olat/selenium/page/course/CoursePageFragment.java @@ -93,11 +93,36 @@ public class CoursePageFragment { } public CoursePageFragment assertOnLearnPathNodeDone(String nodeTitle) { - By nodeDoneBy = By.xpath("//div[contains(@class,'o_lp_tree')]//span[contains(@class,'o_tree_l1')]/a[i[contains(@class,'o_lp_done')]][span[text()[contains(.,'" + nodeTitle + "')]]]"); + return assertOnLearnPathNodeStatus(nodeTitle, "o_lp_done"); + } + + public CoursePageFragment assertOnLearnPathNodeReady(String nodeTitle) { + return assertOnLearnPathNodeStatus(nodeTitle, "o_lp_ready"); + } + + public CoursePageFragment assertOnLearnPathNodeInProgress(String nodeTitle) { + return assertOnLearnPathNodeStatus(nodeTitle, "o_lp_in_progress"); + } + + public CoursePageFragment assertOnLearnPathNodeNotAccessible(String nodeTitle) { + return assertOnLearnPathNodeStatus(nodeTitle, "o_lp_not_accessible"); + } + + private CoursePageFragment assertOnLearnPathNodeStatus(String nodeTitle, String statusCssClass) { + if(nodeTitle.length() > 20) { + nodeTitle = nodeTitle.substring(0, 20); + } + By nodeDoneBy = By.xpath("//div[contains(@class,'o_lp_tree')]//span[contains(@class,'o_tree_l')]/a[i[contains(@class,'" + statusCssClass + "')]][span[text()[contains(.,'" + nodeTitle + "')]]]"); OOGraphene.waitElement(nodeDoneBy, browser); return this; } + public CoursePageFragment assertOnLearnPathPercent(int percent) { + By percentageBy = By.xpath("//span[contains(@class,'o_progress')]//span[contains(@class,'percentage')]//span[text()[contains(.,'" + percent + "%')]]"); + OOGraphene.waitElement(percentageBy, browser); + return this; + } + /** * Assert if the password field is displayed. * @return @@ -148,6 +173,21 @@ public class CoursePageFragment { return menuTree.selectRoot(); } + /** + * + * @return + */ + public CoursePageFragment confirmNode() { + By confirmationBy = By.cssSelector("div.o_course_pagination div.o_confirm a.btn"); + OOGraphene.waitElement(confirmationBy, browser); + browser.findElement(confirmationBy).click(); + OOGraphene.waitBusy(browser); + + By confirmedBy = By.cssSelector("div.o_course_pagination div.o_confirm a.btn.o_course_pagination_status_done"); + OOGraphene.waitElement(confirmedBy, browser); + return this; + } + /** * Open the tools drop-down * @return @@ -276,6 +316,11 @@ public class CoursePageFragment { return new BookingPage(browser); } + /** + * Set the course status to published. + * + * @return Itself + */ public CoursePageFragment publish() { return changeStatus(RepositoryEntryStatusEnum.published); } diff --git a/src/test/java/org/olat/selenium/page/qti/QTI21Page.java b/src/test/java/org/olat/selenium/page/qti/QTI21Page.java index 56be8baa524..906042a87b1 100644 --- a/src/test/java/org/olat/selenium/page/qti/QTI21Page.java +++ b/src/test/java/org/olat/selenium/page/qti/QTI21Page.java @@ -117,6 +117,21 @@ public class QTI21Page { return this; } + public QTI21Page passE4() { + start() + .answerSingleChoiceWithParagraph("Correct answer") + .saveAnswer() + .answerMultipleChoice("Correct answer", "The answer is correct") + .saveAnswer() + .answerCorrectKPrim("This answer", "Plus answer") + .answerIncorrectKPrim("Not answer", "Minus answer") + .saveAnswer() + .answerGapText("not", "qtiworks_response_oofibc4c14bfe94a41861fe19c70091182_5_1_RESPONSE_1") + .saveAnswer() + .endTest(); + return this; + } + /** * Check the answer of a single choice. * @param answer The answer @@ -807,7 +822,7 @@ public class QTI21Page { public QTI21Page closeTest() { By closeBy = By.cssSelector("a.o_sel_close_test"); - OOGraphene.waitElement(closeBy, 5, browser); + OOGraphene.waitElement(closeBy, browser); browser.findElement(closeBy).click(); OOGraphene.waitBusy(browser); confirm(); diff --git a/src/test/java/org/olat/test/file_resources/course_certificates_exrules.zip b/src/test/java/org/olat/test/file_resources/course_certificates_exrules.zip new file mode 100644 index 0000000000000000000000000000000000000000..988bde8a23373ac1b09c2bf06d3730f68769d507 GIT binary patch literal 27733 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 literal 0 HcmV?d00001 -- GitLab