Skip to content

Commit

Permalink
feat: add CLI and project addition commands
Browse files Browse the repository at this point in the history
  • Loading branch information
UlisesGascon committed Dec 3, 2024
1 parent 480199b commit 0088847
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 0 deletions.
134 changes: 134 additions & 0 deletions __tests__/cli.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
const inquirer = require('inquirer').default
const knexInit = require('knex')
const { getConfig } = require('../src/config/')
const { runAddProjectCommand } = require('../src/cli')
const { resetDatabase, getAllProjects } = require('./utils')

const { dbSettings } = getConfig('test')

// Mock inquirer for testing
jest.spyOn(inquirer, 'prompt').mockImplementation(async (questions) => {
const questionMap = {
'What is the name of the project?': 'eslint',
'Enter the GitHub URLs (comma-separated):': 'https://github.com/eslint',
'Select a category:': 'impact'
}
return questions.reduce((acc, question) => {
acc[question.name] = questionMap[question.message]
return acc
}, {})
})

let knex

beforeAll(() => {
knex = knexInit(dbSettings)
})
beforeEach(() => resetDatabase(knex))
afterEach(jest.clearAllMocks)
afterAll(async () => {
await resetDatabase(knex)
await knex.destroy()
})

describe('Interactive Mode', () => {
test('Add a project with name, GitHub URLs, and category', async () => {
let projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
await runAddProjectCommand(knex, {})
projects = await getAllProjects(knex)
expect(projects.length).toBe(1)
expect(projects[0].name).toBe('eslint')
expect(projects[0].category).toBe('impact')
//@TODO: Add test for githubUrls when it is implemented
})

test('Prevent to add a project that already exists', async () => {
let projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
await runAddProjectCommand(knex, {})
projects = await getAllProjects(knex)
expect(projects.length).toBe(1)
await expect(runAddProjectCommand(knex, {}))
.rejects
.toThrow('Project with name eslint already exists')
projects = await getAllProjects(knex)
expect(projects.length).toBe(1)
})
})

describe('Non-Interactive Mode', () => {
test('Add a project with name, GitHub URLs, and category', async () => {
let projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
await runAddProjectCommand(knex, { name: 'eslint', githubUrls: ['https://github.com/eslint'], category: 'impact' })
projects = await getAllProjects(knex)
expect(projects.length).toBe(1)
expect(projects[0].name).toBe('eslint')
expect(projects[0].category).toBe('impact')
//@TODO: Add test for githubUrls when it is implemented
})

test('Prevent to add a project that already exists', async () => {
let projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
await runAddProjectCommand(knex, { name: 'eslint', githubUrls: ['https://github.com/eslint'], category: 'impact' })
projects = await getAllProjects(knex)
expect(projects.length).toBe(1)
await expect(runAddProjectCommand(knex, { name: 'eslint', githubUrls: ['https://github.com/eslint'], category: 'impact' }))
.rejects
.toThrow('Project with name eslint already exists')
projects = await getAllProjects(knex)
expect(projects.length).toBe(1)
})

test('Error when no name is provided', async () => {
let projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
await expect(runAddProjectCommand(knex, { githubUrls: ['https://github.com/eslint'], category: 'impact' }))
.rejects
.toThrow('Project name is required')
projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
})

test('Error when no GitHub URLs are provided', async () => {
let projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
await expect(runAddProjectCommand(knex, { name: 'eslint', category: 'impact' }))
.rejects
.toThrow('GitHub URLs are required')
projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
})

test('Error when no category is provided', async () => {
let projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
await expect(runAddProjectCommand(knex, { name: 'eslint', githubUrls: ['https://github.com/eslint'] }))
.rejects
.toThrow('Category is required')
projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
})

test('Error when invalid category is provided', async () => {
let projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
await expect(runAddProjectCommand(knex, { name: 'eslint', githubUrls: ['https://github.com/eslint'], category: 'invalid' }))
.rejects
.toThrow('Invalid category, use one of the following')
projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
})

test('Error when invalid GitHub URLs are provided', async () => {
let projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
await expect(runAddProjectCommand(knex, { name: 'eslint', githubUrls: ['invalid-url'], category: 'impact' }))
.rejects
.toThrow('Invalid URL: invalid-url. Please enter valid GitHub URLs.')
projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
})
})
7 changes: 7 additions & 0 deletions __tests__/utils/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const resetDatabase = (knex) => knex('projects').del()
const getAllProjects = (knex) => knex('projects').select('*')

