Skip to content

Commit 16818cf

Browse files
authored
Merge pull request #12 from G8XSU/api-test
Add AbstractVssApi, get/put/list api handlers
2 parents ff4b5fc + f2c59ed commit 16818cf

File tree

10 files changed

+447
-0
lines changed

10 files changed

+447
-0
lines changed

app/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ buildscript {
44
ext.protobufVersion = '3.21.7'
55
ext.jerseyVersion = '3.1.0'
66
ext.junitVersion = '5.9.0'
7+
ext.mockitoVersion = '5.2.0'
78
ext.postgresVersion = '42.5.1'
89
ext.jooqVersion = '3.17.7'
910
ext.guiceVersion = '5.1.0'
@@ -33,6 +34,8 @@ version '1.0'
3334
dependencies {
3435
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
3536

37+
implementation('org.glassfish.jersey.containers:jersey-container-servlet:3.1.0')
38+
3639
//jOOQ & Postgres impl deps
3740
implementation "org.jooq:jooq:$jooqVersion"
3841
implementation "org.jooq:jooq-meta:$jooqVersion"
@@ -42,7 +45,12 @@ dependencies {
4245

4346
implementation "com.google.inject:guice:$guiceVersion"
4447

48+
compileOnly 'org.projectlombok:lombok:1.18.24'
49+
annotationProcessor 'org.projectlombok:lombok:1.18.24'
50+
4551
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
52+
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
53+
testImplementation "org.mockito:mockito-core:$mockitoVersion"
4654
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
4755
testImplementation "org.hamcrest:hamcrest-library:2.2"
4856
testImplementation "org.testcontainers:junit-jupiter:1.17.6"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.vss;
2+
3+
import jakarta.ws.rs.ApplicationPath;
4+
import org.glassfish.jersey.server.ResourceConfig;
5+
6+
@ApplicationPath("/")
7+
public class VSSApplication extends ResourceConfig {
8+
public VSSApplication() {
9+
}
10+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package org.vss.api;
2+
3+
import com.google.protobuf.GeneratedMessageV3;
4+
import com.google.protobuf.InvalidProtocolBufferException;
5+
import jakarta.inject.Inject;
6+
import jakarta.ws.rs.core.Response;
7+
import org.vss.ErrorCode;
8+
import org.vss.ErrorResponse;
9+
import org.vss.KVStore;
10+
import org.vss.exception.ConflictException;
11+
12+
public abstract class AbstractVssApi {
13+
final KVStore kvStore;
14+
15+
@Inject
16+
public AbstractVssApi(KVStore kvStore) {
17+
this.kvStore = kvStore;
18+
}
19+
20+
Response toResponse(GeneratedMessageV3 protoResponse) {
21+
22+
return Response
23+
.status(Response.Status.OK)
24+
.entity(protoResponse.toByteArray())
25+
.build();
26+
}
27+
28+
Response toErrorResponse(Exception e) {
29+
ErrorCode errorCode;
30+
if (e instanceof ConflictException) {
31+
errorCode = ErrorCode.CONFLICT_EXCEPTION;
32+
} else if (e instanceof IllegalArgumentException
33+
|| e instanceof InvalidProtocolBufferException) {
34+
errorCode = ErrorCode.INVALID_REQUEST_EXCEPTION;
35+
} else {
36+
errorCode = ErrorCode.INTERNAL_SERVER_EXCEPTION;
37+
}
38+
39+
ErrorResponse errorResponse = ErrorResponse.newBuilder()
40+
.setErrorCode(errorCode)
41+
.setMessage(e.getMessage())
42+
.build();
43+
44+
return Response.status(errorCode.getNumber())
45+
.entity(errorResponse.toByteArray())
46+
.build();
47+
}
48+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.vss.api;
2+
3+
import jakarta.inject.Inject;
4+
import jakarta.ws.rs.POST;
5+
import jakarta.ws.rs.Path;
6+
import jakarta.ws.rs.Produces;
7+
import jakarta.ws.rs.core.MediaType;
8+
import jakarta.ws.rs.core.Response;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.vss.GetObjectRequest;
11+
import org.vss.GetObjectResponse;
12+
import org.vss.KVStore;
13+
14+
@Path(VssApiEndpoint.GET_OBJECT)
15+
@Slf4j
16+
public class GetObjectApi extends AbstractVssApi {
17+
18+
@Inject
19+
public GetObjectApi(KVStore kvstore) {
20+
super(kvstore);
21+
}
22+
23+
@POST
24+
@Produces(MediaType.APPLICATION_OCTET_STREAM)
25+
public Response execute(byte[] payload) {
26+
try {
27+
GetObjectRequest request = GetObjectRequest.parseFrom(payload);
28+
GetObjectResponse response = kvStore.get(request);
29+
return toResponse(response);
30+
} catch (Exception e) {
31+
log.error("Exception in GetObjectApi: ", e);
32+
return toErrorResponse(e);
33+
}
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.vss.api;
2+
3+
import jakarta.inject.Inject;
4+
import jakarta.ws.rs.POST;
5+
import jakarta.ws.rs.Path;
6+
import jakarta.ws.rs.Produces;
7+
import jakarta.ws.rs.core.MediaType;
8+
import jakarta.ws.rs.core.Response;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.vss.KVStore;
11+
import org.vss.ListKeyVersionsRequest;
12+
import org.vss.ListKeyVersionsResponse;
13+
14+
@Path(VssApiEndpoint.LIST_KEY_VERSIONS)
15+
@Slf4j
16+
public class ListKeyVersionsApi extends AbstractVssApi {
17+
18+
@Inject
19+
public ListKeyVersionsApi(KVStore kvStore) {
20+
super(kvStore);
21+
}
22+
23+
@POST
24+
@Produces(MediaType.APPLICATION_OCTET_STREAM)
25+
public Response execute(byte[] payload) {
26+
try {
27+
ListKeyVersionsRequest request = ListKeyVersionsRequest.parseFrom(payload);
28+
ListKeyVersionsResponse response = kvStore.listKeyVersions(request);
29+
return toResponse(response);
30+
} catch (Exception e) {
31+
log.error("Exception in ListKeyVersionsApi: ", e);
32+
return toErrorResponse(e);
33+
}
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.vss.api;
2+
3+
import jakarta.inject.Inject;
4+
import jakarta.ws.rs.POST;
5+
import jakarta.ws.rs.Path;
6+
import jakarta.ws.rs.Produces;
7+
import jakarta.ws.rs.core.MediaType;
8+
import jakarta.ws.rs.core.Response;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.vss.KVStore;
11+
import org.vss.PutObjectRequest;
12+
import org.vss.PutObjectResponse;
13+
14+
@Path(VssApiEndpoint.PUT_OBJECTS)
15+
@Slf4j
16+
public class PutObjectsApi extends AbstractVssApi {
17+
18+
@Inject
19+
public PutObjectsApi(KVStore kvStore) {
20+
super(kvStore);
21+
}
22+
23+
@POST
24+
@Produces(MediaType.APPLICATION_OCTET_STREAM)
25+
public Response execute(byte[] payload) {
26+
try {
27+
PutObjectRequest putObjectRequest = PutObjectRequest.parseFrom(payload);
28+
PutObjectResponse response = kvStore.put(putObjectRequest);
29+
return toResponse(response);
30+
} catch (Exception e) {
31+
log.error("Exception in PutObjectsApi: ", e);
32+
return toErrorResponse(e);
33+
}
34+
}
35+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.vss.api;
2+
3+
public class VssApiEndpoint {
4+
public static final String GET_OBJECT = "/getObject";
5+
public static final String PUT_OBJECTS = "/putObjects";
6+
public static final String LIST_KEY_VERSIONS = "/listKeyVersions";
7+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package org.vss.api;
2+
3+
import com.google.protobuf.ByteString;
4+
import jakarta.ws.rs.core.Response;
5+
import java.nio.charset.StandardCharsets;
6+
import java.util.stream.Stream;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.params.ParameterizedTest;
10+
import org.junit.jupiter.params.provider.Arguments;
11+
import org.junit.jupiter.params.provider.MethodSource;
12+
import org.vss.ErrorCode;
13+
import org.vss.ErrorResponse;
14+
import org.vss.GetObjectRequest;
15+
import org.vss.GetObjectResponse;
16+
import org.vss.KVStore;
17+
import org.vss.KeyValue;
18+
import org.vss.exception.ConflictException;
19+
20+
import static org.hamcrest.MatcherAssert.assertThat;
21+
import static org.hamcrest.Matchers.is;
22+
import static org.mockito.ArgumentMatchers.any;
23+
import static org.mockito.Mockito.mock;
24+
import static org.mockito.Mockito.verify;
25+
import static org.mockito.Mockito.when;
26+
27+
class GetObjectApiTest {
28+
private GetObjectApi getObjectApi;
29+
private KVStore mockKVStore;
30+
31+
private static String TEST_STORE_ID = "storeId";
32+
private static String TEST_KEY = "key";
33+
private static KeyValue TEST_KV = KeyValue.newBuilder().setKey(TEST_KEY).setValue(
34+
ByteString.copyFrom("test_value", StandardCharsets.UTF_8)).build();
35+
36+
@BeforeEach
37+
void setUp() {
38+
mockKVStore = mock(KVStore.class);
39+
getObjectApi = new GetObjectApi(mockKVStore);
40+
}
41+
42+
@Test
43+
void execute_ValidPayload_ReturnsResponse() {
44+
GetObjectRequest expectedRequest =
45+
GetObjectRequest.newBuilder().setStoreId(TEST_STORE_ID).setKey(TEST_KEY).build();
46+
byte[] payload = expectedRequest.toByteArray();
47+
GetObjectResponse mockResponse = GetObjectResponse.newBuilder().setValue(TEST_KV).build();
48+
when(mockKVStore.get(expectedRequest)).thenReturn(mockResponse);
49+
50+
Response actualResponse = getObjectApi.execute(payload);
51+
52+
assertThat(actualResponse.getStatus(), is(Response.Status.OK.getStatusCode()));
53+
assertThat(actualResponse.getEntity(), is(mockResponse.toByteArray()));
54+
verify(mockKVStore).get(expectedRequest);
55+
}
56+
57+
@ParameterizedTest
58+
@MethodSource("provideErrorTestCases")
59+
void execute_InvalidPayload_ReturnsErrorResponse(Exception exception,
60+
ErrorCode errorCode) {
61+
GetObjectRequest expectedRequest = GetObjectRequest.newBuilder()
62+
.setStoreId(TEST_STORE_ID)
63+
.setKey(TEST_KEY)
64+
.build();
65+
byte[] payload = expectedRequest.toByteArray();
66+
when(mockKVStore.get(any())).thenThrow(exception);
67+
68+
Response response = getObjectApi.execute(payload);
69+
70+
ErrorResponse expectedErrorResponse = ErrorResponse.newBuilder()
71+
.setErrorCode(errorCode)
72+
.setMessage("")
73+
.build();
74+
assertThat(response.getEntity(), is(expectedErrorResponse.toByteArray()));
75+
assertThat(response.getStatus(), is(expectedErrorResponse.getErrorCode().getNumber()));
76+
verify(mockKVStore).get(expectedRequest);
77+
}
78+
79+
private static Stream<Arguments> provideErrorTestCases() {
80+
return Stream.of(
81+
Arguments.of(new ConflictException(""), ErrorCode.CONFLICT_EXCEPTION),
82+
Arguments.of(new IllegalArgumentException(""), ErrorCode.INVALID_REQUEST_EXCEPTION),
83+
Arguments.of(new RuntimeException(""), ErrorCode.INTERNAL_SERVER_EXCEPTION)
84+
);
85+
}
86+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package org.vss.api;
2+
3+
import com.google.protobuf.ByteString;
4+
import jakarta.ws.rs.core.Response;
5+
import java.nio.charset.StandardCharsets;
6+
import java.util.List;
7+
import java.util.stream.Stream;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.params.ParameterizedTest;
11+
import org.junit.jupiter.params.provider.Arguments;
12+
import org.junit.jupiter.params.provider.MethodSource;
13+
import org.vss.ErrorCode;
14+
import org.vss.ErrorResponse;
15+
import org.vss.KVStore;
16+
import org.vss.KeyValue;
17+
import org.vss.ListKeyVersionsRequest;
18+
import org.vss.ListKeyVersionsResponse;
19+
import org.vss.exception.ConflictException;
20+
21+
import static org.hamcrest.MatcherAssert.assertThat;
22+
import static org.hamcrest.Matchers.is;
23+
import static org.mockito.ArgumentMatchers.any;
24+
import static org.mockito.Mockito.mock;
25+
import static org.mockito.Mockito.verify;
26+
import static org.mockito.Mockito.when;
27+
28+
public class ListKeyVersionsApiTest {
29+
private ListKeyVersionsApi listKeyVersionsApi;
30+
private KVStore mockKVStore;
31+
32+
private static String TEST_STORE_ID = "storeId";
33+
private static String TEST_KEY = "key";
34+
private static KeyValue TEST_KV = KeyValue.newBuilder().setKey(TEST_KEY).setValue(
35+
ByteString.copyFrom("test_value", StandardCharsets.UTF_8)).build();
36+
37+
@BeforeEach
38+
void setUp() {
39+
mockKVStore = mock(KVStore.class);
40+
listKeyVersionsApi = new ListKeyVersionsApi(mockKVStore);
41+
}
42+
43+
@Test
44+
void execute_ValidPayload_ReturnsResponse() {
45+
ListKeyVersionsRequest expectedRequest =
46+
ListKeyVersionsRequest.newBuilder()
47+
.setStoreId(TEST_STORE_ID)
48+
.setKeyPrefix(TEST_KEY)
49+
.build();
50+
byte[] payload = expectedRequest.toByteArray();
51+
ListKeyVersionsResponse mockResponse = ListKeyVersionsResponse.newBuilder().addAllKeyVersions(
52+
List.of(TEST_KV)).build();
53+
when(mockKVStore.listKeyVersions(expectedRequest)).thenReturn(mockResponse);
54+
55+
Response actualResponse = listKeyVersionsApi.execute(payload);
56+
57+
assertThat(actualResponse.getStatus(), is(Response.Status.OK.getStatusCode()));
58+
assertThat(actualResponse.getEntity(), is(mockResponse.toByteArray()));
59+
verify(mockKVStore).listKeyVersions(expectedRequest);
60+
}
61+
62+
@ParameterizedTest
63+
@MethodSource("provideErrorTestCases")
64+
void execute_InvalidPayload_ReturnsErrorResponse(Exception exception,
65+
ErrorCode errorCode) {
66+
ListKeyVersionsRequest expectedRequest =
67+
ListKeyVersionsRequest.newBuilder()
68+
.setStoreId(TEST_STORE_ID)
69+
.setKeyPrefix(TEST_KEY)
70+
.build();
71+
byte[] payload = expectedRequest.toByteArray();
72+
when(mockKVStore.listKeyVersions(any())).thenThrow(exception);
73+
74+
Response response = listKeyVersionsApi.execute(payload);
75+
76+
ErrorResponse expectedErrorResponse = ErrorResponse.newBuilder()
77+
.setErrorCode(errorCode)
78+
.setMessage("")
79+
.build();
80+
assertThat(response.getEntity(), is(expectedErrorResponse.toByteArray()));
81+
assertThat(response.getStatus(), is(expectedErrorResponse.getErrorCode().getNumber()));
82+
verify(mockKVStore).listKeyVersions(expectedRequest);
83+
}
84+
85+
private static Stream<Arguments> provideErrorTestCases() {
86+
return Stream.of(
87+
Arguments.of(new ConflictException(""), ErrorCode.CONFLICT_EXCEPTION),
88+
Arguments.of(new IllegalArgumentException(""), ErrorCode.INVALID_REQUEST_EXCEPTION),
89+
Arguments.of(new RuntimeException(""), ErrorCode.INTERNAL_SERVER_EXCEPTION)
90+
);
91+
}
92+
}

0 commit comments

Comments
 (0)