Skip to content
Tracon

TenantIsolationContract<TStore\>

Namespace Tracon.Testing.Contracts.Storage · Assembly Tracon.Testing.Contracts.Xunit.dll

The shared base that checks a store’s tenant isolation both ways.

public abstract class TenantIsolationContract<TStore> : StoreCancellationContract<TStore>, IAsyncLifetime, IAsyncDisposable
Type Parameters
TStore The store type under test.
Inheritance
object ← StoreCancellationContract<TStore> ← TenantIsolationContract<TStore>
Implements
IAsyncLifetime, IAsyncDisposable
Inherited Members
StoreCancellationContract<TStore>.CreateStoreAsync(), StoreCancellationContract<TStore>.InitializeAsync(), StoreCancellationContract<TStore>.DisposeAsync(), StoreCancellationContract<TStore>.OnDisposeAsync(), StoreCancellationContract<TStore>.CancellableReadAsync(CancellationToken), StoreCancellationContract<TStore>.CancellableWriteAsync(CancellationToken), StoreCancellationContract<TStore>.WroteAnythingAsync(), StoreCancellationContract<TStore>.Canceled_token_throws_on_read(), StoreCancellationContract<TStore>.The_write_this_contract_checks_can_be_read_back(), StoreCancellationContract<TStore>.Canceled_token_throws_on_write_and_leaves_no_trace(), StoreCancellationContract<TStore>.Store, object.GetType(), object.MemberwiseClone(), object.ToString(), object.Equals(object?), object.Equals(object?, object?), object.ReferenceEquals(object?, object?), object.GetHashCode()

Derived contracts write only their own scenarios and the four isolation hooks; the lifecycle plumbing and the cancellation promise come from Storage.StoreCancellationContract.

The two-way check cannot be skipped. If only “B must not see it” is checked, a broken query that returns nothing at all would also pass the test. Every scenario therefore also asks “does A see its own data”, proving the filter is both sufficient and not too narrow.

protected TenantIsolationContract()

The tenant that writes the data.

protected const string TenantA = "tenant-a"

string

The tenant that must not see the data.

protected const string TenantB = "tenant-b"

string

The tenant stores read from ITenantContext. For stores that do not take the tenant as a parameter, the contract sets this property to the relevant tenant on the first line of its hooks.

protected MutableTenantContext AmbientTenant { get; }

MutableTenantContext

Calls one read method of the store with the given token.

protected override ValueTask CancellableReadAsync(CancellationToken cancellationToken)

cancellationToken CancellationToken

The token the store must observe.

ValueTask

The completion task; any result is discarded.

The listing hook is the read: it needs no key, so it can be called on an untouched store. A contract whose store has no listing endpoint – and whose TenantIsolationContract.CountAsync therefore makes no store call at all on an empty store – overrides this with its own read.

Calls one write method of the store with the given token.

protected override ValueTask CancellableWriteAsync(CancellationToken cancellationToken)

cancellationToken CancellationToken

The token the store must observe.

ValueTask

The completion task.

The write must be one StoreCancellationContract.WroteAnythingAsync can see. A write this contract cannot read back proves nothing about side effects, which is why StoreCancellationContract.The_write_this_contract_checks_can_be_read_back runs the same pair with a live token and requires the read to find it.

Reports how many records the given tenant sees through the listing endpoint.

protected abstract ValueTask<int> CountAsync(string tenantId, CancellationToken cancellationToken)

tenantId string

The reading tenant.

cancellationToken CancellationToken

The token every store call inside the hook must be handed; see TenantIsolationContract.SeedAsync.

ValueTask<int>

The number of records seen.

Reports whether the record is visible from the given tenant’s viewpoint.

protected abstract ValueTask<bool> ExistsAsync(string tenantId, object key)

tenantId string

The reading tenant.

key object

The key returned by TenantIsolationContract.SeedAsync.

ValueTask<bool>

true if the record is visible.

Same_name_lives_independently_across_two_tenants()

Section titled “ Same_name_lives_independently_across_two_tenants()”
[Fact]
public Task Same_name_lives_independently_across_two_tenants()

Task

SeedAsync(string, string, CancellationToken)

Section titled “ SeedAsync(string, string, CancellationToken)”

Writes a sample record for the given tenant.

protected abstract ValueTask<object> SeedAsync(string tenantId, string name, CancellationToken cancellationToken)

tenantId string

The tenant that owns the record.

name string

The name distinguishing the record. The same name may be used by two tenants.

cancellationToken CancellationToken

The token every store call inside the hook must be handed. The cancellation contract calls this hook with a token that is already cancelled, so a hook that drops the token reports a store as sound when it is not.

ValueTask<object>

The key to use for reading the record back.

Tenant_cannot_delete_another_tenants_record()

Section titled “ Tenant_cannot_delete_another_tenants_record()”
[Fact]
public Task Tenant_cannot_delete_another_tenants_record()

Task

Tenant_cannot_read_another_tenants_record()

Section titled “ Tenant_cannot_read_another_tenants_record()”
[Fact]
public Task Tenant_cannot_read_another_tenants_record()

Task

Tenant_does_not_see_another_tenants_record_in_the_list()

Section titled “ Tenant_does_not_see_another_tenants_record_in_the_list()”
[Fact]
public Task Tenant_does_not_see_another_tenants_record_in_the_list()

Task

[Fact]
public Task Tenant_reads_its_own_record()

Task

Attempts to delete the record on behalf of the given tenant.

protected virtual ValueTask<bool?> TryDeleteAsync(string tenantId, object key)

tenantId string

The tenant attempting the delete.

key object

The key returned by TenantIsolationContract.SeedAsync.

ValueTask<bool?>

true if the delete succeeded, false if the record was not found; null if the store does not offer deletion.

Attempts to change the record’s content on behalf of the given tenant.

protected virtual ValueTask<bool> TryOverwriteAsync(string tenantId, string name)

tenantId string

The tenant attempting the update.

name string

The record’s name.

ValueTask<bool>

true if the update was attempted; false if the store does not support this scenario.

The default implementation calls TenantIsolationContract.SeedAsync a second time: writing a record with the same name verifies that two tenants can carry the same name independently.

Attaches to a tenant context shared per class (used by multiple tests) and resets the tenant back to TenantIsolationContract.TenantA.

protected void UseAmbientTenant(MutableTenantContext tenant)

tenant MutableTenantContext

The shared context supplied by the schema/test CLASS fixture.

When store instances are set up once per class (see the schema fixtures), they all capture the SAME ITenantContext object; each test must therefore call this method inside StoreCancellationContract.CreateStoreAsync to attach to that object and reset any state a previous test may have left on TenantIsolationContract.TenantB.

Reports whether StoreCancellationContract.CancellableWriteAsync left anything behind.

protected override ValueTask<bool> WroteAnythingAsync()

ValueTask<bool>

true if the store holds any record the write would have created.