0.31.9
This commit is contained in:
1 parent
180a0e4877
commit
d23fa5e154
39 files changed
+1521
-376
No files matched your search
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""0.31.9 Certificate API source/OpenAPI/UI contracts; no Rust runtime claims."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tomllib
|
||||
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
read = lambda path: (root / path).read_text(encoding='utf-8')
|
||||
version = tomllib.loads(read('Cargo.toml'))['package']['version']
|
||||
assert version == '0.31.9', version
|
||||
spec = json.loads(read('src/admin/openapi.json'))
|
||||
assert spec['info']['version'] == version
|
||||
paths = spec['paths']
|
||||
assert set(paths['/api/certificate-api/{key_id}']) == {'get', 'patch', 'delete'}
|
||||
assert set(paths['/api/certificate-api/{key_id}/regenerate']) == {'post'}
|
||||
for method, path in [('get', '/api/certificate-api/{key_id}'), ('patch', '/api/certificate-api/{key_id}'),
|
||||
('post', '/api/certificate-api/{key_id}/regenerate')]:
|
||||
operation = paths[path][method]
|
||||
assert operation['security'] == ([{'AdminSession': []}] if method == 'get' else [{'AdminSession': [], 'CsrfToken': []}])
|
||||
assert operation['parameters'][0]['name'] == 'key_id'
|
||||
assert '401' in operation['responses'] and '404' in operation['responses']
|
||||
edit = spec['components']['schemas']['CertificateApiEdit']
|
||||
assert edit['additionalProperties'] is False
|
||||
assert set(edit['required']) == {'comment', 'certificate_ids', 'allow_deploy'}
|
||||
assert not {'token', 'token_hash', 'key_id'} & edit['properties'].keys()
|
||||
assert set(spec['components']['schemas']['CertificateApiDetailResult']['properties']) == {'credential'}
|
||||
assert set(spec['components']['schemas']['CertificateApiRegenerateResult']['properties']) == {'credential', 'token'}
|
||||
assert 'readOnly' in spec['components']['schemas']['CertificateApiRegenerateResult']['properties']['token']
|
||||
backend = read('src/admin/certificate_api.rs')
|
||||
router = read('src/admin/router.rs')
|
||||
db = read('src/db/certificate_api.rs')
|
||||
assert 'include!("certificate_api.rs")' in read('src/admin/mod.rs')
|
||||
for symbol in ('certificate_api_detail', 'certificate_api_edit', 'certificate_api_regenerate'):
|
||||
assert f'async fn {symbol}(' in backend
|
||||
assert symbol in router
|
||||
assert 'deny_unknown_fields' in backend
|
||||
read_source = backend.split('async fn certificate_api_detail(')[1].split('async fn certificate_api_edit(')[0]
|
||||
edit_source = backend.split('async fn certificate_api_edit(')[1].split('async fn certificate_api_regenerate(')[0]
|
||||
regenerate_source = backend.split('async fn certificate_api_regenerate(')[1].split('async fn certificate_api_revoke(')[0]
|
||||
assert '"token"' not in read_source
|
||||
assert '"token"' not in edit_source and '"token_hash"' not in edit_source
|
||||
assert '"token": token' in regenerate_source
|
||||
assert 'auth::random_secret()' in regenerate_source
|
||||
assert '"certificate_api.invalid_certificate_ids"' in edit_source
|
||||
update_source = db.split('pub async fn edit_certificate_api_credential(')[1].split('pub async fn regenerate_certificate_api_token(')[0]
|
||||
rotate_source = db.split('pub async fn regenerate_certificate_api_token(')[1].split('pub async fn revoke_certificate_api_credential(')[0]
|
||||
assert 'SET comment = ?' in update_source
|
||||
assert 'SET token_hash' not in update_source
|
||||
assert 'SET token_hash = ?, created_at = ?, last_used_at = NULL' in rotate_source
|
||||
assert 'certificate_api.edit' in update_source and 'certificate_api.regenerate' in rotate_source
|
||||
assert 'WHERE key_id = ?' in rotate_source
|
||||
assert 'certificate_api_operation(method: &Method' in router or 'fn certificate_api_operation(method: &Method' in read('src/admin/access.rs') + router
|
||||
assert 'if method != Method::POST' in router
|
||||
panel = read('src/admin/web/certificate-api.js')
|
||||
assert 'include_str!("web/certificate-api.js")' in router
|
||||
for marker in ('function certificateApiDetailNode(', 'beginCertificateApiEdit(', 'certificateApiEditingKey',
|
||||
'method: "PATCH"', '/regenerate', 'showCertificateApiSecret(data.token || "")',
|
||||
'clearCertificateApiSecret()', 'token-field', 'certificateApiTokenHidden'):
|
||||
assert marker in panel, marker
|
||||
for item in ('certificate-api-save', 'certificate-api-cancel-edit', 'certificate-api-secret', 'certificate-api-token-field'):
|
||||
assert f'id="{item}"' in read('src/admin/web/index.html')
|
||||
for language in ('en_US', 'pl_pl'):
|
||||
strings = json.loads(read(f'langs/{language}.json'))['translations']
|
||||
for key in ('certificateApiView', 'certificateApiEdit', 'certificateApiSave', 'certificateApiRegenerate',
|
||||
'certificateApiRegenerateConfirm', 'certificateApiTokenHidden', 'certificateApiCreatedAt'):
|
||||
assert strings.get(key), (language, key)
|
||||
assert '0.31.9' in read('docs/TEST_REPORT.md')
|
||||
print('PASS: 0.31.9 per-credential API routes, separated metadata editing/token regeneration, OpenAPI security, UI and locales (source-only).')
|
||||
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env node
|
||||
// Execute the real 0.31.9 Certificate API panel code against a simulated DOM/API.
|
||||
// This is not a compiled Rust proxy test.
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const source = readFileSync('src/admin/web/certificate-api.js', 'utf8');
|
||||
const html = readFileSync('src/admin/web/index.html', 'utf8');
|
||||
const english = JSON.parse(readFileSync('langs/en_US.json', 'utf8')).translations;
|
||||
const polish = JSON.parse(readFileSync('langs/pl_pl.json', 'utf8')).translations;
|
||||
for (const pack of [english, polish]) {
|
||||
for (const key of ['certificateApiView', 'certificateApiHide', 'certificateApiEdit',
|
||||
'certificateApiSave', 'certificateApiCancelEdit', 'certificateApiRegenerate',
|
||||
'certificateApiRegenerateConfirm', 'certificateApiRegenerated', 'certificateApiTokenHidden']) {
|
||||
assert.ok(pack[key], key);
|
||||
}
|
||||
}
|
||||
for (const id of ['certificate-api-save', 'certificate-api-cancel-edit', 'certificate-api-token-field',
|
||||
'certificate-api-secret', 'certificate-api-credentials', 'certificate-api-edit-note']) {
|
||||
assert.ok(html.includes(`id="${id}"`), id);
|
||||
}
|
||||
class FakeNode {
|
||||
constructor(tag = 'div', content = '', cls = '') {
|
||||
this.tagName = tag; this.value = ''; this.content = String(content ?? '');
|
||||
this.className = cls; this.children = []; this.events = {};
|
||||
this.dataset = {}; this.hidden = false; this.disabled = false;
|
||||
this.checked = false; this.readOnly = false;
|
||||
this.classList = { add: cls => { this.className += ` ${cls}`; } };
|
||||
}
|
||||
get textContent() { return this.content + this.children.map(child => child.textContent).join(''); }
|
||||
set textContent(value) { this.content = String(value ?? ''); this.children = []; }
|
||||
addEventListener(type, callback) { this.events[type] = callback; }
|
||||
append(...children) { this.children.push(...children); }
|
||||
replaceChildren(...children) { this.children = [...children]; this.content = ''; }
|
||||
scrollIntoView() { }
|
||||
focus() { }
|
||||
async fire(type, event = {}) { assert.ok(this.events[type], `${this.tagName} missing ${type}`); await this.events[type](event); }
|
||||
findByText(value) { return [this, ...this.children.flatMap(child => child.findAll())].find(child => child.content === value); }
|
||||
findAll() { return [this, ...this.children.flatMap(child => child.findAll())]; }
|
||||
}
|
||||
const fields = new Map();
|
||||
const dollar = id => {
|
||||
if (!fields.has(id)) fields.set(id, new FakeNode(id));
|
||||
return fields.get(id);
|
||||
};
|
||||
const node = (tag, text, cls) => new FakeNode(tag, text, cls);
|
||||
let persisted = [{ key_id: 'renew-prod', comment: 'Old owner', certificate_ids: ['first-cert'],
|
||||
allow_deploy: false, created_at: 1690000000, last_used_at: 1700000000 }];
|
||||
let activeToken = 'original-secret-not-readable';
|
||||
const requests = [];
|
||||
const makeStatus = () => ({ configured: persisted.length > 0, credentials: structuredClone(persisted),
|
||||
max_credentials: 32, scopes: ['certificates:reload', 'certificates:deploy'] });
|
||||
async function api(path, options = {}) {
|
||||
const method = options.method || 'GET';
|
||||
const data = options.body ? JSON.parse(options.body) : {};
|
||||
requests.push({ path, method, data });
|
||||
if (path === '/api/certificate-api' && method === 'GET') return makeStatus();
|
||||
if (path === '/api/certificate-api' && method === 'DELETE') { persisted = []; return { revoked: 1 }; }
|
||||
if (path === '/api/certificate-api' && method === 'PUT') {
|
||||
activeToken = data.token || 'created-once-token';
|
||||
const entry = { key_id: data.key_id || 'generated-id', comment: data.comment,
|
||||
certificate_ids: data.certificate_ids, allow_deploy: data.allow_deploy,
|
||||
created_at: 1800000000, last_used_at: null };
|
||||
persisted.push(entry);
|
||||
return { ...makeStatus(), credential: entry, token: activeToken, generated: !data.token };
|
||||
}
|
||||
const match = path.match(/^\/api\/certificate-api\/([^/]+)(\/regenerate)?$/);
|
||||
assert.ok(match, `Unexpected ${method} ${path}`);
|
||||
const credential = persisted.find(entry => entry.key_id === match[1]);
|
||||
assert.ok(credential);
|
||||
if (method === 'GET') return { credential: structuredClone(credential) };
|
||||
if (method === 'PATCH') {
|
||||
assert.deepEqual(Object.keys(data).sort(), ['allow_deploy', 'certificate_ids', 'comment']);
|
||||
assert.ok(!('token' in data) && !('token_hash' in data));
|
||||
Object.assign(credential, { comment: data.comment, certificate_ids: data.certificate_ids, allow_deploy: data.allow_deploy });
|
||||
return { credential: structuredClone(credential) };
|
||||
}
|
||||
if (method === 'POST' && match[2] === '/regenerate') {
|
||||
activeToken = 'replacement-secret-once';
|
||||
credential.last_used_at = null;
|
||||
credential.created_at = 1800000001;
|
||||
return { credential: structuredClone(credential), token: activeToken };
|
||||
}
|
||||
if (method === 'DELETE') {
|
||||
persisted = persisted.filter(entry => entry.key_id !== credential.key_id);
|
||||
return { revoked: true };
|
||||
}
|
||||
throw new Error(`Unsupported ${method} ${path}`);
|
||||
}
|
||||
const toasts = [];
|
||||
const ctx = {
|
||||
document: {}, window: {}, $: dollar, el: node,
|
||||
action: (text, cb) => { const result = node('button', text, 'quiet'); result.addEventListener('click', cb); result.type = 'button'; return result; },
|
||||
t: key => english[key] || key,
|
||||
location: { origin: 'https://admin.example.test' },
|
||||
authenticated: true, api, appConfirm: async () => true,
|
||||
formatDateTime: timestamp => new Date(timestamp).toISOString(),
|
||||
toast: value => toasts.push(value),
|
||||
fieldError: (_, message) => new Error(message), showFieldError: () => {},
|
||||
};
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(source, ctx, { filename: 'src/admin/web/certificate-api.js' });
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
const list = dollar('certificate-api-credentials');
|
||||
const actionsFor = (rowIndex = 0) => list.children[rowIndex].children[0].children[1].children;
|
||||
const click = async (label, rowIndex = 0) => {
|
||||
const btn = actionsFor(rowIndex).find(item => item.content === label);
|
||||
assert.ok(btn, `Missing button: ${label}`);
|
||||
await btn.fire('click');
|
||||
};
|
||||
assert.equal(list.children.length, 1);
|
||||
assert.ok(requests.some(request => request.path === '/api/certificate-api' && request.method === 'GET'));
|
||||
|
||||
await click(english.certificateApiView);
|
||||
assert.equal(list.children[0].children.length, 2, 'details expanded');
|
||||
const previewText = list.children[0].children[1].textContent;
|
||||
assert.match(previewText, /renew-prod/);
|
||||
assert.match(previewText, /Old owner/);
|
||||
assert.match(previewText, /first-cert/);
|
||||
assert.match(previewText, /Unavailable/);
|
||||
assert.ok(!previewText.includes(activeToken), 'never show existing bearer token');
|
||||
assert.ok(requests.some(request => request.path === '/api/certificate-api/renew-prod' && request.method === 'GET'));
|
||||
|
||||
await click(english.certificateApiEdit);
|
||||
assert.equal(dollar('certificate-api-key-id').value, 'renew-prod');
|
||||
assert.equal(dollar('certificate-api-key-id').readOnly, true);
|
||||
assert.equal(dollar('certificate-api-comment').value, 'Old owner');
|
||||
assert.equal(dollar('certificate-api-token-field').hidden, true);
|
||||
assert.equal(dollar('certificate-api-save').content, english.certificateApiSave);
|
||||
assert.equal(dollar('certificate-api-cancel-edit').hidden, false);
|
||||
dollar('certificate-api-comment').value = 'New owner';
|
||||
dollar('certificate-api-certificate-ids').value = 'second-cert,third-cert';
|
||||
dollar('certificate-api-allow-deploy').checked = true;
|
||||
await dollar('certificate-api-form').fire('submit', { preventDefault() {} });
|
||||
assert.equal(persisted[0].comment, 'New owner');
|
||||
assert.deepEqual(persisted[0].certificate_ids, ['second-cert', 'third-cert']);
|
||||
assert.equal(persisted[0].allow_deploy, true);
|
||||
assert.equal(persisted[0].created_at, 1690000000, 'editing must not rotate');
|
||||
assert.equal(activeToken, 'original-secret-not-readable');
|
||||
assert.equal(dollar('certificate-api-token-field').hidden, false);
|
||||
assert.equal(dollar('certificate-api-save').content, english.certificateApiRotate);
|
||||
assert.equal(dollar('certificate-api-secret-wrap').hidden, true);
|
||||
|
||||
await click(english.certificateApiRegenerate);
|
||||
assert.equal(activeToken, 'replacement-secret-once');
|
||||
assert.equal(dollar('certificate-api-secret').textContent, activeToken);
|
||||
assert.equal(dollar('certificate-api-secret-wrap').hidden, false);
|
||||
assert.equal(persisted[0].comment, 'New owner', 'metadata unchanged by regeneration');
|
||||
assert.equal(persisted[0].allow_deploy, true);
|
||||
assert.equal(persisted[0].last_used_at, null);
|
||||
assert.equal(list.textContent.includes(activeToken), false, 'token not in persistent credential DOM');
|
||||
await click(english.certificateApiHide);
|
||||
assert.equal(dollar('certificate-api-secret').textContent, 'replacement-secret-once', 'collapsing is not a new API secret read');
|
||||
await click(english.certificateApiView);
|
||||
assert.equal(dollar('certificate-api-secret-wrap').hidden, true, 'reading settings does not re-expose the token');
|
||||
assert.equal(dollar('certificate-api-secret').textContent, '');
|
||||
assert.ok(!list.textContent.includes(activeToken));
|
||||
|
||||
ctx.window.rustProxyCertificateApi.reset();
|
||||
assert.equal(dollar('certificate-api-secret').textContent, '');
|
||||
assert.equal(list.children.length, 0);
|
||||
await ctx.window.rustProxyCertificateApi.load();
|
||||
assert.equal(list.children.length, 1);
|
||||
assert.ok(!list.textContent.includes(activeToken));
|
||||
assert.equal(requests.filter(item => item.path.endsWith('/regenerate')).length, 1);
|
||||
assert.ok(toasts.includes(english.certificateApiSaved));
|
||||
assert.ok(toasts.includes(english.certificateApiRegenerated));
|
||||
console.log('PASS: 0.31.9 Certificate API view/edit/regenerate/one-time secret UI (real module, simulated DOM/API).');
|
||||
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
|
||||
|
||||
preflight(root)
|
||||
manifest = tomllib.loads((root / "Cargo.toml").read_text())
|
||||
assert manifest["package"]["version"] == "0.31.8"
|
||||
assert manifest["package"]["version"] == "0.31.9"
|
||||
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
|
||||
h3dep = manifest["dependencies"]["h3"]
|
||||
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.20" and "quinn" in h3dep["features"]
|
||||
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
|
||||
lock_path = root / "Cargo.lock"
|
||||
if lock_path.exists():
|
||||
lock = lock_path.read_text()
|
||||
assert 'name = "rust-proxy"\nversion = "0.31.8"' in lock
|
||||
assert 'name = "rust-proxy"\nversion = "0.31.9"' in lock
|
||||
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
|
||||
for release_doc, marker in {
|
||||
"README.md": "# RustProxy 0.31.8",
|
||||
"CHANGELOG.md": "## 0.31.8",
|
||||
"ROADMAP.md": "## 0.31.8",
|
||||
"AGENTS.md": "Current source version: 0.31.8.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.31.8",
|
||||
"README.md": "# RustProxy 0.31.9",
|
||||
"CHANGELOG.md": "## 0.31.9",
|
||||
"ROADMAP.md": "## 0.31.9",
|
||||
"AGENTS.md": "Current source version: 0.31.9.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.31.9",
|
||||
}.items():
|
||||
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
|
||||
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
|
||||
@@ -285,7 +285,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
|
||||
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
|
||||
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
|
||||
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
|
||||
for marker in ['"version": "0.31.8"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
||||
for marker in ['"version": "0.31.9"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
||||
assert marker in openapi, marker
|
||||
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
assert marker in openapi, marker
|
||||
@@ -569,6 +569,7 @@ assert 'const panelPaths = new Set' in login_js and 'panelPaths.has(location.pat
|
||||
assert '"/404-hosts"' in login_js
|
||||
assert 'async function redirectExistingSession' in login_js and '/api/auth/session' in login_js
|
||||
settings_js = (root / "src/admin/web/settings.js").read_text()
|
||||
certificate_settings_js = (root / "src/admin/web/certificate-api.js").read_text()
|
||||
assert '["/settings", "/configuration", "/logging"].includes(location.pathname)' in settings_js
|
||||
assert 'new URLSearchParams(location.search).get("tab")' in settings_js and 'function syncLocation()' in settings_js and 'history.replaceState' in settings_js
|
||||
assert 'url.pathname = "/settings"' in settings_js and 'url.searchParams.set("tab", active)' in settings_js
|
||||
@@ -607,7 +608,7 @@ assert 'data-i18n="processDiagnostics">Process<' in html
|
||||
for marker in ['function setOptionalMetric', 'setOptionalMetric("process-rss-anon"', 'setOptionalMetric("system-swap"', 'closest(".mini-live-chart").hidden']:
|
||||
assert marker in app_js, marker
|
||||
for marker in ['/api/certificate-api', 'certificate-api-form', 'certificate-api-revoke-all', 'certificate-api-credentials', 'data-cache-settings-tab']:
|
||||
assert marker in settings_js or marker in html, marker
|
||||
assert marker in settings_js or marker in certificate_settings_js or marker in html, marker
|
||||
db_source = rust_source(root, "src/db.rs")
|
||||
acme_source = rust_source(root, "src/acme.rs")
|
||||
acme_runtime_source = (root / "src/acme/runtime_env.rs").read_text(encoding="utf-8") + (root / "src/acme/runtime_bootstrap.rs").read_text(encoding="utf-8")
|
||||
@@ -1395,7 +1396,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
|
||||
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
|
||||
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
|
||||
assert 'location.pathname === "/logging"' in settings_js_0241
|
||||
assert '"RuntimeInformation"' in openapi and '"version": "0.31.8"' in openapi
|
||||
assert '"RuntimeInformation"' in openapi and '"version": "0.31.9"' in openapi
|
||||
|
||||
# 0.24.1 compiler and hostname-cardinality regressions.
|
||||
tls_0241 = rust_source(root, "src/tls.rs")
|
||||
@@ -1634,5 +1635,5 @@ for marker_0282 in [
|
||||
assert 'Zewnętrzne ścieżki plików' in (root / 'langs/pl_pl.json').read_text()
|
||||
assert 'setfacl -m u:rustproxy:--x /etc/ssl/' in (root / 'docs/INSTALL.md').read_text()
|
||||
|
||||
print("PASS: 0.31.8 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v9 migrations.")
|
||||
print("PASS: 0.31.9 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v9 migrations.")
|
||||
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the 0.31.8 dependency/source migration; optionally require a resolved lock.
|
||||
"""Check the 0.31.9 dependency/source migration; optionally require a resolved lock.
|
||||
|
||||
This is not a substitute for rustc, cargo metadata or runtime tests.
|
||||
"""
|
||||
@@ -14,7 +14,7 @@ parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--require-lock', action='store_true', help='Require the post-upgrade Cargo.lock produced by cargo update')
|
||||
args = parser.parse_args()
|
||||
manifest = tomllib.loads((ROOT / 'Cargo.toml').read_text())
|
||||
assert manifest['package']['version'] == '0.31.8'
|
||||
assert manifest['package']['version'] == '0.31.9'
|
||||
assert manifest['package']['rust-version'] == '1.99'
|
||||
deps = manifest['dependencies']
|
||||
expected_requirements = {
|
||||
@@ -78,4 +78,4 @@ if missing:
|
||||
print(f'PENDING: lockfile still requires cargo update ({details})')
|
||||
else:
|
||||
print('PASS: pinned key packages found in post-upgrade lock; cargo --locked verification is still required')
|
||||
print('PASS: 0.31.8 source dependency APIs, manifest, TLS features, toolchain images and Swagger pin (static only)')
|
||||
print('PASS: 0.31.9 source dependency APIs, manifest, TLS features, toolchain images and Swagger pin (static only)')
|
||||
@@ -164,4 +164,4 @@ config.docker_agents.agents = [{ ...agent, enabled: false }];
|
||||
ctx.window.rustProxyDocker.render();
|
||||
assert.equal(refreshSpec.active(), false, 'disabled agents do not cause polling');
|
||||
assert.doesNotMatch(destination.textContent, /Last Docker agent check failed/);
|
||||
console.log('PASS: 0.31.8 Docker startup awaiting/poll failure/success, pinned TLS semantics, Live-off bootstrap and return-navigation refresh (real JS, simulated DOM/API).');
|
||||
console.log('PASS: 0.31.9 Docker startup awaiting/poll failure/success, pinned TLS semantics, Live-off bootstrap and return-navigation refresh (real JS, simulated DOM/API).');
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rust-independent documentation/Reader consistency checks for the 0.31.8 release."""
|
||||
"""Rust-independent documentation/Reader consistency checks for the 0.31.9 release."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
from rust_source import rust_source
|
||||
@@ -51,7 +51,7 @@ assert '"/docs/content.json"' in admin
|
||||
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
|
||||
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
||||
assert readme.startswith("# RustProxy 0.31.8")
|
||||
assert readme.startswith("# RustProxy 0.31.9")
|
||||
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
|
||||
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
|
||||
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
|
||||
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
html = (ROOT / 'src/admin/web/index.html').read_text()
|
||||
assert '0.31.8' in (ROOT / "Cargo.toml").read_text()
|
||||
assert '0.31.9' in (ROOT / "Cargo.toml").read_text()
|
||||
assert '<a class="brand" href="/"' in html, 'logo must remain a true full-page navigation'
|
||||
assert 'addEventListener("click"' not in html.split('<a class="brand" href="/"',1)[1].split('</a>',1)[0]
|
||||
body = (ROOT / 'src/proxy/cache_helpers.rs').read_text()
|
||||
|
||||
@@ -76,13 +76,13 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
|
||||
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
|
||||
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/vhost/error_fallback.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/rates.js", "src/admin/web/vhosts/health.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/session-resume.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "tools/checks/docs_check.py", "tools/checks/web_server_contracts.py", "tools/checks/rust_source.py", "tools/checks/rust_module_contracts.py", "tools/checks/source_module_contracts.py", "scripts/runtime/acme_provider.py", "src/basic_auth.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json"]:
|
||||
if not (root / path).is_file():
|
||||
raise ValueError(f"Incomplete RustProxy 0.31.8 source tree: missing {path}")
|
||||
raise ValueError(f"Incomplete RustProxy 0.31.9 source tree: missing {path}")
|
||||
# The Reader uses compile-time include_str! for its bundled Markdown. A
|
||||
# dangling registration is a real compiler error, unlike an omitted old
|
||||
# release audit. Match both reader_doc! and the expanded include_str! form.
|
||||
registry = root / "src/admin/documentation/documents.rs"
|
||||
if not registry.is_file():
|
||||
raise ValueError("Incomplete RustProxy 0.31.8 source tree: missing src/admin/documentation/documents.rs")
|
||||
raise ValueError("Incomplete RustProxy 0.31.9 source tree: missing src/admin/documentation/documents.rs")
|
||||
embedded_docs = set(re.findall(
|
||||
r'"(docs/[A-Za-z0-9_.-]+\.md)"', registry.read_text(encoding="utf-8")
|
||||
))
|
||||
@@ -91,7 +91,7 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
|
||||
raise ValueError(f"Missing embedded Reader document: {doc_path}; restore the file or remove its Reader registration")
|
||||
text = (root / "src/lib.rs").read_text(encoding="utf-8")
|
||||
if '#[path = "auth/mod.rs"]' not in text:
|
||||
raise ValueError("src/lib.rs is not the 0.31.8 entry point; extract the complete package, not individual files.")
|
||||
raise ValueError("src/lib.rs is not the 0.31.9 entry point; extract the complete package, not individual files.")
|
||||
return messages
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""0.31.8 source-only regression; does NOT claim Rust compilation or memory profiling."""
|
||||
"""0.31.9 source-only regression; does NOT claim Rust compilation or memory profiling."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import re
|
||||
@@ -8,9 +8,9 @@ from html.parser import HTMLParser
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
def read(rel): return (ROOT / rel).read_text(encoding='utf-8')
|
||||
assert tomllib.loads(read('Cargo.toml'))['package']['version'] == '0.31.8'
|
||||
assert 'name = "rust-proxy"\nversion = "0.31.8"' in read('Cargo.lock')
|
||||
assert json.loads(read('src/admin/openapi.json'))['info']['version'] == '0.31.8'
|
||||
assert tomllib.loads(read('Cargo.toml'))['package']['version'] == '0.31.9'
|
||||
assert 'name = "rust-proxy"\nversion = "0.31.9"' in read('Cargo.lock')
|
||||
assert json.loads(read('src/admin/openapi.json'))['info']['version'] == '0.31.9'
|
||||
|
||||
pages = read('src/pages.rs')
|
||||
for code in (400, 401, 403, 404, 408, 410, 413, 429, 500, 502, 503, 504):
|
||||
@@ -43,4 +43,4 @@ for marker in ('sort_by: Option<String>', 'sort_descending: Option<bool>', 'vali
|
||||
'file_log_field(record, field)', 'None, Some(_)', 'sort_file_log_records(&mut records'):
|
||||
assert marker in q, marker
|
||||
assert q.index('sort_file_log_records(&mut records') < q.index('let page = records')
|
||||
print('PASS: 0.31.8 versions, 13 matching HTML pages, GeoIP packed/boxed sources, cache ownership/accounting and bounded server sorting (static only).')
|
||||
print('PASS: 0.31.9 versions, 13 matching HTML pages, GeoIP packed/boxed sources, cache ownership/accounting and bounded server sorting (static only).')
|
||||
@@ -60,4 +60,4 @@ assert.equal(ctx.sortDescending, true);
|
||||
assert.equal(row.children[1].attrs['aria-sort'], 'descending');
|
||||
assert.equal(ctx.saved, 2);
|
||||
assert.equal(ctx.refreshed, 2);
|
||||
console.log('PASS: 0.31.8 Stored Logs headers sort by column, toggle direction, reset pagination, persist preferences and request server sorting.');
|
||||
console.log('PASS: 0.31.9 Stored Logs headers sort by column, toggle direction, reset pagination, persist preferences and request server sorting.');
|
||||
@@ -135,7 +135,7 @@ def upgrade_smoke(tmp: Path) -> None:
|
||||
assert backup.is_dir()
|
||||
assert (backup / "src/obsolete.rs").is_file()
|
||||
assert not (target / "src/obsolete.rs").exists()
|
||||
assert 'version = "0.31.8"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert 'version = "0.31.9"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
|
||||
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
|
||||
assert (target / "langs/site.json").is_file()
|
||||
@@ -146,7 +146,7 @@ def upgrade_smoke(tmp: Path) -> None:
|
||||
assert (target / "docker-agent/certificate.go").is_file()
|
||||
assert (target / "docker-agent/server.go").is_file()
|
||||
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
|
||||
assert manifest["to_version"] == "0.31.8"
|
||||
assert manifest["to_version"] == "0.31.9"
|
||||
assert manifest["removed_obsolete_runtime_state"] is True
|
||||
assert not legacy.exists()
|
||||
assert not (backup / "legacy-data").exists()
|
||||
@@ -617,7 +617,7 @@ def source_contract_smoke() -> None:
|
||||
assert marker in admin, marker
|
||||
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
|
||||
assert '"/api/status"' not in anonymous_block
|
||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.31.8"' in openapi_0241
|
||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.31.9"' in openapi_0241
|
||||
assert 'id="panel-logging"' not in html
|
||||
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
|
||||
assert marker in html, marker
|
||||
@@ -1102,7 +1102,7 @@ def source_contract_smoke() -> None:
|
||||
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
|
||||
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
|
||||
assert '"/api/config/history/restore"' in openapi
|
||||
for marker in ['"version": "0.31.8"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
for marker in ['"version": "0.31.9"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
assert marker in openapi, marker
|
||||
assert '"software_update"' in openapi and '"update_available"' in openapi
|
||||
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
|
||||
@@ -1315,7 +1315,7 @@ def main() -> None:
|
||||
source_contract_smoke()
|
||||
container_contract_smoke()
|
||||
grafana_contract_smoke()
|
||||
print("PASS: 0.31.8 SQLite/schema-9/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||
print("PASS: 0.31.9 SQLite/schema-9/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -21,7 +21,7 @@ const openapi = json('src/admin/openapi.json');
|
||||
const en = json('langs/en_US.json').translations;
|
||||
const pl = json('langs/pl_pl.json').translations;
|
||||
|
||||
assert.match(manifest, /version = "0\.31\.8"/);
|
||||
assert.match(manifest, /version = "0\.31\.9"/);
|
||||
assert.match(lib, /pub mod user_agent;/);
|
||||
assert.match(model, /user_agent_blocking: crate::user_agent::Policy/);
|
||||
assert.match(vhost, /user_agent_blocking: crate::user_agent::Policy/);
|
||||
@@ -52,7 +52,7 @@ assert.match(fileLogs, /localStorage\.setItem\(CONSOLIDATION_STORAGE_KEY/);
|
||||
assert.match(fileLogs, /aggregate_min_count/);
|
||||
assert.match(fileLogs, /file-log-aggregate-copy/);
|
||||
|
||||
assert.equal(openapi.info.version, '0.31.8');
|
||||
assert.equal(openapi.info.version, '0.31.9');
|
||||
assert.ok(openapi.components.schemas.Config.properties.user_agent_blocking);
|
||||
assert.ok(openapi.components.schemas.Vhost.properties.user_agent_blocking);
|
||||
assert.ok(openapi.components.schemas.FileLogSource.properties.group_id);
|
||||
|
||||
@@ -43,7 +43,7 @@ const code = js.slice(js.indexOf(' const runtime = latestStatus?.runtime || {
|
||||
const elements = new Map();
|
||||
const dollar = id => { if (!elements.has(id)) elements.set(id, { textContent: '' }); return elements.get(id); };
|
||||
const status = {
|
||||
version: '0.31.8', uptime_seconds: 4201,
|
||||
version: '0.31.9', uptime_seconds: 4201,
|
||||
runtime: {
|
||||
platform: 'Linux', operating_system: 'Debian GNU/Linux 13', kernel_version: '6.12.0', architecture: 'x86_64',
|
||||
statistics_source: 'systemstat+sysinfo', data_directory: '/var/lib/rustproxy', database_path: '/var/lib/rustproxy/config.sqlite3',
|
||||
@@ -63,7 +63,7 @@ const context = {
|
||||
vm.runInNewContext(code, context);
|
||||
assert.equal(dollar('runtime-operating-system').textContent, 'Debian GNU/Linux 13');
|
||||
assert.equal(dollar('runtime-kernel-version').textContent, '6.12.0');
|
||||
assert.equal(dollar('runtime-version').textContent, '0.31.8');
|
||||
assert.equal(dollar('runtime-version').textContent, '0.31.9');
|
||||
assert.equal(dollar('runtime-logical-cpus').textContent, '8');
|
||||
assert.equal(dollar('runtime-cpu-usage').textContent, '17.2%');
|
||||
assert.equal(dollar('runtime-python').textContent, 'Available · 3.13.5');
|
||||
@@ -124,4 +124,4 @@ for (const key of ['python_version','pip_version','certbot_version']) {
|
||||
assert.ok(runtimeSchema.properties.certificate_runtime.properties[key]);
|
||||
assert.equal(runtimeSchema.properties.certificate_runtime.properties[key].nullable, true);
|
||||
}
|
||||
console.log('PASS: 0.31.8 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.');
|
||||
console.log('PASS: 0.31.9 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.');
|
||||
@@ -5,7 +5,7 @@ PANEL_PARTS = (
|
||||
"management.js", "drafts.js", "integrations.js", "vhosts.js",
|
||||
"web-server-profiles.js", "web-server.js", "docker.js", "streams.js",
|
||||
"redirects.js", "host404.js", "traffic-panel.js", "cache-panel.js",
|
||||
"transform-panel.js", "acme.js", "settings.js", "file-logs.js",
|
||||
"transform-panel.js", "acme.js", "settings.js", "certificate-api.js", "file-logs.js",
|
||||
)
|
||||
VHOST_PARTS = ("core.js", "health.js", "rates.js", "editor.js", "render.js")
|
||||
|
||||
|
||||
Reference in new issue
Block a user