Skip to content

Add GitHub Actions CI and expand unit test coverage - #2

Merged
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests
Aug 27, 2026
Merged

Add GitHub Actions CI and expand unit test coverage#2
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests

Conversation

@daedeloth

Copy link
Copy Markdown
Member

Workflow

.github/workflows/tests.yml runs on push to master and on every pull request:

  • actions/checkout@v4
  • shivammathur/setup-php@v2 on a PHP 8.1 / 8.2 / 8.3 / 8.4 / 8.5 matrix (fail-fast: false), extensions: gettext, mbstring, curl (gettext is a hard requirement of catlabinteractive/neuron), no coverage
  • composer install --prefer-dist --no-progress --no-interaction
  • vendor/bin/phpunit

composer.lock is git-ignored, so every job resolves fresh; all dependencies (neuron >=7.4, guzzle ^7, phpunit ^9.6) accept the full range, so no version was dropped from the matrix. Locally the suite is green on PHP 8.5.4 with error_reporting=E_ALL and display_errors=1 (no notices/deprecations) against a fresh composer install (neuron v3.1.6).

Tests added (9 → 39 tests)

All run without a database or network; HTTP goes through Guzzle's MockHandler, following the existing BasicFlowTest style. Each new test was checked against a deliberate breakage of the behaviour it pins (removing the state unset, the is_string guard, the updateLastPing call, the post-login-redirect clear, letting mergeFromInput copy the email) and fails as expected.

tests/BasicFlowTest.php (extended)

  • array scope / response_type are space-joined
  • every authorization request gets a fresh 32-hex-char state; only the latest is accepted
  • state is single-use: a replayed callback is rejected and the session key is gone
  • callback with no pending request, missing state, or a non-string (array) state is rejected with OpenIDConnectException (no TypeError from hash_equals)
  • getAuthorizationCode() falls back to $_GET
  • error_description ends up in the exception message
  • token response without access_token → exception; error wins even when access_token is present
  • non-object bodies (HTML, empty, null, string, number) are rejected as invalid JSON (userinfo)
  • userinfo HTTP 401 → OpenIDConnectException
  • transport failures (ConnectException) become OpenIDConnectException with the Guzzle exception as getPrevious()
  • the client secret travels only in the POST body (no Authorization header, not in the URL)

tests/UserModelTest.php (new, uses tests/Fakes/RecordingUserMapper.php)

  • mergeFromInput() only takes username; email/sub from the claims payload are ignored; display name kept when username absent
  • shouldPing(): true when never pinged / when older than pingInterval (and stamps lastPing, calls updateLastPing), false within the interval (nothing persisted), pingInterval is per-instance
  • anonymize() nulls email + display name, keeps id/sub, persists via update()
  • Guest never exposes an id or personal data even if setters are called

tests/ModuleTest.php (new)

  • login() stores catlab-user-id + catlab-openid-access-token in the session, fires user:login, returns a 302
  • post-login-redirect is honoured once and then cleared (with cancel-login-redirect); fallback is the app root
  • logout() clears both session keys, fires user:logout, redirects to /

The fake mapper is registered once in Neuron's MapperFactory (a process-wide singleton without a reset), which is why it lives in tests/Fakes/ and is shared.

Deliberately not covered

  • Mappers\UserMapper (all methods build Neuron\DB\Query objects and execute them) and Module::setRequestUser() / setUserMapper() — need MySQL or a real mapper in the singleton.
  • Controllers\LoginController (login, next, cookieGate, processLogin, touchUser) — depends on Neuron\Config, the DB-backed mapper, setcookie() and Application::getInstance(); would need an integration harness.
  • Helpers\LoginForm — needs a running Neuron\Application router and template paths.
  • User::ping() / activity() / createAuthenticatedRequest() — owned by 4.1: ping/activity send the access token in the Authorization header #1 (fix/bearer-header-tokens, tests/UserTokenTransportTest.php); nothing here touches those methods or that file, so the two PRs merge cleanly.
  • The client_secret form param when authentication_info is absent is sent as an empty value; existing behaviour, left as is rather than pinned.

No changes to src/. .phpunit.result.cache is now git-ignored.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o

CI: .github/workflows/tests.yml runs vendor/bin/phpunit on push to master
and on pull requests, on a PHP 8.1-8.5 matrix (composer.lock is not
tracked, so each job resolves fresh; every dependency accepts the whole
range). gettext is enabled explicitly because catlabinteractive/neuron
requires ext-gettext.
Tests (no database, no network; HTTP goes through Guzzle's MockHandler):
- BasicFlowTest: array scope/response_type joining, fresh state per
request, state is single-use, callback without pending request,
missing/non-string state (no TypeError), $_GET fallback, error
description in the message, token response without access_token,
error wins over access_token, non-object JSON bodies rejected
(userinfo), userinfo HTTP error, Guzzle transport exception kept as
previous, client secret only in the POST body.
- UserModelTest: mergeFromInput only takes the username, shouldPing
first/stale/recent/custom-interval behaviour and its updateLastPing
persistence, anonymize strips email+display name and persists, Guest
never exposes an id or personal data. Uses Tests\Fakes\
RecordingUserMapper registered once in Neuron's MapperFactory.
- ModuleTest: login stores user id + access token in the session and
fires user:login, post-login-redirect is honoured once and cleared,
fallback to app root, logout clears the session and fires user:logout.
.phpunit.result.cache is now ignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o
@daedeloth
daedeloth merged commit d9815b5 into masterAug 27, 2026
5 checks passed
Sign up for freeto 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

@daedeloth
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Add GitHub Actions CI and expand unit test coverage by daedeloth · Pull Request #2 · CatLabInteractive/OpenID-Connect-Client · GitHub
Skip to content

Add GitHub Actions CI and expand unit test coverage - #2

Merged
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests
Aug 27, 2026
Merged

Add GitHub Actions CI and expand unit test coverage#2
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests

Conversation

@daedeloth

Copy link
Copy Markdown
Member

Workflow

.github/workflows/tests.yml runs on push to master and on every pull request:

  • actions/checkout@v4
  • shivammathur/setup-php@v2 on a PHP 8.1 / 8.2 / 8.3 / 8.4 / 8.5 matrix (fail-fast: false), extensions: gettext, mbstring, curl (gettext is a hard requirement of catlabinteractive/neuron), no coverage
  • composer install --prefer-dist --no-progress --no-interaction
  • vendor/bin/phpunit

composer.lock is git-ignored, so every job resolves fresh; all dependencies (neuron >=7.4, guzzle ^7, phpunit ^9.6) accept the full range, so no version was dropped from the matrix. Locally the suite is green on PHP 8.5.4 with error_reporting=E_ALL and display_errors=1 (no notices/deprecations) against a fresh composer install (neuron v3.1.6).

Tests added (9 → 39 tests)

All run without a database or network; HTTP goes through Guzzle's MockHandler, following the existing BasicFlowTest style. Each new test was checked against a deliberate breakage of the behaviour it pins (removing the state unset, the is_string guard, the updateLastPing call, the post-login-redirect clear, letting mergeFromInput copy the email) and fails as expected.

tests/BasicFlowTest.php (extended)

  • array scope / response_type are space-joined
  • every authorization request gets a fresh 32-hex-char state; only the latest is accepted
  • state is single-use: a replayed callback is rejected and the session key is gone
  • callback with no pending request, missing state, or a non-string (array) state is rejected with OpenIDConnectException (no TypeError from hash_equals)
  • getAuthorizationCode() falls back to $_GET
  • error_description ends up in the exception message
  • token response without access_token → exception; error wins even when access_token is present
  • non-object bodies (HTML, empty, null, string, number) are rejected as invalid JSON (userinfo)
  • userinfo HTTP 401 → OpenIDConnectException
  • transport failures (ConnectException) become OpenIDConnectException with the Guzzle exception as getPrevious()
  • the client secret travels only in the POST body (no Authorization header, not in the URL)

tests/UserModelTest.php (new, uses tests/Fakes/RecordingUserMapper.php)

  • mergeFromInput() only takes username; email/sub from the claims payload are ignored; display name kept when username absent
  • shouldPing(): true when never pinged / when older than pingInterval (and stamps lastPing, calls updateLastPing), false within the interval (nothing persisted), pingInterval is per-instance
  • anonymize() nulls email + display name, keeps id/sub, persists via update()
  • Guest never exposes an id or personal data even if setters are called

tests/ModuleTest.php (new)

  • login() stores catlab-user-id + catlab-openid-access-token in the session, fires user:login, returns a 302
  • post-login-redirect is honoured once and then cleared (with cancel-login-redirect); fallback is the app root
  • logout() clears both session keys, fires user:logout, redirects to /

The fake mapper is registered once in Neuron's MapperFactory (a process-wide singleton without a reset), which is why it lives in tests/Fakes/ and is shared.

Deliberately not covered

  • Mappers\UserMapper (all methods build Neuron\DB\Query objects and execute them) and Module::setRequestUser() / setUserMapper() — need MySQL or a real mapper in the singleton.
  • Controllers\LoginController (login, next, cookieGate, processLogin, touchUser) — depends on Neuron\Config, the DB-backed mapper, setcookie() and Application::getInstance(); would need an integration harness.
  • Helpers\LoginForm — needs a running Neuron\Application router and template paths.
  • User::ping() / activity() / createAuthenticatedRequest() — owned by 4.1: ping/activity send the access token in the Authorization header #1 (fix/bearer-header-tokens, tests/UserTokenTransportTest.php); nothing here touches those methods or that file, so the two PRs merge cleanly.
  • The client_secret form param when authentication_info is absent is sent as an empty value; existing behaviour, left as is rather than pinned.

No changes to src/. .phpunit.result.cache is now git-ignored.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o

CI: .github/workflows/tests.yml runs vendor/bin/phpunit on push to master
and on pull requests, on a PHP 8.1-8.5 matrix (composer.lock is not
tracked, so each job resolves fresh; every dependency accepts the whole
range). gettext is enabled explicitly because catlabinteractive/neuron
requires ext-gettext.
Tests (no database, no network; HTTP goes through Guzzle's MockHandler):
- BasicFlowTest: array scope/response_type joining, fresh state per
request, state is single-use, callback without pending request,
missing/non-string state (no TypeError), $_GET fallback, error
description in the message, token response without access_token,
error wins over access_token, non-object JSON bodies rejected
(userinfo), userinfo HTTP error, Guzzle transport exception kept as
previous, client secret only in the POST body.
- UserModelTest: mergeFromInput only takes the username, shouldPing
first/stale/recent/custom-interval behaviour and its updateLastPing
persistence, anonymize strips email+display name and persists, Guest
never exposes an id or personal data. Uses Tests\Fakes\
RecordingUserMapper registered once in Neuron's MapperFactory.
- ModuleTest: login stores user id + access token in the session and
fires user:login, post-login-redirect is honoured once and cleared,
fallback to app root, logout clears the session and fires user:logout.
.phpunit.result.cache is now ignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o
@daedeloth
daedeloth merged commit d9815b5 into masterAug 27, 2026
5 checks passed
Sign up for freeto 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

@daedeloth
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add GitHub Actions CI and expand unit test coverage by daedeloth · Pull Request #2 · CatLabInteractive/OpenID-Connect-Client · GitHub
Skip to content

Add GitHub Actions CI and expand unit test coverage - #2

Merged
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests
Aug 27, 2026
Merged

Add GitHub Actions CI and expand unit test coverage#2
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests

Conversation

@daedeloth

Copy link
Copy Markdown
Member

Workflow

.github/workflows/tests.yml runs on push to master and on every pull request:

  • actions/checkout@v4
  • shivammathur/setup-php@v2 on a PHP 8.1 / 8.2 / 8.3 / 8.4 / 8.5 matrix (fail-fast: false), extensions: gettext, mbstring, curl (gettext is a hard requirement of catlabinteractive/neuron), no coverage
  • composer install --prefer-dist --no-progress --no-interaction
  • vendor/bin/phpunit

composer.lock is git-ignored, so every job resolves fresh; all dependencies (neuron >=7.4, guzzle ^7, phpunit ^9.6) accept the full range, so no version was dropped from the matrix. Locally the suite is green on PHP 8.5.4 with error_reporting=E_ALL and display_errors=1 (no notices/deprecations) against a fresh composer install (neuron v3.1.6).

Tests added (9 → 39 tests)

All run without a database or network; HTTP goes through Guzzle's MockHandler, following the existing BasicFlowTest style. Each new test was checked against a deliberate breakage of the behaviour it pins (removing the state unset, the is_string guard, the updateLastPing call, the post-login-redirect clear, letting mergeFromInput copy the email) and fails as expected.

tests/BasicFlowTest.php (extended)

  • array scope / response_type are space-joined
  • every authorization request gets a fresh 32-hex-char state; only the latest is accepted
  • state is single-use: a replayed callback is rejected and the session key is gone
  • callback with no pending request, missing state, or a non-string (array) state is rejected with OpenIDConnectException (no TypeError from hash_equals)
  • getAuthorizationCode() falls back to $_GET
  • error_description ends up in the exception message
  • token response without access_token → exception; error wins even when access_token is present
  • non-object bodies (HTML, empty, null, string, number) are rejected as invalid JSON (userinfo)
  • userinfo HTTP 401 → OpenIDConnectException
  • transport failures (ConnectException) become OpenIDConnectException with the Guzzle exception as getPrevious()
  • the client secret travels only in the POST body (no Authorization header, not in the URL)

tests/UserModelTest.php (new, uses tests/Fakes/RecordingUserMapper.php)

  • mergeFromInput() only takes username; email/sub from the claims payload are ignored; display name kept when username absent
  • shouldPing(): true when never pinged / when older than pingInterval (and stamps lastPing, calls updateLastPing), false within the interval (nothing persisted), pingInterval is per-instance
  • anonymize() nulls email + display name, keeps id/sub, persists via update()
  • Guest never exposes an id or personal data even if setters are called

tests/ModuleTest.php (new)

  • login() stores catlab-user-id + catlab-openid-access-token in the session, fires user:login, returns a 302
  • post-login-redirect is honoured once and then cleared (with cancel-login-redirect); fallback is the app root
  • logout() clears both session keys, fires user:logout, redirects to /

The fake mapper is registered once in Neuron's MapperFactory (a process-wide singleton without a reset), which is why it lives in tests/Fakes/ and is shared.

Deliberately not covered

  • Mappers\UserMapper (all methods build Neuron\DB\Query objects and execute them) and Module::setRequestUser() / setUserMapper() — need MySQL or a real mapper in the singleton.
  • Controllers\LoginController (login, next, cookieGate, processLogin, touchUser) — depends on Neuron\Config, the DB-backed mapper, setcookie() and Application::getInstance(); would need an integration harness.
  • Helpers\LoginForm — needs a running Neuron\Application router and template paths.
  • User::ping() / activity() / createAuthenticatedRequest() — owned by 4.1: ping/activity send the access token in the Authorization header #1 (fix/bearer-header-tokens, tests/UserTokenTransportTest.php); nothing here touches those methods or that file, so the two PRs merge cleanly.
  • The client_secret form param when authentication_info is absent is sent as an empty value; existing behaviour, left as is rather than pinned.

No changes to src/. .phpunit.result.cache is now git-ignored.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o

CI: .github/workflows/tests.yml runs vendor/bin/phpunit on push to master
and on pull requests, on a PHP 8.1-8.5 matrix (composer.lock is not
tracked, so each job resolves fresh; every dependency accepts the whole
range). gettext is enabled explicitly because catlabinteractive/neuron
requires ext-gettext.
Tests (no database, no network; HTTP goes through Guzzle's MockHandler):
- BasicFlowTest: array scope/response_type joining, fresh state per
request, state is single-use, callback without pending request,
missing/non-string state (no TypeError), $_GET fallback, error
description in the message, token response without access_token,
error wins over access_token, non-object JSON bodies rejected
(userinfo), userinfo HTTP error, Guzzle transport exception kept as
previous, client secret only in the POST body.
- UserModelTest: mergeFromInput only takes the username, shouldPing
first/stale/recent/custom-interval behaviour and its updateLastPing
persistence, anonymize strips email+display name and persists, Guest
never exposes an id or personal data. Uses Tests\Fakes\
RecordingUserMapper registered once in Neuron's MapperFactory.
- ModuleTest: login stores user id + access token in the session and
fires user:login, post-login-redirect is honoured once and cleared,
fallback to app root, logout clears the session and fires user:logout.
.phpunit.result.cache is now ignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o
@daedeloth
daedeloth merged commit d9815b5 into masterAug 27, 2026
5 checks passed
Sign up for freeto 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

@daedeloth
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add GitHub Actions CI and expand unit test coverage by daedeloth · Pull Request #2 · CatLabInteractive/OpenID-Connect-Client · GitHub
Skip to content

Add GitHub Actions CI and expand unit test coverage - #2

Merged
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests
Aug 27, 2026
Merged

Add GitHub Actions CI and expand unit test coverage#2
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests

Conversation

@daedeloth

Copy link
Copy Markdown
Member

Workflow

.github/workflows/tests.yml runs on push to master and on every pull request:

  • actions/checkout@v4
  • shivammathur/setup-php@v2 on a PHP 8.1 / 8.2 / 8.3 / 8.4 / 8.5 matrix (fail-fast: false), extensions: gettext, mbstring, curl (gettext is a hard requirement of catlabinteractive/neuron), no coverage
  • composer install --prefer-dist --no-progress --no-interaction
  • vendor/bin/phpunit

composer.lock is git-ignored, so every job resolves fresh; all dependencies (neuron >=7.4, guzzle ^7, phpunit ^9.6) accept the full range, so no version was dropped from the matrix. Locally the suite is green on PHP 8.5.4 with error_reporting=E_ALL and display_errors=1 (no notices/deprecations) against a fresh composer install (neuron v3.1.6).

Tests added (9 → 39 tests)

All run without a database or network; HTTP goes through Guzzle's MockHandler, following the existing BasicFlowTest style. Each new test was checked against a deliberate breakage of the behaviour it pins (removing the state unset, the is_string guard, the updateLastPing call, the post-login-redirect clear, letting mergeFromInput copy the email) and fails as expected.

tests/BasicFlowTest.php (extended)

  • array scope / response_type are space-joined
  • every authorization request gets a fresh 32-hex-char state; only the latest is accepted
  • state is single-use: a replayed callback is rejected and the session key is gone
  • callback with no pending request, missing state, or a non-string (array) state is rejected with OpenIDConnectException (no TypeError from hash_equals)
  • getAuthorizationCode() falls back to $_GET
  • error_description ends up in the exception message
  • token response without access_token → exception; error wins even when access_token is present
  • non-object bodies (HTML, empty, null, string, number) are rejected as invalid JSON (userinfo)
  • userinfo HTTP 401 → OpenIDConnectException
  • transport failures (ConnectException) become OpenIDConnectException with the Guzzle exception as getPrevious()
  • the client secret travels only in the POST body (no Authorization header, not in the URL)

tests/UserModelTest.php (new, uses tests/Fakes/RecordingUserMapper.php)

  • mergeFromInput() only takes username; email/sub from the claims payload are ignored; display name kept when username absent
  • shouldPing(): true when never pinged / when older than pingInterval (and stamps lastPing, calls updateLastPing), false within the interval (nothing persisted), pingInterval is per-instance
  • anonymize() nulls email + display name, keeps id/sub, persists via update()
  • Guest never exposes an id or personal data even if setters are called

tests/ModuleTest.php (new)

  • login() stores catlab-user-id + catlab-openid-access-token in the session, fires user:login, returns a 302
  • post-login-redirect is honoured once and then cleared (with cancel-login-redirect); fallback is the app root
  • logout() clears both session keys, fires user:logout, redirects to /

The fake mapper is registered once in Neuron's MapperFactory (a process-wide singleton without a reset), which is why it lives in tests/Fakes/ and is shared.

Deliberately not covered

  • Mappers\UserMapper (all methods build Neuron\DB\Query objects and execute them) and Module::setRequestUser() / setUserMapper() — need MySQL or a real mapper in the singleton.
  • Controllers\LoginController (login, next, cookieGate, processLogin, touchUser) — depends on Neuron\Config, the DB-backed mapper, setcookie() and Application::getInstance(); would need an integration harness.
  • Helpers\LoginForm — needs a running Neuron\Application router and template paths.
  • User::ping() / activity() / createAuthenticatedRequest() — owned by 4.1: ping/activity send the access token in the Authorization header #1 (fix/bearer-header-tokens, tests/UserTokenTransportTest.php); nothing here touches those methods or that file, so the two PRs merge cleanly.
  • The client_secret form param when authentication_info is absent is sent as an empty value; existing behaviour, left as is rather than pinned.

No changes to src/. .phpunit.result.cache is now git-ignored.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o

CI: .github/workflows/tests.yml runs vendor/bin/phpunit on push to master
and on pull requests, on a PHP 8.1-8.5 matrix (composer.lock is not
tracked, so each job resolves fresh; every dependency accepts the whole
range). gettext is enabled explicitly because catlabinteractive/neuron
requires ext-gettext.
Tests (no database, no network; HTTP goes through Guzzle's MockHandler):
- BasicFlowTest: array scope/response_type joining, fresh state per
request, state is single-use, callback without pending request,
missing/non-string state (no TypeError), $_GET fallback, error
description in the message, token response without access_token,
error wins over access_token, non-object JSON bodies rejected
(userinfo), userinfo HTTP error, Guzzle transport exception kept as
previous, client secret only in the POST body.
- UserModelTest: mergeFromInput only takes the username, shouldPing
first/stale/recent/custom-interval behaviour and its updateLastPing
persistence, anonymize strips email+display name and persists, Guest
never exposes an id or personal data. Uses Tests\Fakes\
RecordingUserMapper registered once in Neuron's MapperFactory.
- ModuleTest: login stores user id + access token in the session and
fires user:login, post-login-redirect is honoured once and cleared,
fallback to app root, logout clears the session and fires user:logout.
.phpunit.result.cache is now ignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o
@daedeloth
daedeloth merged commit d9815b5 into masterAug 27, 2026
5 checks passed
Sign up for freeto 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

@daedeloth
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Add GitHub Actions CI and expand unit test coverage by daedeloth · Pull Request #2 · CatLabInteractive/OpenID-Connect-Client · GitHub
Skip to content

Add GitHub Actions CI and expand unit test coverage - #2

Merged
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests
Aug 27, 2026
Merged

Add GitHub Actions CI and expand unit test coverage#2
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests

Conversation

@daedeloth

Copy link
Copy Markdown
Member

Workflow

.github/workflows/tests.yml runs on push to master and on every pull request:

  • actions/checkout@v4
  • shivammathur/setup-php@v2 on a PHP 8.1 / 8.2 / 8.3 / 8.4 / 8.5 matrix (fail-fast: false), extensions: gettext, mbstring, curl (gettext is a hard requirement of catlabinteractive/neuron), no coverage
  • composer install --prefer-dist --no-progress --no-interaction
  • vendor/bin/phpunit

composer.lock is git-ignored, so every job resolves fresh; all dependencies (neuron >=7.4, guzzle ^7, phpunit ^9.6) accept the full range, so no version was dropped from the matrix. Locally the suite is green on PHP 8.5.4 with error_reporting=E_ALL and display_errors=1 (no notices/deprecations) against a fresh composer install (neuron v3.1.6).

Tests added (9 → 39 tests)

All run without a database or network; HTTP goes through Guzzle's MockHandler, following the existing BasicFlowTest style. Each new test was checked against a deliberate breakage of the behaviour it pins (removing the state unset, the is_string guard, the updateLastPing call, the post-login-redirect clear, letting mergeFromInput copy the email) and fails as expected.

tests/BasicFlowTest.php (extended)

  • array scope / response_type are space-joined
  • every authorization request gets a fresh 32-hex-char state; only the latest is accepted
  • state is single-use: a replayed callback is rejected and the session key is gone
  • callback with no pending request, missing state, or a non-string (array) state is rejected with OpenIDConnectException (no TypeError from hash_equals)
  • getAuthorizationCode() falls back to $_GET
  • error_description ends up in the exception message
  • token response without access_token → exception; error wins even when access_token is present
  • non-object bodies (HTML, empty, null, string, number) are rejected as invalid JSON (userinfo)
  • userinfo HTTP 401 → OpenIDConnectException
  • transport failures (ConnectException) become OpenIDConnectException with the Guzzle exception as getPrevious()
  • the client secret travels only in the POST body (no Authorization header, not in the URL)

tests/UserModelTest.php (new, uses tests/Fakes/RecordingUserMapper.php)

  • mergeFromInput() only takes username; email/sub from the claims payload are ignored; display name kept when username absent
  • shouldPing(): true when never pinged / when older than pingInterval (and stamps lastPing, calls updateLastPing), false within the interval (nothing persisted), pingInterval is per-instance
  • anonymize() nulls email + display name, keeps id/sub, persists via update()
  • Guest never exposes an id or personal data even if setters are called

tests/ModuleTest.php (new)

  • login() stores catlab-user-id + catlab-openid-access-token in the session, fires user:login, returns a 302
  • post-login-redirect is honoured once and then cleared (with cancel-login-redirect); fallback is the app root
  • logout() clears both session keys, fires user:logout, redirects to /

The fake mapper is registered once in Neuron's MapperFactory (a process-wide singleton without a reset), which is why it lives in tests/Fakes/ and is shared.

Deliberately not covered

  • Mappers\UserMapper (all methods build Neuron\DB\Query objects and execute them) and Module::setRequestUser() / setUserMapper() — need MySQL or a real mapper in the singleton.
  • Controllers\LoginController (login, next, cookieGate, processLogin, touchUser) — depends on Neuron\Config, the DB-backed mapper, setcookie() and Application::getInstance(); would need an integration harness.
  • Helpers\LoginForm — needs a running Neuron\Application router and template paths.
  • User::ping() / activity() / createAuthenticatedRequest() — owned by 4.1: ping/activity send the access token in the Authorization header #1 (fix/bearer-header-tokens, tests/UserTokenTransportTest.php); nothing here touches those methods or that file, so the two PRs merge cleanly.
  • The client_secret form param when authentication_info is absent is sent as an empty value; existing behaviour, left as is rather than pinned.

No changes to src/. .phpunit.result.cache is now git-ignored.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o

CI: .github/workflows/tests.yml runs vendor/bin/phpunit on push to master
and on pull requests, on a PHP 8.1-8.5 matrix (composer.lock is not
tracked, so each job resolves fresh; every dependency accepts the whole
range). gettext is enabled explicitly because catlabinteractive/neuron
requires ext-gettext.
Tests (no database, no network; HTTP goes through Guzzle's MockHandler):
- BasicFlowTest: array scope/response_type joining, fresh state per
request, state is single-use, callback without pending request,
missing/non-string state (no TypeError), $_GET fallback, error
description in the message, token response without access_token,
error wins over access_token, non-object JSON bodies rejected
(userinfo), userinfo HTTP error, Guzzle transport exception kept as
previous, client secret only in the POST body.
- UserModelTest: mergeFromInput only takes the username, shouldPing
first/stale/recent/custom-interval behaviour and its updateLastPing
persistence, anonymize strips email+display name and persists, Guest
never exposes an id or personal data. Uses Tests\Fakes\
RecordingUserMapper registered once in Neuron's MapperFactory.
- ModuleTest: login stores user id + access token in the session and
fires user:login, post-login-redirect is honoured once and cleared,
fallback to app root, logout clears the session and fires user:logout.
.phpunit.result.cache is now ignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o
@daedeloth
daedeloth merged commit d9815b5 into masterAug 27, 2026
5 checks passed
Sign up for freeto 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

@daedeloth
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add GitHub Actions CI and expand unit test coverage by daedeloth · Pull Request #2 · CatLabInteractive/OpenID-Connect-Client · GitHub
Skip to content

Add GitHub Actions CI and expand unit test coverage - #2

Merged
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests
Aug 27, 2026
Merged

Add GitHub Actions CI and expand unit test coverage#2
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests

Conversation

@daedeloth

Copy link
Copy Markdown
Member

Workflow

.github/workflows/tests.yml runs on push to master and on every pull request:

  • actions/checkout@v4
  • shivammathur/setup-php@v2 on a PHP 8.1 / 8.2 / 8.3 / 8.4 / 8.5 matrix (fail-fast: false), extensions: gettext, mbstring, curl (gettext is a hard requirement of catlabinteractive/neuron), no coverage
  • composer install --prefer-dist --no-progress --no-interaction
  • vendor/bin/phpunit

composer.lock is git-ignored, so every job resolves fresh; all dependencies (neuron >=7.4, guzzle ^7, phpunit ^9.6) accept the full range, so no version was dropped from the matrix. Locally the suite is green on PHP 8.5.4 with error_reporting=E_ALL and display_errors=1 (no notices/deprecations) against a fresh composer install (neuron v3.1.6).

Tests added (9 → 39 tests)

All run without a database or network; HTTP goes through Guzzle's MockHandler, following the existing BasicFlowTest style. Each new test was checked against a deliberate breakage of the behaviour it pins (removing the state unset, the is_string guard, the updateLastPing call, the post-login-redirect clear, letting mergeFromInput copy the email) and fails as expected.

tests/BasicFlowTest.php (extended)

  • array scope / response_type are space-joined
  • every authorization request gets a fresh 32-hex-char state; only the latest is accepted
  • state is single-use: a replayed callback is rejected and the session key is gone
  • callback with no pending request, missing state, or a non-string (array) state is rejected with OpenIDConnectException (no TypeError from hash_equals)
  • getAuthorizationCode() falls back to $_GET
  • error_description ends up in the exception message
  • token response without access_token → exception; error wins even when access_token is present
  • non-object bodies (HTML, empty, null, string, number) are rejected as invalid JSON (userinfo)
  • userinfo HTTP 401 → OpenIDConnectException
  • transport failures (ConnectException) become OpenIDConnectException with the Guzzle exception as getPrevious()
  • the client secret travels only in the POST body (no Authorization header, not in the URL)

tests/UserModelTest.php (new, uses tests/Fakes/RecordingUserMapper.php)

  • mergeFromInput() only takes username; email/sub from the claims payload are ignored; display name kept when username absent
  • shouldPing(): true when never pinged / when older than pingInterval (and stamps lastPing, calls updateLastPing), false within the interval (nothing persisted), pingInterval is per-instance
  • anonymize() nulls email + display name, keeps id/sub, persists via update()
  • Guest never exposes an id or personal data even if setters are called

tests/ModuleTest.php (new)

  • login() stores catlab-user-id + catlab-openid-access-token in the session, fires user:login, returns a 302
  • post-login-redirect is honoured once and then cleared (with cancel-login-redirect); fallback is the app root
  • logout() clears both session keys, fires user:logout, redirects to /

The fake mapper is registered once in Neuron's MapperFactory (a process-wide singleton without a reset), which is why it lives in tests/Fakes/ and is shared.

Deliberately not covered

  • Mappers\UserMapper (all methods build Neuron\DB\Query objects and execute them) and Module::setRequestUser() / setUserMapper() — need MySQL or a real mapper in the singleton.
  • Controllers\LoginController (login, next, cookieGate, processLogin, touchUser) — depends on Neuron\Config, the DB-backed mapper, setcookie() and Application::getInstance(); would need an integration harness.
  • Helpers\LoginForm — needs a running Neuron\Application router and template paths.
  • User::ping() / activity() / createAuthenticatedRequest() — owned by 4.1: ping/activity send the access token in the Authorization header #1 (fix/bearer-header-tokens, tests/UserTokenTransportTest.php); nothing here touches those methods or that file, so the two PRs merge cleanly.
  • The client_secret form param when authentication_info is absent is sent as an empty value; existing behaviour, left as is rather than pinned.

No changes to src/. .phpunit.result.cache is now git-ignored.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o

CI: .github/workflows/tests.yml runs vendor/bin/phpunit on push to master
and on pull requests, on a PHP 8.1-8.5 matrix (composer.lock is not
tracked, so each job resolves fresh; every dependency accepts the whole
range). gettext is enabled explicitly because catlabinteractive/neuron
requires ext-gettext.
Tests (no database, no network; HTTP goes through Guzzle's MockHandler):
- BasicFlowTest: array scope/response_type joining, fresh state per
request, state is single-use, callback without pending request,
missing/non-string state (no TypeError), $_GET fallback, error
description in the message, token response without access_token,
error wins over access_token, non-object JSON bodies rejected
(userinfo), userinfo HTTP error, Guzzle transport exception kept as
previous, client secret only in the POST body.
- UserModelTest: mergeFromInput only takes the username, shouldPing
first/stale/recent/custom-interval behaviour and its updateLastPing
persistence, anonymize strips email+display name and persists, Guest
never exposes an id or personal data. Uses Tests\Fakes\
RecordingUserMapper registered once in Neuron's MapperFactory.
- ModuleTest: login stores user id + access token in the session and
fires user:login, post-login-redirect is honoured once and cleared,
fallback to app root, logout clears the session and fires user:logout.
.phpunit.result.cache is now ignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o
@daedeloth
daedeloth merged commit d9815b5 into masterAug 27, 2026
5 checks passed
Sign up for freeto 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

@daedeloth
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add GitHub Actions CI and expand unit test coverage by daedeloth · Pull Request #2 · CatLabInteractive/OpenID-Connect-Client · GitHub
Skip to content

Add GitHub Actions CI and expand unit test coverage - #2

Merged
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests
Aug 27, 2026
Merged

Add GitHub Actions CI and expand unit test coverage#2
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests

Conversation

@daedeloth

Copy link
Copy Markdown
Member

Workflow

.github/workflows/tests.yml runs on push to master and on every pull request:

  • actions/checkout@v4
  • shivammathur/setup-php@v2 on a PHP 8.1 / 8.2 / 8.3 / 8.4 / 8.5 matrix (fail-fast: false), extensions: gettext, mbstring, curl (gettext is a hard requirement of catlabinteractive/neuron), no coverage
  • composer install --prefer-dist --no-progress --no-interaction
  • vendor/bin/phpunit

composer.lock is git-ignored, so every job resolves fresh; all dependencies (neuron >=7.4, guzzle ^7, phpunit ^9.6) accept the full range, so no version was dropped from the matrix. Locally the suite is green on PHP 8.5.4 with error_reporting=E_ALL and display_errors=1 (no notices/deprecations) against a fresh composer install (neuron v3.1.6).

Tests added (9 → 39 tests)

All run without a database or network; HTTP goes through Guzzle's MockHandler, following the existing BasicFlowTest style. Each new test was checked against a deliberate breakage of the behaviour it pins (removing the state unset, the is_string guard, the updateLastPing call, the post-login-redirect clear, letting mergeFromInput copy the email) and fails as expected.

tests/BasicFlowTest.php (extended)

  • array scope / response_type are space-joined
  • every authorization request gets a fresh 32-hex-char state; only the latest is accepted
  • state is single-use: a replayed callback is rejected and the session key is gone
  • callback with no pending request, missing state, or a non-string (array) state is rejected with OpenIDConnectException (no TypeError from hash_equals)
  • getAuthorizationCode() falls back to $_GET
  • error_description ends up in the exception message
  • token response without access_token → exception; error wins even when access_token is present
  • non-object bodies (HTML, empty, null, string, number) are rejected as invalid JSON (userinfo)
  • userinfo HTTP 401 → OpenIDConnectException
  • transport failures (ConnectException) become OpenIDConnectException with the Guzzle exception as getPrevious()
  • the client secret travels only in the POST body (no Authorization header, not in the URL)

tests/UserModelTest.php (new, uses tests/Fakes/RecordingUserMapper.php)

  • mergeFromInput() only takes username; email/sub from the claims payload are ignored; display name kept when username absent
  • shouldPing(): true when never pinged / when older than pingInterval (and stamps lastPing, calls updateLastPing), false within the interval (nothing persisted), pingInterval is per-instance
  • anonymize() nulls email + display name, keeps id/sub, persists via update()
  • Guest never exposes an id or personal data even if setters are called

tests/ModuleTest.php (new)

  • login() stores catlab-user-id + catlab-openid-access-token in the session, fires user:login, returns a 302
  • post-login-redirect is honoured once and then cleared (with cancel-login-redirect); fallback is the app root
  • logout() clears both session keys, fires user:logout, redirects to /

The fake mapper is registered once in Neuron's MapperFactory (a process-wide singleton without a reset), which is why it lives in tests/Fakes/ and is shared.

Deliberately not covered

  • Mappers\UserMapper (all methods build Neuron\DB\Query objects and execute them) and Module::setRequestUser() / setUserMapper() — need MySQL or a real mapper in the singleton.
  • Controllers\LoginController (login, next, cookieGate, processLogin, touchUser) — depends on Neuron\Config, the DB-backed mapper, setcookie() and Application::getInstance(); would need an integration harness.
  • Helpers\LoginForm — needs a running Neuron\Application router and template paths.
  • User::ping() / activity() / createAuthenticatedRequest() — owned by 4.1: ping/activity send the access token in the Authorization header #1 (fix/bearer-header-tokens, tests/UserTokenTransportTest.php); nothing here touches those methods or that file, so the two PRs merge cleanly.
  • The client_secret form param when authentication_info is absent is sent as an empty value; existing behaviour, left as is rather than pinned.

No changes to src/. .phpunit.result.cache is now git-ignored.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o

CI: .github/workflows/tests.yml runs vendor/bin/phpunit on push to master
and on pull requests, on a PHP 8.1-8.5 matrix (composer.lock is not
tracked, so each job resolves fresh; every dependency accepts the whole
range). gettext is enabled explicitly because catlabinteractive/neuron
requires ext-gettext.
Tests (no database, no network; HTTP goes through Guzzle's MockHandler):
- BasicFlowTest: array scope/response_type joining, fresh state per
request, state is single-use, callback without pending request,
missing/non-string state (no TypeError), $_GET fallback, error
description in the message, token response without access_token,
error wins over access_token, non-object JSON bodies rejected
(userinfo), userinfo HTTP error, Guzzle transport exception kept as
previous, client secret only in the POST body.
- UserModelTest: mergeFromInput only takes the username, shouldPing
first/stale/recent/custom-interval behaviour and its updateLastPing
persistence, anonymize strips email+display name and persists, Guest
never exposes an id or personal data. Uses Tests\Fakes\
RecordingUserMapper registered once in Neuron's MapperFactory.
- ModuleTest: login stores user id + access token in the session and
fires user:login, post-login-redirect is honoured once and cleared,
fallback to app root, logout clears the session and fires user:logout.
.phpunit.result.cache is now ignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o
@daedeloth
daedeloth merged commit d9815b5 into masterAug 27, 2026
5 checks passed
Sign up for freeto 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

@daedeloth
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Add GitHub Actions CI and expand unit test coverage by daedeloth · Pull Request #2 · CatLabInteractive/OpenID-Connect-Client · GitHub
Skip to content

Add GitHub Actions CI and expand unit test coverage - #2

Merged
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests
Aug 27, 2026
Merged

Add GitHub Actions CI and expand unit test coverage#2
daedeloth merged 1 commit into
masterfrom
ci/github-actions-and-tests

Conversation

@daedeloth

Copy link
Copy Markdown
Member

Workflow

.github/workflows/tests.yml runs on push to master and on every pull request:

  • actions/checkout@v4
  • shivammathur/setup-php@v2 on a PHP 8.1 / 8.2 / 8.3 / 8.4 / 8.5 matrix (fail-fast: false), extensions: gettext, mbstring, curl (gettext is a hard requirement of catlabinteractive/neuron), no coverage
  • composer install --prefer-dist --no-progress --no-interaction
  • vendor/bin/phpunit

composer.lock is git-ignored, so every job resolves fresh; all dependencies (neuron >=7.4, guzzle ^7, phpunit ^9.6) accept the full range, so no version was dropped from the matrix. Locally the suite is green on PHP 8.5.4 with error_reporting=E_ALL and display_errors=1 (no notices/deprecations) against a fresh composer install (neuron v3.1.6).

Tests added (9 → 39 tests)

All run without a database or network; HTTP goes through Guzzle's MockHandler, following the existing BasicFlowTest style. Each new test was checked against a deliberate breakage of the behaviour it pins (removing the state unset, the is_string guard, the updateLastPing call, the post-login-redirect clear, letting mergeFromInput copy the email) and fails as expected.

tests/BasicFlowTest.php (extended)

  • array scope / response_type are space-joined
  • every authorization request gets a fresh 32-hex-char state; only the latest is accepted
  • state is single-use: a replayed callback is rejected and the session key is gone
  • callback with no pending request, missing state, or a non-string (array) state is rejected with OpenIDConnectException (no TypeError from hash_equals)
  • getAuthorizationCode() falls back to $_GET
  • error_description ends up in the exception message
  • token response without access_token → exception; error wins even when access_token is present
  • non-object bodies (HTML, empty, null, string, number) are rejected as invalid JSON (userinfo)
  • userinfo HTTP 401 → OpenIDConnectException
  • transport failures (ConnectException) become OpenIDConnectException with the Guzzle exception as getPrevious()
  • the client secret travels only in the POST body (no Authorization header, not in the URL)

tests/UserModelTest.php (new, uses tests/Fakes/RecordingUserMapper.php)

  • mergeFromInput() only takes username; email/sub from the claims payload are ignored; display name kept when username absent
  • shouldPing(): true when never pinged / when older than pingInterval (and stamps lastPing, calls updateLastPing), false within the interval (nothing persisted), pingInterval is per-instance
  • anonymize() nulls email + display name, keeps id/sub, persists via update()
  • Guest never exposes an id or personal data even if setters are called

tests/ModuleTest.php (new)

  • login() stores catlab-user-id + catlab-openid-access-token in the session, fires user:login, returns a 302
  • post-login-redirect is honoured once and then cleared (with cancel-login-redirect); fallback is the app root
  • logout() clears both session keys, fires user:logout, redirects to /

The fake mapper is registered once in Neuron's MapperFactory (a process-wide singleton without a reset), which is why it lives in tests/Fakes/ and is shared.

Deliberately not covered

  • Mappers\UserMapper (all methods build Neuron\DB\Query objects and execute them) and Module::setRequestUser() / setUserMapper() — need MySQL or a real mapper in the singleton.
  • Controllers\LoginController (login, next, cookieGate, processLogin, touchUser) — depends on Neuron\Config, the DB-backed mapper, setcookie() and Application::getInstance(); would need an integration harness.
  • Helpers\LoginForm — needs a running Neuron\Application router and template paths.
  • User::ping() / activity() / createAuthenticatedRequest() — owned by 4.1: ping/activity send the access token in the Authorization header #1 (fix/bearer-header-tokens, tests/UserTokenTransportTest.php); nothing here touches those methods or that file, so the two PRs merge cleanly.
  • The client_secret form param when authentication_info is absent is sent as an empty value; existing behaviour, left as is rather than pinned.

No changes to src/. .phpunit.result.cache is now git-ignored.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o

CI: .github/workflows/tests.yml runs vendor/bin/phpunit on push to master
and on pull requests, on a PHP 8.1-8.5 matrix (composer.lock is not
tracked, so each job resolves fresh; every dependency accepts the whole
range). gettext is enabled explicitly because catlabinteractive/neuron
requires ext-gettext.
Tests (no database, no network; HTTP goes through Guzzle's MockHandler):
- BasicFlowTest: array scope/response_type joining, fresh state per
request, state is single-use, callback without pending request,
missing/non-string state (no TypeError), $_GET fallback, error
description in the message, token response without access_token,
error wins over access_token, non-object JSON bodies rejected
(userinfo), userinfo HTTP error, Guzzle transport exception kept as
previous, client secret only in the POST body.
- UserModelTest: mergeFromInput only takes the username, shouldPing
first/stale/recent/custom-interval behaviour and its updateLastPing
persistence, anonymize strips email+display name and persists, Guest
never exposes an id or personal data. Uses Tests\Fakes\
RecordingUserMapper registered once in Neuron's MapperFactory.
- ModuleTest: login stores user id + access token in the session and
fires user:login, post-login-redirect is honoured once and cleared,
fallback to app root, logout clears the session and fires user:logout.
.phpunit.result.cache is now ignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qz9QvEe8uJ65wx8aVKwk7o
@daedeloth
daedeloth merged commit d9815b5 into masterAug 27, 2026
5 checks passed
Sign up for freeto 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

@daedeloth