Skip to content

Feat/key crt upload#27

Merged
emilwojtaszek merged 9 commits into
mainfrom
feat/key-crt-upload
Feb 9, 2026
Merged

Feat/key crt upload#27
emilwojtaszek merged 9 commits into
mainfrom
feat/key-crt-upload

Conversation

@emilwojtaszek

@emilwojtaszek emilwojtaszek commented Feb 9, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Dual certificate upload modes: upload a .p12/.pfx or upload .key + .crt which are converted server-side; UI toggle, mode-specific fields, and improved validation/messages.
  • Documentation

    • Added ADR documenting the certificate upload and server-side conversion workflow.
  • Tests

    • Added converter and live-view tests with fixtures covering new upload flows.

emilwojtaszek and others added 8 commits February 9, 2026 15:42
Document the decision to add server-side PKCS12 conversion from
separate .key + .crt files, making certificate upload accessible
to non-technical users.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move write, path, and delete helpers into KsefHub.SecureTemp module
for reuse by the upcoming Pkcs12Converter. Remove unused :timeout
option from System.cmd call (not supported in Elixir 1.18).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Define convert/3 callback for key+crt to PKCS12 conversion.
Generate test certificates (unencrypted key, encrypted key,
mismatched key) for converter tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Convert .key + .crt to .p12 via openssl CLI with secure temp files.
Generates random 32-byte password for the output PKCS12 bundle.
Passes passwords via file (not CLI args) to avoid ps exposure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add :pkcs12_converter config key with Openssl as default and Mock
for tests. Define Mox mock for the Pkcs12Converter.Behaviour.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add upload_mode toggle (:p12 | :key_crt) with conditional form
sections. In key_crt mode, upload separate .key and .crt files,
call Pkcs12Converter, then reuse existing save_credential flow.
Extract consume_single_upload/2 for shared upload consumption.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add upload_mode toggle (:p12 | :key_crt) with conditional form
sections. In key_crt mode, upload separate .key and .crt files,
call Pkcs12Converter, then reuse existing save_credential flow.
Register all uploads in mount (accept :any for key/crt) and toggle
visibility via upload_mode assign.

Add tests for mode toggle, successful conversion, converter errors,
missing files, and passphrase forwarding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 9, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds server-side PKCS#12 conversion for separate .key + .crt uploads, a SecureTemp utility, a Pkcs12Converter behaviour plus an OpenSSL implementation, LiveView UI/flow changes to support dual upload modes, configuration and mocks, tests, and fixtures.

Changes

Cohort / File(s) Summary
Configuration
config/config.exs, config/test.exs
Register :pkcs12_converter — production -> KsefHub.Credentials.Pkcs12Converter.Openssl, test -> KsefHub.Credentials.Pkcs12Converter.Mock.
Docs / ADR
docs/adr/0010-key-crt-certificate-upload.md
New ADR describing key+.crt upload flow, server-side conversion with OpenSSL, secure-temp handling, UX toggle, and security notes.
Pkcs12 Converter API & Impl
lib/ksef_hub/credentials/pkcs12_converter/behaviour.ex, lib/ksef_hub/credentials/pkcs12_converter/openssl.ex
New behaviour convert/3 and OpenSSL-backed implementation that writes secure temp files, runs openssl pkcs12 -export, returns {p12_data, p12_password}, handles errors and always cleans up temps.
Secure Temp Utility
lib/ksef_hub/secure_temp.ex
New KsefHub.SecureTemp with write/2, path/1, delete/1 — creates 0600 files, produces unique paths, overwrites-with-zeros on delete, and tolerates missing files.
XAdES signer refactor
lib/ksef_hub/xades_signer/xmlsec1.ex
Replaced ad-hoc temp-file logic with SecureTemp, switched xmlsec invocation to Task with 30s yield timeout, and removed internal secure-temp helpers.
Certificate LiveView
lib/ksef_hub_web/live/certificate_live.ex
Adds dual upload modes (:p12, :key_crt), new uploads :private_key and :certificate_crt, mode toggle handler, mode-specific save flows (save_p12, save_key_crt) using configured converter, helper functions and updated rendering/validation.
Tests
test/ksef_hub/credentials/pkcs12_converter_test.exs, test/ksef_hub_web/live/certificate_live_test.exs
New converter unit tests (success and error cases); extended LiveView tests with Mox, mode toggle tests, and key+crt save-path tests (including passphrase and failure handling).
Test fixtures & helpers
test/test_helper.exs, test/support/fixtures/test_cert.crt, test/support/fixtures/test_cert.key, test/support/fixtures/test_cert_encrypted.key, test/support/fixtures/test_cert_other.key
Added Mox mock for the converter and PEM fixtures: certificate, unencrypted key, encrypted key, and alternate key file.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant LiveView as CertificateLive
    participant Converter as Pkcs12Converter.Openssl
    participant SecureTemp as SecureTemp
    participant OpenSSL as OpenSSL
    participant FS as File System
    participant DB as Database

    User->>LiveView: Upload key + crt files (key_crt mode)
    LiveView->>LiveView: Read private key & certificate
    LiveView->>Converter: convert(key_data, crt_data, passphrase)

    Converter->>SecureTemp: write(key_data, ".key")
    SecureTemp->>FS: Create file (0600 perms)
    SecureTemp-->>Converter: key_path

    Converter->>SecureTemp: write(crt_data, ".crt")
    SecureTemp->>FS: Create file (0600 perms)
    SecureTemp-->>Converter: crt_path

    Converter->>SecureTemp: write(password, ".pass")
    SecureTemp->>FS: Create file (0600 perms)
    SecureTemp-->>Converter: pass_path

    Converter->>OpenSSL: Execute pkcs12 -export (reads files, writes p12)
    OpenSSL->>FS: Read key/crt/pass files
    OpenSSL->>FS: Create p12 file
    OpenSSL-->>Converter: Success

    Converter->>FS: Read p12 file content
    FS-->>Converter: p12 binary

    Converter->>SecureTemp: delete(key_path)
    Converter->>SecureTemp: delete(crt_path)
    Converter->>SecureTemp: delete(pass_path)
    Converter->>SecureTemp: delete(p12_path)
    SecureTemp->>FS: Overwrite with zeros, remove files

    Converter-->>LiveView: {:ok, %{p12_data, p12_password}}
    LiveView->>DB: save_credential(p12_data, p12_password)
    DB-->>LiveView: Success
    LiveView-->>User: Credential saved
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nibble bytes and weave a stitch,
Two files hop, then become one rich.
OpenSSL hums, temp files fade away,
A p12 is born to save the day.
Hooray — small paws, big change, hooray!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Feat/key crt upload' clearly summarizes the main change: adding support for uploading separate .key and .crt certificate files with server-side PKCS12 conversion.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/key-crt-upload

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
lib/ksef_hub/xades_signer/xmlsec1.ex (1)

15-16: Add missing @doc and @spec for public function.

As per coding guidelines, every public function must have @doc documentation and every function must have @spec type specification.

📝 Proposed fix
   `@impl` true
+  `@doc` """
+  Signs a challenge from KSeF using the provided PKCS12 certificate.
+
+  Returns `{:ok, signed_xml}` on success or `{:error, reason}` on failure.
+  """
+  `@spec` sign_challenge(String.t(), String.t(), binary(), String.t()) ::
+          {:ok, String.t()} | {:error, term()}
   def sign_challenge(challenge, nip, certificate_data, certificate_password) do
lib/ksef_hub_web/live/certificate_live.ex (1)

28-37: Tighten key/crt upload accept lists to enable client-side validation.

accept: :any allows arbitrary file types and bypasses the :not_accepted path for those uploads. Whitelisting supported types provides earlier feedback and aligns with UI hints. Note that .crt and .cer files may be DER-encoded rather than PEM, so use appropriate MIME types for each format.

♻️ Suggested change
       |> allow_upload(:private_key,
-        accept: :any,
+        accept: ~w(.key .pem application/pkcs8 application/x-pem-file),
         max_entries: 1,
         max_file_size: 1_000_000
       )
       |> allow_upload(:certificate_crt,
-        accept: :any,
+        accept: ~w(.crt .cer .pem application/x-x509-ca-cert application/pkix-cert application/x-pem-file),
         max_entries: 1,
         max_file_size: 1_000_000
       )

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
lib/ksef_hub/xades_signer/xmlsec1.ex (1)

36-43: ⚠️ Potential issue | 🟠 Major

Missing timeout on System.cmd call.

The timeout option was removed from the System.cmd call. Per project learnings, xmlsec1 calls should have a 30-second timeout to prevent indefinite hangs.

🐛 Proposed fix: restore timeout
-     case System.cmd("xmlsec1", args, stderr_to_stdout: true) do
+     case System.cmd("xmlsec1", args, stderr_to_stdout: true, timeout: 30_000) do

Based on learnings: "When calling xmlsec1 for XADES signing: ... apply 30-second timeout on System.cmd calls"

lib/ksef_hub_web/live/certificate_live.ex (2)

274-373: ⚠️ Potential issue | 🟠 Major

Use <.form for={@Form}> component instead of plain <form> element and add a unique DOM ID.

The @form is correctly assigned via to_form/2, but the template must use the <.form> Phoenix component with the id attribute for proper LiveView form binding and event handling.

✅ Suggested fix
-        <form phx-submit="save" phx-change="validate" class="space-y-4">
+        <.form for={`@form`} phx-submit="save" phx-change="validate" class="space-y-4" id="certificate-upload">
           ...
-        </form>
+        </.form>

14-63: ⚠️ Potential issue | 🟠 Major

Add @doc and @spec for public functions.

The public LiveView callbacks mount/3, handle_event/3, and render/1 lack required documentation and type specifications. Each must include @doc describing the function's purpose and @spec specifying parameter and return types.

✍️ Example pattern
+  `@doc` "Initializes the certificate management LiveView."
+  `@spec` mount(map(), map(), Phoenix.LiveView.Socket.t()) ::
+          {:ok, Phoenix.LiveView.Socket.t()}
   `@impl` true
   def mount(_params, _session, socket) do
     ...
   end

+  `@doc` "Handles LiveView events for certificate management."
+  `@spec` handle_event(String.t(), map(), Phoenix.LiveView.Socket.t()) ::
+          {:noreply, Phoenix.LiveView.Socket.t()}
   `@impl` true
   def handle_event("validate", %{"credential" => params}, socket) do
     ...
   end

+  `@doc` "Renders the certificate management view."
+  `@spec` render(map()) :: Phoenix.LiveView.Rendered.t()
   `@impl` true
   def render(assigns) do
     ...
   end

Per coding guidelines: "Every public function must have @doc documentation" and "Every function (public and private) must have @spec type specification."

🤖 Fix all issues with AI agents
In `@lib/ksef_hub_web/live/certificate_live.ex`:
- Around line 195-196: Add a private type specification above the defp
pkcs12_converter function: declare `@spec` pkcs12_converter() :: module() and
place it directly above the defp pkcs12_converter do ... end definition so the
private function has an explicit spec indicating it returns a module.
- Around line 47-55: handle_event("toggle_upload_mode", ...) currently uses
String.to_existing_atom(mode) which can raise on unexpected input; replace that
conversion with an explicit whitelist/case that maps allowed string values to
atoms (e.g. "local" -> :local, "remote" -> :remote) and provide a safe fallback
(e.g. :default or keep current socket.assigns.upload_mode) so unexpected strings
do not crash the process; update the assignment to use the sanitized new_mode
when calling assign(upload_mode: new_mode) and preserve the existing form assign
code.

In `@lib/ksef_hub/credentials/pkcs12_converter/openssl.ex`:
- Around line 15-18: Add a `@doc` block for the public function convert/3
describing its purpose, parameters and return values; update the module in which
convert/3 is defined (the OpenSSL PKCS12 converter) to include a short
description, `@param`-style notes for key_data, crt_data and key_passphrase
semantics (nil allowed), and document the {:ok, %{p12_data: binary(),
p12_password: String.t()}} and {:error, term()} return shapes and possible error
conditions; keep the doc concise and placed immediately above the `@spec/def` so
it satisfies the project’s public-function documentation requirement.
- Around line 39-46: In the clause that matches {output, exit_code} (the OpenSSL
failure branch), stop logging or returning the raw OpenSSL output to avoid
leaking secrets: change Logger.error("openssl pkcs12 failed (exit #{exit_code}):
#{output}") to log only the non-sensitive exit_code (e.g. Logger.error("openssl
pkcs12 failed (exit #{exit_code})")) and change the returned error tuple from
{:error, {:openssl_failed, exit_code, output}} to a form that omits output (e.g.
{:error, {:openssl_failed, exit_code}}), and ensure you do not reference the
output variable elsewhere in that branch.
- Around line 33-46: The System.cmd call invoking "openssl" currently has no
timeout and can hang; update the call in the pkcs12 conversion block (the case
matching System.cmd("openssl", args, stderr_to_stdout: true)) to include a
30_000 ms timeout by adding timeout: 30_000 to the options map passed to
System.cmd so the call fails after 30 seconds and the existing error handling
(the {output, exit_code} and file read branches) continues to work unchanged.

In `@test/ksef_hub_web/live/certificate_live_test.exs`:
- Around line 39-42: Add an id to the template span and update the test to
assert presence via LiveView helpers: in the template locate the span with class
"label-text" that contains "Certificate File (.p12 / .pfx)" and add
id="p12-certificate-label" to it, and in the test "defaults to p12 upload mode"
(the test function shown) change the live/3 return binding to capture the live
view (e.g., {:ok, view, _html} = live(conn, ~p"/certificates")) and replace the
string assertion with assert has_element?(view, "#p12-certificate-label").
- Around line 4-13: The LiveView tests need Mox expectations propagated to the
LiveView process; add the setup callback setup :set_mox_from_context before the
existing setup :verify_on_exit! so the test process can set Mox expectations
used by LiveView. Locate the test module where setup :verify_on_exit! is
declared and insert setup :set_mox_from_context immediately above it (using the
same atom-style setup invocation).
- Around line 89-200: Replace raw string assertions using render(view) =~ with
selector-based assertions that check the flash elements: in the "converts and
saves credential" and "passes key passphrase to converter" tests (inside
describe "save with key_crt mode") replace assert render(view) =~ "Certificate
uploaded successfully." with assert has_element?(view, "#flash-info") (or
has_element?(view, "#flash-info", "Certificate uploaded successfully.") if you
want to assert text too); in the "shows error when converter fails" and "shows
error when files missing" tests replace assert render(view) =~ "Certificate
conversion failed" and assert render(view) =~ "Please upload both private key
and certificate files." with assert has_element?(view, "#flash-error")
(optionally including the message text in the selector check). Ensure you update
only the assertions and keep the surrounding setup
(file_input/render_upload/form/render_submit) and mocks
(KsefHub.Credentials.Pkcs12Converter.Mock expectations) unchanged.
🧹 Nitpick comments (2)
lib/ksef_hub/secure_temp.ex (2)

