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
2 changes: 0 additions & 2 deletions .github/workflows/dashboard-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ on:
paths:
- dashboard/**
pull_request:
branches:
- main
paths:
- dashboard/**
workflow_dispatch:
Expand Down
3 changes: 3 additions & 0 deletions dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@
"@rushstack/eslint-patch": "^1.3.2",
"@types/file-saver": "^2.0.7",
"@types/is-url": "1.2.30",
"@types/jest": "27.5.2",
"@types/node": "~20.10.0",
"@types/vue": "^1.0.31",
"@types/webpack-env": "^1.18.5",
"@vue/eslint-config-standard": "5.1.2",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/test-utils": "2.4.6",
Expand Down
3 changes: 1 addition & 2 deletions dashboard/pkg/epinio/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@
"../../node_modules/@rancher/shell/types"
],
"types": [
"node",
"webpack-env",
"@types/webpack-env",
"@types/node",
"@types/jest",
"@types/lodash",
Expand Down
114 changes: 114 additions & 0 deletions dashboard/pkg/epinio/utils/application/application.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { AppUtils } from './application';
import { APPLICATION_MANIFEST_SOURCE_TYPE, APPLICATION_SOURCE_TYPE } from '../../types';

const { getSourceType, getManifestSourceType, getGitData, sourceFingerprint } = AppUtils;

describe('getSourceType', () => {
it.each([
[{ Kind: APPLICATION_MANIFEST_SOURCE_TYPE.PATH }, APPLICATION_SOURCE_TYPE.FOLDER],
[{ Kind: APPLICATION_MANIFEST_SOURCE_TYPE.PATH, archive: true }, APPLICATION_SOURCE_TYPE.ARCHIVE],
[{ Kind: APPLICATION_MANIFEST_SOURCE_TYPE.GIT }, APPLICATION_SOURCE_TYPE.GIT_URL],
[{ Kind: APPLICATION_MANIFEST_SOURCE_TYPE.GIT, git: { provider: 'github' } }, APPLICATION_SOURCE_TYPE.GIT_HUB],
[{ Kind: APPLICATION_MANIFEST_SOURCE_TYPE.CONTAINER }, APPLICATION_SOURCE_TYPE.CONTAINER_URL],
[{ Kind: APPLICATION_MANIFEST_SOURCE_TYPE.NONE }, APPLICATION_SOURCE_TYPE.FOLDER],
[{}, APPLICATION_SOURCE_TYPE.FOLDER],
])('maps %p to %s', (origin, expected) => {
expect(getSourceType(origin as any)).toBe(expected);
});
});

describe('getManifestSourceType', () => {
it.each([
[{ git: { provider: 'gitlab' } }, APPLICATION_SOURCE_TYPE.GIT_LAB],
[{ git: {} }, APPLICATION_SOURCE_TYPE.GIT_URL],
[{ archive: true }, APPLICATION_SOURCE_TYPE.ARCHIVE],
[{ container: 'nginx' }, APPLICATION_SOURCE_TYPE.CONTAINER_URL],
[{}, APPLICATION_SOURCE_TYPE.FOLDER],
])('maps %p to %s', (origin, expected) => {
expect(getManifestSourceType(origin as any)).toBe(expected);
});
});

describe('getGitData', () => {
it('splits org and repo out of the url', () => {
expect(getGitData({
repository: 'https://github.com/epinio/epinio',
branch: 'main',
revision: 'abc123',
gitconfig: 'gh',
})).toEqual({
usernameOrOrg: 'epinio',
branch: { name: 'main' },
commit: 'abc123',
repo: { name: 'epinio' },
gitconfig: 'gh',
url: 'https://github.com/epinio/epinio',
});
});

it('falls back to url when repository is missing', () => {
expect(getGitData({ url: 'https://gitlab.com/org/repo' }).repo).toEqual({ name: 'repo' });
});
});

describe('sourceFingerprint', () => {
const git = (url: string, branch: any, extra = {}) => ({
type: APPLICATION_SOURCE_TYPE.GIT_HUB,
git: { url, branch, commit: 'abc', ...extra },
}) as any;

it('handles a missing source', () => {
expect(sourceFingerprint(null)).toBe(JSON.stringify({ type: '' }));
expect(sourceFingerprint()).toBe(JSON.stringify({ type: '' }));
});

it('ignores trailing slashes and .git in git urls', () => {
expect(sourceFingerprint(git('https://github.com/o/r.git', 'main')))
.toBe(sourceFingerprint(git('https://github.com/o/r/', 'main')));
});

it('treats a branch object and branch name as the same', () => {
expect(sourceFingerprint(git('https://github.com/o/r', { name: 'main', sha: 'x' })))
.toBe(sourceFingerprint(git('https://github.com/o/r', 'main')));
});

it('ignores fields from other source types', () => {
const base = git('https://github.com/o/r', 'main');

expect(sourceFingerprint({ ...base, container: { url: 'nginx' } }))
.toBe(sourceFingerprint(base));
});

it('changes when the commit changes', () => {
expect(sourceFingerprint(git('https://github.com/o/r', 'main', { commit: 'def' })))
.not.toBe(sourceFingerprint(git('https://github.com/o/r', 'main')));
});

it('compares git url sources by url, branch, and gitconfig', () => {
const src = (url: string, branch: string) => ({
type: APPLICATION_SOURCE_TYPE.GIT_URL,
gitUrl: { url, branch },
}) as any;

expect(sourceFingerprint(src('https://x/o/r.git', 'main'))).toBe(sourceFingerprint(src('https://x/o/r', 'main')));
expect(sourceFingerprint(src('https://x/o/r', 'main'))).not.toBe(sourceFingerprint(src('https://x/o/r', 'dev')));
});

it('only tracks whether an archive or folder was picked', () => {
const src = (type: string, tarball?: any) => ({ type, archive: { tarball } }) as any;

expect(sourceFingerprint(src(APPLICATION_SOURCE_TYPE.ARCHIVE, 'a.tgz')))
.toBe(sourceFingerprint(src(APPLICATION_SOURCE_TYPE.ARCHIVE, 'b.tgz')));
expect(sourceFingerprint(src(APPLICATION_SOURCE_TYPE.FOLDER, 'a')))
.not.toBe(sourceFingerprint(src(APPLICATION_SOURCE_TYPE.FOLDER)));
});

it('only tracks the image for container sources', () => {
const src = (url: string, builderImage?: string) => ({
type: APPLICATION_SOURCE_TYPE.CONTAINER_URL, container: { url }, builderImage
}) as any;

expect(sourceFingerprint(src('nginx', 'a'))).toBe(sourceFingerprint(src('nginx', 'b')));
expect(sourceFingerprint(src('nginx'))).not.toBe(sourceFingerprint(src('redis')));
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { APPLICATION_MANIFEST_SOURCE_TYPE, APPLICATION_SOURCE_TYPE, EpinioApplicationResource, EpinioAppSource, EPINIO_APP_GIT_SOURCE } from '../types';
import { parse as parseUrl } from '@shell/utils/url';
import { AppOrigin } from '../models/application/ui-types';
import { APPLICATION_MANIFEST_SOURCE_TYPE, APPLICATION_SOURCE_TYPE, EpinioApplicationResource, EpinioAppSource, EPINIO_APP_GIT_SOURCE } from '../../types';
import { parse as parseUrl } from '../url';
import { AppOrigin } from '../../models/application/ui-types';

interface Utils {
getSourceType: (origin: AppOrigin) => APPLICATION_SOURCE_TYPE;
Expand Down
1 change: 1 addition & 0 deletions dashboard/pkg/epinio/utils/application/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './application';
26 changes: 26 additions & 0 deletions dashboard/pkg/epinio/utils/custom-routing/custom-routing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { rootEpinioRoute, createEpinioRoute } from './custom-routing';

describe('rootEpinioRoute', () => {
it('points at the epinio product', () => {
expect(rootEpinioRoute()).toEqual({ name: 'epinio', params: { product: 'epinio' } });
});
});

describe('createEpinioRoute', () => {
it('prefixes the name and merges params with the product', () => {
expect(createEpinioRoute('c-cluster-applications', { cluster: 'local' })).toEqual({
name: 'epinio-c-cluster-applications',
params: { product: 'epinio', cluster: 'local' },
});
});

it('spreads extra fields onto the route', () => {
const route = createEpinioRoute('c-cluster', { cluster: 'local' }, { query: { tab: 'logs' } });

expect(route.query).toEqual({ tab: 'logs' });
});

it('lets params override the product', () => {
expect(createEpinioRoute('x', { product: 'other' }).params.product).toBe('other');
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EPINIO_PRODUCT_NAME } from '../types';
import { EPINIO_PRODUCT_NAME } from '../../types';

type Dictionary<T> = { [key: string]: T }

Expand Down
1 change: 1 addition & 0 deletions dashboard/pkg/epinio/utils/custom-routing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './custom-routing';
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { dashboardUrl } from './embedded-helpers';

describe('dashboardUrl', () => {
const original = process.env.dev;

afterEach(() => {
if (original === undefined) {
delete process.env.dev;
} else {
process.env.dev = original;
}
});

it('appends /dashboard in production', () => {
delete process.env.dev;

expect(dashboardUrl()).toBe(`${ window.origin }/dashboard`);
});

it('returns the bare origin in dev', () => {
process.env.dev = 'true';

expect(dashboardUrl()).toBe(window.origin);
});
});
1 change: 1 addition & 0 deletions dashboard/pkg/epinio/utils/embedded-helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './embedded-helpers';
1 change: 1 addition & 0 deletions dashboard/pkg/epinio/utils/namespaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './namespaces';
121 changes: 121 additions & 0 deletions dashboard/pkg/epinio/utils/namespaces/namespaces.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { useNamespaces } from './namespaces';

const ns = (name: string) => ({ meta: { name } });

const makeStore = (data: any[] = [], active: Record<string, boolean> = {}) => ({
state: { activeNamespaceCache: active },
dispatch: jest.fn((action: string, payload: any) => {
if (action === 'epinio/request') {
return Promise.resolve({ data });
}

// epinio/create: hand the payload back as the "model"
return Promise.resolve(payload);
}),
});

describe('useNamespaces', () => {
it('fetches, sorts, and maps namespaces to options', async() => {
const store = makeStore([ns('zeta'), ns('alpha')]);
const { fetchAll, options, firstName } = useNamespaces(store);

await fetchAll();

expect(options.value).toEqual([
{ label: 'alpha', value: 'alpha' },
{ label: 'zeta', value: 'zeta' },
]);
expect(firstName.value).toBe('alpha');
});

it('caches fetchAll results', async() => {
const store = makeStore([ns('a')]);
const { fetchAll } = useNamespaces(store);

await fetchAll();
await fetchAll();

const requests = store.dispatch.mock.calls.filter(([action]) => action === 'epinio/request');

expect(requests).toHaveLength(1);
});

it('reports fetch errors through onError', async() => {
const store = makeStore();
const onError = jest.fn();

store.dispatch.mockRejectedValueOnce(new Error('boom'));

const { fetchAll, isLoading } = useNamespaces(store, { onError });

await fetchAll();

expect(onError).toHaveBeenCalledWith(expect.any(Error));
expect(isLoading.value).toBe(false);
});

it('url-encodes search queries', async() => {
const store = makeStore([ns('a b')]);
const { search, namespaces } = useNamespaces(store);

await search('a b');

expect(store.dispatch.mock.calls[0][1].opt.url).toBe('/api/v1/namespaces?search=a%20b');
expect(namespaces.value).toHaveLength(1);
});

it('clears results when search fails', async() => {
const store = makeStore([ns('a')]);
const { fetchAll, search, namespaces } = useNamespaces(store);

await fetchAll();
store.dispatch.mockRejectedValueOnce(new Error('boom'));
await search('x');

expect(namespaces.value).toEqual([]);
});

it('seeds a single namespace and drops the cache', async() => {
const store = makeStore([ns('a')]);
const { fetchAll, seed, firstName } = useNamespaces(store);

await fetchAll();
seed('picked');
expect(firstName.value).toBe('picked');

seed('');
expect(firstName.value).toBe('');

await fetchAll();
const requests = store.dispatch.mock.calls.filter(([action]) => action === 'epinio/request');

expect(requests).toHaveLength(2);
});

it('scopes to the active namespace filter when asked', async() => {
const store = makeStore([ns('a'), ns('b')], { b: true });
const { fetchAll, options } = useNamespaces(store, { scopeToActiveFilter: true });

await fetchAll();

expect(options.value.map((o) => o.value)).toEqual(['b']);
});

it('ignores the active filter by default', async() => {
const store = makeStore([ns('a'), ns('b')], { b: true });
const { fetchAll, options } = useNamespaces(store);

await fetchAll();

expect(options.value).toHaveLength(2);
});

it('shows everything when the active filter is empty', async() => {
const store = makeStore([ns('a'), ns('b')], {});
const { fetchAll, options } = useNamespaces(store, { scopeToActiveFilter: true });

await fetchAll();

expect(options.value).toHaveLength(2);
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { computed, ref } from 'vue';
import sortBy from 'lodash/sortBy';

import { EPINIO_TYPES } from '../types';
import { EPINIO_TYPES } from '../../types';

interface UseNamespacesOptions {
// Limit the default list to the navbar's namespace filter. Never applied to
Expand Down
1 change: 1 addition & 0 deletions dashboard/pkg/epinio/utils/permissions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './permissions';
Loading
Loading