Bifrost/tests/Bifrost.Web.Tests/PlatformEndpointsTests.cs
2026-06-22 15:07:22 +01:00

1391 lines
59 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.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_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
});
using var response = await client.GetAsync("/patronage/velvet/checkout?tier=velvet-room");
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
});
using var request = new HttpRequestMessage(HttpMethod.Get, "/patronage/velvet/checkout?tier=velvet-room");
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%3Ftier%3Dvelvet-room",
response.Headers.Location?.ToString());
}
[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%3Ftier%3Dvelvet-room");
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;
using (var setupScope = _factory.Services.CreateScope())
{
var setupContext = setupScope.ServiceProvider.GetRequiredService<BifrostDbContext>();
patronageAccountId = setupContext.UserAccounts.Single(x => x.NormalizedGitHubLogin == "TEST-ADMIN").Id;
}
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",
["model"] = "deru",
["tier"] = "after-hours-bundle",
["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(39m, supportEvent.Amount);
Assert.Equal("USD", supportEvent.CurrencyCode);
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("/eve/governance/commands", json);
}
[Fact]
public async Task Eve_governance_vote_command_uses_canonical_motion_commit_path()
{
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.OK, response.StatusCode);
var surfaceJson = await response.Content.ReadAsStringAsync();
Assert.Contains("bifrost-motion-verse", surfaceJson);
using var verificationScope = _factory.Services.CreateScope();
var verificationContext = verificationScope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var vote = verificationContext.Votes.Single(x => x.MotionId == motionId);
Assert.Equal(VoteChoice.For, vote.Choice);
Assert.Equal("Cast from Eve.", vote.Comment);
Assert.Contains(verificationContext.AuditEvents, x => x.Action == "motion.voted");
}
[Fact]
public async Task Local_bridge_token_can_authorize_and_receipt_agent_github_action()
{
using var client = _factory.CreateClient();
await PostTransportReceiptAsync(client, new
{
requestId = "req_bridge_123",
title = "Queue the GitHub bridge hardening pass",
targetRepoName = "Bifrost",
targetRepositoryFullName = "GameCult/Bifrost",
targetAgentIdentity = "nibu",
activityKind = "Claimed",
status = "claimed",
actorName = "bifrost-dispatcher",
note = "Request claimed for Bifrost."
});
var requestPayload = JsonSerializer.Serialize(new
{
actorKind = "Agent",
actorName = "nibu",
targetSurface = "GitHub",
actionKind = "GitHubDraftPullRequest",
targetRepositoryFullName = "GameCult/Bifrost",
targetLocator = "pulls",
sourceKind = "bifrost_agent_transport_request",
sourceId = "req_bridge_123",
authorityReference = "dispatch-approved",
title = "Draft motion implementation PR",
summary = "Open a draft PR for the approved topic."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/bridge/actions/request");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(requestPayload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
var action = await response.Content.ReadFromJsonAsync<BridgeActionHttpResult>();
Assert.NotNull(action);
Assert.Equal("Authorized", action.Status);
using var startRequest = new HttpRequestMessage(HttpMethod.Post, $"/bridge/actions/{action.Id}/start");
startRequest.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
using var startResponse = await client.SendAsync(startRequest);
Assert.Equal(HttpStatusCode.Accepted, startResponse.StatusCode);
var completionPayload = JsonSerializer.Serialize(new
{
receiptUrl = "https://github.com/GameCult/Bifrost/pull/99",
externalReceiptId = "99",
receiptPayload = "{\"pr\":99}"
});
using var completeRequest = new HttpRequestMessage(HttpMethod.Post, $"/bridge/actions/{action.Id}/complete");
completeRequest.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
completeRequest.Content = new StringContent(completionPayload, Encoding.UTF8, "application/json");
using var completeResponse = await client.SendAsync(completeRequest);
Assert.Equal(HttpStatusCode.OK, completeResponse.StatusCode);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var savedAction = dbContext.BridgeActions.Single(x => x.Id == action.Id);
Assert.Equal(BridgeActionStatus.Completed, savedAction.Status);
Assert.Equal("https://github.com/GameCult/Bifrost/pull/99", savedAction.ReceiptUrl);
Assert.Equal("99", savedAction.ExternalReceiptId);
Assert.Contains(dbContext.AuditEvents, x => x.EntityType == nameof(BridgeAction) && x.Action == "bridge.completed");
}
[Fact]
public async Task Agent_github_action_without_provenance_is_denied_and_recorded()
{
using var client = _factory.CreateClient();
var requestPayload = JsonSerializer.Serialize(new
{
actorKind = "Agent",
actorName = "nibu",
targetSurface = "GitHub",
actionKind = "GitHubDraftPullRequest",
targetRepositoryFullName = "GameCult/Bifrost",
targetLocator = "pulls",
title = "Unproven draft",
summary = "This should not pass policy."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/bridge/actions/request");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(requestPayload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
var action = await response.Content.ReadFromJsonAsync<BridgeActionHttpResult>();
Assert.NotNull(action);
Assert.Equal("Denied", action.Status);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var savedAction = dbContext.BridgeActions.Single(x => x.Id == action.Id);
Assert.Equal(BridgeActionStatus.Denied, savedAction.Status);
Assert.Contains("must cite", savedAction.PolicyDecision, StringComparison.OrdinalIgnoreCase);
Assert.Contains(dbContext.AuditEvents, x => x.EntityType == nameof(BridgeAction) && x.Action == "bridge.denied");
}
[Fact]
public async Task Agent_github_action_with_unknown_bifrost_request_is_denied_and_recorded()
{
using var client = _factory.CreateClient();
var requestPayload = JsonSerializer.Serialize(new
{
actorKind = "Agent",
actorName = "nibu",
targetSurface = "GitHub",
actionKind = "GitHubDraftPullRequest",
targetRepositoryFullName = "GameCult/Bifrost",
targetLocator = "pulls",
sourceKind = "bifrost_agent_transport_request",
sourceId = "req_missing_123",
authorityReference = "dispatch-approved",
title = "Unbacked request bridge action",
summary = "This should not pass policy."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/bridge/actions/request");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(requestPayload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
var action = await response.Content.ReadFromJsonAsync<BridgeActionHttpResult>();
Assert.NotNull(action);
Assert.Equal("Denied", action.Status);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var savedAction = dbContext.BridgeActions.Single(x => x.Id == action.Id);
Assert.Equal(BridgeActionStatus.Denied, savedAction.Status);
Assert.Contains("unknown request", savedAction.PolicyDecision, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Agent_github_action_with_mismatched_bifrost_request_repo_is_denied_and_recorded()
{
using var client = _factory.CreateClient();
await PostTransportReceiptAsync(client, new
{
requestId = "req_bridge_mismatch_123",
title = "Queue the VoidBot bridge hardening pass",
targetRepoName = "VoidBot",
targetRepositoryFullName = "GameCult/VoidBot",
targetAgentIdentity = "nibu",
activityKind = "Claimed",
status = "claimed",
actorName = "bifrost-dispatcher",
note = "Request claimed for VoidBot."
});
var requestPayload = JsonSerializer.Serialize(new
{
actorKind = "Agent",
actorName = "nibu",
targetSurface = "GitHub",
actionKind = "GitHubDraftPullRequest",
targetRepositoryFullName = "GameCult/Bifrost",
targetLocator = "pulls",
sourceKind = "bifrost_agent_transport_request",
sourceId = "req_bridge_mismatch_123",
authorityReference = "dispatch-approved",
title = "Repo mismatch bridge action",
summary = "This should not pass policy."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/bridge/actions/request");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(requestPayload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
var action = await response.Content.ReadFromJsonAsync<BridgeActionHttpResult>();
Assert.NotNull(action);
Assert.Equal("Denied", action.Status);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var savedAction = dbContext.BridgeActions.Single(x => x.Id == action.Id);
Assert.Equal(BridgeActionStatus.Denied, savedAction.Status);
Assert.Contains("target repository does not match", savedAction.PolicyDecision, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Agent_github_action_with_unapproved_governance_topic_is_denied_and_recorded()
{
using var client = _factory.CreateClient();
await PostGovernanceReceiptAsync(client, new
{
topicId = "topic_open_only_123",
commentId = "comment_open_only_123",
dispatchRequestId = string.Empty,
title = "Open topic only",
jurisdictionRepoName = "Bifrost",
jurisdictionAgentIdentity = "nibu",
activityKind = "TopicOpened",
actorKind = "face",
actorName = "nibu",
note = "Topic opened."
});
var requestPayload = JsonSerializer.Serialize(new
{
actorKind = "Agent",
actorName = "nibu",
targetSurface = "GitHub",
actionKind = "GitHubDraftPullRequest",
targetRepositoryFullName = "GameCult/Bifrost",
targetLocator = "pulls",
sourceKind = "governance_topic",
sourceId = "topic_open_only_123",
authorityReference = "dispatch-approved",
title = "Open-only topic bridge action",
summary = "This should not pass policy."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/bridge/actions/request");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(requestPayload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
var action = await response.Content.ReadFromJsonAsync<BridgeActionHttpResult>();
Assert.NotNull(action);
Assert.Equal("Denied", action.Status);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var savedAction = dbContext.BridgeActions.Single(x => x.Id == action.Id);
Assert.Equal(BridgeActionStatus.Denied, savedAction.Status);
Assert.Contains("not been approved or promoted", savedAction.PolicyDecision, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Agent_github_action_with_approved_governance_topic_is_authorized()
{
using var client = _factory.CreateClient();
await PostGovernanceReceiptAsync(client, new
{
topicId = "topic_approved_123",
commentId = "comment_approved_123",
dispatchRequestId = string.Empty,
title = "Approved governance topic",
jurisdictionRepoName = "Bifrost",
jurisdictionAgentIdentity = "nibu",
activityKind = "TopicApproved",
actorKind = "face",
actorName = "nibu",
note = "Topic approved."
});
var requestPayload = JsonSerializer.Serialize(new
{
actorKind = "Agent",
actorName = "nibu",
targetSurface = "GitHub",
actionKind = "GitHubDraftPullRequest",
targetRepositoryFullName = "GameCult/Bifrost",
targetLocator = "pulls",
sourceKind = "bifrost_governance_topic",
sourceId = "topic_approved_123",
authorityReference = "dispatch-approved",
title = "Approved topic bridge action",
summary = "This should pass policy."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/bridge/actions/request");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(requestPayload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
var action = await response.Content.ReadFromJsonAsync<BridgeActionHttpResult>();
Assert.NotNull(action);
Assert.Equal("Authorized", action.Status);
}
[Fact]
public async Task Persona_reddit_action_without_bifrost_identity_or_heimdall_reference_is_denied()
{
using var client = _factory.CreateClient();
var requestPayload = JsonSerializer.Serialize(new
{
actorKind = "Persona",
actorName = "Epiphany Persona",
targetSurface = "Reddit",
actionKind = "RedditPost",
targetLocator = "r/GameCultOrg",
sourceKind = "epiphany_persona_reddit",
sourceId = "persona-speech-audit-missing-identity",
authorityReference = "epiphany.persona_speech_audit",
title = "Persona Reddit thread",
summary = "This should not pass without Bifrost identity plus Heimdall reference."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/bridge/actions/request");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(requestPayload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
var action = await response.Content.ReadFromJsonAsync<BridgeActionHttpResult>();
Assert.NotNull(action);
Assert.Equal("Denied", action.Status);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var savedAction = dbContext.BridgeActions.Single(x => x.Id == action.Id);
Assert.Equal(BridgeActionStatus.Denied, savedAction.Status);
Assert.Contains("Bifrost identity", savedAction.PolicyDecision, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Persona_discord_action_requires_heimdall_reference_after_bifrost_identity()
{
using var client = _factory.CreateClient();
var requestPayload = JsonSerializer.Serialize(new
{
actorKind = "Persona",
actorName = "Epiphany Persona",
targetSurface = "Discord",
actionKind = "DiscordPost",
targetLocator = "channel/1501196543150264332",
sourceKind = "epiphany_persona_speech",
sourceId = "persona-speech-audit-missing-heimdall",
authorityReference = "epiphany.persona_speech_audit",
bifrostIdentity = "epiphany.Persona",
title = "Persona Discord post",
summary = "This should not pass without an outside-account capability reference."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/bridge/actions/request");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(requestPayload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
var action = await response.Content.ReadFromJsonAsync<BridgeActionHttpResult>();
Assert.NotNull(action);
Assert.Equal("Denied", action.Status);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var savedAction = dbContext.BridgeActions.Single(x => x.Id == action.Id);
Assert.Equal(BridgeActionStatus.Denied, savedAction.Status);
Assert.Contains("Heimdall", savedAction.PolicyDecision, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Persona_reddit_action_with_bifrost_identity_and_heimdall_reference_is_authorized_and_stored()
{
using var client = _factory.CreateClient();
var requestPayload = JsonSerializer.Serialize(new
{
actorKind = "Persona",
actorName = "Epiphany Persona",
targetSurface = "Reddit",
actionKind = "RedditPost",
targetLocator = "r/GameCultOrg",
sourceKind = "epiphany_persona_reddit",
sourceId = "persona-speech-audit-authorized",
authorityReference = "epiphany.persona_speech_audit",
bifrostIdentity = "epiphany.Persona",
heimdallCapabilityReference = "heimdall:reddit:capability:epiphany-persona",
epiphanyRunId = "epiphany-run-identity-gate",
epiphanyLaneId = "Persona",
epiphanyAgentIdentity = "epiphany.Persona",
title = "Persona Reddit thread",
summary = "This should pass with Bifrost identity and Heimdall reference."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/bridge/actions/request");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(requestPayload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
var action = await response.Content.ReadFromJsonAsync<BridgeActionHttpResult>();
Assert.NotNull(action);
Assert.Equal("Authorized", action.Status);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var savedAction = dbContext.BridgeActions.Single(x => x.Id == action.Id);
Assert.Equal(BridgeActionStatus.Authorized, savedAction.Status);
Assert.Equal("epiphany.Persona", savedAction.BifrostIdentity);
Assert.Equal("heimdall:reddit:capability:epiphany-persona", savedAction.HeimdallCapabilityReference);
Assert.Equal("epiphany-run-identity-gate", savedAction.EpiphanyRunId);
Assert.Equal("Persona", savedAction.EpiphanyLaneId);
Assert.Equal("epiphany.Persona", savedAction.EpiphanyAgentIdentity);
}
[Fact]
public async Task Local_bridge_token_can_record_dispatch_run_lifecycle()
{
using var client = _factory.CreateClient();
_ = await client.GetAsync("/App");
await PostTransportReceiptAsync(client, new
{
requestId = "req_dispatch_123",
title = "Queue the GitHub bridge hardening pass",
targetRepoName = "Bifrost",
targetRepositoryFullName = "GameCult/Bifrost",
targetAgentIdentity = "nibu",
activityKind = "Claimed",
status = "claimed",
actorName = "bifrost-dispatcher",
note = "Request claimed for Bifrost."
});
var startPayload = JsonSerializer.Serialize(new
{
requestId = "req_dispatch_123",
targetRepoName = "Bifrost",
targetRepositoryFullName = "GameCult/Bifrost",
targetAgentIdentity = "nibu",
launchMode = "app-server",
workerProcessId = 4242,
threadId = "thread_1",
turnId = "turn_1",
logPath = "E:/Projects/Bifrost/.bifrost/agent-dispatch/req_dispatch_123/codex.log",
resultPath = "E:/Projects/Bifrost/.bifrost/agent-dispatch/req_dispatch_123/result.json",
note = "Codex app-server turn started."
});
using var startRequest = new HttpRequestMessage(HttpMethod.Post, "/dispatch/runs/start");
startRequest.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
startRequest.Content = new StringContent(startPayload, Encoding.UTF8, "application/json");
using var startResponse = await client.SendAsync(startRequest);
Assert.Equal(HttpStatusCode.Accepted, startResponse.StatusCode);
var started = await startResponse.Content.ReadFromJsonAsync<AgentDispatchRunHttpResult>();
Assert.NotNull(started);
Assert.Equal("Started", started.Status);
var completePayload = JsonSerializer.Serialize(new
{
status = "Completed",
threadId = "thread_1",
turnId = "turn_1",
resultPath = "E:/Projects/Bifrost/.bifrost/agent-dispatch/req_dispatch_123/result.json",
note = "Codex app turn completed for thread_1/turn_1."
});
using var completeRequest = new HttpRequestMessage(HttpMethod.Post, $"/dispatch/runs/{started.Id}/complete");
completeRequest.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
completeRequest.Content = new StringContent(completePayload, Encoding.UTF8, "application/json");
using var completeResponse = await client.SendAsync(completeRequest);
Assert.Equal(HttpStatusCode.OK, completeResponse.StatusCode);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var savedRun = dbContext.AgentDispatchRuns.Single(x => x.Id == started.Id);
Assert.Equal(AgentDispatchRunStatus.Completed, savedRun.Status);
Assert.Equal("req_dispatch_123", savedRun.RequestId);
Assert.Equal("turn_1", savedRun.TurnId);
Assert.Null(savedRun.StartedByUserAccountId);
Assert.Contains(dbContext.AuditEvents, x => x.EntityType == nameof(AgentDispatchRun) && x.Action == "agent-dispatch.completed");
}
[Fact]
public async Task Local_bridge_token_can_record_dispatch_run_failure()
{
using var client = _factory.CreateClient();
_ = await client.GetAsync("/App");
await PostTransportReceiptAsync(client, new
{
requestId = "req_dispatch_fail_123",
title = "Queue the GitHub bridge hardening pass",
targetRepoName = "Bifrost",
targetRepositoryFullName = "GameCult/Bifrost",
targetAgentIdentity = "nibu",
activityKind = "Claimed",
status = "claimed",
actorName = "bifrost-dispatcher",
note = "Request claimed for Bifrost."
});
var startPayload = JsonSerializer.Serialize(new
{
requestId = "req_dispatch_fail_123",
targetRepoName = "Bifrost",
targetRepositoryFullName = "GameCult/Bifrost",
targetAgentIdentity = "nibu",
launchMode = "app-server",
workerProcessId = 4242,
threadId = string.Empty,
turnId = string.Empty,
logPath = "E:/Projects/Bifrost/.bifrost/agent-dispatch/req_dispatch_fail_123/codex.log",
resultPath = "E:/Projects/Bifrost/.bifrost/agent-dispatch/req_dispatch_fail_123/result.json",
note = "Codex app-server launch started."
});
using var startRequest = new HttpRequestMessage(HttpMethod.Post, "/dispatch/runs/start");
startRequest.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
startRequest.Content = new StringContent(startPayload, Encoding.UTF8, "application/json");
using var startResponse = await client.SendAsync(startRequest);
Assert.Equal(HttpStatusCode.Accepted, startResponse.StatusCode);
var started = await startResponse.Content.ReadFromJsonAsync<AgentDispatchRunHttpResult>();
Assert.NotNull(started);
var failPayload = JsonSerializer.Serialize(new
{
threadId = string.Empty,
turnId = string.Empty,
resultPath = "E:/Projects/Bifrost/.bifrost/agent-dispatch/req_dispatch_fail_123/result.json",
note = "Codex dispatch failed.",
error = "Codex app-server thread/start returned no thread id."
});
using var failRequest = new HttpRequestMessage(HttpMethod.Post, $"/dispatch/runs/{started.Id}/fail");
failRequest.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
failRequest.Content = new StringContent(failPayload, Encoding.UTF8, "application/json");
using var failResponse = await client.SendAsync(failRequest);
Assert.Equal(HttpStatusCode.OK, failResponse.StatusCode);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var savedRun = dbContext.AgentDispatchRuns.Single(x => x.Id == started.Id);
Assert.Equal(AgentDispatchRunStatus.Failed, savedRun.Status);
Assert.Equal("Codex app-server thread/start returned no thread id.", savedRun.Error);
Assert.Null(savedRun.StartedByUserAccountId);
Assert.Contains(dbContext.AuditEvents, x => x.EntityType == nameof(AgentDispatchRun) && x.Action == "agent-dispatch.failed");
}
[Fact]
public async Task Local_bridge_token_can_record_transport_receipt()
{
using var client = _factory.CreateClient();
_ = await client.GetAsync("/App");
var payload = JsonSerializer.Serialize(new
{
requestId = "req_transport_123",
title = "Queue the GitHub bridge hardening pass",
targetRepoName = "Bifrost",
targetRepositoryFullName = "GameCult/Bifrost",
targetAgentIdentity = "nibu",
activityKind = "Claimed",
status = "claimed",
actorName = "bifrost-dispatcher",
note = "Request claimed for Bifrost."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/transport/receipts");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
var receipt = await response.Content.ReadFromJsonAsync<AgentTransportReceiptHttpResult>();
Assert.NotNull(receipt);
Assert.Equal("req_transport_123", receipt.RequestId);
Assert.Equal("Claimed", receipt.ActivityKind);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var savedReceipt = dbContext.AgentTransportReceipts.Single(x => x.Id == receipt.Id);
Assert.Equal("Claimed", savedReceipt.ActivityKind.ToString());
Assert.Equal("bifrost-dispatcher", savedReceipt.ActorName);
Assert.Null(savedReceipt.ActorUserAccountId);
Assert.Contains(dbContext.AuditEvents, x => x.EntityType == nameof(AgentTransportReceipt) && x.Action == "agent-transport.claimed");
}
[Fact]
public async Task Local_bridge_token_can_record_governance_receipt()
{
using var client = _factory.CreateClient();
_ = await client.GetAsync("/App");
await PostTransportReceiptAsync(client, new
{
requestId = "req_transport_123",
title = "Queue the GitHub bridge hardening pass",
targetRepoName = "Bifrost",
targetRepositoryFullName = "GameCult/Bifrost",
targetAgentIdentity = "nibu",
activityKind = "Queued",
status = "queued",
actorName = "bifrost-dispatcher",
note = "Request queued for Bifrost."
});
var payload = JsonSerializer.Serialize(new
{
topicId = "topic_123",
commentId = "comment_123",
dispatchRequestId = "req_transport_123",
title = "Queue the GitHub bridge hardening pass",
jurisdictionRepoName = "Bifrost",
jurisdictionAgentIdentity = "nibu",
activityKind = "TopicPromoted",
actorKind = "face",
actorName = "nibu",
note = "Governance topic promoted to update request req_transport_123."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/governance/receipts");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
var receipt = await response.Content.ReadFromJsonAsync<GovernanceActivityReceiptHttpResult>();
Assert.NotNull(receipt);
Assert.Equal("topic_123", receipt.TopicId);
Assert.Equal("TopicPromoted", receipt.ActivityKind);
using var scope = _factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BifrostDbContext>();
var savedReceipt = dbContext.GovernanceActivityReceipts.Single(x => x.Id == receipt.Id);
Assert.Equal("TopicPromoted", savedReceipt.ActivityKind.ToString());
Assert.Equal("nibu", savedReceipt.ActorName);
Assert.Null(savedReceipt.ActorUserAccountId);
Assert.Contains(dbContext.AuditEvents, x => x.EntityType == nameof(GovernanceActivityReceipt) && x.Action == "governance.topic-promoted");
}
[Fact]
public async Task Active_member_session_cannot_record_dispatch_run_without_local_bridge_token()
{
using var client = _factory.CreateClient();
_ = await client.GetAsync("/App");
var payload = JsonSerializer.Serialize(new
{
requestId = "req_dispatch_member_123",
targetRepoName = "Bifrost",
targetRepositoryFullName = "GameCult/Bifrost",
targetAgentIdentity = "nibu",
launchMode = "app-server",
workerProcessId = 4242,
threadId = "thread_member",
turnId = "turn_member",
logPath = "E:/Projects/Bifrost/.bifrost/agent-dispatch/req_dispatch_member_123/codex.log",
resultPath = "E:/Projects/Bifrost/.bifrost/agent-dispatch/req_dispatch_member_123/result.json",
note = "Should be rejected without the local bridge token."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/dispatch/runs/start");
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task Dispatch_run_without_request_lane_receipt_is_rejected()
{
using var client = _factory.CreateClient();
var payload = JsonSerializer.Serialize(new
{
requestId = "req_dispatch_missing_123",
targetRepoName = "Bifrost",
targetRepositoryFullName = "GameCult/Bifrost",
targetAgentIdentity = "nibu",
launchMode = "app-server",
workerProcessId = 4242,
threadId = "thread_missing",
turnId = "turn_missing",
logPath = "E:/Projects/Bifrost/.bifrost/agent-dispatch/req_dispatch_missing_123/codex.log",
resultPath = "E:/Projects/Bifrost/.bifrost/agent-dispatch/req_dispatch_missing_123/result.json",
note = "Should fail because the request lane has no receipt."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/dispatch/runs/start");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Contains("existing request-lane receipt", body, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Governance_promotion_receipt_requires_known_dispatch_request()
{
using var client = _factory.CreateClient();
var payload = JsonSerializer.Serialize(new
{
topicId = "topic_unknown_dispatch_123",
commentId = "comment_unknown_dispatch_123",
dispatchRequestId = "req_unknown_transport_123",
title = "Promote an unknown dispatch request",
jurisdictionRepoName = "Bifrost",
jurisdictionAgentIdentity = "nibu",
activityKind = "TopicPromoted",
actorKind = "face",
actorName = "nibu",
note = "Should fail because the dispatch request is unknown."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/governance/receipts");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Contains("unknown dispatch request", body, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Request_lane_receipt_cannot_change_repo_identity_for_existing_request()
{
using var client = _factory.CreateClient();
await PostTransportReceiptAsync(client, new
{
requestId = "req_transport_conflict_123",
title = "Queue the GitHub bridge hardening pass",
targetRepoName = "Bifrost",
targetRepositoryFullName = "GameCult/Bifrost",
targetAgentIdentity = "nibu",
activityKind = "Queued",
status = "queued",
actorName = "bifrost-dispatcher",
note = "Request queued for Bifrost."
});
var conflictPayload = JsonSerializer.Serialize(new
{
requestId = "req_transport_conflict_123",
title = "Queue the GitHub bridge hardening pass",
targetRepoName = "VoidBot",
targetRepositoryFullName = "GameCult/VoidBot",
targetAgentIdentity = "nibu",
activityKind = "Claimed",
status = "claimed",
actorName = "bifrost-dispatcher",
note = "This should not be allowed to drift."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/transport/receipts");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(conflictPayload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Contains("target repo", body, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Active_member_session_cannot_record_transport_receipt_without_local_bridge_token()
{
using var client = _factory.CreateClient();
_ = await client.GetAsync("/App");
var payload = JsonSerializer.Serialize(new
{
requestId = "req_transport_member_123",
title = "Member should not mint runtime receipts",
targetRepoName = "Bifrost",
targetRepositoryFullName = "GameCult/Bifrost",
targetAgentIdentity = "nibu",
activityKind = "Claimed",
status = "claimed",
actorName = "test-admin",
note = "Should be rejected without the local bridge token."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/transport/receipts");
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task Active_member_session_cannot_record_governance_receipt_without_local_bridge_token()
{
using var client = _factory.CreateClient();
_ = await client.GetAsync("/App");
var payload = JsonSerializer.Serialize(new
{
topicId = "topic_member_123",
commentId = "comment_member_123",
dispatchRequestId = "req_transport_member_123",
title = "Member should not mint governance runtime receipts",
jurisdictionRepoName = "Bifrost",
jurisdictionAgentIdentity = "nibu",
activityKind = "TopicPromoted",
actorKind = "member",
actorName = "test-admin",
note = "Should be rejected without the local bridge token."
});
using var request = new HttpRequestMessage(HttpMethod.Post, "/governance/receipts");
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
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 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 static async Task PostTransportReceiptAsync(HttpClient client, object payload)
{
using var request = new HttpRequestMessage(HttpMethod.Post, "/transport/receipts");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
Assert.True(
response.StatusCode == HttpStatusCode.Accepted,
$"Expected transport receipt seed to succeed but got {(int)response.StatusCode}: {body}");
}
private static async Task PostGovernanceReceiptAsync(HttpClient client, object payload)
{
using var request = new HttpRequestMessage(HttpMethod.Post, "/governance/receipts");
request.Headers.Add("X-Bifrost-Bridge-Token", "test-bridge-token");
request.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
Assert.True(
response.StatusCode == HttpStatusCode.Accepted,
$"Expected governance receipt seed to succeed but got {(int)response.StatusCode}: {body}");
}
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;
}
}