15-20: Race condition between file creation and permission setting.

There's a brief window between File.write! and File.chmod! where the file exists with default permissions. For sensitive data like private keys, consider using :erlang.open_port with {:spawn_executable, ...} and exclusive file creation, or accept this minor risk given the temp directory context.

A safer approach would be to set umask or use atomic file creation:

♻️ Optional: atomic secure write
  def write(content, suffix) do
    path = path(suffix)
+   # Create file with restrictive permissions atomically
+   {:ok, fd} = :file.open(path, [:write, :exclusive, {:mode, 0o600}])
+   :ok = :file.write(fd, content)
+   :ok = :file.close(fd)
-   File.write!(path, content)
-   File.chmod!(path, 0o600)
    path
  end

39-55: Silent error swallowing in delete/1 may hide issues.

The blanket rescue _ -> :ok hides all errors, and the results of File.write/2 (line 43) and File.rm/1 (line 49) are discarded. While this is acceptable for cleanup code, consider at minimum logging unexpected errors to aid debugging if secure deletion fails.

♻️ Optional: log unexpected errors
+ require Logger
+
  `@spec` delete(String.t()) :: :ok
  def delete(path) do
    if File.exists?(path) do
      case File.stat(path) do
        {:ok, %{size: size}} when size > 0 ->
-         File.write(path, :binary.copy(<<0>>, size))
+         _ = File.write(path, :binary.copy(<<0>>, size))

        _ ->
          :ok
      end

-     File.rm(path)
+     _ = File.rm(path)
    end

    :ok
  rescue
-   _ -> :ok
+   error ->
+     Logger.warning("SecureTemp.delete failed for #{path}: #{inspect(error)}")
+     :ok
  end

Comment thread lib/ksef_hub_web/live/certificate_live.ex
Comment thread lib/ksef_hub_web/live/certificate_live.ex
Comment thread lib/ksef_hub/credentials/pkcs12_converter/openssl.ex
Comment thread lib/ksef_hub/credentials/pkcs12_converter/openssl.ex Outdated
Comment thread lib/ksef_hub/credentials/pkcs12_converter/openssl.ex Outdated
Comment thread test/ksef_hub_web/live/certificate_live_test.exs
Comment on lines +39 to +42
test "defaults to p12 upload mode", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/certificates")
assert html =~ "Certificate File (.p12 / .pfx)"
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the certificate live test file and examine it
find . -name "*certificate_live*" -type f | head -20

Repository: appunite/appunite-ksef-ex

Length of output: 163


🏁 Script executed:

# Check the test file structure
head -50 test/ksef_hub_web/live/certificate_live_test.exs

Repository: appunite/appunite-ksef-ex

Length of output: 1524


🏁 Script executed:

# Examine the certificate live template file
cat -n lib/ksef_hub_web/live/certificate_live.ex

Repository: appunite/appunite-ksef-ex

Length of output: 18722


🏁 Script executed:

# Look for any .heex files related to certificates
find . -name "*certificate*" -type f | grep -E "\.(heex|html\.heex)$"

