Some checks failed
API CI / API Lint (push) Successful in 47s
API CI / API Type Check (push) Failing after 47s
API CI / API Test (push) Successful in 1m0s
API CI / API Build (push) Failing after 50s
API CI / Build Docker Image (push) Has been skipped
Build Crossplane Provider / build (push) Failing after 5m51s
CD Pipeline / Deploy to Staging (push) Failing after 29s
CI Pipeline / Lint and Type Check (push) Failing after 36s
CI Pipeline / Build (push) Has been skipped
CI Pipeline / Test Backend (push) Failing after 1m33s
CI Pipeline / Test Frontend (push) Failing after 30s
CI Pipeline / Security Scan (push) Failing after 1m16s
Crossplane Provider CI / Go Test (push) Failing after 3m23s
Crossplane Provider CI / Go Lint (push) Failing after 7m27s
Crossplane Provider CI / Go Build (push) Failing after 3m27s
Deploy to Staging / Deploy to Staging (push) Failing after 30s
Portal CI / Portal Lint (push) Failing after 21s
Portal CI / Portal Type Check (push) Failing after 21s
Portal CI / Portal Test (push) Failing after 21s
Portal CI / Portal Build (push) Failing after 22s
Test Suite / frontend-tests (push) Failing after 30s
Test Suite / api-tests (push) Failing after 49s
Test Suite / blockchain-tests (push) Failing after 30s
Type Check / type-check (map[directory:. name:root]) (push) Failing after 23s
Type Check / type-check (map[directory:api name:api]) (push) Failing after 21s
Type Check / type-check (map[directory:portal name:portal]) (push) Failing after 19s
Validate Configuration Files / validate (push) Failing after 1m52s
CD Pipeline / Deploy to Production (push) Has been skipped
Co-authored-by: Cursor <cursoragent@cursor.com>
313 lines
9.5 KiB
TypeScript
313 lines
9.5 KiB
TypeScript
/**
|
|
* Unit tests for scan-projects.ts
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { ProjectScanner, ProjectInfo } from '../scan-projects'
|
|
import { z } from 'zod'
|
|
|
|
// Mock dependencies
|
|
vi.mock('fs/promises')
|
|
vi.mock('fs')
|
|
vi.mock('child_process')
|
|
vi.mock('cli-progress')
|
|
vi.mock('dotenv')
|
|
|
|
describe('ProjectScanner', () => {
|
|
const mockOptions = {
|
|
projectsDir: '/test/projects',
|
|
dryRun: false,
|
|
skipExisting: false,
|
|
apiUrl: 'http://localhost:4000/graphql',
|
|
email: 'test@example.com',
|
|
password: 'password123',
|
|
outputFormat: 'text' as const,
|
|
verbose: false,
|
|
batchSize: 1,
|
|
}
|
|
|
|
describe('validateProjectName', () => {
|
|
it('should validate and sanitize valid project names', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
const testCases = [
|
|
{ input: 'my-project', expected: 'my-project' },
|
|
{ input: 'my_project', expected: 'my_project' },
|
|
{ input: 'myProject123', expected: 'myProject123' },
|
|
{ input: 'my-project_123', expected: 'my-project_123' },
|
|
]
|
|
|
|
testCases.forEach(({ input, expected }) => {
|
|
const result = (scanner as any).validateProjectName(input)
|
|
expect(result).toBe(expected)
|
|
})
|
|
})
|
|
|
|
it('should sanitize invalid characters', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
const testCases = [
|
|
{ input: 'my project', expected: 'my-project' },
|
|
{ input: 'my@project', expected: 'my-project' },
|
|
{ input: 'my.project', expected: 'my-project' },
|
|
{ input: 'my---project', expected: 'my-project' },
|
|
{ input: '-my-project-', expected: 'my-project' },
|
|
]
|
|
|
|
testCases.forEach(({ input, expected }) => {
|
|
const result = (scanner as any).validateProjectName(input)
|
|
expect(result).toBe(expected)
|
|
})
|
|
})
|
|
|
|
it('should truncate long names', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
const longName = 'a'.repeat(300)
|
|
const result = (scanner as any).validateProjectName(longName)
|
|
expect(result.length).toBeLessThanOrEqual(255)
|
|
})
|
|
|
|
it('should handle empty names', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
const result = (scanner as any).validateProjectName('')
|
|
expect(result).toBe('project')
|
|
})
|
|
})
|
|
|
|
describe('sanitizeMetadata', () => {
|
|
it('should sanitize metadata and remove invalid types', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
const metadata = {
|
|
string: 'value',
|
|
number: 123,
|
|
boolean: true,
|
|
null: null,
|
|
array: [1, 2, 3],
|
|
object: { nested: 'value' },
|
|
function: () => {}, // Should be removed
|
|
undefined: undefined, // Should be removed
|
|
}
|
|
|
|
const result = (scanner as any).sanitizeMetadata(metadata)
|
|
expect(result.string).toBe('value')
|
|
expect(result.number).toBe(123)
|
|
expect(result.boolean).toBe(true)
|
|
expect(result.null).toBeNull()
|
|
expect(Array.isArray(result.array)).toBe(true)
|
|
expect(typeof result.object).toBe('object')
|
|
expect(result.function).toBeUndefined()
|
|
expect(result.undefined).toBeUndefined()
|
|
})
|
|
|
|
it('should enforce size limits', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
const largeMetadata = {
|
|
description: 'x'.repeat(15000), // Too large
|
|
version: '1.0.0',
|
|
}
|
|
|
|
expect(() => {
|
|
(scanner as any).sanitizeMetadata(largeMetadata)
|
|
}).toThrow('Metadata too large')
|
|
})
|
|
})
|
|
|
|
describe('extractDomainFromGitRemote', () => {
|
|
it('should extract domain from HTTPS URLs', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
const testCases = [
|
|
{
|
|
input: 'https://github.com/user/repo.git',
|
|
expected: 'github-com.dev',
|
|
},
|
|
{
|
|
input: 'https://gitlab.com/user/repo.git',
|
|
expected: 'gitlab-com.dev',
|
|
},
|
|
{
|
|
input: 'https://bitbucket.org/user/repo.git',
|
|
expected: 'bitbucket-org.dev',
|
|
},
|
|
]
|
|
|
|
testCases.forEach(({ input, expected }) => {
|
|
const result = (scanner as any).extractDomainFromGitRemote(input)
|
|
expect(result).toBe(expected)
|
|
})
|
|
})
|
|
|
|
it('should extract domain from SSH URLs', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
const testCases = [
|
|
{
|
|
input: 'git@github.com:user/repo.git',
|
|
expected: 'github-com.dev',
|
|
},
|
|
{
|
|
input: 'ssh://git@gitlab.com:user/repo.git',
|
|
expected: 'gitlab-com.dev',
|
|
},
|
|
]
|
|
|
|
testCases.forEach(({ input, expected }) => {
|
|
const result = (scanner as any).extractDomainFromGitRemote(input)
|
|
expect(result).toBe(expected)
|
|
})
|
|
})
|
|
|
|
it('should return undefined for invalid URLs', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
const result = (scanner as any).extractDomainFromGitRemote('invalid-url')
|
|
expect(result).toBeUndefined()
|
|
})
|
|
|
|
it('should return undefined for empty input', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
const result = (scanner as any).extractDomainFromGitRemote(undefined)
|
|
expect(result).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('detectPackageManager', () => {
|
|
it('should detect pnpm from pnpm-lock.yaml', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
const { existsSync } = require('fs')
|
|
vi.mocked(existsSync).mockImplementation((path: string) => {
|
|
return path.includes('pnpm-lock.yaml')
|
|
})
|
|
|
|
const result = (scanner as any).detectPackageManager('/test/project')
|
|
expect(result).toBe('pnpm')
|
|
})
|
|
|
|
it('should detect yarn from yarn.lock', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
const { existsSync } = require('fs')
|
|
vi.mocked(existsSync).mockImplementation((path: string) => {
|
|
return path.includes('yarn.lock')
|
|
})
|
|
|
|
const result = (scanner as any).detectPackageManager('/test/project')
|
|
expect(result).toBe('yarn')
|
|
})
|
|
|
|
it('should detect npm from package-lock.json', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
const { existsSync } = require('fs')
|
|
vi.mocked(existsSync).mockImplementation((path: string) => {
|
|
return path.includes('package-lock.json')
|
|
})
|
|
|
|
const result = (scanner as any).detectPackageManager('/test/project')
|
|
expect(result).toBe('npm')
|
|
})
|
|
|
|
it('should return unknown if no lock file found', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
const { existsSync } = require('fs')
|
|
vi.mocked(existsSync).mockReturnValue(false)
|
|
|
|
const result = (scanner as any).detectPackageManager('/test/project')
|
|
expect(result).toBe('unknown')
|
|
})
|
|
})
|
|
|
|
describe('isDuplicate', () => {
|
|
it('should detect duplicates by name', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
;(scanner as any).existingTenants = new Map([
|
|
['my-project', { id: '123', name: 'my-project' }],
|
|
])
|
|
|
|
const project: ProjectInfo = {
|
|
name: 'my-project',
|
|
path: '/test',
|
|
type: 'repo',
|
|
metadata: {},
|
|
}
|
|
|
|
const result = (scanner as any).isDuplicate(project)
|
|
expect(result).not.toBeNull()
|
|
expect(result.name).toBe('my-project')
|
|
})
|
|
|
|
it('should detect duplicates by domain', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
;(scanner as any).existingTenants = new Map([
|
|
['github-com.dev', { id: '123', name: 'existing', domain: 'github-com.dev' }],
|
|
])
|
|
|
|
const project: ProjectInfo = {
|
|
name: 'new-project',
|
|
path: '/test',
|
|
type: 'repo',
|
|
gitRemote: 'https://github.com/user/repo.git',
|
|
metadata: {},
|
|
}
|
|
|
|
const result = (scanner as any).isDuplicate(project)
|
|
expect(result).not.toBeNull()
|
|
})
|
|
|
|
it('should return null for non-duplicates', () => {
|
|
const scanner = new ProjectScanner(mockOptions)
|
|
;(scanner as any).existingTenants = new Map()
|
|
|
|
const project: ProjectInfo = {
|
|
name: 'new-project',
|
|
path: '/test',
|
|
type: 'repo',
|
|
metadata: {},
|
|
}
|
|
|
|
const result = (scanner as any).isDuplicate(project)
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Validation Schemas', () => {
|
|
describe('projectNameSchema', () => {
|
|
it('should accept valid project names', () => {
|
|
const validNames = ['my-project', 'my_project', 'myProject123', 'my-project_123']
|
|
validNames.forEach((name) => {
|
|
expect(() => {
|
|
z.string().min(1).max(255).regex(/^[a-zA-Z0-9_-]+$/).parse(name)
|
|
}).not.toThrow()
|
|
})
|
|
})
|
|
|
|
it('should reject invalid project names', () => {
|
|
const invalidNames = ['my project', 'my@project', 'my.project', 'my-project!']
|
|
invalidNames.forEach((name) => {
|
|
expect(() => {
|
|
z.string().min(1).max(255).regex(/^[a-zA-Z0-9_-]+$/).parse(name)
|
|
}).toThrow()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('gitUrlSchema', () => {
|
|
it('should accept valid Git URLs', () => {
|
|
const validUrls = [
|
|
'https://github.com/user/repo.git',
|
|
'git@github.com:user/repo.git',
|
|
'ssh://git@gitlab.com:user/repo.git',
|
|
]
|
|
validUrls.forEach((url) => {
|
|
expect(() => {
|
|
z.string().url().parse(url)
|
|
}).not.toThrow()
|
|
})
|
|
})
|
|
|
|
it('should reject invalid URLs', () => {
|
|
const invalidUrls = ['not-a-url', 'http://', 'git@']
|
|
invalidUrls.forEach((url) => {
|
|
expect(() => {
|
|
z.string().url().parse(url)
|
|
}).toThrow()
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|