Skip to content
Open
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,120 @@
using Shouldly;
using Wemogy.Infrastructure.Database.Core.Abstractions;
using Wemogy.Infrastructure.Database.Core.Attributes;
using Xunit;

namespace Wemogy.Infrastructure.Database.Core.UnitTests.Abstractions;

public class DatabaseClientBaseTests
{
[Fact]
public void ResolveETagValue_ShouldReturnETagPropertyValue()
{
// Arrange
var client = new TestableDatabaseClient<EntityWithETag>();
var entity = new EntityWithETag
{
ETag = "etag-123"
};

// Act
var eTag = client.ResolveETag(entity);

// Assert
eTag.ShouldBe("etag-123");
}

[Fact]
public void SetETagValue_ShouldSetETagProperty()
{
// Arrange
var client = new TestableDatabaseClient<EntityWithETag>();
var entity = new EntityWithETag();

// Act
client.SetETag(entity, "etag-456");

// Assert
entity.ETag.ShouldBe("etag-456");
}

[Fact]
public void SetETagValue_ShouldSupportNull()
{
// Arrange
var client = new TestableDatabaseClient<EntityWithETag>();
var entity = new EntityWithETag
{
ETag = "etag-123"
};

// Act
client.SetETag(entity, null);

// Assert
entity.ETag.ShouldBeNull();
}

[Fact]
public void ResolveETagValue_ShouldReturnNullForEntityWithoutETagProperty()
{
// Arrange
var client = new TestableDatabaseClient<EntityWithoutETag>();
var entity = new EntityWithoutETag();

// Act
var eTag = client.ResolveETag(entity);

// Assert
eTag.ShouldBeNull();
}

[Fact]
public void SetETagValue_ShouldBeNoOpForEntityWithoutETagProperty()
{
// Arrange
var client = new TestableDatabaseClient<EntityWithoutETag>();
var entity = new EntityWithoutETag();

// Act
var exception = Record.Exception(() => client.SetETag(entity, "etag-123"));

// Assert
exception.ShouldBeNull();
}

private class EntityWithETag
{
[Id]
public string Id { get; set; } = string.Empty;

[PartitionKey]
public string TenantId { get; set; } = string.Empty;

[ETag]
public string? ETag { get; set; }
}

private class EntityWithoutETag
{
[Id]
public string Id { get; set; } = string.Empty;

[PartitionKey]
public string TenantId { get; set; } = string.Empty;
}

private class TestableDatabaseClient<TEntity> : DatabaseClientBase<TEntity>
where TEntity : class
{
public string? ResolveETag(TEntity entity)
{
return ResolveETagValue(entity);
}

public void SetETag(TEntity entity, string? eTag)
{
SetETagValue(entity, eTag);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ public static Faker<User> Faker
f => f.Random.Guid().ToString())
.RuleFor(
x => x.Firstname,
f => f.Name.FirstName())
f =>
{
// "John" is silently excluded by GeneralUserReadFilter,
// which makes count/getAll tests flaky
var firstname = f.Name.FirstName();
return firstname == "John" ? "Jane" : firstname;
})
.RuleFor(
x => x.Lastname,
f => f.Name.LastName())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,45 @@ void UpdateAction(User u)
updatedUser.TenantId.ShouldBe(user.TenantId);
}

[Fact]
public async Task UpdateAsync_ShouldRefreshTheUpdatedAtTimestamp()
{
// Arrange
await ResetAsync();
var user = User.Faker.Generate();
var originalUpdatedAt = user.UpdatedAt;
await MicrosoftUserRepository.CreateAsync(user);

// Act
var updatedUser = await MicrosoftUserRepository.UpdateAsync(
user.Id,
user.TenantId,
u => u.Firstname = "Updated");

// Assert
updatedUser.UpdatedAt.ShouldNotBe(originalUpdatedAt);
updatedUser.UpdatedAt.ShouldBe(DateTime.UtcNow, TimeSpan.FromMinutes(1));
}

[Fact]
public async Task UpdateAsyncWithoutPartitionKey_ShouldRefreshTheUpdatedAtTimestamp()
{
// Arrange
await ResetAsync();
var user = User.Faker.Generate();
var originalUpdatedAt = user.UpdatedAt;
await MicrosoftUserRepository.CreateAsync(user);

// Act
var updatedUser = await MicrosoftUserRepository.UpdateAsync(
user.Id,
u => u.Firstname = "Updated");

// Assert
updatedUser.UpdatedAt.ShouldNotBe(originalUpdatedAt);
updatedUser.UpdatedAt.ShouldBe(DateTime.UtcNow, TimeSpan.FromMinutes(1));
}

