Files
easypow/tests/test_cross_language.py
2026-09-27 05:35:51 +00:00

76 lines
2.2 KiB
Python

"""Cross-language tests: the bundled JS solver and the Python validator must agree.
These tests execute the actual bundled easypow.js with Node.js, guarding
against the two implementations drifting apart (e.g. mismatched masks).
Skipped when node is not available.
"""
import json
import secrets
import shutil
import subprocess
from importlib.resources import files
import base64url
import pytest
from easypow import generate, solve, validate
from easypow.pow import _decode
NODE = shutil.which("node")
SOLVER_PATH = files("easypow").joinpath("easypow.js")
pytestmark = pytest.mark.skipif(NODE is None, reason="node is not available")
def _run_node(script: str) -> str:
result = subprocess.run(
[NODE, "--input-type=module", "-e", script],
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
def _js_solves(challenge: str) -> None:
script = (
f"import solve from {json.dumps(str(SOLVER_PATH))}\n"
f"console.log(await solve({json.dumps(challenge)}))"
)
validate(challenge, _run_node(script).splitlines()[-1])
def test_js_solution_validates_in_python() -> None:
_js_solves(generate(seconds=0.2))
def test_js_solution_validates_in_python_custom_params() -> None:
_js_solves(base64url.enc(bytes([2, 9 << 4 | 6]) + secrets.token_bytes(7)))
def test_python_solution_validates_in_js() -> None:
challenge = generate(seconds=0.2)
_, _, mask, iterations = _decode(challenge)
script = f"""
const B64URL = {{ alphabet: 'base64url', omitPadding: true }}
const challenge = Uint8Array.fromBase64({json.dumps(challenge)}, B64URL)
const key = await crypto.subtle.importKey(
'raw', challenge, 'PBKDF2', false, ['deriveBits']
)
const args = {{
name: 'PBKDF2', salt: null, hash: 'SHA-512', iterations: {iterations}
}}
for (const fragment of {json.dumps(solve(challenge))}.split('.')) {{
const nonce = Uint8Array.fromBase64(fragment.padEnd(6, 'A'), B64URL)
const result = new Uint32Array(await crypto.subtle.deriveBits(
{{ ...args, salt: nonce }}, key, 32
))
if (result[0] & {mask}) {{
console.error('nonce failed mask check')
process.exit(1)
}}
}}
"""
_run_node(script)