fastify
Version:
Fast and low overhead web framework, for Node.js
323 lines (259 loc) • 9.81 kB
JavaScript
'use strict'
const { describe, it, beforeEach, afterEach } = require('node:test')
const assert = require('node:assert')
const fs = require('node:fs')
const { MockAgent, setGlobalDispatcher, getGlobalDispatcher } = require('undici')
function loadValidateEcosystemLinksModule () {
const modulePath = require.resolve('../../scripts/validate-ecosystem-links')
delete require.cache[modulePath]
return require(modulePath)
}
describe('extractGitHubLinks', () => {
const { extractGitHubLinks } = loadValidateEcosystemLinksModule()
it('extracts simple GitHub repository links', () => {
const content = `
# Ecosystem
- [fastify-helmet](https://github.com/fastify/fastify-helmet) - Important security headers for Fastify
- [fastify-cors](https://github.com/fastify/fastify-cors) - CORS support
`
const links = extractGitHubLinks(content)
assert.strictEqual(links.length, 2)
assert.deepStrictEqual(links[0], {
name: 'fastify-helmet',
url: 'https://github.com/fastify/fastify-helmet',
owner: 'fastify',
repo: 'fastify-helmet'
})
assert.deepStrictEqual(links[1], {
name: 'fastify-cors',
url: 'https://github.com/fastify/fastify-cors',
owner: 'fastify',
repo: 'fastify-cors'
})
})
it('extracts links with different owner/repo combinations', () => {
const content = `
- [some-plugin](https://github.com/user123/awesome-plugin)
- [another-lib](https://github.com/org-name/lib-name)
`
const links = extractGitHubLinks(content)
assert.strictEqual(links.length, 2)
assert.strictEqual(links[0].owner, 'user123')
assert.strictEqual(links[0].repo, 'awesome-plugin')
assert.strictEqual(links[1].owner, 'org-name')
assert.strictEqual(links[1].repo, 'lib-name')
})
it('handles links with hash fragments', () => {
const content = `
- [project](https://github.com/owner/repo#readme)
`
const links = extractGitHubLinks(content)
assert.strictEqual(links.length, 1)
assert.strictEqual(links[0].repo, 'repo')
assert.strictEqual(links[0].url, 'https://github.com/owner/repo#readme')
})
it('handles links with query parameters', () => {
const content = `
- [project](https://github.com/owner/repo?tab=readme)
`
const links = extractGitHubLinks(content)
assert.strictEqual(links.length, 1)
assert.strictEqual(links[0].repo, 'repo')
})
it('handles links with subpaths', () => {
const content = `
- [docs](https://github.com/owner/repo/tree/main/docs)
`
const links = extractGitHubLinks(content)
assert.strictEqual(links.length, 1)
assert.strictEqual(links[0].owner, 'owner')
assert.strictEqual(links[0].repo, 'repo')
})
it('returns empty array for content with no GitHub links', () => {
const content = `
# No GitHub links here
Just some regular text and [a link](https://example.com).
`
const links = extractGitHubLinks(content)
assert.strictEqual(links.length, 0)
})
it('ignores non-GitHub links', () => {
const content = `
- [gitlab](https://gitlab.com/owner/repo)
- [github](https://github.com/owner/repo)
- [bitbucket](https://bitbucket.org/owner/repo)
`
const links = extractGitHubLinks(content)
assert.strictEqual(links.length, 1)
assert.strictEqual(links[0].owner, 'owner')
})
it('extracts multiple links from complex markdown', () => {
const content = `
## Category 1
Some description [inline link](https://github.com/a/b).
| Plugin | Description |
|--------|-------------|
| [plugin1](https://github.com/x/y) | Desc 1 |
| [plugin2](https://github.com/z/w) | Desc 2 |
`
const links = extractGitHubLinks(content)
assert.strictEqual(links.length, 3)
})
})
describe('checkGitHubRepo', () => {
let originalDispatcher
let mockAgent
let originalSetTimeout
beforeEach(() => {
delete process.env.GITHUB_TOKEN
originalDispatcher = getGlobalDispatcher()
mockAgent = new MockAgent()
mockAgent.disableNetConnect()
setGlobalDispatcher(mockAgent)
originalSetTimeout = global.setTimeout
})
afterEach(async () => {
global.setTimeout = originalSetTimeout
setGlobalDispatcher(originalDispatcher)
await mockAgent.close()
delete process.env.GITHUB_TOKEN
})
it('returns exists: true for status 200', async () => {
const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
const mockPool = mockAgent.get('https://api.github.com')
mockPool.intercept({
path: '/repos/fastify/fastify',
method: 'HEAD'
}).reply(200)
const result = await checkGitHubRepo('fastify', 'fastify')
assert.strictEqual(result.exists, true)
assert.strictEqual(result.status, 200)
assert.strictEqual(result.owner, 'fastify')
assert.strictEqual(result.repo, 'fastify')
})
it('returns exists: false for status 404', async () => {
const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
const mockPool = mockAgent.get('https://api.github.com')
mockPool.intercept({
path: '/repos/nonexistent/repo',
method: 'HEAD'
}).reply(404)
const result = await checkGitHubRepo('nonexistent', 'repo')
assert.strictEqual(result.exists, false)
assert.strictEqual(result.status, 404)
})
it('returns invalid status for malformed owner or repository names', async () => {
const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
const result = await checkGitHubRepo('owner/evil', 'repo', 1)
assert.strictEqual(result.exists, false)
assert.strictEqual(result.status, 'invalid')
assert.strictEqual(result.error, 'Invalid GitHub repository identifier')
})
it('retries on rate limit responses', async () => {
const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
let attempts = 0
global.setTimeout = (fn) => { fn(); return 0 }
const mockPool = mockAgent.get('https://api.github.com')
mockPool.intercept({
path: '/repos/owner/repo',
method: 'HEAD'
}).reply(() => { attempts++; return { statusCode: 403 } })
mockPool.intercept({
path: '/repos/owner/repo',
method: 'HEAD'
}).reply(() => { attempts++; return { statusCode: 200 } })
const result = await checkGitHubRepo('owner', 'repo', 1)
assert.strictEqual(attempts, 2)
assert.strictEqual(result.exists, true)
assert.strictEqual(result.status, 200)
})
it('adds authorization header when GITHUB_TOKEN is set', async () => {
process.env.GITHUB_TOKEN = 'my-token'
const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
const mockPool = mockAgent.get('https://api.github.com')
mockPool.intercept({
path: '/repos/owner/repo',
method: 'HEAD',
headers: {
authorization: 'token my-token'
}
}).reply(200)
const result = await checkGitHubRepo('owner', 'repo')
assert.strictEqual(result.exists, true)
assert.strictEqual(result.status, 200)
})
it('handles network errors', async () => {
const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
const mockPool = mockAgent.get('https://api.github.com')
mockPool.intercept({
path: '/repos/owner/repo',
method: 'HEAD'
}).replyWithError(new Error('Network error'))
const result = await checkGitHubRepo('owner', 'repo')
assert.strictEqual(result.exists, false)
assert.strictEqual(result.status, 'error')
assert.ok(result.error.length > 0)
})
})
describe('validateAllLinks', () => {
let originalReadFileSync
let originalSetTimeout
let originalConsoleLog
let originalStdoutWrite
let mockAgent
let originalDispatcher
beforeEach(() => {
originalReadFileSync = fs.readFileSync
originalSetTimeout = global.setTimeout
originalConsoleLog = console.log
originalStdoutWrite = process.stdout.write
originalDispatcher = getGlobalDispatcher()
mockAgent = new MockAgent()
mockAgent.disableNetConnect()
setGlobalDispatcher(mockAgent)
console.log = () => {}
process.stdout.write = () => true
global.setTimeout = (fn) => { fn(); return 0 }
})
afterEach(async () => {
fs.readFileSync = originalReadFileSync
global.setTimeout = originalSetTimeout
console.log = originalConsoleLog
process.stdout.write = originalStdoutWrite
setGlobalDispatcher(originalDispatcher)
await mockAgent.close()
})
it('validates links, deduplicates repositories and groups inaccessible links', async () => {
const { validateAllLinks } = loadValidateEcosystemLinksModule()
let requests = 0
fs.readFileSync = () => `
- [repo one](https://github.com/owner/repo)
- [repo one duplicate](https://github.com/owner/repo)
- [repo two](https://github.com/another/project)
`
const mockPool = mockAgent.get('https://api.github.com')
mockPool.intercept({
path: '/repos/owner/repo',
method: 'HEAD'
}).reply(() => { requests++; return { statusCode: 404 } })
mockPool.intercept({
path: '/repos/another/project',
method: 'HEAD'
}).reply(() => { requests++; return { statusCode: 200 } })
const result = await validateAllLinks()
assert.strictEqual(requests, 2)
assert.strictEqual(result.notFound.length, 1)
assert.strictEqual(result.found.length, 1)
assert.strictEqual(result.notFound[0].owner, 'owner')
assert.strictEqual(result.notFound[0].repo, 'repo')
assert.strictEqual(result.found[0].owner, 'another')
assert.strictEqual(result.found[0].repo, 'project')
})
it('returns empty result when no GitHub links are present', async () => {
const { validateAllLinks } = loadValidateEcosystemLinksModule()
fs.readFileSync = () => '# Ecosystem\nNo links here.'
const result = await validateAllLinks()
assert.strictEqual(result.notFound.length, 0)
assert.strictEqual(result.found.length, 0)
})
})