Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using EventStore.Core.Services.Storage.Indexing;
using Xunit;

namespace EventStore.Core.XUnit.Tests.Services.Storage.Indexing;

public class InMemoryIndexDefinitionStoreTests
{
[Fact]
public async Task read_returns_null_when_missing()
{
var store = new InMemoryIndexDefinitionStore();

var definition = await store.Read(new IndexName("orders"), CancellationToken.None);

Assert.Null(definition);
}

[Fact]
public async Task create_stores_definition()
{
var store = new InMemoryIndexDefinitionStore();
var name = new IndexName("orders");
var definition = CreateDefinition("event.type == 'order'");

var result = await store.Create(name, definition, CancellationToken.None);
var stored = await store.Read(name, CancellationToken.None);

Assert.Equal(IndexDefinitionCreateResult.Created, result);
Assert.Equal(new StoredIndexDefinition(name, definition), stored);
}

[Fact]
public async Task create_is_idempotent_for_same_definition()
{
var store = new InMemoryIndexDefinitionStore();
var name = new IndexName("orders");
var definition = CreateDefinition("event.type == 'order'");

await store.Create(name, definition, CancellationToken.None);
var result = await store.Create(name, definition, CancellationToken.None);
var stored = await store.Read(name, CancellationToken.None);

Assert.Equal(IndexDefinitionCreateResult.AlreadyExists, result);
Assert.Equal(definition, stored.Definition);
}

[Fact]
public async Task create_is_idempotent_for_equivalent_definition()
{
var store = new InMemoryIndexDefinitionStore();
var name = new IndexName("orders");
var first = CreateDefinition("event.type == 'order'");
var second = CreateDefinition("event.type == 'order'");

await store.Create(name, first, CancellationToken.None);
var result = await store.Create(name, second, CancellationToken.None);
var stored = await store.Read(name, CancellationToken.None);

Assert.Equal(IndexDefinitionCreateResult.AlreadyExists, result);
Assert.Equal(first, stored.Definition);
}

[Fact]
public async Task create_reports_conflict_for_different_definition()
{
var store = new InMemoryIndexDefinitionStore();
var name = new IndexName("orders");
var first = CreateDefinition("event.type == 'order'");
var second = CreateDefinition("event.type == 'invoice'");

await store.Create(name, first, CancellationToken.None);
var result = await store.Create(name, second, CancellationToken.None);
var stored = await store.Read(name, CancellationToken.None);

Assert.Equal(IndexDefinitionCreateResult.Conflicts, result);
Assert.Equal(first, stored.Definition);
}

[Fact]
public async Task list_returns_stored_definitions()
{
var store = new InMemoryIndexDefinitionStore();
var orders = new StoredIndexDefinition(new IndexName("orders"), CreateDefinition("event.type == 'order'"));
var invoices = new StoredIndexDefinition(new IndexName("invoices"), CreateDefinition("event.type == 'invoice'"));

await store.Create(orders.Name, orders.Definition, CancellationToken.None);
await store.Create(invoices.Name, invoices.Definition, CancellationToken.None);
var definitions = await store.List(CancellationToken.None);

Assert.Equal([invoices, orders], definitions);
}

[Fact]
public async Task list_returns_snapshot()
{
var store = new InMemoryIndexDefinitionStore();
var orders = new StoredIndexDefinition(new IndexName("orders"), CreateDefinition("event.type == 'order'"));

await store.Create(orders.Name, orders.Definition, CancellationToken.None);
var first = await store.List(CancellationToken.None);
await store.Delete(orders.Name, CancellationToken.None);
var second = await store.List(CancellationToken.None);

Assert.Equal([orders], first);
Assert.Empty(second);
}

[Fact]
public async Task delete_removes_definition()
{
var store = new InMemoryIndexDefinitionStore();
var name = new IndexName("orders");

await store.Create(name, CreateDefinition("event.type == 'order'"), CancellationToken.None);
var deleted = await store.Delete(name, CancellationToken.None);
var stored = await store.Read(name, CancellationToken.None);

Assert.True(deleted);
Assert.Null(stored);
}

[Fact]
public async Task delete_returns_false_when_missing()
{
var store = new InMemoryIndexDefinitionStore();

var deleted = await store.Delete(new IndexName("orders"), CancellationToken.None);

Assert.False(deleted);
}

[Fact]
public async Task create_rejects_missing_name()
{
var store = new InMemoryIndexDefinitionStore();

var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
store.Create(null, CreateDefinition("event.type == 'order'"), CancellationToken.None).AsTask());

Assert.Equal("name", exception.ParamName);
}

[Fact]
public async Task create_rejects_missing_definition()
{
var store = new InMemoryIndexDefinitionStore();

var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
store.Create(new IndexName("orders"), null, CancellationToken.None).AsTask());

Assert.Equal("definition", exception.ParamName);
}

[Fact]
public async Task read_rejects_missing_name()
{
var store = new InMemoryIndexDefinitionStore();

var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
store.Read(null, CancellationToken.None).AsTask());

Assert.Equal("name", exception.ParamName);
}

