[java-plugin-shibd] branch main updated: Implementation of remote StorageService flow.

Scott Cantor cantor.2 at osu.edu
Mon Jul 1 20:00:30 UTC 2024


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch main
in repository java-plugin-shibd.

View the commit online:
http://git.shibboleth.net/view/?p=java-plugin-shibd.git;a=commit;h=0b25c4e7d9ba6e9e3f74d731f68db2ac78f7ce32

The following commit(s) were added to refs/heads/main by this push:
     new 0b25c4e  Implementation of remote StorageService flow.
0b25c4e is described below

commit 0b25c4e7d9ba6e9e3f74d731f68db2ac78f7ce32
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Mon Jul 1 16:00:26 2024 -0400

    Implementation of remote StorageService flow.
---
 .../idp/flows/sp/storage/storage-beans.xml         |  18 +
 .../idp/flows/sp/storage/storage-flow.xml          |  26 ++
 .../shibboleth/idp/module/conf/sp/sp.properties    |   3 +
 sp-server-impl/pom.xml                             |  11 +
 .../profile/impl/AbstractAgentRequestAction.java   |   2 +-
 .../sp/profile/impl/DoStorageOperation.java        | 312 ++++++++++++++
 .../sp/profile/impl/EncodeAgentResponse.java       |   6 +-
 .../sp/profile/impl/DoStorageOperationTest.java    | 447 +++++++++++++++++++++
 8 files changed, 821 insertions(+), 4 deletions(-)

diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/storage/storage-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/storage/storage-beans.xml
new file mode 100644
index 0000000..7dc672e
--- /dev/null
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/storage/storage-beans.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/parse-request-map" />
+    <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{idp.service.logging.sp:SPAgent}" />
+
+    <bean id="DoStorageOperation"
+        class="net.shibboleth.sp.config.impl.DoStorageOperation" scope="prototype"
+        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+        p:storageService-ref="#{'%{sp.storageService:shibboleth.StorageService}'.trim()}" />
+
+</beans>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/storage/storage-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/storage/storage-flow.xml
new file mode 100644
index 0000000..fed19fd
--- /dev/null
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/storage/storage-flow.xml
@@ -0,0 +1,26 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" 
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    parent="sp/abstract">
+
+    <action-state id="InitializeProfileRequestContext">
+        <evaluate expression="InitializeProfileRequestContext" />
+        <evaluate expression="'proceed'" />
+        
+        <!-- Branch to parent flow for authentication. -->
+        <transition on="proceed" to="AuthenticationSetup" />
+    </action-state>
+
+    <!-- Resume flow operation after set up by parent. -->
+    <action-state id="DoOperation">
+        <evaluate expression="DoStorageOperation" />
+        <evaluate expression="'proceed'" />
+        
+        <!-- Branch to parent flow to send response. -->
+        <transition on="proceed" to="EncodeAgentResponse" />
+    </action-state>
+    
+    <!-- The file really exists in this directory, but it's referenced from extending flow-directories -->
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/storage/storage-beans.xml" />
+
+</flow>
diff --git a/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties b/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
index 754db63..74c0132 100644
--- a/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
+++ b/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
@@ -26,3 +26,6 @@ sp.encryption.cert = %{idp.home}/credentials/sp/sp-encryption.crt
 # Set false to globally disable cookie-based authentication by agents
 #sp.agent.authn.cached = true
 #sp.agent.authn.cacheDuration = PT1H
+
+# Set to StorageService to use for remoted storage data if in use.
+#sp.storageService = shibboleth.StorageService
diff --git a/sp-server-impl/pom.xml b/sp-server-impl/pom.xml
index c094248..c7dfe3b 100644
--- a/sp-server-impl/pom.xml
+++ b/sp-server-impl/pom.xml
@@ -91,6 +91,11 @@
             <artifactId>opensaml-saml-api</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-storage-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-xmlsec-api</artifactId>
