Path: blob/main/extensions/copilot/src/platform/git/test/node/gitService.spec.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { suite, test } from 'vitest';7import { AdoRepoId, getAdoRepoIdFromFetchUrl, getGithubRepoIdFromFetchUrl, GithubRepoId, normalizeFetchUrl, parseRemoteUrl, toGithubWebUrl } from '../../common/gitService';89function assertGitIdEquals(a: GithubRepoId | undefined, b: { org: string; repo: string; host?: string } | undefined, message?: string) {10assert.strictEqual(a?.org, b?.org, message);11assert.strictEqual(a?.repo, b?.repo, message);12if (b?.host !== undefined) {13assert.strictEqual(a?.host, b.host, message);14}15}1617suite('parseRemoteUrl', () => {18test('Should handle basic https', () => {19assert.deepStrictEqual(20parseRemoteUrl('https://example.com/owner/repo.git'),21{ host: 'example.com', rawHost: 'example.com', path: '/owner/repo.git' });22});2324test('Should find full subdomain with https', () => {25assert.deepStrictEqual(26parseRemoteUrl('https://sub1.sub2.example.com/owner/repo.git'),27{ host: 'sub1.sub2.example.com', rawHost: 'sub1.sub2.example.com', path: '/owner/repo.git' });28});2930test('Should handle basic Azure dev ops url', () => {31assert.deepStrictEqual(32parseRemoteUrl('https://[email protected]/test/project/_git/vscode-stuff'),33{ host: 'dev.azure.com', rawHost: 'dev.azure.com', path: '/test/project/_git/vscode-stuff' });34});3536test('Should handle basic visual studio url', () => {37assert.deepStrictEqual(38parseRemoteUrl('https://test.visualstudio.com/project/one/_git/two'),39{ host: 'test.visualstudio.com', rawHost: 'test.visualstudio.com', path: '/project/one/_git/two' });40});4142test('Should strip out ports', () => {43assert.deepStrictEqual(44parseRemoteUrl('https://example.com:8080/owner/repo.git'),45{ host: 'example.com', rawHost: 'example.com', path: '/owner/repo.git' });46});4748test('Should handle ssh syntax', () => {49assert.deepStrictEqual(50parseRemoteUrl('ssh://[email protected]/owner/repo.git'),51{ host: 'github.com', rawHost: 'github.com', path: '/owner/repo.git' });52});5354test('Should strip user ids', () => {55assert.deepStrictEqual(56parseRemoteUrl('https://[email protected]/owner/repo.git'),57{ host: 'github.com', rawHost: 'github.com', path: '/owner/repo.git' },58'https, name only');5960assert.deepStrictEqual(61// [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="test credentials")]62parseRemoteUrl('https://myname:[email protected]/owner/repo.git'),63{ host: 'github.com', rawHost: 'github.com', path: '/owner/repo.git' },64'https, with name and PAT');6566assert.deepStrictEqual(67parseRemoteUrl('https://[email protected]/owner/repo.git'),68{ host: 'github.com', rawHost: 'github.com', path: '/owner/repo.git' },69'https, PAT only');7071assert.deepStrictEqual(72parseRemoteUrl('ssh://[email protected]/owner/repo.git'),73{ host: 'github.com', rawHost: 'github.com', path: '/owner/repo.git' },74'ssh, name only');75});7677test('Should preserve rawHost for SSH alias hostnames', () => {78const result = parseRemoteUrl('[email protected]:owner/repo.git');79assert.strictEqual(result?.host, 'github.com');80assert.strictEqual(result?.rawHost, 'my-user-name-github.com');81});8283test('Should handle GHE SSH shorthand with alias prefix', () => {84const result = parseRemoteUrl('[email protected]:sandbox/repo.git');85assert.strictEqual(result?.host, 'eu.ghe.com');86assert.strictEqual(result?.rawHost, 'msdemo-eu.ghe.com');87});88});8990suite('getGithubRepoIdFromFetchUrl', () => {91test('should return undefined for non-GitHub URLs', () => {92const url = 'https://example.com/owner/repo.git';93const result = getGithubRepoIdFromFetchUrl(url);94assert.strictEqual(result, undefined);95});9697test('should return the repo name for git shorthand URLs', () => {98assertGitIdEquals(99getGithubRepoIdFromFetchUrl('[email protected]:owner/repo.git'),100{ org: 'owner', repo: 'repo' });101102assertGitIdEquals(103getGithubRepoIdFromFetchUrl('[email protected]:owner/repo.git'),104{ org: 'owner', repo: 'repo' },105'ghe url');106107assertGitIdEquals(108getGithubRepoIdFromFetchUrl('[email protected]:owner/repo.git'),109{ org: 'owner', repo: 'repo' },110`non 'git' user name`);111112assertGitIdEquals(113getGithubRepoIdFromFetchUrl('[email protected]:owner-xyz/some-repo.git'),114{ org: 'owner-xyz', repo: 'some-repo' },115`non 'git' user name with subdomain alias`);116});117118test('should return the repo name for HTTPS URLs', () => {119assertGitIdEquals(120getGithubRepoIdFromFetchUrl('https://github.com/owner/repo.git'),121{ org: 'owner', repo: 'repo' });122123assertGitIdEquals(124getGithubRepoIdFromFetchUrl('https://xyz.ghe.com/owner/repo.git'),125{ org: 'owner', repo: 'repo' },126'ghe url');127});128129test('should return the repos with trailing slash', () => {130assertGitIdEquals(131getGithubRepoIdFromFetchUrl('https://github.com/owner/repo/'),132{ org: 'owner', repo: 'repo' });133134assertGitIdEquals(135getGithubRepoIdFromFetchUrl('https://github.com/owner/repo.git/'),136{ org: 'owner', repo: 'repo' });137});138139test('should return the repo name for URLs without .git extension', () => {140assertGitIdEquals(141getGithubRepoIdFromFetchUrl('https://github.com/owner/repo'),142{ org: 'owner', repo: 'repo' });143144assertGitIdEquals(145getGithubRepoIdFromFetchUrl('https://github.com/owner/repo/'),146{ org: 'owner', repo: 'repo' },147'With trailing slash');148});149150test('should return the repo name for ssh:// URLs', () => {151assertGitIdEquals(152getGithubRepoIdFromFetchUrl('ssh://[email protected]/owner/repo.git'),153{ org: 'owner', repo: 'repo' });154155assertGitIdEquals(156getGithubRepoIdFromFetchUrl('ssh://[email protected]/owner/repo'),157{ org: 'owner', repo: 'repo' });158159assertGitIdEquals(160getGithubRepoIdFromFetchUrl('ssh://[email protected]/owner/repo.git'),161{ org: 'owner', repo: 'repo' },162'On ssh.github.com subdomain');163164assertGitIdEquals(165getGithubRepoIdFromFetchUrl('ssh://[email protected]/owner/repo.git'),166{ org: 'owner', repo: 'repo' },167'ghe.com subdomain');168169assertGitIdEquals(170getGithubRepoIdFromFetchUrl('ssh://[email protected]:443/owner/repo.git'),171{ org: 'owner', repo: 'repo' },172'With port');173});174175test('should return undefined for invalid GitHub URLs', () => {176{177const url = 'https://github.com/owner';178const result = getGithubRepoIdFromFetchUrl(url);179assert.deepStrictEqual(result, undefined);180}181{182const url = 'https://github.com/';183const result = getGithubRepoIdFromFetchUrl(url);184assert.deepStrictEqual(result, undefined);185}186});187188test('should return undefined for invalid URLs', () => {189const url = 'invalid-url';190const result = getGithubRepoIdFromFetchUrl(url);191assert.deepStrictEqual(result, undefined);192});193194test('should return undefined for unsupported scheme', () => {195const url = 'gopher://github.com/owner/repo.git';196const result = getGithubRepoIdFromFetchUrl(url);197assert.deepStrictEqual(result, undefined);198});199200test('should support github url that uses www subdomain', () => {201// Likely a mistake but we can parse it easily202assertGitIdEquals(203getGithubRepoIdFromFetchUrl('https://www.github.com/owner/repo.git'),204{ org: 'owner', repo: 'repo' });205});206207test('should support github url using http', () => {208// This is a mistake but we can parse it easily209assertGitIdEquals(210getGithubRepoIdFromFetchUrl('http://github.com/owner/repo.git'),211{ org: 'owner', repo: 'repo' });212});213214test('should support urls with custom user names and PAT in urls', () => {215assertGitIdEquals(216getGithubRepoIdFromFetchUrl('https://[email protected]/owner/repo.git'),217{ org: 'owner', repo: 'repo' },218'https, name only');219220assertGitIdEquals(221// [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="test credentials")]222getGithubRepoIdFromFetchUrl('https://myname:[email protected]/owner/repo.git'),223{ org: 'owner', repo: 'repo' },224'https, with name and PAT');225226assertGitIdEquals(227getGithubRepoIdFromFetchUrl('https://[email protected]/owner/repo.git'),228{ org: 'owner', repo: 'repo' },229'https, PAT only');230231assertGitIdEquals(232getGithubRepoIdFromFetchUrl('ssh://[email protected]/owner/repo.git'),233{ org: 'owner', repo: 'repo' },234'ssh, name only');235});236237test('should support github urls that are likely ssh aliases', () => {238assertGitIdEquals(239getGithubRepoIdFromFetchUrl('[email protected]:owner/repo.git'),240{ org: 'owner', repo: 'repo' },241'Custom name before github.com');242243assertGitIdEquals(244getGithubRepoIdFromFetchUrl('[email protected]:owner/repo.git'),245{ org: 'owner', repo: 'repo' },246'Custom name after github.com');247});248249test('should set host to github.com for github.com URLs', () => {250const result = getGithubRepoIdFromFetchUrl('https://github.com/owner/repo.git');251assert.strictEqual(result?.host, 'github.com');252});253254test('should set host to ghe.com subdomain for GHE URLs', () => {255const result = getGithubRepoIdFromFetchUrl('https://xyz.ghe.com/owner/repo.git');256assert.strictEqual(result?.host, 'xyz.ghe.com');257});258259test('should set host to ghe.com subdomain for GHE SSH shorthand URLs', () => {260const result = getGithubRepoIdFromFetchUrl('[email protected]:sandbox/repo.git');261assert.strictEqual(result?.host, 'msdemo-eu.ghe.com');262});263264test('should set host to github.com for SSH alias URLs', () => {265const result = getGithubRepoIdFromFetchUrl('[email protected]:owner/repo.git');266assert.strictEqual(result?.host, 'github.com');267});268269test('should set host to github.com for ssh:// github.com URLs', () => {270const result = getGithubRepoIdFromFetchUrl('ssh://[email protected]/owner/repo.git');271assert.strictEqual(result?.host, 'github.com');272});273274test('should set host to ghe.com subdomain for ssh:// ghe URLs', () => {275const result = getGithubRepoIdFromFetchUrl('ssh://[email protected]/owner/repo.git');276assert.strictEqual(result?.host, 'myco.ghe.com');277});278279test('should set host to github.com for http github.com URLs', () => {280const result = getGithubRepoIdFromFetchUrl('http://github.com/owner/repo.git');281assert.strictEqual(result?.host, 'github.com');282});283284test('should set host to github.com for www.github.com URLs', () => {285const result = getGithubRepoIdFromFetchUrl('https://www.github.com/owner/repo.git');286assert.strictEqual(result?.host, 'github.com');287});288289test('should set host to github.com for SSH alias with suffix', () => {290const result = getGithubRepoIdFromFetchUrl('[email protected]:owner/repo.git');291assert.strictEqual(result?.host, 'github.com');292});293294test('should resolve host and org/repo correctly for real-world GHE SSH shorthand', () => {295// Real-world scenario: [email protected]:sandbox/repo.git296const result = getGithubRepoIdFromFetchUrl('[email protected]:sandbox/repo.git');297assert.ok(result, 'Should parse successfully');298assert.strictEqual(result.org, 'sandbox');299assert.strictEqual(result.repo, 'repo');300assert.strictEqual(result.host, 'msdemo-eu.ghe.com');301});302303test('should resolve host for GHE HTTPS with credentials', () => {304const result = getGithubRepoIdFromFetchUrl('https://[email protected]/org/repo.git');305assert.ok(result, 'Should parse successfully');306assert.strictEqual(result.host, 'myco.ghe.com');307assert.strictEqual(result.org, 'org');308assert.strictEqual(result.repo, 'repo');309});310311test('should resolve host for GHE HTTPS without .git extension', () => {312const result = getGithubRepoIdFromFetchUrl('https://myco.ghe.com/org/repo');313assert.ok(result, 'Should parse successfully');314assert.strictEqual(result.host, 'myco.ghe.com');315});316});317318suite('GithubRepoId', () => {319test('should default host to github.com', () => {320const id = new GithubRepoId('owner', 'repo');321assert.strictEqual(id.host, 'github.com');322});323324test('should accept custom host', () => {325const id = new GithubRepoId('owner', 'repo', 'myco.ghe.com');326assert.strictEqual(id.host, 'myco.ghe.com');327});328329test('toString should return org/repo without host', () => {330const id = new GithubRepoId('Owner', 'Repo', 'myco.ghe.com');331assert.strictEqual(id.toString(), 'owner/repo');332});333334test('parse should default host to github.com', () => {335const id = GithubRepoId.parse('owner/repo');336assert.ok(id);337assert.strictEqual(id.host, 'github.com');338});339340test('parse should return undefined for invalid nwo', () => {341assert.strictEqual(GithubRepoId.parse('invalid'), undefined);342assert.strictEqual(GithubRepoId.parse('a/b/c'), undefined);343assert.strictEqual(GithubRepoId.parse(''), undefined);344});345346test('type should be github', () => {347const id = new GithubRepoId('owner', 'repo');348assert.strictEqual(id.type, 'github');349});350});351352suite('toGithubWebUrl', () => {353test('should return github.com URL for default host', () => {354const id = new GithubRepoId('owner', 'repo');355assert.strictEqual(toGithubWebUrl(id), 'https://github.com/owner/repo');356});357358test('should return GHE URL for custom host', () => {359const id = new GithubRepoId('owner', 'repo', 'myco.ghe.com');360assert.strictEqual(toGithubWebUrl(id), 'https://myco.ghe.com/owner/repo');361});362363test('should preserve case in org and repo', () => {364const id = new GithubRepoId('MyOrg', 'MyRepo', 'myco.ghe.com');365assert.strictEqual(toGithubWebUrl(id), 'https://myco.ghe.com/MyOrg/MyRepo');366});367368test('should handle deeply nested GHE subdomain', () => {369const id = new GithubRepoId('org', 'repo', 'dept.company.ghe.com');370assert.strictEqual(toGithubWebUrl(id), 'https://dept.company.ghe.com/org/repo');371});372});373374suite('parseRemoteUrl rawHost', () => {375test('should return matching host and rawHost for plain HTTPS URLs', () => {376const result = parseRemoteUrl('https://github.com/owner/repo.git');377assert.strictEqual(result?.host, 'github.com');378assert.strictEqual(result?.rawHost, 'github.com');379});380381test('should return matching host and rawHost for GHE HTTPS', () => {382const result = parseRemoteUrl('https://myco.ghe.com/owner/repo.git');383assert.strictEqual(result?.host, 'myco.ghe.com');384assert.strictEqual(result?.rawHost, 'myco.ghe.com');385});386387test('should return different host and rawHost for SSH alias prefix', () => {388const result = parseRemoteUrl('[email protected]:owner/repo.git');389assert.strictEqual(result?.rawHost, 'my-alias-github.com');390assert.strictEqual(result?.host, 'github.com');391});392393test('should return different host and rawHost for SSH alias suffix', () => {394const result = parseRemoteUrl('[email protected]:owner/repo.git');395assert.strictEqual(result?.rawHost, 'github.com-my-alias');396assert.strictEqual(result?.host, 'github.com');397});398399test('should strip port from rawHost', () => {400const result = parseRemoteUrl('ssh://[email protected]:443/owner/repo.git');401assert.strictEqual(result?.rawHost, 'github.com');402});403404test('should strip user from rawHost', () => {405const result = parseRemoteUrl('https://[email protected]/owner/repo.git');406assert.strictEqual(result?.rawHost, 'myco.ghe.com');407});408});409410suite('Sanitize Remote Repo Urls', () => {411test('Https url is unchanged', () => {412const url = 'https://github.com/owner/repo.git';413const result = normalizeFetchUrl(url);414assert.strictEqual(result, url);415});416417test('Http url is converted to https', () => {418const url = 'http://github.com/owner/repo.git';419const result = normalizeFetchUrl(url);420assert.strictEqual(result, 'https://github.com/owner/repo.git');421});422423test('Query parameters are removed', () => {424const url = 'https://github.com/owner/repo.git';425const urlWithQuery = `${url}?query=param`;426const result = normalizeFetchUrl(urlWithQuery);427assert.strictEqual(result, url);428});429430test('SSH is converted to HTTPS', () => {431const url = '[email protected]:owner/repo.git';432const result = normalizeFetchUrl(url);433assert.strictEqual(result, 'https://github.com/owner/repo.git');434});435436test('Credentials are removed from HTTPs url', () => {437const url = 'https://user:[email protected]/org/repo';438const result = normalizeFetchUrl(url);439assert.strictEqual(result, 'https://server.com/org/repo');440});441442test('SSH ports are normalized and removed', () => {443const url = 'ssh://[email protected]:7999/project/repo.git';444const result = normalizeFetchUrl(url);445assert.strictEqual(result, 'https://bitbucket.company.pl/project/repo.git');446});447448test('Bitbucket https urls are properly normalized', () => {449const url = 'https://bitbucket.company.pl/scm/project/repo.git';450const result = normalizeFetchUrl(url);451assert.strictEqual(result, 'https://bitbucket.company.pl/project/repo.git');452});453454test('Repos named scm by org foo are not improperly truncated', () => {455const url = 'https://github.com/foo/scm.git';456const result = normalizeFetchUrl(url);457assert.strictEqual(result, 'https://github.com/foo/scm.git');458});459460test('Repos named scm by user scm are not improperly truncated', () => {461const url = 'https://github.com/scm/scm.git';462const result = normalizeFetchUrl(url);463assert.strictEqual(result, 'https://github.com/scm/scm.git');464});465});466467suite('getAdoRepoIdFromFetchUrl', () => {468test('should return undefined for non-ADO URLs', () => {469assert.strictEqual(470getAdoRepoIdFromFetchUrl('https://example.com/owner/repo.git'),471undefined);472assert.strictEqual(473getAdoRepoIdFromFetchUrl('https://github.com/scm/scm.git'),474undefined);475});476477test('should parse https format', () => {478assert.deepStrictEqual(479getAdoRepoIdFromFetchUrl('https://dev.azure.com/organization/project/_git/repository'),480new AdoRepoId('organization', 'project', 'repository'));481});482483test('should parse https format with _optimized', () => {484assert.deepStrictEqual(485getAdoRepoIdFromFetchUrl('https://dev.azure.com/organization/project/_git/_optimized/repository'),486new AdoRepoId('organization', 'project', 'repository'));487});488489test('should parse https format with _full', () => {490assert.deepStrictEqual(491getAdoRepoIdFromFetchUrl('https://dev.azure.com/organization/project/_git/_full/repository'),492new AdoRepoId('organization', 'project', 'repository'));493});494495test('should parse legacy https format', () => {496assert.deepStrictEqual(497getAdoRepoIdFromFetchUrl('https://organization.visualstudio.com/project/_git/repository'),498new AdoRepoId('organization', 'project', 'repository'));499});500501test('should parse legacy https format with _optimized', () => {502assert.deepStrictEqual(503getAdoRepoIdFromFetchUrl('https://organization.visualstudio.com/project/_git/_optimized/repository'),504new AdoRepoId('organization', 'project', 'repository'));505});506507test('should parse legacy https format with _full', () => {508assert.deepStrictEqual(509getAdoRepoIdFromFetchUrl('https://organization.visualstudio.com/project/_git/_full/repository'),510new AdoRepoId('organization', 'project', 'repository'));511});512513test('should parse ssh format', () => {514assert.deepStrictEqual(515getAdoRepoIdFromFetchUrl('[email protected]:v3/organization/project/repository'),516new AdoRepoId('organization', 'project', 'repository'));517});518519test('should parse ssh format with _optimized', () => {520assert.deepStrictEqual(521getAdoRepoIdFromFetchUrl('[email protected]:v3/organization/project/_optimized/repository'),522new AdoRepoId('organization', 'project', 'repository'));523});524525test('should parse ssh format with _full', () => {526assert.deepStrictEqual(527getAdoRepoIdFromFetchUrl('[email protected]:v3/organization/project/_full/repository'),528new AdoRepoId('organization', 'project', 'repository'));529});530531test('should parse legacy ssh format', () => {532assert.deepStrictEqual(533getAdoRepoIdFromFetchUrl('[email protected]:v3/organization/project/repository'),534new AdoRepoId('organization', 'project', 'repository'));535});536537test('should parse legacy ssh format with _optimized', () => {538assert.deepStrictEqual(539getAdoRepoIdFromFetchUrl('[email protected]:v3/organization/project/_optimized/repository'),540new AdoRepoId('organization', 'project', 'repository'));541});542543test('should parse legacy ssh format with _full', () => {544assert.deepStrictEqual(545getAdoRepoIdFromFetchUrl('[email protected]:v3/organization/project/_full/repository'),546new AdoRepoId('organization', 'project', 'repository'));547});548});549550551