[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