@@ -151,6 +156,12 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-storage-impl</artifactId>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>${spring.groupId}</groupId>
             <artifactId>spring-test</artifactId>
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/AbstractAgentRequestAction.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/AbstractAgentRequestAction.java
index a60b97e..86be721 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/AbstractAgentRequestAction.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/AbstractAgentRequestAction.java
@@ -115,7 +115,7 @@ public abstract class AbstractAgentRequestAction extends AbstractProfileAction {
         if (ctx != null) {
             final Agent agent = ctx.getAgent();
             if (agent != null) {
-                s.append("Agent: ").append(agent.getId()).append(": ");
+                s.append(" Agent: ").append(agent.getId()).append(":");
             }
         }
         
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/DoStorageOperation.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/DoStorageOperation.java
new file mode 100644
index 0000000..38794dd
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/DoStorageOperation.java
@@ -0,0 +1,312 @@
+/*
+ * Licensed 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.sp.profile.impl;
+
+import java.io.IOException;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+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 net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.Agent;
+import net.shibboleth.sp.ddf.DDF;
+import jakarta.servlet.http.HttpServletRequest;
+
+/**
+ * Action that implements a remote API for SP agents to access or alter {@link StorageService} records.
+ * 
+ * <p>All contexts are prefixed with {@link Agent#getId} to prevent conflicts across agents.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link EventIds#IO_ERROR}
+ * @event {@link #DUPLICATE_RECORD}
+ * @event {@link #RECORD_NOT_FOUND}
+ * @event {@link #VERSION_MISMATCH}
+ */
+public class DoStorageOperation extends AbstractAgentAction {
+
+    /** Custom event for duplicate record on creation. */
+    @Nonnull @NotEmpty public static final String RECORD_NOT_FOUND = "RecordNotFound";
+
+    /** Custom event for duplicate record on creation. */
+    @Nonnull @NotEmpty public static final String DUPLICATE_RECORD = "DuplicateRecord";
+
+    /** Custom event for version mismatch on conditional update. */
+    @Nonnull @NotEmpty public static final String VERSION_MISMATCH = "VersionMismatch";
+
+    /** Input member for storage context. */
+    @Nonnull @NotEmpty public static final String CONTEXT = "context";
+
+    /** Input member for storage key. */
+    @Nonnull @NotEmpty public static final String KEY = "key";
+
+    /** Member for storage value. */
+    @Nonnull @NotEmpty public static final String VALUE = "value";
+
+    /** Member for storage expiration. */
+    @Nonnull @NotEmpty public static final String EXP = "exp";
+
+    /** Member for storage version. */
+    @Nonnull @NotEmpty public static final String VERSION = "version";
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DoStorageOperation.class);
+    
+    /** {@link StorageService} to operate on. */
+    @NonnullAfterInit private StorageService storageService;
+
+    /** Input message. */
+    @NonnullBeforeExec private DDF input;
+    
+    /**
+     * 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) {
+        checkSetterPreconditions();
+        
+        storageService = storage;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (storageService == null) {
+            throw new ComponentInitializationException("StorageService cannot be null");
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        } else if (getHttpServletRequest() == null) {
+            log.warn("{} No HttpServletRequest available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        input = ensureAgentRequestContext().getInput();
+        if (input == null || !input.isstruct()) {
+            log.warn("{} Invalid or missing input message", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        try {
+            final HttpServletRequest request = ensureHttpServletRequest();
+            
+            if ("GET".equals(request.getMethod())) {
+                doRead(profileRequestContext);
+            } else if ("PUT".equals(request.getMethod())) {
+                doCreate(profileRequestContext);
+            } else if ("POST".equals(request.getMethod())) {
+                doUpdate(profileRequestContext);
+            } else if ("DELETE".equals(request.getMethod())) {
+                doDelete(profileRequestContext);
+            } else {
+                log.warn("{} Invalid method: {}", getLogPrefix(), request.getMethod());
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            }
+        } catch (final IOException e) {
+            log.error("{} I/O error responding to request", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+        }
+    }
+    
+    /**
+     * Perform read operation.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @throws IOException if an error is raised
+     */
+    private void doRead(@Nonnull final ProfileRequestContext profileRequestContext) throws IOException {
+        
+        String context = input.getmember(CONTEXT).string();
+        final String key = input.getmember(KEY).string();
+        if (context == null || key == null) {
+            log.warn("{} Context and key are required members for read operation", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return;
+        }
+        
+        // Decorate context with Agent ID for uniqueness.
+        context = ensureAgent().getId() + '!' + context;
+        
+        final StorageRecord<?> record = storageService.read(context, key);
+        if (record != null) {
+            final DDF output = new DDF().structure();
+            output.addmember(VALUE).string(record.getValue());
+            output.addmember(VERSION).longinteger(record.getVersion());
+            final Long exp = record.getExpiration();
+            if (exp != null) {
+                output.addmember(EXP).longinteger(exp);
+            }
+            ensureAgentRequestContext().setOutput(output);
+        } else {
+            log.trace("{} No record found with context ({}), key ({})", getLogPrefix(), context, key);
+            ActionSupport.buildEvent(profileRequestContext, RECORD_NOT_FOUND);
+        }
+    }
+    
+    
+    /**
+     * Perform create operation.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @throws IOException if an error is raised
+     */
+    private void doCreate(@Nonnull final ProfileRequestContext profileRequestContext) throws IOException {
+        
+        String context = input.getmember(CONTEXT).string();
+        final String key = input.getmember(KEY).string();
+        final String value = input.getmember(VALUE).string();
+        
+        if (context == null || key == null || value == null) {
+            log.warn("{} Context, key, and value are required members for create operation", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return;
+        }
+
+        // Decorate context with Agent ID for uniqueness.
+        context = ensureAgent().getId() + '!' + context;
+        
+        final Long exp = input.getmember(EXP).longinteger();
+
+        if (storageService.create(context, key, value, exp)) {
+            log.trace("{} Created record with context ({}), key ({}), expiration ({})", getLogPrefix(), context, key,
+                    exp);
+        } else {
+            log.warn("{} Record exists with context ({}), key ({})", getLogPrefix(), context, key);
+            ActionSupport.buildEvent(profileRequestContext, DUPLICATE_RECORD);
+        }
+    }
+
+    /**
+     * Perform update operation.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @throws IOException if an error is raised
+     */
+    private void doUpdate(@Nonnull final ProfileRequestContext profileRequestContext) throws IOException {
+
+        String context = input.getmember(CONTEXT).string();
+        final String key = input.getmember(KEY).string();
+        final String value = input.getmember(VALUE).string();
+        
+        if (context == null || key == null || value == null) {
+            log.warn("{} Context, key, and value are required members for update operation", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return;
+        }
+
+        // Decorate context with Agent ID for uniqueness.
+        context = ensureAgent().getId() + '!' + context;
+        
+        final Long exp = input.getmember(EXP).longinteger();
+        Long version = input.getmember(VERSION).longinteger();
+        
+        if (version != null) {
+            try {
+                version = storageService.updateWithVersion(version, context, key, value, exp);
+                if (version != null) {
+                    log.trace("{} Updated record with context ({}), key ({}), version ({})", getLogPrefix(), context,
+                            key, version);
+                    final DDF output = new DDF().structure();
+                    output.addmember(VERSION).longinteger(version);
+                    ensureAgentRequestContext().setOutput(output);
+                } else {
+                    log.info("{} No record to update with context ({}), key ({})", getLogPrefix(), context, key);
+                    ActionSupport.buildEvent(profileRequestContext, RECORD_NOT_FOUND);
+                }
+            } catch (final VersionMismatchException e) {
+                log.info("{} Existing record with context ({}), key ({}). but version ({})", getLogPrefix(), context,
+                        key, version);
+                ActionSupport.buildEvent(profileRequestContext, VERSION_MISMATCH);
+            }
+        } else {
+            if (storageService.update(context, key, value, exp)) {
+                log.trace("{} Updated record with context ({}), key ({}), expiration ({})", getLogPrefix(), context,
+                        key, exp);
+            } else if (storageService.create(context, key, value, exp)) {
+                log.trace("{} Created record with context ({}), key ({}), expiration ({})", getLogPrefix(), context,
+                        key, exp);
+            } else {
+                log.warn("{} Failed to update or create record with context ({}), key ({})", getLogPrefix(), context,
+                        key);
+                ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+            }
+        }
+    }
+    
+    /**
+     * Perform delete operation.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @throws IOException if an error is raised
+     */
+    private void doDelete(@Nonnull final ProfileRequestContext profileRequestContext) throws IOException {
+
+        String context = input.getmember(CONTEXT).string();
+        final String key = input.getmember(KEY).string();
+        if (context == null || key == null) {
+            log.warn("{} Context and key are required members for delete operation", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return;
+        }
+        
+        // Decorate context with Agent ID for uniqueness.
+        context = ensureAgent().getId() + '!' + context;
+        
+        if (storageService.delete(context, key)) {
+            log.trace("{} Deleted record with context ({}), key ({})", getLogPrefix(), context, key);
+        } else {
+            log.trace("{} No record to delete with context ({}), key ({})", getLogPrefix(), context, key);
+            ActionSupport.buildEvent(profileRequestContext, RECORD_NOT_FOUND);
+        }
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/EncodeAgentResponse.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/EncodeAgentResponse.java
index 86aaac5..b214f32 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/EncodeAgentResponse.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/EncodeAgentResponse.java
@@ -59,9 +59,9 @@ public class EncodeAgentResponse extends AbstractAgentRequestAction {
         final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
         outputMessage = agentRequestContext.getOutput();
         if (outputMessage == null) {
-            log.error("{} No output message found", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
-            return false;
+            log.debug("{} No output message found, creating empty one", getLogPrefix());
+            agentRequestContext.setOutput(new DDF());
+            outputMessage = agentRequestContext.getOutput();
         }
         
         return true;
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/DoStorageOperationTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/DoStorageOperationTest.java
new file mode 100644
index 0000000..fc0f033
--- /dev/null
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/DoStorageOperationTest.java
@@ -0,0 +1,447 @@
+/*
+ * Licensed 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.sp.profile.impl;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.impl.MemoryStorageService;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.testing.ConstantSupplier;
+import net.shibboleth.sp.ddf.DDF;
+
+/**
+ * Unit test for {@link DoStorageOperation} action.
+ */
+public class DoStorageOperationTest extends BaseAgentRequestTest {
+
+    /** Test context. */
+    @Nonnull @NotEmpty private final static String CONTEXT = "testContext";
+
+    /** Test context. */
+    @Nonnull @NotEmpty private final static String AGENT_CONTEXT = "test!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 DoStorageOperation action;
+
+    private MockHttpServletRequest request;
+    
+    /**
+     * Set up test.
+     * 
+     * @throws ComponentInitializationException
+     */
+    @BeforeMethod
+    public void setUp() throws ComponentInitializationException {
+        super.beforeMethod();
+        
+        request = (MockHttpServletRequest) src.getExternalContext().getNativeRequest();
+
+        storageService = new MemoryStorageService();
+        storageService.setId("test");
+        final Duration zeroDuration = Duration.ZERO;
+        assert zeroDuration!=null;
+        storageService.setCleanupInterval(zeroDuration);
+        storageService.initialize();
+        
+        action = new DoStorageOperation();
+        assert request != null;
+        action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+        action.setStorageService(storageService);
+        action.initialize();
+    }
+    
+    /**
+     * Tear down test.
+     */
+    @AfterMethod
+    public void tearDown() {
+        action.destroy();
+        storageService.destroy();
+    }
+
+    /**
+     * Test invalidMethod
+     */
+    @Test
+    public void invalidMethod() {
+        
+        arc.setInput(new DDF().structure());
+        
+        request.setMethod("FOO");
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);
+    }
+
+
+    /**
+     * Test read with no inputs.
+     */
+    @Test
+    public void readNoParams() {
+        arc.setInput(new DDF().structure());
+
+        request.setMethod("GET");
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);
+    }
+
+    /**
+     * Test read with no record found.
+     */
+    @Test
+    public void missingRead() {
+        
+        request.setMethod("GET");
+        
+        final DDF input = new DDF().structure();
+        input.addmember(DoStorageOperation.CONTEXT).string(CONTEXT);
+        input.addmember(DoStorageOperation.KEY).string(KEY);
+        arc.setInput(input);
+        
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertEvent(event, DoStorageOperation.RECORD_NOT_FOUND);
+        
+        final DDF output = arc.getOutput();
+        Assert.assertNull(output);
+    }
+
+    /**
+     * Test successful get.
+     * 
+     * @throws IOException 
+     * @throws ParseException 
+     */
+    @Test
+    public void successRead() throws IOException, ParseException {
+        
+        final long exp = Instant.now().plus(Duration.ofMinutes(15)).toEpochMilli();
+        
+        storageService.create(AGENT_CONTEXT, KEY, VALUE, exp);
+        
+        request.setMethod("GET");
+        
+        final DDF input = new DDF().structure();
+        input.addmember(DoStorageOperation.CONTEXT).string(CONTEXT);
+        input.addmember(DoStorageOperation.KEY).string(KEY);
+        arc.setInput(input);
+        
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertProceedEvent(event);
+
+        final DDF output = arc.getOutput();
+        assert output != null;
+        
+        Assert.assertEquals(output.getmember(DoStorageOperation.VALUE).string(), VALUE);
+        Assert.assertEquals(output.getmember(DoStorageOperation.VERSION).longinteger(), 1);
+        Assert.assertEquals(output.getmember(DoStorageOperation.EXP).longinteger(), exp);
+    }
+    
+    /**
+     * Test missing delete.
+     * 
+     * @throws IOException 
+     * @throws ParseException 
+     */
+    @Test
+    public void missingDelete() throws IOException, ParseException {
+        
+        storageService.create(AGENT_CONTEXT, KEY, VALUE, null);
+        
+        request.setMethod("DELETE");
+
+        final DDF input = new DDF().structure();
+        input.addmember(DoStorageOperation.CONTEXT).string(CONTEXT);
+        input.addmember(DoStorageOperation.KEY).string(KEY + "2");
+        arc.setInput(input);
+        
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertEvent(event, DoStorageOperation.RECORD_NOT_FOUND);
+
+        final DDF output = arc.getOutput();
+        Assert.assertNull(output);
+
+        Assert.assertNotNull(storageService.read(AGENT_CONTEXT, KEY));
+    }
+    
+    /**
+     * Test successful delete.
+     * 
+     * @throws IOException 
+     * @throws ParseException 
+     */
+    @Test
+    public void successDelete() throws IOException, ParseException {
+        
+        storageService.create(AGENT_CONTEXT, KEY, VALUE, null);
+        
+        request.setMethod("DELETE");
+
+        final DDF input = new DDF().structure();
+        input.addmember(DoStorageOperation.CONTEXT).string(CONTEXT);
+        input.addmember(DoStorageOperation.KEY).string(KEY);
+        arc.setInput(input);
+        
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF output = arc.getOutput();
+        Assert.assertNull(output);
+        
+        Assert.assertNull(storageService.read(AGENT_CONTEXT, KEY));
+    }
+
+    /**
+     * Test successful create.
+     * 
+     * @throws IOException 
+     * @throws ParseException 
+     */
+    @Test
+    public void successCreate() throws IOException, ParseException {
+        
+        request.setMethod("PUT");
+        
+        final DDF input = new DDF().structure();
+        input.addmember(DoStorageOperation.CONTEXT).string(CONTEXT);
+        input.addmember(DoStorageOperation.KEY).string(KEY);
+        input.addmember(DoStorageOperation.VALUE).string(VALUE);
+        arc.setInput(input);
+        
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF output = arc.getOutput();
+        Assert.assertNull(output);
+        
+        final StorageRecord<?> record = storageService.read(AGENT_CONTEXT, KEY);
+        assert record != null;
+        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(AGENT_CONTEXT, KEY, VALUE, null);
+        
+        request.setMethod("PUT");
+        
+        final DDF input = new DDF().structure();
+        input.addmember(DoStorageOperation.CONTEXT).string(CONTEXT);
+        input.addmember(DoStorageOperation.KEY).string(KEY);
+        input.addmember(DoStorageOperation.VALUE).string(VALUE);
+        arc.setInput(input);
+        
+        final Event event = action.execute(src);
+
+        ActionTestingSupport.assertEvent(event, DoStorageOperation.DUPLICATE_RECORD);
+        
+        final DDF output = arc.getOutput();
+        Assert.assertNull(output);
+    }
+
+    /**
+     * Test successful update.
+     * 
+     * @throws IOException 
+     * @throws ParseException 
+     */
+    @Test
+    public void successUpdate() throws IOException, ParseException {
+        
+        storageService.create(AGENT_CONTEXT, KEY, VALUE, null);
+        
+        request.setMethod("POST");
+
+        final DDF input = new DDF().structure();
+        input.addmember(DoStorageOperation.CONTEXT).string(CONTEXT);
+        input.addmember(DoStorageOperation.KEY).string(KEY);
+        input.addmember(DoStorageOperation.VALUE).string("changed");
+        arc.setInput(input);
+        
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF output = arc.getOutput();
+        Assert.assertNull(output);
+        
+        final StorageRecord<?> record = storageService.read(AGENT_CONTEXT, KEY);
+        assert record != null;
+        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");
+        
+        final DDF input = new DDF().structure();
+        input.addmember(DoStorageOperation.CONTEXT).string(CONTEXT);
+        input.addmember(DoStorageOperation.KEY).string(KEY);
+        input.addmember(DoStorageOperation.VALUE).string(VALUE);
+        arc.setInput(input);
+        
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF output = arc.getOutput();
+        Assert.assertNull(output);
+        
+        final StorageRecord<?> record = storageService.read(AGENT_CONTEXT, KEY);
+        assert record != null;
+        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(AGENT_CONTEXT, KEY, VALUE, null);
+        
+        request.setMethod("POST");
+        
+        final DDF input = new DDF().structure();
+        input.addmember(DoStorageOperation.CONTEXT).string(CONTEXT);
+        input.addmember(DoStorageOperation.KEY).string(KEY);
+        input.addmember(DoStorageOperation.VALUE).string("changed");
+        input.addmember(DoStorageOperation.VERSION).longinteger(1);
+        arc.setInput(input);
+        
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF output = arc.getOutput();
+        assert output != null;
+        Assert.assertEquals(output.getmember(DoStorageOperation.VERSION).longinteger(), 2);
+        
+        final StorageRecord<?> record = storageService.read(AGENT_CONTEXT, KEY);
+        assert record != null;
+        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(AGENT_CONTEXT, KEY, VALUE, null);
+        
+        request.setMethod("POST");
+
+        final DDF input = new DDF().structure();
+        input.addmember(DoStorageOperation.CONTEXT).string(CONTEXT);
+        input.addmember(DoStorageOperation.KEY).string(KEY);
+        input.addmember(DoStorageOperation.VALUE).string("changed");
+        input.addmember(DoStorageOperation.VERSION).longinteger(2);
+        arc.setInput(input);
+        
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertEvent(event, DoStorageOperation.VERSION_MISMATCH);
+        
+        final DDF output = arc.getOutput();
+        Assert.assertNull(output);
+        
+        final StorageRecord<?> record = storageService.read(AGENT_CONTEXT, KEY);
+        assert record != null;
+        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");
+
+        final DDF input = new DDF().structure();
+        input.addmember(DoStorageOperation.CONTEXT).string(CONTEXT);
+        input.addmember(DoStorageOperation.KEY).string(KEY);
+        input.addmember(DoStorageOperation.VALUE).string("changed");
+        input.addmember(DoStorageOperation.VERSION).longinteger(2);
+        arc.setInput(input);
+        
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertEvent(event, DoStorageOperation.RECORD_NOT_FOUND);
+
+        final DDF output = arc.getOutput();
+        Assert.assertNull(output);
+
+        Assert.assertNull(storageService.read(AGENT_CONTEXT, KEY));
+    }
+
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list