Bifrost/tests/Bifrost.Web.Tests/PlatformEndpointsTests.cs

723 lines
30 KiB
C#

using System.Net;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Bifrost.Web.Features.Patronage;
using Bifrost.Web.Data;
using Bifrost.Web.Domain;
using Bifrost.Web.Features.Membership;
using Bifrost.Web.Tests.Support;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Bifrost.Web.Tests;
public sealed class PlatformEndpointsTests : IClassFixture<TestWebApplicationFactory>
{
private readonly TestWebApplicationFactory _factory;
public PlatformEndpointsTests(TestWebApplicationFactory factory)
{
_factory = factory;
}
[Fact]
public async Task Health_endpoint_returns_ok()
{
using var client = _factory.CreateClient();
using var response = await client.GetAsync("/healthz");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task Ready_endpoint_returns_ok_for_test_configuration()
{
using var client = _factory.CreateClient();
using var response = await client.GetAsync("/readyz");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task Bifrost_native_registration_creates_authenticated_identity_without_oauth()
{
using var client = _factory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "/auth/bifrost/register");
request.Headers.Add("X-Test-Anonymous", "true");
request.Content = JsonContent.Create(new
{
identity = $"native-{Guid.NewGuid():N}",
displayName = "Native Bifrost User",
returnUrl = "/App"
});
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<JsonElement>();
var identity = payload.GetProperty("bifrostIdentity").GetString();
Assert.False(string.IsNullOrWhiteSpace(identity));
Assert.Equal("Authenticated", payload.GetProperty("membershipStatus").GetString());
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var userAccount = dbContext.UserAccounts
.Include(x => x.Membership)
.Single(x => x.BifrostIdentity == identity);
Assert.Null(userAccount.GitHubUserId);
Assert.Equal(string.Empty, userAccount.HeimdallAccountId);
Assert.Equal(MembershipStatus.Authenticated, userAccount.Membership!.Status);
}
[Fact]
public async Task Bifrost_native_registration_rejects_duplicate_identity()
{
using var client = _factory.CreateClient();
var identity = $"native-{Guid.NewGuid():N}";
using var firstRequest = new HttpRequestMessage(HttpMethod.Post, "/auth/bifrost/register");
firstRequest.Headers.Add("X-Test-Anonymous", "true");
firstRequest.Content = JsonContent.Create(new { identity, displayName = "First" });
using var firstResponse = await client.SendAsync(firstRequest);
using var secondRequest = new HttpRequestMessage(HttpMethod.Post, "/auth/bifrost/register");
secondRequest.Headers.Add("X-Test-Anonymous", "true");
secondRequest.Content = JsonContent.Create(new { identity = identity.ToUpperInvariant(), displayName = "Second" });
using var secondResponse = await client.SendAsync(secondRequest);
Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode);
Assert.Equal(HttpStatusCode.BadRequest, secondResponse.StatusCode);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
Assert.Single(dbContext.UserAccounts.Where(x => x.NormalizedBifrostIdentity == identity));
}
[Fact]
public async Task GitHub_issue_webhook_creates_work_item()
{
using var client = _factory.CreateClient();
_ = await client.GetAsync("/App");
const string payload = """
{
"action": "opened",
"repository": { "full_name": "GameCult/Bifrost" },
"issue": {
"number": 42,
"state": "open",
"html_url": "https://github.com/GameCult/Bifrost/issues/42",
"title": "Sync issue into Bifrost",
"body": "Track this from GitHub"
},
"sender": { "login": "test-admin" }
}
""";
using var request = new HttpRequestMessage(HttpMethod.Post, "/github/webhooks");
request.Headers.Add("X-GitHub-Event", "issues");
request.Headers.Add("X-GitHub-Delivery", Guid.NewGuid().ToString("N"));
request.Headers.Add("X-Hub-Signature-256", ComputeSignature("test-webhook-secret", payload));
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var workItem = dbContext.WorkItems.Single();
var issueLink = dbContext.GitHubIssueLinks.Single();
var project = dbContext.Projects.Single(x => x.Id == workItem.ProjectId);
Assert.Equal(WorkItemSourceType.GitHubIssue, workItem.SourceType);
Assert.Equal(42, issueLink.IssueNumber);
Assert.Equal("bifrost", project.Slug);
Assert.Equal("GameCult/Bifrost".ToLowerInvariant(), project.GitHubRepository);
}
[Fact]
public async Task Heimdall_paypal_support_event_records_patron_support_and_is_idempotent()
{
using var client = _factory.CreateClient();
var now = DateTimeOffset.UtcNow;
var heimdallAccountId = $"heimdall-paypal-{Guid.NewGuid():N}";
var providerEventId = $"WH-{Guid.NewGuid():N}";
Guid patronUserAccountId;
using (var scope = _factory.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var patron = new UserAccount
{
HeimdallAccountId = heimdallAccountId,
DisplayName = "PayPal Patron",
GitHubLogin = $"paypal-{Guid.NewGuid():N}",
NormalizedGitHubLogin = $"paypal-{Guid.NewGuid():N}",
CreatedAtUtc = now,
LastSeenAtUtc = now
};
patronUserAccountId = patron.Id;
patron.Membership = new Membership
{
UserAccountId = patron.Id,
Status = MembershipStatus.Active,
CreatedAtUtc = now
};
dbContext.UserAccounts.Add(patron);
await dbContext.SaveChangesAsync();
}
var payload = JsonSerializer.Serialize(new
{
heimdallAccountId,
provider = "PayPal",
providerEventId,
kind = "OneTimeDonation",
amount = 125m,
currencyCode = "USD",
externalSupportId = "PAYMENT.CAPTURE.COMPLETED:CAPTURE-1",
supportedAtUtc = now,
isCurrentRecurringSupport = false,
providerPayerId = "PAYER-1",
notes = "Verified PayPal checkout capture from Heimdall."
});
using var firstRequest = CreateSignedHeimdallRequest(payload);
using var firstResponse = await client.SendAsync(firstRequest);
using var secondRequest = CreateSignedHeimdallRequest(payload);
using var secondResponse = await client.SendAsync(secondRequest);
Assert.Equal(HttpStatusCode.Accepted, firstResponse.StatusCode);
Assert.Equal(HttpStatusCode.Accepted, secondResponse.StatusCode);
using var verificationScope = _factory.Services.CreateScope();
var verificationContext = verificationScope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var supportEvents = verificationContext.PatronSupportEvents
.Where(x => x.Provider == ExternalPatronProvider.PayPal && x.ProviderEventId == providerEventId)
.ToList();
var tierSnapshot = verificationContext.TierSnapshots
.Single(x => x.Kind == TierSnapshotKind.Patron &&
x.IsCurrent &&
x.Membership.UserAccountId == patronUserAccountId);
Assert.Single(supportEvents);
Assert.Equal("PAYMENT.CAPTURE.COMPLETED:CAPTURE-1", supportEvents.Single().ExternalSupportId);
Assert.Equal("Patron Silver", tierSnapshot.Label);
Assert.Equal(2m, tierSnapshot.Weight);
}
[Fact]
public async Task Heimdall_patreon_support_event_records_current_patronage()
{
using var client = _factory.CreateClient();
var now = DateTimeOffset.UtcNow;
var heimdallAccountId = $"heimdall-patreon-{Guid.NewGuid():N}";
var providerEventId = $"patreon-member-sync-{Guid.NewGuid():N}";
Guid patronUserAccountId;
using (var scope = _factory.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var patron = new UserAccount
{
HeimdallAccountId = heimdallAccountId,
DisplayName = "Patreon Patron",
GitHubLogin = $"patreon-{Guid.NewGuid():N}",
NormalizedGitHubLogin = $"patreon-{Guid.NewGuid():N}",
CreatedAtUtc = now,
LastSeenAtUtc = now
};
patronUserAccountId = patron.Id;
patron.Membership = new Membership
{
UserAccountId = patron.Id,
Status = MembershipStatus.Active,
CreatedAtUtc = now
};
dbContext.UserAccounts.Add(patron);
await dbContext.SaveChangesAsync();
}
var payload = JsonSerializer.Serialize(new
{
heimdallAccountId,
provider = "Patreon",
providerEventId,
kind = "RecurringSupportSnapshot",
amount = 125m,
currencyCode = "USD",
externalSupportId = "patreon:campaign:member-current-support",
supportedAtUtc = now,
isCurrentRecurringSupport = true,
providerPayerId = "patreon-user-1",
providerSubscriptionId = "patreon-member-1",
notes = "Verified current Patreon entitlement from Heimdall."
});
using var request = CreateSignedHeimdallRequest(payload);
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
using var verificationScope = _factory.Services.CreateScope();
var verificationContext = verificationScope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var supportEvent = verificationContext.PatronSupportEvents
.Single(x => x.Provider == ExternalPatronProvider.Patreon && x.ProviderEventId == providerEventId);
var pointTransaction = verificationContext.PointTransactions
.Single(x => x.UserAccountId == patronUserAccountId);
var tierSnapshot = verificationContext.TierSnapshots
.Single(x => x.Kind == TierSnapshotKind.Patron &&
x.IsCurrent &&
x.Membership.UserAccountId == patronUserAccountId);
Assert.Equal(patronUserAccountId, supportEvent.UserAccountId);
Assert.Equal(PatronSupportEventKind.RecurringSupportSnapshot, supportEvent.Kind);
Assert.True(supportEvent.IsCurrentRecurringSupport);
Assert.Equal("patreon-user-1", supportEvent.ProviderPayerId);
Assert.Equal("patreon-member-1", supportEvent.ProviderSubscriptionId);
Assert.Equal(PointTransactionType.PatronSupport, pointTransaction.Type);
Assert.False(pointTransaction.IsDecaying);
Assert.Equal("Patron Silver", tierSnapshot.Label);
Assert.Equal(2m, tierSnapshot.Weight);
Assert.Contains(verificationContext.AuditEvents, x => x.Action == "patron-support.recorded");
Assert.Contains(verificationContext.AuditEvents, x => x.Action == "patron-tier.derived");
}
[Fact]
public async Task Heimdall_paypal_adjustment_reduces_derived_patron_tier()
{
using var client = _factory.CreateClient();
var now = DateTimeOffset.UtcNow;
var heimdallAccountId = $"heimdall-refund-{Guid.NewGuid():N}";
Guid patronUserAccountId;
using (var scope = _factory.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var patron = new UserAccount
{
HeimdallAccountId = heimdallAccountId,
DisplayName = "Refunded Patron",
GitHubLogin = $"refund-{Guid.NewGuid():N}",
NormalizedGitHubLogin = $"refund-{Guid.NewGuid():N}",
CreatedAtUtc = now,
LastSeenAtUtc = now
};
patronUserAccountId = patron.Id;
patron.Membership = new Membership
{
UserAccountId = patron.Id,
Status = MembershipStatus.Active,
CreatedAtUtc = now
};
dbContext.UserAccounts.Add(patron);
await dbContext.SaveChangesAsync();
}
var donationPayload = JsonSerializer.Serialize(new
{
heimdallAccountId,
provider = "PayPal",
providerEventId = $"WH-{Guid.NewGuid():N}",
kind = "OneTimeDonation",
amount = 125m,
currencyCode = "USD",
externalSupportId = "PAYMENT.CAPTURE.COMPLETED:CAPTURE-2",
supportedAtUtc = now,
isCurrentRecurringSupport = false
});
var refundPayload = JsonSerializer.Serialize(new
{
heimdallAccountId,
provider = "PayPal",
providerEventId = $"WH-{Guid.NewGuid():N}",
kind = "SupportAdjustment",
amount = -125m,
currencyCode = "USD",
externalSupportId = "PAYMENT.CAPTURE.REFUNDED:CAPTURE-2",
supportedAtUtc = now.AddMinutes(1),
isCurrentRecurringSupport = false,
notes = "Verified PayPal refund from Heimdall."
});
using var donationRequest = CreateSignedHeimdallRequest(donationPayload);
using var donationResponse = await client.SendAsync(donationRequest);
using var refundRequest = CreateSignedHeimdallRequest(refundPayload);
using var refundResponse = await client.SendAsync(refundRequest);
Assert.Equal(HttpStatusCode.Accepted, donationResponse.StatusCode);
Assert.Equal(HttpStatusCode.Accepted, refundResponse.StatusCode);
using var verificationScope = _factory.Services.CreateScope();
var verificationContext = verificationScope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var currentPatronTiers = verificationContext.TierSnapshots
.Where(x => x.Kind == TierSnapshotKind.Patron &&
x.IsCurrent &&
x.Membership.UserAccountId == patronUserAccountId)
.ToList();
Assert.Empty(currentPatronTiers);
}
[Fact]
public async Task Velvet_patronage_checkout_redirects_to_stripe_checkout()
{
using var client = _factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
await EnsureVelvetProjectAsync(client);
using var response = await client.GetAsync("/patronage/velvet/checkout?amountCents=1900&item=velvet-room&project=velvet");
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("https://checkout.stripe.com/c/pay/cs_test_velvet", response.Headers.Location?.ToString());
}
[Fact]
public async Task Velvet_patronage_checkout_requires_bifrost_account()
{
using var client = _factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
await EnsureVelvetProjectAsync(client);
using var request = new HttpRequestMessage(HttpMethod.Get, "/patronage/velvet/checkout?amountCents=1900&item=velvet-room&project=velvet");
request.Headers.Add("X-Test-Anonymous", "1");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal(
"/auth/sign-in?returnUrl=%2Fpatronage%2Fvelvet%2Fcheckout%3FamountCents%3D1900%26item%3Dvelvet-room%26project%3Dvelvet",
response.Headers.Location?.ToString());
}
[Fact]
public async Task Velvet_patronage_checkout_rejects_out_of_policy_amount()
{
using var client = _factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
await EnsureVelvetProjectAsync(client);
using var response = await client.GetAsync("/patronage/velvet/checkout?amountCents=50&item=velvet-room&project=velvet");
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task Sign_in_chooser_lists_supported_transport_providers()
{
using var client = _factory.CreateClient();
using var response = await client.GetAsync("/auth/sign-in?returnUrl=%2Fpatronage%2Fvelvet%2Fcheckout%3FamountCents%3D1900%26item%3Dvelvet-room%26project%3Dvelvet");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var html = await response.Content.ReadAsStringAsync();
Assert.Contains("GitHub", html);
Assert.Contains("/auth/heimdall/discord", html);
Assert.Contains("/auth/heimdall/patreon", html);
Assert.Contains("/auth/heimdall/twitch", html);
}
[Fact]
public async Task Stripe_checkout_completed_webhook_records_general_patronage()
{
using var client = _factory.CreateClient();
_ = await client.GetAsync("/App");
Guid patronageAccountId;
Guid projectId;
using (var setupScope = _factory.Services.CreateScope())
{
var setupContext = setupScope.ServiceProvider.GetRequiredService<BifrostDbContext>();
patronageAccountId = setupContext.UserAccounts.Single(x => x.NormalizedGitHubLogin == "TEST-ADMIN").Id;
projectId = await EnsureVelvetProjectAsync(setupContext, patronageAccountId);
}
var created = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var payload = JsonSerializer.Serialize(new
{
id = $"evt_{Guid.NewGuid():N}",
type = "checkout.session.completed",
created,
data = new
{
@object = new
{
id = "cs_test_velvet_paid",
customer = "cus_velvet_1",
currency = "usd",
amount_total = 3900,
payment_status = "paid",
metadata = new Dictionary<string, string>
{
["source"] = "velvet.gamecult.org",
["ledger"] = "bifrost",
["purpose"] = "general_patronage",
["project"] = "velvet",
["project_id"] = projectId.ToString("N"),
["item"] = "after-hours-bundle",
["model"] = "deru",
["bifrost_user_account_id"] = patronageAccountId.ToString("N"),
["bifrost_github_login"] = "test-admin"
}
}
}
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/patronage/stripe/webhook");
request.Headers.Add("Stripe-Signature", ComputeStripeSignature("whsec_test_webhook_secret", payload));
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var patronageAccount = dbContext.UserAccounts.Single(x => x.NormalizedGitHubLogin == "TEST-ADMIN");
var supportEvent = dbContext.PatronSupportEvents.Single(x => x.Provider == ExternalPatronProvider.Stripe);
Assert.Equal(patronageAccount.Id, supportEvent.UserAccountId);
Assert.Equal("cs_test_velvet_paid", supportEvent.ExternalSupportId);
Assert.Equal(projectId, supportEvent.ProjectId);
Assert.Equal(39m, supportEvent.Amount);
Assert.Equal("USD", supportEvent.CurrencyCode);
Assert.Contains("velvet", supportEvent.Notes);
Assert.Contains("after-hours-bundle", supportEvent.Notes);
Assert.Contains(dbContext.AuditEvents, x => x.Action == "patron-support.recorded");
}
[Fact]
public async Task Eve_governance_surface_returns_motion_verse()
{
using var client = _factory.CreateClient();
using var response = await client.GetAsync("/eve/governance/surface");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var json = await response.Content.ReadAsStringAsync();
Assert.Contains("gamecult.eve.surface.v1", json);
Assert.Contains("bifrost-motion-verse", json);
Assert.Contains("motion.create", json);
Assert.Contains("cultmesh://asgard.starfire.bifrost/commands/motion", json);
Assert.DoesNotContain("/eve/governance/commands", json);
}
[Fact]
public async Task Eve_governance_http_command_route_is_not_mapped()
{
using var client = _factory.CreateClient();
_ = await client.GetAsync("/App");
Guid motionId;
using (var setupScope = _factory.Services.CreateScope())
{
var dbContext = setupScope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var actor = dbContext.UserAccounts.Single(x => x.NormalizedGitHubLogin == "TEST-ADMIN");
var motion = new Motion
{
CreatedByUserAccountId = actor.Id,
Scope = MotionScope.Management,
Category = MotionCategory.Features,
Title = "Govern through Eve",
Summary = "The Motion Verse should command the canonical motion path.",
ApprovalThreshold = 0.50m,
OpensAtUtc = DateTimeOffset.UtcNow,
ClosesAtUtc = DateTimeOffset.UtcNow.AddDays(7),
CreatedAtUtc = DateTimeOffset.UtcNow
};
dbContext.Motions.Add(motion);
await dbContext.SaveChangesAsync();
motionId = motion.Id;
}
var payload = JsonSerializer.Serialize(new
{
command = "motion.vote",
motionId,
choice = "For",
comment = "Cast from Eve."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/eve/governance/commands");
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
using var verificationScope = _factory.Services.CreateScope();
var verificationContext = verificationScope.ServiceProvider.GetRequiredService<BifrostDbContext>();
Assert.Empty(verificationContext.Votes.Where(x => x.MotionId == motionId));
Assert.DoesNotContain(verificationContext.AuditEvents, x => x.Action == "motion.voted");
}
[Theory]
[InlineData("/bridge/actions/request")]
[InlineData("/bridge/actions/action-123/start")]
[InlineData("/bridge/actions/action-123/complete")]
[InlineData("/dispatch/runs/start")]
[InlineData("/dispatch/runs/run-123/complete")]
[InlineData("/dispatch/runs/run-123/fail")]
[InlineData("/transport/receipts")]
[InlineData("/governance/receipts")]
public async Task Removed_http_bridge_routes_are_not_mapped(string path)
{
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var bridgeActions = await dbContext.BridgeActions.CountAsync();
var dispatchRuns = await dbContext.AgentDispatchRuns.CountAsync();
var transportReceipts = await dbContext.AgentTransportReceipts.CountAsync();
var governanceReceipts = await dbContext.GovernanceActivityReceipts.CountAsync();
using var client = _factory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Post, path);
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent("{}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
Assert.Equal(bridgeActions, await dbContext.BridgeActions.CountAsync());
Assert.Equal(dispatchRuns, await dbContext.AgentDispatchRuns.CountAsync());
Assert.Equal(transportReceipts, await dbContext.AgentTransportReceipts.CountAsync());
Assert.Equal(governanceReceipts, await dbContext.GovernanceActivityReceipts.CountAsync());
}
private static string ComputeSignature(string secret, string payload)
{
using var hasher = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hasher.ComputeHash(Encoding.UTF8.GetBytes(payload));
return $"sha256={Convert.ToHexString(hash).ToLowerInvariant()}";
}
private static string ComputeStripeSignature(string secret, string payload)
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
using var hasher = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var signature = Convert.ToHexString(
hasher.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{payload}"))).ToLowerInvariant();
return $"t={timestamp},v1={signature}";
}
private async Task<Guid> EnsureVelvetProjectAsync(HttpClient client)
{
_ = await client.GetAsync("/App");
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var owner = await dbContext.UserAccounts.SingleAsync(x => x.NormalizedGitHubLogin == "TEST-ADMIN");
return await EnsureVelvetProjectAsync(dbContext, owner.Id);
}
private static async Task<Guid> EnsureVelvetProjectAsync(BifrostDbContext dbContext, Guid ownerUserAccountId)
{
var existing = await dbContext.Projects
.Where(x => x.Slug == "velvet")
.OrderBy(x => x.CreatedAtUtc)
.FirstOrDefaultAsync();
if (existing is not null)
{
if (existing.Status != ProjectStatus.Active)
{
existing.Status = ProjectStatus.Active;
await dbContext.SaveChangesAsync();
}
return existing.Id;
}
var now = DateTimeOffset.UtcNow;
var project = new Project
{
OwnerUserAccountId = ownerUserAccountId,
Slug = "velvet",
Name = "Velvet",
Summary = "General patronage for Velvet publication.",
Status = ProjectStatus.Active,
CreatedAtUtc = now,
UpdatedAtUtc = now
};
dbContext.Projects.Add(project);
await dbContext.SaveChangesAsync();
return project.Id;
}
private static HttpRequestMessage CreateSignedHeimdallRequest(string payload)
{
var request = new HttpRequestMessage(HttpMethod.Post, "/heimdall/patron-support/events");
request.Headers.Add("X-Heimdall-Signature-256", ComputeSignature("test-heimdall-intake-secret", payload));
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
return request;
}
private async Task EnsureBifrostIdentityRegisteredAsync(string identity, string heimdallAccountId = "")
{
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var normalizedIdentity = BifrostIdentityService.NormalizeIdentity(identity);
if (await dbContext.UserAccounts.AnyAsync(x => x.NormalizedBifrostIdentity == normalizedIdentity))
{
return;
}
var now = DateTimeOffset.UtcNow;
dbContext.UserAccounts.Add(new UserAccount
{
BifrostIdentity = normalizedIdentity,
NormalizedBifrostIdentity = normalizedIdentity,
HeimdallAccountId = heimdallAccountId,
DisplayName = normalizedIdentity,
CreatedAtUtc = now,
LastSeenAtUtc = now,
Membership = new Bifrost.Web.Domain.Membership
{
Status = MembershipStatus.Authenticated,
CreatedAtUtc = now,
Notes = "Registered test Bifrost identity"
}
});
await dbContext.SaveChangesAsync();
}
private sealed class BridgeActionHttpResult
{
public Guid Id { get; init; }
public string Status { get; init; } = string.Empty;
}
private sealed class AgentDispatchRunHttpResult
{
public Guid Id { get; init; }
public string Status { get; init; } = string.Empty;
}
private sealed class AgentTransportReceiptHttpResult
{
public Guid Id { get; init; }
public string RequestId { get; init; } = string.Empty;
public string ActivityKind { get; init; } = string.Empty;
}
private sealed class GovernanceActivityReceiptHttpResult
{
public Guid Id { get; init; }
public string TopicId { get; init; } = string.Empty;
public string ActivityKind { get; init; } = string.Empty;
}
}