import { describe, expect, it } from 'vitest'; import { getGitHubCompareUrl, gitRemoteUrlToWeb, isGitHubUrl, isValidGitUrl, sshGitUrlToHttps } from './git-url.js'; describe('isValidGitUrl', () => { it.each([ 'https://github.com/org/repo.git', 'https://github.com/org/repo', 'http://gitlab.com/org/repo.git', 'https://gitlab-pr-18183.superblocks.dev/root/ndavid.git', 'git@github.com:org/repo.git', 'git@gitlab.com:org/repo' ])('returns true for %s', (url) => { expect(isValidGitUrl(url)).toBe(true); }); it.each(['', 'not-a-url', 'ftp://github.com/org/repo', 'https://', 'https://github.com/org'])('returns false for %j', (url) => { expect(isValidGitUrl(url)).toBe(false); }); it('trims whitespace', () => { expect(isValidGitUrl(' https://github.com/org/repo ')).toBe(true); }); }); describe('isGitHubUrl', () => { it.each(['https://github.com/org/repo.git', 'https://github.com/org/repo', 'git@github.com:org/repo.git'])( 'returns true for %s', (url) => { expect(isGitHubUrl(url)).toBe(true); } ); it.each(['https://gitlab.com/org/repo', 'git@bitbucket.org:org/repo.git', 'https://example.com/org/repo', 'not-a-url'])( 'returns false for %s', (url) => { expect(isGitHubUrl(url)).toBe(false); } ); it('is case-insensitive on the host', () => { expect(isGitHubUrl('https://GitHub.COM/org/repo')).toBe(true); }); }); describe('gitRemoteUrlToWeb', () => { it.each<[string, string]>([ ['https://github.com/org/repo.git', 'https://github.com/org/repo'], ['https://github.com/org/repo', 'https://github.com/org/repo'], ['http://gitlab.com/org/repo.git', 'http://gitlab.com/org/repo'], ['git@github.com:org/repo.git', 'https://github.com/org/repo'], ['git@gitlab.com:group/sub/repo.git', 'https://gitlab.com/group/sub/repo'], // Azure DevOps HTTPS with userinfo (clone URL default format) ['https://myorg@dev.azure.com/myorg/myproject/_git/myrepo', 'https://dev.azure.com/myorg/myproject/_git/myrepo'], // Azure DevOps SSH formats ['git@ssh.dev.azure.com:v3/myorg/myproject/myrepo', 'https://dev.azure.com/myorg/myproject/_git/myrepo'], ['ssh://git@ssh.dev.azure.com/v3/myorg/myproject/myrepo', 'https://dev.azure.com/myorg/myproject/_git/myrepo'] ])('converts %s → %s', (input, expected) => { expect(gitRemoteUrlToWeb(input)).toBe(expected); }); it('strips userinfo from HTTPS URLs', () => { expect(gitRemoteUrlToWeb('https://myorg@dev.azure.com/myorg/myproject/_git/myrepo')).toBe( 'https://dev.azure.com/myorg/myproject/_git/myrepo' ); }); it('returns null for unconvertible URLs', () => { expect(gitRemoteUrlToWeb('not-a-url')).toBeNull(); }); }); describe('getGitHubCompareUrl', () => { const base = { fromBranch: 'feature/cool', toBranch: 'main' }; it('builds a GitHub compare URL', () => { expect( getGitHubCompareUrl({ gitRemoteUrl: 'https://github.com/org/repo.git', ...base }) ).toBe('https://github.com/org/repo/compare/main...feature%2Fcool'); }); it('appends title and body', () => { expect( getGitHubCompareUrl({ gitRemoteUrl: 'https://github.com/org/repo.git', ...base, title: 'My PR', body: '## Summary' }) ).toBe('https://github.com/org/repo/compare/main...feature%2Fcool?expand=1&title=My+PR&body=%23%23+Summary'); }); it('returns null for non-GitHub URLs', () => { expect( getGitHubCompareUrl({ gitRemoteUrl: 'https://gitlab.com/org/repo', ...base }) ).toBeNull(); }); it('returns null for unconvertible URLs', () => { expect(getGitHubCompareUrl({ gitRemoteUrl: 'not-a-url', ...base })).toBeNull(); }); }); describe('sshGitUrlToHttps', () => { it.each<[string, string]>([ ['git@github.com:org/repo.git', 'https://github.com/org/repo.git'], ['git@github.com:org/repo', 'https://github.com/org/repo.git'], ['git@gitlab.com:group/sub/repo.git', 'https://gitlab.com/group/sub/repo.git'], ['git@gitlab-pr-17501.superblocks.dev:root/iojitest.git', 'https://gitlab-pr-17501.superblocks.dev/root/iojitest.git'], ['git@bitbucket.org:org/repo.git', 'https://bitbucket.org/org/repo.git'], ['ssh://git@github.com/org/repo.git', 'https://github.com/org/repo.git'], ['ssh://git@github.com/org/repo', 'https://github.com/org/repo.git'], ['git@ssh.dev.azure.com:v3/myorg/myproject/myrepo', 'https://dev.azure.com/myorg/myproject/_git/myrepo'], ['ssh://git@ssh.dev.azure.com/v3/myorg/myproject/myrepo', 'https://dev.azure.com/myorg/myproject/_git/myrepo'] ])('converts %s → %s', (input, expected) => { expect(sshGitUrlToHttps(input)).toBe(expected); }); it('returns HTTPS URLs unchanged', () => { expect(sshGitUrlToHttps('https://github.com/org/repo.git')).toBe('https://github.com/org/repo.git'); }); it('appends .git suffix when missing on HTTPS passthrough', () => { expect(sshGitUrlToHttps('https://github.com/org/repo')).toBe('https://github.com/org/repo.git'); }); it('does NOT append .git suffix to Azure DevOps HTTPS URLs', () => { expect(sshGitUrlToHttps('https://dev.azure.com/myorg/myproject/_git/myrepo')).toBe('https://dev.azure.com/myorg/myproject/_git/myrepo'); }); it('handles Azure DevOps HTTPS URLs with userinfo', () => { expect( sshGitUrlToHttps('https://superblocks-azure@dev.azure.com/superblocks-azure/Superblocks%20Devops%20Azure%20Test/_git/david-sb-gh') ).toBe('https://superblocks-azure@dev.azure.com/superblocks-azure/Superblocks%20Devops%20Azure%20Test/_git/david-sb-gh'); }); it('returns null for unconvertible URLs', () => { expect(sshGitUrlToHttps('not-a-url')).toBeNull(); }); });