squash.booking.lambdas.core.OptimisticPersisterTest.java Source code

Java tutorial

Introduction

Here is the source code for squash.booking.lambdas.core.OptimisticPersisterTest.java

Source

/**
 * Copyright 2016 Robin Steel
 *
 * 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 squash.booking.lambdas.core;

import static org.junit.Assert.assertTrue;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.Sequence;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import com.amazonaws.services.simpledb.AmazonSimpleDB;
import com.amazonaws.services.simpledb.model.Attribute;
import com.amazonaws.services.simpledb.model.DeleteAttributesRequest;
import com.amazonaws.services.simpledb.model.GetAttributesRequest;
import com.amazonaws.services.simpledb.model.GetAttributesResult;
import com.amazonaws.services.simpledb.model.Item;
import com.amazonaws.services.simpledb.model.PutAttributesRequest;
import com.amazonaws.services.simpledb.model.ReplaceableAttribute;
import com.amazonaws.services.simpledb.model.SelectRequest;
import com.amazonaws.services.simpledb.model.SelectResult;
import com.amazonaws.services.simpledb.model.UpdateCondition;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

/**
 * Tests the {@link OptimisticPersister}.
 * 
 * @author robinsteel19@outlook.com (Robin Steel)
 */
public class OptimisticPersisterTest {
    squash.booking.lambdas.core.OptimisticPersisterTest.TestOptimisticPersister optimisticPersister;

    String testItemName = "itemName";
    String testSimpleDBDomainName = "testSimpleDbDomainName";

    // Some database attributes for testing
    Set<Attribute> allAttributes;
    Set<Attribute> nonVersionAttributes;
    Set<Attribute> activeNonVersionAttributes;
    int testVersionNumber = 42; // Arbitrary
    String versionAttributeName = "VersionNumber";

    // Mocks
    Mockery mockery = new Mockery();
    LambdaLogger mockLogger;
    AmazonSimpleDB mockSimpleDBClient;

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Before
    public void beforeTest() {

        optimisticPersister = new TestOptimisticPersister();

        // Set up the mocks
        mockLogger = mockery.mock(LambdaLogger.class);
        mockery.checking(new Expectations() {
            {
                ignoring(mockLogger);
            }
        });
        mockSimpleDBClient = mockery.mock(AmazonSimpleDB.class);

        optimisticPersister.setSimpleDBClient(mockSimpleDBClient);

        // Set up some typical test attributes
        allAttributes = new HashSet<>();
        nonVersionAttributes = new HashSet<>();
        activeNonVersionAttributes = new HashSet<>();
        Attribute versionAttribute = new Attribute();
        versionAttribute.setName(versionAttributeName);
        versionAttribute.setValue(Integer.toString(testVersionNumber));
        Attribute activeAttribute = new Attribute();
        activeAttribute.setName("ActiveAttribute");
        activeAttribute.setValue("Active");
        Attribute inactiveAttribute = new Attribute();
        inactiveAttribute.setName("InactiveAttribute");
        inactiveAttribute.setValue("Inactive");
        allAttributes.add(versionAttribute);
        allAttributes.add(activeAttribute);
        allAttributes.add(inactiveAttribute);
        nonVersionAttributes.add(activeAttribute);
        nonVersionAttributes.add(inactiveAttribute);
        activeNonVersionAttributes.add(activeAttribute);
    }

    @After
    public void afterTest() {
        mockery.assertIsSatisfied();
    }

    private void initialiseOptimisticPersister() throws Exception {
        optimisticPersister.initialise(42, mockLogger);
    }

    // Define a test optimistic persister with some overrides to facilitate
    // testing
    public class TestOptimisticPersister extends OptimisticPersister {
        private AmazonSimpleDB simpleDBClient;

        public void setSimpleDBClient(AmazonSimpleDB simpleDBClient) {
            this.simpleDBClient = simpleDBClient;
        }

        @Override
        public AmazonSimpleDB getSimpleDBClient() {
            return simpleDBClient;
        }

        @Override
        protected String getEnvironmentVariable(String variableName) throws IOException {
            if (variableName.equals("SimpleDBDomainName")) {
                return testSimpleDBDomainName;
            }
            if (variableName.equals("AWS_REGION")) {
                return "eu-west-1";
            }
            return null;
        }
    }

