From 472e5ee7c92116a3afae2dab223922371e5334e1 Mon Sep 17 00:00:00 2001 From: Sarah Etter Date: Fri, 13 Feb 2026 09:55:51 -0500 Subject: [PATCH 1/2] feat: fix handling of gitlab repos --- package-lock.json | 1 + src/utils/init/config-manual.ts | 4 +- tests/unit/utils/get-repo-data.test.ts | 120 +++++++++++++ tests/unit/utils/init/config-manual.test.ts | 179 ++++++++++++++++++++ 4 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 tests/unit/utils/get-repo-data.test.ts create mode 100644 tests/unit/utils/init/config-manual.test.ts diff --git a/package-lock.json b/package-lock.json index 850fc986971..2b8d045d074 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5813,6 +5813,7 @@ }, "node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": { "version": "1.1.0", + "extraneous": true, "inBundle": true, "license": "MIT" }, diff --git a/src/utils/init/config-manual.ts b/src/utils/init/config-manual.ts index 7b8c6779a3e..90ff5477fee 100644 --- a/src/utils/init/config-manual.ts +++ b/src/utils/init/config-manual.ts @@ -88,8 +88,8 @@ export default async function configManual({ const repoPath = await getRepoPath({ repoData }) const repo = { - provider: 'manual', - repo_path: repoPath, + provider: repoData.provider ?? 'manual', + repo_path: repoData.repo ?? repoPath, repo_branch: repoData.branch, allowed_branches: [repoData.branch], deploy_key_id: deployKey.id, diff --git a/tests/unit/utils/get-repo-data.test.ts b/tests/unit/utils/get-repo-data.test.ts new file mode 100644 index 00000000000..feaa1c8fb36 --- /dev/null +++ b/tests/unit/utils/get-repo-data.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it, vi } from 'vitest' +import type { RepoData } from '../../../src/utils/get-repo-data.js' + +vi.mock('../../../src/utils/command-helpers.js', () => ({ + log: vi.fn(), +})) + +describe('getRepoData', () => { + describe('RepoData structure for different Git providers', () => { + it('should construct correct httpsUrl for GitHub SSH URLs', () => { + const mockRepoData: RepoData = { + name: 'test', + owner: 'ownername', + repo: 'ownername/test', + url: 'git@github.com:ownername/test.git', + branch: 'main', + provider: 'github', + httpsUrl: 'https://github.com/ownername/test', + } + + expect(mockRepoData.httpsUrl).toBe('https://github.com/ownername/test') + expect(mockRepoData.provider).toBe('github') + expect(mockRepoData.repo).toBe('ownername/test') + }) + + it('should construct correct httpsUrl for GitLab SSH URLs', () => { + const mockRepoData: RepoData = { + name: 'test', + owner: 'ownername', + repo: 'ownername/test', + url: 'git@gitlab.com:ownername/test.git', + branch: 'main', + provider: 'gitlab', + httpsUrl: 'https://gitlab.com/ownername/test', + } + + expect(mockRepoData.httpsUrl).toBe('https://gitlab.com/ownername/test') + expect(mockRepoData.provider).toBe('gitlab') + expect(mockRepoData.repo).toBe('ownername/test') + }) + + it('should construct correct httpsUrl for GitHub HTTPS URLs', () => { + const mockRepoData: RepoData = { + name: 'test', + owner: 'ownername', + repo: 'ownername/test', + url: 'https://github.com/ownername/test.git', + branch: 'main', + provider: 'github', + httpsUrl: 'https://github.com/ownername/test', + } + + expect(mockRepoData.httpsUrl).toBe('https://github.com/ownername/test') + expect(mockRepoData.provider).toBe('github') + expect(mockRepoData.repo).toBe('ownername/test') + }) + + it('should construct correct httpsUrl for GitLab HTTPS URLs', () => { + const mockRepoData: RepoData = { + name: 'test', + owner: 'ownername', + repo: 'ownername/test', + url: 'https://gitlab.com/ownername/test.git', + branch: 'main', + provider: 'gitlab', + httpsUrl: 'https://gitlab.com/ownername/test', + } + + expect(mockRepoData.httpsUrl).toBe('https://gitlab.com/ownername/test') + expect(mockRepoData.provider).toBe('gitlab') + expect(mockRepoData.repo).toBe('ownername/test') + }) + + it('should use host as provider for unknown Git hosts', () => { + const mockRepoData: RepoData = { + name: 'test', + owner: 'user', + repo: 'user/test', + url: 'git@custom-git.example.com:user/test.git', + branch: 'main', + provider: 'custom-git.example.com', + httpsUrl: 'https://custom-git.example.com/user/test', + } + + expect(mockRepoData.httpsUrl).toBe('https://custom-git.example.com/user/test') + expect(mockRepoData.provider).toBe('custom-git.example.com') + expect(mockRepoData.repo).toBe('user/test') + }) + }) + + describe('provider field mapping', () => { + it('should map github.com to "github" provider', () => { + const mockRepoData: RepoData = { + name: 'test', + owner: 'user', + repo: 'user/test', + url: 'git@github.com:user/test.git', + branch: 'main', + provider: 'github', + httpsUrl: 'https://github.com/user/test', + } + + expect(mockRepoData.provider).toBe('github') + }) + + it('should map gitlab.com to "gitlab" provider', () => { + const mockRepoData: RepoData = { + name: 'test', + owner: 'user', + repo: 'user/test', + url: 'git@gitlab.com:user/test.git', + branch: 'main', + provider: 'gitlab', + httpsUrl: 'https://gitlab.com/user/test', + } + + expect(mockRepoData.provider).toBe('gitlab') + }) + }) +}) diff --git a/tests/unit/utils/init/config-manual.test.ts b/tests/unit/utils/init/config-manual.test.ts new file mode 100644 index 00000000000..099486385de --- /dev/null +++ b/tests/unit/utils/init/config-manual.test.ts @@ -0,0 +1,179 @@ +import { describe, expect, it, vi, beforeEach, type Mock } from 'vitest' +import type { RepoData } from '../../../../src/utils/get-repo-data.js' +import type { NetlifyAPI } from '@netlify/api' + +const mockPrompt = vi.fn() +const mockLog = vi.fn() +const mockExit = vi.fn() +const mockCreateDeployKey = vi.fn() +const mockGetBuildSettings = vi.fn() +const mockSaveNetlifyToml = vi.fn() +const mockSetupSite = vi.fn() + +vi.mock('inquirer', () => ({ + default: { + prompt: mockPrompt, + }, +})) + +vi.mock('../../../../src/utils/command-helpers.js', () => ({ + log: mockLog, + exit: mockExit, +})) + +vi.mock('../../../../src/utils/init/utils.js', () => ({ + createDeployKey: mockCreateDeployKey, + getBuildSettings: mockGetBuildSettings, + saveNetlifyToml: mockSaveNetlifyToml, + setupSite: mockSetupSite, +})) + +describe('config-manual', () => { + let mockApi: Partial + let mockCommand: any + + beforeEach(() => { + vi.clearAllMocks() + + mockApi = {} + mockCommand = { + netlify: { + api: mockApi, + cachedConfig: { configPath: '/test/netlify.toml' }, + config: { plugins: [] }, + repositoryRoot: '/test', + }, + } + + mockPrompt.mockResolvedValue({ + sshKeyAdded: true, + repoPath: 'git@gitlab.com:test/repo.git', + deployHookAdded: true, + }) + + mockCreateDeployKey.mockResolvedValue({ id: 'key-123', public_key: 'ssh-rsa test' }) + mockGetBuildSettings.mockResolvedValue({ + baseDir: '', + buildCmd: 'npm run build', + buildDir: 'dist', + functionsDir: 'functions', + pluginsToInstall: [], + }) + mockSaveNetlifyToml.mockResolvedValue(undefined) + mockSetupSite.mockResolvedValue({ deploy_hook: 'https://api.netlify.com/hooks/test' }) + }) + + describe('GitLab repository configuration', () => { + it('should use provider from repoData for GitLab repos', async () => { + const configManual = (await import('../../../../src/utils/init/config-manual.js')).default + + const repoData: RepoData = { + name: 'test', + owner: 'ownername', + repo: 'ownername/test', + url: 'git@gitlab.com:ownername/test.git', + branch: 'main', + provider: 'gitlab', + httpsUrl: 'https://gitlab.com/ownername/test', + } + + await configManual({ + command: mockCommand, + repoData, + siteId: 'site-123', + }) + + expect(mockSetupSite).toHaveBeenCalledWith( + expect.objectContaining({ + repo: expect.objectContaining({ + provider: 'gitlab', + repo_path: 'ownername/test', + }), + }), + ) + }) + + it('should use repo path (owner/name format) instead of SSH URL for GitLab', async () => { + const configManual = (await import('../../../../src/utils/init/config-manual.js')).default + + const repoData: RepoData = { + name: 'test', + owner: 'ownername', + repo: 'ownername/test', + url: 'git@gitlab.com:ownername/test.git', + branch: 'main', + provider: 'gitlab', + httpsUrl: 'https://gitlab.com/ownername/test', + } + + await configManual({ + command: mockCommand, + repoData, + siteId: 'site-123', + }) + + const setupSiteCall = (mockSetupSite as Mock).mock.calls[0][0] + expect(setupSiteCall.repo.repo_path).toBe('ownername/test') + expect(setupSiteCall.repo.repo_path).not.toBe('git@gitlab.com:ownername/test.git') + }) + + it('should fallback to manual provider when provider is null', async () => { + const configManual = (await import('../../../../src/utils/init/config-manual.js')).default + + const repoData: RepoData = { + name: 'test', + owner: 'user', + repo: 'user/test', + url: 'git@custom.com:user/test.git', + branch: 'main', + provider: null, + httpsUrl: 'https://custom.com/user/test', + } + + await configManual({ + command: mockCommand, + repoData, + siteId: 'site-123', + }) + + expect(mockSetupSite).toHaveBeenCalledWith( + expect.objectContaining({ + repo: expect.objectContaining({ + provider: 'manual', + }), + }), + ) + }) + }) + + describe('GitHub repository configuration', () => { + it('should use provider from repoData for GitHub repos', async () => { + const configManual = (await import('../../../../src/utils/init/config-manual.js')).default + + const repoData: RepoData = { + name: 'test', + owner: 'user', + repo: 'user/test', + url: 'git@github.com:user/test.git', + branch: 'main', + provider: 'github', + httpsUrl: 'https://github.com/user/test', + } + + await configManual({ + command: mockCommand, + repoData, + siteId: 'site-123', + }) + + expect(mockSetupSite).toHaveBeenCalledWith( + expect.objectContaining({ + repo: expect.objectContaining({ + provider: 'github', + repo_path: 'user/test', + }), + }), + ) + }) + }) +}) From a2b718198f7bbf955f7114fd80836f881e94f968 Mon Sep 17 00:00:00 2001 From: Sarah Etter Date: Fri, 13 Feb 2026 10:09:42 -0500 Subject: [PATCH 2/2] fix: types --- tests/unit/utils/init/config-manual.test.ts | 56 ++++++++------------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/tests/unit/utils/init/config-manual.test.ts b/tests/unit/utils/init/config-manual.test.ts index 099486385de..0cdc807d28f 100644 --- a/tests/unit/utils/init/config-manual.test.ts +++ b/tests/unit/utils/init/config-manual.test.ts @@ -1,6 +1,7 @@ -import { describe, expect, it, vi, beforeEach, type Mock } from 'vitest' +import { describe, expect, it, vi, beforeEach } from 'vitest' import type { RepoData } from '../../../../src/utils/get-repo-data.js' import type { NetlifyAPI } from '@netlify/api' +import type BaseCommand from '../../../../src/commands/base-command.js' const mockPrompt = vi.fn() const mockLog = vi.fn() @@ -30,7 +31,7 @@ vi.mock('../../../../src/utils/init/utils.js', () => ({ describe('config-manual', () => { let mockApi: Partial - let mockCommand: any + let mockCommand: Pick beforeEach(() => { vi.clearAllMocks() @@ -38,11 +39,11 @@ describe('config-manual', () => { mockApi = {} mockCommand = { netlify: { - api: mockApi, - cachedConfig: { configPath: '/test/netlify.toml' }, - config: { plugins: [] }, + api: mockApi as NetlifyAPI, + cachedConfig: { configPath: '/test/netlify.toml' } as BaseCommand['netlify']['cachedConfig'], + config: { plugins: [] } as BaseCommand['netlify']['config'], repositoryRoot: '/test', - }, + } as BaseCommand['netlify'], } mockPrompt.mockResolvedValue({ @@ -78,19 +79,14 @@ describe('config-manual', () => { } await configManual({ - command: mockCommand, + command: mockCommand as BaseCommand, repoData, siteId: 'site-123', }) - expect(mockSetupSite).toHaveBeenCalledWith( - expect.objectContaining({ - repo: expect.objectContaining({ - provider: 'gitlab', - repo_path: 'ownername/test', - }), - }), - ) + const setupCall = mockSetupSite.mock.calls[0][0] as { repo: { provider: string; repo_path: string } } + expect(setupCall.repo.provider).toBe('gitlab') + expect(setupCall.repo.repo_path).toBe('ownername/test') }) it('should use repo path (owner/name format) instead of SSH URL for GitLab', async () => { @@ -107,12 +103,14 @@ describe('config-manual', () => { } await configManual({ - command: mockCommand, + command: mockCommand as BaseCommand, repoData, siteId: 'site-123', }) - const setupSiteCall = (mockSetupSite as Mock).mock.calls[0][0] + const setupSiteCall = mockSetupSite.mock.calls[0][0] as { + repo: { repo_path: string } + } expect(setupSiteCall.repo.repo_path).toBe('ownername/test') expect(setupSiteCall.repo.repo_path).not.toBe('git@gitlab.com:ownername/test.git') }) @@ -131,18 +129,13 @@ describe('config-manual', () => { } await configManual({ - command: mockCommand, + command: mockCommand as BaseCommand, repoData, siteId: 'site-123', }) - expect(mockSetupSite).toHaveBeenCalledWith( - expect.objectContaining({ - repo: expect.objectContaining({ - provider: 'manual', - }), - }), - ) + const setupCall = mockSetupSite.mock.calls[0][0] as { repo: { provider: string } } + expect(setupCall.repo.provider).toBe('manual') }) }) @@ -161,19 +154,14 @@ describe('config-manual', () => { } await configManual({ - command: mockCommand, + command: mockCommand as BaseCommand, repoData, siteId: 'site-123', }) - expect(mockSetupSite).toHaveBeenCalledWith( - expect.objectContaining({ - repo: expect.objectContaining({ - provider: 'github', - repo_path: 'user/test', - }), - }), - ) + const setupCall = mockSetupSite.mock.calls[0][0] as { repo: { provider: string; repo_path: string } } + expect(setupCall.repo.provider).toBe('github') + expect(setupCall.repo.repo_path).toBe('user/test') }) }) })