Skip to content

Commit

Permalink
✨ add stop command
Browse files Browse the repository at this point in the history
  • Loading branch information
ulisesantana committed Dec 9, 2023
1 parent 6360714 commit 95f42d6
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 59 deletions.
43 changes: 43 additions & 0 deletions src/commands/stop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {Command} from '@oclif/core'
import path from "node:path";

import {GetConfigurationUseCase, StopCurrentTimeEntryUseCase} from "../application/cases";
import {ConfigurationValidator, configFilename} from "../core";
import {FileSystemDataSource, TogglApi, http} from "../infrastructure/data-sources";
import {ConfigurationRepositoryImplementation, TimeEntryRepositoryImplementation} from "../infrastructure/repositories";

export default class Stop extends Command {
static description = 'Stop running time entry.'

static examples = [
'<%= config.bin %> <%= command.id %>',
]

public async run(): Promise<void> {
const configurationRepository = new ConfigurationRepositoryImplementation(new FileSystemDataSource(path.join(this.config.configDir, configFilename)))
const config = await new GetConfigurationUseCase(configurationRepository).exec()
const configValidation = ConfigurationValidator.isRequiredConfigAvailable(config)
if (configValidation.error) {
this.error(configValidation.message)
}

const togglAPI = new TogglApi({
http,
token: config.apiToken,
workspaceId: config.workspaceId
})
const timeEntryRepository = new TimeEntryRepositoryImplementation(togglAPI)
const useCase = new StopCurrentTimeEntryUseCase(timeEntryRepository)

try {
const updatedEntry = await useCase.exec()
if (updatedEntry) {
this.log(`Time entry "${updatedEntry.description}" stopped for "${updatedEntry.project.name}" project.`)
} else {
this.log("There is no time entry running.")
}
} catch (error) {
this.error(`Unexpected error: ${error}`)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class TimeEntryRepositoryImplementation implements TimeEntryRepository {
}

async getCurrentEntry(): Promise<Nullable<TimeEntry>> {
const [entry] = await this.api.getTimeEntries()
const entry = await this.api.getCurrentEntry()
if (!entry) {
return null
}
Expand Down Expand Up @@ -106,7 +106,7 @@ export class TimeEntryRepositoryImplementation implements TimeEntryRepository {
return null
}

const project = await this.api.getProjectById(entry.id)
const project = await this.api.getProjectById(entry.project_id)
return TimeEntryRepositoryImplementation.mapToTimeEntry(entry, project)
}
}
45 changes: 22 additions & 23 deletions test/commands/start.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
/* eslint-disable camelcase */
import {Config, ux} from '@oclif/core'
import axios from 'axios';
import MockAdapter from "axios-mock-adapter";
import {expect} from 'chai'
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import path from "node:path";
import {SinonSandbox, SinonStub, createSandbox} from 'sinon'

import {TogglApi} from "../../src/infrastructure/data-sources";
import {buildTogglProject, buildTogglTimeEntry} from "../builders";
import {configuration} from "../fixtures";
import httpMock from "../http.mock"

const mock = new MockAdapter(axios); // here uses axios because here we are testing the command, which uses transpiled code.
chai.use(chaiAsPromised);
const {expect} = chai;

describe('start command runs', () => {
const projects = [buildTogglProject({name: 'Evil Company'}), buildTogglProject({name: 'Good Company'})]
Expand All @@ -27,13 +28,13 @@ describe('start command runs', () => {

afterEach(async () => {
sandbox.restore()
mock.reset()
httpMock.reset()
})

it('creating entry by passing all arguments and flags', async () => {
const [evilProject] = projects
const {id, name} = evilProject
mock.onGet(`${TogglApi.baseUrl}/api/v9/workspaces/${configuration.workspaceId}/projects?active=true`)
httpMock.onGet(`${TogglApi.baseUrl}/api/v9/workspaces/${configuration.workspaceId}/projects?active=true`)
.reply(200, projects)
.onPost(`${TogglApi.baseUrl}/api/v9/workspaces/${configuration.workspaceId}/time_entries`)
.reply(200, buildTogglTimeEntry({description: configuration.defaultTimeEntry, project_id: id}))
Expand All @@ -44,10 +45,13 @@ describe('start command runs', () => {
})

it('creating entry using default project', async () => {
const projects = [buildTogglProject({id: configuration.projectId, name: 'Evil Company'}), buildTogglProject({name: 'Good Company'})]
const projects = [buildTogglProject({
id: configuration.projectId,
name: 'Evil Company'
}), buildTogglProject({name: 'Good Company'})]
const [evilProject] = projects
const {id, name} = evilProject
mock
httpMock
.onGet(`${TogglApi.baseUrl}/api/v9/workspaces/${configuration.workspaceId}/projects/${id}`)
.reply(200, evilProject)
.onPost(`${TogglApi.baseUrl}/api/v9/workspaces/${configuration.workspaceId}/time_entries`)
Expand All @@ -61,7 +65,7 @@ describe('start command runs', () => {
it('creating entry using default time entry', async () => {
const [evilProject] = projects
const {id, name} = evilProject
mock
httpMock
.onGet(`${TogglApi.baseUrl}/api/v9/workspaces/${configuration.workspaceId}/projects/${id}`)
.reply(200, evilProject)
.onPost(`${TogglApi.baseUrl}/api/v9/workspaces/${configuration.workspaceId}/time_entries`)
Expand All @@ -73,10 +77,13 @@ describe('start command runs', () => {
})

it('creating entry using default time entry and default project', async () => {
const projects = [buildTogglProject({id: configuration.projectId, name: 'Evil Company'}), buildTogglProject({name: 'Good Company'})]
const projects = [buildTogglProject({
id: configuration.projectId,
name: 'Evil Company'
}), buildTogglProject({name: 'Good Company'})]
const [evilProject] = projects
const {id, name} = evilProject
mock
httpMock
.onGet(`${TogglApi.baseUrl}/api/v9/workspaces/${configuration.workspaceId}/projects/${id}`)
.reply(200, evilProject)
.onPost(`${TogglApi.baseUrl}/api/v9/workspaces/${configuration.workspaceId}/time_entries`)
Expand All @@ -92,23 +99,15 @@ describe('start command runs', () => {
const [evilProject] = projects
const {id} = evilProject

try {
await config.runCommand("start", ["-p", id.toString()])
throw new Error('Start command should throw error')
} catch (error) {
expect(`${error}`).to.contains("Missing time entry description argument")
}
await expect(config.runCommand("start", ["-p", id.toString()]))
.to.be.rejectedWith("Missing time entry description argument")
})

it('showing error if project is missing and default project id is not defined', async () => {
config.configDir = path.join(process.cwd(), 'test/fixtures/min-config')

try {
await config.runCommand("start", ["Doing stuff"])
throw new Error('Start command should throw error')
} catch (error) {
expect(`${error}`).to.contains("Missing project flag for the time entry.")
}
await expect(config.runCommand("start", ["Doing stuff"]))
.to.be.rejectedWith("Missing project flag for the time entry.")
})

})
59 changes: 59 additions & 0 deletions test/commands/stop.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* eslint-disable camelcase */
import {Config, ux} from '@oclif/core'
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import path from "node:path";
import {SinonSandbox, SinonStub, createSandbox} from 'sinon'

import {TogglApi} from "../../dist/infrastructure/data-sources"; // here uses dist directory because here we are testing the command, which uses transpiled code.
import {buildTogglProject, buildTogglTimeEntry} from "../builders";
import {configuration} from "../fixtures";
import httpMock from "../http.mock";

chai.use(chaiAsPromised);
const {expect} = chai;

describe('stop command runs', () => {
const projects = [buildTogglProject({name: 'Evil Company'}), buildTogglProject({name: 'Good Company'})]
let sandbox: SinonSandbox
let config: Config
let stdoutStub: SinonStub

beforeEach(async () => {
sandbox = createSandbox()
stdoutStub = sandbox.stub(ux.write, 'stdout')
config = await Config.load({root: process.cwd()})
config.configDir = path.join(process.cwd(), 'test/fixtures')
})

afterEach(async () => {
sandbox.restore()
httpMock.reset()
})

it('stopping current time entry', async () => {
const [evilProject] = projects
const {id, name} = evilProject
const entry = buildTogglTimeEntry({project_id: id})
httpMock.onGet(`${TogglApi.baseUrl}/api/v9/me/time_entries/current`)
.reply(200, entry)
.onGet(`${TogglApi.baseUrl}/api/v9/workspaces/${configuration.workspaceId}/projects/${id}`)
.reply(200, evilProject)
.onPut(`${TogglApi.baseUrl}/api/v9/workspaces/${configuration.workspaceId}/time_entries/${entry.id}`)
.reply(200, entry)

await config.runCommand("stop")

expect(stdoutStub.args.flat().join(',')).to.contains(`Time entry "${entry.description}" stopped for "${name}" project.`)
})

it('showing there is no entry to stop if there is no current time entry', async () => {
httpMock.onGet(`${TogglApi.baseUrl}/api/v9/me/time_entries/current`)
.reply(200, null)

await config.runCommand("stop")

expect(stdoutStub.args.flat().join(',')).to.contains(`There is no time entry running.`)
})

})
5 changes: 5 additions & 0 deletions test/http.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import MockAdapter from "axios-mock-adapter";

import {http} from "../dist/infrastructure/data-sources/http";

export default new MockAdapter(http);
Loading

0 comments on commit 95f42d6

Please sign in to comment.