Add Tier 1 test coverage: EnrichmentStatus, Markdown, health, RemoteIp, Maidenhead
EnrichmentStatus 0→100%, HealthController 0→100%, Maidenhead 74→96%, RemoteIp 56→94%, Markdown 0→92%. Total coverage 53→55%.
This commit is contained in:
parent
f1db86e5b1
commit
7e52c6660d
5 changed files with 405 additions and 0 deletions
169
test/microwaveprop/markdown_test.exs
Normal file
169
test/microwaveprop/markdown_test.exs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
defmodule Microwaveprop.MarkdownTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Microwaveprop.Markdown
|
||||
|
||||
describe "headings" do
|
||||
test "renders h1" do
|
||||
assert Markdown.to_html!("# Hello") =~ "<h1>Hello</h1>"
|
||||
end
|
||||
|
||||
test "renders h2 through h6" do
|
||||
for level <- 2..6 do
|
||||
hashes = String.duplicate("#", level)
|
||||
assert Markdown.to_html!("#{hashes} Title") =~ "<h#{level}>Title</h#{level}>"
|
||||
end
|
||||
end
|
||||
|
||||
test "caps at h6 for excessive hashes" do
|
||||
assert Markdown.to_html!("####### Too many") =~ "<h6>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "paragraphs" do
|
||||
test "renders simple paragraph" do
|
||||
assert Markdown.to_html!("Hello world") =~ "<p>Hello world</p>"
|
||||
end
|
||||
|
||||
test "joins multi-line paragraphs" do
|
||||
result = Markdown.to_html!("Line one\nLine two")
|
||||
assert result =~ "<p>Line one Line two</p>"
|
||||
end
|
||||
|
||||
test "separates paragraphs by blank lines" do
|
||||
result = Markdown.to_html!("Para one\n\nPara two")
|
||||
assert result =~ "<p>Para one</p>"
|
||||
assert result =~ "<p>Para two</p>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "fenced code blocks" do
|
||||
test "renders code block" do
|
||||
md = "```\nfoo\nbar\n```"
|
||||
result = Markdown.to_html!(md)
|
||||
assert result =~ "<pre><code>foo\nbar</code></pre>"
|
||||
end
|
||||
|
||||
test "renders code block with language" do
|
||||
md = "```elixir\nIO.puts(\"hi\")\n```"
|
||||
result = Markdown.to_html!(md)
|
||||
assert result =~ ~s(<code class="language-elixir">)
|
||||
end
|
||||
|
||||
test "escapes HTML in code blocks" do
|
||||
md = "```\n<script>alert('xss')</script>\n```"
|
||||
result = Markdown.to_html!(md)
|
||||
assert result =~ "<script>"
|
||||
refute result =~ "<script>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "tables" do
|
||||
test "renders table with header" do
|
||||
md = "| A | B |\n|---|---|\n| 1 | 2 |"
|
||||
result = Markdown.to_html!(md)
|
||||
assert result =~ "<thead>"
|
||||
assert result =~ "<th>A</th>"
|
||||
assert result =~ "<td>1</td>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "lists" do
|
||||
test "renders unordered list" do
|
||||
md = "- one\n- two\n- three"
|
||||
result = Markdown.to_html!(md)
|
||||
assert result =~ "<ul>"
|
||||
assert result =~ "<li>one</li>"
|
||||
assert result =~ "<li>two</li>"
|
||||
assert result =~ "<li>three</li>"
|
||||
end
|
||||
|
||||
test "renders ordered list" do
|
||||
md = "1. first\n2. second"
|
||||
result = Markdown.to_html!(md)
|
||||
assert result =~ "<ol>"
|
||||
assert result =~ "<li>first</li>"
|
||||
assert result =~ "<li>second</li>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "inline formatting" do
|
||||
test "renders bold with **" do
|
||||
assert Markdown.to_html!("**bold**") =~ "<strong>bold</strong>"
|
||||
end
|
||||
|
||||
test "renders bold with __" do
|
||||
assert Markdown.to_html!("__bold__") =~ "<strong>bold</strong>"
|
||||
end
|
||||
|
||||
test "renders italic with *" do
|
||||
assert Markdown.to_html!("*italic*") =~ "<em>italic</em>"
|
||||
end
|
||||
|
||||
test "renders inline code" do
|
||||
assert Markdown.to_html!("`code`") =~ "<code>code</code>"
|
||||
end
|
||||
|
||||
test "renders links" do
|
||||
assert Markdown.to_html!("[text](http://example.com)") =~ ~s(<a href="http://example.com">text</a>)
|
||||
end
|
||||
end
|
||||
|
||||
describe "list continuation" do
|
||||
test "continues unordered list items with indentation" do
|
||||
md = "- item one\n continued\n- item two"
|
||||
result = Markdown.to_html!(md)
|
||||
assert result =~ "<li>item one continued</li>"
|
||||
assert result =~ "<li>item two</li>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "table without separator" do
|
||||
test "renders table body only when no separator row" do
|
||||
md = "| A | B |"
|
||||
result = Markdown.to_html!(md)
|
||||
assert result =~ "<tbody>"
|
||||
refute result =~ "<thead>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "CRLF handling" do
|
||||
test "normalizes CRLF to LF" do
|
||||
result = Markdown.to_html!("Hello\r\nworld")
|
||||
assert result =~ "<p>Hello world</p>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "empty input" do
|
||||
test "handles empty string" do
|
||||
assert Markdown.to_html!("") == ""
|
||||
end
|
||||
end
|
||||
|
||||
describe "horizontal rules" do
|
||||
test "renders --- as hr" do
|
||||
assert Markdown.to_html!("---") =~ "<hr />"
|
||||
end
|
||||
|
||||
test "renders *** as hr" do
|
||||
assert Markdown.to_html!("***") =~ "<hr />"
|
||||
end
|
||||
|
||||
test "renders ___ as hr" do
|
||||
assert Markdown.to_html!("___") =~ "<hr />"
|
||||
end
|
||||
end
|
||||
|
||||
describe "HTML escaping" do
|
||||
test "escapes angle brackets in paragraphs" do
|
||||
result = Markdown.to_html!("x < y > z")
|
||||
assert result =~ "<"
|
||||
assert result =~ ">"
|
||||
end
|
||||
|
||||
test "escapes ampersands" do
|
||||
result = Markdown.to_html!("a & b")
|
||||
assert result =~ "&"
|
||||
end
|
||||
end
|
||||
end
|
||||
130
test/microwaveprop/radio/enrichment_status_test.exs
Normal file
130
test/microwaveprop/radio/enrichment_status_test.exs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
defmodule Microwaveprop.Radio.EnrichmentStatusTest do
|
||||
use Microwaveprop.DataCase, async: true
|
||||
|
||||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Radio.EnrichmentStatus
|
||||
|
||||
describe "states/0" do
|
||||
test "returns all valid states" do
|
||||
assert EnrichmentStatus.states() == [:pending, :queued, :processing, :complete, :failed, :unavailable]
|
||||
end
|
||||
end
|
||||
|
||||
describe "fields/0" do
|
||||
test "returns all status fields" do
|
||||
assert EnrichmentStatus.fields() == [:hrrr_status, :weather_status, :terrain_status, :iemre_status]
|
||||
end
|
||||
end
|
||||
|
||||
describe "valid_transition?/2" do
|
||||
test "pending can transition to queued" do
|
||||
assert EnrichmentStatus.valid_transition?(:pending, :queued)
|
||||
end
|
||||
|
||||
test "pending can transition to unavailable" do
|
||||
assert EnrichmentStatus.valid_transition?(:pending, :unavailable)
|
||||
end
|
||||
|
||||
test "pending cannot transition to complete" do
|
||||
refute EnrichmentStatus.valid_transition?(:pending, :complete)
|
||||
end
|
||||
|
||||
test "queued can transition to processing" do
|
||||
assert EnrichmentStatus.valid_transition?(:queued, :processing)
|
||||
end
|
||||
|
||||
test "queued can transition to failed" do
|
||||
assert EnrichmentStatus.valid_transition?(:queued, :failed)
|
||||
end
|
||||
|
||||
test "queued cannot transition to complete" do
|
||||
refute EnrichmentStatus.valid_transition?(:queued, :complete)
|
||||
end
|
||||
|
||||
test "processing can transition to complete" do
|
||||
assert EnrichmentStatus.valid_transition?(:processing, :complete)
|
||||
end
|
||||
|
||||
test "processing can transition to failed" do
|
||||
assert EnrichmentStatus.valid_transition?(:processing, :failed)
|
||||
end
|
||||
|
||||
test "processing cannot transition to pending" do
|
||||
refute EnrichmentStatus.valid_transition?(:processing, :pending)
|
||||
end
|
||||
|
||||
test "failed can transition to queued (retry)" do
|
||||
assert EnrichmentStatus.valid_transition?(:failed, :queued)
|
||||
end
|
||||
|
||||
test "failed cannot transition to complete" do
|
||||
refute EnrichmentStatus.valid_transition?(:failed, :complete)
|
||||
end
|
||||
|
||||
test "unavailable can transition to queued (retry)" do
|
||||
assert EnrichmentStatus.valid_transition?(:unavailable, :queued)
|
||||
end
|
||||
|
||||
test "complete can transition to pending (reset)" do
|
||||
assert EnrichmentStatus.valid_transition?(:complete, :pending)
|
||||
end
|
||||
|
||||
test "complete cannot transition to queued" do
|
||||
refute EnrichmentStatus.valid_transition?(:complete, :queued)
|
||||
end
|
||||
end
|
||||
|
||||
describe "transition/3" do
|
||||
test "applies valid transition to changeset" do
|
||||
changeset =
|
||||
%Contact{}
|
||||
|> Ecto.Changeset.change(%{hrrr_status: :pending})
|
||||
|> EnrichmentStatus.transition(:hrrr_status, :queued)
|
||||
|
||||
assert Ecto.Changeset.get_change(changeset, :hrrr_status) == :queued
|
||||
assert changeset.valid?
|
||||
end
|
||||
|
||||
test "adds error for invalid transition" do
|
||||
changeset =
|
||||
%Contact{}
|
||||
|> Ecto.Changeset.change(%{hrrr_status: :pending})
|
||||
|> EnrichmentStatus.transition(:hrrr_status, :complete)
|
||||
|
||||
assert {:hrrr_status, {"invalid transition from pending to complete", []}} in changeset.errors
|
||||
end
|
||||
|
||||
test "works for all status fields" do
|
||||
for field <- EnrichmentStatus.fields() do
|
||||
changeset =
|
||||
%Contact{}
|
||||
|> Ecto.Changeset.change(%{field => :pending})
|
||||
|> EnrichmentStatus.transition(field, :queued)
|
||||
|
||||
assert Ecto.Changeset.get_change(changeset, field) == :queued
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "force_status/3" do
|
||||
test "sets status without transition validation" do
|
||||
changeset =
|
||||
%Contact{}
|
||||
|> Ecto.Changeset.change(%{hrrr_status: :pending})
|
||||
|> EnrichmentStatus.force_status(:hrrr_status, :complete)
|
||||
|
||||
assert Ecto.Changeset.get_change(changeset, :hrrr_status) == :complete
|
||||
assert changeset.valid?
|
||||
end
|
||||
|
||||
test "allows normally invalid transitions" do
|
||||
changeset =
|
||||
%Contact{}
|
||||
|> Ecto.Changeset.change(%{weather_status: :pending})
|
||||
|> EnrichmentStatus.force_status(:weather_status, :failed)
|
||||
|
||||
assert Ecto.Changeset.get_change(changeset, :weather_status) == :failed
|
||||
assert changeset.valid?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -77,6 +77,19 @@ defmodule Microwaveprop.Radio.MaidenheadTest do
|
|||
assert :error = Maidenhead.to_latlon(nil)
|
||||
end
|
||||
|
||||
test "returns :error for non-binary" do
|
||||
assert :error = Maidenhead.to_latlon(123)
|
||||
end
|
||||
|
||||
test "handles 8-char extended grid EM12kp37" do
|
||||
assert {:ok, {lat, lon}} = Maidenhead.to_latlon("EM12kp37")
|
||||
assert is_float(lat)
|
||||
assert is_float(lon)
|
||||
# Should be within EM12kp subsquare
|
||||
assert lat > 32.0 and lat < 33.0
|
||||
assert lon > -98.0 and lon < -96.0
|
||||
end
|
||||
|
||||
test "AA00 returns southwest corner center" do
|
||||
# lon = 0*20 - 180 + 0*2 + 1 = -179, lat = 0*10 - 90 + 0*1 + 0.5 = -89.5
|
||||
assert {:ok, {lat, lon}} = Maidenhead.to_latlon("AA00")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
defmodule MicrowavepropWeb.HealthControllerTest do
|
||||
use MicrowavepropWeb.ConnCase, async: true
|
||||
|
||||
test "GET /health returns 200 ok", %{conn: conn} do
|
||||
conn = get(conn, "/health")
|
||||
|
||||
assert response(conn, 200) == "ok"
|
||||
assert response_content_type(conn, :text) =~ "text/plain"
|
||||
end
|
||||
end
|
||||
83
test/microwaveprop_web/plugs/remote_ip_test.exs
Normal file
83
test/microwaveprop_web/plugs/remote_ip_test.exs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
defmodule MicrowavepropWeb.Plugs.RemoteIpTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
import Plug.Conn
|
||||
import Plug.Test
|
||||
|
||||
alias MicrowavepropWeb.Plugs.RemoteIp
|
||||
|
||||
defp call_plug(conn) do
|
||||
RemoteIp.call(conn, RemoteIp.init([]))
|
||||
end
|
||||
|
||||
describe "call/2" do
|
||||
test "extracts IP from cf-connecting-ip header" do
|
||||
conn =
|
||||
conn(:get, "/")
|
||||
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
||||
|> call_plug()
|
||||
|
||||
assert conn.remote_ip == {1, 2, 3, 4}
|
||||
end
|
||||
|
||||
test "falls back to x-forwarded-for when no cf-connecting-ip" do
|
||||
conn =
|
||||
conn(:get, "/")
|
||||
|> put_req_header("x-forwarded-for", "5.6.7.8")
|
||||
|> call_plug()
|
||||
|
||||
assert conn.remote_ip == {5, 6, 7, 8}
|
||||
end
|
||||
|
||||
test "prefers cf-connecting-ip over x-forwarded-for" do
|
||||
conn =
|
||||
conn(:get, "/")
|
||||
|> put_req_header("cf-connecting-ip", "1.2.3.4")
|
||||
|> put_req_header("x-forwarded-for", "5.6.7.8")
|
||||
|> call_plug()
|
||||
|
||||
assert conn.remote_ip == {1, 2, 3, 4}
|
||||
end
|
||||
|
||||
test "takes first IP from comma-separated x-forwarded-for" do
|
||||
conn =
|
||||
conn(:get, "/")
|
||||
|> put_req_header("x-forwarded-for", "10.0.0.1, 10.0.0.2, 10.0.0.3")
|
||||
|> call_plug()
|
||||
|
||||
assert conn.remote_ip == {10, 0, 0, 1}
|
||||
end
|
||||
|
||||
test "preserves original remote_ip when no headers present" do
|
||||
original_ip = {127, 0, 0, 1}
|
||||
|
||||
conn =
|
||||
conn(:get, "/")
|
||||
|> Map.put(:remote_ip, original_ip)
|
||||
|> call_plug()
|
||||
|
||||
assert conn.remote_ip == original_ip
|
||||
end
|
||||
|
||||
test "handles IPv6 addresses" do
|
||||
conn =
|
||||
conn(:get, "/")
|
||||
|> put_req_header("cf-connecting-ip", "::1")
|
||||
|> call_plug()
|
||||
|
||||
assert conn.remote_ip == {0, 0, 0, 0, 0, 0, 0, 1}
|
||||
end
|
||||
|
||||
test "ignores malformed IP addresses" do
|
||||
original_ip = {127, 0, 0, 1}
|
||||
|
||||
conn =
|
||||
conn(:get, "/")
|
||||
|> Map.put(:remote_ip, original_ip)
|
||||
|> put_req_header("cf-connecting-ip", "not-an-ip")
|
||||
|> call_plug()
|
||||
|
||||
assert conn.remote_ip == original_ip
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue