< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Data.Persistence.ApplicationCredentialSlots
Assembly: ArturRios.Heimdall.Data
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Infrastructure/ArturRios.Heimdall.Data/Persistence/ApplicationCredentialSlots.cs
Line coverage
100%
Covered lines: 28
Uncovered lines: 0
Coverable lines: 28
Total lines: 54
Line coverage: 100%
Branch coverage
75%
Covered branches: 6
Total branches: 8
Branch coverage: 75%
Method coverage
100%
Covered methods: 3
Fully covered methods: 3
Total methods: 3
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
TryAddWithinLimitAsync()100%11100%
<TryAddWithinLimitAsync()75%88100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Infrastructure/ArturRios.Heimdall.Data/Persistence/ApplicationCredentialSlots.cs

#LineLine coverage
 1using ArturRios.Heimdall.Data.Configuration;
 2using ArturRios.Heimdall.Domain.Entities;
 3using ArturRios.Heimdall.Domain.Persistence;
 4using Microsoft.EntityFrameworkCore;
 5
 6namespace ArturRios.Heimdall.Data.Persistence;
 7
 8/// <summary>
 9///     <see cref="IApplicationCredentialSlots" /> on PostgreSQL: a transaction that locks the
 10///     application's row, counts its live credentials, and inserts only below the limit.
 11/// </summary>
 12/// <remarks>
 13///     A count followed by an insert is a race on its own — two requests both count one, both
 14///     insert, and the application holds three live secrets. <c>SELECT … FOR UPDATE</c> on the
 15///     application's row makes a second issuer for the same application wait until the first commits,
 16///     and then count the row the first inserted. Other applications are not blocked. Unlike
 17///     <see cref="AtomicWrites" />, this needs a transaction, because the rule spans rows a single
 18///     conditional <c>UPDATE</c> cannot see.
 19/// </remarks>
 2020public sealed class ApplicationCredentialSlots(AppDbContext context) : IApplicationCredentialSlots
 21{
 22    public async Task<bool> TryAddWithinLimitAsync(ApplicationCredential credential, int maxLive, DateTime now)
 23    {
 24        // The table name comes from the model, never from input, so building it into the SQL is safe;
 25        // the id stays a parameter.
 1626        var applicationTable = context.Model.FindEntityType(typeof(Application))!.GetSchemaQualifiedTableName();
 1627        var strategy = context.Database.CreateExecutionStrategy();
 28
 1629        return await strategy.ExecuteAsync(async () =>
 1630        {
 1631            await using var transaction = await context.Database.BeginTransactionAsync();
 1632
 1633            var sql = $"SELECT 1 FROM {applicationTable} WHERE id = {{0}} FOR UPDATE";
 1634            await context.Database.ExecuteSqlRawAsync(sql, credential.ApplicationId);
 1635
 1636            var live = await context.ApplicationCredentials
 1637                .Where(x => x.ApplicationId == credential.ApplicationId)
 1638                .CountAsync(ApplicationCredential.LiveAt(now));
 1639
 1640            if (live >= maxLive)
 1641            {
 742                await transaction.RollbackAsync();
 1643
 744                return false;
 1645            }
 1646
 947            context.ApplicationCredentials.Add(credential);
 948            await context.SaveChangesAsync();
 949            await transaction.CommitAsync();
 1650
 951            return true;
 1652        });
 3253    }
 54}