test/openagents/settlement/constraints_test.exs

main at 58e6347eeb72 · 8 KB

defmodule OpenAgents.Settlement.ConstraintsTest do
  @moduledoc """
  SETTLEMENT-001, at the database. Every claim here bypasses the Ecto changeset
  and writes raw SQL, because the property is that PostgreSQL refuses the row —
  not that the application declines to build it.
  """

  use OpenAgents.DataCase, async: false

  import OpenAgents.IssuesFixtures

  alias OpenAgents.Repo

  @insert_policy """
  INSERT INTO settlement_treasury_policies
    (id, policy_id, version, policy_digest, rules, actor_id, auth_method,
     approval_receipt_ref, inserted_at)
  VALUES ($1, $2, 1, $3, $4, $5, $6, $7, $8)
  """

  @insert_spec """
  INSERT INTO settlement_bounty_specs
    (id, treasury_policy_id, issue_id, revision, buyer_ref, amount_sats,
     acceptance_criteria, verification_policy, destination_kind, expires_at,
     spec_fingerprint, actor_id, auth_method, approval_receipt_ref, inserted_at)
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
  """

  @insert_claim """
  INSERT INTO settlement_claims
    (id, bounty_spec_id, spec_fingerprint, claimant_ref, work_job_ref,
     destination_kind, destination, destination_digest, state, claim_digest,
     expires_at, inserted_at, updated_at)
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $12)
  """

  @insert_verification """
  INSERT INTO settlement_verifications
    (id, claim_id, spec_fingerprint, commit_sha, work_job_ref, verifier_ref,
     verifier_policy_digest, evidence_digest, outcome, reason_code, auth_method,
     decision_receipt_ref, inserted_at)
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
  """

  @insert_intent """
  INSERT INTO settlement_payment_intents
    (id, claim_id, verification_id, idempotency_key, amount_sats, commit_sha,
     destination_digest, spec_fingerprint, state, attempts, intent_digest,
     actor_id, auth_method, approval_receipt_ref, inserted_at, updated_at)
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $15)
  """

  @insert_receipt """
  INSERT INTO settlement_payment_receipts
    (id, payment_intent_id, claim_id, amount_sats, fee_sats, payment_hash,
     preimage_digest, gateway_ref, paid_at, receipt_digest, inserted_at)
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
  """

  setup do
    repository = repository_fixture()
    issue = issue_fixture(repository, %{title: "Settlement constraint test"})
    now = DateTime.utc_now()
    expires_at = DateTime.add(now, 900, :second)

    policy = insert_policy!(now)
    spec = insert_spec!(issue.id, policy.id, 1, now, expires_at)
    claim = insert_claim!(spec, now, expires_at)
    verification = insert_verification!(claim, now)

    %{
      issue: issue,
      policy: policy,
      spec: spec,
      claim: claim,
      verification: verification,
      now: now,
      expires_at: expires_at
    }
  end

  describe "payment receipt uniqueness" do
    test "a payment hash cannot be reused for another receipt", context do
      intent_1_id = Ecto.UUID.generate()
      payment_hash = hex64()

      assert {:ok, %{num_rows: 1}} =
               insert_intent(intent_1_id, context, "intent-#{unique()}", "paid")

      assert {:ok, %{num_rows: 1}} =
               insert_receipt(
                 Ecto.UUID.generate(),
                 intent_1_id,
                 context.claim.id,
                 payment_hash,
                 context.now
               )

      second =
        insert_issue_chain!(
          context.issue.id,
          context.policy.id,
          2,
          context.now,
          context.expires_at
        )
        |> Map.put(:now, context.now)

      intent_2_id = Ecto.UUID.generate()

      assert {:ok, %{num_rows: 1}} =
               insert_intent(intent_2_id, second, "intent-#{unique()}", "paid")

      assert {:error, %Postgrex.Error{} = error} =
               insert_receipt(
                 Ecto.UUID.generate(),
                 intent_2_id,
                 second.claim.id,
                 payment_hash,
                 context.now
               )

      assert error.postgres.constraint ==
               "settlement_payment_receipts_payment_hash_index"
    end

    test "a second receipt for one payment intent is refused", context do
      intent_id = Ecto.UUID.generate()
      first_hash = hex64()
      second_hash = hex64()

      assert {:ok, %{num_rows: 1}} =
               insert_intent(intent_id, context, "intent-#{unique()}", "paid")

      assert {:ok, %{num_rows: 1}} =
               insert_receipt(
                 Ecto.UUID.generate(),
                 intent_id,
                 context.claim.id,
                 first_hash,
                 context.now
               )

      assert {:error, %Postgrex.Error{} = error} =
               insert_receipt(
                 Ecto.UUID.generate(),
                 intent_id,
                 context.claim.id,
                 second_hash,
                 context.now
               )

      assert error.postgres.constraint ==
               "settlement_payment_receipts_payment_intent_id_index"
    end
  end

  describe "payment intent partial uniqueness" do
    test "two paid intents for one claim are refused", context do
      first_id = Ecto.UUID.generate()

      assert {:ok, %{num_rows: 1}} =
               insert_intent(first_id, context, "intent-#{unique()}", "paid")

      second_id = Ecto.UUID.generate()

      assert {:error, %Postgrex.Error{} = error} =
               insert_intent(second_id, context, "intent-#{unique()}", "paid")

      assert error.postgres.constraint == "settlement_payment_intent_single_paid"
    end
  end

  defp insert_policy!(now) do
    id = Ecto.UUID.generate()

    Repo.query!(@insert_policy, [
      uuid(id),
      "policy:#{unique()}",
      hex64(),
      %{"max_payment_sats" => 100_000, "daily_budget_sats" => 1_000_000},
      "actor:operator",
      "session",
      "approval:#{unique()}",
      now
    ])

    %{id: id}
  end

  defp insert_spec!(issue_id, policy_id, revision, now, expires_at) do
    id = Ecto.UUID.generate()
    fingerprint = hex64()

    Repo.query!(@insert_spec, [
      uuid(id),
      uuid(policy_id),
      issue_id,
      revision,
      "buyer:openagents",
      2_500,
      ["The constraint holds."],
      %{"name" => "forge.precommit.v1", "requires" => ["mix precommit"]},
      "bolt12_offer",
      expires_at,
      fingerprint,
      "actor:operator",
      "session",
      "approval:#{unique()}",
      now
    ])

    %{id: id, spec_fingerprint: fingerprint}
  end

  defp insert_claim!(spec, now, expires_at) do
    id = Ecto.UUID.generate()
    destination_digest = hex64()
    fingerprint = spec.spec_fingerprint

    Repo.query!(@insert_claim, [
      uuid(id),
      uuid(spec.id),
      fingerprint,
      "agent:claimant-#{unique()}",
      "work-job:#{unique()}",
      "bolt12_offer",
      "lno1#{String.duplicate("q", 40)}",
      destination_digest,
      "verified",
      hex64(),
      expires_at,
      now
    ])

    %{id: id, spec_fingerprint: fingerprint, destination_digest: destination_digest}
  end

  defp insert_verification!(claim, now) do
    id = Ecto.UUID.generate()
    commit_sha = hex40()

    Repo.query!(@insert_verification, [
      uuid(id),
      uuid(claim.id),
      claim.spec_fingerprint,
      commit_sha,
      "work-job:#{unique()}",
      "verifier:forge-precommit",
      hex64(),
      hex64(),
      "accepted",
      "criteria_met",
      "session",
      "decision:#{unique()}",
      now
    ])

    %{id: id, commit_sha: commit_sha}
  end

  defp insert_issue_chain!(issue_id, policy_id, revision, now, expires_at) do
    spec = insert_spec!(issue_id, policy_id, revision, now, expires_at)
    claim = insert_claim!(spec, now, expires_at)
    verification = insert_verification!(claim, now)

    %{spec: spec, claim: claim, verification: verification}
  end

  defp insert_intent(intent_id, context, idempotency_key, state) do
    Repo.query(@insert_intent, [
      uuid(intent_id),
      uuid(context.claim.id),
      uuid(context.verification.id),
      idempotency_key,
      2_500,
      context.verification.commit_sha,
      context.claim.destination_digest,
      context.spec.spec_fingerprint,
      state,
      0,
      hex64(),
      "actor:operator",
      "session",
      "approval:#{unique()}",
      context.now
    ])
  end

  defp insert_receipt(receipt_id, payment_intent_id, claim_id, payment_hash, now) do
    Repo.query(@insert_receipt, [
      uuid(receipt_id),
      uuid(payment_intent_id),
      uuid(claim_id),
      2_500,
      3,
      payment_hash,
      hex64(),
      "gateway:#{unique()}",
      now,
      hex64(),
      now
    ])
  end

  defp hex40, do: Base.encode16(:crypto.strong_rand_bytes(20), case: :lower)
  defp hex64, do: Base.encode16(:crypto.strong_rand_bytes(32), case: :lower)
  defp unique, do: System.unique_integer([:positive, :monotonic])
  defp uuid(value), do: Ecto.UUID.dump!(value)
end