Skip to content

Commit

Permalink
Merge pull request #23 from secure-dashboards/feat/add-wf-update-gith…
Browse files Browse the repository at this point in the history
…ub-orgs
  • Loading branch information
UlisesGascon authored Dec 4, 2024
2 parents 084b836 + a575010 commit d22de43
Show file tree
Hide file tree
Showing 17 changed files with 1,080 additions and 81 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ jobs:
run: npm run db:seed

- name: Run tests
run: npm test
run: npm run test:ci
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,21 @@ For example, to add a project named "express" with GitHub URLs:
node index.js project add --name express --github-urls https://github.com/expressjs https://github.com/pillarjs https://github.com/jshttp --category impact
```

### Workflows

To run a workflow, use the following command:

```bash
node index.js workflow run [--name <name>]
```

To list all available workflows, use the following command:

```bash
node index.js workflow list
```


## Debug mode

This project uses the [debug library](https://www.npmjs.com/package/debug), so you can always use the environmental variable `DEBUG=*` to print more detailed information of the execution.
Expand Down
10 changes: 10 additions & 0 deletions __tests__/cli/__snapshots__/workflows.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`list - Non-Interactive Mode Should provide a list of available workflows 1`] = `
[
{
"description": "Check the organizations stored and update the information with the GitHub API.",
"name": "update-github-orgs",
},
]
`;
10 changes: 6 additions & 4 deletions __tests__/cli.test.js → __tests__/cli/project.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const inquirer = require('inquirer').default
const knexInit = require('knex')
const { getConfig } = require('../src/config/')
const { runAddProjectCommand } = require('../src/cli')
const { resetDatabase, getAllProjects, getAllGithubOrgs } = require('./utils')
const { getConfig } = require('../../src/config')
const { runAddProjectCommand } = require('../../src/cli')
const { resetDatabase, getAllProjects, getAllGithubOrgs } = require('../utils')

const { dbSettings } = getConfig('test')

Expand All @@ -24,7 +24,9 @@ let knex
beforeAll(() => {
knex = knexInit(dbSettings)
})
beforeEach(() => resetDatabase(knex))
beforeEach(async () => {
await resetDatabase(knex)
})
afterEach(jest.clearAllMocks)
afterAll(async () => {
await resetDatabase(knex)
Expand Down
114 changes: 114 additions & 0 deletions __tests__/cli/workflows.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
const inquirer = require('inquirer').default
const knexInit = require('knex')
const { getConfig } = require('../../src/config')
const { runWorkflowCommand, listWorkflowCommand } = require('../../src/cli')
const { resetDatabase, getAllProjects, getAllGithubOrgs, addGithubOrg, addProject } = require('../utils')
const { github } = require('../../src/providers')
const { sampleGithubOrg } = require('../fixtures')

const { dbSettings } = getConfig('test')

let knex

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

describe('list - Non-Interactive Mode', () => {
jest.spyOn(inquirer, 'prompt').mockImplementation(async () => ({}))

test('Should provide a list of available workflows', async () => {
// @TODO: This test can be improved, currently is used to ensure that all the commands are listed
await expect(listWorkflowCommand()).toMatchSnapshot()
})
})

describe('run update-github-orgs - Interactive Mode', () => {
// Mock inquirer for testing
jest.spyOn(inquirer, 'prompt').mockImplementation(async (questions) => {
const questionMap = {
'What is the name of the workflow?': 'update-github-orgs'
}
return questions.reduce((acc, question) => {
acc[question.name] = questionMap[question.message]
return acc
}, {})
})

test('Should throw an error when no Github orgs are stored in the database', async () => {
const projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
const githubOrgs = await getAllGithubOrgs(knex)
expect(githubOrgs.length).toBe(0)
await expect(runWorkflowCommand(knex, {}))
.rejects
.toThrow('No organizations found. Please add organizations/projects before running this workflow.')
})

test('Should update the project with new information available', async () => {
// Prepare the database
await addProject(knex, { name: sampleGithubOrg.login, category: 'impact' })
await addGithubOrg(knex, { login: sampleGithubOrg.login, html_url: sampleGithubOrg.html_url })
const projects = await getAllProjects(knex)
expect(projects.length).toBe(1)
let githubOrgs = await getAllGithubOrgs(knex)
expect(githubOrgs.length).toBe(1)
expect(githubOrgs[0].description).toBe(null)
// Mock the fetchOrgByLogin method
jest.spyOn(github, 'fetchOrgByLogin').mockResolvedValue(sampleGithubOrg)
await runWorkflowCommand(knex, {})
// Check the database changes
githubOrgs = await getAllGithubOrgs(knex)
expect(githubOrgs.length).toBe(1)
expect(githubOrgs[0].description).toBe(sampleGithubOrg.description)
})

test.todo('Should throw an error when the Github API is not available')
})

describe('run update-github-orgs - Non-Interactive Mode', () => {
// Mock inquirer for testing
jest.spyOn(inquirer, 'prompt').mockImplementation(async (questions) => {
const questionMap = {
'What is the name of the workflow?': 'update-github-orgs'
}
return questions.reduce((acc, question) => {
acc[question.name] = questionMap[question.message]
return acc
}, {})
})

test('Should throw an error when invalid name is provided', async () => {
await expect(runWorkflowCommand(knex, { name: 'invented' }))
.rejects
.toThrow('Invalid workflow name. Please enter a valid workflow name.')
})

test('Should throw an error when no name is provided', async () => {
await expect(runWorkflowCommand(knex, { name: undefined }))
.rejects
.toThrow('Invalid workflow name. Please enter a valid workflow name.')
})

test('Should throw an error when no Github orgs are stored in the database', async () => {
const projects = await getAllProjects(knex)
expect(projects.length).toBe(0)
const githubOrgs = await getAllGithubOrgs(knex)
expect(githubOrgs.length).toBe(0)
await expect(runWorkflowCommand(knex, { name: 'update-github-orgs' }))
.rejects
.toThrow('No organizations found. Please add organizations/projects before running this workflow.')
})

test.todo('Should update the project with new information available')
test.todo('Should throw an error when the Github API is not available')
})
70 changes: 70 additions & 0 deletions __tests__/fixtures/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// https://docs.github.com/en/rest/orgs/orgs?apiVersion=2022-11-28#get-an-organization
const sampleGithubOrg = {
login: 'github',
id: 1,
node_id: 'MDEyOk9yZ2FuaXphdGlvbjE=',
url: 'https://api.github.com/orgs/github',
repos_url: 'https://api.github.com/orgs/github/repos',
events_url: 'https://api.github.com/orgs/github/events',
hooks_url: 'https://api.github.com/orgs/github/hooks',
issues_url: 'https://api.github.com/orgs/github/issues',
members_url: 'https://api.github.com/orgs/github/members{/member}',
public_members_url: 'https://api.github.com/orgs/github/public_members{/member}',
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
description: 'A great organization',
name: 'github',
company: 'GitHub',
blog: 'https://github.com/blog',
location: 'San Francisco',
email: '[email protected]',
twitter_username: 'github',
is_verified: true,
has_organization_projects: true,
has_repository_projects: true,
public_repos: 2,
public_gists: 1,
followers: 20,
following: 0,
html_url: 'https://github.com/octocat',
created_at: '2008-01-14T04:33:35Z',
type: 'Organization',
total_private_repos: 100,
owned_private_repos: 100,
private_gists: 81,
disk_usage: 10000,
collaborators: 8,
billing_email: '[email protected]',
plan: {
name: 'Medium',
space: 400,
private_repos: 20,
filled_seats: 4,
seats: 5
},
default_repository_permission: 'read',
members_can_create_repositories: true,
two_factor_requirement_enabled: true,
members_allowed_repository_creation_type: 'all',
members_can_create_public_repositories: false,
members_can_create_private_repositories: false,
members_can_create_internal_repositories: false,
members_can_create_pages: true,
members_can_create_public_pages: true,
members_can_create_private_pages: true,
members_can_fork_private_repositories: false,
web_commit_signoff_required: false,
updated_at: '2014-03-03T18:58:10Z',
deploy_keys_enabled_for_repositories: false,
dependency_graph_enabled_for_new_repositories: false,
dependabot_alerts_enabled_for_new_repositories: false,
dependabot_security_updates_enabled_for_new_repositories: false,
advanced_security_enabled_for_new_repositories: false,
secret_scanning_enabled_for_new_repositories: false,
secret_scanning_push_protection_enabled_for_new_repositories: false,
secret_scanning_push_protection_custom_link: 'https://github.com/octo-org/octo-repo/blob/main/im-blocked.md',
secret_scanning_push_protection_custom_link_enabled: false
}

module.exports = {
sampleGithubOrg
}
68 changes: 1 addition & 67 deletions __tests__/schemas.test.js
Original file line number Diff line number Diff line change
@@ -1,72 +1,6 @@
const { sampleGithubOrg } = require('./fixtures')
const { validateGithubOrg } = require('../src/schemas')

// https://docs.github.com/en/rest/orgs/orgs?apiVersion=2022-11-28#get-an-organization
const sampleGithubOrg = {
login: 'github',
id: 1,
node_id: 'MDEyOk9yZ2FuaXphdGlvbjE=',
url: 'https://api.github.com/orgs/github',
repos_url: 'https://api.github.com/orgs/github/repos',
events_url: 'https://api.github.com/orgs/github/events',
hooks_url: 'https://api.github.com/orgs/github/hooks',
issues_url: 'https://api.github.com/orgs/github/issues',
members_url: 'https://api.github.com/orgs/github/members{/member}',
public_members_url: 'https://api.github.com/orgs/github/public_members{/member}',
avatar_url: 'https://github.com/images/error/octocat_happy.gif',
description: 'A great organization',
name: 'github',
company: 'GitHub',
blog: 'https://github.com/blog',
location: 'San Francisco',
email: '[email protected]',
twitter_username: 'github',
is_verified: true,
has_organization_projects: true,
has_repository_projects: true,
public_repos: 2,
public_gists: 1,
followers: 20,
following: 0,
html_url: 'https://github.com/octocat',
created_at: '2008-01-14T04:33:35Z',
type: 'Organization',
total_private_repos: 100,
owned_private_repos: 100,
private_gists: 81,
disk_usage: 10000,
collaborators: 8,
billing_email: '[email protected]',
plan: {
name: 'Medium',
space: 400,
private_repos: 20,
filled_seats: 4,
seats: 5
},
default_repository_permission: 'read',
members_can_create_repositories: true,
two_factor_requirement_enabled: true,
members_allowed_repository_creation_type: 'all',
members_can_create_public_repositories: false,
members_can_create_private_repositories: false,
members_can_create_internal_repositories: false,
members_can_create_pages: true,
members_can_create_public_pages: true,
members_can_create_private_pages: true,
members_can_fork_private_repositories: false,
web_commit_signoff_required: false,
updated_at: '2014-03-03T18:58:10Z',
deploy_keys_enabled_for_repositories: false,
dependency_graph_enabled_for_new_repositories: false,
dependabot_alerts_enabled_for_new_repositories: false,
dependabot_security_updates_enabled_for_new_repositories: false,
advanced_security_enabled_for_new_repositories: false,
secret_scanning_enabled_for_new_repositories: false,
secret_scanning_push_protection_enabled_for_new_repositories: false,
secret_scanning_push_protection_custom_link: 'https://github.com/octo-org/octo-repo/blob/main/im-blocked.md',
secret_scanning_push_protection_custom_link_enabled: false
}

describe('schemas', () => {
describe('validateGithubOrg', () => {
test('Should not throw an error with valid data', () => {
Expand Down
18 changes: 15 additions & 3 deletions __tests__/utils/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
const resetDatabase = async (knex) => {
await knex('github_organizations').del()
await knex('projects').del()
await knex.raw('TRUNCATE TABLE github_organizations RESTART IDENTITY CASCADE')
await knex.raw('TRUNCATE TABLE projects RESTART IDENTITY CASCADE')
}

const getAllProjects = (knex) => knex('projects').select('*')
const getAllGithubOrgs = (knex) => knex('github_organizations').select('*')

const addProject = async (knex, { name, category }) => {
const [project] = await knex('projects').insert({ name, category }).returning('*')
return project
}

const addGithubOrg = async (knex, data) => {
const [githubOrg] = await knex('github_organizations').insert(data).returning('*')
return githubOrg
}

module.exports = {
resetDatabase,
getAllProjects,
getAllGithubOrgs
getAllGithubOrgs,
addProject,
addGithubOrg
}
26 changes: 25 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { Command } = require('commander')
const { getConfig } = require('./src/config')
const { projectCategories, dbSettings } = getConfig()
const { runAddProjectCommand } = require('./src/cli')
const { runAddProjectCommand, runWorkflowCommand, listWorkflowCommand } = require('./src/cli')
const knex = require('knex')(dbSettings)

const program = new Command()
Expand All @@ -26,4 +26,28 @@ project
}
})

// Workflow commands
const workflow = program.command('workflow').description('Manage workflows')

workflow
.command('run')
.description('Run a workflow')
.option('--name <name>', 'Name of the workflow')
.action(async (options) => {
try {
await runWorkflowCommand(knex, options)
} catch (error) {
console.error('Error running workflow:', error.message)
process.exit(1)
} finally {
await knex.destroy()
}
})

workflow
.command('list')
.description('List all available workflows')
.action((options) => {
listWorkflowCommand(options)
})
program.parse(process.argv)
Loading

0 comments on commit d22de43

Please sign in to comment.