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
1 change: 1 addition & 0 deletions src/hooks.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ consumeAuthCookies();
initCoreProvider({
getAccessToken: async () => getAuthUser().accessToken ?? '',
getIdToken: async () => getAuthUser().idToken,
getRoutePrefix: () => '',
api: {
preRequest: ossPreRequest,
postResponse: ossPostResponse,
Expand Down
8 changes: 8 additions & 0 deletions src/lib/utilities/core-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type PostResponseHook = (
export type CoreProvider = {
getAccessToken: () => Promise<string>;
getIdToken: () => Promise<string | undefined>;
getRoutePrefix: () => string;
api: {
preRequest: PreRequestHook;
postResponse: PostResponseHook;
Expand All @@ -28,6 +29,7 @@ export type CoreProvider = {
export type InitOptions = {
getAccessToken: () => Promise<string>;
getIdToken?: () => Promise<string | undefined>;
getRoutePrefix?: () => string;
api?: {
preRequest?: PreRequestHook;
postResponse?: PostResponseHook;
Expand All @@ -45,6 +47,7 @@ export function initCoreProvider(options: InitOptions): void {
provider = {
getAccessToken: options.getAccessToken,
getIdToken: options.getIdToken ?? (async () => undefined),
getRoutePrefix: options.getRoutePrefix ?? (() => ''),
api: {
preRequest: options.api?.preRequest ?? passthrough,
postResponse: options.api?.postResponse ?? passthroughResponse,
Expand All @@ -64,6 +67,11 @@ export async function getIdToken(): Promise<string | undefined> {
return provider.getIdToken();
}

export function getRoutePrefix(): string {
if (!BROWSER || !provider) return '';
return provider.getRoutePrefix();
}

export async function getDataEncoderEndpoint(
namespace: string,
): Promise<string> {
Expand Down
182 changes: 181 additions & 1 deletion src/lib/utilities/route-for-base-path.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it } from 'vitest';

import { base } from '$app/paths';

import { initCoreProvider } from './core-provider';
import * as routeForModule from './route-for';
import {
routeForArchivalEventHistory,
Expand Down Expand Up @@ -257,3 +258,182 @@ describe('routeFor functions should resolve the base path exactly once', () => {
}
});
});

describe('routeFor functions with prefix should resolve base + prefix correctly', () => {
const prefix = '/projects/my-project';

const namespaceParams = { namespace: 'default' };
const workflowParams = {
namespace: 'default',
workflow: 'wf-id',
run: 'run-id',
};
const scheduleParams = { namespace: 'default', scheduleId: 'sched-1' };
const activityParams = {
namespace: 'default',
activityId: 'act-1',
runId: 'run-1',
};

afterEach(() => {
initCoreProvider({
getAccessToken: async () => '',
getRoutePrefix: () => '',
});
});

const prefixedCases: [string, () => string | undefined][] = [
['routeForNamespaces', () => routeForNamespaces()],
['routeForNexus', () => routeForNexus()],
['routeForNexusEndpoint', () => routeForNexusEndpoint('ep-1')],
['routeForNexusEndpointEdit', () => routeForNexusEndpointEdit('ep-1')],
['routeForNexusEndpointCreate', () => routeForNexusEndpointCreate()],
['routeForNamespace', () => routeForNamespace(namespaceParams)],
['routeForNamespaceSelector', () => routeForNamespaceSelector()],
['routeForWorkflows', () => routeForWorkflows(namespaceParams)],
[
'routeForArchivalWorkflows',
() => routeForArchivalWorkflows(namespaceParams),
],
['routeForWorkflow', () => routeForWorkflow(workflowParams)],
['routeForSchedules', () => routeForSchedules(namespaceParams)],
['routeForScheduleCreate', () => routeForScheduleCreate(namespaceParams)],
['routeForSchedule', () => routeForSchedule(scheduleParams)],
['routeForScheduleEdit', () => routeForScheduleEdit(scheduleParams)],
[
'routeForArchivalEventHistory',
() => routeForArchivalEventHistory(workflowParams),
],
[
'routeForEventHistoryEvent',
() => routeForEventHistoryEvent({ ...workflowParams, eventId: '1' }),
],
['routeForEventHistory', () => routeForEventHistory(workflowParams)],
['routeForTimeline', () => routeForTimeline(workflowParams)],
['routeForWorkers', () => routeForWorkers(workflowParams)],
[
'routeForWorkerDeployments',
() => routeForWorkerDeployments(namespaceParams),
],
[
'routeForWorkerDeployment',
() =>
routeForWorkerDeployment({
namespace: 'default',
deployment: 'dep-1',
}),
],
[
'routeForWorkerDeploymentVersion',
() =>
routeForWorkerDeploymentVersion({
namespace: 'default',
deployment: 'dep-1',
version: 'v1',
}),
],
['routeForRelationships', () => routeForRelationships(workflowParams)],
[
'routeForTaskQueue',
() => routeForTaskQueue({ namespace: 'default', queue: 'q-1' }),
],
['routeForCallStack', () => routeForCallStack(workflowParams)],
['routeForWorkflowQuery', () => routeForWorkflowQuery(workflowParams)],
['routeForUserMetadata', () => routeForUserMetadata(workflowParams)],
[
'routeForWorkflowSearchAttributes',
() => routeForWorkflowSearchAttributes(workflowParams),
],
['routeForWorkflowMemo', () => routeForWorkflowMemo(workflowParams)],
['routeForWorkflowUpdate', () => routeForWorkflowUpdate(workflowParams)],
[
'routeForPendingActivities',
() => routeForPendingActivities(workflowParams),
],
['routeForNexusLinks', () => routeForNexusLinks(workflowParams)],
['routeForEventHistoryImport', () => routeForEventHistoryImport()],
['routeForBatchOperations', () => routeForBatchOperations(namespaceParams)],
[
'routeForBatchOperation',
() => routeForBatchOperation({ namespace: 'default', jobId: 'job-1' }),
],
[
'routeForStandaloneActivities',
() => routeForStandaloneActivities(namespaceParams),
],
[
'routeForStandaloneActivitiesWithQuery',
() =>
routeForStandaloneActivitiesWithQuery(namespaceParams, 'test-query'),
],
[
'routeForStartStandaloneActivity',
() => routeForStartStandaloneActivity(namespaceParams),
],
[
'routeForStandaloneActivityDetails',
() => routeForStandaloneActivityDetails(activityParams),
],
[
'routeForStandaloneActivityWorkers',
() => routeForStandaloneActivityWorkers(activityParams),
],
[
'routeForStandaloneActivitySearchAttributes',
() => routeForStandaloneActivitySearchAttributes(activityParams),
],
[
'routeForStandaloneActivityMetadata',
() => routeForStandaloneActivityMetadata(activityParams),
],
[
'routeForWorkflowStart',
() => routeForWorkflowStart({ namespace: 'default' }),
],
[
'routeForWorkflowsWithQuery',
() => routeForWorkflowsWithQuery({ namespace: 'default', query: 'test' }),
],
];

const authCases: [string, () => string | undefined][] = [
[
'routeForAuthentication',
() =>
routeForAuthentication({
settings: { auth: {}, baseUrl: 'https://example.com' },
searchParams: new URLSearchParams(),
}),
],
['routeForLoginPage', () => routeForLoginPage('', false)],
];

it.each(prefixedCases)(
'%s should include base + prefix when prefix is set',
(_name, fn) => {
initCoreProvider({
getAccessToken: async () => '',
getRoutePrefix: () => prefix,
});
const result = fn();
expect(typeof result).toBe('string');
expect(result).toMatch(new RegExp(`^${base}${prefix}`));
expect(result).not.toMatch(
new RegExp(`${base}${prefix}${base}${prefix}`),
);
},
);

it.each(authCases)(
'%s should NOT include prefix (auth routes excluded)',
(_name, fn) => {
initCoreProvider({
getAccessToken: async () => '',
getRoutePrefix: () => prefix,
});
const result = fn();
expect(typeof result).toBe('string');
expect(result).not.toContain(prefix);
},
);
});
80 changes: 79 additions & 1 deletion src/lib/utilities/route-for.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { writable } from 'svelte/store';

import { afterEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { base } from '$app/paths';

Expand All @@ -9,6 +9,7 @@ import type {
WorkflowViewPreference,
} from '$lib/stores/event-view';

import { initCoreProvider } from './core-provider';
import {
baseRouteForWorkflow,
hasParameters,
Expand All @@ -24,6 +25,7 @@ import {
routeForLoginPage,
routeForNamespace,
routeForNamespaces,
routeForNexus,
routeForPendingActivities,
routeForSchedule,
routeForScheduleCreate,
Expand Down Expand Up @@ -561,3 +563,79 @@ describe('routeFor worker deployment version and serverless routes', () => {
expect(path).toBe(`${base}/namespaces/default/workers/deployments/create`);
});
});

describe('routeFor with prefix', () => {
const prefix = '/projects/my-project';

beforeEach(() => {
initCoreProvider({
getAccessToken: async () => '',
getRoutePrefix: () => prefix,
});
});

it('should prepend prefix to root route', () => {
expect(routeForNamespaces()).toBe(`${base}${prefix}/namespaces`);
});

it('should prepend prefix to namespace route', () => {
expect(routeForNamespace({ namespace: 'default' })).toBe(
`${base}${prefix}/namespaces/default`,
);
});

it('should propagate prefix through leaf functions', () => {
expect(routeForWorkflows({ namespace: 'default' })).toBe(
`${base}${prefix}/namespaces/default/workflows`,
);
});

it('should propagate prefix through deep leaf functions', () => {
expect(
routeForCallStack({
namespace: 'default',
workflow: 'abc',
run: 'def',
}),
).toBe(`${base}${prefix}/namespaces/default/workflows/abc/def/call-stack`);
});

it('should propagate prefix to nexus routes', () => {
expect(routeForNexus()).toBe(`${base}${prefix}/nexus`);
});

it('should propagate prefix to schedule routes', () => {
expect(routeForSchedules({ namespace: 'default' })).toBe(
`${base}${prefix}/namespaces/default/schedules`,
);
});

it('should not apply prefix when store is empty', () => {
initCoreProvider({
getAccessToken: async () => '',
getRoutePrefix: () => '',
});
expect(routeForNamespaces()).toBe(`${base}/namespaces`);
});

it('should not apply prefix to auth routes', () => {
const settings = { auth: {}, baseUrl: 'https://localhost' };
const searchParams = new URLSearchParams();
const sso = routeForAuthentication({ settings, searchParams });
expect(sso).not.toContain(prefix);
});

it('should not apply prefix to login page', () => {
const login = routeForLoginPage('', false);
expect(login).not.toContain(prefix);
});

it('should revert to default behavior when prefix is cleared', () => {
expect(routeForNamespaces()).toBe(`${base}${prefix}/namespaces`);
initCoreProvider({
getAccessToken: async () => '',
getRoutePrefix: () => '',
});
expect(routeForNamespaces()).toBe(`${base}/namespaces`);
});
});
Loading
Loading