module.exports = {
resetDatabase,
getAllProjects
}
3 changes: 3 additions & 0 deletions src/cli/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const project = require('./project')

module.exports = project
109 changes: 109 additions & 0 deletions src/cli/project.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const inquirer = require('inquirer').default
const { stringToArray } = require('@ulisesgascon/string-to-array')
const isSlug = require('validator/lib/isSlug.js')
const debug = require('debug')('cli')
const { getConfig } = require('../config')
const { validateGithubUrl } = require('../utils')
const { initializeStore } = require('../store')

const { projectCategories } = getConfig()

async function runAddProjectCommand (knex, options = {}) {
const { addProject } = initializeStore(knex)

if (Object.keys(options).length > 0) {
if (!options.name) {
throw new Error('Project name is required')
}

if (!options.githubUrls?.length) {
throw new Error('GitHub URLs are required')
}

if (!options.category) {
throw new Error('Category is required')
}

if (!projectCategories.includes(options.category)) {
throw new Error(`Invalid category, use one of the following: ${projectCategories.join(', ')}`)
}

if (options.githubUrls) {
const urls = options.githubUrls
if (urls.length === 0) {
throw new Error('At least one GitHub URL is required.')
}
for (const url of urls) {
if (!validateGithubUrl(url)) {
throw new Error(`Invalid URL: ${url}. Please enter valid GitHub URLs.`)
}
}
}
}

const answers = options.name && options.githubUrls && options.category
? options
: await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'What is the name of the project?',
transformer: (input) => input.toLowerCase(),
validate: (input) => {
if (!isSlug(input)) {
return 'Invalid project name. Please enter a valid slug.'
}
return true
},
when: () => !options.name
},
{
type: 'input',
name: 'githubUrls',
message: 'Enter the GitHub URLs (comma-separated):',
filter: (input) => stringToArray(input),
transformer: (input) => input.toLowerCase(),
validate: (input) => {
const urls = stringToArray(input)
if (urls.length === 0) {
return 'At least one GitHub URL is required.'
}
for (const url of urls) {
if (!validateGithubUrl(url)) {
return `Invalid URL: ${url}. Please enter valid GitHub URLs.`
}
}
return true
},
when: () => !options.githubUrls
},
{
type: 'list',
name: 'category',
message: 'Select a category:',
choices: projectCategories,
when: () => !options.category
}
])

answers.githubUrls = Array.isArray(answers.githubUrls) ? answers.githubUrls : stringToArray(answers.githubUrls)

await addProject({
name: answers.name.toLowerCase(),
category: answers.category,
githubOrgs: answers.githubUrls.map((url) => ({
url,
name: url.split('https://github.com/')[1]
}))
})

debug(`Project (${answers.name}) added successfully!`)

// @TODO: Add Organizations to the database

return answers
}

module.exports = {
runAddProjectCommand
}
26 changes: 26 additions & 0 deletions src/store/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const debug = require('debug')('store')

const addProject = knex => async (project) => {
const { name, category } = project
const projectExists = await knex('projects').where({ name }).first()
debug(`Checking if project ${name} exists...`)
if (projectExists) {
throw new Error(`Project with name ${name} already exists`)
}
debug(`Inserting project ${name}...`)
return knex('projects').insert({
name,
category
})
}

const initializeStore = (knex) => {
debug('Initializing store...')
return {
addProject: addProject(knex)
}
}

module.exports = {
initializeStore
}

0 comments on commit 0088847

Please sign in to comment.