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
@@ -1,6 +1,6 @@
namespace CodeBeam.UltimateAuth.Core.Contracts;

// TODO: Add global scope
// TODO: Add global scope and implement other scopes properly and remove obsolete attribute.
/// <summary>
/// Defines the scope of uniqueness for an identifier.
/// </summary>
Expand All @@ -10,11 +10,13 @@ public enum UniquenessScope
/// No uniqueness is enforced.
/// Note that users still can't create duplicate identifiers.
/// </summary>
[Obsolete("None uniqueness is not currently supported. Use Tenant until the v0.5.0 uniqueness expansion.")]
None = 0,

/// <summary>
/// Uniqueness is enforced within a single user.
/// </summary>
[Obsolete("WithinUser uniqueness is not currently supported. Use Tenant until the v0.5.0 uniqueness expansion.")]
WithinUser = 10,

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ public async Task<UserIdentifierExistenceResult> ExistsAsync(IdentifierExistence
.Where(x =>
x.Tenant == _tenant &&
x.Type == query.Type &&
x.NormalizedValue == query.NormalizedValue &&
x.DeletedAt == null);
x.NormalizedValue == query.NormalizedValue);

if (query.ExcludeIdentifierId.HasValue)
q = q.Where(x => x.Id != query.ExcludeIdentifierId.Value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ public Task<UserIdentifierExistenceResult> ExistsAsync(IdentifierExistenceQuery
var candidates = TenantValues()
.Where(x =>
x.Type == query.Type &&
x.NormalizedValue == query.NormalizedValue &&
!x.IsDeleted);
x.NormalizedValue == query.NormalizedValue);

if (query.ExcludeIdentifierId.HasValue)
candidates = candidates.Where(x => x.Id != query.ExcludeIdentifierId.Value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -598,37 +598,62 @@ public async Task ExistsAsync_ExcludeIdentifierId_ExcludesCurrentIdentifier()
await store.AddAsync(identifier);

var result = await store.ExistsAsync(
new IdentifierExistenceQuery(
identifier.Type,
identifier.NormalizedValue,
IdentifierExistenceScope.TenantAny,
ExcludeIdentifierId: identifier.Id));
new IdentifierExistenceQuery(
identifier.Type,
identifier.NormalizedValue,
IdentifierExistenceScope.TenantAny,
ExcludeIdentifierId: identifier.Id));

result.Exists.Should().BeFalse();
}

[Fact]
public async Task ExistsAsync_DoesNotMatchDeletedIdentifier()

[Theory]
[InlineData(UserIdentifierType.Username, "alice", "ALICE")]
[InlineData(UserIdentifierType.Email, "alice@example.com", "ALICE@EXAMPLE.COM")]
public async Task ExistsAsync_SoftDeletedIdentifier_RemainsReserved(UserIdentifierType type, string value, string normalizedValue)
{
await using var db = await CreateDatabaseAsync();
var store = db.CreateStore(TenantA);

var identifier = CreateIdentifier(TenantA);
var identifier = CreateIdentifier(
TenantA,
type: type,
value: value,
normalizedValue: normalizedValue,
isPrimary: true);

await store.AddAsync(identifier);

await store.DeleteAsync(
identifier.Id,
0,
expectedVersion: 0,
DeleteMode.Soft,
Now.AddHours(1));

var result = await store.ExistsAsync(
new IdentifierExistenceQuery(
identifier.Type,
identifier.NormalizedValue,
IdentifierExistenceScope.TenantAny));

result.Exists.Should().BeFalse();
// A deleted identifier still reserves its normalized value.
var exists = await store.ExistsAsync(
new IdentifierExistenceQuery(
type,
normalizedValue,
IdentifierExistenceScope.TenantAny));

exists.Exists.Should().BeTrue();
exists.OwnerIdentifierId.Should().Be(identifier.Id);

// Ordinary lookup must not resolve deleted identifiers.
var lookup = await store.GetAsync(type, normalizedValue);
lookup.Should().BeNull();

// Excluding the matching identifier must still work.
var excluded = await store.ExistsAsync(
new IdentifierExistenceQuery(
type,
normalizedValue,
IdentifierExistenceScope.TenantAny,
ExcludeIdentifierId: identifier.Id));

excluded.Exists.Should().BeFalse();
}

[Fact]
Expand Down Expand Up @@ -919,4 +944,104 @@ await store.AddAsync(CreateIdentifier(
"B@EXAMPLE.COM",
"C@EXAMPLE.COM");
}

// TODO: Think these about with UniquenessScope
//[Fact]
//public async Task AddAsync_DuplicateNormalizedValue_IsRejected()
//{
// await using var db = await CreateDatabaseAsync();
// var store = db.CreateStore(TenantA);

// var first = CreateIdentifier(
// TenantA,
// userKey: UserKey.New(),
// value: "first@example.com",
// normalizedValue: "SAME@EXAMPLE.COM");

// var second = CreateIdentifier(
// TenantA,
// userKey: UserKey.New(),
// value: "second@example.com",
// normalizedValue: "SAME@EXAMPLE.COM");

// await store.AddAsync(first);

// var act = () => store.AddAsync(second);

// await act.Should().ThrowAsync<Exception>();

// (await store.GetByIdAsync(first.Id))
// .Should().NotBeNull();

// (await store.GetByIdAsync(second.Id))
// .Should().BeNull();
//}

//[Fact]
//public async Task AddAsync_SoftDeletedNormalizedValue_RemainsReserved()
//{
// await using var db = await CreateDatabaseAsync();
// var store = db.CreateStore(TenantA);

// var first = CreateIdentifier(
// TenantA,
// normalizedValue: "RESERVED@EXAMPLE.COM");

// await store.AddAsync(first);

// await store.DeleteAsync(
// first.Id,
// expectedVersion: 0,
// DeleteMode.Soft,
// Now.AddHours(1));

// var second = CreateIdentifier(
// TenantA,
// userKey: UserKey.New(),
// value: "reserved@example.com",
// normalizedValue: "RESERVED@EXAMPLE.COM");

// var act = () => store.AddAsync(second);

// await act.Should().ThrowAsync<Exception>();

// (await store.GetByIdAsync(second.Id))
// .Should().BeNull();
//}

//[Fact]
//public async Task SaveAsync_DuplicateNormalizedValue_IsRejected()
//{
// await using var db = await CreateDatabaseAsync();
// var store = db.CreateStore(TenantA);

// var first = CreateIdentifier(
// TenantA,
// value: "first@example.com",
// normalizedValue: "FIRST@EXAMPLE.COM");

// var second = CreateIdentifier(
// TenantA,
// value: "second@example.com",
// normalizedValue: "SECOND@EXAMPLE.COM");

// await store.AddAsync(first);
// await store.AddAsync(second);

// second.ChangeValue(
// "first@example.com",
// "FIRST@EXAMPLE.COM",
// Now.AddMinutes(1));

// var act = () => store.SaveAsync(
// second,
// expectedVersion: 0);

// await act.Should().ThrowAsync<Exception>();

// var persisted = await store.GetByIdAsync(second.Id);

// persisted.Should().NotBeNull();
// persisted!.NormalizedValue.Should().Be("SECOND@EXAMPLE.COM");
//}
}
Loading