[Fact]
public async Task UpdateAsync_ShouldThrowIfTheItemNotExists()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public abstract class DatabaseClientBase<TEntity>
{
private readonly PropertyInfo _partitionKeyPropertyInfo;
private readonly PropertyInfo _idPropertyInfo;
private readonly PropertyInfo? _eTagPropertyInfo;

protected DatabaseClientBase()
{
Expand All @@ -32,6 +33,9 @@ protected DatabaseClientBase()
}

_partitionKeyPropertyInfo = partitionKeyPropertyInfo;

// optional: entities opt into optimistic concurrency with [ETag]
_eTagPropertyInfo = typeof(TEntity).GetPropertyByCustomAttribute<ETagAttribute>();
}

protected string ResolveIdValue(TEntity entity)
Expand All @@ -45,4 +49,14 @@ protected string ResolvePartitionKeyValue(TEntity entity)
var partitionKeyValue = (string)_partitionKeyPropertyInfo.GetValue(entity);
return partitionKeyValue;
}

protected string? ResolveETagValue(TEntity entity)
{
return (string?)_eTagPropertyInfo?.GetValue(entity);
}

protected void SetETagValue(TEntity entity, string? eTag)
{
_eTagPropertyInfo?.SetValue(entity, eTag);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ protected EntityBase(string id)
[Id]
public string Id { get; set; }

[ETag]
public string? ETag { get; set; }

public DateTime CreatedAt { get; set; }

public DateTime UpdatedAt { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public interface IEntityBase
public DateTime CreatedAt { get; set; }

public DateTime UpdatedAt { get; set; }

public string? ETag { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System;

namespace Wemogy.Infrastructure.Database.Core.Attributes;

[AttributeUsage(AttributeTargets.Property)]
public class ETagAttribute : Attribute
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public async Task<TEntity> UpdateAsync(string id, Func<TEntity, Task> updateActi
{
var entity = await GetAsync(id);
await updateAction(entity);
entity.UpdatedAt = DateTime.UtcNow;
var updatedEntity = await ReplaceAsync(entity);
return updatedEntity;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public async Task<TEntity> UpdateAsync(string id, string partitionKey, Func<TEnt
id,
partitionKey);
await updateAction(entity);
entity.UpdatedAt = DateTime.UtcNow;
var updatedEntity = await _database.ReplaceAsync(entity);
return updatedEntity;
}
Expand All @@ -44,6 +45,7 @@ public async Task<TEntity> UpdateAsync(string id, Func<TEntity, Task> updateActi
{
var entity = await GetAsync(id);
await updateAction(entity);
entity.UpdatedAt = DateTime.UtcNow;
var updatedEntity = await _database.ReplaceAsync(entity);
return updatedEntity;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Wemogy.Infrastructure.Database.Core.Abstractions;
using Wemogy.Infrastructure.Database.Core.Attributes;

namespace Wemogy.Infrastructure.Database.Cosmos.UnitTests.Fakes;

// reuses the existing "users" container (partition key path /tenantId),
// so no additional container provisioning is required locally or in CI
[RepositoryOptions(collectionName: "users")]
public interface IUserWithETagRepository : IDatabaseRepository<UserWithETag>
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Wemogy.Infrastructure.Database.Core.Abstractions;
using Wemogy.Infrastructure.Database.Core.Attributes;

namespace Wemogy.Infrastructure.Database.Cosmos.UnitTests.Fakes;

/// <summary>
/// Cosmos-only fake that opts into optimistic concurrency via <see cref="ETagAttribute"/>.
/// Kept out of the shared Core.UnitTests fakes on purpose: the eTag value is
/// path-dependent (point operations populate it, queries do not), which breaks
/// the cross-path BeEquivalentTo assertions of the shared repository test suite.
/// </summary>
public class UserWithETag : EntityBase
{
[PartitionKey]
public string TenantId { get; set; } = string.Empty;

public string Firstname { get; set; } = string.Empty;

public string Lastname { get; set; } = string.Empty;
}
Loading