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) } assertQuoteCostEnvelope(t, quote.CostEnvelope, quote.Currency, quote.Decimals, quote.AmountAtomic) assertRegulatoryProfileID(t, quote.RegulatoryProfileID, cfg.RegulatoryProfileID) } 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 TestEDUTIDAliasRoutesActivateMembership(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/id/quote", membershipQuoteRequest{ DesignationCode: verifyRes.DesignationCode, Address: ownerAddr, ChainID: cfg.ChainID, }, http.StatusOK) if strings.TrimSpace(quote.QuoteID) == "" { t.Fatalf("expected quote id from /secret/id/quote, got %+v", quote) } confirm := postJSONExpect[membershipConfirmResponse](t, a, "/secret/id/confirm", membershipConfirmRequest{ DesignationCode: verifyRes.DesignationCode, QuoteID: quote.QuoteID, TxHash: "0x" + strings.Repeat("b", 64), Address: ownerAddr, ChainID: cfg.ChainID, }, http.StatusOK) if confirm.Status != "membership_active" { t.Fatalf("expected membership_active status from /secret/id/confirm, got %+v", confirm) } status := getJSONExpect[membershipStatusResponse](t, a, "/secret/id/status?wallet="+ownerAddr, http.StatusOK) if status.Status != "active" { t.Fatalf("expected active status from /secret/id/status, got %+v", status) } if status.DesignationCode != verifyRes.DesignationCode { t.Fatalf("designation mismatch: got=%s want=%s", status.DesignationCode, verifyRes.DesignationCode) } } 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) } if quote.MerchantID != defaultMarketplaceMerchantID { t.Fatalf("expected default merchant id, got %+v", quote) } assertQuoteCostEnvelope(t, quote.CostEnvelope, quote.Currency, quote.Decimals, quote.TotalAmountAtomic) assertRegulatoryProfileID(t, quote.RegulatoryProfileID, cfg.RegulatoryProfileID) 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) } assertRegulatoryProfileID(t, confirm.RegulatoryProfileID, cfg.RegulatoryProfileID) 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) } assertRegulatoryProfileID(t, status.RegulatoryProfileID, cfg.RegulatoryProfileID) 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]) } if entitlements.Entitlements[0].MerchantID != defaultMarketplaceMerchantID { t.Fatalf("expected default merchant id on entitlement, got %+v", entitlements.Entitlements[0]) } } func TestMarketplaceQuoteRejectsUnknownMerchant(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{ MerchantID: "acme.workspace", Wallet: ownerAddr, OfferID: offerIDWorkspaceCore, OrgRootID: "org.marketplace.acme", PrincipalID: "human.owner", PrincipalRole: "org_root_owner", }, http.StatusNotFound) if code := errResp["code"]; code != "offer_not_found" { t.Fatalf("expected offer_not_found for unknown merchant, got %+v", errResp) } } func TestMarketplaceEntitlementsFilterByMerchant(t *testing.T) { a, _, cleanup := newTestApp(t) defer cleanup() ownerKey := mustKey(t) ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex()) now := time.Now().UTC() if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{ EntitlementID: "ent:seed:firstparty", QuoteID: "seed-q-firstparty", MerchantID: defaultMarketplaceMerchantID, OfferID: offerIDWorkspaceCore, Wallet: ownerAddr, OrgRootID: "org.marketplace.filter", PrincipalID: "human.owner", PrincipalRole: "org_root_owner", State: "active", AccessClass: "connected", AvailabilityState: "active", PolicyHash: "sha256:testpolicy", IssuedAt: now, TxHash: "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", }); err != nil { t.Fatalf("seed first-party entitlement: %v", err) } if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{ EntitlementID: "ent:seed:other", QuoteID: "seed-q-other", MerchantID: "acme.workspace", OfferID: "acme.offer.alpha", Wallet: ownerAddr, OrgRootID: "org.marketplace.filter", PrincipalID: "human.owner", PrincipalRole: "org_root_owner", State: "active", AccessClass: "connected", AvailabilityState: "active", PolicyHash: "sha256:testpolicy", IssuedAt: now, TxHash: "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", }); err != nil { t.Fatalf("seed third-party entitlement: %v", err) } all := getJSONExpect[marketplaceEntitlementsResponse](t, a, "/marketplace/entitlements?wallet="+ownerAddr, http.StatusOK) if len(all.Entitlements) != 2 { t.Fatalf("expected both merchant entitlements, got %+v", all.Entitlements) } firstParty := getJSONExpect[marketplaceEntitlementsResponse](t, a, "/marketplace/entitlements?wallet="+ownerAddr+"&merchant_id="+defaultMarketplaceMerchantID, http.StatusOK) if len(firstParty.Entitlements) != 1 { t.Fatalf("expected one first-party entitlement, got %+v", firstParty.Entitlements) } if firstParty.Entitlements[0].MerchantID != defaultMarketplaceMerchantID { t.Fatalf("expected first-party merchant id, got %+v", firstParty.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 assertQuoteCostEnvelope(t *testing.T, got quoteCostEnvelope, wantCurrency string, wantDecimals int, wantAmountAtomic string) { t.Helper() if got.Version != quoteCostEnvelopeVersion { t.Fatalf("cost envelope version mismatch: got=%s want=%s", got.Version, quoteCostEnvelopeVersion) } if !strings.EqualFold(strings.TrimSpace(got.CheckoutCurrency), strings.TrimSpace(wantCurrency)) { t.Fatalf("cost envelope checkout currency mismatch: got=%s want=%s", got.CheckoutCurrency, wantCurrency) } if got.CheckoutDecimals != wantDecimals { t.Fatalf("cost envelope decimals mismatch: got=%d want=%d", got.CheckoutDecimals, wantDecimals) } if strings.TrimSpace(got.CheckoutTotalAtomic) != strings.TrimSpace(wantAmountAtomic) { t.Fatalf("cost envelope total atomic mismatch: got=%s want=%s", got.CheckoutTotalAtomic, wantAmountAtomic) } if strings.TrimSpace(got.CheckoutTotal) != strings.TrimSpace(formatAtomicAmount(wantAmountAtomic, wantDecimals)) { t.Fatalf("cost envelope total mismatch: got=%s want=%s", got.CheckoutTotal, formatAtomicAmount(wantAmountAtomic, wantDecimals)) } if got.ProviderFeePolicy != quoteProviderFeePolicyEdutAbsorbed { t.Fatalf("cost envelope provider fee policy mismatch: got=%s", got.ProviderFeePolicy) } if !got.ProviderFeeIncluded { t.Fatalf("cost envelope provider fee should be included") } if got.ProviderFeeEstimateStatus != quoteProviderFeeEstimateStatusAbsorbed { t.Fatalf("cost envelope provider fee status mismatch: got=%s", got.ProviderFeeEstimateStatus) } if got.ProviderFeeEstimateAtomic != "0" { t.Fatalf("cost envelope provider fee estimate mismatch: got=%s", got.ProviderFeeEstimateAtomic) } if got.NetworkFeePolicy != quoteNetworkFeePolicyPayerPaysGas { t.Fatalf("cost envelope network fee policy mismatch: got=%s", got.NetworkFeePolicy) } if got.NetworkFeeCurrency != quoteNetworkFeeDefaultCurrency { t.Fatalf("cost envelope network fee currency mismatch: got=%s", got.NetworkFeeCurrency) } if got.NetworkFeeEstimateStatus != quoteNetworkFeeEstimateStatusWalletQuoted { t.Fatalf("cost envelope network fee status mismatch: got=%s", got.NetworkFeeEstimateStatus) } if got.NetworkFeeEstimateAtomic != "0" { t.Fatalf("cost envelope network fee estimate mismatch: got=%s", got.NetworkFeeEstimateAtomic) } } func assertRegulatoryProfileID(t *testing.T, got string, want string) { t.Helper() if normalizeRegulatoryProfileID(got) != normalizeRegulatoryProfileID(want) { t.Fatalf("regulatory profile mismatch: got=%s want=%s", got, want) } } 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) }