public void DeleteIfLatestWithoutCurrentData_Throws_IfStopsMakingProgress()
        {
            // Arrange
            IVersionMetadataMapper versionMapper = CreateMapper();
            string id = "Id";

            ConcurrentInnerStore innerStore = new ConcurrentInnerStore(id, new ConcurrentMetadataText("1",
                CreateMetadata(DateTimeOffset.MinValue, versionMapper, "ExistingKey", "ExistingValue"),
                "ExistingText"));

            IDictionary<string, string> updatedMetadata = CreateMetadata(DateTimeOffset.Now, versionMapper,
                "UpdatedKey", "UpdatedValue");
            string updatedText = "UpdatedText";

            // Simulate an erroneous inner store that returns false from TryDelete even though the ETag has not changed.
            // A correct inner store would throw rather that return false if retrying can't help.
            innerStore.OnReadingMetadata += (calls) =>
            {
                if (calls == 1)
                {
                    Assert.True(innerStore.TryDelete(id, "2"));
                    Assert.True(innerStore.TryCreate(id, updatedMetadata, updatedText));
                }
            };
            innerStore.OnReadMetadata += (calls) =>
            {
                if (calls == 0)
                {
                    Assert.True(innerStore.TryUpdate(id, "1", updatedMetadata, updatedText));
                }
            };

            IVersionedMetadataTextStore product = CreateProductUnderTest(innerStore, versionMapper);

            // Act & Assert
            ExceptionAssert.ThrowsInvalidOperation(() => product.DeleteIfLatest(id, DateTimeOffset.MaxValue),
                "The operation stopped making progress.");
        }
        public void DeleteIfLatestWithoutCurrentData_Deletes_IfOlderConcurrentlyExists()
        {
            // Arrange
            IVersionMetadataMapper versionMapper = CreateMapper();
            string id = "Id";

            ConcurrentInnerStore innerStore = new ConcurrentInnerStore(id, new ConcurrentMetadataText("1",
                CreateMetadata(DateTimeOffset.MinValue, versionMapper, "ExistingKey", "ExistingValue"),
                "ExistingText"));

            IDictionary<string, string> updatedMetadata = CreateMetadata(DateTimeOffset.Now, versionMapper,
                "UpdatedKey", "UpdatedValue");
            string updatedText = "UpdatedText";

            innerStore.OnReadingMetadata += (calls) =>
            {
                if (calls == 0)
                {
                    Assert.True(innerStore.TryUpdate(id, "1", updatedMetadata, updatedText));
                }
            };

            IVersionedMetadataTextStore product = CreateProductUnderTest(innerStore, versionMapper);

            // Act
            bool isLatest = product.DeleteIfLatest(id, DateTimeOffset.MaxValue);

            // Assert
            Assert.True(isLatest);
            ConcurrentMetadataText storedItem = innerStore.Read(id);
            Assert.Null(storedItem);
        }
        public void CreateOrUpdateIfLatest_Throws_IfCreateKeepsLooping()
        {
            // Arrange
            IVersionMetadataMapper versionMapper = CreateMapper();
            string id = "Id";

            ConcurrentInnerStore innerStore = new ConcurrentInnerStore(id, new ConcurrentMetadataText("1",
                CreateMetadata(DateTimeOffset.MinValue, versionMapper, "ExistingKey", "ExistingValue"),
                "ExistingText"));

            IDictionary<string, string> updatedMetadata = CreateMetadata(DateTimeOffset.Now, versionMapper,
                "UpdatedKey", "UpdatedValue");
            string updatedText = "UpdatedText";

            // Simulate an erroneous inner store that returns false from TryCreate but null from Read even though the
            // item exists. A correct inner store would throw rather that return false if retrying can't help.
            innerStore.OnReadingMetadata += (_) =>
            {
                Assert.True(innerStore.TryDelete(id, "1"));
            };
            innerStore.OnReadMetadata += (_) =>
            {
                Assert.True(innerStore.TryCreate(id, updatedMetadata, updatedText));
            };

            IVersionedMetadataTextStore product = CreateProductUnderTest(innerStore, versionMapper);

            DateTimeOffset newVersion = DateTimeOffset.MaxValue;
            IDictionary<string, string> newOtherMetadata = CreateMetadata("NewKey", "NewValue");
            string newText = "NewText";

            // Act & Assert
            ExceptionAssert.ThrowsInvalidOperation(
                () => product.CreateOrUpdateIfLatest(id, newVersion, newOtherMetadata, newText),
                "The operation gave up due to repeated failed creation attempts.");
        }
        public void DeleteIfLatestWithCurrentData_Returns_IfConcurrentlyDeleted()
        {
            // Arrange
            IVersionMetadataMapper versionMapper = CreateMapper();
            string id = "Id";

            DateTimeOffset existingVersion = DateTimeOffset.Now;
            ConcurrentInnerStore innerStore = new ConcurrentInnerStore(id, new ConcurrentMetadataText("2",
                CreateMetadata(existingVersion, versionMapper, "ExistingKey", "ExistingValue"), "ExistingText"));

            innerStore.OnReadingMetadata += (calls) =>
            {
                if (calls == 0)
                {
                    Assert.True(innerStore.TryDelete(id, "2"));
                }
            };

            IVersionedMetadataTextStore product = CreateProductUnderTest(innerStore, versionMapper);

            // Act
            bool isLatest = product.DeleteIfLatest(id, DateTimeOffset.MaxValue, "1", existingVersion);

            // Assert
            Assert.True(isLatest);
            ConcurrentMetadataText storedItem = innerStore.Read(id);
            Assert.Null(storedItem);
        }
        public void CreateOrUpdateIfLatest_Updates_IfOlderConcurrentlyExists()
        {
            // Arrange
            IVersionMetadataMapper versionMapper = CreateMapper();
            string id = "Id";

            ConcurrentInnerStore innerStore = new ConcurrentInnerStore(id, new ConcurrentMetadataText("1",
                CreateMetadata(DateTimeOffset.Now, versionMapper, "ExistingKey", "ExistingValue"),
                "ExistingText"));

            IDictionary<string, string> updatedMetadata = CreateMetadata(DateTimeOffset.Now, versionMapper,
                "UpdatedKey", "UpdatedValue");
            string updatedText = "UpdatedText";

            innerStore.OnReadMetadata += (calls) =>
            {
                if (calls == 0)
                {
                    Assert.True(innerStore.TryUpdate(id, "1", updatedMetadata, updatedText));
                }
            };

            IVersionedMetadataTextStore product = CreateProductUnderTest(innerStore, versionMapper);

            DateTimeOffset newVersion = DateTimeOffset.MaxValue;
            IDictionary<string, string> newOtherMetadata = CreateMetadata("NewKey", "NewValue");
            string newText = "NewText";

            // Act
            bool isLatest = product.CreateOrUpdateIfLatest(id, newVersion, newOtherMetadata, newText);

            // Assert
            Assert.True(isLatest);
            ConcurrentMetadataText storedItem = innerStore.Read(id);
            IDictionary<string, string> expectedMetadata = CreateMetadata(newVersion, versionMapper, newOtherMetadata);
            ConcurrentMetadataText expectedItem = new ConcurrentMetadataText("3", expectedMetadata, newText);
            AssertEqual(expectedItem, storedItem);
        }