Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test server endpoints #483

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
63e3f41
feat: add Server::stop()
ThanGerlek Nov 17, 2024
2bfe27f
feat: add TestServerFacade (a simple client for testing Server endpoi…
ThanGerlek Nov 17, 2024
b56d696
test: add MockEndpointProvider class
ThanGerlek Nov 17, 2024
7f81ef6
test: add ServerTest
ThanGerlek Nov 17, 2024
3c1463d
standardize test names
ThanGerlek Nov 17, 2024
7521e6a
fix: add awaitInitialization()
ThanGerlek Nov 17, 2024
27308ea
use port 8080, not 0
ThanGerlek Nov 17, 2024
bd6104f
fix: return a valid JSON response from MockEndpointProvider
ThanGerlek Nov 17, 2024
d6b6e86
fix: remove irrelevant test
ThanGerlek Nov 17, 2024
4aabb81
test: verify beforeAll and afterAll are called, in the right order, f…
ThanGerlek Nov 18, 2024
2ca3594
test: verify authentication middleware is called when needed
ThanGerlek Nov 18, 2024
9a6edc1
feat: add junit-jupiter-params dependency (for parameterized tests)
ThanGerlek Nov 18, 2024
d91348b
test: change endpoint verification to use parameterized tests rather …
ThanGerlek Nov 18, 2024
076cec9
fix: remove duplicate test
ThanGerlek Nov 18, 2024
3cf5857
refactor: remove unneeded parameters in runHandler()
ThanGerlek Nov 18, 2024
f091e48
feat: add capability of verifying path parameters to MockEndpointProv…
ThanGerlek Nov 18, 2024
a39fbeb
test: verify that pathParams are receiving values
ThanGerlek Nov 18, 2024
c89b8d4
docs: clarify MockEndpointProvider should be used with spy(), not mock()
ThanGerlek Nov 18, 2024
a8786a2
Merge branch 'reduce-endpoint-dependencies' into test-server-endpoints
ThanGerlek Nov 19, 2024
095b864
Merge branch 'reduce-endpoint-dependencies' into test-server-endpoints
ThanGerlek Nov 20, 2024
b37696c
fix: add scheduleShutdown to ServerTests
ThanGerlek Nov 20, 2024
d640614
enhance: rearrange arguments so path is before name first
ThanGerlek Nov 20, 2024
6b0521c
enhance: rearrange endpoint list using paths
ThanGerlek Nov 20, 2024
9ff3cdf
fix: replace HTTP PATCH requests with POST
ThanGerlek Nov 20, 2024
b93d600
add no-longer-patch requests to ServerTest
ThanGerlek Nov 20, 2024
a7276a9
add no-longer-patch requests to ServerTest
ThanGerlek Nov 20, 2024
629352f
Merge remote-tracking branch 'origin/test-server-endpoints' into test…
ThanGerlek Nov 20, 2024
46d9eee
enhance: un-hardcode Server ports
ThanGerlek Nov 21, 2024
6886c1b
test: rmv custom ServerFacadeTest exceptions
ThanGerlek Nov 21, 2024
8d4ad38
Merge branch 'main' into test-server-endpoints
ThanGerlek Nov 25, 2024
4999c4e
fix: add updatePenalties() to ServerTest
ThanGerlek Nov 25, 2024
26aa6e3
fix: call awaitInitialization() before port()
ThanGerlek Nov 25, 2024
58b52f7
Merge branch 'main' into test-server-endpoints
ThanGerlek Dec 11, 2024
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
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>


</dependencies>
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/edu/byu/cs/server/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import edu.byu.cs.server.endpointprovider.EndpointProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Spark;

import static spark.Spark.*;

Expand All @@ -19,9 +20,14 @@ public Server(EndpointProvider endpointProvider) {
public int start(int desiredPort) {
int chosenPort = setupEndpoints(desiredPort);
LOGGER.info("Server started on port {}", chosenPort);
awaitInitialization();
return chosenPort;
}

public void stop() {
Spark.stop();
}

private int setupEndpoints(int port) {
port(port);

Expand Down
148 changes: 148 additions & 0 deletions src/test/java/edu/byu/cs/server/ServerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package edu.byu.cs.server;

import edu.byu.cs.server.endpointprovider.MockEndpointProvider;
import edu.byu.cs.server.exception.ResponseParseException;
import edu.byu.cs.server.exception.ServerConnectionException;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InOrder;

import java.io.IOException;
import java.util.*;
import java.util.stream.Stream;

import static org.mockito.Mockito.*;

class ServerTest {
private static TestServerFacade serverFacade;
private static Server server;

private static final MockEndpointProvider mockedMockProvider = spy(new MockEndpointProvider());

public static Stream<Arguments> getPathParamEndpoints() {
return Stream.of(
Arguments.of("GET", "commitAnalyticsGet", "/api/admin/analytics/commit", ":option"),
Arguments.of("GET", "honorCheckerZipGet", "/api/admin/honorChecker/zip", ":section"),
Arguments.of("GET", "submissionXGet", "/api/submission", ":phase"),
Arguments.of("GET", "latestSubmissionsGet", "/api/admin/submissions/latest", ":count"),
Arguments.of("GET", "studentSubmissionsGet", "/api/admin/submissions/student", ":netid")
);
}

public static Stream<Arguments> getEndpoints() {
return Stream.of(
Arguments.of("GET", "callbackGet", "/auth/callback"),
Arguments.of("GET", "loginGet", "/auth/login"),
Arguments.of("GET", "usersGet", "/api/admin/users"),
Arguments.of("GET", "testModeGet", "/api/admin/test_mode"),
Arguments.of("GET", "commitAnalyticsGet", "/api/admin/analytics/commit"),
Arguments.of("GET", "sectionsGet", "/api/admin/sections"),
Arguments.of("GET", "meGet", "/api/me"),
Arguments.of("GET", "getConfigAdmin", "/api/admin/config"),
Arguments.of("GET", "getConfigStudent", "/api/config"),
Arguments.of("GET", "updateCourseIdsUsingCanvasGet", "/api/admin/config/courseIds"),
Arguments.of("GET", "submitGet", "/api/submit"),
Arguments.of("GET", "latestSubmissionForMeGet", "/api/latest"),
Arguments.of("GET", "submissionXGet", "/api/submission"),
Arguments.of("GET", "latestSubmissionsGet", "/api/admin/submissions/latest"),
Arguments.of("GET", "submissionsActiveGet", "/api/admin/submissions/active"),
Arguments.of("GET", "repoHistoryAdminGet", "/api/admin/repo/history"),
Arguments.of("POST", "logoutPost", "/auth/logout"),
Arguments.of("POST", "updateLivePhases", "/api/admin/config/phases"),
Arguments.of("POST", "updateBannerMessage", "/api/admin/config/banner"),
Arguments.of("POST", "updateCourseIdsPost", "/api/admin/config/courseIds"),
Arguments.of("POST", "submitPost", "/api/submit"),
Arguments.of("POST", "adminRepoSubmitPost", "/api/admin/submit"),
Arguments.of("POST", "approveSubmissionPost", "/api/admin/submissions/approve"),
Arguments.of("POST", "submissionsReRunPost", "/api/admin/submissions/rerun")
);
}

// TODO figure out how to test PATCH calls... HttpURLConnection thinks it's an invalid method
ThanGerlek marked this conversation as resolved.
Show resolved Hide resolved

@AfterAll
static void stopServer() {
server.stop();
}

@BeforeAll
public static void init() {
server = new Server(mockedMockProvider);
int port = server.start(8080);
ThanGerlek marked this conversation as resolved.
Show resolved Hide resolved
System.out.println("Started test HTTP server on " + port);

serverFacade = new TestServerFacade("localhost", port);
}

@AfterEach
public void tearDown() {
reset(mockedMockProvider);
}

@ParameterizedTest
@MethodSource("getEndpoints")
public void verifyEndpointCallsItsHandlersExactlyOnceInOrder(String method, String endpointName, String path)
throws ServerConnectionException, ResponseParseException, IOException {
// When
serverFacade.makeRequest(method, path);

InOrder inOrder = inOrder(mockedMockProvider);

// Then
inOrder.verify(mockedMockProvider, times(1)).runHandler("beforeAll");
this.verifyInOrder_authenticationMiddleware(path, inOrder);
inOrder.verify(mockedMockProvider, times(1)).runHandler(endpointName);
inOrder.verify(mockedMockProvider, times(1)).runHandler("afterAll");
}

@ParameterizedTest
@MethodSource("getPathParamEndpoints")
public void verifyPathParameterHasAValueWhenGivenOne(String method, String endpointName, String path, String pathParamName)
throws ServerConnectionException, ResponseParseException, IOException {
// Given
String fullPath = path + "/testParamValue";

// When
serverFacade.makeRequest(method, fullPath);

// Then
verify(mockedMockProvider, times(1)).hasPathParam(endpointName, pathParamName, "testParamValue");
}

private void verifyInOrder_authenticationMiddleware(String path, InOrder inOrder) {
List<String> pathNodes = Arrays.stream(path.split("/")).toList();

if (!pathNodes.contains("api")) {
return;
}

if (!pathNodes.contains("auth")) {
// Requires authentication
inOrder.verify(mockedMockProvider, times(1)).runHandler("verifyAuthenticatedMiddleware");
}

if (pathNodes.contains("admin")) {
// Requires admin
inOrder.verify(mockedMockProvider, times(1)).runHandler("verifyAdminMiddleware");
}
}

@Test
void nonexistent_GET_endpoint_calls_beforeAll_then_defaultGet_then_afterAll_exactly_once_in_order()
throws IOException, ServerConnectionException, ResponseParseException {
serverFacade.makeRequest("GET", "/iDoNotExist");

// Verify they ran in order
InOrder inOrder = inOrder(mockedMockProvider);
inOrder.verify(mockedMockProvider).runHandler("beforeAll");
inOrder.verify(mockedMockProvider).runHandler("defaultGet");
inOrder.verify(mockedMockProvider).runHandler("afterAll");

// Verify they only ran once
verify(mockedMockProvider, times(1)).runHandler("beforeAll");
verify(mockedMockProvider, times(1)).runHandler("defaultGet");
verify(mockedMockProvider, times(1)).runHandler("afterAll");
}
}
112 changes: 112 additions & 0 deletions src/test/java/edu/byu/cs/server/TestServerFacade.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package edu.byu.cs.server;

import com.google.gson.Gson;
import edu.byu.cs.server.exception.ResponseParseException;
import edu.byu.cs.server.exception.ServerConnectionException;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;

public class TestServerFacade {
private final String serverURL;

public TestServerFacade(String serverURL, int port) {
this.serverURL = "http://%s:%d".formatted(serverURL, port);
}

public Object makeRequest(String method, String path) throws IOException, ServerConnectionException, ResponseParseException {
return makeRequest(method, path, null, null, Object.class);
}

public <T> T makeRequest(String method, String path, Object request, Map<String, String> headers,
Class<T> responseClass) throws IOException, ServerConnectionException, ResponseParseException {
HttpURLConnection http = getConnection(method, serverURL + path);
writeRequest(http, request, headers);
connect(http);
return readResponse(http, responseClass);
}

private HttpURLConnection getConnection(String method, String urlString) throws ServerConnectionException {
try {
URL url = (new URI(urlString)).toURL();
HttpURLConnection http = (HttpURLConnection) url.openConnection();
http.setRequestMethod(method);
http.setDoOutput("POST".equals(method) || "PUT".equals(method));
return http;
} catch (IOException | URISyntaxException e) {
throw new ServerConnectionException("Failed to set up HTTP connection: " + e.getMessage());
}
}

private void writeRequest(HttpURLConnection http, Object requestBody, Map<String, String> headers) throws ServerConnectionException {
try {
writeHeaders(http, headers);
writeRequestBody(http, requestBody);
} catch (IOException e) {
throw new ServerConnectionException("Could not write request body: " + e.getMessage());
}
}

private void connect(HttpURLConnection http) throws ServerConnectionException {
try {
http.connect();
} catch (IOException e) {
throw new ServerConnectionException("Failed to connect to server: " + e.getMessage());
}
}

private <T> T readResponse(HttpURLConnection http, Class<T> responseClass) throws IOException,
ResponseParseException {
String respString = getRespString(http);
try {
return new Gson().fromJson(respString, responseClass);
} catch (Exception e) {
String message = String.format("Error parsing response. Expected JSON, got '%s'", respString);
throw new ResponseParseException(message, e);
}
}

private void writeHeaders(HttpURLConnection http, Map<String, String> headers) {
http.addRequestProperty("Content-type", "application/json");
if (headers != null) {
for (String headerName : headers.keySet()) {
String headerValue = headers.get(headerName);
http.addRequestProperty(headerName, headerValue);
}
}
}

private void writeRequestBody(HttpURLConnection http, Object request) throws IOException {
if (request != null) {
String requestData = new Gson().toJson(request);
try (OutputStream reqBody = http.getOutputStream()) {
reqBody.write(requestData.getBytes());
}
}
}

private String getRespString(HttpURLConnection http) throws IOException {
InputStream respBody;
if (http.getResponseCode() / 100 == 2) {
respBody = http.getInputStream();
} else {
respBody = http.getErrorStream();
}
StringBuilder sb = new StringBuilder();
InputStreamReader sr = new InputStreamReader(respBody);
char[] buf = new char[1024];
int len;
while ((len = sr.read(buf)) > 0) {
sb.append(buf, 0, len);
}
respBody.close();
return sb.toString();
}
}
Loading