import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import {
DEFAULT_GITLAB_API_BASE_URL,
DEFAULT_GITLAB_INTEGRATION_NAME,
DEFAULT_GITLAB_SECRET_NAME,
GITLAB_OAUTH_CONNECT_URL,
GITLAB_PAT_SETUP_URL,
buildOauthConnectUrl,
buildPatSetupUrl,
resolveApiBaseUrl,
resolveGitlabAuth,
} from '../src/auth.ts'
import {
encodeProjectId,
normalizeIssueLocator,
normalizeMergeRequestLocator,
normalizePipelineLocator,
normalizeProjectLocator,
} from '../src/locators.ts'
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
test('default auth is OAuth integration gitlab', () => {
const auth = resolveGitlabAuth()
assert.equal(auth.mode, 'oauth')
assert.equal(auth.integrationName, DEFAULT_GITLAB_INTEGRATION_NAME)
assert.equal(auth.apiBaseUrl, DEFAULT_GITLAB_API_BASE_URL)
assert.equal(auth.apiHost, 'gitlab.com')
})
test('secretName selects the PAT lane and ignores integrationName', () => {
const auth = resolveGitlabAuth({
secretName: DEFAULT_GITLAB_SECRET_NAME,
integrationName: 'gitlab-work',
})
assert.equal(auth.mode, 'pat')
assert.equal(auth.secretName, DEFAULT_GITLAB_SECRET_NAME)
assert.equal(auth.integrationName, null)
assert.equal(auth.label, DEFAULT_GITLAB_SECRET_NAME)
})
test('multi-account OAuth uses gitlab-* integration names', () => {
const auth = resolveGitlabAuth({ integrationName: 'gitlab-work' })
assert.equal(auth.mode, 'oauth')
assert.equal(auth.integrationName, 'gitlab-work')
assert.equal(auth.label, 'gitlab-work')
})
test('account aliases integrationName', () => {
const auth = resolveGitlabAuth({ account: 'gitlab-bot' })
assert.equal(auth.integrationName, 'gitlab-bot')
})
test('self-hosted instanceUrl becomes /api/v4', () => {
assert.equal(
resolveApiBaseUrl({ instanceUrl: 'https://gitlab.example.com' }),
'https://gitlab.example.com/api/v4',
)
assert.equal(
resolveApiBaseUrl({ apiBaseUrl: 'https://gitlab.example.com/api/v4/' }),
'https://gitlab.example.com/api/v4',
)
})
test('rejects non-https API bases', () => {
assert.throws(() => resolveApiBaseUrl({ instanceUrl: 'http://gitlab.example.com' }), /https/)
})
test('connect URLs are prefilled and generic', () => {
assert.match(GITLAB_OAUTH_CONNECT_URL, /^https:\/\/kody\.codes\/connect\/oauth\?/)
assert.match(GITLAB_OAUTH_CONNECT_URL, /provider=gitlab/)
assert.match(GITLAB_OAUTH_CONNECT_URL, /authorizeUrl=https%3A%2F%2Fgitlab.com%2Foauth%2Fauthorize/)
assert.match(GITLAB_OAUTH_CONNECT_URL, /tokenUrl=https%3A%2F%2Fgitlab.com%2Foauth%2Ftoken/)
assert.match(GITLAB_OAUTH_CONNECT_URL, /allowedHosts=gitlab.com/)
assert.match(GITLAB_PAT_SETUP_URL, /^https:\/\/kody\.codes\/account\/secrets\/new\?/)
assert.match(GITLAB_PAT_SETUP_URL, /name=gitlabAccessToken/)
assert.match(GITLAB_PAT_SETUP_URL, /allowedHosts=gitlab.com/)
const work = buildOauthConnectUrl({ provider: 'gitlab-work' })
assert.match(work, /provider=gitlab-work/)
const selfHosted = buildOauthConnectUrl({
provider: 'gitlab',
instanceUrl: 'https://gitlab.example.com',
})
assert.match(selfHosted, /gitlab.example.com%2Foauth%2Fauthorize/)
assert.equal(
buildPatSetupUrl('gitlabAccessTokenWork', 'gitlab.example.com').includes(
'allowedHosts=gitlab.example.com',
),
true,
)
})
test('encodes namespace/project paths once', () => {
assert.equal(encodeProjectId(42), '42')
assert.equal(encodeProjectId('example-group/example-project'), 'example-group%2Fexample-project')
assert.equal(encodeProjectId('example-group%2Fexample-project'), 'example-group%2Fexample-project')
})
test('project locators accept id, path, and URL', () => {
assert.deepEqual(normalizeProjectLocator({ project: 99 }), {
projectId: '99',
projectPath: '99',
})
assert.deepEqual(normalizeProjectLocator({ project: 'example-group/nested/example-project' }), {
projectId: 'example-group%2Fnested%2Fexample-project',
projectPath: 'example-group/nested/example-project',
})
assert.deepEqual(
normalizeProjectLocator({
projectUrl: 'https://gitlab.com/example-group/example-project/-/issues/3',
}),
{
projectId: 'example-group%2Fexample-project',
projectPath: 'example-group/example-project',
},
)
})
test('issue, merge request, and pipeline locators parse GitLab URLs', () => {
assert.deepEqual(
normalizeIssueLocator({
issueUrl: 'https://gitlab.com/example-group/nested/example-project/-/issues/12',
}),
{
projectId: 'example-group%2Fnested%2Fexample-project',
projectPath: 'example-group/nested/example-project',
issueIid: 12,
},
)
assert.deepEqual(
normalizeMergeRequestLocator({
project: 'example-group/example-project',
mrIid: 4,
}),
{
projectId: 'example-group%2Fexample-project',
projectPath: 'example-group/example-project',
mergeRequestIid: 4,
},
)
assert.deepEqual(
normalizePipelineLocator({
pipelineUrl: 'https://gitlab.example.com/example-group/example-project/-/pipelines/88',
}),
{
projectId: 'example-group%2Fexample-project',
projectPath: 'example-group/example-project',
pipelineId: 88,
},
)
})
test('rejects GitHub URLs', () => {
assert.throws(
() =>
normalizeProjectLocator({
projectUrl: 'https://github.com/example-org/example',
}),
/not GitHub/,
)
})
test('source and docs stay generic', () => {
const files = [
'README.md',
'package.json',
'src/index.ts',
'src/auth.ts',
'src/locators.ts',
'src/issues/create.ts',
'src/merge-requests/create.ts',
]
for (const file of files) {
const text = readFileSync(join(root, file), 'utf8')
assert.doesNotMatch(text, /kentcdodds/i)
assert.doesNotMatch(text, /kent-explicit/)
assert.doesNotMatch(text, /epicweb/i)
assert.doesNotMatch(text, /\/community\/[0-9a-f-]{36}/)
}
const readme = readFileSync(join(root, 'README.md'), 'utf8')
assert.match(readme, /^## Intent$/m)
assert.match(readme, /https:\/\/kody\.codes\/@kody\/gitlab/)
assert.match(readme, /https:\/\/kody\.codes\/connect\/oauth\?provider=gitlab/)
assert.match(readme, /https:\/\/kody\.codes\/account\/secrets\/new\?name=gitlabAccessToken/)
})