Browse Source

Tests for event store and asset store finalized.

pull/349/head
Sebastian Stehle 7 years ago
parent
commit
eff21ef772
  1. 44
      src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs
  2. 51
      src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs
  3. 5
      src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs
  4. 2
      src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs
  5. 2
      tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs
  6. 1
      tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreFixture.cs
  7. 2
      tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs
  8. 13
      tests/Squidex.Infrastructure.Tests/Assets/MongoGridFSAssetStoreFixture.cs
  9. 2
      tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs
  10. 123
      tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs
  11. 62
      tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs
  12. 29
      tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs
  13. 16
      tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs
  14. 2
      tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests.cs
  15. 1
      tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj

44
src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs

@ -6,6 +6,8 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EventStore.ClientAPI;
using Squidex.Infrastructure.Json;
@ -15,24 +17,54 @@ namespace Squidex.Infrastructure.EventSourcing
{
public static class Formatter
{
public static StoredEvent Read(ResolvedEvent resolvedEvent, IJsonSerializer serializer)
private static readonly HashSet<string> PrivateHeaders = new HashSet<string> { "$v", "$p", "$c", "$causedBy" };
public static StoredEvent Read(ResolvedEvent resolvedEvent, string prefix, IJsonSerializer serializer)
{
var @event = resolvedEvent.Event;
var metadata = Encoding.UTF8.GetString(@event.Data);
var eventPayload = Encoding.UTF8.GetString(@event.Data);
var eventHeaders = GetHeaders(serializer, @event);
var headersJson = Encoding.UTF8.GetString(@event.Metadata);
var headers = serializer.Deserialize<EnvelopeHeaders>(headersJson);
var eventData = new EventData(@event.EventType, eventHeaders, eventPayload);
var eventData = new EventData(@event.EventType, headers, metadata);
var streamName = GetStreamName(prefix, @event);
return new StoredEvent(
@event.EventStreamId,
streamName,
resolvedEvent.OriginalEventNumber.ToString(),
resolvedEvent.Event.EventNumber,
eventData);
}
private static string GetStreamName(string prefix, RecordedEvent @event)
{
var streamName = @event.EventStreamId;
if (streamName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
streamName = streamName.Substring(prefix.Length + 1);
}
return streamName;
}
private static EnvelopeHeaders GetHeaders(IJsonSerializer serializer, RecordedEvent @event)
{
var headersJson = Encoding.UTF8.GetString(@event.Metadata);
var headers = serializer.Deserialize<EnvelopeHeaders>(headersJson);
foreach (var key in headers.Keys.ToList())
{
if (PrivateHeaders.Contains(key))
{
headers.Remove(key);
}
}
return headers;
}
public static EventStoreData Write(EventData eventData, IJsonSerializer serializer)
{
var payload = Encoding.UTF8.GetBytes(eventData.Payload);

51
src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs

@ -11,6 +11,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EventStore.ClientAPI;
using EventStore.ClientAPI.Exceptions;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Log;
@ -54,7 +55,7 @@ namespace Squidex.Infrastructure.EventSourcing
public IEventSubscription CreateSubscription(IEventSubscriber subscriber, string streamFilter, string position = null)
{
return new GetEventStoreSubscription(connection, subscriber, serializer, projectionClient, position, streamFilter);
return new GetEventStoreSubscription(connection, subscriber, serializer, projectionClient, position, prefix, streamFilter);
}
public Task CreateIndexAsync(string property)
@ -91,7 +92,7 @@ namespace Squidex.Infrastructure.EventSourcing
StreamEventsSlice currentSlice;
do
{
currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, false);
currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, true);
if (currentSlice.Status == SliceReadStatus.Success)
{
@ -99,7 +100,7 @@ namespace Squidex.Infrastructure.EventSourcing
foreach (var resolved in currentSlice.Events)
{
var storedEvent = Formatter.Read(resolved, serializer);
var storedEvent = Formatter.Read(resolved, prefix, serializer);
await callback(storedEvent);
}
@ -114,12 +115,12 @@ namespace Squidex.Infrastructure.EventSourcing
{
var result = new List<StoredEvent>();
var sliceStart = streamPosition;
var sliceStart = streamPosition >= 0 ? streamPosition : StreamPosition.Start;
StreamEventsSlice currentSlice;
do
{
currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, ReadPageSize, false);
currentSlice = await connection.ReadStreamEventsForwardAsync(GetStreamName(streamName), sliceStart, ReadPageSize, true);
if (currentSlice.Status == SliceReadStatus.Success)
{
@ -127,7 +128,7 @@ namespace Squidex.Infrastructure.EventSourcing
foreach (var resolved in currentSlice.Events)
{
var storedEvent = Formatter.Read(resolved, serializer);
var storedEvent = Formatter.Read(resolved, prefix, serializer);
result.Add(storedEvent);
}
@ -141,7 +142,7 @@ namespace Squidex.Infrastructure.EventSourcing
public Task DeleteStreamAsync(string streamName)
{
return connection.DeleteStreamAsync(streamName, ExpectedVersion.Any);
return connection.DeleteStreamAsync(GetStreamName(streamName), ExpectedVersion.Any);
}
public Task AppendAsync(Guid commitId, string streamName, ICollection<EventData> events)
@ -168,27 +169,39 @@ namespace Squidex.Infrastructure.EventSourcing
return;
}
var eventsToSave = events.Select(x => Formatter.Write(x, serializer)).ToList();
if (eventsToSave.Count < WritePageSize)
{
await connection.AppendToStreamAsync(GetStreamName(streamName), expectedVersion, eventsToSave);
}
else
try
{
using (var transaction = await connection.StartTransactionAsync(GetStreamName(streamName), expectedVersion))
var eventsToSave = events.Select(x => Formatter.Write(x, serializer)).ToList();
if (eventsToSave.Count < WritePageSize)
{
for (var p = 0; p < eventsToSave.Count; p += WritePageSize)
await connection.AppendToStreamAsync(GetStreamName(streamName), expectedVersion, eventsToSave);
}
else
{
using (var transaction = await connection.StartTransactionAsync(GetStreamName(streamName), expectedVersion))
{
await transaction.WriteAsync(eventsToSave.Skip(p).Take(WritePageSize));
}
for (var p = 0; p < eventsToSave.Count; p += WritePageSize)
{
await transaction.WriteAsync(eventsToSave.Skip(p).Take(WritePageSize));
}
await transaction.CommitAsync();
await transaction.CommitAsync();
}
}
}
catch (WrongExpectedVersionException ex)
{
throw new WrongEventVersionException(ParseVersion(ex.Message), expectedVersion);
}
}
}
private static int ParseVersion(string message)
{
return int.Parse(message.Substring(message.LastIndexOf(':') + 1));
}
private string GetStreamName(string streamName)
{
return $"{prefix}-{streamName}";

5
src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs

@ -18,6 +18,7 @@ namespace Squidex.Infrastructure.EventSourcing
private readonly IEventStoreConnection connection;
private readonly IEventSubscriber subscriber;
private readonly IJsonSerializer serializer;
private readonly string prefix;
private readonly EventStoreCatchUpSubscription subscription;
private readonly long? position;
@ -27,6 +28,7 @@ namespace Squidex.Infrastructure.EventSourcing
IJsonSerializer serializer,
ProjectionClient projectionClient,
string position,
string prefix,
string streamFilter)
{
Guard.NotNull(subscriber, nameof(subscriber));
@ -34,6 +36,7 @@ namespace Squidex.Infrastructure.EventSourcing
this.connection = connection;
this.position = projectionClient.ParsePositionOrNull(position);
this.prefix = prefix;
var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result;
@ -61,7 +64,7 @@ namespace Squidex.Infrastructure.EventSourcing
return connection.SubscribeToStreamFrom(streamName, position, settings,
(s, e) =>
{
var storedEvent = Formatter.Read(e, serializer);
var storedEvent = Formatter.Read(e, prefix, serializer);
subscriber.OnEventAsync(this, storedEvent).Wait();
}, null,

2
src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionClient.cs

@ -137,7 +137,7 @@ namespace Squidex.Infrastructure.EventSourcing
public long ParsePosition(string position)
{
return long.TryParse(position, out var parsedPosition) ? parsedPosition : 0;
return long.TryParse(position, out var parsedPosition) ? parsedPosition + 1 : StreamPosition.Start;
}
}
}

2
tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs

@ -9,7 +9,7 @@ using Xunit;
namespace Squidex.Infrastructure.Assets
{
[Trait("Dependency", "Azure")]
[Trait("Category", "Dependencies")]
public class AzureBlobAssetStoreTests : AssetStoreTests<AzureBlobAssetStore>, IClassFixture<AzureBlobAssetStoreFixture>
{
private readonly AzureBlobAssetStoreFixture fixture;

1
tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreFixture.cs

@ -16,6 +16,7 @@ namespace Squidex.Infrastructure.Assets
public GoogleCloudAssetStoreFixture()
{
AssetStore = new GoogleCloudAssetStore("squidex-test");
AssetStore.InitializeAsync().Wait();
}
public void Dispose()

2
tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs

@ -9,7 +9,7 @@ using Xunit;
namespace Squidex.Infrastructure.Assets
{
[Trait("Dependency", "GC")]
[Trait("Category", "Dependencies")]
public class GoogleCloudAssetStoreTests : AssetStoreTests<GoogleCloudAssetStore>, IClassFixture<GoogleCloudAssetStoreFixture>
{
private readonly GoogleCloudAssetStoreFixture fixture;

13
tests/Squidex.Infrastructure.Tests/Assets/MongoGridFSAssetStoreFixture.cs

@ -13,17 +13,16 @@ namespace Squidex.Infrastructure.Assets
{
public sealed class MongoGridFSAssetStoreFixture : IDisposable
{
public MongoGridFsAssetStore AssetStore { get; }
public IMongoClient MongoClient { get; } = new MongoClient("mongodb://localhost");
private readonly IMongoClient mongoClient = new MongoClient("mongodb://localhost");
private readonly IMongoDatabase mongoDatabase;
public IMongoDatabase MongoDatabase { get; }
public MongoGridFsAssetStore AssetStore { get; }
public MongoGridFSAssetStoreFixture()
{
MongoDatabase = MongoClient.GetDatabase("GridFSTest");
mongoDatabase = mongoClient.GetDatabase("GridFSTest");
var gridFSBucket = new GridFSBucket<string>(MongoDatabase, new GridFSBucketOptions
var gridFSBucket = new GridFSBucket<string>(mongoDatabase, new GridFSBucketOptions
{
BucketName = "fs"
});
@ -34,7 +33,7 @@ namespace Squidex.Infrastructure.Assets
public void Dispose()
{
MongoClient.DropDatabase("GridFSTest");
mongoClient.DropDatabase("GridFSTest");
}
}
}

2
tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs

@ -9,7 +9,7 @@ using Xunit;
namespace Squidex.Infrastructure.Assets
{
[Trait("Dependency", "MongoDB")]
[Trait("Category", "Dependencies")]
public class MongoGridFsAssetStoreTests : AssetStoreTests<MongoGridFsAssetStore>, IClassFixture<MongoGridFSAssetStoreFixture>
{
private readonly MongoGridFSAssetStoreFixture fixture;

123
tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Squidex.Infrastructure.Tasks;
@ -88,7 +89,7 @@ namespace Squidex.Infrastructure.EventSourcing
var events = new EventData[]
{
new EventData("Type1", new EnvelopeHeaders(), "1"),
new EventData("Type2", new EnvelopeHeaders(), "2"),
new EventData("Type2", new EnvelopeHeaders(), "2")
};
await Sut.AppendAsync(Guid.NewGuid(), streamName, events);
@ -117,31 +118,18 @@ namespace Squidex.Infrastructure.EventSourcing
new EventData("Type2", new EnvelopeHeaders(), "2"),
};
var subscriber = new EventSubscriber();
IEventSubscription subscription = null;
try
var readEvents = await QueryWithSubscriptionAsync(streamName, async () =>
{
subscription = Sut.CreateSubscription(subscriber, streamName);
await Sut.AppendAsync(Guid.NewGuid(), streamName, events);
});
subscription.WakeUp();
await Task.Delay(SubscriptionDelayInMs);
var expected = new StoredEvent[]
{
new StoredEvent(streamName, "Position", 0, events[0]),
new StoredEvent(streamName, "Position", 1, events[1])
};
ShouldBeEquivalentTo(subscriber.Events, expected);
}
finally
var expected = new StoredEvent[]
{
await subscription.StopAsync();
}
new StoredEvent(streamName, "Position", 0, events[0]),
new StoredEvent(streamName, "Position", 1, events[1])
};
ShouldBeEquivalentTo(readEvents, expected);
}
[Fact]
@ -183,13 +171,12 @@ namespace Squidex.Infrastructure.EventSourcing
};
await Sut.AppendAsync(Guid.NewGuid(), streamName, events);
await Sut.DeleteStreamAsync(streamName);
var readEvents1 = await QueryAsync(streamName);
var readEvents2 = await QueryWithCallbackAsync(streamName);
var readEvents = await QueryAsync(streamName);
Assert.Empty(readEvents1);
Assert.Empty(readEvents2);
Assert.Empty(readEvents);
}
[Fact]
@ -198,8 +185,8 @@ namespace Squidex.Infrastructure.EventSourcing
var keyed1 = new EnvelopeHeaders();
var keyed2 = new EnvelopeHeaders();
keyed1.Add("key", "1");
keyed2.Add("key", "2");
keyed1.Add("key", Guid.NewGuid().ToString());
keyed2.Add("key", Guid.NewGuid().ToString());
var streamName1 = $"test-{Guid.NewGuid()}";
var streamName2 = $"test-{Guid.NewGuid()}";
@ -221,7 +208,7 @@ namespace Squidex.Infrastructure.EventSourcing
await Sut.AppendAsync(Guid.NewGuid(), streamName1, events1);
await Sut.AppendAsync(Guid.NewGuid(), streamName2, events2);
var readEvents = await QueryWithFilterAsync("key", "2");
var readEvents = await QueryWithFilterAsync("key", keyed2["key"].ToString());
var expected = new StoredEvent[]
{
@ -239,20 +226,86 @@ namespace Squidex.Infrastructure.EventSourcing
private async Task<IReadOnlyList<StoredEvent>> QueryWithFilterAsync(string property, object value)
{
var readEvents = new List<StoredEvent>();
using (var cts = new CancellationTokenSource(30000))
{
while (!cts.IsCancellationRequested)
{
var readEvents = new List<StoredEvent>();
await Sut.QueryAsync(x => { readEvents.Add(x); return TaskHelper.Done; }, property, value, null, cts.Token);
await Task.Delay(500, cts.Token);
if (readEvents.Count > 0)
{
return readEvents;
}
}
await Sut.QueryAsync(x => { readEvents.Add(x); return TaskHelper.Done; }, property, value);
cts.Token.ThrowIfCancellationRequested();
return readEvents;
return null;
}
}
private async Task<IReadOnlyList<StoredEvent>> QueryWithCallbackAsync(string streamFilter = null, string position = null)
{
var readEvents = new List<StoredEvent>();
using (var cts = new CancellationTokenSource(30000))
{
while (!cts.IsCancellationRequested)
{
var readEvents = new List<StoredEvent>();
await Sut.QueryAsync(x => { readEvents.Add(x); return TaskHelper.Done; }, streamFilter, position, cts.Token);
await Task.Delay(500, cts.Token);
if (readEvents.Count > 0)
{
return readEvents;
}
}
cts.Token.ThrowIfCancellationRequested();
await Sut.QueryAsync(x => { readEvents.Add(x); return TaskHelper.Done; }, streamFilter, position);
return null;
}
}
return readEvents;
private async Task<IReadOnlyList<StoredEvent>> QueryWithSubscriptionAsync(string streamFilter, Func<Task> action)
{
var subscriber = new EventSubscriber();
IEventSubscription subscription = null;
try
{
subscription = Sut.CreateSubscription(subscriber, streamFilter);
await action();
using (var cts = new CancellationTokenSource(30000))
{
while (!cts.IsCancellationRequested)
{
subscription.WakeUp();
await Task.Delay(500, cts.Token);
if (subscriber.Events.Count > 0)
{
return subscriber.Events;
}
}
cts.Token.ThrowIfCancellationRequested();
return null;
}
}
finally
{
await subscription.StopAsync();
}
}
private void ShouldBeEquivalentTo(IEnumerable<StoredEvent> actual, params StoredEvent[] expected)

62
tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreFixture.cs

@ -0,0 +1,62 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using EventStore.ClientAPI;
using EventStore.ClientAPI.Projections;
using Squidex.Infrastructure.TestHelpers;
namespace Squidex.Infrastructure.EventSourcing
{
public sealed class GetEventStoreFixture : IDisposable
{
private readonly IEventStoreConnection connection;
public GetEventStore EventStore { get; }
public GetEventStoreFixture()
{
connection = EventStoreConnection.Create("ConnectTo=tcp://admin:changeit@localhost:1113; HeartBeatTimeout=500; MaxReconnections=-1");
EventStore = new GetEventStore(connection, JsonHelper.DefaultSerializer, "test", "localhost");
EventStore.InitializeAsync().Wait();
}
public void Dispose()
{
CleanupAsync().Wait();
}
private async Task CleanupAsync()
{
var endpoints = await Dns.GetHostAddressesAsync("localhost");
var endpoint = new IPEndPoint(endpoints.First(x => x.AddressFamily == AddressFamily.InterNetwork), 2113);
var credentials = connection.Settings.DefaultUserCredentials;
var projectionsManager =
new ProjectionsManager(
connection.Settings.Log, endpoint,
connection.Settings.OperationTimeout);
foreach (var projection in await projectionsManager.ListAllAsync(credentials))
{
var name = projection.Name;
if (name.StartsWith("by-test", StringComparison.OrdinalIgnoreCase))
{
await projectionsManager.DisableAsync(name, credentials);
await projectionsManager.DeleteAsync(name, true, credentials);
}
}
}
}
}

29
tests/Squidex.Infrastructure.Tests/EventSourcing/GetEventStoreTests.cs

@ -0,0 +1,29 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Xunit;
namespace Squidex.Infrastructure.EventSourcing
{
[Trait("Category", "Dependencies")]
public class GetEventStoreTests : EventStoreTests<GetEventStore>, IClassFixture<GetEventStoreFixture>
{
private readonly GetEventStoreFixture fixture;
protected override int SubscriptionDelayInMs { get; } = 1000;
public GetEventStoreTests(GetEventStoreFixture fixture)
{
this.fixture = fixture;
}
public override GetEventStore CreateStore()
{
return fixture.EventStore;
}
}
}

16
tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs

@ -16,27 +16,25 @@ namespace Squidex.Infrastructure.EventSourcing
{
public sealed class MongoEventStoreFixture : IDisposable
{
public MongoEventStore EventStore { get; }
public IMongoClient MongoClient { get; } = new MongoClient("mongodb://localhost");
private readonly IMongoClient mongoClient = new MongoClient("mongodb://localhost");
private readonly IMongoDatabase mongoDatabase;
private readonly IEventNotifier notifier = A.Fake<IEventNotifier>();
public IMongoDatabase MongoDatabase { get; }
public IEventNotifier Notifier { get; } = A.Fake<IEventNotifier>();
public MongoEventStore EventStore { get; }
public MongoEventStoreFixture()
{
MongoDatabase = MongoClient.GetDatabase("EventStoreTest");
mongoDatabase = mongoClient.GetDatabase("EventStoreTest");
BsonJsonConvention.Register(JsonSerializer.Create(JsonHelper.DefaultSettings()));
EventStore = new MongoEventStore(MongoDatabase, Notifier);
EventStore = new MongoEventStore(mongoDatabase, notifier);
EventStore.InitializeAsync().Wait();
}
public void Dispose()
{
MongoClient.DropDatabase("EventStoreTest");
mongoClient.DropDatabase("EventStoreTest");
}
}
}

2
tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreTests.cs

@ -9,7 +9,7 @@ using Xunit;
namespace Squidex.Infrastructure.EventSourcing
{
[Trait("Dependency", "MongoDB")]
[Trait("Category", "Dependencies")]
public class MongoEventStoreTests : EventStoreTests<MongoEventStore>, IClassFixture<MongoEventStoreFixture>
{
private readonly MongoEventStoreFixture fixture;

1
tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj

@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Squidex.Infrastructure.Azure\Squidex.Infrastructure.Azure.csproj" />
<ProjectReference Include="..\..\src\Squidex.Infrastructure.GetEventStore\Squidex.Infrastructure.GetEventStore.csproj" />
<ProjectReference Include="..\..\src\Squidex.Infrastructure.GoogleCloud\Squidex.Infrastructure.GoogleCloud.csproj" />
<ProjectReference Include="..\..\src\Squidex.Infrastructure.MongoDb\Squidex.Infrastructure.MongoDb.csproj" />
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />

Loading…
Cancel
Save