-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add CLI and project addition commands
- Loading branch information
1 parent
480199b
commit 0088847
Showing
5 changed files
with
279 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
const project = require('./project') | ||
|
||
module.exports = project |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |