Skip to content

Commit 87631ad

Browse files
authored
Merge pull request #264 from trdoyle81/GITOPS-10535-console-plugin-core-data-test
GITOPS-10535: add unit tests for core string and URL utilities
2 parents 89bc2b8 + 5247fe0 commit 87631ad

6 files changed

Lines changed: 155 additions & 20 deletions

File tree

__mocks__/patternfly-react-core.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,27 @@ export const Popover: React.FC<any> = ({ headerContent, bodyContent, children })
1212
</div>
1313
);
1414

15-
export const MenuToggle = React.forwardRef<any, any>(({ children, variant, ...rest }, ref) => (
16-
<button ref={ref} data-variant={variant} {...rest}>{children}</button>
17-
));
15+
export const MenuToggle = React.forwardRef<any, any>(
16+
({ children, variant, isExpanded, ...rest }, ref) => (
17+
<button ref={ref} data-variant={variant} data-expanded={isExpanded} {...rest}>
18+
{children}
19+
</button>
20+
),
21+
);
1822
MenuToggle.displayName = 'MenuToggle';
1923

2024
export type MenuToggleElement = HTMLButtonElement;
2125
export type MenuToggleProps = any;
2226

23-
export const Dropdown: React.FC<any> = ({ children, isOpen, toggle, ...props }) => (
27+
export const Dropdown: React.FC<any> = ({
28+
children,
29+
isOpen,
30+
toggle,
31+
//patternfly-only props — keep off the dom to avoid react warnings in tests
32+
popperProps: _popperProps,
33+
onOpenChange: _onOpenChange,
34+
...props
35+
}) => (
2436
<div data-testid="dropdown" data-open={isOpen} {...props}>
2537
{typeof toggle === 'function' ? toggle(null) : toggle}
2638
{isOpen && children}
@@ -30,7 +42,14 @@ export const Dropdown: React.FC<any> = ({ children, isOpen, toggle, ...props })
3042
export const DropdownList: React.FC<any> = ({ children }) => <ul>{children}</ul>;
3143

3244
export const DropdownItem: React.FC<any> = ({ children, description, isDisabled, ...props }) => (
33-
<li data-disabled={isDisabled} {...props}>{children}{description && <small>{description}</small>}</li>
45+
<li data-disabled={isDisabled} {...props}>
46+
{children}
47+
{description && <small>{description}</small>}
48+
</li>
49+
);
50+
51+
export const Divider: React.FC<any> = ({ component: Component = 'hr', ...props }) => (
52+
<Component data-testid="divider" {...props} />
3453
);
3554

3655
export const Tooltip: React.FC<any> = ({ content, children }) => (

src/gitops/utils/components/ActionDropDown/ActionDropDown.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('ActionsDropdown', () => {
1515
/>,
1616
),
1717
).toMatchInlineSnapshot(
18-
`"<div data-testid="dropdown" data-open="false" popperProps="[object Object]"><button>Actions</button></div>"`,
18+
`"<div data-testid="dropdown" data-open="false"><button data-expanded="false">Actions</button></div>"`,
1919
);
2020
});
2121

@@ -28,13 +28,13 @@ describe('ActionsDropdown', () => {
2828
/>,
2929
),
3030
).toMatchInlineSnapshot(
31-
`"<div data-testid="dropdown" data-open="false" popperProps="[object Object]"><button data-variant="plain"><svg data-icon="EllipsisVIcon"></svg></button></div>"`,
31+
`"<div data-testid="dropdown" data-open="false"><button data-variant="plain" data-expanded="false"><svg data-icon="EllipsisVIcon"></svg></button></div>"`,
3232
);
3333
});
3434

3535
it('renders with no actions', () => {
3636
expect(renderToStaticMarkup(<ActionsDropdown actions={[]} />)).toMatchInlineSnapshot(
37-
`"<div data-testid="dropdown" data-open="false" popperProps="[object Object]"><button>Actions</button></div>"`,
37+
`"<div data-testid="dropdown" data-open="false"><button data-expanded="false">Actions</button></div>"`,
3838
);
3939
});
4040
});
Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,74 @@
11
import { detectGitType, gitUrlRegex } from './stringHelpers';
22

33
describe('gitUrlRegex', () => {
4-
it('matches valid git URLs', () => {
4+
it('matches valid https and git URLs', () => {
55
expect(gitUrlRegex.test('https://github.com/foo/bar')).toMatchInlineSnapshot(`true`);
66
expect(gitUrlRegex.test('https://github.com/foo/bar.git')).toMatchInlineSnapshot(`true`);
7+
expect(gitUrlRegex.test('https://www.github.com/foo/bar')).toMatchInlineSnapshot(`true`);
8+
expect(gitUrlRegex.test('http://github.com/foo/bar')).toMatchInlineSnapshot(`true`);
9+
expect(gitUrlRegex.test('git://github.com/foo/bar.git')).toMatchInlineSnapshot(`true`);
10+
expect(gitUrlRegex.test('https://gitlab.com/group/sub/project.git')).toMatchInlineSnapshot(
11+
`true`,
12+
);
13+
});
14+
15+
it('matches valid ssh and scp-style URLs', () => {
716
expect(gitUrlRegex.test('git@github.com:foo/bar.git')).toMatchInlineSnapshot(`true`);
817
expect(gitUrlRegex.test('ssh://git@github.com/foo/bar')).toMatchInlineSnapshot(`true`);
9-
expect(gitUrlRegex.test('not a url')).toMatchInlineSnapshot(`false`);
18+
expect(gitUrlRegex.test('ssh://git@gitlab.com/foo/bar.git')).toMatchInlineSnapshot(`true`);
19+
expect(gitUrlRegex.test('git@bitbucket.org:team/repo.git')).toMatchInlineSnapshot(`true`);
20+
});
21+
22+
it('rejects empty, blank, and malformed URLs', () => {
1023
expect(gitUrlRegex.test('')).toMatchInlineSnapshot(`false`);
24+
expect(gitUrlRegex.test(' ')).toMatchInlineSnapshot(`false`);
25+
expect(gitUrlRegex.test('not a url')).toMatchInlineSnapshot(`false`);
26+
expect(gitUrlRegex.test('not-a-url')).toMatchInlineSnapshot(`false`);
27+
expect(gitUrlRegex.test('https://')).toMatchInlineSnapshot(`false`);
28+
expect(gitUrlRegex.test('ftp://github.com/foo/bar')).toMatchInlineSnapshot(`false`);
29+
});
30+
31+
it('matches non-standard but syntactically valid git hosts', () => {
32+
expect(gitUrlRegex.test('https://gitea.example.com/foo/bar.git')).toMatchInlineSnapshot(`true`);
33+
expect(gitUrlRegex.test('https://github.enterprise.example.com/foo/bar')).toMatchInlineSnapshot(
34+
`true`,
35+
);
36+
//host alone is still considered a url shape by this regex
37+
expect(gitUrlRegex.test('https://github.com')).toMatchInlineSnapshot(`true`);
1138
});
1239
});
1340

1441
describe('detectGitType', () => {
15-
it('detects git providers', () => {
42+
it('detects known providers from https URLs', () => {
1643
expect(detectGitType('https://github.com/foo/bar')).toMatchInlineSnapshot(`"github"`);
44+
expect(detectGitType('https://www.github.com/foo/bar')).toMatchInlineSnapshot(`"github"`);
1745
expect(detectGitType('https://gitlab.com/foo/bar')).toMatchInlineSnapshot(`"gitlab"`);
46+
expect(detectGitType('https://www.gitlab.com/foo/bar')).toMatchInlineSnapshot(`"gitlab"`);
1847
expect(detectGitType('https://bitbucket.org/foo/bar')).toMatchInlineSnapshot(`"bitbucket"`);
19-
expect(detectGitType('https://example.com/foo/bar')).toMatchInlineSnapshot(`"other"`);
20-
expect(detectGitType('not a url')).toMatchInlineSnapshot(`""`);
48+
expect(detectGitType('https://www.bitbucket.org/foo/bar')).toMatchInlineSnapshot(`"bitbucket"`);
49+
});
50+
51+
it('detects known providers from scp-style SSH URLs', () => {
2152
expect(detectGitType('git@github.com:foo/bar.git')).toMatchInlineSnapshot(`"github"`);
53+
expect(detectGitType('git@gitlab.com:foo/bar.git')).toMatchInlineSnapshot(`"gitlab"`);
54+
expect(detectGitType('git@bitbucket.org:team/repo.git')).toMatchInlineSnapshot(`"bitbucket"`);
55+
});
56+
57+
it('returns empty string for invalid or empty input', () => {
58+
expect(detectGitType('')).toMatchInlineSnapshot(`""`);
59+
expect(detectGitType('not a url')).toMatchInlineSnapshot(`""`);
60+
expect(detectGitType('https://')).toMatchInlineSnapshot(`""`);
61+
});
62+
63+
it('returns other for unrecognized or non-standard formats', () => {
64+
//valid git url shape, but not a known public provider
65+
expect(detectGitType('https://example.com/foo/bar')).toMatchInlineSnapshot(`"other"`);
66+
expect(detectGitType('https://gitea.example.com/foo/bar.git')).toMatchInlineSnapshot(`"other"`);
67+
expect(detectGitType('https://github.enterprise.example.com/foo/bar')).toMatchInlineSnapshot(
68+
`"other"`,
69+
);
70+
//http (not https) and ssh:// do not match hasDomain checks, so provider is unsure
71+
expect(detectGitType('http://github.com/foo/bar')).toMatchInlineSnapshot(`"other"`);
72+
expect(detectGitType('ssh://git@github.com/foo/bar')).toMatchInlineSnapshot(`"other"`);
2273
});
2374
});

src/gitops/utils/urls.test.ts

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,44 @@
11
import { isSHA, repoUrl, revisionUrl } from './urls';
22

33
describe('isSHA', () => {
4-
it('identifies SHA hashes', () => {
4+
it('identifies short and full hex SHAs', () => {
5+
expect(isSHA('abcde')).toMatchInlineSnapshot(`true`);
56
expect(isSHA('abc123def')).toMatchInlineSnapshot(`true`);
67
expect(isSHA('abc123def456789012345678901234567890abcd')).toMatchInlineSnapshot(`true`);
8+
expect(isSHA('1234567890123456789012345678901234567890')).toMatchInlineSnapshot(`true`);
9+
});
10+
11+
it('identifies sha256-prefixed hashes', () => {
12+
expect(isSHA('sha256:abc123de')).toMatchInlineSnapshot(`true`);
713
expect(isSHA('sha256:abc123def456789012345678901234567890abcd')).toMatchInlineSnapshot(`true`);
14+
});
15+
16+
it('rejects empty strings, branches, tags, and missing SHAs', () => {
17+
expect(isSHA('')).toMatchInlineSnapshot(`false`);
18+
expect(isSHA('HEAD')).toMatchInlineSnapshot(`false`);
819
expect(isSHA('main')).toMatchInlineSnapshot(`false`);
20+
expect(isSHA('develop')).toMatchInlineSnapshot(`false`);
921
expect(isSHA('v1.0.0')).toMatchInlineSnapshot(`false`);
22+
expect(isSHA('v1.2.3')).toMatchInlineSnapshot(`false`);
23+
});
24+
25+
it('rejects values outside the supported hex length and charset', () => {
26+
//too short for plain sha (needs 5–40 hex chars)
1027
expect(isSHA('abc')).toMatchInlineSnapshot(`false`);
28+
expect(isSHA('abcd')).toMatchInlineSnapshot(`false`);
29+
//too long for plain sha (>40)
30+
expect(isSHA('12345678901234567890123456789012345678901')).toMatchInlineSnapshot(`false`);
31+
//uppercase is not matched by the lowercase-only regex
32+
expect(isSHA('ABCDEF')).toMatchInlineSnapshot(`false`);
33+
expect(isSHA('abc123DEF')).toMatchInlineSnapshot(`false`);
34+
//sha256 prefix with empty or too-short hash
35+
expect(isSHA('sha256:')).toMatchInlineSnapshot(`false`);
36+
expect(isSHA('sha256:abc')).toMatchInlineSnapshot(`false`);
1137
});
1238
});
1339

1440
describe('repoUrl', () => {
15-
it('extracts repo URLs from various formats', () => {
41+
it('extracts canonical https repo paths from common formats', () => {
1642
expect(repoUrl('https://github.com/argoproj/argo-cd.git')).toMatchInlineSnapshot(
1743
`"https://github.com/argoproj/argo-cd"`,
1844
);
@@ -22,24 +48,30 @@ describe('repoUrl', () => {
2248
expect(repoUrl('git@github.com:argoproj/argo-cd.git')).toMatchInlineSnapshot(
2349
`"https://github.com/argoproj/argo-cd"`,
2450
);
51+
expect(repoUrl('ssh://git@github.com/foo/bar.git')).toMatchInlineSnapshot(
52+
`"https://github.com/foo/bar"`,
53+
);
2554
expect(repoUrl('https://gitlab.com/group/project.git')).toMatchInlineSnapshot(
2655
`"https://gitlab.com/group/project"`,
2756
);
2857
expect(repoUrl('https://bitbucket.org/team/repo.git')).toMatchInlineSnapshot(
2958
`"https://bitbucket.org/team/repo"`,
3059
);
60+
});
61+
62+
it('returns null for empty, malformed, or unsupported providers', () => {
63+
expect(repoUrl('')).toMatchInlineSnapshot(`null`);
64+
expect(repoUrl('not a url')).toMatchInlineSnapshot(`null`);
3165
expect(repoUrl('https://internal.example.com/repo.git')).toMatchInlineSnapshot(`null`);
66+
expect(repoUrl('https://gitea.io/foo/bar')).toMatchInlineSnapshot(`null`);
3267
});
3368
});
3469

3570
describe('revisionUrl', () => {
36-
it('builds revision URLs for different providers', () => {
71+
it('builds commit URLs for SHA revisions', () => {
3772
expect(revisionUrl('https://github.com/foo/bar.git', 'abc123def', false)).toMatchInlineSnapshot(
3873
`"https://github.com/foo/bar/commit/abc123def"`,
3974
);
40-
expect(revisionUrl('https://github.com/foo/bar.git', 'main', false)).toMatchInlineSnapshot(
41-
`"https://github.com/foo/bar/tree/main"`,
42-
);
4375
expect(revisionUrl('https://gitlab.com/foo/bar.git', 'abc123def', false)).toMatchInlineSnapshot(
4476
`"https://gitlab.com/foo/bar/-/commit/abc123def"`,
4577
);
@@ -49,8 +81,37 @@ describe('revisionUrl', () => {
4981
expect(
5082
revisionUrl('https://bitbucket.org/foo/bar.git', 'abc123def', true),
5183
).toMatchInlineSnapshot(`"https://bitbucket.org/foo/bar/src/abc123def"`);
84+
});
85+
86+
it('builds tree/src URLs for branch names', () => {
87+
expect(revisionUrl('https://github.com/foo/bar.git', 'main', false)).toMatchInlineSnapshot(
88+
`"https://github.com/foo/bar/tree/main"`,
89+
);
90+
expect(revisionUrl('https://gitlab.com/foo/bar.git', 'main', false)).toMatchInlineSnapshot(
91+
`"https://gitlab.com/foo/bar/-/tree/main"`,
92+
);
93+
expect(revisionUrl('https://bitbucket.org/foo/bar.git', 'main', false)).toMatchInlineSnapshot(
94+
`"https://bitbucket.org/foo/bar/src/main"`,
95+
);
96+
expect(revisionUrl('https://bitbucket.org/foo/bar.git', 'main', true)).toMatchInlineSnapshot(
97+
`"https://bitbucket.org/foo/bar/src/main"`,
98+
);
99+
});
100+
101+
it('defaults missing revision to HEAD', () => {
52102
expect(revisionUrl('https://github.com/foo/bar.git', '', false)).toMatchInlineSnapshot(
53103
`"https://github.com/foo/bar/tree/HEAD"`,
54104
);
105+
expect(revisionUrl('https://github.com/foo/bar.git', null as any, false)).toMatchInlineSnapshot(
106+
`"https://github.com/foo/bar/tree/HEAD"`,
107+
);
108+
});
109+
110+
it('returns null for empty, malformed, or unsupported repo URLs', () => {
111+
expect(revisionUrl('', 'abc123', false)).toMatchInlineSnapshot(`null`);
112+
expect(revisionUrl('not a url', 'abc123', false)).toMatchInlineSnapshot(`null`);
113+
expect(revisionUrl('https://gitea.io/foo/bar.git', 'abc123', false)).toMatchInlineSnapshot(
114+
`null`,
115+
);
55116
});
56117
});

src/gitops/utils/urls.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
* https://github.com/argoproj/argo-cd/blob/4bd8b07c514e26c6b7837f30d52afd1a3cdedcfd/ui/src/app/shared/components/urls.ts
44
*/
55

6-
import * as GitUrlParse from 'git-url-parse';
6+
//cjs package — default import needs esmoduleinterop or jest can't call it
7+
import GitUrlParse from 'git-url-parse';
78
import { GitUrl } from 'git-url-parse';
89

910
export const isSHA = (revision: string) => {

tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
"allowJs": true,
99
"strict": false,
1010
"allowSyntheticDefaultImports": true,
11+
//so cjs default imports (git-url-parse) work in jest
12+
"esModuleInterop": true,
1113
"noUnusedLocals": true,
1214
"lib": ["dom", "es2017"],
15+
"types": ["jest"],
1316
"paths": {
1417
"@gitops/*": ["src/gitops/*"],
1518
"@gitops-models/*": ["src/gitops/models/*"],

0 commit comments

Comments
 (0)