Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions integrationTests/fixtures/path-segment-routing-app/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Integration test fixture: verifies the OAuth Resource correctly extracts
# the providerName from the URL path segment, NOT the literal "oauth" prefix.
#
# Two providers are configured with distinctive client_ids. A request to
# /oauth/<name>/login should redirect with that provider's client_id. If
# parseRoute were to extract the literal "oauth" path segment as providerName
# (the symptom Dawson reported on 2026-05-22 from CM/Studio), the request
# would NOT produce a tenant-shaped 302 — it would either route to the
# oauth-named decoy provider with action='oac-oauth-tenant' (an unknown
# action → 404) or route to some other unintended state. The exact failure
# shape depends on parseRoute's specific regression; the assertion only
# requires that the tenant.test authorizationUrl + tenant client_id
# appear in the redirect.
#
# The tenant provider name deliberately contains the substring "oauth" so
# this fixture also guards against an over-aggressive stripping regression
# (e.g., parseRoute mangling /oauth/ matches that occur inside the segment
# rather than only at the prefix).

rest: true

'@harperfast/oauth':
package: '@harperfast/oauth'
providers:
# Provider literally named "oauth" — present specifically as a decoy.
# If parseRoute is broken and treats "oauth" as the providerName, this
# is what would (incorrectly) be selected as the provider.
oauth:
provider: generic
clientId: ${OAUTH_DECOY_CLIENT_ID}
clientSecret: decoy-secret
authorizationUrl: 'http://decoy.test/authorize'
tokenUrl: 'http://decoy.test/token'
userInfoUrl: 'http://decoy.test/userinfo'
# Tenant-style provider with a distinctive client_id we can assert on.
# The name contains "oauth" as a substring to catch over-aggressive
# path-stripping regressions in addition to the prefix-extraction case.
oac-oauth-tenant:
provider: generic
clientId: ${OAUTH_TENANT_CLIENT_ID}
clientSecret: tenant-secret
authorizationUrl: 'http://tenant.test/authorize'
tokenUrl: 'http://tenant.test/token'
userInfoUrl: 'http://tenant.test/userinfo'
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "path-segment-routing-app-fixture",
"private": true,
"type": "module",
"description": "Integration-test fixture for OAuth Resource path-segment routing. The @harperfast/oauth dep is installed at test setup time via scripts/install-fixtures.js (npm pack + install); harper is resolved from the repo root."
}
75 changes: 75 additions & 0 deletions integrationTests/path-segment-routing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Regression guard for the symptom Dawson reported on 2026-05-22 (CM/Studio
* Okta SSO):
*
* "providerName is 'oauth', not the {configId} described, that isn't passed"
*
* If parseRoute extracted the literal "oauth" path prefix as providerName
* (rather than the segment after it), requests to /oauth/<tenantId>/login
* would resolve to whatever provider is registered under the name "oauth" —
* with the rest of the URL becoming the action — and would NOT produce the
* expected tenant-shaped 302.
*
* This test configures two providers — one literally named "oauth" (a decoy)
* and one named "oac-oauth-tenant" (deliberately containing the substring
* "oauth" to also catch over-aggressive stripping) — with distinctive
* client_ids, then asserts that /oauth/oac-oauth-tenant/login redirects to
* the tenant provider's authorizationUrl with the tenant's client_id. Any
* regression that breaks path-segment routing will fail this assertion.
*/
import { suite, test, before, after } from 'node:test';
import { strictEqual } from 'node:assert/strict';
import { join, dirname } from 'node:path';
import { createRequire } from 'node:module';
import { setupHarperWithFixture, teardownHarper, type ContextWithHarper } from '@harperfast/integration-testing';

const require = createRequire(import.meta.url);

function getHarperBinPath(): string {
return join(dirname(require.resolve('harper')), 'bin', 'harper.js');
}

const fixturePath = join(import.meta.dirname, 'fixtures', 'path-segment-routing-app');

const TENANT_CLIENT_ID = 'oauth-tenant-client-id';
const DECOY_CLIENT_ID = 'decoy-oauth-client-id';

suite('OAuth Resource routes by URL path segment (not literal "oauth")', (ctx: ContextWithHarper) => {
before(async () => {
await setupHarperWithFixture(ctx, fixturePath, {
harperBinPath: getHarperBinPath(),
env: {
OAUTH_TENANT_CLIENT_ID: TENANT_CLIENT_ID,
OAUTH_DECOY_CLIENT_ID: DECOY_CLIENT_ID,
},
config: { logging: { stdStreams: true } },
});
});

after(async () => {
await teardownHarper(ctx);
});

test('/oauth/oac-oauth-tenant/login dispatches to the tenant provider', async () => {
const response = await fetch(`${ctx.harper.httpURL}/oauth/oac-oauth-tenant/login`, {
redirect: 'manual',
});

strictEqual(response.status, 302, `expected 302 redirect, got ${response.status}`);
const location = response.headers.get('location');
strictEqual(typeof location, 'string', 'Location header missing');
const url = new URL(location!);

// The tenant provider's authorizationUrl is http://tenant.test/authorize;
// the decoy's is http://decoy.test/authorize. A parseRoute that returned
// "oauth" as providerName would either 404 (action mismatch) or pull
// the decoy's client_id — both fail these assertions.
strictEqual(url.origin, 'http://tenant.test');
strictEqual(url.pathname, '/authorize');
strictEqual(
url.searchParams.get('client_id'),
TENANT_CLIENT_ID,
`client_id was ${url.searchParams.get('client_id')} — expected tenant's (${TENANT_CLIENT_ID}), decoy's is ${DECOY_CLIENT_ID}`
);
});
});
Loading