< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Data.Seeding.DatabaseSeeder
Assembly: ArturRios.Heimdall.Data
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Infrastructure/ArturRios.Heimdall.Data/Seeding/DatabaseSeeder.cs
Line coverage
79%
Covered lines: 46
Uncovered lines: 12
Coverable lines: 58
Total lines: 132
Line coverage: 79.3%
Branch coverage
65%
Covered branches: 13
Total branches: 20
Branch coverage: 65%
Method coverage
100%
Covered methods: 6
Fully covered methods: 3
Total methods: 6
Method coverage: 100%
Full method coverage: 50%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
SeedAsync()100%11100%
EnsureSchemaIsUpToDateAsync()50%3244.44%
EnsureRolesAsync()75%9878.57%
EnsureSystemAdminAsync()75%4483.33%
DescriptionOf(...)50%66100%

File(s)

/home/ci/agent/workspace/heimdall-api-docs_main/app/src/Infrastructure/ArturRios.Heimdall.Data/Seeding/DatabaseSeeder.cs

#LineLine coverage
 1using System.ComponentModel;
 2using System.Reflection;
 3using ArturRios.Heimdall.Command.Services;
 4using ArturRios.Heimdall.Data.Configuration;
 5using ArturRios.Heimdall.Domain.Entities;
 6using ArturRios.Heimdall.Domain.Enums;
 7using ArturRios.Util.Hashing;
 8using Microsoft.EntityFrameworkCore;
 9using Microsoft.Extensions.Logging;
 10
 11namespace ArturRios.Heimdall.Data.Seeding;
 12
 13/// <summary>
 14///     Brings a migrated database to the state the application assumes: every <see cref="Roles" />
 15///     member present as a row, and at least one system administrator to sign in as. Idempotent, so
 16///     it runs on every startup. It never applies migrations — that is <c>scripts/migrations.py</c>'s
 17///     job — and refuses to seed a schema that is behind.
 18/// </summary>
 97019public class DatabaseSeeder(
 97020    AppDbContext context,
 97021    MasterUserOptions masterUser,
 97022    ILogger<DatabaseSeeder> logger)
 23{
 24    public async Task SeedAsync(CancellationToken cancellationToken = default)
 25    {
 97026        await EnsureSchemaIsUpToDateAsync(cancellationToken);
 97027        await EnsureRolesAsync(cancellationToken);
 97028        await EnsureSystemAdminAsync(cancellationToken);
 97029    }
 30
 31    private async Task EnsureSchemaIsUpToDateAsync(CancellationToken cancellationToken)
 32    {
 97033        var pending = (await context.Database.GetPendingMigrationsAsync(cancellationToken)).ToList();
 34
 97035        if (pending.Count == 0)
 36        {
 97037            return;
 38        }
 39
 040        var names = string.Join(", ", pending);
 41
 042        logger.LogCritical("Database is behind by {Count} migration(s): {Migrations}", pending.Count, names);
 43
 044        throw new InvalidOperationException(
 045            $"The database is missing {pending.Count} migration(s): {names}. Apply them with " +
 046            "scripts/migrations.py before starting the API.");
 97047    }
 48
 49    private async Task EnsureRolesAsync(CancellationToken cancellationToken)
 50    {
 387151        var stored = await context.Roles.ToDictionaryAsync(x => x.Id, cancellationToken);
 52
 776053        foreach (var role in Enum.GetValues<Roles>())
 54        {
 291055            var id = (long)role;
 291056            var name = role.ToString();
 291057            var description = DescriptionOf(role);
 58
 291059            if (!stored.TryGetValue(id, out var existing))
 60            {
 961                context.Roles.Add(new Role { Id = id, Name = name, Description = description });
 62
 963                logger.LogInformation("Seeding role {RoleName} with id {RoleId}", name, id);
 64
 965                continue;
 66            }
 67
 290168            if (existing.Name == name && existing.Description == description)
 69            {
 70                continue;
 71            }
 72
 073            existing.Name = name;
 074            existing.Description = description;
 75
 076            logger.LogInformation("Realigning role {RoleId} with the Roles enum", id);
 77        }
 78
 97079        await context.SaveChangesAsync(cancellationToken);
 97080    }
 81
 82    private async Task EnsureSystemAdminAsync(CancellationToken cancellationToken)
 83    {
 84        const long systemAdminRoleId = (long)Roles.SystemAdmin;
 85
 97086        var exists = await context.Persons
 97087            .AnyAsync(x => x.RoleId == systemAdminRoleId && !x.IsDeleted, cancellationToken);
 88
 97089        if (exists)
 90        {
 96791            return;
 92        }
 93
 394        if (!masterUser.IsComplete)
 95        {
 096            throw new InvalidOperationException(
 097                "The database has no system administrator and the master user is not configured. Set " +
 098                $"{MasterUserOptions.NameVariable}, {MasterUserOptions.EmailVariable} and " +
 099                $"{MasterUserOptions.PasswordVariable} before starting the API.");
 100        }
 101
 3102        var passwordHash = Hash.EncodeWithRandomSalt(masterUser.Password, out var salt, PasswordHashing.Configuration);
 103
 3104        var master = new Person
 3105        {
 3106            Name = masterUser.Name,
 3107            Email = masterUser.Email,
 3108            PasswordHash = passwordHash,
 3109            Salt = salt,
 3110            EmailVerified = true,
 3111            RoleId = systemAdminRoleId
 3112        };
 113
 114        // NFR-23: the master is an identity like any other, and the one that exists before any
 115        // handler can run, so the seeder has to record its basis itself. The master belongs to no
 116        // scope, so the default basis applies.
 3117        LegalBasisRecorder.Record(master, null, DateTime.UtcNow);
 118
 3119        context.Persons.Add(master);
 120
 3121        await context.SaveChangesAsync(cancellationToken);
 122
 123        // The address is deliberately not logged (NFR-22). It came from the environment the
 124        // operator set, so the line tells them nothing they do not already know, and it would
 125        // otherwise be written on every start-up for the life of the deployment.
 3126        logger.LogInformation("Seeded the master system administrator");
 970127    }
 128
 129    private static string DescriptionOf(Roles role) =>
 2910130        typeof(Roles).GetField(role.ToString())?.GetCustomAttribute<DescriptionAttribute>()?.Description
 2910131        ?? role.ToString();
 132}