    @Test
    public void testInitialiseThrowsWhenOptimisticPersisterAlreadyInitialised() throws Exception {

        // ARRANGE
        thrown.expect(Exception.class);
        thrown.expectMessage("The optimistic persister has already been initialised");

        int maxNumberOfAttributes = 32; // Arbitrary
        optimisticPersister.initialise(maxNumberOfAttributes, mockLogger);

        // ACT
        // A second initialise call should throw
        optimisticPersister.initialise(maxNumberOfAttributes, mockLogger);
    }

    @Test
    public void testGetThrowsWhenOptimisticPersisterUninitialised() throws Exception {
        // ARRANGE
        thrown.expect(Exception.class);
        thrown.expectMessage("The optimistic persister has not been initialised");

        // ACT
        // Do not initialise the optimistic persister first - so get should throw
        optimisticPersister.get(testItemName);
    }

    @Test
    public void testGetCorrectlyCallsSimpleDB() throws Exception {

        // ARRANGE
        initialiseOptimisticPersister();
        GetAttributesRequest simpleDBRequest = new GetAttributesRequest(testSimpleDBDomainName, testItemName);
        simpleDBRequest.setConsistentRead(true);

        GetAttributesResult getAttributesResult = new GetAttributesResult();
        getAttributesResult.setAttributes(allAttributes);
        mockery.checking(new Expectations() {
            {
                oneOf(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                will(returnValue(getAttributesResult));
            }
        });

        // ACT
        optimisticPersister.get(testItemName);
    }

    @Test
    public void testGetReturnsTheCorrectVersionNumberAndAttributes() throws Exception {
        // Get should not return the version-number attribute (but should return the
        // version number as part of its result pair) and it should not return
        // inactive attributes.

        // ARRANGE
        initialiseOptimisticPersister();
        GetAttributesRequest simpleDBRequest = new GetAttributesRequest(testSimpleDBDomainName, testItemName);
        simpleDBRequest.setConsistentRead(true);

        // First assert that our allAttribute set has both version and inactive
        // attributes (which should get filtered out).
        assertTrue("AllAttribute list should contain an inactive attribute", allAttributes.stream()
                .filter(attribute -> attribute.getValue().startsWith("Inactive")).count() > 0);
        assertTrue("AllAttribute list should contain a version number attribute", allAttributes.stream()
                .filter(attribute -> attribute.getName().equals(versionAttributeName)).count() > 0);

        GetAttributesResult getAttributesResult = new GetAttributesResult();
        getAttributesResult.setAttributes(allAttributes);
        mockery.checking(new Expectations() {
            {
                allowing(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                will(returnValue(getAttributesResult));
            }
        });

        // ACT
        ImmutablePair<Optional<Integer>, Set<Attribute>> result = optimisticPersister.get(testItemName);

        // ASSERT
        assertTrue("OptimisticPersister should return the correct attributes. Actual: " + result.right
                + ", Expected: " + activeNonVersionAttributes, result.right.equals(activeNonVersionAttributes));
        assertTrue("OptimisticPersister should return a version number", result.left.isPresent());
        assertTrue("OptimisticPersister should return the correct version number",
                result.left.get().equals(testVersionNumber));
    }

    @Test
    public void testGetReturnsAnEmptyVersionNumberAndNoAttributesWhenThereAreNoAttributesInTheDatabase()
            throws Exception {

        // ARRANGE
        initialiseOptimisticPersister();
        GetAttributesRequest simpleDBRequest = new GetAttributesRequest(testSimpleDBDomainName, testItemName);
        simpleDBRequest.setConsistentRead(true);

        // First assert that our allAttribute set has both version and inactive
        // attributes (which should get filtered out).
        assertTrue("AllAttribute list should contain an inactive attribute", allAttributes.stream()
                .filter(attribute -> attribute.getValue().startsWith("Inactive")).count() > 0);
        assertTrue("AllAttribute list should contain a version number attribute", allAttributes.stream()
                .filter(attribute -> attribute.getName().equals(versionAttributeName)).count() > 0);

        // Mimic database with no attributes
        GetAttributesResult getAttributesResult = new GetAttributesResult();
        mockery.checking(new Expectations() {
            {
                allowing(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                will(returnValue(getAttributesResult));
            }
        });

        // ACT
        ImmutablePair<Optional<Integer>, Set<Attribute>> result = optimisticPersister.get(testItemName);

        // ASSERT
        assertTrue("OptimisticPersister should return no attributes", result.right.size() == 0);
        assertTrue("OptimisticPersister should return an empty version number", !result.left.isPresent());
    }

    @Test
    public void testGetAllItemsThrowsWhenOptimisticPersisterUninitialised() throws Exception {
        // ARRANGE
        thrown.expect(Exception.class);
        thrown.expectMessage("The optimistic persister has not been initialised");

        // ACT
        // Do not initialise the optimistic persister first - so getAllItems should
        // throw
        optimisticPersister.getAllItems();
    }

    @Test
    public void testGetAllItemsCorrectlyCallsSimpleDB() throws Exception {

        // ARRANGE
        initialiseOptimisticPersister();

        SelectRequest selectRequest = new SelectRequest();
        selectRequest.setConsistentRead(true);
        selectRequest.setSelectExpression("select * from `" + testSimpleDBDomainName + "`");

        // Configure select result with an item to be returned:
        SelectResult selectResult = new SelectResult();
        Set<Item> items = new HashSet<>();
        Item item = new Item();
        String itemDate = "2016-07-23";
        item.setName(itemDate);
        item.setAttributes(allAttributes);
        items.add(item);
        selectResult.setItems(items);
        mockery.checking(new Expectations() {
            {
                oneOf(mockSimpleDBClient).select(with(equal(selectRequest)));
                will(returnValue(selectResult));
            }
        });

        List<ImmutablePair<String, List<Attribute>>> expectedItems = new ArrayList<>();
        ImmutablePair<String, List<Attribute>> pair = new ImmutablePair<>(itemDate,
                new ArrayList<>(activeNonVersionAttributes));
        expectedItems.add(pair);

        // ACT
        List<ImmutablePair<String, List<Attribute>>> actualItems = optimisticPersister.getAllItems();

        // ASSERT
        assertTrue("OptimisticPersister should return the correct items. Actual: " + actualItems + ", Expected: "
                + expectedItems, actualItems.equals(expectedItems));
    }

    @Test
    public void testPutThrowsWhenOptimisticPersisterUninitialised() throws Exception {
        // ARRANGE
        thrown.expect(Exception.class);
        thrown.expectMessage("The optimistic persister has not been initialised");

        // ACT
        // Do not initialise the optimistic persister first - so put should throw
        ReplaceableAttribute testAttribute = new ReplaceableAttribute();
        testAttribute.setName("Name");
        testAttribute.setValue("Value");
        optimisticPersister.put(testItemName, Optional.of(42), testAttribute);
    }

    @Test
    public void testPutThrowsWhenMaximumNumberOfAttributesIsAlreadyPresent() throws Exception {
        // ARRANGE
        thrown.expect(Exception.class);
        thrown.expectMessage("Database put failed");

        // Initialiser persister with a max number of attributes equal to the
        // current number (1 as there is only one active attribute) - so new put's
        // will be rejected.
        optimisticPersister.initialise(1, mockLogger);

        // Configure attributes for database to return - the get is used for logging
        // only, so does not really matter.
        GetAttributesRequest simpleDBRequest = new GetAttributesRequest(testSimpleDBDomainName, testItemName);
        simpleDBRequest.setConsistentRead(true);
        GetAttributesResult getAttributesResult = new GetAttributesResult();
        getAttributesResult.setAttributes(allAttributes);
        mockery.checking(new Expectations() {
            {
                allowing(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                will(returnValue(getAttributesResult));
            }
        });

        // ACT
        ReplaceableAttribute testAttribute = new ReplaceableAttribute();
        testAttribute.setName("Name");
        testAttribute.setValue("Value");
        // This should throw since we already have the max number of attributes.
        optimisticPersister.put(testItemName, Optional.of(42), testAttribute);
    }

    @Test
    public void testPutDoesNotThrowWhenMaximumNumberOfAttributesIsAlreadyPresentIfPutIsToInactivate()
            throws Exception {
        // Our 2-stage deletion process involves an initial put to 'inactivate' the
        // attribute being deleted. We must allow such put's to exceed the limit -
        // or else we could never delete attributes when we're over the limit.

        // ARRANGE

        // Initialiser persister with a max number of attributes equal to the
        // current number (1 as there is only one active attribute) - so new put's
        // will be rejected, unless they're inactivating puts.
        optimisticPersister.initialise(1, mockLogger);

        // Configure attributes for database to return - the get is used for logging
        // only, so does not really matter.
        GetAttributesRequest simpleDBRequest = new GetAttributesRequest(testSimpleDBDomainName, testItemName);
        simpleDBRequest.setConsistentRead(true);
        GetAttributesResult getAttributesResult = new GetAttributesResult();
        getAttributesResult.setAttributes(allAttributes);
        mockery.checking(new Expectations() {
            {
                allowing(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                will(returnValue(getAttributesResult));
            }
        });

        // Don't care about further calls to SimpleDB.
        mockery.checking(new Expectations() {
            {
                ignoring(mockSimpleDBClient);
            }
        });

        // ACT
        // Use an 'inactivating' put i.e. value prefixed with 'Inactive'
        ReplaceableAttribute testAttribute = new ReplaceableAttribute();
        testAttribute.setName("Name");
        testAttribute.setValue("InactiveValue");
        // This should not throw even though we already have the max number of
        // attributes.
        optimisticPersister.put(testItemName, Optional.of(42), testAttribute);
    }

    @Test
    public void testPutUsesCorrectVersionNumberWhenCallingTheDatabase_EmptyVersionNumber() throws Exception {
        // In order for the persister to verify that the relevant item has not been
        // changed by someone else since we did a get, we need to provide the
        // version number when we call put. If noone had written to the item when we
        // called get on it, then we will have an empty version number, otherwise it
        // will have some finite value. This test verifies that put uses the
        // version-number we supply when it calls on to the database, and tests for
        // the empty version number case.

        doTestPutUsesCorrectVersionWhenCallingTheDatabase(Optional.empty());
    }

    @Test
    public void testPutUsesCorrectVersionNumberWhenCallingTheDatabase_NonEmptyVersionNumber() throws Exception {
        // In order for the persister to verify that the relevant item has not been
        // changed by someone else since we did a get, we need to provide the
        // version number when we call put. If noone had written to the item when we
        // called get on it, then we will have an empty version number, otherwise it
        // will have some finite value. This test verifies that put uses the
        // version-number we supply when it calls on to the database, and tests for
        // the nonempty version number case.

        // N.B. 51 is arbitrary here - but not empty!
        doTestPutUsesCorrectVersionWhenCallingTheDatabase(Optional.of(51));
    }

    private void doTestPutUsesCorrectVersionWhenCallingTheDatabase(Optional<Integer> version) throws Exception {

        // ARRANGE
        initialiseOptimisticPersister();

        // Configure attributes for database to return - the get is used for logging
        // only, so does not really matter.
        GetAttributesRequest simpleDBRequest = new GetAttributesRequest(testSimpleDBDomainName, testItemName);
        simpleDBRequest.setConsistentRead(true);
        GetAttributesResult getAttributesResult = new GetAttributesResult();
        mockery.checking(new Expectations() {
            {
                allowing(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                will(returnValue(getAttributesResult));
            }
        });

        // Configure expectations for the put:
        UpdateCondition updateCondition = new UpdateCondition();
        updateCondition.setName(versionAttributeName);
        ReplaceableAttribute versionAttribute = new ReplaceableAttribute();
        versionAttribute.setName(versionAttributeName);
        versionAttribute.setReplace(true);
        if (!version.isPresent()) {
            // A version number attribute did not exist - so it still should not
            updateCondition.setExists(false);
            // Set initial value for our version number attribute
            versionAttribute.setValue("0");
        } else {
            // A version number attribute exists - so it should be unchanged
            updateCondition.setValue(Integer.toString(version.get()));
            // Bump up our version number attribute
            versionAttribute.setValue(Integer.toString(version.get() + 1));
        }

        List<ReplaceableAttribute> replaceableAttributes = new ArrayList<>();
        replaceableAttributes.add(versionAttribute);

        // Add the new attribute
        ReplaceableAttribute testAttribute = new ReplaceableAttribute();
        testAttribute.setName("Name");
        testAttribute.setValue("Value");
        replaceableAttributes.add(testAttribute);

        PutAttributesRequest simpleDBPutRequest = new PutAttributesRequest(testSimpleDBDomainName, testItemName,
                replaceableAttributes, updateCondition);
        mockery.checking(new Expectations() {
            {
                oneOf(mockSimpleDBClient).putAttributes(with(equal(simpleDBPutRequest)));
            }
        });

        // ACT
        optimisticPersister.put(testItemName, version, testAttribute);
    }

    @Test
    public void testPutHandlesConditionalCheckFailedExceptionCorrectly() throws Exception {
        // The persister should forward all simpleDB exceptions to us, but it should
        // convert ConditionalCheckFailed exceptions before forwarding, as clients
        // will likely want to handle that case differently. This tests conversion
        // of ConditionalCheckFailed exceptions.

        doTestPutHandlesExceptionsCorrectly(true);
    }

    @Test
    public void testPutHandlesOtherExceptionsCorrectly() throws Exception {
        // The persister should forward all simpleDB exceptions to us, but it should
        // convert ConditionalCheckFailed exceptions before forwarding, as clients
        // will likely want to handle that case differently. This tests forwarding
        // of exceptions other than the ConditionalCheckFailed exception.

        doTestPutHandlesExceptionsCorrectly(false);
    }

    private void doTestPutHandlesExceptionsCorrectly(Boolean isConditionalCheckFailedException) throws Exception {
        // The persister should forward all simpleDB exceptions to us, but it should
        // convert ConditionalCheckFailed exceptions before forwarding, as clients
        // will likely want to handle that case differently.

        // ARRANGE
        thrown.expect(Exception.class);
        thrown.expectMessage(isConditionalCheckFailedException ? "Database put failed" : "Boom!");

        initialiseOptimisticPersister();

        // Configure attributes for database to return - the get is used for logging
        // only, so does not really matter.
        GetAttributesRequest simpleDBRequest = new GetAttributesRequest(testSimpleDBDomainName, testItemName);
        simpleDBRequest.setConsistentRead(true);
        GetAttributesResult getAttributesResult = new GetAttributesResult();
        mockery.checking(new Expectations() {
            {
                allowing(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                will(returnValue(getAttributesResult));
            }
        });

        // Make the simpleDB call throw the correct exception
        AmazonServiceException exception = new AmazonServiceException("Boom!");
        exception.setErrorCode(
                isConditionalCheckFailedException ? "ConditionalCheckFailed" : "SomeOtherArbitraryCode");
        mockery.checking(new Expectations() {
            {
                oneOf(mockSimpleDBClient).putAttributes(with(anything()));
                will(throwException(exception));
            }
        });

        ReplaceableAttribute testAttribute = new ReplaceableAttribute();
        testAttribute.setName("Name");
        testAttribute.setValue("Value");

        // ACT
        optimisticPersister.put(testItemName, Optional.of(42), testAttribute);
    }

    @Test
    public void testPutReturnsCorrectVersion() throws Exception {
        // A successful put should return the version that the put-to item has after
        // the put - i.e. one higher than it had initially.

        // ARRANGE
        initialiseOptimisticPersister();

        // Configure attributes for database to return - the get is used for logging
        // only, so does not really matter.
        GetAttributesRequest simpleDBRequest = new GetAttributesRequest(testSimpleDBDomainName, testItemName);
        simpleDBRequest.setConsistentRead(true);
        GetAttributesResult getAttributesResult = new GetAttributesResult();
        mockery.checking(new Expectations() {
            {
                allowing(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                will(returnValue(getAttributesResult));
            }
        });

        mockery.checking(new Expectations() {
            {
                allowing(mockSimpleDBClient).putAttributes(with(anything()));
            }
        });

        ReplaceableAttribute testAttribute = new ReplaceableAttribute();
        testAttribute.setName("Name");
        testAttribute.setValue("Value");

        int initialVersion = 42; // Arbitrary

        // ACT
        int finalVersion = optimisticPersister.put(testItemName, Optional.of(initialVersion), testAttribute);

        // ASSERT
        assertTrue("The returned version should be one higher than the initial version",
                finalVersion == (initialVersion + 1));
    }

    @Test
    public void testDeleteThrowsWhenOptimisticPersisterUninitialised() throws Exception {
        // ARRANGE
        thrown.expect(Exception.class);
        thrown.expectMessage("The optimistic persister has not been initialised");

        // ACT
        // Do not initialise the optimistic persister first - so delete should throw
        Attribute testAttribute = new Attribute();
        testAttribute.setName("Name");
        testAttribute.setValue("Value");
        optimisticPersister.delete(testItemName, testAttribute);
    }

    @Test
    public void testDeleteReturnsEarlyIfAttributeBeingDeletedDoesNotExist_NoVersionNumber() throws Exception {
        // We should return early (without calling delete on simpleDB) if the
        // attribute we're deleting no longer exists. This tests for when the item
        // does not even have a version attribute.
        doTestDeleteReturnsEarlyIfAttributeBeingDeletedDoesNotExist(false);
    }

    @Test
    public void testDeleteReturnsEarlyIfAttributeBeingDeletedDoesNotExist_VersionNumber() throws Exception {
        // We should return early (without calling delete on simpleDB) if the
        // attribute we're deleting no longer exists. This tests for when the item
        // has a version attribute, but does not have the attribute we're deleting.
        doTestDeleteReturnsEarlyIfAttributeBeingDeletedDoesNotExist(true);
    }

    private void doTestDeleteReturnsEarlyIfAttributeBeingDeletedDoesNotExist(Boolean versionNumberExists)
            throws Exception {

        // ARRANGE
        initialiseOptimisticPersister();

        // Configure database to return no attributes.
        GetAttributesRequest simpleDBRequest = new GetAttributesRequest(testSimpleDBDomainName, testItemName);
        simpleDBRequest.setConsistentRead(true);
        GetAttributesResult getAttributesResult = new GetAttributesResult();
        if (versionNumberExists) {
            // Ensure we return at least the version number attribute
            getAttributesResult.setAttributes(allAttributes);
        }
        mockery.checking(new Expectations() {
            {
                oneOf(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                will(returnValue(getAttributesResult));
            }
        });

        // Ensure we return early.
        mockery.checking(new Expectations() {
            {
                never(mockSimpleDBClient).deleteAttributes(with(anything()));
            }
        });

        // ACT
        Attribute attributeToDelete = new Attribute();
        attributeToDelete.setName("Name");
        attributeToDelete.setValue("Value");
        optimisticPersister.delete(testItemName, attributeToDelete);
    }

    @Test
    public void testDeleteMarksAttributeAsInactiveBeforeDeletingIt() throws Exception {
        // Deletion is currently a 2-stage process (to ensure the deletion is
        // concurrency-safe). This tests that this 2-stage process is followed.
        // This is a horrible test!

        testDelete(false, Optional.empty(), true);
    }

    @Test
    public void testDeleteThrowsIfTheSimpledbConditionalCheckFailsThreeTimesRunning() throws Exception {

        // SimpleDB can throw a conditional check failed exception if two database
        // writes happen to get interleaved. Almost always, a retry
        // should fix this, and we allow up to three tries. This tests that if
        // simpledb throws three times running, then the persister gives up and also
        // throws.

        testDelete(true, Optional.of(new Exception("Database put failed - conditional check failed")), true, 3);
    }

    @Test
    public void testDeleteDoesNotThrowIfTheSimpledbConditionalCheckFailsOnlyOnce() throws Exception {

        // SimpleDB can throw a conditional check failed exception if two database
        // writes happen to get interleaved. Almost always, a retry
        // should fix this, and we allow up to three tries. This tests that if
        // simpledb throws once, but then succeeds, the persister does not throw.
        // (It should also succeed if simpledb throws twice and then succeeds - but
        // we don't test this.)

        testDelete(false, Optional.empty(), true, 2);
    }

    @Test
    public void testDeleteWorksEvenWhenTheMaximumNumberOfAttributesAlreadyExists() throws Exception {
        // Deletion is currently a 2-stage process (to ensure the deletion is
        // concurrency-safe). This tests that this 2-stage process is followed.
        // This is a horrible test!

        // Initialise with no space for more attributes
        optimisticPersister.initialise(1, mockLogger);
        testDelete(false, Optional.empty(), false);
    }

    private void testDelete(Boolean expectToThrow, Optional<Exception> exceptionToThrow, Boolean doInitialise)
            throws Exception {
        testDelete(expectToThrow, exceptionToThrow, doInitialise, 1);
    }

    private void testDelete(Boolean expectToThrow, Optional<Exception> exceptionToThrow, Boolean doInitialise,
            int numCalls) throws Exception {

        // This has become really horrible. Cause of trouble is difficulty of
        // deleting an attribute from simpleDb whilst also incrementing the version
        // number - necessitating a 2-stage process. Maybe I'm missing something...

        // ARRANGE
        if (exceptionToThrow.isPresent() && expectToThrow) {
            thrown.expect(Exception.class);
            thrown.expectMessage(exceptionToThrow.get().getMessage());
        }
        if (doInitialise) {
            initialiseOptimisticPersister();
        }

        // Configure database to return attributes - including that being deleted.
        GetAttributesRequest simpleDBRequest = new GetAttributesRequest(testSimpleDBDomainName, testItemName);
        simpleDBRequest.setConsistentRead(true);
        GetAttributesResult getAttributesResult = new GetAttributesResult();
        GetAttributesResult getAttributesResult2 = new GetAttributesResult();
        GetAttributesResult getAttributesResult3 = new GetAttributesResult();
        Set<Attribute> allAttributesCopy = new HashSet<>();
        Set<Attribute> allAttributesCopy2 = new HashSet<>();
        Set<Attribute> allAttributesCopy3 = new HashSet<>();
        allAttributesCopy.addAll(allAttributes);
        allAttributesCopy2.addAll(allAttributes);
        allAttributesCopy3.addAll(allAttributes);
        getAttributesResult.setAttributes(allAttributesCopy);
        getAttributesResult2.setAttributes(allAttributesCopy2);
        getAttributesResult3.setAttributes(allAttributesCopy3);
        mockery.checking(new Expectations() {
            {
                // Initial get of attributes. We need copies here as code being tested
                // removes the version attribute each time.
                oneOf(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                will(returnValue(getAttributesResult));
                if (numCalls == 2) {
                    oneOf(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                    will(returnValue(getAttributesResult2));
                }
                if (numCalls == 3) {
                    oneOf(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                    will(returnValue(getAttributesResult2));
                    oneOf(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                    will(returnValue(getAttributesResult3));
                }
            }
        });

        // Ensure we first mark the to-be-deleted attribute as inactive.
        // We will delete the 'active' attribute.
        Attribute inactivatedAttribute = new Attribute();
        inactivatedAttribute.setName("ActiveAttribute");
        // An inactivated attribute should get an 'Inactive' prefix:
        inactivatedAttribute.setValue("InactiveActive");
        ReplaceableAttribute toBeInactivatedAttribute = new ReplaceableAttribute();
        toBeInactivatedAttribute.setName(inactivatedAttribute.getName());
        toBeInactivatedAttribute.setValue(inactivatedAttribute.getValue());
        toBeInactivatedAttribute.setReplace(true);
        ReplaceableAttribute versionNumberAttribute = new ReplaceableAttribute();
        versionNumberAttribute.setName(versionAttributeName);
        versionNumberAttribute.setValue(Integer.toString(testVersionNumber + 1));
        versionNumberAttribute.setReplace(true);
        List<ReplaceableAttribute> replaceableAttributes = new ArrayList<>();
        replaceableAttributes.add(versionNumberAttribute);
        replaceableAttributes.add(toBeInactivatedAttribute);
        UpdateCondition updateCondition = new UpdateCondition();
        updateCondition.setName(versionAttributeName);
        updateCondition.setValue(Integer.toString(testVersionNumber));
        PutAttributesRequest simpleDBPutRequest = new PutAttributesRequest(testSimpleDBDomainName, testItemName,
                replaceableAttributes, updateCondition);

        final Sequence retrySequence = mockery.sequence("retry");
        mockery.checking(new Expectations() {
            {
                // Put the inactive attribute
                if (numCalls > 1) {
                    // This must be a conditional-check-failed test

                    // Initial calls will always throw... It just so happens that when we
                    // test with numCalls == 3, we also want all three calls to throw.
                    AmazonServiceException ase = new AmazonServiceException("");
                    ase.setErrorCode("ConditionalCheckFailed");
                    exactly(numCalls == 3 ? 3 : numCalls - 1).of(mockSimpleDBClient)
                            .putAttributes(with(equal(simpleDBPutRequest)));
                    will(throwException(ase));
                    inSequence(retrySequence);
                    // ...but final call will not throw unless numCalls is 3
                    if (numCalls != 3) {
                        oneOf(mockSimpleDBClient).putAttributes(with(equal(simpleDBPutRequest)));
                        inSequence(retrySequence);
                    }
                } else {
                    oneOf(mockSimpleDBClient).putAttributes(with(equal(simpleDBPutRequest)));
                }
            }
        });

        // Cannot reuse earlier expectation as the persister removes the
        // version-number attribute.
        GetAttributesResult secondGetAttributesResult = new GetAttributesResult();
        secondGetAttributesResult.setAttributes(allAttributes);
        GetAttributesResult secondGetAttributesResult2 = new GetAttributesResult();
        secondGetAttributesResult2.setAttributes(allAttributes);
        GetAttributesResult secondGetAttributesResult3 = new GetAttributesResult();
        secondGetAttributesResult3.setAttributes(allAttributes);
        mockery.checking(new Expectations() {
            {
                // Get as part of the put to create the inactive attribute
                oneOf(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                will(returnValue(secondGetAttributesResult));
                if (numCalls == 2) {
                    oneOf(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                    will(returnValue(secondGetAttributesResult2));
                }
                if (numCalls == 3) {
                    oneOf(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                    will(returnValue(secondGetAttributesResult2));
                    oneOf(mockSimpleDBClient).getAttributes(with(equal(simpleDBRequest)));
                    will(returnValue(secondGetAttributesResult3));
                }
            }
        });

        // Finally, ensure we delete the now-inactivated attribute.
        Attribute inactivatedAttributeToDelete = new Attribute();
        inactivatedAttributeToDelete.setName(toBeInactivatedAttribute.getName());
        inactivatedAttributeToDelete.setValue(toBeInactivatedAttribute.getValue());
        List<Attribute> attributesToDelete = new ArrayList<>();
        attributesToDelete.add(inactivatedAttributeToDelete);
        UpdateCondition deleteUpdateCondition = new UpdateCondition();
        deleteUpdateCondition.setName(inactivatedAttributeToDelete.getName());
        deleteUpdateCondition.setValue(inactivatedAttributeToDelete.getValue());
        deleteUpdateCondition.setExists(true);
        DeleteAttributesRequest simpleDBDeleteRequest = new DeleteAttributesRequest(testSimpleDBDomainName,
                testItemName, attributesToDelete, deleteUpdateCondition);
        mockery.checking(new Expectations() {
            {
                // We always have only one of these calls - unless all three put calls
                // throw Conditional-check-failed exceptions.
                exactly(numCalls == 3 ? 0 : 1).of(mockSimpleDBClient)
                        .deleteAttributes(with(equal(simpleDBDeleteRequest)));
                if (exceptionToThrow.isPresent()) {
                    will(throwException(exceptionToThrow.get()));
                }
            }
        });

        Attribute attributeToDelete = new Attribute();
        attributeToDelete.setName("ActiveAttribute");
        attributeToDelete.setValue("Active");

        // ACT
        optimisticPersister.delete(testItemName, attributeToDelete);
    }

    @Test
    public void testDeleteHandlesDoesNotExistExceptionCorrectly() throws Exception {
        // The persister should forward all simpleDB exceptions to us except
        // DoesNotExist exceptions which it should swallow. This tests swallowing
        // of DoesNotExist exceptions.

        AmazonServiceException exception = new AmazonServiceException("Boom!");
        exception.setErrorCode("AttributeDoesNotExist");
        testDelete(false, Optional.of(exception), true);
    }

    @Test
    public void testDeleteHandlesOtherExceptionsCorrectly() throws Exception {
        // The persister should forward all simpleDB exceptions to us except
        // DoesNotExist exceptions which it should swallow. This tests forwarding
        // of other exceptions.

        AmazonServiceException exception = new AmazonServiceException("Boom!");
        exception.setErrorCode("SomeOtherArbitraryCode");
        testDelete(true, Optional.of(exception), true);
    }

    @Test
    public void testDeleteAllAttributesThrowsWhenOptimisticPersisterUninitialised() throws Exception {
        // ARRANGE
        thrown.expect(Exception.class);
        thrown.expectMessage("The optimistic persister has not been initialised");

        // ACT
        // Do not initialise the optimistic persister first - so delete should throw
        optimisticPersister.deleteAllAttributes(testItemName);
    }

    public void testDeleteAllAttributesCorrectlyCallsTheDatabase() throws Exception {
        // ARRANGE
        initialiseOptimisticPersister();

        DeleteAttributesRequest deleteAttributesRequest = new DeleteAttributesRequest(testSimpleDBDomainName,
                testItemName);
        mockery.checking(new Expectations() {
            {
                oneOf(mockSimpleDBClient).deleteAttributes(with(equal(deleteAttributesRequest)));
            }
        });

        // ACT
        optimisticPersister.deleteAllAttributes(testItemName);
    }
}