Repository: appunite/appunite-ksef-ex

Length of output: 51


Add element ID to template and use has_element?/2 for testing.

The template's label (line 278) lacks an id attribute. Add id="p12-certificate-label" to the span, then update the test to use assert has_element?(view, "#p12-certificate-label") instead of html =~ "...".

<span id="p12-certificate-label" class="label-text">Certificate File (.p12 / .pfx)</span>
🤖 Prompt for AI Agents
In `@test/ksef_hub_web/live/certificate_live_test.exs` around lines 39 - 42, Add
an id to the template span and update the test to assert presence via LiveView
helpers: in the template locate the span with class "label-text" that contains
"Certificate File (.p12 / .pfx)" and add id="p12-certificate-label" to it, and
in the test "defaults to p12 upload mode" (the test function shown) change the
live/3 return binding to capture the live view (e.g., {:ok, view, _html} =
live(conn, ~p"/certificates")) and replace the string assertion with assert
has_element?(view, "#p12-certificate-label").

Comment thread test/ksef_hub_web/live/certificate_live_test.exs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/ksef_hub_web/live/certificate_live.ex (1)

285-299: ⚠️ Potential issue | 🟡 Minor

Run mix format to fix formatting.

The CI pipeline indicates this file has formatting issues. Please run mix format to resolve.

🤖 Fix all issues with AI agents
In `@test/ksef_hub_web/live/certificate_live_test.exs`:
- Around line 165-171: This file has formatting issues; run the Elixir formatter
on the test file (e.g., run mix format) to reformat the test module around the
"passes key passphrase to converter" test and the surrounding assertions
(references: test block named "passes key passphrase to converter" and module
KsefHub.Credentials.Pkcs12Converter.Mock) so the spacing/line breaks match mix
format expectations and CI will pass.
🧹 Nitpick comments (2)
lib/ksef_hub_web/live/certificate_live.ex (1)

28-37: Consider restricting accepted file types for key/cert uploads.

Using accept: :any allows any file type. While this provides flexibility for various PEM extensions (.key, .pem, .crt, .cer), it also allows users to accidentally upload unrelated files. Consider using a more specific accept list if you want early client-side filtering:

accept: ~w(.key .pem .crt .cer application/x-pem-file)

However, since PEM files have inconsistent MIME types across browsers, accept: :any with server-side validation is also a valid approach.

test/ksef_hub_web/live/certificate_live_test.exs (1)

46-74: Consider using element IDs for content assertions.

While the mode toggle tests work correctly, the assertions using html =~ "Private Key File" etc. (lines 55-57, 72-73) test against raw HTML content. Per coding guidelines, prefer testing for element presence with unique IDs:

# Instead of:
assert html =~ "Private Key File (.key / .pem)"

# Consider adding IDs to template and asserting:
assert has_element?(view, "#private-key-label")

This is a minor refinement since the current tests are functional.

Comment on lines +165 to +171
|> render_submit()

assert has_element?(view, "#flash-error", "Please upload both private key and certificate files.")
end

test "passes key passphrase to converter", %{conn: conn} do
KsefHub.Credentials.Pkcs12Converter.Mock

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Run mix format to fix formatting.

The CI pipeline indicates this file has formatting issues around lines 165-171. Please run mix format to resolve.

🧰 Tools
🪛 GitHub Actions: CI

[error] 165-171: mix format check failed. The following file is not formatted: test/ksef_hub_web/live/certificate_live_test.exs. Please run 'mix format' to fix formatting.

🤖 Prompt for AI Agents
In `@test/ksef_hub_web/live/certificate_live_test.exs` around lines 165 - 171,
This file has formatting issues; run the Elixir formatter on the test file
(e.g., run mix format) to reformat the test module around the "passes key
passphrase to converter" test and the surrounding assertions (references: test
block named "passes key passphrase to converter" and module
KsefHub.Credentials.Pkcs12Converter.Mock) so the spacing/line breaks match mix
format expectations and CI will pass.

- Whitelist upload mode toggle input instead of String.to_existing_atom
- Stop logging raw openssl output to avoid leaking secrets
- Add 30s timeout (Task.yield) to openssl and xmlsec1 System.cmd calls
- Switch <form> to <.form> component with DOM id
- Add @doc/@SPEC on public LiveView callbacks and pkcs12_converter/0
- Use has_element? selectors in tests instead of raw string matching
- Add setup :set_mox_from_context for proper Mox propagation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@emilwojtaszek emilwojtaszek merged commit 2f54e96 into main Feb 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant