Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions src/main/java/uk/ac/cam/cl/dtg/isaac/api/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,6 @@ public final class Constants {
public static final Set<String> 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;

Expand Down
30 changes: 20 additions & 10 deletions src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -109,14 +112,21 @@ 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;

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;
}
}

/**
Expand Down Expand Up @@ -995,7 +1005,7 @@ private List<GameboardItem> getSelectedGameboardQuestions(final GameFilter gameF
Set<GameboardItem> gameboardReadyQuestions = Sets.newHashSet();
List<GameboardItem> 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 {
Expand All @@ -1015,12 +1025,12 @@ private List<GameboardItem> 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;
}

Expand All @@ -1032,11 +1042,11 @@ private List<GameboardItem> 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;
}
}
Expand Down Expand Up @@ -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())) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/uk/ac/cam/cl/dtg/segue/api/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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));
Expand Down
33 changes: 33 additions & 0 deletions src/test/java/uk/ac/cam/cl/dtg/isaac/api/CookieJarFilter.java
Original file line number Diff line number Diff line change
@@ -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<String, NewCookie> 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));
}
}
94 changes: 94 additions & 0 deletions src/test/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacadeIT.java
Original file line number Diff line number Diff line change
@@ -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<String> subjects = new ArrayList<>();
subjects.add("physics");
gameFilter.setSubjects(subjects);
gameboardDTO.setGameFilter(gameFilter);

// Add 30 questions
List<GameboardItem> 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<String> subjects = new ArrayList<>();
subjects.add("physics");
gameFilter.setSubjects(subjects);
gameboardDTO.setGameFilter(gameFilter);

// Add over 30 questions
List<GameboardItem> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -110,22 +116,36 @@ static class TestClient {
String baseUrl;
Consumer<ThrowingRunnable> registerCleanup;
RequestBuilder builder;
RegisteredUserDTO currentUser;
Client client;

TestClient(
final String baseUrl, final Consumer<ThrowingRunnable> registerCleanup, final RequestBuilder builder
) {
TestClient(final String baseUrl, final Consumer<ThrowingRunnable> 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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand All @@ -55,13 +58,18 @@ public class GameManagerTest {
private GameboardPersistenceManager dummyGameboardPersistenceManager;
private MapperFacade dummyMapper;
private QuestionManager dummyQuestionManager;
private AbstractConfigLoader dummyConfigLoader;

@Before
public void setUp() {
this.dummyContentManager = PowerMock.createMock(GitContentManager.class);
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
Expand All @@ -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()
Expand Down
Loading
Loading