diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f9cfe35f1d..bbc3d72e27 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,11 +2,9 @@ name: "CodeQL" on: push: - branches: [ "master", "redesign-2024" ] + branches: [ "main" ] pull_request: - branches: [ "master", "redesign-2024" ] - schedule: - - cron: "44 13 * * 4" + branches: [ "main" ] jobs: analyze: diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 42922106d1..4fd2f11a4e 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -5,9 +5,9 @@ name: Java CI with Maven on: push: - branches: [ "master", "redesign-2024" ] + branches: [ "main" ] pull_request: - branches: [ "master", "redesign-2024" ] + branches: [ "main" ] jobs: build-and-test: diff --git a/README.md b/README.md index 8b7c94a740..828442f5d4 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,6 @@ [![codecov](https://codecov.io/gh/isaacphysics/isaac-api/branch/master/graph/badge.svg)](https://codecov.io/gh/isaacphysics/isaac-api) -`isaac-api` is the server and API for the [Isaac Physics](https://isaacphysics.org/about) and [Ada Computer Science](https://adacomputerscience.org/about) projects. Together with [`isaac-react-app`](https://github.com/isaacphysics/isaac-react-app), it forms the core stack of the Isaac platform. +`isaac-api` is the server and API for the [Isaac Science](https://isaacscience.org/about) and [Ada Computer Science](https://adacomputerscience.org/about) projects. Together with [`isaac-react-app`](https://github.com/isaacphysics/isaac-react-app), it forms the core stack of the Isaac platform. The API runs on Jetty, and runs in [Docker](https://www.docker.com/) in production. diff --git a/compose-local-deps.yml b/compose-local-deps.yml index 8e84f37a29..8065ed508a 100644 --- a/compose-local-deps.yml +++ b/compose-local-deps.yml @@ -87,6 +87,21 @@ services: ports: - "5432:5432" + it-postgres: + network_mode: bridge + container_name: it-postgres + image: postgres:16 + volumes: + - ./src/main/resources/db_scripts/postgres-rutherford-create-script.sql:/docker-entrypoint-initdb.d/00-isaac-create.sql:ro + - ./src/main/resources/db_scripts/postgres-rutherford-functions.sql:/docker-entrypoint-initdb.d/01-isaac-functions.sql:ro + - ./src/main/resources/db_scripts/quartz_scheduler_create_script.sql:/docker-entrypoint-initdb.d/02-isaac-quartz.sql:ro + - ./src/test/resources/test-postgres-rutherford-data-dump.sql:/docker-entrypoint-initdb.d/03-isaac-test-data.sql:ro + environment: + POSTGRES_USER: rutherford + POSTGRES_PASSWORD: rutherf0rd + ports: + - "5433:5432" + # app-physics: # network_mode: bridge # container_name: app-physics diff --git a/pom.xml b/pom.xml index e329479f0b..73cbcd9f7d 100644 --- a/pom.xml +++ b/pom.xml @@ -10,28 +10,33 @@ UTF-8 - v3.20.6-SNAPSHOT - 2.25.1 - 6.2.12.Final + v3.20.11-SNAPSHOT + 2.25.2 + 6.2.14.Final 7.0.0 1.39.0 - 2.19.2 - 2.19.2 + 2.20.0 + + 2.20.0 + + 2.20 2.0.9 4.13.2 - 5.13.4 + 5.14.0 4.13.2 0.16.0 - 11.0.25 + 11.0.26 8080 8090 1.21.3 web-api-live.xml web-etl.xml web-api-local.xml - 12.1.3 + 12.1.6 3.2.0 - 2.2.34 + 2.2.38 6.10.1.202505221210-r @@ -52,7 +57,7 @@ org.yaml snakeyaml - 2.4 + 2.5 org.isaacphysics.thirdparty @@ -148,7 +153,7 @@ com.google.guava guava - 33.4.8-jre + 33.5.0-jre @@ -223,7 +228,7 @@ org.assertj assertj-core - 3.27.3 + 3.27.6 test @@ -235,7 +240,7 @@ com.auth0 jwks-rsa - 0.22.2 + 0.23.0 org.testcontainers @@ -314,7 +319,7 @@ com.fasterxml.jackson.core jackson-annotations - ${jackson.version} + ${jackson-annotations.version} com.fasterxml.jackson.jakarta.rs @@ -325,7 +330,7 @@ org.apache.commons commons-lang3 - 3.18.0 + 3.19.0 org.apache.commons @@ -382,7 +387,7 @@ org.postgresql postgresql - 42.7.7 + 42.7.8 @@ -471,13 +476,13 @@ org.bouncycastle bcprov-jdk18on - 1.81 + 1.82 com.maxmind.geoip2 geoip2 - 4.3.1 + 4.4.0 org.apache.maven.surefire surefire-junit47 - 3.5.3 + 3.5.4 @@ -722,29 +727,19 @@ - + - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - 2.19.2 - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - 2.19.2 - - - com.fasterxml.jackson.jakarta.rs - jackson-jakarta-rs-base - 2.19.2 - - - com.fasterxml.jackson.module - jackson-module-jakarta-xmlbind-annotations - 2.19.2 + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/Constants.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/Constants.java index c9aa4771bb..aaec542028 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/Constants.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/Constants.java @@ -65,11 +65,6 @@ public final class Constants { public static final Set SEARCHABLE_DOC_TYPES = ImmutableSet.of( QUESTION_TYPE, FAST_TRACK_QUESTION_TYPE, CONCEPT_TYPE, TOPIC_SUMMARY_PAGE_TYPE, BOOK_INDEX_TYPE, BOOK_DETAIL_TYPE, PAGE_TYPE, EVENT_TYPE); - /* - * Game specific variables. - */ - public static final int GAME_BOARD_TARGET_SIZE = 10; - public enum CompletionState { ALL_CORRECT, ALL_ATTEMPTED, ALL_INCORRECT, IN_PROGRESS, NOT_ATTEMPTED; @@ -230,7 +225,7 @@ public enum IsaacClientLogType implements LogType { * User preference categories */ public enum IsaacUserPreferences { - BETA_FEATURE, EXAM_BOARD, PROGRAMMING_LANGUAGE, BOOLEAN_NOTATION, DISPLAY_SETTING, CONSENT + BETA_FEATURE, EXAM_BOARD, PROGRAMMING_LANGUAGE, BOOLEAN_NOTATION, DISPLAY_SETTING, ACCESSIBILITY, CONSENT } /** diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacade.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacade.java index 5794a758fc..d3b7f9543e 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacade.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacade.java @@ -311,7 +311,7 @@ public final Response getFastTrackConceptFromHistory(@Context final Request requ this.questionManager.getQuestionAttemptsByUser(currentUser); List conceptQuestionsProgress = Lists.newArrayList(); - if (upperQuestionId.isEmpty()) { + if (null == upperQuestionId || upperQuestionId.isEmpty()) { List upperAndLower = Arrays.asList(FASTTRACK_LEVEL.ft_upper, FASTTRACK_LEVEL.ft_lower); conceptQuestionsProgress.addAll(fastTrackManger.getConceptProgress( gameboard, upperAndLower, currentConceptTitle, userQuestionAttempts)); diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/QuizFacade.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/QuizFacade.java index 2fc9011302..e372a0a43e 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/QuizFacade.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/QuizFacade.java @@ -197,9 +197,16 @@ public final Response getAvailableQuizzes(@Context final Request request, try { String userRoleString = Role.STUDENT.name(); // Allow anonymous users to list STUDENT quizzes. + boolean showNofilterQuizzes = false; + AbstractSegueUserDTO currentUser = userManager.getCurrentUser(httpServletRequest); if (currentUser instanceof RegisteredUserDTO) { userRoleString = ((RegisteredUserDTO) currentUser).getRole().name(); + try { + showNofilterQuizzes = isUserStaff(userManager, (RegisteredUserDTO) currentUser); + } catch (final NoUserLoggedInException e) { + // Not possible inside this if block! + } } // Cache the list of quizzes based on current content version, user's role, and startIndex: @@ -217,7 +224,7 @@ public final Response getAvailableQuizzes(@Context final Request request, // FIXME: ** HARD-CODED DANGER AHEAD ** // The limit parameter in the following call is hard-coded and should be returned to a more reasonable // number once we have a front-end pagination/load-more system in place. - ResultsWrapper summary = this.quizManager.getAvailableQuizzes(userRoleString, startIndex, 9000); + ResultsWrapper summary = this.quizManager.getAvailableQuizzes(userRoleString, startIndex, 9000, showNofilterQuizzes); return ok(summary).tag(etag) .cacheControl(getCacheControl(NEVER_CACHE_WITHOUT_ETAG_CHECK, false)) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java index 990b33a317..40945ba2a8 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java @@ -25,7 +25,6 @@ import org.apache.commons.lang3.tuple.ImmutablePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import uk.ac.cam.cl.dtg.isaac.api.Constants; import uk.ac.cam.cl.dtg.isaac.dao.GameboardPersistenceManager; import uk.ac.cam.cl.dtg.isaac.dos.AudienceContext; import uk.ac.cam.cl.dtg.isaac.dos.GameboardContentDescriptor; @@ -60,6 +59,8 @@ import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; +import uk.ac.cam.cl.dtg.util.AbstractConfigLoader; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -87,6 +88,8 @@ public class GameManager { private static final float DEFAULT_QUESTION_PASS_MARK = 75; private static final int MAX_QUESTIONS_TO_SEARCH = 20; + private static final int GAMEBOARD_QUESTIONS_DEFAULT = 10; + private static int gameboardQuestionsLimit; private final GameboardPersistenceManager gameboardPersistenceManager; private final Random randomGenerator; @@ -109,7 +112,8 @@ public class GameManager { @Inject public GameManager(final GitContentManager contentManager, final GameboardPersistenceManager gameboardPersistenceManager, final MapperFacade mapper, - final QuestionManager questionManager) { + final QuestionManager questionManager, + final AbstractConfigLoader properties) { this.contentManager = contentManager; this.gameboardPersistenceManager = gameboardPersistenceManager; this.questionManager = questionManager; @@ -117,6 +121,12 @@ public GameManager(final GitContentManager contentManager, this.randomGenerator = new Random(); this.mapper = mapper; + + try { + GameManager.gameboardQuestionsLimit = Integer.parseInt(properties.getProperty(GAMEBOARD_QUESTION_LIMIT)); + } catch (NumberFormatException e) { + GameManager.gameboardQuestionsLimit = GAMEBOARD_QUESTIONS_DEFAULT; + } } /** @@ -995,7 +1005,7 @@ private List getSelectedGameboardQuestions(final GameFilter gameF Set gameboardReadyQuestions = Sets.newHashSet(); List completedQuestions = Lists.newArrayList(); // choose the gameboard questions to include. - while (gameboardReadyQuestions.size() < GAME_BOARD_TARGET_SIZE && !selectionOfGameboardQuestions.isEmpty()) { + while (gameboardReadyQuestions.size() < GAMEBOARD_QUESTIONS_DEFAULT && !selectionOfGameboardQuestions.isEmpty()) { for (GameboardItem gameboardItem : selectionOfGameboardQuestions) { CompletionState questionState; try { @@ -1015,12 +1025,12 @@ private List getSelectedGameboardQuestions(final GameFilter gameF } // stop inner loop if we have reached our target - if (gameboardReadyQuestions.size() == GAME_BOARD_TARGET_SIZE) { + if (gameboardReadyQuestions.size() == GAMEBOARD_QUESTIONS_DEFAULT) { break; } } - if (gameboardReadyQuestions.size() == GAME_BOARD_TARGET_SIZE) { + if (gameboardReadyQuestions.size() == GAMEBOARD_QUESTIONS_DEFAULT) { break; } @@ -1032,11 +1042,11 @@ private List getSelectedGameboardQuestions(final GameFilter gameF } // Try and make up the difference with completed ones if we haven't reached our target size - if (gameboardReadyQuestions.size() < GAME_BOARD_TARGET_SIZE && !completedQuestions.isEmpty()) { + if (gameboardReadyQuestions.size() < GAMEBOARD_QUESTIONS_DEFAULT && !completedQuestions.isEmpty()) { for (GameboardItem completedQuestion : completedQuestions) { - if (gameboardReadyQuestions.size() < GAME_BOARD_TARGET_SIZE) { + if (gameboardReadyQuestions.size() < GAMEBOARD_QUESTIONS_DEFAULT) { gameboardReadyQuestions.add(completedQuestion); - } else if (gameboardReadyQuestions.size() == GAME_BOARD_TARGET_SIZE) { + } else if (gameboardReadyQuestions.size() == GAMEBOARD_QUESTIONS_DEFAULT) { break; } } @@ -1373,9 +1383,9 @@ private void validateGameboard(final GameboardDTO gameboardDTO) throws InvalidGa "Your gameboard must not contain illegal characters e.g. spaces"); } - if (gameboardDTO.getContents().size() > Constants.GAME_BOARD_TARGET_SIZE) { + if (gameboardDTO.getContents().size() > gameboardQuestionsLimit) { throw new InvalidGameboardException(String.format("Your gameboard must not contain more than %s questions", - GAME_BOARD_TARGET_SIZE)); + gameboardQuestionsLimit)); } if (gameboardDTO.getGameFilter() == null || !validateFilterQuery(gameboardDTO.getGameFilter())) { diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizManager.java index 62e3e2ba4b..3c836bfd6a 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizManager.java @@ -84,12 +84,17 @@ public QuizManager(final AbstractConfigLoader properties, final ContentService c this.contentSummarizerService = contentSummarizerService; } - public ResultsWrapper getAvailableQuizzes(String visibleToRole, @Nullable Integer startIndex, @Nullable Integer limit) throws ContentManagerException { + public ResultsWrapper getAvailableQuizzes(String visibleToRole, @Nullable Integer startIndex, @Nullable Integer limit, boolean showNoFilterQuizzes) throws ContentManagerException { List fieldsToMatch = Lists.newArrayList(); fieldsToMatch.add(new GitContentManager.BooleanSearchClause( TYPE_FIELDNAME, Constants.BooleanOperator.AND, Collections.singletonList(QUIZ_TYPE))); + if (!showNoFilterQuizzes) { + fieldsToMatch.add(new GitContentManager.BooleanSearchClause(TAGS_FIELDNAME, BooleanOperator.NOT, + Collections.singletonList(HIDE_FROM_FILTER_TAG))); + } + if (null != visibleToRole) { fieldsToMatch.add(new GitContentManager.BooleanSearchClause(HIDDEN_FROM_ROLES_FIELDNAME, BooleanOperator.NOT, Collections.singletonList(visibleToRole))); diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/content/SeguePage.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/content/SeguePage.java index cdd9548ee9..f1bed4439e 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/content/SeguePage.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/content/SeguePage.java @@ -99,6 +99,22 @@ public void setSupersededBy(String supersededBy) { this.supersededBy = supersededBy; } + public String getPermissions() { + return permissions; + } + + public void setPermissions(final String permissions) { + this.permissions = permissions; + } + + public String getNotes() { + return notes; + } + + public void setNotes(final String notes) { + this.notes = notes; + } + public String getTeacherNotes() { return teacherNotes; } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/IsaacLLMFreeTextQuestionDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/IsaacLLMFreeTextQuestionDTO.java index 30f94e9981..d7087bf16e 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/IsaacLLMFreeTextQuestionDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/IsaacLLMFreeTextQuestionDTO.java @@ -10,6 +10,15 @@ @JsonContentType(LLM_FREE_TEXT_QUESTION_TYPE) @ValidatesWith(IsaacLLMFreeTextValidator.class) public class IsaacLLMFreeTextQuestionDTO extends QuestionDTO { + private Integer maxMarks; + public IsaacLLMFreeTextQuestionDTO() { } + + public Integer getMaxMarks() { + return maxMarks; + } + public void setMaxMarks(Integer maxMarks) { + this.maxMarks = maxMarks; + } } diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/api/Constants.java b/src/main/java/uk/ac/cam/cl/dtg/segue/api/Constants.java index 0f6743c965..c2ca274452 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/api/Constants.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/api/Constants.java @@ -588,6 +588,8 @@ public static SchoolInfoStatus get(final boolean schoolIdProvided, final boolean public static final String ASSOCIATION_TOKEN_FIELDNAME = "token"; + public static final String GAMEBOARD_QUESTION_LIMIT = "GAMEBOARD_QUESTION_LIMIT"; + public static final String GROUP_FK = "groupId"; public static final String ASSIGNMENT_FK = "assignmentId"; public static final String ASSIGNMENT_DUEDATE = "dueDate"; diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/content/GitContentManager.java b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/content/GitContentManager.java index 7b5907dce0..c4d3df652e 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/dao/content/GitContentManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/dao/content/GitContentManager.java @@ -349,9 +349,20 @@ public final ResultsWrapper siteWideSearch( // Restrict content types .includeContentTypes(contentTypes) + // High priority matches on untokenised search string + .searchFor(new SearchInField(Constants.ID_FIELDNAME + "." + + Constants.UNPROCESSED_SEARCH_FIELD_SUFFIX, Collections.singleton(searchString)) + .priority(Priority.HIGH).strategy(Strategy.SIMPLE)) + .searchFor(new SearchInField(Constants.TITLE_FIELDNAME + "." + + Constants.UNPROCESSED_SEARCH_FIELD_SUFFIX, Collections.singleton(searchString)) + .priority(Priority.HIGH).strategy(Strategy.SIMPLE)) + .searchFor(new SearchInField(Constants.SUBTITLE_FIELDNAME + "." + + Constants.UNPROCESSED_SEARCH_FIELD_SUFFIX, Collections.singleton(searchString)) + .priority(Priority.HIGH).strategy(Strategy.SIMPLE)) + // Fuzzy search term matches .searchFor(new SearchInField(Constants.ID_FIELDNAME, searchTerms) - .priority(Priority.HIGH).strategy(Strategy.FUZZY)) + .priority(Priority.HIGH).strategy(Strategy.DEFAULT)) .searchFor(new SearchInField(Constants.TITLE_FIELDNAME, searchTerms) .priority(Priority.HIGH).strategy(Strategy.FUZZY)) .searchFor(new SearchInField(Constants.SUBTITLE_FIELDNAME, searchTerms) @@ -360,10 +371,8 @@ public final ResultsWrapper siteWideSearch( .priority(Priority.HIGH).strategy(Strategy.FUZZY)) .searchFor(new SearchInField(Constants.TAGS_FIELDNAME, searchTerms) .priority(Priority.HIGH).strategy(Strategy.FUZZY)) - .searchFor(new SearchInField(Constants.PRIORITISED_SEARCHABLE_CONTENT_FIELDNAME, searchTerms) - .priority(Priority.HIGH).strategy(Strategy.FUZZY)) - .searchFor(new SearchInField(Constants.SEARCHABLE_CONTENT_FIELDNAME, searchTerms) - .strategy(Strategy.FUZZY)) + .searchFor(new SearchInField(Constants.PRIORITISED_SEARCHABLE_CONTENT_FIELDNAME, searchTerms)) + .searchFor(new SearchInField(Constants.SEARCHABLE_CONTENT_FIELDNAME, searchTerms)) // Event specific queries .searchFor(new SearchInField(Constants.ADDRESS_PSEUDO_FIELDNAME, searchTerms)) @@ -450,10 +459,22 @@ public final ResultsWrapper questionSearch( .priority(Priority.HIGH).strategy(Strategy.SUBSTRING)) .searchFor(new SearchInField(Constants.TAGS_FIELDNAME, searchTerms) .priority(Priority.HIGH).strategy(Strategy.SUBSTRING)) - .searchFor(new SearchInField(Constants.PRIORITISED_SEARCHABLE_CONTENT_FIELDNAME, searchTerms) - .priority(Priority.HIGH).strategy(Strategy.SUBSTRING)) - .searchFor(new SearchInField(Constants.SEARCHABLE_CONTENT_FIELDNAME, searchTerms) - .strategy(Strategy.SUBSTRING)); + .searchFor(new SearchInField(Constants.PRIORITISED_SEARCHABLE_CONTENT_FIELDNAME, searchTerms)) + .searchFor(new SearchInField(Constants.SEARCHABLE_CONTENT_FIELDNAME, searchTerms)); + + if (searchString != null && !searchString.isBlank()) { + // High priority matches on untokenised search string + searchInstructionBuilder + .searchFor(new SearchInField(Constants.ID_FIELDNAME + "." + + Constants.UNPROCESSED_SEARCH_FIELD_SUFFIX, Collections.singleton(searchString)) + .priority(Priority.HIGH).strategy(Strategy.SIMPLE)) + .searchFor(new SearchInField(Constants.TITLE_FIELDNAME + "." + + Constants.UNPROCESSED_SEARCH_FIELD_SUFFIX, Collections.singleton(searchString)) + .priority(Priority.HIGH).strategy(Strategy.SIMPLE)) + .searchFor(new SearchInField(Constants.SUBTITLE_FIELDNAME + "." + + Constants.UNPROCESSED_SEARCH_FIELD_SUFFIX, Collections.singleton(searchString)) + .priority(Priority.HIGH).strategy(Strategy.SIMPLE)); + } // FIXME: Make this and PageFacade agnostic // It doesn't need to know about books, just have required tags diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/etl/ElasticSearchIndexer.java b/src/main/java/uk/ac/cam/cl/dtg/segue/etl/ElasticSearchIndexer.java index 41838d5cd1..96ce5be527 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/etl/ElasticSearchIndexer.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/etl/ElasticSearchIndexer.java @@ -58,7 +58,7 @@ class ElasticSearchIndexer extends ElasticSearchProvider { @Inject public ElasticSearchIndexer(RestHighLevelClient searchClient) { super(searchClient); - rawFieldsListByType.put("content", Lists.newArrayList("id", "title")); + rawFieldsListByType.put("content", Lists.newArrayList("id", "title", "subtitle")); rawFieldsListByType.put("school", Lists.newArrayList("urn")); nestedFieldsByType.put("content", Lists.newArrayList("audience")); } diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/search/IsaacSearchInstructionBuilder.java b/src/main/java/uk/ac/cam/cl/dtg/segue/search/IsaacSearchInstructionBuilder.java index cfac04729c..22fa58f6ef 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/search/IsaacSearchInstructionBuilder.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/search/IsaacSearchInstructionBuilder.java @@ -50,14 +50,16 @@ public class IsaacSearchInstructionBuilder { private static final float PRIORITY_CONTENT_BOOST = 5L; private List searchesInFields; - public static final Long NO_BOOST = 1L; + private static final Long NO_BOOST = 1L; private static final Long FIELD_BOOST = 5L; private static final Long FIELD_BOOST_FUZZY = 1L; private static final Long WILDCARD_FIELD_BOOST = 1L; private static final Long HIGH_PRIORITY_FIELD_BOOST = 10L; - private static final Long HIGH_PRIORITY_FIELD_BOOST_FUZZY = 3L; - private static final Long HIGH_PRIORITY_WILDCARD_FIELD_BOOST = 2L; + private static final Long HIGH_PRIORITY_FIELD_BOOST_FUZZY = 8L; + private static final Long HIGH_PRIORITY_WILDCARD_FIELD_BOOST = 5L; + + private static final Long SEARCHABLE_CONTENT_FIELD_BOOST = 2L; private static final Long EVENT_ADDRESS_FIELD_BOOST = 3L; private static final Long EVENT_ADDRESS_FIELD_BOOST_FUZZY = 1L; @@ -345,8 +347,13 @@ private void addSearchesInFieldsToInstruction(final BooleanInstruction instructi } else { // Generic fields + boolean isSearchableContentField = searchInField.getField().equals(Constants.SEARCHABLE_CONTENT_FIELDNAME) || + searchInField.getField().equals(Constants.PRIORITISED_SEARCHABLE_CONTENT_FIELDNAME); for (String term : searchInField.getTerms()) { - if (searchInField.getStrategy() == Strategy.DEFAULT) { + if (isSearchableContentField) { + generatedSubInstructions.add(new MatchInstruction(searchInField.getField(), term, + SEARCHABLE_CONTENT_FIELD_BOOST, true)); + } else if (searchInField.getStrategy() == Strategy.DEFAULT) { Long boost = searchInField.getPriority() == Priority.HIGH ? HIGH_PRIORITY_FIELD_BOOST : FIELD_BOOST; Long fuzzyBoost = searchInField.getPriority() == Priority.HIGH @@ -359,7 +366,7 @@ private void addSearchesInFieldsToInstruction(final BooleanInstruction instructi } else if (searchInField.getStrategy() == Strategy.SUBSTRING) { Long boost = searchInField.getPriority() == Priority.HIGH - ? HIGH_PRIORITY_FIELD_BOOST : FIELD_BOOST; + ? HIGH_PRIORITY_WILDCARD_FIELD_BOOST : WILDCARD_FIELD_BOOST; generatedSubInstructions.add( new MatchInstruction(searchInField.getField(), term, boost, false)); @@ -368,16 +375,16 @@ private void addSearchesInFieldsToInstruction(final BooleanInstruction instructi } else if (searchInField.getStrategy() == Strategy.FUZZY) { Long boost = searchInField.getPriority() == Priority.HIGH - ? HIGH_PRIORITY_WILDCARD_FIELD_BOOST : WILDCARD_FIELD_BOOST; + ? HIGH_PRIORITY_WILDCARD_FIELD_BOOST : WILDCARD_FIELD_BOOST; generatedSubInstructions.add(new MatchInstruction(searchInField.getField(), term, boost, true)); - generatedSubInstructions.add( - new WildcardInstruction(searchInField.getField(), "*" + term + "*", boost)); - // Use a multi-match instruction, and ensure multi-match instructions for a particular term are - // grouped together + generatedSubInstructions.add(new WildcardInstruction(searchInField.getField(), "*" + term + "*", boost)); + + // Use a multi-match instruction, and ensure multi-match instructions for a + // particular term are grouped together multiMatchSearchesGroupedByTerm.putIfAbsent(term, Sets.newHashSet()); multiMatchSearchesGroupedByTerm.get(term).add(searchInField.getField()); - + } else if (searchInField.getStrategy() == Strategy.SIMPLE) { Long boost = searchInField.getPriority() == Priority.HIGH ? HIGH_PRIORITY_FIELD_BOOST : NO_BOOST; diff --git a/src/main/resources/db_scripts/migrations/2025-08-accessibility-user-preferences.sql b/src/main/resources/db_scripts/migrations/2025-08-accessibility-user-preferences.sql new file mode 100644 index 0000000000..e6562460af --- /dev/null +++ b/src/main/resources/db_scripts/migrations/2025-08-accessibility-user-preferences.sql @@ -0,0 +1 @@ +UPDATE user_preferences SET preference_type = 'ACCESSIBILITY' WHERE preference_name = 'REDUCED_MOTION' OR preference_name = 'PREFER_MATHML'; \ No newline at end of file diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/IsaacTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/IsaacTest.java index 75d596ed1e..6b9e01f64b 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/IsaacTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/IsaacTest.java @@ -258,8 +258,8 @@ protected void initializeMocks() throws ContentManagerException, SegueDatabaseEx quizManager = createMock(QuizManager.class); registerDefaultsFor(quizManager, m -> { - expect(m.getAvailableQuizzes("STUDENT", 0, 9000)).andStubReturn(wrap(studentQuizSummary)); - expect(m.getAvailableQuizzes("TEACHER", 0, 9000)).andStubReturn(wrap(studentQuizSummary, teacherQuizSummary)); + expect(m.getAvailableQuizzes("STUDENT", 0, 9000, false)).andStubReturn(wrap(studentQuizSummary)); + expect(m.getAvailableQuizzes("TEACHER", 0, 9000, false)).andStubReturn(wrap(studentQuizSummary, teacherQuizSummary)); expect(m.findQuiz(studentQuiz.getId())).andStubReturn(studentQuiz); expect(m.findQuiz(studentQuizPreQuizAnswerChange.getId())).andStubReturn(studentQuizPreQuizAnswerChange); expect(m.findQuiz(studentQuizPostQuizAnswerChange.getId())).andStubReturn(studentQuizPostQuizAnswerChange); diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java index ef618bcd32..96652e6da3 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java @@ -17,6 +17,7 @@ import org.testcontainers.utility.MountableFile; import uk.ac.cam.cl.dtg.isaac.api.managers.AssignmentManager; import uk.ac.cam.cl.dtg.isaac.api.managers.EventBookingManager; +import uk.ac.cam.cl.dtg.isaac.api.managers.FastTrackManger; import uk.ac.cam.cl.dtg.isaac.api.managers.GameManager; import uk.ac.cam.cl.dtg.isaac.api.managers.QuizAssignmentManager; import uk.ac.cam.cl.dtg.isaac.api.managers.QuizAttemptManager; @@ -140,6 +141,7 @@ public class AbstractIsaacIntegrationTest { protected static QuizManager quizManager; protected static PgPasswordDataManager passwordDataManager; protected static UserAttemptManager userAttemptManager; + protected static FastTrackManger fastTrackManger; // Manager dependencies protected static IQuizAssignmentPersistenceManager quizAssignmentPersistenceManager; @@ -290,7 +292,7 @@ public static void setUpClass() throws Exception { IAssignmentPersistenceManager assignmentPersistenceManager = new PgAssignmentPersistenceManager(postgresSqlDb, mapperFacade); GameboardPersistenceManager gameboardPersistenceManager = new GameboardPersistenceManager(postgresSqlDb, contentManager, mapperFacade, contentMapper); - gameManager = new GameManager(contentManager, gameboardPersistenceManager, mapperFacade, questionManager); + gameManager = new GameManager(contentManager, gameboardPersistenceManager, mapperFacade, questionManager, properties); groupManager = new GroupManager(pgUserGroupPersistenceManager, userAccountManager, gameManager, mapperFacade); userAssociationManager = new UserAssociationManager(pgAssociationDataManager, userAccountManager, groupManager); PgTransactionManager pgTransactionManager = new PgTransactionManager(postgresSqlDb); @@ -307,6 +309,7 @@ public static void setUpClass() throws Exception { quizQuestionAttemptPersistenceManager = new PgQuizQuestionAttemptPersistenceManager(postgresSqlDb, contentMapper); quizQuestionManager = new QuizQuestionManager(questionManager, contentMapper, quizQuestionAttemptPersistenceManager, quizManager, quizAttemptManager); userAttemptManager = new UserAttemptManager(questionManager); + fastTrackManger = new FastTrackManger(properties, contentManager, gameManager); misuseMonitor = new InMemoryMisuseMonitor(); misuseMonitor.registerHandler(GroupManagerLookupMisuseHandler.class.getSimpleName(), new GroupManagerLookupMisuseHandler(emailManager, properties)); diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/CookieJarFilter.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/CookieJarFilter.java new file mode 100644 index 0000000000..fb4e54c346 --- /dev/null +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/CookieJarFilter.java @@ -0,0 +1,33 @@ +package uk.ac.cam.cl.dtg.isaac.api; + +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.client.ClientResponseContext; +import jakarta.ws.rs.client.ClientResponseFilter; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.NewCookie; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/* + * JAX-RS Client filter for storing and retrieving cookies across requests. + */ +public class CookieJarFilter implements ClientRequestFilter, ClientResponseFilter { + private final Map cookieJar = new ConcurrentHashMap<>(); + + @Override + public void filter(final ClientRequestContext requestContext) { + if (!cookieJar.isEmpty()) { + String header = cookieJar.values().stream() + .map(c -> c.getName() + "=" + c.getValue()) + .collect(Collectors.joining("; ")); + requestContext.getHeaders().putSingle(HttpHeaders.COOKIE, header); + } + } + + @Override + public void filter(final ClientRequestContext requestContext, final ClientResponseContext responseContext) { + responseContext.getCookies().values().forEach(c -> cookieJar.put(c.getName(), c)); + } +} diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacadeIT.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacadeIT.java new file mode 100644 index 0000000000..3633890732 --- /dev/null +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacadeIT.java @@ -0,0 +1,94 @@ +package uk.ac.cam.cl.dtg.isaac.api; + +import org.junit.jupiter.api.Test; +import uk.ac.cam.cl.dtg.isaac.dto.GameFilter; +import uk.ac.cam.cl.dtg.isaac.dto.GameboardDTO; +import uk.ac.cam.cl.dtg.isaac.dto.GameboardItem; +import uk.ac.cam.cl.dtg.segue.api.AuthenticationFacade; + +import jakarta.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static uk.ac.cam.cl.dtg.isaac.api.ITConstants.ASSIGNMENT_TEST_PAGE_ID; + +public class GameboardsFacadeIT extends IsaacIntegrationTestWithREST { + TestServer subject() throws Exception { + return startServer( + new AuthenticationFacade(properties, userAccountManager, logManager, misuseMonitor), + new GameboardsFacade(properties, logManager, gameManager, questionManager, userAccountManager, + fastTrackManger) + ); + } + + @Test + public void saveNewGameboard_validGameboard_isAccepted() throws Exception { + // Arrange + GameboardDTO gameboardDTO = new GameboardDTO(); + gameboardDTO.setTitle("Test Gameboard"); + + // Create gameboard + GameFilter gameFilter = new GameFilter(); + List subjects = new ArrayList<>(); + subjects.add("physics"); + gameFilter.setSubjects(subjects); + gameboardDTO.setGameFilter(gameFilter); + + // Add 30 questions + List questions = new ArrayList<>(); + for (int i = 0; i < 30; i++) { + GameboardItem item = new GameboardItem(); + item.setId(ASSIGNMENT_TEST_PAGE_ID); + item.setTitle(ASSIGNMENT_TEST_PAGE_ID); + questions.add(item); + } + gameboardDTO.setContents(questions); + + TestClient client = subject().client(); + + // Log in as student + client.loginAs(integrationTestUsers.TEST_STUDENT); + + // Act + TestResponse r = client.post("/gameboards", gameboardDTO); + + // Assert + assertEquals(200, r.response.getStatus()); + } + + @Test + public void saveNewGameboard_tooManyQuestions_isRejected() throws Exception { + // Arrange + GameboardDTO gameboardDTO = new GameboardDTO(); + gameboardDTO.setTitle("Test Gameboard"); + + // Create an otherwise valid gameboard + GameFilter gameFilter = new GameFilter(); + List subjects = new ArrayList<>(); + subjects.add("physics"); + gameFilter.setSubjects(subjects); + gameboardDTO.setGameFilter(gameFilter); + + // Add over 30 questions + List questions = new ArrayList<>(); + for (int i = 0; i < 31; i++) { + GameboardItem item = new GameboardItem(); + item.setId(ASSIGNMENT_TEST_PAGE_ID); + item.setTitle(ASSIGNMENT_TEST_PAGE_ID); + questions.add(item); + } + gameboardDTO.setContents(questions); + + TestClient client = subject().client(); + + // Log in as student + client.loginAs(integrationTestUsers.TEST_STUDENT); + + // Act + TestResponse r = client.post("/gameboards", gameboardDTO); + + // Assert + r.assertError("The gameboard you provided is invalid", Response.Status.BAD_REQUEST); + } +} diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/ITConstants.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/ITConstants.java index dc6ec0cbb2..3bce434d84 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/ITConstants.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/ITConstants.java @@ -129,4 +129,5 @@ public final class ITConstants { public static final String QUIZ_TEST_QUIZ_ID = "_quiz_test"; public static final String QUIZ_HIDDEN_FROM_ROLE_STUDENTS_QUIZ_ID = "_hidden_from_roles_student_quiz_test"; public static final String QUIZ_HIDDEN_FROM_ROLE_TUTORS_QUIZ_ID = "_hidden_from_roles_tutor_quiz_test"; + public static final String QUIZ_TEST_NOFILTER_QUIZ_ID = "_quiz_test_nofilter"; } diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/IsaacIntegrationTestWithREST.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/IsaacIntegrationTestWithREST.java index 4055920f48..fa5c71e7f0 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/IsaacIntegrationTestWithREST.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/IsaacIntegrationTestWithREST.java @@ -8,10 +8,16 @@ import org.json.JSONObject; import org.junit.function.ThrowingRunnable; import org.junit.jupiter.api.AfterEach; +import uk.ac.cam.cl.dtg.isaac.dos.users.RegisteredUser; +import uk.ac.cam.cl.dtg.isaac.dto.LocalAuthDTO; +import uk.ac.cam.cl.dtg.isaac.dto.users.RegisteredUserDTO; +import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.client.Invocation; import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.util.HashSet; import java.util.Map; @@ -110,22 +116,36 @@ static class TestClient { String baseUrl; Consumer registerCleanup; RequestBuilder builder; + RegisteredUserDTO currentUser; + Client client; - TestClient( - final String baseUrl, final Consumer registerCleanup, final RequestBuilder builder - ) { + TestClient(final String baseUrl, final Consumer registerCleanup, final RequestBuilder builder) { this.baseUrl = baseUrl; this.registerCleanup = registerCleanup; this.builder = builder; + this.client = ClientBuilder.newClient().register(new CookieJarFilter()); } public TestResponse get(final String url) { - try (var client = ClientBuilder.newClient()) { - var request = client.target(baseUrl + url).request(); - var response = builder.apply(request).get(); - registerCleanup.accept(response::close); - return new TestResponse(response); - } + var request = client.target(baseUrl + url).request(MediaType.APPLICATION_JSON); + var response = builder.apply(request).get(); + registerCleanup.accept(response::close); + return new TestResponse(response); + } + + public TestResponse post(final String url, final Object body) { + var request = client.target(baseUrl + url).request(MediaType.APPLICATION_JSON); + var response = builder.apply(request).post(Entity.json(body)); + registerCleanup.accept(response::close); + return new TestResponse(response); + } + + public void loginAs(final RegisteredUser user) { + var request = client.target(baseUrl + "/auth/SEGUE/authenticate").request(MediaType.APPLICATION_JSON); + var body = new LocalAuthDTO(); + body.setEmail(user.getEmail()); + body.setPassword("test1234"); + this.currentUser = builder.apply(request).post(Entity.json(body), RegisteredUserDTO.class); } } diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/QuizFacadeIT.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/QuizFacadeIT.java index 56ad05feb9..76629a343c 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/QuizFacadeIT.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/QuizFacadeIT.java @@ -19,7 +19,11 @@ import org.apache.commons.lang3.time.DateUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import uk.ac.cam.cl.dtg.isaac.dos.QuizFeedbackMode; +import uk.ac.cam.cl.dtg.isaac.dos.users.RegisteredUser; import uk.ac.cam.cl.dtg.isaac.dto.AssignmentStatusDTO; import uk.ac.cam.cl.dtg.isaac.dto.IsaacQuizDTO; import uk.ac.cam.cl.dtg.isaac.dto.QuizAssignmentDTO; @@ -32,6 +36,8 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.ws.rs.core.Request; import jakarta.ws.rs.core.Response; +import uk.ac.cam.cl.dtg.isaac.dto.users.RegisteredUserDTO; + import java.util.Date; import java.util.LinkedList; import java.util.List; @@ -164,6 +170,58 @@ public void getAvailableQuizzesEndpoint_getQuizzesAsTutor_returnsNoHiddenFromRol assertFalse(responseBody.getResults().stream().anyMatch(q -> q.getId().equals(QUIZ_HIDDEN_FROM_ROLE_TUTORS_QUIZ_ID))); } + @ParameterizedTest + @ValueSource(strings = { + ITConstants.TEST_TUTOR_EMAIL, + ITConstants.TEST_TEACHER_EMAIL, + }) + public void getAvailableQuizzesEndpoint_getQuizzesAsNonStaff_omitsNofilterTaggedQuizzes(String email) throws Exception { + // Arrange + // log in, create request + LoginResult login = loginAs(httpSession, email, "test1234"); + HttpServletRequest availableQuizzesRequest = createRequestWithCookies(new Cookie[]{login.cookie}); + replay(availableQuizzesRequest); + + // Act + // make request + Response getQuizzesResponse = quizFacade.getAvailableQuizzes(createNiceMock(Request.class), availableQuizzesRequest); + + // Assert + // check status code is OK + assertEquals(Response.Status.OK.getStatusCode(), getQuizzesResponse.getStatus()); + + // check nofilter quizzes are not returned as available + @SuppressWarnings("unchecked") ResultsWrapper responseBody = + (ResultsWrapper) getQuizzesResponse.getEntity(); + assertFalse(responseBody.getResults().stream().anyMatch(q -> q.getId().equals(QUIZ_TEST_NOFILTER_QUIZ_ID))); + } + + @ParameterizedTest + @ValueSource(strings = { + ITConstants.TEST_EVENTMANAGER_EMAIL, + ITConstants.TEST_EDITOR_EMAIL + }) + public void getAvailableQuizzesEndpoint_getQuizzesAsStaff_includesNofilterTaggedQuizzes(String email) throws Exception { + // Arrange + // log in, create request + LoginResult login = loginAs(httpSession, email, "test1234"); + HttpServletRequest availableQuizzesRequest = createRequestWithCookies(new Cookie[]{login.cookie}); + replay(availableQuizzesRequest); + + // Act + // make request + Response getQuizzesResponse = quizFacade.getAvailableQuizzes(createNiceMock(Request.class), availableQuizzesRequest); + + // Assert + // check status code is OK + assertEquals(Response.Status.OK.getStatusCode(), getQuizzesResponse.getStatus()); + + // check nofilter quizzes are returned as available + @SuppressWarnings("unchecked") ResultsWrapper responseBody = + (ResultsWrapper) getQuizzesResponse.getEntity(); + assertTrue(responseBody.getResults().stream().anyMatch(q -> q.getId().equals(QUIZ_TEST_NOFILTER_QUIZ_ID))); + } + @Test public void previewQuizEndpoint_previewInvisibleToStudentQuizAsTeacher_succeeds() throws Exception { // Arrange @@ -208,6 +266,34 @@ public void previewQuizEndpoint_previewHiddenFromRoleTutorQuizAsTutor_fails() th assertEquals("You do not have the permissions to complete this action", responseBody.getErrorMessage()); } + @ParameterizedTest + @ValueSource(strings = { + ITConstants.TEST_TUTOR_EMAIL, + ITConstants.TEST_TEACHER_EMAIL, + ITConstants.TEST_EVENTMANAGER_EMAIL, + ITConstants.TEST_EDITOR_EMAIL + }) + public void previewQuizEndpoint_previewNofilterQuizAsTutorOrAbove_succeeds(String email) throws Exception { + // Arrange + // log in as user, create request + LoginResult login = loginAs(httpSession, email, "test1234"); + HttpServletRequest previewQuizRequest = createRequestWithCookies(new Cookie[]{login.cookie}); + replay(previewQuizRequest); + + // Act + // make request + Response previewQuizResponse = quizFacade.previewQuiz(createNiceMock(Request.class), previewQuizRequest, + QUIZ_TEST_NOFILTER_QUIZ_ID); + + // Assert + // check status code is OK + assertEquals(Response.Status.OK.getStatusCode(), previewQuizResponse.getStatus()); + + // check the quiz is returned for preview + IsaacQuizDTO responseBody = (IsaacQuizDTO) previewQuizResponse.getEntity(); + assertEquals(QUIZ_TEST_NOFILTER_QUIZ_ID, responseBody.getId()); + } + @Test public void viewQuizRubricEndpoint_viewRubricAvailableToRoleStudent_succeeds() throws Exception { // Arrange diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManagerTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManagerTest.java index 33d1c89744..fb2dbcd7a5 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManagerTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManagerTest.java @@ -35,6 +35,8 @@ import uk.ac.cam.cl.dtg.segue.dao.content.ContentManagerException; import uk.ac.cam.cl.dtg.segue.dao.content.GitContentManager; import uk.ac.cam.cl.dtg.segue.dao.content.GitContentManager.BooleanSearchClause; +import uk.ac.cam.cl.dtg.util.AbstractConfigLoader; +import uk.ac.cam.cl.dtg.util.YamlLoader; import java.util.Collections; import java.util.List; @@ -44,6 +46,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.powermock.api.easymock.PowerMock.replay; +import static uk.ac.cam.cl.dtg.segue.api.Constants.*; @RunWith(PowerMockRunner.class) @@ -55,6 +58,7 @@ public class GameManagerTest { private GameboardPersistenceManager dummyGameboardPersistenceManager; private MapperFacade dummyMapper; private QuestionManager dummyQuestionManager; + private AbstractConfigLoader dummyConfigLoader; @Before public void setUp() { @@ -62,6 +66,10 @@ public void setUp() { this.dummyGameboardPersistenceManager = PowerMock.createMock(GameboardPersistenceManager.class); this.dummyMapper = PowerMock.createMock(MapperFacade.class); this.dummyQuestionManager = PowerMock.createMock(QuestionManager.class); + this.dummyConfigLoader = PowerMock.createMock(YamlLoader.class); + + EasyMock.expect(dummyConfigLoader.getProperty(GAMEBOARD_QUESTION_LIMIT)).andStubReturn("30"); + replay(dummyConfigLoader); } @Test @@ -73,7 +81,8 @@ public void getNextQuestionsForFilter_appliesExclusionFilterForDeprecatedQuestio this.dummyContentManager, this.dummyGameboardPersistenceManager, this.dummyMapper, - this.dummyQuestionManager + this.dummyQuestionManager, + this.dummyConfigLoader ); // configure the mock GitContentManager to record the filters that are sent to it by getNextQuestionsForFilter() diff --git a/src/test/resources/isaac-test-es-data.tar.gz b/src/test/resources/isaac-test-es-data.tar.gz index 0fcd5e3cdb..2a808bdcd3 100644 --- a/src/test/resources/isaac-test-es-data.tar.gz +++ b/src/test/resources/isaac-test-es-data.tar.gz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7260a3593f9a051113f8ee6cc4658fa13b8f6f7e60ae4d329688325673dcbe57 -size 2894090 +oid sha256:bd48a022d90d5862dd9a5be9e2cd4e8f6e493752d2602805b5306b255678928b +size 2922446 diff --git a/src/test/resources/segue-integration-test-config.yaml b/src/test/resources/segue-integration-test-config.yaml index bbe3f93fa8..b1b522944d 100644 --- a/src/test/resources/segue-integration-test-config.yaml +++ b/src/test/resources/segue-integration-test-config.yaml @@ -22,6 +22,8 @@ HIDE_REGRESSION_TEST_CONTENT: "false" MAX_CONTENT_CACHE_TIME: "0" LOGGING_ENABLED: "true" RESTRICTED_SIGNUP_EMAIL_REGEX: .*@(isaacphysics|adacomputerscience)\.org +GAMEBOARD_QUESTION_LIMIT: "30" +# # ETL ETL_HOSTNAME: localhost ETL_PORT: "8090"