[Fact]
public async Task delete_rejects_missing_name()
{
var store = new InMemoryIndexDefinitionStore();

var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
store.Delete(null, CancellationToken.None).AsTask());

Assert.Equal("name", exception.ParamName);
}

[Fact]
public async Task create_honors_cancelled_token()
{
var store = new InMemoryIndexDefinitionStore();
using var cancellation = new CancellationTokenSource();
await cancellation.CancelAsync();

await Assert.ThrowsAsync<OperationCanceledException>(() =>
store.Create(new IndexName("orders"), CreateDefinition("event.type == 'order'"), cancellation.Token).AsTask());
}

[Fact]
public async Task read_honors_cancelled_token()
{
var store = new InMemoryIndexDefinitionStore();
using var cancellation = new CancellationTokenSource();
await cancellation.CancelAsync();

await Assert.ThrowsAsync<OperationCanceledException>(() =>
store.Read(new IndexName("orders"), cancellation.Token).AsTask());
}

[Fact]
public async Task list_honors_cancelled_token()
{
var store = new InMemoryIndexDefinitionStore();
using var cancellation = new CancellationTokenSource();
await cancellation.CancelAsync();

await Assert.ThrowsAsync<OperationCanceledException>(() =>
store.List(cancellation.Token).AsTask());
}

[Fact]
public async Task delete_honors_cancelled_token()
{
var store = new InMemoryIndexDefinitionStore();
using var cancellation = new CancellationTokenSource();
await cancellation.CancelAsync();

await Assert.ThrowsAsync<OperationCanceledException>(() =>
store.Delete(new IndexName("orders"), cancellation.Token).AsTask());
}

private static IndexDefinition CreateDefinition(string filter) =>
new(new IndexEventFilter(filter), [new IndexFieldDefinition("id", new IndexFieldSelector("event.body.id"))]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,33 @@ public void constructor_copies_fields()
Assert.Equal([field], definition.Fields);
}

[Fact]
public void equivalent_definitions_are_equal()
{
var first = new IndexDefinition(
new IndexEventFilter("event.type == 'order'"),
[new IndexFieldDefinition("customerId", new IndexFieldSelector("event.body.customerId"))]);
var second = new IndexDefinition(
new IndexEventFilter("event.type == 'order'"),
[new IndexFieldDefinition("customerId", new IndexFieldSelector("event.body.customerId"))]);

Assert.Equal(first, second);
Assert.Equal(first.GetHashCode(), second.GetHashCode());
}

[Fact]
public void different_fields_are_not_equal()
{
var first = new IndexDefinition(
new IndexEventFilter("event.type == 'order'"),
[new IndexFieldDefinition("customerId", new IndexFieldSelector("event.body.customerId"))]);
var second = new IndexDefinition(
new IndexEventFilter("event.type == 'order'"),
[new IndexFieldDefinition("tenantId", new IndexFieldSelector("event.body.tenantId"))]);

Assert.NotEqual(first, second);
}

[Theory]
[InlineData(null)]
[InlineData("")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using EventStore.Core.Services.Storage.Indexing;
using Xunit;

namespace EventStore.Core.XUnit.Tests.Services.Storage.Indexing;

public class IndexNameTests
{
[Theory]
[InlineData("orders")]
[InlineData("orders_by_customer")]
[InlineData("orders-by-customer")]
[InlineData("orders2")]
public void constructor_accepts_valid_name(string value)
{
var name = new IndexName(value);

Assert.Equal(value, name.Value);
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void constructor_rejects_empty_name(string value)
{
var exception = Assert.Throws<ArgumentException>(() => new IndexName(value));

Assert.Equal("value", exception.ParamName);
}

[Theory]
[InlineData("Orders")]
[InlineData("orders by customer")]
[InlineData("orders.by.customer")]
[InlineData("orders/active")]
public void constructor_rejects_invalid_characters(string value)
{
var exception = Assert.Throws<ArgumentException>(() => new IndexName(value));

Assert.Equal("value", exception.ParamName);
}

[Fact]
public void to_string_returns_name()
{
var name = new IndexName("orders");

Assert.Equal("orders", name.ToString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

#nullable enable

namespace EventStore.Core.Services.Storage.Indexing;

public interface IIndexDefinitionStore
{
ValueTask<IndexDefinitionCreateResult> Create(IndexName name, IndexDefinition definition, CancellationToken token);

ValueTask<StoredIndexDefinition?> Read(IndexName name, CancellationToken token);

ValueTask<IReadOnlyList<StoredIndexDefinition>> List(CancellationToken token);

ValueTask<bool> Delete(IndexName name, CancellationToken token);
}

public enum IndexDefinitionCreateResult
{
Created,
AlreadyExists,
Conflicts
}

public sealed record StoredIndexDefinition
{
public IndexName Name { get; }

public IndexDefinition Definition { get; }

public StoredIndexDefinition(IndexName name, IndexDefinition definition)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Definition = definition ?? throw new ArgumentNullException(nameof(definition));
}
}
Loading
Loading