[java-identity-provider] branch maint-4 updated: IDP-1976 - Extend storage admin flow with create/update operations
Scott Cantor
cantor.2 at osu.edu
Wed Jul 20 19:18:03 UTC 2022
This is an automated email from the git hooks/post-receive script.
scantor pushed a commit to branch maint-4
in repository java-identity-provider.
View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=3c24c16eecf8312251a78f0dbbf54ae20e9b6059
The following commit(s) were added to refs/heads/maint-4 by this push:
new 3c24c16ee IDP-1976 - Extend storage admin flow with create/update operations
3c24c16ee is described below
commit 3c24c16eecf8312251a78f0dbbf54ae20e9b6059
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Jul 20 15:18:00 2022 -0400
IDP-1976 - Extend storage admin flow with create/update operations
https://shibboleth.atlassian.net/browse/IDP-1976
---
.../idp/admin/impl/DoStorageOperation.java | 233 +++++++++---
.../idp/admin/impl/DoStorageOperationTest.java | 398 +++++++++++++++++++++
2 files changed, 577 insertions(+), 54 deletions(-)
diff --git a/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/DoStorageOperation.java b/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/DoStorageOperation.java
index da48be4cf..faeef6b88 100644
--- a/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/DoStorageOperation.java
+++ b/idp-admin-impl/src/main/java/net/shibboleth/idp/admin/impl/DoStorageOperation.java
@@ -18,7 +18,6 @@
package net.shibboleth.idp.admin.impl;
import java.io.IOException;
-import java.time.Instant;
import java.util.Collections;
import javax.annotation.Nonnull;
@@ -39,13 +38,15 @@ import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.storage.StorageRecord;
import org.opensaml.storage.StorageService;
+import org.opensaml.storage.VersionMismatchException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.beans.BeansException;
import org.springframework.webflow.execution.RequestContext;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.jasminb.jsonapi.models.errors.Error;
import com.github.jasminb.jsonapi.models.errors.Errors;
@@ -96,6 +97,19 @@ public class DoStorageOperation extends AbstractProfileAction {
objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
}
+
+ /**
+ * Sets the {@link StorageService} to use.
+ *
+ * <p>Primarily for testing, to bypass use of Spring to obtain the service to use.</p>
+ *
+ * @param storage storage service
+ */
+ public void setStorageService(@Nullable final StorageService storage) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ storageService = storage;
+ }
/** {@inheritDoc} */
@Override
@@ -107,6 +121,7 @@ public class DoStorageOperation extends AbstractProfileAction {
}
}
+// Checkstyle: CyclomaticComplexity OFF
/** {@inheritDoc} */
@Override
protected boolean doPreExecute(final ProfileRequestContext profileRequestContext) {
@@ -137,11 +152,13 @@ public class DoStorageOperation extends AbstractProfileAction {
return false;
}
- storageService = getStorageService(requestContext);
if (storageService == null) {
- sendError(HttpServletResponse.SC_NOT_FOUND,
- "Invalid Storage Service", "Invalid storage service identifier in path.");
- return false;
+ storageService = getStorageService(requestContext);
+ if (storageService == null) {
+ sendError(HttpServletResponse.SC_NOT_FOUND,
+ "Invalid Storage Service", "Invalid storage service identifier in path.");
+ return false;
+ }
}
context = (String) requestContext.getFlowScope().get(CONTEXT);
@@ -160,6 +177,7 @@ public class DoStorageOperation extends AbstractProfileAction {
return true;
}
+// Checkstyle: CyclomaticComplexity ON
/** {@inheritDoc} */
@Override protected void doExecute(final ProfileRequestContext profileRequestContext) {
@@ -172,52 +190,18 @@ public class DoStorageOperation extends AbstractProfileAction {
response.setHeader("Cache-Control", "must-revalidate,no-cache,no-store");
if ("GET".equals(request.getMethod())) {
- final StorageRecord<?> record;
- try {
- record = storageService.read(context, key);
- if (record != null) {
- response.setStatus(HttpServletResponse.SC_OK);
- final JsonFactory jsonFactory = new JsonFactory();
- try (final JsonGenerator g = jsonFactory.createGenerator(
- response.getOutputStream()).useDefaultPrettyPrinter()) {
- g.setCodec(objectMapper);
- g.writeStartObject();
- g.writeObjectFieldStart("data");
- g.writeStringField("type", "records");
- g.writeStringField("id", storageService.getId() + '/' + context +'/' + key);
- g.writeObjectFieldStart("attributes");
- g.writeStringField("value", record.getValue());
- g.writeNumberField("version", record.getVersion());
- if (record.getExpiration() != null) {
- g.writeFieldName("expiration");
- g.writeObject(Instant.ofEpochMilli(record.getExpiration()));
- }
- }
- } else {
- sendError(HttpServletResponse.SC_NOT_FOUND,
- "Record Not Found", "The specified record was not present or has expired.");
- }
- } catch (final IOException e) {
- sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error", "Storage error.");
- }
+ doRead();
+ } else if ("PUT".equals(request.getMethod())) {
+ doCreate();
+ } else if ("POST".equals(request.getMethod())) {
+ doUpdate();
} else if ("DELETE".equals(request.getMethod())) {
- try {
- if (storageService.delete(context, key)) {
- response.setStatus(HttpServletResponse.SC_NO_CONTENT);
- } else {
- sendError(HttpServletResponse.SC_NOT_FOUND,
- "Record Not Found", "The specified record was not present or has expired.");
- }
- } catch (final IOException e) {
- sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error", "Storage error.");
- }
-
+ doDelete();
} else {
log.warn("{} Invalid method: {}", getLogPrefix(), request.getMethod());
sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED,
- "Unknown Operation", "Only GET and DELETE are supported.");
+ "Unknown Operation", "GET, PUT, POST, DELETE are supported.");
}
-
} catch (final IOException e) {
log.error("{} I/O error responding to request", getLogPrefix(), e);
ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
@@ -239,17 +223,158 @@ public class DoStorageOperation extends AbstractProfileAction {
return null;
}
+ return getBean(requestContext, id, StorageService.class);
+ }
+
+ /**
+ * Perform read operation.
+ *
+ * @throws IOException if an error is raised
+ */
+ private void doRead() throws IOException {
+ final StorageRecord<?> record;
try {
- final Object bean = requestContext.getActiveFlow().getApplicationContext().getBean(id);
- if (bean != null && bean instanceof StorageService) {
- return (StorageService) bean;
+ record = storageService.read(context, key);
+ if (record != null) {
+ getHttpServletResponse().setStatus(HttpServletResponse.SC_OK);
+ final JsonFactory jsonFactory = new JsonFactory();
+ try (final JsonGenerator g = jsonFactory.createGenerator(
+ getHttpServletResponse().getOutputStream()).useDefaultPrettyPrinter()) {
+ g.setCodec(objectMapper);
+ g.writeStartObject();
+ g.writeObjectFieldStart("data");
+ g.writeStringField("type", "records");
+ g.writeStringField("id", storageService.getId() + '/' + context +'/' + key);
+ g.writeObjectFieldStart("attributes");
+ g.writeStringField("value", record.getValue());
+ g.writeNumberField("version", record.getVersion());
+ if (record.getExpiration() != null) {
+ g.writeFieldName("expiration");
+ g.writeObject(record.getExpiration());
+ }
+ }
+ } else {
+ sendError(HttpServletResponse.SC_NOT_FOUND,
+ "Record Not Found", "The specified record was not present or has expired.");
}
- } catch (final BeansException e) {
-
+ } catch (final IOException e) {
+ sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error", "Storage error.");
+ }
+ }
+
+
+ /**
+ * Perform create operation.
+ *
+ * @throws IOException if an error is raised
+ */
+ private void doCreate() throws IOException {
+ final JsonFactory jsonFactory = new JsonFactory();
+ final JsonParser parser = jsonFactory.createParser(getHttpServletRequest().getInputStream());
+
+ if (parser.nextToken() != JsonToken.START_OBJECT) {
+ throw new IOException("Expected data to start with an Object");
}
- log.warn("{} No bean of the correct type found named {}", getLogPrefix(), id);
- return null;
+ String value = null;
+ Long exp = null;
+
+ while (parser.nextToken() != JsonToken.END_OBJECT) {
+ final String fieldName = parser.getCurrentName();
+ parser.nextToken();
+ if ("value".equals(fieldName)) {
+ value = parser.getText();
+ } else if ("expiration".equals(fieldName)) {
+ exp = parser.getLongValue();
+ }
+ }
+
+ if (value == null) {
+ throw new IOException("Input missing 'val' field");
+ }
+
+ if (storageService.create(context, key, value, exp)) {
+ getHttpServletResponse().setStatus(HttpServletResponse.SC_CREATED);
+ } else {
+ sendError(HttpServletResponse.SC_CONFLICT, "Duplicate Record",
+ "Context and key matched an existing record.");
+ }
+ }
+
+// Checkstyle: CyclomaticComplexity OFF
+ /**
+ * Perform update operation.
+ *
+ * @throws IOException if an error is raised
+ */
+ private void doUpdate() throws IOException {
+ final JsonFactory jsonFactory = new JsonFactory();
+ final JsonParser parser = jsonFactory.createParser(getHttpServletRequest().getInputStream());
+
+ if (parser.nextToken() != JsonToken.START_OBJECT) {
+ throw new IOException("Expected data to start with an Object");
+ }
+
+ String value = null;
+ Long version = null;
+ Long exp = null;
+
+ while (parser.nextToken() != JsonToken.END_OBJECT) {
+ final String fieldName = parser.getCurrentName();
+ parser.nextToken();
+ if ("value".equals(fieldName)) {
+ value = parser.getText();
+ } else if ("expiration".equals(fieldName)) {
+ exp = parser.getLongValue();
+ } else if ("version".equals(fieldName)) {
+ version = parser.getLongValue();
+ }
+ }
+
+ if (value == null) {
+ throw new IOException("Input missing 'value' field");
+ }
+
+ if (version != null) {
+ try {
+ version = storageService.updateWithVersion(version, context, key, value, exp);
+ if (version != null) {
+ getHttpServletResponse().setStatus(HttpServletResponse.SC_OK);
+ } else {
+ sendError(HttpServletResponse.SC_NOT_FOUND, "Not Found", "Record to update was absent.");
+ }
+ } catch (final VersionMismatchException e) {
+ sendError(HttpServletResponse.SC_CONFLICT, "Version Mismatch", "Record version did not match.");
+ }
+ } else {
+ if (storageService.update(context, key, value, exp)) {
+ getHttpServletResponse().setStatus(HttpServletResponse.SC_OK);
+ } else if (storageService.create(context, key, value, exp)) {
+ getHttpServletResponse().setStatus(HttpServletResponse.SC_CREATED);
+ } else {
+ sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error",
+ "Record to update was absent and create attempt failed.");
+ }
+ }
+ }
+// Checkstyle: CyclomaticComplexity ON
+
+ /**
+ * Perform delete operation.
+ *
+ * @throws IOException if an error is raised
+ */
+ private void doDelete() throws IOException {
+ try {
+ if (storageService.delete(context, key)) {
+ getHttpServletResponse().setStatus(HttpServletResponse.SC_NO_CONTENT);
+ } else {
+ sendError(HttpServletResponse.SC_NOT_FOUND,
+ "Record Not Found", "The specified record was not present or has expired.");
+ }
+ } catch (final IOException e) {
+ sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error", "Storage error.");
+ }
}
/**
diff --git a/idp-admin-impl/src/test/java/net/shibboleth/idp/admin/impl/DoStorageOperationTest.java b/idp-admin-impl/src/test/java/net/shibboleth/idp/admin/impl/DoStorageOperationTest.java
new file mode 100644
index 000000000..02d8700e8
--- /dev/null
+++ b/idp-admin-impl/src/test/java/net/shibboleth/idp/admin/impl/DoStorageOperationTest.java
@@ -0,0 +1,398 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.admin.impl;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.time.Duration;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.servlet.http.HttpServletResponse;
+
+import org.joda.time.Instant;
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.impl.MemoryStorageService;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.webflow.execution.Event;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Unit test for {@link DoStorageOperation} action.
+ */
+public class DoStorageOperationTest {
+
+ /** Test context. */
+ @Nonnull @NotEmpty private final static String CONTEXT = "testContext";
+
+ /** Test key. */
+ @Nonnull @NotEmpty private final static String KEY = "testKey";
+
+ /** Test value. */
+ @Nonnull @NotEmpty private final static String VALUE = "testValue";
+
+ private MemoryStorageService storageService;
+ private ObjectMapper mapper;
+ private DoStorageOperation action;
+
+ private RequestContext rc;
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+
+ /**
+ * Set up test.
+ *
+ * @throws ComponentInitializationException
+ */
+ @BeforeMethod
+ public void setUp() throws ComponentInitializationException {
+
+ rc = new RequestContextBuilder().buildRequestContext();
+
+ request = (MockHttpServletRequest) rc.getExternalContext().getNativeRequest();
+ response = (MockHttpServletResponse) rc.getExternalContext().getNativeResponse();
+
+ storageService = new MemoryStorageService();
+ storageService.setId("test");
+ storageService.setCleanupInterval(Duration.ZERO);
+ storageService.initialize();
+
+ mapper = new ObjectMapper();
+ mapper.setSerializationInclusion(Include.NON_NULL);
+
+ action = new DoStorageOperation();
+ action.setHttpServletRequest(request);
+ action.setHttpServletResponse(response);
+ action.setStorageService(storageService);
+ action.setObjectMapper(mapper);
+ action.initialize();
+ }
+
+ /**
+ * Tear down test.
+ */
+ @AfterMethod
+ public void tearDown() {
+ action.destroy();
+ storageService.destroy();
+ }
+
+ /**
+ * Test with no inputs.
+ */
+ @Test
+ public void noParams() {
+ final Event event = action.execute(rc);
+
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getStatus(), HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ /**
+ * Test invalidMethod
+ */
+ @Test
+ public void invalidMethod() {
+
+ request.setMethod("FOO");
+ rc.getFlowScope().put(DoStorageOperation.CONTEXT, CONTEXT);
+ rc.getFlowScope().put(DoStorageOperation.KEY, KEY);
+
+ final Event event = action.execute(rc);
+
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getStatus(), HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+ }
+
+ /**
+ * Test with no inputs.
+ */
+ @Test
+ public void missingGet() {
+
+ request.setMethod("GET");
+ rc.getFlowScope().put(DoStorageOperation.CONTEXT, CONTEXT);
+ rc.getFlowScope().put(DoStorageOperation.KEY, KEY);
+
+ final Event event = action.execute(rc);
+
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getStatus(), HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ /**
+ * Test successful get.
+ *
+ * @throws IOException
+ * @throws ParseException
+ */
+ @SuppressWarnings("unchecked")
+ @Test
+ public void successGet() throws IOException, ParseException {
+
+ final long exp = Instant.now().getMillis() + Duration.ofMinutes(15).toMillis();
+
+ storageService.create(CONTEXT, KEY, VALUE, exp);
+
+ request.setMethod("GET");
+ rc.getFlowScope().put(DoStorageOperation.CONTEXT, CONTEXT);
+ rc.getFlowScope().put(DoStorageOperation.KEY, KEY);
+
+ final Event event = action.execute(rc);
+
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getStatus(), HttpServletResponse.SC_OK);
+
+ final Map<String,Object> record = mapper.readerFor(Map.class).readValue(response.getContentAsByteArray());
+ final Map<String,Object> data = (Map<String, Object>) record.get("data");
+ Assert.assertEquals(data.get("type"), "records");
+ Assert.assertEquals(data.get("id"), "test/" + CONTEXT + "/" + KEY);
+ final Map<String,Object> attributes = (Map<String, Object>) data.get("attributes");
+ Assert.assertEquals(attributes.get("value"), VALUE);
+ Assert.assertEquals(attributes.get("version"), 1);
+ Assert.assertEquals(attributes.get("expiration"), exp);
+ }
+
+ /**
+ * Test missing delete.
+ *
+ * @throws IOException
+ * @throws ParseException
+ */
+ @Test
+ public void missingDelete() throws IOException, ParseException {
+
+ storageService.create(CONTEXT, KEY, VALUE, null);
+
+ request.setMethod("DELETE");
+ rc.getFlowScope().put(DoStorageOperation.CONTEXT, CONTEXT);
+ rc.getFlowScope().put(DoStorageOperation.KEY, KEY + "2");
+
+ final Event event = action.execute(rc);
+
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getStatus(), HttpServletResponse.SC_NOT_FOUND);
+ Assert.assertNotNull(storageService.read(CONTEXT, KEY));
+ }
+
+ /**
+ * Test successful delete.
+ *
+ * @throws IOException
+ * @throws ParseException
+ */
+ @Test
+ public void successDelete() throws IOException, ParseException {
+
+ storageService.create(CONTEXT, KEY, VALUE, null);
+
+ request.setMethod("DELETE");
+ rc.getFlowScope().put(DoStorageOperation.CONTEXT, CONTEXT);
+ rc.getFlowScope().put(DoStorageOperation.KEY, KEY);
+
+ final Event event = action.execute(rc);
+
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getStatus(), HttpServletResponse.SC_NO_CONTENT);
+
+ Assert.assertNull(storageService.read(CONTEXT, KEY));
+ }
+
+ /**
+ * Test successful create.
+ *
+ * @throws IOException
+ * @throws ParseException
+ */
+ @Test
+ public void successCreate() throws IOException, ParseException {
+
+ request.setMethod("PUT");
+ rc.getFlowScope().put(DoStorageOperation.CONTEXT, CONTEXT);
+ rc.getFlowScope().put(DoStorageOperation.KEY, KEY);
+ request.setContent("{ \"value\": \"testValue\" }".getBytes());
+
+ final Event event = action.execute(rc);
+
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getStatus(), HttpServletResponse.SC_CREATED);
+
+ final StorageRecord<?> record = storageService.read(CONTEXT, KEY);
+ Assert.assertEquals(record.getVersion(), 1);
+ Assert.assertEquals(record.getValue(), VALUE);
+ }
+
+ /**
+ * Test duplicate create.
+ *
+ * @throws IOException
+ * @throws ParseException
+ */
+ @Test
+ public void duplicateCreate() throws IOException, ParseException {
+
+ storageService.create(CONTEXT, KEY, VALUE, null);
+
+ request.setMethod("PUT");
+ rc.getFlowScope().put(DoStorageOperation.CONTEXT, CONTEXT);
+ rc.getFlowScope().put(DoStorageOperation.KEY, KEY);
+ request.setContent("{ \"value\": \"testValue\" }".getBytes());
+
+ final Event event = action.execute(rc);
+
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getStatus(), HttpServletResponse.SC_CONFLICT);
+ }
+
+ /**
+ * Test successful update.
+ *
+ * @throws IOException
+ * @throws ParseException
+ */
+ @Test
+ public void successUpdate() throws IOException, ParseException {
+
+ storageService.create(CONTEXT, KEY, VALUE, null);
+
+ request.setMethod("POST");
+ rc.getFlowScope().put(DoStorageOperation.CONTEXT, CONTEXT);
+ rc.getFlowScope().put(DoStorageOperation.KEY, KEY);
+ request.setContent("{ \"value\": \"changed\" }".getBytes());
+
+ final Event event = action.execute(rc);
+
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getStatus(), HttpServletResponse.SC_OK);
+
+ final StorageRecord<?> record = storageService.read(CONTEXT, KEY);
+ Assert.assertEquals(record.getVersion(), 2);
+ Assert.assertEquals(record.getValue(), "changed");
+ }
+
+ /**
+ * Test successful update as a create.
+ *
+ * @throws IOException
+ * @throws ParseException
+ */
+ @Test
+ public void successUpdateAsCreate() throws IOException, ParseException {
+
+ request.setMethod("POST");
+ rc.getFlowScope().put(DoStorageOperation.CONTEXT, CONTEXT);
+ rc.getFlowScope().put(DoStorageOperation.KEY, KEY);
+ request.setContent("{ \"value\": \"testValue\" }".getBytes());
+
+ final Event event = action.execute(rc);
+
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getStatus(), HttpServletResponse.SC_CREATED);
+
+ final StorageRecord<?> record = storageService.read(CONTEXT, KEY);
+ Assert.assertEquals(record.getVersion(), 1);
+ Assert.assertEquals(record.getValue(), VALUE);
+ }
+
+ /**
+ * Test successful update with a version.
+ *
+ * @throws IOException
+ * @throws ParseException
+ */
+ @Test
+ public void successUpdateWithVersion() throws IOException, ParseException {
+
+ storageService.create(CONTEXT, KEY, VALUE, null);
+
+ request.setMethod("POST");
+ rc.getFlowScope().put(DoStorageOperation.CONTEXT, CONTEXT);
+ rc.getFlowScope().put(DoStorageOperation.KEY, KEY);
+ request.setContent("{ \"value\": \"changed\", \"version\": 1 }".getBytes());
+
+ final Event event = action.execute(rc);
+
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getStatus(), HttpServletResponse.SC_OK);
+
+ final StorageRecord<?> record = storageService.read(CONTEXT, KEY);
+ Assert.assertEquals(record.getVersion(), 2);
+ Assert.assertEquals(record.getValue(), "changed");
+ }
+
+ /**
+ * Test failed update with a version.
+ *
+ * @throws IOException
+ * @throws ParseException
+ */
+ @Test
+ public void failedUpdateWithVersion() throws IOException, ParseException {
+
+ storageService.create(CONTEXT, KEY, VALUE, null);
+
+ request.setMethod("POST");
+ rc.getFlowScope().put(DoStorageOperation.CONTEXT, CONTEXT);
+ rc.getFlowScope().put(DoStorageOperation.KEY, KEY);
+ request.setContent("{ \"value\": \"changed\", \"version\": 2 }".getBytes());
+
+ final Event event = action.execute(rc);
+
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getStatus(), HttpServletResponse.SC_CONFLICT);
+
+ final StorageRecord<?> record = storageService.read(CONTEXT, KEY);
+ Assert.assertEquals(record.getVersion(), 1);
+ Assert.assertEquals(record.getValue(), VALUE);
+ }
+
+ /**
+ * Test failed update with a version when record missing.
+ *
+ * @throws IOException
+ * @throws ParseException
+ */
+ @Test
+ public void missingUpdateWithVersion() throws IOException, ParseException {
+
+ request.setMethod("POST");
+ rc.getFlowScope().put(DoStorageOperation.CONTEXT, CONTEXT);
+ rc.getFlowScope().put(DoStorageOperation.KEY, KEY);
+ request.setContent("{ \"value\": \"changed\", \"version\": 2 }".getBytes());
+
+ final Event event = action.execute(rc);
+
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getStatus(), HttpServletResponse.SC_NOT_FOUND);
+ Assert.assertNull(storageService.read(CONTEXT, KEY));
+ }
+
+}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list