web/backend/secretapi/app_test.go
Joshua 0040620649
Some checks are pending
check / secretapi (push) Waiting to run
Block tx-hash replay across membership and checkout confirms
2026-02-19 14:23:57 -08:00

1902 lines
69 KiB
Go

package main
import (
"bytes"
"context"
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
)
func TestMembershipDistinctPayerProof(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
payerKey := mustKey(t)
payerAddr := strings.ToLower(crypto.PubkeyToAddress(payerKey.PublicKey).Hex())
intentRes := postJSONExpect[tWalletIntentResponse](t, a, "/secret/wallet/intent", walletIntentRequest{
Address: ownerAddr,
Origin: "https://edut.ai",
Locale: "en",
ChainID: cfg.ChainID,
}, http.StatusOK)
issuedAt, err := time.Parse(time.RFC3339Nano, intentRes.IssuedAt)
if err != nil {
t.Fatalf("parse issued_at: %v", err)
}
td := buildTypedData(cfg, designationRecord{
Code: intentRes.DesignationCode,
DisplayToken: intentRes.DisplayToken,
Nonce: intentRes.Nonce,
IssuedAt: issuedAt,
Origin: "https://edut.ai",
})
sig := signTypedData(t, ownerKey, td)
verifyRes := postJSONExpect[tWalletVerifyResponse](t, a, "/secret/wallet/verify", walletVerifyRequest{
IntentID: intentRes.IntentID,
Address: ownerAddr,
ChainID: cfg.ChainID,
Signature: sig,
}, http.StatusOK)
if verifyRes.Status != "signature_verified" {
t.Fatalf("unexpected verify status: %+v", verifyRes)
}
// Distinct payer without proof must fail closed.
_ = postJSONExpect[map[string]string](t, a, "/secret/membership/quote", membershipQuoteRequest{
DesignationCode: verifyRes.DesignationCode,
Address: ownerAddr,
ChainID: cfg.ChainID,
PayerWallet: payerAddr,
}, http.StatusForbidden)
payerProofSig := signPersonalMessage(
t,
ownerKey,
payerProofMessage(verifyRes.DesignationCode, ownerAddr, payerAddr, cfg.ChainID),
)
quote := postJSONExpect[membershipQuoteResponse](t, a, "/secret/membership/quote", membershipQuoteRequest{
DesignationCode: verifyRes.DesignationCode,
Address: ownerAddr,
ChainID: cfg.ChainID,
PayerWallet: payerAddr,
PayerProof: payerProofSig,
SponsorOrgRoot: "org_company_a",
}, http.StatusOK)
if got := strings.ToLower(strings.TrimSpace(quote.OwnerWallet)); got != ownerAddr {
t.Fatalf("owner wallet mismatch: got=%s want=%s", got, ownerAddr)
}
if got := strings.ToLower(strings.TrimSpace(quote.PayerWallet)); got != payerAddr {
t.Fatalf("payer wallet mismatch: got=%s want=%s", got, payerAddr)
}
if quote.Tx["from"] != payerAddr {
t.Fatalf("tx.from mismatch: %+v", quote.Tx)
}
}
func TestMembershipCompanySponsorWithoutOwnerProof(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
companyPayerKey := mustKey(t)
companyPayerAddr := strings.ToLower(crypto.PubkeyToAddress(companyPayerKey.PublicKey).Hex())
now := time.Now().UTC()
if err := a.store.putGovernancePrincipal(context.Background(), governancePrincipalRecord{
Wallet: companyPayerAddr,
OrgRootID: "org_company_b",
PrincipalID: "principal_company_owner",
PrincipalRole: "org_root_owner",
EntitlementID: "gov_company_b",
EntitlementStatus: "active",
AccessClass: "connected",
AvailabilityState: "active",
UpdatedAt: now,
}); err != nil {
t.Fatalf("seed governance principal: %v", err)
}
if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{
EntitlementID: "ent:sponsor:workspace-core",
QuoteID: "seed-q-sponsor-workspace-core",
OfferID: offerIDWorkspaceCore,
Wallet: companyPayerAddr,
OrgRootID: "org_company_b",
PrincipalID: "principal_company_owner",
PrincipalRole: "org_root_owner",
State: "active",
AccessClass: "connected",
AvailabilityState: "active",
PolicyHash: "sha256:testpolicy",
IssuedAt: now,
TxHash: "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
}); err != nil {
t.Fatalf("seed sponsor workspace core entitlement: %v", err)
}
intentRes := postJSONExpect[tWalletIntentResponse](t, a, "/secret/wallet/intent", walletIntentRequest{
Address: ownerAddr,
Origin: "https://edut.ai",
Locale: "en",
ChainID: cfg.ChainID,
}, http.StatusOK)
issuedAt, err := time.Parse(time.RFC3339Nano, intentRes.IssuedAt)
if err != nil {
t.Fatalf("parse issued_at: %v", err)
}
td := buildTypedData(cfg, designationRecord{
Code: intentRes.DesignationCode,
DisplayToken: intentRes.DisplayToken,
Nonce: intentRes.Nonce,
IssuedAt: issuedAt,
Origin: "https://edut.ai",
})
sig := signTypedData(t, ownerKey, td)
verifyRes := postJSONExpect[tWalletVerifyResponse](t, a, "/secret/wallet/verify", walletVerifyRequest{
IntentID: intentRes.IntentID,
Address: ownerAddr,
ChainID: cfg.ChainID,
Signature: sig,
}, http.StatusOK)
quote := postJSONExpect[membershipQuoteResponse](t, a, "/secret/membership/quote", membershipQuoteRequest{
DesignationCode: verifyRes.DesignationCode,
Address: ownerAddr,
ChainID: cfg.ChainID,
PayerWallet: companyPayerAddr,
SponsorOrgRoot: "org_company_b",
}, http.StatusOK)
if quote.SponsorshipMode != "sponsored_company" {
t.Fatalf("expected sponsored_company mode, got %+v", quote)
}
}
func TestMembershipConfirmRequiresChainRPCWhenStrictVerificationEnabled(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireOnchainTxVerify = true
a.cfg.ChainRPCURL = ""
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
intentRes := postJSONExpect[tWalletIntentResponse](t, a, "/secret/wallet/intent", walletIntentRequest{
Address: ownerAddr,
Origin: "https://edut.ai",
Locale: "en",
ChainID: cfg.ChainID,
}, http.StatusOK)
issuedAt, err := time.Parse(time.RFC3339Nano, intentRes.IssuedAt)
if err != nil {
t.Fatalf("parse issued_at: %v", err)
}
td := buildTypedData(cfg, designationRecord{
Code: intentRes.DesignationCode,
DisplayToken: intentRes.DisplayToken,
Nonce: intentRes.Nonce,
IssuedAt: issuedAt,
Origin: "https://edut.ai",
})
sig := signTypedData(t, ownerKey, td)
verifyRes := postJSONExpect[tWalletVerifyResponse](t, a, "/secret/wallet/verify", walletVerifyRequest{
IntentID: intentRes.IntentID,
Address: ownerAddr,
ChainID: cfg.ChainID,
Signature: sig,
}, http.StatusOK)
quote := postJSONExpect[membershipQuoteResponse](t, a, "/secret/membership/quote", membershipQuoteRequest{
DesignationCode: verifyRes.DesignationCode,
Address: ownerAddr,
ChainID: cfg.ChainID,
}, http.StatusOK)
fail := postJSONExpect[map[string]string](t, a, "/secret/membership/confirm", membershipConfirmRequest{
DesignationCode: verifyRes.DesignationCode,
QuoteID: quote.QuoteID,
TxHash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Address: ownerAddr,
ChainID: cfg.ChainID,
}, http.StatusServiceUnavailable)
if fail["code"] != "chain_verification_unavailable" {
t.Fatalf("expected chain_verification_unavailable code, got %+v", fail)
}
}
func TestMembershipConfirmDefaultsToCryptoDirectAssurance(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
intentRes := postJSONExpect[tWalletIntentResponse](t, a, "/secret/wallet/intent", walletIntentRequest{
Address: ownerAddr,
Origin: "https://edut.ai",
Locale: "en",
ChainID: cfg.ChainID,
}, http.StatusOK)
issuedAt, err := time.Parse(time.RFC3339Nano, intentRes.IssuedAt)
if err != nil {
t.Fatalf("parse issued_at: %v", err)
}
td := buildTypedData(cfg, designationRecord{
Code: intentRes.DesignationCode,
DisplayToken: intentRes.DisplayToken,
Nonce: intentRes.Nonce,
IssuedAt: issuedAt,
Origin: "https://edut.ai",
})
sig := signTypedData(t, ownerKey, td)
verifyRes := postJSONExpect[tWalletVerifyResponse](t, a, "/secret/wallet/verify", walletVerifyRequest{
IntentID: intentRes.IntentID,
Address: ownerAddr,
ChainID: cfg.ChainID,
Signature: sig,
}, http.StatusOK)
quote := postJSONExpect[membershipQuoteResponse](t, a, "/secret/membership/quote", membershipQuoteRequest{
DesignationCode: verifyRes.DesignationCode,
Address: ownerAddr,
ChainID: cfg.ChainID,
}, http.StatusOK)
confirm := postJSONExpect[membershipConfirmResponse](t, a, "/secret/membership/confirm", membershipConfirmRequest{
DesignationCode: verifyRes.DesignationCode,
QuoteID: quote.QuoteID,
TxHash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Address: ownerAddr,
ChainID: cfg.ChainID,
}, http.StatusOK)
if confirm.IdentityAssurance != assuranceCryptoDirect {
t.Fatalf("expected %s assurance, got %+v", assuranceCryptoDirect, confirm)
}
status := getJSONExpect[membershipStatusResponse](t, a, "/secret/membership/status?wallet="+ownerAddr, http.StatusOK)
if status.IdentityAssurance != assuranceCryptoDirect {
t.Fatalf("expected status assurance %s, got %+v", assuranceCryptoDirect, status)
}
}
func TestMembershipConfirmAcceptsOnrampAttestationAssurance(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
intentRes := postJSONExpect[tWalletIntentResponse](t, a, "/secret/wallet/intent", walletIntentRequest{
Address: ownerAddr,
Origin: "https://edut.ai",
Locale: "en",
ChainID: cfg.ChainID,
}, http.StatusOK)
issuedAt, err := time.Parse(time.RFC3339Nano, intentRes.IssuedAt)
if err != nil {
t.Fatalf("parse issued_at: %v", err)
}
td := buildTypedData(cfg, designationRecord{
Code: intentRes.DesignationCode,
DisplayToken: intentRes.DisplayToken,
Nonce: intentRes.Nonce,
IssuedAt: issuedAt,
Origin: "https://edut.ai",
})
sig := signTypedData(t, ownerKey, td)
verifyRes := postJSONExpect[tWalletVerifyResponse](t, a, "/secret/wallet/verify", walletVerifyRequest{
IntentID: intentRes.IntentID,
Address: ownerAddr,
ChainID: cfg.ChainID,
Signature: sig,
}, http.StatusOK)
quote := postJSONExpect[membershipQuoteResponse](t, a, "/secret/membership/quote", membershipQuoteRequest{
DesignationCode: verifyRes.DesignationCode,
Address: ownerAddr,
ChainID: cfg.ChainID,
}, http.StatusOK)
confirm := postJSONExpect[membershipConfirmResponse](t, a, "/secret/membership/confirm", membershipConfirmRequest{
DesignationCode: verifyRes.DesignationCode,
QuoteID: quote.QuoteID,
TxHash: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
Address: ownerAddr,
ChainID: cfg.ChainID,
IdentityAssurance: assuranceOnrampAttested,
IdentityAttestedBy: "moonpay",
IdentityAttestationID: "onramp-session-123",
}, http.StatusOK)
if confirm.IdentityAssurance != assuranceOnrampAttested || confirm.IdentityAttestedBy != "moonpay" {
t.Fatalf("unexpected attested confirm response: %+v", confirm)
}
}
func TestMembershipStatusPrefersActiveDesignationWhenNewerRecordIsUnactivated(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
now := time.Now().UTC()
activeIssuedAt := now.Add(-2 * time.Hour)
activeCode := "1111111111111"
if err := a.store.putDesignation(context.Background(), designationRecord{
Code: activeCode,
DisplayToken: "1111-1111-1111-1",
IntentID: "intent-active",
Nonce: "nonce-active",
Origin: "https://edut.ai",
Locale: "en",
Address: ownerAddr,
ChainID: 84532,
IssuedAt: activeIssuedAt,
ExpiresAt: activeIssuedAt.Add(time.Hour),
VerifiedAt: &activeIssuedAt,
MembershipStatus: "active",
MembershipTxHash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
ActivatedAt: &activeIssuedAt,
IdentityAssurance: assuranceCryptoDirect,
IdentityAttestedBy: "",
IdentityAttestationID: "",
}); err != nil {
t.Fatalf("seed active designation: %v", err)
}
newerIssuedAt := now
if err := a.store.putDesignation(context.Background(), designationRecord{
Code: "2222222222222",
DisplayToken: "2222-2222-2222-2",
IntentID: "intent-newer",
Nonce: "nonce-newer",
Origin: "https://edut.ai",
Locale: "en",
Address: ownerAddr,
ChainID: 84532,
IssuedAt: newerIssuedAt,
ExpiresAt: newerIssuedAt.Add(time.Hour),
VerifiedAt: &newerIssuedAt,
MembershipStatus: "none",
IdentityAssurance: assuranceNone,
IdentityAttestedBy: "",
IdentityAttestationID: "",
}); err != nil {
t.Fatalf("seed newer unactivated designation: %v", err)
}
status := getJSONExpect[membershipStatusResponse](t, a, "/secret/membership/status?wallet="+ownerAddr, http.StatusOK)
if status.Status != "active" {
t.Fatalf("expected active status, got %+v", status)
}
if status.DesignationCode != activeCode {
t.Fatalf("expected active designation code %s, got %+v", activeCode, status)
}
}
func TestMembershipConfirmRejectsAlreadyConfirmedQuote(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
intentRes := postJSONExpect[tWalletIntentResponse](t, a, "/secret/wallet/intent", walletIntentRequest{
Address: ownerAddr,
Origin: "https://edut.ai",
Locale: "en",
ChainID: cfg.ChainID,
}, http.StatusOK)
issuedAt, err := time.Parse(time.RFC3339Nano, intentRes.IssuedAt)
if err != nil {
t.Fatalf("parse issued_at: %v", err)
}
td := buildTypedData(cfg, designationRecord{
Code: intentRes.DesignationCode,
DisplayToken: intentRes.DisplayToken,
Nonce: intentRes.Nonce,
IssuedAt: issuedAt,
Origin: "https://edut.ai",
})
sig := signTypedData(t, ownerKey, td)
verifyRes := postJSONExpect[tWalletVerifyResponse](t, a, "/secret/wallet/verify", walletVerifyRequest{
IntentID: intentRes.IntentID,
Address: ownerAddr,
ChainID: cfg.ChainID,
Signature: sig,
}, http.StatusOK)
quote := postJSONExpect[membershipQuoteResponse](t, a, "/secret/membership/quote", membershipQuoteRequest{
DesignationCode: verifyRes.DesignationCode,
Address: ownerAddr,
ChainID: cfg.ChainID,
}, http.StatusOK)
_ = postJSONExpect[membershipConfirmResponse](t, a, "/secret/membership/confirm", membershipConfirmRequest{
DesignationCode: verifyRes.DesignationCode,
QuoteID: quote.QuoteID,
TxHash: "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
Address: ownerAddr,
ChainID: cfg.ChainID,
}, http.StatusOK)
_ = postJSONExpect[map[string]string](t, a, "/secret/membership/confirm", membershipConfirmRequest{
DesignationCode: verifyRes.DesignationCode,
QuoteID: quote.QuoteID,
TxHash: "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
Address: ownerAddr,
ChainID: cfg.ChainID,
}, http.StatusConflict)
}
func TestMembershipConfirmRejectsTxHashReplayAcrossDesignations(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
signVerify := func(intent tWalletIntentResponse) tWalletVerifyResponse {
issuedAt, err := time.Parse(time.RFC3339Nano, intent.IssuedAt)
if err != nil {
t.Fatalf("parse issued_at: %v", err)
}
td := buildTypedData(cfg, designationRecord{
Code: intent.DesignationCode,
DisplayToken: intent.DisplayToken,
Nonce: intent.Nonce,
IssuedAt: issuedAt,
Origin: "https://edut.ai",
})
sig := signTypedData(t, ownerKey, td)
return postJSONExpect[tWalletVerifyResponse](t, a, "/secret/wallet/verify", walletVerifyRequest{
IntentID: intent.IntentID,
Address: ownerAddr,
ChainID: cfg.ChainID,
Signature: sig,
}, http.StatusOK)
}
intent1 := postJSONExpect[tWalletIntentResponse](t, a, "/secret/wallet/intent", walletIntentRequest{
Address: ownerAddr,
Origin: "https://edut.ai",
Locale: "en",
ChainID: cfg.ChainID,
}, http.StatusOK)
verify1 := signVerify(intent1)
quote1 := postJSONExpect[membershipQuoteResponse](t, a, "/secret/membership/quote", membershipQuoteRequest{
DesignationCode: verify1.DesignationCode,
Address: ownerAddr,
ChainID: cfg.ChainID,
}, http.StatusOK)
replayedTxHash := "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
_ = postJSONExpect[membershipConfirmResponse](t, a, "/secret/membership/confirm", membershipConfirmRequest{
DesignationCode: verify1.DesignationCode,
QuoteID: quote1.QuoteID,
TxHash: replayedTxHash,
Address: ownerAddr,
ChainID: cfg.ChainID,
}, http.StatusOK)
intent2 := postJSONExpect[tWalletIntentResponse](t, a, "/secret/wallet/intent", walletIntentRequest{
Address: ownerAddr,
Origin: "https://edut.ai",
Locale: "en",
ChainID: cfg.ChainID,
}, http.StatusOK)
verify2 := signVerify(intent2)
quote2 := postJSONExpect[membershipQuoteResponse](t, a, "/secret/membership/quote", membershipQuoteRequest{
DesignationCode: verify2.DesignationCode,
Address: ownerAddr,
ChainID: cfg.ChainID,
}, http.StatusOK)
errResp := postJSONExpect[map[string]string](t, a, "/secret/membership/confirm", membershipConfirmRequest{
DesignationCode: verify2.DesignationCode,
QuoteID: quote2.QuoteID,
TxHash: replayedTxHash,
Address: ownerAddr,
ChainID: cfg.ChainID,
}, http.StatusConflict)
if code := errResp["code"]; code != "tx_hash_replay" {
t.Fatalf("expected tx_hash_replay, got %+v", errResp)
}
}
func TestGovernanceInstallOwnerOnlyAndConfirm(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
memberKey := mustKey(t)
memberAddr := strings.ToLower(crypto.PubkeyToAddress(memberKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, memberAddr); err != nil {
t.Fatalf("seed member membership: %v", err)
}
_ = postJSONExpect[map[string]string](t, a, "/governance/install/token", governanceInstallTokenRequest{
Wallet: memberAddr,
OrgRootID: "org_root_a",
PrincipalID: "principal_member",
PrincipalRole: "workspace_member",
DeviceID: "macstudio-001",
LauncherVersion: "0.1.0",
Platform: "macos",
}, http.StatusForbidden)
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed membership: %v", err)
}
now := time.Now().UTC()
if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{
EntitlementID: "ent:install:workspace-core",
QuoteID: "seed-q-install-workspace-core",
OfferID: offerIDWorkspaceCore,
Wallet: ownerAddr,
OrgRootID: "org_root_a",
PrincipalID: "principal_owner",
PrincipalRole: "org_root_owner",
State: "active",
AccessClass: "connected",
AvailabilityState: "active",
PolicyHash: cfg.GovernancePolicyHash,
IssuedAt: now,
TxHash: "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
}); err != nil {
t.Fatalf("seed workspace core entitlement: %v", err)
}
if err := a.store.putGovernancePrincipal(context.Background(), governancePrincipalRecord{
Wallet: ownerAddr,
OrgRootID: "org_root_a",
PrincipalID: "principal_owner",
PrincipalRole: "org_root_owner",
EntitlementID: "ent:install:workspace-core",
EntitlementStatus: "active",
AccessClass: "connected",
AvailabilityState: "active",
UpdatedAt: now,
}); err != nil {
t.Fatalf("seed owner principal: %v", err)
}
tokenRes := postJSONExpect[governanceInstallTokenResponse](t, a, "/governance/install/token", governanceInstallTokenRequest{
Wallet: ownerAddr,
OrgRootID: "org_root_a",
PrincipalID: "principal_owner",
PrincipalRole: "org_root_owner",
DeviceID: "macstudio-001",
LauncherVersion: "0.1.0",
Platform: "macos",
}, http.StatusOK)
if tokenRes.InstallToken == "" {
t.Fatalf("missing install token")
}
_ = postJSONExpect[map[string]string](t, a, "/governance/install/confirm", governanceInstallConfirmRequest{
InstallToken: tokenRes.InstallToken,
Wallet: ownerAddr,
DeviceID: "macstudio-001",
EntitlementID: tokenRes.EntitlementID,
PackageHash: "sha256:wrong",
RuntimeVersion: cfg.GovernanceRuntimeVersion,
InstalledAt: time.Now().UTC().Format(time.RFC3339Nano),
}, http.StatusConflict)
confirm := postJSONExpect[governanceInstallConfirmResponse](t, a, "/governance/install/confirm", governanceInstallConfirmRequest{
InstallToken: tokenRes.InstallToken,
Wallet: ownerAddr,
DeviceID: "macstudio-001",
EntitlementID: tokenRes.EntitlementID,
PackageHash: cfg.GovernancePackageHash,
RuntimeVersion: cfg.GovernanceRuntimeVersion,
InstalledAt: time.Now().UTC().Format(time.RFC3339Nano),
LauncherReceiptRef: "receipt-hash-1",
}, http.StatusOK)
if confirm.Status != "governance_active" {
t.Fatalf("unexpected confirm status: %+v", confirm)
}
statusReq := httptest.NewRequest(http.MethodGet, "/governance/install/status?wallet="+ownerAddr+"&device_id=macstudio-001", nil)
statusRec := httptest.NewRecorder()
a.routes().ServeHTTP(statusRec, statusReq)
if statusRec.Code != http.StatusOK {
t.Fatalf("status code=%d body=%s", statusRec.Code, statusRec.Body.String())
}
var status governanceInstallStatusResponse
if err := json.Unmarshal(statusRec.Body.Bytes(), &status); err != nil {
t.Fatalf("decode status: %v", err)
}
if status.ActivationStatus != "active" {
t.Fatalf("expected active status got %+v", status)
}
}
func TestGovernanceInstallTokenRequiresOnrampAssurance(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedMembershipWithAssurance(context.Background(), a.store, ownerAddr, assuranceCryptoDirect); err != nil {
t.Fatalf("seed membership: %v", err)
}
now := time.Now().UTC()
if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{
EntitlementID: "ent:install:workspace-core:assurance",
QuoteID: "seed-q-install-workspace-core-assurance",
OfferID: offerIDWorkspaceCore,
Wallet: ownerAddr,
OrgRootID: "org_root_assurance",
PrincipalID: "principal_owner_assurance",
PrincipalRole: "org_root_owner",
State: "active",
AccessClass: "connected",
AvailabilityState: "active",
PolicyHash: "sha256:testpolicy",
IssuedAt: now,
TxHash: "0xabababababababababababababababababababababababababababababababab",
}); err != nil {
t.Fatalf("seed workspace core entitlement: %v", err)
}
if err := a.store.putGovernancePrincipal(context.Background(), governancePrincipalRecord{
Wallet: ownerAddr,
OrgRootID: "org_root_assurance",
PrincipalID: "principal_owner_assurance",
PrincipalRole: "org_root_owner",
EntitlementID: "ent:install:workspace-core:assurance",
EntitlementStatus: "active",
AccessClass: "connected",
AvailabilityState: "active",
UpdatedAt: now,
}); err != nil {
t.Fatalf("seed owner principal: %v", err)
}
errResp := postJSONExpect[map[string]string](t, a, "/governance/install/token", governanceInstallTokenRequest{
Wallet: ownerAddr,
OrgRootID: "org_root_assurance",
PrincipalID: "principal_owner_assurance",
PrincipalRole: "org_root_owner",
DeviceID: "macstudio-assurance",
LauncherVersion: "0.1.0",
Platform: "macos",
}, http.StatusForbidden)
if code := errResp["code"]; code != "identity_assurance_insufficient" {
t.Fatalf("expected identity_assurance_insufficient, got %+v", errResp)
}
}
func TestGovernanceLeaseAndOfflineRenew(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed membership: %v", err)
}
now := time.Now().UTC()
if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{
EntitlementID: "ent:lease:workspace-core",
QuoteID: "seed-q-lease-workspace-core",
OfferID: offerIDWorkspaceCore,
Wallet: ownerAddr,
OrgRootID: "org_root_b",
PrincipalID: "principal_owner_b",
PrincipalRole: "org_root_owner",
State: "active",
AccessClass: "connected",
AvailabilityState: "active",
PolicyHash: "sha256:testpolicy",
IssuedAt: now,
TxHash: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
}); err != nil {
t.Fatalf("seed workspace core entitlement: %v", err)
}
if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{
EntitlementID: "ent:lease:workspace-sovereign",
QuoteID: "seed-q-lease-workspace-sovereign",
OfferID: offerIDWorkspaceSovereign,
Wallet: ownerAddr,
OrgRootID: "org_root_b",
PrincipalID: "principal_owner_b",
PrincipalRole: "org_root_owner",
State: "active",
AccessClass: "sovereign",
AvailabilityState: "active",
PolicyHash: "sha256:testpolicy",
IssuedAt: now,
TxHash: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
}); err != nil {
t.Fatalf("seed workspace sovereign entitlement: %v", err)
}
if err := a.store.putGovernancePrincipal(context.Background(), governancePrincipalRecord{
Wallet: ownerAddr,
OrgRootID: "org_root_b",
PrincipalID: "principal_owner_b",
PrincipalRole: "org_root_owner",
EntitlementID: "ent:lease:workspace-core",
EntitlementStatus: "active",
AccessClass: "connected",
AvailabilityState: "active",
UpdatedAt: now,
}); err != nil {
t.Fatalf("seed owner principal: %v", err)
}
_ = postJSONExpect[governanceInstallTokenResponse](t, a, "/governance/install/token", governanceInstallTokenRequest{
Wallet: ownerAddr,
OrgRootID: "org_root_b",
PrincipalID: "principal_owner_b",
PrincipalRole: "org_root_owner",
DeviceID: "macstudio-002",
LauncherVersion: "0.1.0",
Platform: "macos",
}, http.StatusOK)
_ = postJSONExpect[map[string]string](t, a, "/governance/lease/heartbeat", governanceLeaseHeartbeatRequest{
Wallet: ownerAddr,
OrgRootID: "wrong_org",
PrincipalID: "principal_owner_b",
DeviceID: "macstudio-002",
}, http.StatusForbidden)
leaseRes := postJSONExpect[governanceLeaseHeartbeatResponse](t, a, "/governance/lease/heartbeat", governanceLeaseHeartbeatRequest{
Wallet: ownerAddr,
OrgRootID: "org_root_b",
PrincipalID: "principal_owner_b",
DeviceID: "macstudio-002",
}, http.StatusOK)
if leaseRes.Status != "lease_refreshed" || leaseRes.AvailabilityState != "active" {
t.Fatalf("unexpected lease response: %+v", leaseRes)
}
renewRes := postJSONExpect[governanceOfflineRenewResponse](t, a, "/governance/lease/offline-renew", governanceOfflineRenewRequest{
Wallet: ownerAddr,
OrgRootID: "org_root_b",
PrincipalID: "principal_owner_b",
RenewalBundle: map[string]any{
"bundle_id": "renew-1",
},
}, http.StatusOK)
if renewRes.Status != "renewal_applied" || renewRes.AvailabilityState != "active" {
t.Fatalf("unexpected renew response: %+v", renewRes)
}
}
func TestGovernanceInstallTokenBlockedWhenParked(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed membership: %v", err)
}
now := time.Now().UTC()
if err := a.store.putGovernancePrincipal(context.Background(), governancePrincipalRecord{
Wallet: ownerAddr,
OrgRootID: "org_parked",
PrincipalID: "principal_parked",
PrincipalRole: "org_root_owner",
EntitlementID: "gov_parked",
EntitlementStatus: "active",
AccessClass: "connected",
AvailabilityState: "parked",
UpdatedAt: now,
}); err != nil {
t.Fatalf("seed principal: %v", err)
}
_ = postJSONExpect[map[string]string](t, a, "/governance/install/token", governanceInstallTokenRequest{
Wallet: ownerAddr,
OrgRootID: "org_parked",
PrincipalID: "principal_parked",
PrincipalRole: "org_root_owner",
DeviceID: "macstudio-parked",
LauncherVersion: "0.1.0",
Platform: "macos",
}, http.StatusForbidden)
}
func TestMemberChannelRegisterPollAckAndUnregister(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed membership: %v", err)
}
register := postJSONExpect[memberChannelDeviceRegisterResponse](t, a, "/member/channel/device/register", memberChannelDeviceRegisterRequest{
Wallet: ownerAddr,
ChainID: 84532,
DeviceID: "desktop-01",
Platform: "desktop",
OrgRootID: "org.test.root",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
AppVersion: "0.1.0",
PushProvider: "none",
}, http.StatusOK)
if register.Status != "active" || register.ChannelBindingID == "" {
t.Fatalf("unexpected register response: %+v", register)
}
events := getJSONExpect[memberChannelEventsResponse](t, a, "/member/channel/events?wallet="+ownerAddr+"&device_id=desktop-01&limit=25", http.StatusOK)
if len(events.Events) == 0 {
t.Fatalf("expected seeded events, got none")
}
if events.MembershipStatus != "active" {
t.Fatalf("expected active membership status in events payload, got %+v", events)
}
if events.IdentityAssurance != assuranceOnrampAttested {
t.Fatalf("expected onramp assurance in events payload, got %+v", events)
}
first := events.Events[0]
ackTime := time.Now().UTC()
ack := postJSONExpect[memberChannelEventAckResponse](t, a, "/member/channel/events/"+first.EventID+"/ack", memberChannelEventAckRequest{
Wallet: ownerAddr,
DeviceID: "desktop-01",
AcknowledgedAt: ackTime.Format(time.RFC3339Nano),
}, http.StatusOK)
if ack.Status != "acknowledged" || ack.EventID != first.EventID {
t.Fatalf("unexpected ack response: %+v", ack)
}
// Idempotent ack should keep original acknowledgement timestamp.
ack2 := postJSONExpect[memberChannelEventAckResponse](t, a, "/member/channel/events/"+first.EventID+"/ack", memberChannelEventAckRequest{
Wallet: ownerAddr,
DeviceID: "desktop-01",
AcknowledgedAt: ackTime.Add(30 * time.Second).Format(time.RFC3339Nano),
}, http.StatusOK)
if ack2.AcknowledgedAt != ack.AcknowledgedAt {
t.Fatalf("ack should be idempotent: first=%s second=%s", ack.AcknowledgedAt, ack2.AcknowledgedAt)
}
_ = postJSONExpect[memberChannelDeviceUnregisterResponse](t, a, "/member/channel/device/unregister", memberChannelDeviceUnregisterRequest{
Wallet: ownerAddr,
DeviceID: "desktop-01",
}, http.StatusOK)
_ = getJSONExpect[map[string]string](t, a, "/member/channel/events?wallet="+ownerAddr+"&device_id=desktop-01&limit=10", http.StatusForbidden)
}
func TestMemberChannelEventsAllowUnattestedMembership(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
memberKey := mustKey(t)
memberAddr := strings.ToLower(crypto.PubkeyToAddress(memberKey.PublicKey).Hex())
if err := seedMembershipWithAssurance(context.Background(), a.store, memberAddr, assuranceCryptoDirect); err != nil {
t.Fatalf("seed member membership: %v", err)
}
_ = postJSONExpect[memberChannelDeviceRegisterResponse](t, a, "/member/channel/device/register", memberChannelDeviceRegisterRequest{
Wallet: memberAddr,
ChainID: 84532,
DeviceID: "desktop-unattested-01",
Platform: "desktop",
OrgRootID: "org.events.unattested",
PrincipalID: "human.member",
PrincipalRole: "workspace_member",
AppVersion: "0.1.0",
PushProvider: "none",
}, http.StatusOK)
events := getJSONExpect[memberChannelEventsResponse](t, a, "/member/channel/events?wallet="+memberAddr+"&device_id=desktop-unattested-01&limit=25", http.StatusOK)
if len(events.Events) == 0 {
t.Fatalf("expected seeded events for unattested member, got none")
}
if events.IdentityAssurance != assuranceCryptoDirect {
t.Fatalf("expected crypto_direct_unattested assurance for unattested member events, got %+v", events)
}
}
func TestMemberChannelSupportTicketOwnerOnly(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
memberKey := mustKey(t)
memberAddr := strings.ToLower(crypto.PubkeyToAddress(memberKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, memberAddr); err != nil {
t.Fatalf("seed member membership: %v", err)
}
_ = postJSONExpect[memberChannelDeviceRegisterResponse](t, a, "/member/channel/device/register", memberChannelDeviceRegisterRequest{
Wallet: memberAddr,
ChainID: 84532,
DeviceID: "desktop-member-01",
Platform: "desktop",
OrgRootID: "org.support.test",
PrincipalID: "human.member",
PrincipalRole: "workspace_member",
AppVersion: "0.1.0",
PushProvider: "none",
}, http.StatusOK)
errResp := postJSONExpect[map[string]string](t, a, "/member/channel/support/ticket", memberChannelSupportTicketRequest{
Wallet: memberAddr,
OrgRootID: "org.support.test",
PrincipalID: "human.member",
Category: "health_diagnostic",
Summary: "Please investigate",
}, http.StatusForbidden)
if code := errResp["code"]; code != "owner_role_required" {
t.Fatalf("expected owner_role_required, got %+v", errResp)
}
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed owner membership: %v", err)
}
_ = postJSONExpect[memberChannelDeviceRegisterResponse](t, a, "/member/channel/device/register", memberChannelDeviceRegisterRequest{
Wallet: ownerAddr,
ChainID: 84532,
DeviceID: "desktop-owner-01",
Platform: "desktop",
OrgRootID: "org.support.test",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
AppVersion: "0.1.0",
PushProvider: "none",
}, http.StatusOK)
ticket := postJSONExpect[memberChannelSupportTicketResponse](t, a, "/member/channel/support/ticket", memberChannelSupportTicketRequest{
Wallet: ownerAddr,
OrgRootID: "org.support.test",
PrincipalID: "human.owner",
Category: "admin_support",
Summary: "Need owner diagnostics.",
Context: map[string]any{
"scope": "full",
},
}, http.StatusOK)
if ticket.Status != "accepted" || ticket.TicketID == "" {
t.Fatalf("unexpected ticket response: %+v", ticket)
}
}
func TestMemberChannelSupportTicketRequiresOnrampAttestation(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedMembershipWithAssurance(context.Background(), a.store, ownerAddr, assuranceCryptoDirect); err != nil {
t.Fatalf("seed owner membership: %v", err)
}
_ = postJSONExpect[memberChannelDeviceRegisterResponse](t, a, "/member/channel/device/register", memberChannelDeviceRegisterRequest{
Wallet: ownerAddr,
ChainID: 84532,
DeviceID: "desktop-owner-unattested-01",
Platform: "desktop",
OrgRootID: "org.support.assurance",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
AppVersion: "0.1.0",
PushProvider: "none",
}, http.StatusOK)
errResp := postJSONExpect[map[string]string](t, a, "/member/channel/support/ticket", memberChannelSupportTicketRequest{
Wallet: ownerAddr,
OrgRootID: "org.support.assurance",
PrincipalID: "human.owner",
Category: "admin_support",
Summary: "Need owner diagnostics.",
}, http.StatusForbidden)
if code := errResp["code"]; code != "identity_assurance_insufficient" {
t.Fatalf("expected identity_assurance_insufficient, got %+v", errResp)
}
}
func TestMarketplaceCheckoutBundlesMembershipAndMintsEntitlement(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDWorkspaceCore,
OrgRootID: "org.marketplace.a",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
}, http.StatusOK)
if !quote.MembershipActivationIncluded {
t.Fatalf("expected bundled membership on first checkout: %+v", quote)
}
if len(quote.LineItems) < 2 {
t.Fatalf("expected license + membership line items: %+v", quote.LineItems)
}
confirm := postJSONExpect[marketplaceCheckoutConfirmResponse](t, a, "/marketplace/checkout/confirm", marketplaceCheckoutConfirmRequest{
QuoteID: quote.QuoteID,
Wallet: ownerAddr,
OfferID: offerIDWorkspaceCore,
OrgRootID: "org.marketplace.a",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
TxHash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
ChainID: cfg.ChainID,
}, http.StatusOK)
if confirm.Status != "entitlement_active" || confirm.EntitlementID == "" {
t.Fatalf("unexpected confirm response: %+v", confirm)
}
status := getJSONExpect[membershipStatusResponse](t, a, "/secret/membership/status?wallet="+ownerAddr, http.StatusOK)
if status.Status != "active" {
t.Fatalf("expected active membership after bundled checkout, got %+v", status)
}
entitlements := getJSONExpect[marketplaceEntitlementsResponse](t, a, "/marketplace/entitlements?wallet="+ownerAddr, http.StatusOK)
if len(entitlements.Entitlements) == 0 {
t.Fatalf("expected active entitlement after confirm")
}
if entitlements.Entitlements[0].OfferID != offerIDWorkspaceCore {
t.Fatalf("unexpected entitlement offer: %+v", entitlements.Entitlements[0])
}
}
func TestMarketplaceDistinctPayerRequiresOwnershipProof(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
payerKey := mustKey(t)
payerAddr := strings.ToLower(crypto.PubkeyToAddress(payerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
errResp := postJSONExpect[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
PayerWallet: payerAddr,
OfferID: offerIDWorkspaceCore,
}, http.StatusForbidden)
if code := errResp["code"]; code != "ownership_proof_required" {
t.Fatalf("expected ownership_proof_required, got %+v", errResp)
}
quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
PayerWallet: payerAddr,
OfferID: offerIDWorkspaceCore,
OwnershipProof: "0x" + strings.Repeat("a", 130),
OrgRootID: "org.marketplace.ownerproof",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
}, http.StatusOK)
if got := strings.ToLower(strings.TrimSpace(quote.PayerWallet)); got != payerAddr {
t.Fatalf("payer wallet mismatch: got=%s want=%s", got, payerAddr)
}
if quote.Decimals != 6 || quote.OfferID != offerIDWorkspaceCore || quote.AccessClass != "connected" {
t.Fatalf("unexpected quote response: %+v", quote)
}
}
func TestMarketplaceCheckoutConfirmRejectsPayerWalletMismatch(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
payerKey := mustKey(t)
payerAddr := strings.ToLower(crypto.PubkeyToAddress(payerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
PayerWallet: payerAddr,
OfferID: offerIDWorkspaceCore,
OwnershipProof: "0x" + strings.Repeat("a", 130),
OrgRootID: "org.marketplace.payer",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
}, http.StatusOK)
errResp := postJSONExpect[map[string]string](t, a, "/marketplace/checkout/confirm", marketplaceCheckoutConfirmRequest{
QuoteID: quote.QuoteID,
Wallet: ownerAddr,
PayerWallet: ownerAddr,
OfferID: offerIDWorkspaceCore,
OrgRootID: "org.marketplace.payer",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
TxHash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
ChainID: cfg.ChainID,
}, http.StatusBadRequest)
if code := errResp["code"]; code != "context_mismatch" {
t.Fatalf("expected context_mismatch, got %+v", errResp)
}
}
func TestMarketplaceCheckoutConfirmRejectsTxHashReplayAcrossQuotes(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
ownerAKey := mustKey(t)
ownerAAddr := strings.ToLower(crypto.PubkeyToAddress(ownerAKey.PublicKey).Hex())
ownerBKey := mustKey(t)
ownerBAddr := strings.ToLower(crypto.PubkeyToAddress(ownerBKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAAddr); err != nil {
t.Fatalf("seed owner A membership: %v", err)
}
if err := seedActiveMembership(context.Background(), a.store, ownerBAddr); err != nil {
t.Fatalf("seed owner B membership: %v", err)
}
quoteA := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAAddr,
OfferID: offerIDWorkspaceCore,
OrgRootID: "org.marketplace.replay.a",
PrincipalID: "human.owner.a",
PrincipalRole: "org_root_owner",
}, http.StatusOK)
replayedTxHash := "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
_ = postJSONExpect[marketplaceCheckoutConfirmResponse](t, a, "/marketplace/checkout/confirm", marketplaceCheckoutConfirmRequest{
QuoteID: quoteA.QuoteID,
Wallet: ownerAAddr,
OfferID: offerIDWorkspaceCore,
OrgRootID: "org.marketplace.replay.a",
PrincipalID: "human.owner.a",
PrincipalRole: "org_root_owner",
TxHash: replayedTxHash,
ChainID: cfg.ChainID,
}, http.StatusOK)
quoteB := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerBAddr,
OfferID: offerIDWorkspaceCore,
OrgRootID: "org.marketplace.replay.b",
PrincipalID: "human.owner.b",
PrincipalRole: "org_root_owner",
}, http.StatusOK)
errResp := postJSONExpect[map[string]string](t, a, "/marketplace/checkout/confirm", marketplaceCheckoutConfirmRequest{
QuoteID: quoteB.QuoteID,
Wallet: ownerBAddr,
OfferID: offerIDWorkspaceCore,
OrgRootID: "org.marketplace.replay.b",
PrincipalID: "human.owner.b",
PrincipalRole: "org_root_owner",
TxHash: replayedTxHash,
ChainID: cfg.ChainID,
}, http.StatusConflict)
if code := errResp["code"]; code != "tx_hash_replay" {
t.Fatalf("expected tx_hash_replay, got %+v", errResp)
}
}
func TestMarketplaceConfirmRequiresChainRPCWhenStrictVerificationEnabled(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireOnchainTxVerify = true
a.cfg.ChainRPCURL = ""
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDWorkspaceCore,
OrgRootID: "org.marketplace.strict",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
}, http.StatusOK)
errResp := postJSONExpect[map[string]string](t, a, "/marketplace/checkout/confirm", marketplaceCheckoutConfirmRequest{
QuoteID: quote.QuoteID,
Wallet: ownerAddr,
OfferID: offerIDWorkspaceCore,
OrgRootID: "org.marketplace.strict",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
TxHash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
ChainID: cfg.ChainID,
}, http.StatusServiceUnavailable)
if code := errResp["code"]; code != "chain_verification_unavailable" {
t.Fatalf("expected chain_verification_unavailable, got %+v", errResp)
}
}
func TestMarketplaceQuoteUsesEntitlementContractTransactionWhenConfigured(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.EntitlementContract = "0x1111111111111111111111111111111111111111"
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDWorkspaceCore,
OrgRootID: "org.marketplace.txshape",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
WorkspaceID: "workspace.alpha",
}, http.StatusOK)
if got := strings.ToLower(strings.TrimSpace(fmt.Sprint(quote.Tx["to"]))); got != strings.ToLower(a.cfg.EntitlementContract) {
t.Fatalf("tx.to mismatch: got=%s want=%s quote=%+v", got, a.cfg.EntitlementContract, quote.Tx)
}
if got := strings.ToLower(strings.TrimSpace(fmt.Sprint(quote.Tx["from"]))); got != ownerAddr {
t.Fatalf("tx.from mismatch: got=%s want=%s quote=%+v", got, ownerAddr, quote.Tx)
}
data := strings.ToLower(strings.TrimSpace(fmt.Sprint(quote.Tx["data"])))
if !strings.HasPrefix(data, "0x") || data == "0x" {
t.Fatalf("expected encoded entitlement calldata, got=%+v", quote.Tx)
}
}
func TestMarketplaceQuoteFailsWhenEntitlementContractUnconfigured(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.EntitlementContract = "0x0000000000000000000000000000000000000000"
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
errResp := postJSONExpect[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDWorkspaceCore,
OrgRootID: "org.marketplace.unconfigured",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
}, http.StatusServiceUnavailable)
if code := errResp["code"]; code != "entitlement_contract_unconfigured" {
t.Fatalf("expected entitlement_contract_unconfigured, got %+v", errResp)
}
}
func TestMarketplaceWorkspaceAddOnRequiresCoreEntitlement(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
errResp := postJSONExpect[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDWorkspaceAI,
OrgRootID: "org.marketplace.addon",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
}, http.StatusForbidden)
if code := errResp["code"]; code != "prerequisite_required" {
t.Fatalf("expected prerequisite_required, got %+v", errResp)
}
if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{
EntitlementID: "ent:seed:workspace-core",
QuoteID: "seed-q-workspace-core",
OfferID: offerIDWorkspaceCore,
Wallet: ownerAddr,
OrgRootID: "org.marketplace.addon",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
State: "active",
AccessClass: "connected",
AvailabilityState: "active",
PolicyHash: "sha256:testpolicy",
IssuedAt: time.Now().UTC(),
TxHash: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}); err != nil {
t.Fatalf("seed workspace core entitlement: %v", err)
}
quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDWorkspaceAI,
OrgRootID: "org.marketplace.addon",
PrincipalID: "human.owner",
PrincipalRole: "org_root_owner",
}, http.StatusOK)
if quote.OfferID != offerIDWorkspaceAI || quote.AmountAtomic != marketplaceStandardOfferAtomic {
t.Fatalf("unexpected add-on quote response: %+v", quote)
}
}
func TestMarketplaceSoloCoreScopeValidation(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
quote := postJSONExpect[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusOK)
expectedRoot := defaultSoloOrgRootID(ownerAddr)
if !strings.EqualFold(strings.TrimSpace(quote.OrgRootID), expectedRoot) {
t.Fatalf("expected auto solo org root %s, got %+v", expectedRoot, quote)
}
errResp := postJSONExpect[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
OrgRootID: "org.invalid.workspace",
PrincipalRole: "org_root_owner",
}, http.StatusBadRequest)
if code := errResp["code"]; code != "invalid_scope" {
t.Fatalf("expected invalid_scope, got %+v", errResp)
}
}
func TestWalletSessionRequiredForMarketplaceQuote(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
errResp := postJSONExpect[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusUnauthorized)
if code := errResp["code"]; code != "wallet_session_required" {
t.Fatalf("expected wallet_session_required, got %+v", errResp)
}
session, err := a.issueWalletSession(context.Background(), ownerAddr, "1234567890123")
if err != nil {
t.Fatalf("issue wallet session: %v", err)
}
quote := postJSONExpectWithHeaders[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusOK, map[string]string{
sessionHeaderToken: session.SessionToken,
})
if strings.TrimSpace(quote.QuoteID) == "" {
t.Fatalf("expected quote id with valid wallet session")
}
}
func TestWalletSessionMismatchBlocked(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
otherKey := mustKey(t)
otherAddr := strings.ToLower(crypto.PubkeyToAddress(otherKey.PublicKey).Hex())
session, err := a.issueWalletSession(context.Background(), otherAddr, "9999999999999")
if err != nil {
t.Fatalf("issue wallet session: %v", err)
}
errResp := postJSONExpectWithHeaders[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusForbidden, map[string]string{
sessionHeaderToken: session.SessionToken,
})
if code := errResp["code"]; code != "wallet_session_mismatch" {
t.Fatalf("expected wallet_session_mismatch, got %+v", errResp)
}
}
func TestWalletVerifyIssuesSessionToken(t *testing.T) {
a, cfg, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
intentRes := postJSONExpect[tWalletIntentResponse](t, a, "/secret/wallet/intent", walletIntentRequest{
Address: ownerAddr,
Origin: "https://edut.ai",
Locale: "en",
ChainID: cfg.ChainID,
}, http.StatusOK)
issuedAt, err := time.Parse(time.RFC3339Nano, intentRes.IssuedAt)
if err != nil {
t.Fatalf("parse issued_at: %v", err)
}
td := buildTypedData(cfg, designationRecord{
Code: intentRes.DesignationCode,
DisplayToken: intentRes.DisplayToken,
Nonce: intentRes.Nonce,
IssuedAt: issuedAt,
Origin: "https://edut.ai",
})
sig := signTypedData(t, ownerKey, td)
verifyRes := postJSONExpect[tWalletVerifyResponse](t, a, "/secret/wallet/verify", walletVerifyRequest{
IntentID: intentRes.IntentID,
Address: ownerAddr,
ChainID: cfg.ChainID,
Signature: sig,
}, http.StatusOK)
if strings.TrimSpace(verifyRes.SessionToken) == "" {
t.Fatalf("expected wallet verify to issue session token: %+v", verifyRes)
}
if strings.TrimSpace(verifyRes.SessionExpiresAt) == "" {
t.Fatalf("expected wallet verify to return session expiry: %+v", verifyRes)
}
}
func TestWalletSessionInvalidBlocked(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
errResp := postJSONExpectWithHeaders[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusUnauthorized, map[string]string{
sessionHeaderToken: "deadbeef",
})
if code := errResp["code"]; code != "wallet_session_invalid" {
t.Fatalf("expected wallet_session_invalid, got %+v", errResp)
}
}
func TestWalletSessionExpiredBlocked(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
now := time.Now().UTC()
session := walletSessionRecord{
SessionToken: "expired-session-token",
Wallet: ownerAddr,
DesignationCode: "1234567890123",
ChainID: a.cfg.ChainID,
IssuedAt: now.Add(-2 * time.Hour),
ExpiresAt: now.Add(-1 * time.Minute),
}
if err := a.store.putWalletSession(context.Background(), session); err != nil {
t.Fatalf("seed wallet session: %v", err)
}
errResp := postJSONExpectWithHeaders[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusUnauthorized, map[string]string{
sessionHeaderToken: session.SessionToken,
})
if code := errResp["code"]; code != "wallet_session_expired" {
t.Fatalf("expected wallet_session_expired, got %+v", errResp)
}
}
func TestWalletSessionRevokedBlocked(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
now := time.Now().UTC()
session := walletSessionRecord{
SessionToken: "revoked-session-token",
Wallet: ownerAddr,
DesignationCode: "1234567890123",
ChainID: a.cfg.ChainID,
IssuedAt: now.Add(-1 * time.Hour),
ExpiresAt: now.Add(1 * time.Hour),
RevokedAt: &now,
}
if err := a.store.putWalletSession(context.Background(), session); err != nil {
t.Fatalf("seed wallet session: %v", err)
}
errResp := postJSONExpectWithHeaders[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusUnauthorized, map[string]string{
sessionHeaderToken: session.SessionToken,
})
if code := errResp["code"]; code != "wallet_session_revoked" {
t.Fatalf("expected wallet_session_revoked, got %+v", errResp)
}
}
func TestWalletSessionRefreshRotatesToken(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
session, err := a.issueWalletSession(context.Background(), ownerAddr, "1234567890123")
if err != nil {
t.Fatalf("issue wallet session: %v", err)
}
refresh := postJSONExpectWithHeaders[walletSessionRefreshResponse](t, a, "/secret/wallet/session/refresh", walletSessionRefreshRequest{
Wallet: ownerAddr,
}, http.StatusOK, map[string]string{
sessionHeaderToken: session.SessionToken,
})
if refresh.Status != "session_refreshed" {
t.Fatalf("expected refreshed status, got %+v", refresh)
}
if strings.TrimSpace(refresh.SessionToken) == "" || refresh.SessionToken == session.SessionToken {
t.Fatalf("expected rotated token, got %+v", refresh)
}
oldBlocked := postJSONExpectWithHeaders[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusUnauthorized, map[string]string{
sessionHeaderToken: session.SessionToken,
})
if code := oldBlocked["code"]; code != "wallet_session_revoked" {
t.Fatalf("expected old token revoked, got %+v", oldBlocked)
}
newAllowed := postJSONExpectWithHeaders[marketplaceCheckoutQuoteResponse](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusOK, map[string]string{
sessionHeaderToken: refresh.SessionToken,
})
if strings.TrimSpace(newAllowed.QuoteID) == "" {
t.Fatalf("expected quote with refreshed token")
}
}
func TestWalletSessionRevokeEndpointBlocksFurtherUse(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
session, err := a.issueWalletSession(context.Background(), ownerAddr, "1234567890123")
if err != nil {
t.Fatalf("issue wallet session: %v", err)
}
revoked := postJSONExpectWithHeaders[walletSessionRevokeResponse](t, a, "/secret/wallet/session/revoke", walletSessionRevokeRequest{
Wallet: ownerAddr,
}, http.StatusOK, map[string]string{
sessionHeaderToken: session.SessionToken,
})
if revoked.Status != "session_revoked" {
t.Fatalf("unexpected revoke response: %+v", revoked)
}
errResp := postJSONExpectWithHeaders[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
Wallet: ownerAddr,
OfferID: offerIDSoloCore,
PrincipalRole: "org_root_owner",
}, http.StatusUnauthorized, map[string]string{
sessionHeaderToken: session.SessionToken,
})
if code := errResp["code"]; code != "wallet_session_revoked" {
t.Fatalf("expected revoked token to fail, got %+v", errResp)
}
}
func TestMemberChannelRegisterRequiresWalletSession(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
errResp := postJSONExpect[map[string]string](t, a, "/member/channel/device/register", memberChannelDeviceRegisterRequest{
Wallet: ownerAddr,
ChainID: a.cfg.ChainID,
DeviceID: "desktop-local-01",
Platform: "desktop",
OrgRootID: "org_test",
PrincipalID: "principal_test",
PrincipalRole: "org_root_owner",
AppVersion: "0.1.0",
PushProvider: "none",
}, http.StatusUnauthorized)
if code := errResp["code"]; code != "wallet_session_required" {
t.Fatalf("expected wallet_session_required, got %+v", errResp)
}
}
func TestGovernanceInstallTokenRequiresWalletSession(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
a.cfg.RequireWalletSession = true
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
t.Fatalf("seed active membership: %v", err)
}
now := time.Now().UTC()
if err := a.store.putGovernancePrincipal(context.Background(), governancePrincipalRecord{
Wallet: ownerAddr,
OrgRootID: "org_test",
PrincipalID: "principal_test",
PrincipalRole: "org_root_owner",
EntitlementID: "ent_test",
EntitlementStatus: "active",
AccessClass: "connected",
AvailabilityState: "active",
UpdatedAt: now,
}); err != nil {
t.Fatalf("seed governance principal: %v", err)
}
errResp := postJSONExpect[map[string]string](t, a, "/governance/install/token", governanceInstallTokenRequest{
Wallet: ownerAddr,
OrgRootID: "org_test",
PrincipalID: "principal_test",
PrincipalRole: "org_root_owner",
DeviceID: "device-test",
LauncherVersion: "0.1.0",
Platform: "desktop",
}, http.StatusUnauthorized)
if code := errResp["code"]; code != "wallet_session_required" {
t.Fatalf("expected wallet_session_required, got %+v", errResp)
}
}
func TestIssueWalletSessionPrunesExpired(t *testing.T) {
a, _, cleanup := newTestApp(t)
defer cleanup()
ownerKey := mustKey(t)
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
now := time.Now().UTC()
expired := walletSessionRecord{
SessionToken: "prune-me",
Wallet: ownerAddr,
DesignationCode: "1111111111111",
ChainID: a.cfg.ChainID,
IssuedAt: now.Add(-3 * time.Hour),
ExpiresAt: now.Add(-2 * time.Hour),
}
if err := a.store.putWalletSession(context.Background(), expired); err != nil {
t.Fatalf("seed expired session: %v", err)
}
if _, err := a.issueWalletSession(context.Background(), ownerAddr, "2222222222222"); err != nil {
t.Fatalf("issue wallet session: %v", err)
}
_, err := a.store.getWalletSession(context.Background(), expired.SessionToken)
if err != errNotFound {
t.Fatalf("expected expired session to be pruned, got err=%v", err)
}
}
type tWalletIntentResponse struct {
IntentID string `json:"intent_id"`
DesignationCode string `json:"designation_code"`
DisplayToken string `json:"display_token"`
Nonce string `json:"nonce"`
IssuedAt string `json:"issued_at"`
}
type tWalletVerifyResponse struct {
Status string `json:"status"`
DesignationCode string `json:"designation_code"`
SessionToken string `json:"session_token"`
SessionExpiresAt string `json:"session_expires_at"`
}
func newTestApp(t *testing.T) (*app, Config, func()) {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "secretapi-test.db")
cfg := loadConfig()
cfg.DBPath = dbPath
cfg.AllowedOrigin = "*"
cfg.ChainRPCURL = ""
cfg.GovernancePackageHash = "sha256:testpackage"
cfg.GovernancePolicyHash = "sha256:testpolicy"
cfg.GovernancePackageURL = "https://cdn.test/edutd.tar.gz"
cfg.GovernancePackageSig = "sig-test"
cfg.GovernanceRuntimeVersion = "0.2.0"
cfg.RequireWalletSession = false
cfg.EntitlementContract = "0x1111111111111111111111111111111111111111"
st, err := openStore(cfg.DBPath)
if err != nil {
t.Fatalf("open store: %v", err)
}
return newApp(cfg, st), cfg, func() {
_ = st.close()
}
}
func seedActiveMembership(ctx context.Context, st *store, wallet string) error {
return seedMembershipWithAssurance(ctx, st, wallet, assuranceOnrampAttested)
}
func seedMembershipWithAssurance(ctx context.Context, st *store, wallet, assurance string) error {
now := time.Now().UTC()
assurance = normalizeAssuranceLevel(assurance)
attestedBy := ""
var attestedAt *time.Time
if assurance == assuranceOnrampAttested {
attestedBy = "test-onramp"
attestedAt = &now
}
return st.putDesignation(ctx, designationRecord{
Code: "1234567890123",
DisplayToken: "1234-5678-9012-3",
IntentID: "intent-seeded",
Nonce: "nonce-seeded",
Origin: "https://edut.ai",
Locale: "en",
Address: wallet,
ChainID: 84532,
IssuedAt: now.Add(-1 * time.Hour),
ExpiresAt: now.Add(1 * time.Hour),
VerifiedAt: &now,
MembershipStatus: "active",
MembershipTxHash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
ActivatedAt: &now,
IdentityAssurance: assurance,
IdentityAttestedBy: attestedBy,
IdentityAttestationID: "test-attestation",
IdentityAttestedAt: attestedAt,
})
}
func postJSONExpect[T any](t *testing.T, a *app, path string, payload any, expectStatus int) T {
t.Helper()
return postJSONExpectWithHeaders[T](t, a, path, payload, expectStatus, nil)
}
func postJSONExpectWithHeaders[T any](t *testing.T, a *app, path string, payload any, expectStatus int, headers map[string]string) T {
t.Helper()
body, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal request: %v", err)
}
req := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Origin", "https://edut.ai")
for k, v := range headers {
req.Header.Set(k, v)
}
rec := httptest.NewRecorder()
a.routes().ServeHTTP(rec, req)
if rec.Code != expectStatus {
t.Fatalf("%s status=%d body=%s", path, rec.Code, rec.Body.String())
}
var out T
if len(rec.Body.Bytes()) == 0 {
return out
}
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
t.Fatalf("decode response: %v body=%s", err, rec.Body.String())
}
return out
}
func getJSONExpect[T any](t *testing.T, a *app, path string, expectStatus int) T {
t.Helper()
return getJSONExpectWithHeaders[T](t, a, path, expectStatus, nil)
}
func getJSONExpectWithHeaders[T any](t *testing.T, a *app, path string, expectStatus int, headers map[string]string) T {
t.Helper()
req := httptest.NewRequest(http.MethodGet, path, nil)
req.Header.Set("Origin", "https://edut.ai")
for k, v := range headers {
req.Header.Set(k, v)
}
rec := httptest.NewRecorder()
a.routes().ServeHTTP(rec, req)
if rec.Code != expectStatus {
t.Fatalf("%s status=%d body=%s", path, rec.Code, rec.Body.String())
}
var out T
if len(rec.Body.Bytes()) == 0 {
return out
}
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
t.Fatalf("decode response: %v body=%s", err, rec.Body.String())
}
return out
}
func mustKey(t *testing.T) *ecdsa.PrivateKey {
t.Helper()
key, err := crypto.GenerateKey()
if err != nil {
t.Fatalf("generate key: %v", err)
}
return key
}
func signTypedData(t *testing.T, key *ecdsa.PrivateKey, typedData apitypes.TypedData) string {
t.Helper()
hash, _, err := apitypes.TypedDataAndHash(typedData)
if err != nil {
t.Fatalf("typed data hash: %v", err)
}
sig, err := crypto.Sign(hash, key)
if err != nil {
t.Fatalf("sign typed data: %v", err)
}
return "0x" + hex.EncodeToString(sig)
}
func signPersonalMessage(t *testing.T, key *ecdsa.PrivateKey, message string) string {
t.Helper()
hash := accounts.TextHash([]byte(message))
sig, err := crypto.Sign(hash, key)
if err != nil {
t.Fatalf("sign personal message: %v", err)
}
return "0x" + hex.EncodeToString(sig)
}