Skip to content

Commit

Permalink
Integration test (#36)
Browse files Browse the repository at this point in the history
Integration test
  • Loading branch information
dtinth authored Jul 17, 2019
2 parents d6c7ec7 + f31f182 commit fcc52b9
Show file tree
Hide file tree
Showing 12 changed files with 276 additions and 31 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"devDependencies": {
"@types/jest": "^24.0.15",
"@types/node": "^12.0.12",
"@types/lodash.sortby": "^4.7.6",
"@types/mongodb": "^3.1.28",
"@types/node": "^12.0.12",
"jest": "^24.8.0",
"madge": "^3.4.4",
"ts-jest": "^24.0.2",
Expand All @@ -18,6 +19,7 @@
"@octokit/graphql": "^3.0.1",
"@octokit/rest": "^16.28.4",
"dotenv": "^8.0.0",
"lodash.sortby": "^4.7.0",
"mongodb": "^3.2.7",
"tkt": "1.1.0"
}
Expand Down
13 changes: 1 addition & 12 deletions src/CodeRepository.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { existsSync, readFileSync } from 'fs'
import { logger, invariant } from 'tkt'
import { execSync, execFileSync } from 'child_process'
import { ITodo, IFile } from './types'
import { IFile } from './types'
import { File } from './File'

import * as TodoParser from './TodoParser'

const log = logger('CodeRepository')

const event =
Expand Down Expand Up @@ -46,7 +44,6 @@ export const repoContext = {
}

type CodeRepositoryState = {
todoComments: ITodo[]
files: IFile[]
isOnDefaultBranch: boolean
saveChanges(commitMessage: string): Promise<void>
Expand All @@ -59,21 +56,13 @@ export async function scanCodeRepository(): Promise<CodeRepositoryState> {
})
.split('\n')
.filter(name => name)

const todoComments = []
const files: IFile[] = []
log.info('Parsing TODO tags...')
for (const filePath of filesWithTodoMarker) {
// TODO [#22]: Implement ignoring paths
if (filePath === 'README.md') continue
const file = new File(filePath)
const todos = TodoParser.parseTodos(file)
log.info('%s: %s found', filePath, todos.length)
todoComments.push(...todos)
files.push(file)
}
return {
todoComments,
files,
isOnDefaultBranch:
execSync('git rev-parse --abbrev-ref HEAD', {
Expand Down
4 changes: 3 additions & 1 deletion src/File.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export class MockFile implements IFile {
this.contents = new FileContents(contents)
}

save() {}
save() {
this.contents.changed = false
}
}

export class FileContents implements IFileContents {
Expand Down
5 changes: 2 additions & 3 deletions src/TaskManagementSystem.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { invariant, logger } from 'tkt'
import { ITodo } from './types'

import * as CodeRepository from './CodeRepository'

Expand Down Expand Up @@ -47,7 +46,7 @@ export async function createTask(
}

export async function completeTask(taskReference: string): Promise<void> {
const Octokit = await import('@octokit/rest')
const Octokit = (await import('@octokit/rest')).default
const octokit = new Octokit({
auth: `token ${process.env.GITHUB_TOKEN ||
invariant(false, 'Required GITHUB_TOKEN variable.')}`,
Expand All @@ -65,7 +64,7 @@ export async function updateTask(
taskReference: string,
information: TaskInformation,
): Promise<void> {
const Octokit = await import('@octokit/rest')
const Octokit = (await import('@octokit/rest')).default
const octokit = new Octokit({
auth: `token ${process.env.GITHUB_TOKEN ||
invariant(false, 'Required GITHUB_TOKEN variable.')}`,
Expand Down
116 changes: 104 additions & 12 deletions src/TodoActionsMain.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,104 @@
// TODO [#35]: Add integration test for TodoActionsMain.
//
// Code that interface with external data have been separated into their own modules.
// These includes:
//
// - `DataStore`
// - `CodeRepository`
// - `TaskManagementSystem`
//
// They can be mocked by creating a mock version using `__mocks__` folder.
// https://jestjs.io/docs/en/manual-mocks
test.todo('works')
import { runMain } from './TodoActionsMain'
import { resetMockWorld } from './__mocks__/World'
import sortBy from 'lodash.sortby'

jest.mock('./DataStore')
jest.mock('./CodeRepository')
jest.mock('./TaskManagementSystem')

const MARKER = 'TODO'

it('works', async () => {
const world = resetMockWorld()

// Round 1: Arrange
world.file(
'main.js',
`
// ${MARKER}: Hello world
// This is great!
<!--
- ${MARKER}:
- Somebody once told me
- the world is gonna roll me
-->
`,
)

// Round 1: Act
await runMain()

// Round 1: Assert commits
expect(world.commits.length).toEqual(2)
expect(world.commits[0].files.get('main.js')).toMatch(
new RegExp(`${MARKER} \\[\\$\\w+\\]: Hello world`),
)
expect(world.commits[1].files.get('main.js')).toMatch(
new RegExp(`${MARKER} \\[#\\d+\\]: Hello world`),
)

// Round 1: Assert tasks
expect(world.tasks.length).toEqual(2)
expect(sortBy(world.tasks.map(t => t.title))).toEqual([
'Hello world',
'Somebody once told me',
])

// Idempotent check
await runMain()
expect(world.commits.length).toEqual(2)
expect(world.tasks.length).toEqual(2)

// Round 2: Arrange
const task1 = world.tasks.find(t => t.title === 'Hello world')!
const task2 = world.tasks.find(t => t.title === 'Somebody once told me')!
world.file(
'main.js',
`
<!--
- ${MARKER} [#${task2.number}]:
- Somebody once told me?
- the world is gonna roll me
-->
`,
)

// Round 2: Act
await runMain()

// Round 2: Assert commits
// No new commits expected
expect(world.commits.length).toEqual(2)

// Round 2: Assert tasks
expect(task1.completed).toBe(true)
expect(task2.completed).toBe(false)
expect(task2.title).toBe('Somebody once told me?')
})

it('skips non default branch', async () => {
const world = resetMockWorld()

// Arrange
world.branch = 'featureBranch'
world.file(
'main.js',
`
// ${MARKER}: Hello world
// This is great!
<!--
- ${MARKER}:
- Somebody once told me
- the world is gonna roll me
-->
`,
)

// Act
await runMain()

// Assert
expect(world.commits.length).toEqual(0)
})
14 changes: 13 additions & 1 deletion src/TodoActionsMain.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { invariant } from 'tkt'
import { logger } from 'tkt'
import { ObjectId } from 'bson'
import { ITodo } from './types'

import * as TodoParser from './TodoParser'
import * as TaskUpdater from './TaskUpdater'
import * as CodeRepository from './CodeRepository'

Expand All @@ -10,10 +12,20 @@ const log = logger('main')
export async function runMain() {
log.info('Search for files with TODO tags...')
const {
todoComments,
files,
saveChanges,
isOnDefaultBranch,
} = await CodeRepository.scanCodeRepository()

const todoComments: ITodo[] = []
for (const file of files) {
// TODO [#22]: Implement ignoring paths
if (file.fileName === 'README.md') continue
const todos = TodoParser.parseTodos(file)
log.info('%s: %s found', file.fileName, todos.length)
todoComments.push(...todos)
}

log.info('Total TODOs found: %s', todoComments.length)
const todosWithoutReference = todoComments.filter(todo => !todo.reference)
log.info('TODOs without references: %s', todosWithoutReference.length)
Expand Down
26 changes: 26 additions & 0 deletions src/__mocks__/CodeRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { mockWorld } from './World'

type Real = typeof import('../CodeRepository')

export const repoContext: Real['repoContext'] = {
repositoryNodeId: '__GITHUB_REPO_NODE_ID__',
repositoryOwner: '_dtinth',
repositoryName: '_todo-actions',
defaultBranch: 'master',
}

export const scanCodeRepository: Real['scanCodeRepository'] = async () => {
const files = [...mockWorld.files.values()]
return {
files: files,
isOnDefaultBranch: mockWorld.branch === repoContext.defaultBranch,
async saveChanges(commitMessage) {
if (!files.some(f => f.contents.changed)) return
files.forEach(f => f.save())
mockWorld.commits.push({
message: commitMessage,
files: new Map(files.map(f => [f.fileName, f.contents.toString()])),
})
},
}
}
45 changes: 45 additions & 0 deletions src/__mocks__/DataStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { mockWorld } from './World'

type Real = typeof import('../DataStore')

export const beginTaskResolution: Real['beginTaskResolution'] = async (
todoUniqueKey,
repositoryId,
) => {
const existing = mockWorld.store.find(entry => entry._id === todoUniqueKey)
if (existing) {
return { existingTaskReference: existing.reference }
}

return {
async acquireTaskCreationLock() {
return {
async finish(taskReference, state) {
mockWorld.store.push({
_id: todoUniqueKey,
reference: taskReference,
state: state,
completed: false,
})
},
}
},
}
}

export const findAllUncompletedTasks: Real['findAllUncompletedTasks'] = async repositoryId => {
return mockWorld.store
.filter(entry => !entry.completed)
.map(entry => {
return {
taskReference: entry.reference,
state: entry.state,
async markAsCompleted() {
entry.completed = true
},
async updateState(newState) {
entry.state = newState
},
}
})
}
25 changes: 25 additions & 0 deletions src/__mocks__/TaskManagementSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { mockWorld, MockTask } from './World'

type Real = typeof import('../TaskManagementSystem')

export const createTask: Real['createTask'] = async information => {
const number = mockWorld.tasks.length + 1
const task: MockTask = { ...information, number, completed: false }
mockWorld.tasks.push(task)
return `#${task.number}`
}

export const completeTask: Real['completeTask'] = async taskReference => {
getTask(taskReference).completed = true
}

export const updateTask: Real['updateTask'] = async (
taskReference,
information,
) => {
Object.assign(getTask(taskReference), information)
}

function getTask(taskReference: string) {
return mockWorld.tasks.find(t => `#${t.number}` === taskReference)!
}
40 changes: 40 additions & 0 deletions src/__mocks__/World.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { IFile, ITaskState } from '../types'
import { MockFile } from '../File'

export let mockWorld: MockWorld

export type MockTask = {
title: string
body: string
number: number
completed: boolean
}

export type MockDataStoreEntry = {
_id: string
completed: boolean
reference: string
state: ITaskState
}

export type MockCommit = {
message: string
files: Map<string, string>
}

export function resetMockWorld() {
mockWorld = new MockWorld()
return mockWorld
}

class MockWorld {
files: Map<string, IFile> = new Map()
branch = 'master'
store: MockDataStoreEntry[] = []
tasks: MockTask[] = []
commits: MockCommit[] = []

file(fileName: string, contents: string) {
this.files.set(fileName, new MockFile(fileName, contents))
}
}
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"skipLibCheck": true,
"rootDir": "src",
"outDir": "lib",
"sourceMap": true
"sourceMap": true,
"esModuleInterop": true
},
"include": ["src"],
"exclude": ["node_modules", "**/node_modules/*"]
Expand Down
Loading

0 comments on commit fcc52b9

Please sign in to comment.