defmodule VavPipeline.BigdeiKodeshTest do @moduledoc """ Tests for the Bigdei Kodesh — the HTTP API layer. Shemot 28:2 — V'asita vigdei kodesh l'Aharon achicha, l'khavod ul'tifaret. Four garments for four access tiers: /peah/* — public, no auth /courtyard/* — authenticated /chatzer/* — full access /kodesh/* — operator """ use ExUnit.Case, async: false import Plug.Test import Plug.Conn alias VavPipeline.BigdeiKodesh.Router @opts Router.init([]) # ============================================================ # Pe'ah — Public Endpoint (Machaneh Yisrael) # ============================================================ describe "Pe'ah endpoint" do test "GET /peah/ask without question returns usage" do conn = conn(:get, "/peah/ask") |> Router.call(@opts) assert conn.status == 200 body = Jason.decode!(conn.resp_body) assert body["message"] =~ "Shorashim" assert body["usage"] =~ "/peah/ask" end test "GET /peah/ask with question parameter processes it" do conn = conn(:get, "/peah/ask?question=What+is+bereishit") |> Router.call(@opts) # Should return 200 or 503 (if waiting for provision) assert conn.status in [200, 503] end test "POST /peah/ask with valid question" do conn = conn(:post, "/peah/ask", Jason.encode!(%{question: "What is bereishit?"})) |> put_req_header("content-type", "application/json") |> Router.call(@opts) assert conn.status in [200, 503] body = Jason.decode!(conn.resp_body) if conn.status == 200 do assert Map.has_key?(body, "answer") or Map.has_key?(body, "processing") end end test "POST /peah/ask without question returns 422" do conn = conn(:post, "/peah/ask", Jason.encode!(%{not_question: "wrong field"})) |> put_req_header("content-type", "application/json") |> Router.call(@opts) assert conn.status == 422 body = Jason.decode!(conn.resp_body) assert body["error"]["code"] == "KIYOR_VALIDATION_FAILED" assert body["error"]["type"] == "purification_needed" end test "POST /peah/ask with empty question returns 422" do conn = conn(:post, "/peah/ask", Jason.encode!(%{question: ""})) |> put_req_header("content-type", "application/json") |> Router.call(@opts) assert conn.status == 422 end end # ============================================================ # Pe'ah — Azazel disposal log (commit 8, public half of property 9) # ============================================================ # Privacy boundary tests run WITHOUT a database (the empty L3 case # exercises the response shape and the privacy guarantees). Tests # that need actual disposal records to query are tagged :l3. describe "GET /peah/azazel_log — public observability" do test "responds with 200 even when no disposals are recorded" do conn = conn(:get, "/peah/azazel_log") |> Router.call(@opts) assert conn.status == 200 body = Jason.decode!(conn.resp_body) assert is_integer(body["total_disposals"]) assert is_list(body["recent"]) assert is_map(body["categorical_aggregate"]) end test "response carries the privacy boundary statement" do conn = conn(:get, "/peah/azazel_log") |> Router.call(@opts) body = Jason.decode!(conn.resp_body) # Property 9's privacy boundary must be visible to the caller — # they need to know that the items they don't see are # operator-internal, not missing. assert body["privacy_boundary"] =~ "operator-internal" assert body["privacy_boundary"] =~ "Property 9" end test "response carries the disclosure block (kichush + horaah)" do conn = conn(:get, "/peah/azazel_log") |> Router.call(@opts) body = Jason.decode!(conn.resp_body) # Mirrors every other Torah-content response from commit 5e00f75 assert body["disclosure"]["origin"] assert body["disclosure"]["not_horaah"] assert body["disclosure"]["torah_basis"] end test "since=invalid returns all_time" do conn = conn(:get, "/peah/azazel_log?since=not-a-timestamp") |> Router.call(@opts) body = Jason.decode!(conn.resp_body) assert body["since"] == "all_time" end test "since=valid_iso8601 is echoed in response" do conn = conn(:get, "/peah/azazel_log?since=2026-04-28T00:00:00Z") |> Router.call(@opts) body = Jason.decode!(conn.resp_body) assert body["since"] == "2026-04-28T00:00:00Z" end test "categorical_aggregate has all three slots even when empty" do conn = conn(:get, "/peah/azazel_log") |> Router.call(@opts) body = Jason.decode!(conn.resp_body) agg = body["categorical_aggregate"] # All three slots present (the public sees the SHAPE of the # vidui structure, not its contents) assert Map.has_key?(agg, "avonot") assert Map.has_key?(agg, "peshaim") assert Map.has_key?(agg, "chatot") end @tag :l3 test "with disposals: aggregate counts categories accurately" do :ok = Ecto.Adapters.SQL.Sandbox.checkout(VavPipeline.Repo) base = %{ case_ref: "log-001", vidui: %{ "avonot" => ["halt avodah_zarah: x"], "peshaim" => [], "chatot" => [], "confessor" => "kohen", "confessed_at" => "2026-04-28T12:00:00Z" }, routing: "eretz_gezerah", authorized_by: "kohen-gadol", shaliach: "ish-itti", disposed_at: ~U[2026-04-28 12:00:01Z] } {:ok, _} = VavPipeline.L3.Store.record_disposal(base) {:ok, _} = VavPipeline.L3.Store.record_disposal(Map.put(base, :case_ref, "log-002")) conn = conn(:get, "/peah/azazel_log") |> Router.call(@opts) body = Jason.decode!(conn.resp_body) assert body["total_disposals"] == 2 assert body["categorical_aggregate"]["avonot"] == 2 end @tag :l3 test "PRIVACY BOUNDARY: response NEVER contains vidui item strings" do :ok = Ecto.Adapters.SQL.Sandbox.checkout(VavPipeline.Repo) secret_phrase = "kohen-confidential-content-that-must-never-leak-to-public-endpoint" attrs = %{ case_ref: "privacy-001", vidui: %{ "avonot" => [], "peshaim" => [], "chatot" => [secret_phrase], "confessor" => "kohen", "confessed_at" => "2026-04-28T12:00:00Z" }, routing: "eretz_gezerah", authorized_by: "kohen-gadol", shaliach: "ish-itti", disposed_at: ~U[2026-04-28 12:00:01Z] } {:ok, _} = VavPipeline.L3.Store.record_disposal(attrs) conn = conn(:get, "/peah/azazel_log") |> Router.call(@opts) # The response body — as a string — must NOT contain the # vidui item content. This is the load-bearing privacy # guarantee; if the secret_phrase appears anywhere in the # response, the public endpoint is leaking operator-internal # state. refute conn.resp_body =~ secret_phrase end end # ============================================================ # Courtyard — Authenticated Endpoint # ============================================================ describe "Courtyard endpoint" do test "POST /courtyard/offering without auth returns 401" do conn = conn(:post, "/courtyard/offering", Jason.encode!(%{content: "test"})) |> put_req_header("content-type", "application/json") |> Router.call(@opts) assert conn.status == 401 body = Jason.decode!(conn.resp_body) assert body["error"]["code"] == "UNAUTHORIZED" end test "POST /courtyard/offering with auth but no content returns 422" do conn = conn(:post, "/courtyard/offering", Jason.encode!(%{offering_type: "shelamim"})) |> put_req_header("content-type", "application/json") |> put_req_header("authorization", "Bearer test-token") |> Router.call(@opts) assert conn.status == 422 body = Jason.decode!(conn.resp_body) assert body["error"]["code"] == "KIYOR_VALIDATION_FAILED" end test "POST /courtyard/offering with auth and content processes" do conn = conn(:post, "/courtyard/offering", Jason.encode!(%{ content: "bereishit bara elohim", offering_type: "shelamim" })) |> put_req_header("content-type", "application/json") |> put_req_header("authorization", "Bearer test-token") |> Router.call(@opts) assert conn.status in [200, 503] if conn.status == 200 do body = Jason.decode!(conn.resp_body) assert Map.has_key?(body, "offering_id") assert Map.has_key?(body, "offering_receipt") assert body["offering_receipt"]["type"] == "shelamim" end end end # ============================================================ # Chatzer — Sanhedrin Endpoint # ============================================================ describe "Chatzer endpoint" do test "POST /chatzer/sanhedrin without auth returns 401" do conn = conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{case: "test case"})) |> put_req_header("content-type", "application/json") |> Router.call(@opts) assert conn.status == 401 end test "POST /chatzer/sanhedrin without case returns 422" do conn = conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{not_case: "wrong field"})) |> put_req_header("content-type", "application/json") |> put_req_header("authorization", "Bearer test-token") |> Router.call(@opts) assert conn.status == 422 end test "POST /chatzer/sanhedrin with valid case deliberates" do conn = conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{ case: "Is cooking on Shabbat permitted for pikuach nefesh?" })) |> put_req_header("content-type", "application/json") |> put_req_header("authorization", "Bearer test-token") |> Router.call(@opts) assert conn.status == 200 body = Jason.decode!(conn.resp_body) assert Map.has_key?(body, "case_ref") assert Map.has_key?(body, "deliberation") assert body["deliberation"]["outcome"] in ["accepted", "rejected", "teiku"] assert body["note"] =~ "not a binding ruling" end end # ============================================================ # Kodesh — Operator Endpoint # ============================================================ describe "Kodesh endpoint" do test "GET /kodesh/health returns health check" do conn = conn(:get, "/kodesh/health") |> Router.call(@opts) assert conn.status in [200, 503] body = Jason.decode!(conn.resp_body) assert Map.has_key?(body, "overall") assert Map.has_key?(body, "checks") assert body["overall"] in ["healthy", "degraded", "critical", "shabbat"] # Required health checks assert Map.has_key?(body["checks"], "l1_hash") assert Map.has_key?(body["checks"], "halt_monitor_alive") assert Map.has_key?(body["checks"], "esh_tamid") end test "GET /kodesh/status returns system status" do conn = conn(:get, "/kodesh/status") |> Router.call(@opts) assert conn.status == 200 body = Jason.decode!(conn.resp_body) assert Map.has_key?(body, "cloud_state") assert Map.has_key?(body, "components") assert Map.has_key?(body, "halt_status") assert Map.has_key?(body, "signal_count") end end # ============================================================ # Error handling — Chatat protocol # ============================================================ describe "error handling" do test "unknown endpoint returns 404" do conn = conn(:get, "/unknown/path") |> Router.call(@opts) assert conn.status == 404 body = Jason.decode!(conn.resp_body) assert body["error"]["code"] == "NOT_FOUND" end test "all errors follow chatat format" do conn = conn(:post, "/peah/ask", Jason.encode!(%{})) |> put_req_header("content-type", "application/json") |> Router.call(@opts) assert conn.status == 422 body = Jason.decode!(conn.resp_body) # Chatat format: error.code, error.type, error.message, error.guidance assert Map.has_key?(body["error"], "code") assert Map.has_key?(body["error"], "type") assert Map.has_key?(body["error"], "message") assert Map.has_key?(body["error"], "guidance") end test "all responses are JSON" do conn = conn(:get, "/kodesh/health") |> Router.call(@opts) content_type = get_resp_header(conn, "content-type") assert Enum.any?(content_type, &String.contains?(&1, "application/json")) end end end