Skip to content
Open
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
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/utils/init/config-manual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ export default async function configManual({
const deployKey = await createDeployKey({ api })
await addDeployKey(deployKey)

const repoPath = await getRepoPath({ repoData })
const repoPath = repoData.repo ?? (await getRepoPath({ repoData }))
const repo = {
provider: 'manual',
provider: repoData.provider ?? 'manual',
repo_path: repoPath,
repo_branch: repoData.branch,
allowed_branches: [repoData.branch],
Expand Down
120 changes: 120 additions & 0 deletions tests/unit/utils/get-repo-data.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
Comment on lines +8 to +119
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Tests are self-fulfilling; exercise getRepoData instead.

These cases construct RepoData literals and assert the same values, so they don’t validate parsing/normalization behavior. Consider driving the tests through getRepoData (with mocked repo info) and asserting its output to catch regressions.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/utils/get-repo-data.test.ts` around lines 8 - 119, The tests are
self-fulfilling because they construct RepoData literals and assert those same
values; update each spec to call the real getRepoData function instead of
building RepoData objects so parsing/normalization is exercised. Replace the
mockRepoData literals with calls to getRepoData(...) (or the exported function
that returns RepoData) using the raw git URL/branch/name inputs from each case
(e.g., 'git@github.com:ownername/test.git', 'https://gitlab.com/...', custom
host, etc.), then assert on returnedRepo.httpsUrl, returnedRepo.provider and
returnedRepo.repo to validate behavior. Ensure you import getRepoData and keep
the same expected strings in assertions.

})
167 changes: 167 additions & 0 deletions tests/unit/utils/init/config-manual.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
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()
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<NetlifyAPI>
let mockCommand: Pick<BaseCommand, 'netlify'>

beforeEach(() => {
vi.clearAllMocks()

mockApi = {}
mockCommand = {
netlify: {
api: mockApi as NetlifyAPI,
cachedConfig: { configPath: '/test/netlify.toml' } as BaseCommand['netlify']['cachedConfig'],
config: { plugins: [] } as BaseCommand['netlify']['config'],

Check failure on line 44 in tests/unit/utils/init/config-manual.test.ts

View workflow job for this annotation

GitHub Actions / typecheck

Conversion of type '{ plugins: never[]; }' to type 'NormalizedCachedConfigConfig' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
repositoryRoot: '/test',
Comment on lines +41 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "(config-manual\.test\.ts|base-command\.ts)"

Repository: netlify/cli

Length of output: 127


🏁 Script executed:

cat -n tests/unit/utils/init/config-manual.test.ts | sed -n '35,50p'

Repository: netlify/cli

Length of output: 608


🏁 Script executed:

head -100 src/commands/base-command.ts | cat -n

Repository: netlify/cli

Length of output: 4256


🏁 Script executed:

cat -n src/commands/base-command.ts | sed -n '100,250p'

Repository: netlify/cli

Length of output: 6552


🏁 Script executed:

cat -n src/commands/types.ts

Repository: netlify/cli

Length of output: 108


🏁 Script executed:

rg -t ts "type NetlifyOptions|interface NetlifyOptions" --max-count 5

Repository: netlify/cli

Length of output: 109


🏁 Script executed:

cat -n src/commands/types.d.ts

Repository: netlify/cli

Length of output: 1910


🏁 Script executed:

rg -t ts "type NormalizedCachedConfigConfig|interface NormalizedCachedConfigConfig" -A 15

Repository: netlify/cli

Length of output: 14867


🏁 Script executed:

rg -t ts "type CachedConfig|interface CachedConfig" src/lib/build.js -A 20

Repository: netlify/cli

Length of output: 111


🏁 Script executed:

fd build src/lib -t f | head -10

Repository: netlify/cli

Length of output: 71


🏁 Script executed:

rg "type CachedConfig|interface CachedConfig" --max-count 5

Repository: netlify/cli

Length of output: 154


🏁 Script executed:

rg "export interface CachedConfig" -A 30 src/lib/build.ts

Repository: netlify/cli

Length of output: 1068


🏁 Script executed:

rg "export interface CachedConfig" -A 60 src/lib/build.ts | head -80

Repository: netlify/cli

Length of output: 2080


🏁 Script executed:

rg "export interface CachedConfig" -A 150 src/lib/build.ts | grep -A 5 "plugins"

Repository: netlify/cli

Length of output: 499


🏁 Script executed:

cat -n tests/unit/utils/init/config-manual.test.ts | head -80

Repository: netlify/cli

Length of output: 3052


🏁 Script executed:

cat tsconfig.json | head -50

Repository: netlify/cli

Length of output: 94


🏁 Script executed:

cat tsconfig.base.json | head -80

Repository: netlify/cli

Length of output: 403


🏁 Script executed:

cd /tmp && cat > check_types.ts << 'EOF'
import type { CachedConfig } from '/repo/src/lib/build'
import type BaseCommand from '/repo/src/commands/base-command'

// This is what the test is trying to do
const testConfig: BaseCommand['netlify']['config'] = { plugins: [] }
EOF
npx tsc --noEmit check_types.ts 2>&1

Repository: netlify/cli

Length of output: 300


🏁 Script executed:

rg "export interface CachedConfig" -A 200 src/lib/build.ts | tail -50

Repository: netlify/cli

Length of output: 1462


Fix the failing type assertion on config.

Line 44 attempts an invalid direct cast. The config property requires a CachedConfig['config'] object with required properties like build, but only provides { plugins: [] }. Use a two-step cast through unknown:

Proposed fix
-        config: { plugins: [] } as BaseCommand['netlify']['config'],
+        config: { plugins: [] } as unknown as BaseCommand['netlify']['config'],
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
netlify: {
api: mockApi as NetlifyAPI,
cachedConfig: { configPath: '/test/netlify.toml' } as BaseCommand['netlify']['cachedConfig'],
config: { plugins: [] } as BaseCommand['netlify']['config'],
repositoryRoot: '/test',
netlify: {
api: mockApi as NetlifyAPI,
cachedConfig: { configPath: '/test/netlify.toml' } as BaseCommand['netlify']['cachedConfig'],
config: { plugins: [] } as unknown as BaseCommand['netlify']['config'],
repositoryRoot: '/test',
🧰 Tools
πŸͺ› GitHub Actions: Typecheck

[error] 44-44: TypeScript error TS2352: Conversion of type '{ plugins: never[]; }' to type 'NormalizedCachedConfigConfig' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. (During npm run typecheck/tsc)

πŸͺ› GitHub Check: typecheck

[failure] 44-44:
Conversion of type '{ plugins: never[]; }' to type 'NormalizedCachedConfigConfig' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/utils/init/config-manual.test.ts` around lines 41 - 45, The test
is failing due to an unsafe direct cast of the Netlify `config` property; locate
the object literal assigned to `netlify.config` in
tests/unit/utils/init/config-manual.test.ts and change the cast so the provided
`{ plugins: [] }` is first cast to `unknown` and then to the required
`CachedConfig['config']` (or BaseCommand['netlify']['config']) type instead of a
direct cast; this preserves the intended test shape while satisfying
TypeScript's structural checks for `build` and other required properties
referenced by `CachedConfig['config']`.

} as BaseCommand['netlify'],
}

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 as BaseCommand,
repoData,
siteId: 'site-123',
})

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 () => {
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 as BaseCommand,
repoData,
siteId: 'site-123',
})

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')
})

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 as BaseCommand,
repoData,
siteId: 'site-123',
})

const setupCall = mockSetupSite.mock.calls[0][0] as { repo: { provider: string } }
expect(setupCall.repo.provider).toBe('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 as BaseCommand,
repoData,
siteId: 'site-123',
})

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')
})
})
})
Loading