Chat's updates for security, db and workflow
Security and workflow regressions / regression (push) Has been cancelled

This commit is contained in:
2026-10-20 06:45:30 -04:00
parent 02223083f6
commit 1e63076123
116 changed files with 3252 additions and 2932 deletions
+62
View File
@@ -0,0 +1,62 @@
"""Disposable HTTP test application. Requires an explicitly supplied test database server."""
import http.cookiejar
import json
import os
from pathlib import Path
import re
import socket
import subprocess
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
ROOT=Path(__file__).resolve().parents[1]
PHP=json.loads(os.environ.get('PHP_TEST_COMMAND','["php"]'))
class Application:
def __init__(self):
if not os.environ.get('HCR_TEST_DSN'):
raise RuntimeError('An isolated HCR_TEST_DSN is required.')
self.env=os.environ.copy()
self.env.update(HCR_APPLICATION_URL='',HCR_ENVIRONMENT='Development')
self.database=self.fixture('create')['database']
self.env['HCR_DB_NAME']=self.database
with socket.socket() as available:
available.bind(('127.0.0.1',0));port=available.getsockname()[1]
self.base=f'http://127.0.0.1:{port}'
self.log=tempfile.TemporaryFile(mode='w+b')
self.server=subprocess.Popen(PHP+['-S',f'127.0.0.1:{port}','bin/dev-router.php'],cwd=ROOT,env=self.env,stdout=self.log,stderr=self.log)
self.client=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
for _ in range(100):
try:self.request();break
except urllib.error.URLError:time.sleep(.1)
else:self.close();raise RuntimeError('Test HTTP server did not start.')
def fixture(self,action,*extra):
args=PHP+['tests/http_fixture.php',action]+([self.database] if hasattr(self,'database') else [])+list(extra)
result=subprocess.run(args,cwd=ROOT,env=self.env,capture_output=True,text=True)
if result.returncode:raise RuntimeError(result.stdout+result.stderr)
return json.loads(result.stdout.splitlines()[-1]) if result.stdout.strip() else {}
def request(self,path='/',data=None,ajax=False):
req=urllib.request.Request(self.base+path,data=urllib.parse.urlencode(data).encode() if data is not None else None,headers={'X-Requested-With':'XMLHttpRequest'} if ajax else {})
try:
with self.client.open(req,timeout=30) as response:return response.status,response.read(),response.headers
except urllib.error.HTTPError as error:return error.code,error.read(),error.headers
@staticmethod
def csrf(body):
match=re.search(rb'name="csrf(?:_token|-token)"[^>]*(?:value|content)="([a-f0-9]+)"',body)
if not match:raise AssertionError('Missing CSRF token: '+body[:500].decode(errors='replace'))
return match.group(1).decode()
def login(self,email='admin@example.test',password='Admin test password 123!'):
_,body,_=self.request()
status,body,_=self.request(data={'action':'SignIn.login','csrf_token':self.csrf(body),'user_email':email,'user_password':password})
if status!=200 or b'name="csrf-token"' not in body:raise AssertionError('Login failed: '+body[:500].decode(errors='replace'))
self.token=self.csrf(body)
def post(self,action,**values):
return self.request(data={'action':action,'csrf_token':self.token,**values},ajax=True)
def logout(self):self.post('signout')
def close(self):
self.server.terminate();self.server.wait(timeout=10)
self.fixture('drop')
self.log.seek(0);folder=ROOT/'tests/.local';folder.mkdir(exist_ok=True);(folder/'last-http.log').write_bytes(self.log.read());self.log.close()