Repository: lucassabreu/clockify-cli Branch: main Commit: fd1bfddec02c Files: 216 Total size: 852.5 KB Directory structure: gitextract_9hng928q/ ├── .deepsource.toml ├── .github/ │ └── workflows/ │ ├── golangci-lint.yml │ ├── release.yml │ └── test-unit.yaml ├── .gitignore ├── .gitmodules ├── .goreleaser.yml ├── .mockery.yaml ├── .nvimrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── api/ │ ├── client.go │ ├── client_test.go │ ├── dto/ │ │ ├── dto.go │ │ └── request.go │ ├── httpClient.go │ ├── logger.go │ ├── project_test.go │ ├── tag_test.go │ ├── task_test.go │ └── timeentry_test.go ├── cmd/ │ ├── clockify-cli/ │ │ └── main.go │ ├── gendocs/ │ │ └── main.go │ └── release/ │ └── main.go ├── docs/ │ └── project-layout.md ├── go.mod ├── go.sum ├── internal/ │ ├── consoletest/ │ │ └── test.go │ ├── mocks/ │ │ ├── gen.go │ │ ├── mock_Client.go │ │ ├── mock_Config.go │ │ ├── mock_Factory.go │ │ └── simple_config.go │ └── testhlp/ │ └── helper.go ├── netlify.toml ├── pkg/ │ ├── cmd/ │ │ ├── client/ │ │ │ ├── add/ │ │ │ │ ├── add.go │ │ │ │ └── add_test.go │ │ │ ├── client.go │ │ │ ├── list/ │ │ │ │ ├── list.go │ │ │ │ └── list_test.go │ │ │ └── util/ │ │ │ └── util.go │ │ ├── completion/ │ │ │ └── completion.go │ │ ├── config/ │ │ │ ├── config.go │ │ │ ├── get/ │ │ │ │ ├── get.go │ │ │ │ └── get_test.go │ │ │ ├── init/ │ │ │ │ ├── init.go │ │ │ │ └── init_test.go │ │ │ ├── list/ │ │ │ │ ├── list.go │ │ │ │ └── list_test.go │ │ │ ├── set/ │ │ │ │ ├── set.go │ │ │ │ └── set_test.go │ │ │ └── util/ │ │ │ └── util.go │ │ ├── project/ │ │ │ ├── add/ │ │ │ │ ├── add.go │ │ │ │ └── add_test.go │ │ │ ├── edit/ │ │ │ │ ├── edit.go │ │ │ │ └── edit_test.go │ │ │ ├── get/ │ │ │ │ ├── get.go │ │ │ │ └── get_test.go │ │ │ ├── list/ │ │ │ │ ├── list.go │ │ │ │ └── list_test.go │ │ │ ├── project.go │ │ │ └── util/ │ │ │ └── util.go │ │ ├── root.go │ │ ├── tag/ │ │ │ └── tag.go │ │ ├── task/ │ │ │ ├── add/ │ │ │ │ ├── add.go │ │ │ │ └── add_test.go │ │ │ ├── delete/ │ │ │ │ ├── delete.go │ │ │ │ └── delete_test.go │ │ │ ├── done/ │ │ │ │ ├── done.go │ │ │ │ └── done_test.go │ │ │ ├── edit/ │ │ │ │ ├── edit.go │ │ │ │ └── edit_test.go │ │ │ ├── list/ │ │ │ │ ├── list.go │ │ │ │ └── list_test.go │ │ │ ├── quick-add/ │ │ │ │ ├── quick-add.go │ │ │ │ └── quick-add_test.go │ │ │ ├── task.go │ │ │ └── util/ │ │ │ ├── read-flags.go │ │ │ └── report.go │ │ ├── time-entry/ │ │ │ ├── clone/ │ │ │ │ └── clone.go │ │ │ ├── delete/ │ │ │ │ └── delete.go │ │ │ ├── edit/ │ │ │ │ ├── edit.go │ │ │ │ └── edit_test.go │ │ │ ├── edit-multipple/ │ │ │ │ └── edit-multiple.go │ │ │ ├── in/ │ │ │ │ ├── in.go │ │ │ │ └── in_test.go │ │ │ ├── invoiced/ │ │ │ │ └── invoiced.go │ │ │ ├── manual/ │ │ │ │ └── manual.go │ │ │ ├── out/ │ │ │ │ └── out.go │ │ │ ├── report/ │ │ │ │ ├── last-day/ │ │ │ │ │ └── last-day.go │ │ │ │ ├── last-month/ │ │ │ │ │ └── last-month.go │ │ │ │ ├── last-week/ │ │ │ │ │ └── last-week.go │ │ │ │ ├── last-week-day/ │ │ │ │ │ └── last-week-day.go │ │ │ │ ├── report.go │ │ │ │ ├── this-month/ │ │ │ │ │ └── this-month.go │ │ │ │ ├── this-week/ │ │ │ │ │ └── this-week.go │ │ │ │ ├── today/ │ │ │ │ │ ├── today.go │ │ │ │ │ └── today_test.go │ │ │ │ ├── util/ │ │ │ │ │ ├── report.go │ │ │ │ │ ├── report_flag_test.go │ │ │ │ │ └── reportwithrange_test.go │ │ │ │ └── yesterday/ │ │ │ │ └── yesterday.go │ │ │ ├── show/ │ │ │ │ └── show.go │ │ │ ├── split/ │ │ │ │ ├── split.go │ │ │ │ └── split_test.go │ │ │ ├── timeentry.go │ │ │ └── util/ │ │ │ ├── create.go │ │ │ ├── description-completer.go │ │ │ ├── fill-with-flags.go │ │ │ ├── flags.go │ │ │ ├── help.go │ │ │ ├── interactive.go │ │ │ ├── interactive_test.go │ │ │ ├── name-for-id.go │ │ │ ├── out-in-progress.go │ │ │ ├── report.go │ │ │ ├── util.go │ │ │ ├── util_test.go │ │ │ ├── validate-closing.go │ │ │ └── validate.go │ │ ├── user/ │ │ │ ├── me/ │ │ │ │ ├── me.go │ │ │ │ └── me_test.go │ │ │ ├── user.go │ │ │ ├── user_test.go │ │ │ └── util/ │ │ │ └── util.go │ │ ├── version/ │ │ │ ├── version.go │ │ │ └── version_test.go │ │ └── workspace/ │ │ ├── workspace.go │ │ └── workspace_test.go │ ├── cmdcompl/ │ │ ├── flags.go │ │ └── valid-args.go │ ├── cmdcomplutil/ │ │ ├── client.go │ │ ├── factory.go │ │ ├── project.go │ │ ├── project_test.go │ │ ├── tag.go │ │ ├── task.go │ │ ├── user.go │ │ └── workspace.go │ ├── cmdutil/ │ │ ├── args.go │ │ ├── args_test.go │ │ ├── config.go │ │ ├── errors.go │ │ ├── factory.go │ │ ├── flags.go │ │ ├── flags_test.go │ │ ├── project.go │ │ └── version.go │ ├── output/ │ │ ├── client/ │ │ │ ├── csv.go │ │ │ ├── default.go │ │ │ ├── json.go │ │ │ ├── quiet.go │ │ │ └── template.go │ │ ├── project/ │ │ │ ├── csv.go │ │ │ ├── default.go │ │ │ ├── json.go │ │ │ ├── quiet.go │ │ │ └── template.go │ │ ├── tag/ │ │ │ ├── default.go │ │ │ ├── quiet.go │ │ │ └── template.go │ │ ├── task/ │ │ │ ├── csv.go │ │ │ ├── default.go │ │ │ ├── json.go │ │ │ ├── quiet.go │ │ │ └── template.go │ │ ├── time-entry/ │ │ │ ├── csv.go │ │ │ ├── default.go │ │ │ ├── default_test.go │ │ │ ├── duration.go │ │ │ ├── duration_test.go │ │ │ ├── json.go │ │ │ ├── markdown.go │ │ │ ├── markdown.gotmpl.md │ │ │ ├── markdown_test.go │ │ │ ├── quiet.go │ │ │ └── template.go │ │ ├── user/ │ │ │ ├── default.go │ │ │ ├── json.go │ │ │ ├── quiet.go │ │ │ └── template.go │ │ ├── util/ │ │ │ ├── color.go │ │ │ └── template.go │ │ └── workspace/ │ │ ├── default.go │ │ ├── quiet.go │ │ └── template.go │ ├── search/ │ │ ├── client.go │ │ ├── errors.go │ │ ├── find.go │ │ ├── find_test.go │ │ ├── project.go │ │ ├── tag.go │ │ ├── task.go │ │ └── user.go │ ├── timeentryhlp/ │ │ └── timeentry.go │ ├── timehlp/ │ │ ├── range.go │ │ ├── relative.go │ │ ├── time.go │ │ ├── time_test.go │ │ └── util.go │ └── ui/ │ ├── color.go │ └── ui.go ├── scripts/ │ └── site-build ├── site/ │ ├── .gitignore │ ├── archetypes/ │ │ └── default.md │ ├── config.toml │ ├── content/ │ │ ├── _index.md │ │ └── usage/ │ │ └── _index.md │ ├── layouts/ │ │ └── partials/ │ │ ├── logo.html │ │ └── menu-footer.html │ └── static/ │ └── css/ │ └── custom.css └── strhlp/ ├── strhlp.go └── strhlp_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .deepsource.toml ================================================ version = 1 [[analyzers]] name = "go" enabled = true [analyzers.meta] import_root = "github.com/lucassabreu/clockify-cli" dependencies_vendored = false [[analyzers]] name = "test-coverage" enabled = true ================================================ FILE: .github/workflows/golangci-lint.yml ================================================ name: golangci-lint on: push: tags: - v* branches: - main pull_request: permissions: contents: read # Optional: allow read access to pull request. Use with `only-new-issues` option. # pull-requests: read jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version: 1.24 - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: version: latest ================================================ FILE: .github/workflows/release.yml ================================================ name: goreleaser on: pull_request: push: tags: - "*" jobs: goreleaser: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v6 - name: go-setup uses: actions/setup-go@v6 with: go-version: 1.24 - name: install snapcraft uses: samuelmeuli/action-snapcraft@v3 - name: install nix uses: cachix/install-nix-action@v31 - name: goreleaser-setup uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: latest install-only: true - if: startsWith(github.ref, 'refs/tags/') name: release a new version run: | make release "tag=${GITHUB_REF#refs/tags/}" env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN_GORELEASER }} SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} - if: startsWith(github.ref, 'refs/tags/') == false name: test releasing a snapshot version run: make release SNAPSHOT=1 tag=Unreleased - if: startsWith(github.ref, 'refs/tags/') name: trigger Netlify deploy with new release run: | curl -vs -X POST "https://api.netlify.com/build_hooks/${NETLIFY_HOOK}" \ --data-urlencode "trigger_title=triggered+by github actions (tag: ${GITHUB_REF#refs/tags/})" \ --data-urlencode "trigger_branch=main" env: NETLIFY_HOOK: ${{ secrets.NETLIFY_HOOK }} ================================================ FILE: .github/workflows/test-unit.yaml ================================================ name: Unit Tests on: pull_request: push: tags: - v* branches: - main jobs: tests: runs-on: ubuntu-latest steps: - name: Check out code into the Go module directory uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version: 1.24 - name: Get dependencies run: | go mod download go install gotest.tools/gotestsum@latest - name: Generate coverage report run: | gotestsum --format dots -- \ -coverprofile=coverage.txt \ -covermode=atomic \ ./... - name: Upload coverage report uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.txt flags: unittests - name: Report test coverage to DeepSource uses: deepsourcelabs/test-coverage-action@master with: key: go coverage-file: ./coverage.txt dsn: ${{ secrets.DEEPSOURCE_DSN }} ================================================ FILE: .gitignore ================================================ build/ dist/ snap.login /clockify-cli site/content/commands/ site/public/ site/content/license ================================================ FILE: .gitmodules ================================================ [submodule "site/themes/hugo-theme-relearn"] path = site/themes/hugo-theme-relearn url = https://github.com/McShelby/hugo-theme-relearn.git ================================================ FILE: .goreleaser.yml ================================================ version: 2 builds: - env: - CGO_ENABLED=0 goos: - windows - linux - darwin hooks: pre: - go mod download main: ./cmd/clockify-cli archives: - id: default name_template: >- {{- .ProjectName }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end -}} format_overrides: - goos: windows formats: [zip] files: - LICENSE checksum: name_template: "checksums.txt" snapshot: version_template: "{{ .Tag }}-next" changelog: sort: asc filters: exclude: - "^docs:" - "^test:" snapcrafts: - name: clockify-cli summary: Helps to interact with Clockfy's API description: Helps to interact with Clockfy's API grade: stable publish: true confinement: strict apps: clockify-cli: plugs: ["network"] homebrew_casks: - name: clockify-cli repository: owner: lucassabreu name: homebrew-tap homepage: https://github.com/lucassabreu/clockify-cli description: Helps to interact with Clockfy's API nix: - name: clockify-cli goamd64: v1 # The project name and current git tag are used in the format string. # # Templates: allowed. commit_msg_template: "{{ .ProjectName }}: {{ .Tag }}" # Your app's homepage. # # Templates: allowed. # Default: inferred from global metadata. homepage: "https://clockify-cli.netlify.app/" # Your app's description. # # Templates: allowed. # Default: inferred from global metadata. description: "A simple cli to manage your time entries on Clockify from terminal" license: "asl20" # Repository to push the generated files to. repository: # Repository owner. # # Templates: allowed. owner: lucassabreu # Repository name. # # Templates: allowed. name: nur-packages # Optionally a branch can be provided. # # Default: default repository branch. # Templates: allowed. branch: main ================================================ FILE: .mockery.yaml ================================================ dir: internal/mocks template: testify template-data: unroll-variadic: true packages: github.com/lucassabreu/clockify-cli/internal/mocks: interfaces: Client: configs: - filename: "mock_Client.go" Config: configs: - filename: "mock_Config.go" Factory: configs: - filename: "mock_Factory.go" ================================================ FILE: .nvimrc ================================================ set spell set spelllang=en set textwidth=79 set colorcolumn=80 let g:goyo_width = 103 autocmd FileType markdown setlocal ts=2 sts=2 sw=2 expandtab textwidth=99 colorcolumn=100 autocmd FileType markdown setlocal nofoldenable autocmd BufRead,BufNewFile *.md setlocal spell wrap ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [v0.63.0] - 2026-03-26 ### Added - when not informed in the command new time entries will read the `billable` flag from the Task or Project set ### Fixed - updated github workflows to use the newest versions of the actions - removed unused code - fixed assorted lint errors ### Thanks Thank you to [@reva](https://github.com/reva) for the improvements on [#292](https://github.com/lucassabreu/clockify-cli/pull/292). ## [v0.62.0] - 2026-03-20 ### Added - prompt for API URL on `config init` command to allow configuring different Clockify datacenters - new script cmd/release/main.go to help with new releases ### Fixed - changelog was not showing on the site ## [v0.61.1] - 2026-02-21 ### Fixed - when initializing the config the folder might not exist yet ## [v0.61.0] - 2026-02-21 ### Added - support to config file to be at `$HOME/.config` also, instead of just `$HOME`. ### Changed - hugo theme to be compatible with newer versions ## [v0.60.0] - 2026-02-11 ### Added - support to set which api to use with the client, this became necessary because of the EU datacenters ### Thanks Thank you to [@mbosc](https://github.com/mbosc) for the improvements on [#285](https://github.com/lucassabreu/clockify-cli/pull/285). ## [v0.59.0] - 2026-01-20 ### Changed - change url for API token creation ### Thanks Thank you to [@davidsneighbour](https://github.com/davidsneighbour) for the implementing the improvements on [#283](https://github.com/lucassabreu/clockify-cli/pull/283). ## [v0.58.0] - 2025-11-24 ### Added - reporting custom fields for the time entries ### Thanks Thank you to [@calebtrepowski](https://github.com/calebtrepowski) for the implementing the improvements on [#282](https://github.com/lucassabreu/clockify-cli/pull/282). ## [v0.57.0] - 2025-10-14 ### Added - documentation about nix packages ### Thanks Thank you to [@Sekky61](https://github.com/Sekky61) for the information on Issue [#280](https://github.com/lucassabreu/clockify-cli/pull/280). ## [v0.56.2] - 2025-09-30 ### Fixed - NUR repository name was wrong ## [v0.56.1] - 2025-09-30 ### Fixed - license is required ## [v0.56.0] - 2025-09-30 ### Added - support for nix packages ## [v0.55.2] - 2025-07-28 ### Fixed - update README section about installing using homebrew ## [v0.55.1] - 2025-07-28 ### Fixed - migration from homebrew Formula to Casks needed fixing ## [v0.55.0] - 2025-06-26 ### Added - support to limit how many time entries should be listed on the `report` commands, and choose which page to show ## [v0.54.2] - 2025-06-25 ### Fixed - deepsource suggestions - `last` alias on `show`, `clone`, `edit` and `edit-multiple` would select future time entries if they existed, now only time entries started before now will be considered ## [v0.54.1] - 2025-06-20 ### Fixed - when config "show-client" was on, printing time entries without projects were breaking the cli - goreleaser config deprecations - installing snapcraft from apt does not work anymore ### Thanks Thank you to [@melluh](https://github.com/melluh) for fixing the bug on PR [#275](https://github.com/lucassabreu/clockify-cli/pull/275). ## [v0.54.0] - 2024-06-15 ### Changed - markdown output now tries to resemble the time entry calendar dialog ## [v0.53.1] - 2024-06-14 ### Fixed - was printing the language before the duration as float ## [v0.53.0] - 2024-06-14 ### Added - new config `lang` to allow setting the number format to be used when printing - support to using client's name or id for autocompletion on bash - new config `timezone` to allow setting which timezone to use when reporting the time of a time entry ## [v0.52.0] - 2024-06-02 ### Added - new command `split` to allow break a time entry into others with break points ## [v0.51.1] - 2024-05-30 ### Fixed - when using `show-client` without `show-task` column headers became unaligned ## [v0.51.0] - 2024-05-29 ### Added - new config `show-client` that sets the reports/output of time entries to show its client, if exists ## [v0.50.1] - 2024-05-25 ### Fixed - snapcraft requires explicit confinement ## [v0.50.0] - 2024-05-25 ### Added - more unit tests ### Changed - using throttle/ticket providing system to limit requests per second to the clockify's api to prevent the error message: `Too Many Requests (code: 429)` - upgrade go version to 1.19 ## [v0.49.0] - 2024-03-29 ### Added - report subcommands now allowing passing multiple projects to search/filter - report subcommands now will search all the time entries of a client with the flag `--client` without using `--project` ## [v0.48.2] - 2024-02-22 ### Fixed - using name for id options with `[` in the name makes the cli panic ## [v0.48.1] - 2024-02-16 ### Fixed - match how strings are compared when using `allow-name-for-id` and filtering on interactive mode. ## [v0.48.0] - 2024-02-16 ### Added - new config `search-project-with-client` to set whether or not the cli should lookup projects using the client's name too ## [v0.47.0] - 2024-02-09 ### Added - new flag `--client` to filter projects by client when managing time entries ### Changed - `mockey` update and its configuration has changed - github actions steps updated to node20 ## [v0.46.0] - 2023-12-06 ### Added - support for the formats `HMM` and `HHMM` for time input ### Fixed - update github actions workflows to use newer actions ### Thanks Thank you to [@aVolpe](https://github.com/aVolpe) for implementing new time formats as input on PR [#251](https://github.com/lucassabreu/clockify-cli/pull/251). ## [v0.45.0] - 2023-08-05 ### Added - new function `since` to be used on the time entry format to help working with time - new function `until` to be used on the time entry format to help working with time ## [v0.44.2] - 2023-04-04 ### Fixed - when searching for task names with special characters ("-" for example), this was fixed for the filter ## [v0.44.1] - 2023-03-06 ### Fixed - time entries were created as billable without user input. - bump golang.org/x/text from 0.3.7 to 0.3.8 ([#244](https://github.com/lucassabreu/clockify-cli/pull/244)) - `go get` is not a supported option to install the cli ([#245](https://github.com/lucassabreu/clockify-cli/pull/245)) ### Added - test coverage for interactive mode components and `in` command. ### Thanks Thank you to [@diegoquintanav](https://github.com/diegoquintanav) for reporting and fixing the documentation on PR [#245](https://github.com/lucassabreu/clockify-cli/pull/245). ## [v0.44.0] - 2022-12-18 ### Added - new flag and config `interactive-page-size` to set how many entries should be shown on select prompts. - new command `task quick-add` to easily create multiple tasks on a project. ## [v0.43.0] - 2022-12-13 ### Added - support to `last` alias when deleting a time entry. ### Thanks Thank you to [@jjnilton](https://github.com/jjnilton) for fixing the issue [#229](https://github.com/lucassabreu/clockify-cli/issues/229) on PR [#238](https://github.com/lucassabreu/clockify-cli/pull/238). ## [v0.42.2] - 2022-12-06 ### Fixed - `duration` and `estimate` of a task can be `null`, and when the `estimate` where null the cli was failing with: "duration null is invalid" ## [v0.42.1] - 2022-12-01 ### Fixed - when the `duration` of a project is `null`, a error "duration null is invalid" was blocking the use of the cli ## [v0.42.0] - 2022-11-09 ### Added - test help function `runClient` to mock calls to Clockfy's API - added the following methods on `api.Client` (with test coverage) + UpdateProjectUserBillableRate + UpdateProjectUserCostRate + UpdateProjectEstimate + UpdateProjectMemberships + UpdateProjectTemplate + DeleteProject - added flag `hydrated` at `project list` to get "enriched" projects with their custom fields, tasks and memberships in one call, these can be accessed using the `format` or `json` output formats. - new command `project get` to show a project using its ID or name. ### Fixed - when running `edit` for a time entry that had a task to change its project, the command was failing if no task set, because it tried to find the task from the older project in the new one. ## [v0.41.0] - 2022-08-31 ### Added - new parameter called `allow-archived-tags` to allow selection of archived tags. - new flag `tag` on `report` commands, this will filter the time entries with all the tags informed. ## [v0.40.0] - 2022-08-09 ### Added - test coverage for `task` commands - new method `UpdateProject` on `api.Client` to update a project - new command `project edit` to allow batch editing multiple projects ### Fixed - using `\t` and `\n` on format output will behaviour as expected ## [v0.39.0] - 2022-07-31 ### Added - flags `--billable` and `--not-billable` to report commands to filter time entries that are billable or not respectively ## [v0.38.4] - 2022-07-27 ### Fixed - fixing pagination for time entry listing ## [v0.38.3] - 2022-07-26 ### Fixed - `config init` tests were broken - `report today` was using local timezone, which created the wrong range of time for the api ## [v0.38.2] - 2022-07-26 ### Added - tests for pkg/cmd/config - created a helper pkg for interactive console testing - tests for pkg/cmd/version - add codecov to pull requests ### Changed - `api.Client` changed into a interface to easy testing - user reports now show the user timezone - flag `debug` dropped in favor of `log-level` to allow a finer control of the output for reporting bugs. ### Fixed - `Client.WorkspaceUsers` was not paginating over the results, this created bugs on `config init` and `user list` - `make dist` was building all system to the same file ### Thanks Thank you to [@mhogerheijde](https://github.com/mhogerheijde) for fixing the issue [#204](https://github.com/lucassabreu/clockify-cli/issues/204). ## [v0.38.1] - 2022-07-05 ### Added - link to LICENSE added to README.md - link to CHANGELOG added to README.md ### Changed - function `bool2str` substituted with a map to appease deepsource.io - change task prompt to not list tasks that are inactive. ### Fixed - `in` command on interactive mode was exiting when the user tried to start a timer on a project were they don't have direct access to (only by their group). This is a bug on the API, but a fix was done to not block the users. ## [v0.38.0] - 2022-07-01 ### Added - badge with amount of downloads from github releases - most of the commands have better descriptions explaining flag usages and command examples. - document describing the [project layout][] and where to add or find files. - document with [how to contribute][contribute] to the project - site preview on branches going to `main`. - added `golang-lint` as a Github Action on every PR. - functions `json`, `yaml` and `pad` add to all golang template formatters. ### Changed - site specific files moved from `docs/` to `site/` to free docs folder for actual documentation. - moved `cmd/*` and `internal/output/*` into the new locations as stated on [project layout][] - new `cmdutil.Factory` interface to work as a "service locator" so sharing some states, behaviours and "services" can be easier. - project dependencies were updated. - memory and performance improvements - `config --init` changed to `config init` to better organized the commands logic. - site home page changed to better explain how to setup the project and to direct to new documents. ### Fixed - `report` commands could fail to list time entries closer to midnight because of timezone differences. ## [v0.37.0] - 2022-05-17 ### Added - build windows binaries ## [v0.36.2] - 2022-05-13 ### Fixed - `edit-multiple` was not updating time entries without interactive mode. ## [v0.36.1] - 2022-05-10 ### Fixed - `clone` command was using the start time of the copied time entry to close the current one instead of the start time of new one being created. ## [v0.36.0] - 2022-05-09 ### Added - support for relative time for time parameters, can be +1:40, or +1h40m ### Fixed - negative duration was printed broken, now it show as a valid negative duration ### Changed - new error types from required fields and invalid entity ids. - all errors have a minimal context to help on support. - `manageEntry` refactored to not have as many control flags - reduce copy of objects on loops ## [v0.35.1] - 2022-05-04 ### Fixed - fake "not found" errors will have more context for the message ### Changed - all `api/client.go` will have stack traces. ## [v0.35.0] - 2022-05-03 ### Added - `task edit` command allows changing a existing task and changing its status. - `task delete` command allows removing a existing task. - `task done` command is a helper for `task edit --done`. ### Changed - `task add` command now accepts `assignees` and `estimate` for task creation - `end` argument of `report` command accepts the alias `yesterday` for previous date. ## [v0.34.0] - 2022-04-27 ### Changed - `end` argument of `report` command accepts the aliases `now` and `today` for current date. ## [v0.33.1] - 2022-04-25 ### Fixed - enabling `show-task` config were hiding the description column for table report format. ## [v0.33.0] - 2022-04-21 ### Added - flag to filter projects on `report` command. ## [v0.32.2] - 2022-02-25 ### Fixed - examples on `README` were out of date with current commands and outputs. - short description of `show` and `report` were too long. ## [v0.32.1] - 2022-02-25 ### Removed - `log` subcommand removed (deprecated since [v0.28.0]) - `log in-progress` subcommand removed (deprecated since [v0.29.0]) ### Changed - `report` subcommand allows calls using the alias `log` ## [v0.32.0] - 2022-02-14 ### Added - new options `--random-color` when creating a project, to auto-generate a color for the project. ### Thanks Thank you to [@NoF0rte](https://github.com/NoF0rte) for these improvements to the CLI. ## [v0.31.0] - 2022-02-08 ### Added - new commands `task add` and `task list` to manage tasks on projects - new commands `client add` and `client list` to manage clients - new command `project add` create projects ### Changed - command `project list` has new parameter `clients` to filter only the projects related to the clients informed. ### Thanks Thank you to [@NoF0rte](https://github.com/NoF0rte) for these improvements to the CLI. ## [v0.30.1] - 2022-01-17 ### Fixed - `manual` subcommand was allowing creation of open time entries. ## [v0.30.0] - 2022-01-15 ### Changed - if creation of incomplete time entries is not allowed, the commands will verify if the project is active or not. - when closing a running time entry before creating a new one, the client will validate it before asking information on interactive mode. ### Fixed - archived projects were being shown as options to select in interactive mode, now only active are shown. ## [v0.29.0] - 2022-01-12 ### Changed - `show` subcommand has its parameter as optional, and shows current time entry by default when the parameter is omitted. - `report` subcommand has its parameters as optional, and use `today` as value when none is set. - `log in-progress` subcommand is deprecated in favor of `show`/`show current` ## [v0.28.0] - 2022-01-10 ### Changed - `log` subcommand deprecated in favor of `report` - default `report` now allows to set only one date of the range, in this situation it will treat start and end date as being the same. ### Added - new `report today` to show only the time entries from today, with `report` options - new `report yesterday` to show only the time entries from yesterday, with `report` options ### Fixed - golang commands were wrong - there was a output on report subcommands breaking the format - changelog release links were wrong ## [v0.27.1] - 2021-12-31 ### Fixed - `report last-month` was failing to create a valid range time because it was not truncated to 0 hours. ## [v0.27.0] - 2021-12-31 ### Changed - `formatTimeEntry` renamed into `printTimeEntry`, and simplified to just call `printTimeEntries` with a list containing the time entry informed. - go version on `go.mod` updated to 1.17 ### Added - all subcommands that can print more than one time entry will print the total duration for that listing, this can be disabled with the `config` subcommand. - `report` subcommands now have a `description` flag to filter time entries that contains text on its description. - all subcommands that output time entries now have two new formats: `duration-formatted` and `duration-float`, that do sum all durations of the time entries and print only the sum, formatted as time or as "floaty-hour", respectively ### Fixed - `report` subcommands which required pagination on the requests to the api were not doing so, the time entry list shown by this command was incomplete. ## [v0.26.1] - 2021-12-07 ### Changed - hide "interrupted" error from the output ### Fixed - removed `println` in the code breaking the component. - prevent error when no time has been chosen as output for `AskForDateTime` ## [v0.26.0] - 2021-11-02 ### Added - add description suggestion using the recent time entries. ### Changed - some code and style fixes detected by [deepsource](https://deepsource.io/) - refactored date-time flags into its own function. - refactored ask for date-time helper function to not have control flags. ## [v0.25.0] - 2021-10-08 ### Fixed - `report` subcommands were showing only the time not the date when the time entry was created. - `--quiet` help was wrong, it said "print as json", but it prints only the id. ### Added - project color is used to "render" project name on the terminal, if the output is being piped or redirected then colors will be ignored to prevent problems and miss-interpretation of the output. - `show` subcommand prints details about time entries without having to list of the time entries of a given date. - `edit`, `edit-multiple`, `show`, `clone` support "^n" expression to select a time entry to act on, "^0" is the same as "current", "^1" is the same as "last", "^2" chooses the time entry before the last one, etc. - new `md` (markdown) format to print time entries ## [v0.24.1] - 2021-09-20 ### Fixed - `out` subcommand was not setting the user to look on ending the time entry. - listing subcommands didn't show "hydrated" information about time entries, `GetUsersHydratedTimeEntries` was not telling the api to return hydrated data. ### Added - all client method calls now validated for required fields, this makes easier to see bugs and prevent errors to creping up into releases. ## [v0.24.0] - 2021-09-18 ### Added - new commands `mask-invoiced` and `mark-not-invoiced` created to allow users to set this information using the cli. ### Changed - creation/update/out of time entries is made using the current api, instead of the old one - listing of workspaces and users is made using the current api, instead of the old one - all specific calls for the api for listing time entries were refactored to use a main function to request then, the client methods still exist and maintain the same inputs/outputs, but are calling the same function instead of reimplementing the call every time - getting of a project now uses the current api - debug messages of requests now show a "name" on it to help identify what where the intention of the call ### Removed - client method for recent time entries was not listed as a valid api, so its is now removed. ## [v0.23.1] - 2021-09-17 ### Fixed - `last` and `current` aliases were failing to find and select the right time entry, it is a problem with the old api for getting "recent time entries", fixed by [@zerodahero](https://github.com/zerodahero) ## [v0.23.0] - 2021-09-16 ### Added - client uses current api to retrieve all tasks of a project - interactive mode support to select tasks - name or id support for tasks - terminal auto-complete support for `task` flag - new config `show-task` that sets the reports/output of time entries to show its task, if exists ### Fixed - package `golang.org/x/crypto/ssh/terminal` was deprecated, substituted by `golang.org/x/term` ### Removed - output formatters for `dto.TimeEntryImpl` were not being used. ## [v0.22.0] - 2021-09-05 ### Changed - use new go version (1.17) - custom `changed` function is the same as using `Flags.Changed`, changed to use just the later - use `hydrated` parameter on "get time entry" endpoint instead of getting details individually - change in progress time entry using the current api - using "Hydrated" instead of "Full" to be consistent will the api ### Fixed - remove default message for 404 errors from the api - `edit-multiple` without interactive mode were not working with the `allow-name-for-id` flag. ## [v0.21.0] - 2021-08-16 ### Fixed - deploy to Netlify was not being triggered after release build, making the html documentation always wrong. - using terminal size of stdout file descriptor, this may fix problems on windows to print reports. - special characters will be ignored when looking for a project or tag with similar name. ### Added - `--interactive` flag now describes how to disable it (suggestion from [#115](https://github.com/lucassabreu/clockify-cli/issues/115)) - example to create a time entry using only flags no README. - keep the same options to print/output on all commands that show time entries. - support for names for id for tags ### Changed - improved output examples to better resemble real output. - updated go dependencies - `reports` package renamed to `internal/output`, to prevent usage from other packages and solve ambiguity with `report` command and `report api` (to come) - flag `allow-project-name` now will be called `allow-name-for-id` to account for other entities that would benefit from using their names instead of their ids ### Removed - features about integration with github:issues, azure dev and trello will not be implemented, at least not in a foreseeable future. ## [v0.20.0] - 2021-08-10 ### Changed - `manual` and `in` commands now support the use of `--project`, `--description`, `--when` and `--when-to-close` flags besides existing positional arguments (now optional even without interactive mode). ### Added - shorthand names for flags `when`, `when-to-close`, `description`, `project` and `tag` ## [v0.19.5] - 2021-08-03 ### Fixed - select UI component can fail to return a valid option if the default value were not in the list, to prevent that if the default value is empty or not in the list, no default value will be set. ## [v0.19.4] - 2021-07-21 ### Fixed - `edit` command were resetting the start time to "now" if the user didn't set the `--when` flag. - `when` and `when-to-close` flags on `edit` help had the wrong description. ## [v0.19.3] - 2021-07-20 ### Fixed - `clone` should create a open time entry by default. ### Changed - `delete` command accepts multiple ids instead of just one. ## [v0.19.2] - 2021-07-20 ### Fixed - `in` and `clone` commands were starting at 0001-01-01 because the default value of the flag was not being read. ## [v0.19.1] - 2021-07-19 ### Fixed - `README` now contains updated help output. - `edit-multiple` help should be capitalized. ## [v0.19.0] - 2021-07-19 ### Added - subcommand `edit-multiple` allows the user to edit all properties (except for the time interval) of multiple time entries simultaneously. when not in interactive mode the user can choose exactly which properties to change and to keep. ### Changed - flags used for creation and edition of time entries are now centralized into three functions `addFlagsForTimeEntryCreation` to add flags used to create time entries, `addFlagsForTimeEntryEdit` for flags used on edition, and `fillTimeEntryWithFlags` to replicated the flag values into the time entry. ### Deprecated - flag `end-at` on edit subcommand will be removed in favor of `when-to-close` to be consistent with other subcommands. - flag `tags` on many subcommands will be removed in favor of `tag` to imply that its one by flag. ## [v0.18.1] - 2021-07-12 ### Fixed - when the input for start time is cancelled (ctrl+c), clockify-cli was blocking the user by looping on the field until a valid date-time string was used, or the process were killed. ### Changed - library `github.com/AlecAivazis/survey` updated to the latest version. - `README` updated to show new configurations. ## [v0.18.0] - 2021-07-08 ### Added - commands `in`, `clone` and `manual` will show a new "None" option on the projects list on the interactive mode if the workspace allows time entries without projects. - config `allow-incomplete` allows the user to set if they want to create "incomplete time entries" or to validated then before creation. Flag `--allow-incomplete` and environment variable `CLOCKIFY_ALLOW_INCOMPLETE` can be used for the same purpose. by default time entries will be validated. ### Changed - commands `in` and `clone` when creating an "open" time entry will not validate if the workspace requires a project or not, allowing the creation of open incomplete/invalid time entries, similar to the browser application. - `newEntry` function changed to `manageEntry` and will allow a callback to deal with the filled and validated time entry instead of always creating a new one, that way same code that were duplicated between it and the `edit` command can be united. ### Fixed - `no-closing` configuration was removed, because was not used anymore. ## [v0.17.2] - 2021-06-17 ### Fixed - goreleaser needs a GitHub token with more permissions to create the homebrew Formulae. ## [v0.17.1] - 2021-06-16 ### Changed - changing travis ci for gihub actions, seens easier to use and one less login to handle ## [v0.17.0] - 2021-06-16 ### Added - command `report last-day`, this command will list time entries from the last day the user worked. - command `report last-week-day`, this command will look for the last day were the user should have worked (based on the new config `workweek-days`) and list the time entries for that day. - config `workweek-days` for the user to set which days of the week they work. it can be set interactively. ## [v0.16.1] - 2021-06-16 ### Fixed - interactive selection of project would panic if the list were empty (filtering can empty the list) and pressing enter. now will return as "no project selected". ### Changed - `workspaces` command is now named `workspace`, `workspaces` still supported - `workspace` default print format now shows the workspace marked as "default" ## [v0.16.0] - 2021-05-14 ### Added - `project list` can print the projects as JSON and CSV. - `project list` command default print format shows the client name and id ## [v0.15.1] - 2020-09-30 ### Fixed - if the workspace has more the one page of projects, in interactive mode, only the first page was being shown. now fixed to run over all pages to fill the list. ### Added - "Getting Started" section on README.md to help new users to setup theirs environment. ## [v0.15.0] - 2020-09-12 ### Added - support for command line completion on `fish`, `bash` and `zsh` for subcommands and flag name's - command line completion for arguments and flags for Tags, Projects, Workspaces and Users. - alias `remove` to command `delete` ### Changed - using the API `v1` version to get tags available to a workspace. - `api.Client.Workspaces` renamed to `api.Client.GetWorkspaces` to follow pattern used on other functions. - command `config`, `config set` and `config init` combined to be only one command `config` - improvements on help of many commands to show usable values. - `github.com/spf13/cobra` updated to latest possible current version to use completion improvements not yet released - "interactive mode" functions moved to a separate package. ## [v0.14.1] - 2020-09-09 ### Fixed - the project select on interactive mode was not respecting the "default" project when cloning or informed through flags/parameters ## [v0.14.0] - 2020-09-08 ### Changed - ask for "interactive mode" and "auto-closing" global configurations on `config init` command. ## [v0.13.0] - 2020-09-08 ### Added - select and multi-select interactive now support "glob like" expressions to filter a option ### Changed - client name of a project is shown on interactive mode to help identify the project. ### Fixed - select and multi-select options now support "non-english" characters like "á" by converting then to a ASCII equivalent character. ## [v0.12.2] - 2020-09-04 ### Fixed - flag `--token` help was not showing the right env var name. ## [v0.12.1] - 2020-08-22 ### Added - "How to install" section on README to help new users to understand which options are available. ### Fixed - improving the "homebrew tap" to allow installation using: `brew install lucassabreu/tap/clockify-cli` ## [v0.12.0] - 2020-08-31 ### Added - support to homebrew for macOs users. ## [v0.11.0] - 2020-08-22 ### Added - new `delete` command to remove a existing time entry from a workspace. - `edit` command support to interactive mode. ### Fixed - when cloning a time entry, using interactive mode, the tags selected were not being respected. - `edit` command was removing all data from time-entry if the flag to fill the field was not being set. ## [v0.10.1] - 2020-08-10 ### Fixed - `in` and `manual` command were showing a error "Project '' informed was not found", even when no project id/name is informed, this is now fixed. ## [v0.10.0] - 2020-08-07 ### Added - `clone` command now allow to change the project and description on the time entry creation, interactive mode already had this possibility - new flag `archived` on `project list` to list archived projects - a new global config `allow-project-name` that, when enabled, allow the user to the project name (or parts of it) to be used where the project id is asked. - common function to get all pages on a paginated request, to not reimplement it, and guarantee all entities are being used/returned. ### Fixed - `clone` sub-command was not asking to confirm the tags when the original time entry already had some. - `clone` command now will respect flags `--tags` and `--when-to-close`. - "billable" attribute was not being cloned - keep the current CHANGELOG when extracting the last tag - some grammatic errors ("applyied" => applied) - remove mentions to GitHub or Trello token, until integration is implemented ## [v0.9.0] - 2020-07-20 ### Added - new sub-command `version` to allow a quick way to know which version is installed - sub-command `report` now supports `this-week` and `last-week` as time range aliases listing respectively all entries which start this week, and all entries that happened on previous week. ### Changed - all relevant errors now have stack trace with then, which will be printed when the flag `--debug` is used. - error reporting now centralized, removing the need for a helper function in each sub-command - `report`command default output (table) with show in which day the times entries were made. ## [v0.8.1] - 2020-07-09 ### Fixed - `clone` sub-command was not working because the `no-closing` viper config was being connected with a non-existing `--no-closing` flag in the `in` sub-command, that does not exist anymore. ## [v0.8.0] - 2020-07-08 ### Added - created a new sub-command `manual` that will allow to create "completed" time entries in a more easy way. - created a new flag `--when-to-close` on `in` and `clone` to set close time for the time entry being started (if wanted). ### Changed - `clone` sub-command allows the flag `--no-closing` and will have the same flags as `in` to set start and end time (if wanted) - `in` sub-command will always stops time entries that are open in the moment of the sub-command call. - some helps and messages were improved to better describe what the command does ### Removed - flags `--trello-token` and `--github-token` were removed because they are not currently used and may give false impressions about the cli ### Fixed - some code for the in and clone sub-commands were duplicated, now they are in `newEntry` function that they both used. ## [v0.7.2] - 2020-06-21 ### Fixed - using JSON to notify Netlify, to prevent "malformed url errors" ## [v0.7.1] - 2020-06-21 ### Fixed - snapcraft build/release problems after Travis config update ## [v0.7.0] - 2020-06-21 ### Added - build every pull request as a snapshot to check if it is not failing - command to auto-generated hugo formatted markdown files from the commands - implemented a site to better help people to understand what the CLI does, without having to download it (live on: https://clockify-cli.netlify.app/) ### Changed - improved headers on the CHANGELOG to better represent the hierarchies - moved `in clone` to be just `clone` ### Fixed - missing release links for the title on the CHANGELOG - filling the brackets on the LICENSE file ## [v0.6.1] - 2020-06-16 ### Added - `config` command can print the "global" parameters in `json` or `yaml` - `config` now accepts a argument, which is the name of the parameter, when informed only this parameter will be printed ## [v0.6.0] - 2020-06-16 ### Added - some badges, who does not like they? ### Fixed - help was showing `CLOCKIFY_WROKSPACE` as env var for workspace, the right name is `CLOCKIFY_WORKSPACE` - fixed some `golint` warnings ### Changed - go mod dependencies updated - `snapcraft` package only requires network ### Removed - Removed `GetCurrentUser` in favor of `GetMe` to be closer to the APIs format ## [v0.5.0] - 2020-06-15 ### Changed - `in`, `log` and `report` now don't require you to inform a "user-id", if none is set, than will get the user id from the token used to access the api ### Added - `me` command returns information about the user who owns the token used to access the clockify's api ## [v0.4.0] - 2020-06-01 ### Added - table format will show time entry tags ### Changed - when adding fake entries with `--fill-missing-dates`, will set end time as equal to start time, so the duration will be 0 seconds ## [v0.3.2] - 2020-05-22 ### Changed - printing duration as "h:mm:ss" instead of the Go's default format, because is more user and sheet applications friendly. ## [v0.3.1] - 2020-04-01 ### Fixed - fixed `--no-closing` being ignored - interactive flow of `clone` was keeping previous time interval ## [v0.3.0] - 2020-04-01 ### Fixed - minor grammar bug fixes ### Changed - improvements to the code moving interactive logic of the "in" command into `cmd/common.go` - "in clone" is now interactive and will ask the user to confirm the time entry data before creating it. ## [v0.2.2] - 2020-03-18 ### Fixed - the endpoint `workspaces//tags/` does not exist anymore, instead the `api.Client` will get all tags of the workspace (`api.Client.GetTags`) and filter the response to find the tag by its id. ## [v0.2.1] - 2020-03-02 ### Fixed - `clockify-cli report` parameter `--fill-missing-dates`, was not working ## [v0.2.0] - 2020-03-02 ### Added - `clockify-cli report --fill-missing-dates` when this parameters is set, if there are dates from the range informed, will be created "stub" entries to better show that are missing entries. ## [v0.1.7] - 2020-02-03 ### Added - `api.Client` now supports getting one specific time entry from a workspace, without the need to paginate through all time entries to find it (`GetTimeEntry` function). ### Fixed - `clockify-cli report` was not getting all pages from the period, implemented support for pagination and to get "all pages" at once into `Client.Log` and `Client.LogRange` ### Changed - updated README, so it shows the `--help` output as it is now ## [v0.1.6] - 2020-02-03 ### Fixed - fixed bug after Clockify's API changed, where `user` and `project` are not automatically provided by the "time-entries" endpoint, unless sending an extra parameter `hydrated=true`, and `user` is not provided anymore, so now we find it using the user id from the function filter ## [v0.1.5] - 2020-01-08 ### Fixed - fixed bug on the `log` commands, where the previous api url is not available anymore, now using `v1/workspace/{workspace}/user/{user}/times-entries` - spelling of some words fixed and improving some aspects of the code ### Changed - `go.mod` updated ### Added - seamless support for query parameters using the interface `QueryAppender` - support for retrieving the current user of the token (`v1/user`) in the API client. - `.nvimrc` added to provide spell check ## [v0.1.4] - 2019-08-05 ### Added - Permissions to `snap` installation, so configuration file can be used ## [v0.1.3] - 2019-08-02 ### Changed - Set `publish` to `true` so it will be sent to `snapcraft` ## [v0.1.2] - 2019-08-02 ### Added - Add release to snapcraft by the name `clockify-cli` - Add command `clockify-cli report` implemented to generate bigger exports. CSV, JSON, `gofmt` and table formats allowed in this command. ## [v0.1.1] - 2019-06-10 ### Changed - The list returned by the `log` command will the sorted starting from the oldest time entry. ## [v0.1.0] - 2019-04-08 ### Added - Add `goreleaser` to manage binary and releases of the command - `clockify-cli in` asks user about new entry information when `interactive` is enabled - Command `clockify-cli config init` allows to start a fresh setup, creating a configuration file - Command `clockify-cli config set` updates/creates one configuration key into the configuration file - `clockify-cli in` commands now allow more flexible time format inputs, can be: hh:mm, hh:mm:ss, yyyy-mm-dd hh:mm or yyyy-mm-dd hh:mm:ss - Command `clockify-cli out` implemented, it will close any pending time entry, and show the last entry info when closing it with success - Command `clockify-cli in clone` implemented, to allow creation of new time entries based on existing ones, it also close pending ones, if any - Command `clockify-cli project list` was implemented, it allows to list the projects of a workspace, format the return to table, json, and just id. Helps with script automation - Using https://github.com/spf13/viper to link environment variables and configuration files with the global flags. User can set variables `CLOCKIFY_TOKEN`, `CLOCKIFY_WORKSPACE` and `CLOCKIFY_USER_ID` instead of using the command flags - Command `clockify-cli tags` created, to list workspace tags - Command `clockify-cli in` implemented, to allow creation of new time entries, it also close pending ones, if any - Command `clockify-cli edit ` implemented, to allow updates on time entries, including the in-progress one using the id: "current - `--debug` option to allow better understanding of the requests - Command `clockify-cli log in-progress` implemented, with options to format the output, and in the TimeEntry format, instead of TimeEntryImpl - Command `clockify-cli log` implemented, with options to format the output, will require the user for now - Package `dto` created to hold all payload objects - Package `api.Client` to call Clockfy's API - Command `clockify-cli workspaces` created, with options to format the output - Command `clockify-cli workspaces users` created, with options to format the output to allow retrieving the user's ID ## [v0.0.1] - 2019-03-03 ### Added - This CHANGELOG file to hopefully serve as an evolving example of a standardized open source project CHANGELOG. - README now show which features are expected, and that nothings is done yet - Golang CLI using [cobra](https://github.com/spf13/cobra) - Makefile to help setup actions [Unreleased]: https://github.com/lucassabreu/clockify-cli/compare/v0.63.0...HEAD [v0.63.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.63.0 [v0.62.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.62.0 [v0.61.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.61.1 [v0.61.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.61.0 [v0.60.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.60.0 [v0.59.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.59.0 [v0.58.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.58.0 [v0.57.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.57.0 [v0.56.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.56.2 [v0.56.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.56.1 [v0.56.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.56.0 [v0.55.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.55.2 [v0.55.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.55.1 [v0.55.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.55.0 [v0.54.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.54.2 [v0.54.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.54.1 [v0.54.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.54.0 [v0.53.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.53.1 [v0.53.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.53.0 [v0.52.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.52.0 [v0.51.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.51.1 [v0.51.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.51.0 [v0.50.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.50.1 [v0.50.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.50.0 [v0.49.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.49.0 [v0.48.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.48.2 [v0.48.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.48.1 [v0.48.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.48.0 [v0.47.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.47.0 [v0.46.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.46.0 [v0.45.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.45.0 [v0.44.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.44.2 [v0.44.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.44.1 [v0.44.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.44.0 [v0.43.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.43.0 [v0.42.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.42.2 [v0.42.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.42.1 [v0.42.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.42.0 [v0.41.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.41.0 [v0.40.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.40.0 [v0.39.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.39.0 [v0.38.4]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.38.4 [v0.38.3]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.38.3 [v0.38.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.38.2 [v0.38.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.38.1 [v0.38.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.38.0 [v0.37.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.37.0 [v0.36.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.36.2 [v0.36.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.36.1 [v0.36.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.36.0 [v0.35.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.35.1 [v0.35.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.35.0 [v0.34.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.34.0 [v0.33.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.33.1 [v0.33.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.33.0 [v0.32.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.32.2 [v0.32.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.32.1 [v0.32.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.32.0 [v0.31.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.31.0 [v0.30.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.30.1 [v0.30.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.30.0 [v0.29.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.29.0 [v0.28.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.28.0 [v0.27.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.27.1 [v0.27.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.27.0 [v0.26.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.26.1 [v0.26.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.26.0 [v0.25.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.25.0 [v0.24.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.24.1 [v0.24.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.24.0 [v0.23.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.23.1 [v0.23.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.23.0 [v0.22.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.22.0 [v0.21.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.21.0 [v0.20.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.20.0 [v0.19.5]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.19.5 [v0.19.4]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.19.4 [v0.19.3]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.19.3 [v0.19.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.19.2 [v0.19.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.19.1 [v0.19.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.19.0 [v0.18.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.18.1 [v0.18.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.18.0 [v0.17.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.17.2 [v0.17.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.17.1 [v0.17.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.17.0 [v0.16.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.16.1 [v0.16.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.16.0 [v0.15.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.15.1 [v0.15.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.15.0 [v0.14.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.14.1 [v0.14.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.14.0 [v0.13.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.13.0 [v0.12.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.12.1 [v0.12.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.12.0 [v0.11.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.11.0 [v0.10.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.10.1 [v0.10.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.10.0 [v0.9.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.9.0 [v0.8.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.8.1 [v0.8.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.8.0 [v0.7.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.7.2 [v0.7.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.7.1 [v0.7.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.7.0 [v0.6.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.6.1 [v0.6.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.6.0 [v0.5.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.5.0 [v0.4.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.4.0 [v0.3.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.3.2 [v0.3.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.3.1 [v0.3.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.3.0 [v0.2.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.2.2 [v0.2.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.2.1 [v0.2.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.2.0 [v0.1.7]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.7 [v0.1.6]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.6 [v0.1.5]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.5 [v0.1.4]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.4 [v0.1.3]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.3 [v0.1.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.2 [v0.1.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.1 [v0.1.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.0 [v0.0.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.0.1 [project layout]: https://github.com/lucassabreu/clockify-cli/blob/main/docs/project-layout.md [contribute]: https://github.com/lucassabreu/clockify-cli/blob/feat/factory/CONTRIBUTING.md ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Thank you for the interest in Contributing to Clockify CLI. We accept pull requests for bug fixes and features (preferably that were discussed on an issue before). Also opening issues with feature requests and reporting bugs are very important contributions. Please do: - Check in the [issues][issues] if the [bug][bugs] or [feature request][enhancement] has not been submitted. - Open an issue if things aren't working as expected. - Open an issue to propose new features or improvements on existing ones. - Open a pull request to fix a [bug][bugs]. - Open a pull request for any open issue labelled [`type: enhancement`][enhancement]. Please avoid: - Opening pull requests for issues marked as `blocked`. All enhancement and bug issues are marked with a `level` label, it may help you know the size/complexity of it. ## Building the project Prerequisites: - Go 1.19+ Run `make deps-install` to install the packages used by the project. Run `make deps-upgrade` if you need to upgrade all of them, run `go help get` to see how to update individual ones. To build your changes into a binary run `make dist`, all three versions (Windows, Mac and Linux) will be created under the `dist/` folder. You can also just run `go run cmd/clockify-cli/main.go` to execute the source directly. See the [project layout documentation][project layout] to know where to find and create specific components. ## Submitting a pull request Contributions to this project are [released][legal] to the public under the [project's open source license][license]. By participating in this project you agree to abide by its terms. We generate manual pages from source on every release. You do not need to submit pull requests for documentation specifically; manual pages for commands will automatically get updated after your pull requests gets accepted. ### With [`gh`][gh] 1. Clone this repository 2. Make and commit your changes. 3. Submit a pull request: `gh pr create --web` 4. In its body link which issue it is related, if there is one ### Without `gh` 1. [Fork the repository][fork] 2. Make and commit your changes 3. [Open a pull request][open-pr] 4. In its body link which issue it is related, if there is one ## Resources - [How to Contribute to Open Source][] - [Using Pull Requests][] - [GitHub Help][] ## Credits This document is based on the [CONTRIBUTING.md from github/cli/cli][credit]. [fork]: https://github.com/lucassabreu/clockify-cli/fork [open-pr]: https://github.com/lucassabreu/clockify-cli/compare [credit]: https://github.com/cli/cli/blob/trunk/.github/CONTRIBUTING.md [issues]: https://github.com/lucassabreu/clockify-cli/issues [bugs]: https://github.com/lucassabreu/clockify-cli/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+bug%22 [enhancement]: https://github.com/lucassabreu/clockify-cli/issues?q=is%3Aissue+is%3Aopen+label%3A%22type%3A+enhancement%22 [project layout]: ./docs/project-layout.md [gh]: https://github.com/cli/cli [legal]: https://docs.github.com/en/free-pro-team@latest/github/site-policy/github-terms-of-service#6-contributions-under-repository-license [license]: ./LICENSE [How to Contribute to Open Source]: https://opensource.guide/how-to-contribute/ [Using Pull Requests]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/about-pull-requests [GitHub Help]: https://docs.github.com/ ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2020 Lucas dos Santos Abreu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ export GO111MODULE=on MAIN_PKG=./cmd/clockify-cli # Absolutely awesome: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html help: ## show this help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' clean: ## clean all buildable files rm -rf dist deps-install: ## install golang dependencies go mod download deps-upgrade: ## upgrade go dependencies go get -u -v $(MAIN_PKG) go mod tidy build: clean dist dist: deps-install dist/darwin dist/linux dist/windows ## build all cli versions (default) dist-internal: mkdir -p dist/$(goos) GOOS=$(goos) GOARCH=$(goarch) go build -o dist/$(goos)/clockify-cli $(MAIN_PKG) dist/darwin: make dist-internal goos=darwin goarch=amd64 dist/linux: make dist-internal goos=linux goarch=amd64 dist/windows: make dist-internal goos=windows goarch=amd64 go-install: deps-install ## install dev version go install $(MAIN_PKG) go-generate: deps-install ## recreates generate files go install github.com/vektra/mockery/v3@v3.4.0 mockery test-install: deps-install go-generate go install gotest.tools/gotestsum@latest test: test-install ## runs all tests gotestsum --format dots-v2 test_coverprofile=coverage.txt test_covermode=atomic test-coverage: test-install ## runs all tests and output coverage gotestsum --format dots-v2 -- \ -coverprofile=$(coverprofile) \ -covermode=$(covermode) \ ./... test-watch: test-install ## runs all tests and watch changes gotestsum --format testname --watch -- -failfast goreleaser-test: tag=Unreleased goreleaser-test: release ifeq ($(tag),Unreleased) SNAPSHOT=1 endif tag= release: ## releases a tagged version sed "/^## \[$(tag)/, /^## \[/!d" CHANGELOG.md | tail -n +2 | head -n -2 > /tmp/rn.md curl -sL https://git.io/goreleaser | bash /dev/stdin --release-notes /tmp/rn.md \ --clean $(if $(SNAPSHOT),--snapshot --skip=publish,) ifneq ($(SNAPSHOT),1) curl -X POST -d '{"trigger_branch":"$(tag)","trigger_title":"Releasing $(tag)"}' https://api.netlify.com/build_hooks/5eef4f99028bddbb4093e4c6 -v endif site/themes/hugo-theme-relearn/.git: git submodule update --init site-build: site/themes/hugo-theme-relearn/.git ## generates command documents and builds the site ./scripts/site-build site-serve: site-build ## builds the site, and serves it locally cd site && hugo serve create-release-minor: ## create a new minor release go run cmd/release/main.go minor create-release-patch: ## create a new patch release go run cmd/release/main.go patch ================================================ FILE: README.md ================================================ ![Clockify CLI](https://repository-images.githubusercontent.com/173476481/3445a278-9bb9-49e9-8c99-d10c76574489) ============ A simple cli to manage your time entries and projects on Clockify from terminal [![Release](https://img.shields.io/github/release/lucassabreu/clockify-cli.svg?classes=badges)](https://github.com/lucassabreu/clockify-cli/releases/latest) [![GitHub all releases](https://img.shields.io/github/downloads/lucassabreu/clockify-cli/total)](https://github.com/lucassabreu/clockify-cli/releases) [![clockify-cli](https://snapcraft.io//clockify-cli/badge.svg?classes=badges)](https://snapcraft.io/clockify-cli) [![Build Status](https://github.com/lucassabreu/clockify-cli/actions/workflows/release.yml/badge.svg?classes=badges)](.github/workflows/release.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/lucassabreu/clockify-cli?classes=badges)](https://goreportcard.com/report/github.com/lucassabreu/clockify-cli) [![Netlify Status](https://api.netlify.com/api/v1/badges/8667b9f6-4ca2-4ee4-865e-20b5848e7059/deploy-status?classes=badges)](https://app.netlify.com/sites/clockify-cli/deploys) [![DeepSource](https://deepsource.io/gh/lucassabreu/clockify-cli.svg/?classes=badges&label=active+issues&show_trend=true&token=hkvNbnaRCE4DhtW6vDYpFWSR)](https://deepsource.io/gh/lucassabreu/clockify-cli/?ref=repository-badge) Documentation ------------- See the [project site](https://clockify-cli.netlify.app/) for the how to setup and use this CLI. See more information about the sub-commands at: https://clockify-cli.netlify.app/en/commands/clockify-cli/ Contributing ------------ On how to help improve the tool, suggest new features or report bugs, please take a look at the [CONTRIBUTING.md](CONTRIBUTING.md). Features -------- * [x] List time entries from a day * [x] List in progress entry * [x] Report time entries using a date range * [x] Inform date range as parameters * [x] "auto filter" for last month * [x] "auto filter" for this month * [x] Start a new time entry * [x] Cloning last time entry * [x] Ask input interactively * [x] Stop the last entry * [x] List workspace projects * [x] List Clockify Workspaces * [x] List Clockify Workspaces Users * [x] List Clockify Tags * [x] Edit time entry * [x] Configuration management * [x] Initialize configuration * [x] Update individual configuration * [x] Show current configuration How to install [![Powered By: GoReleaser](https://img.shields.io/badge/powered%20by-goreleaser-green.svg?classes=badges)](https://github.com/goreleaser) -------------- #### Using [`homebrew`](https://brew.sh/): ```sh brew install --cask lucassabreu/tap/clockify-cli ``` #### Using [`snapcraft`](https://snapcraft.io/clockify-cli) ```sh sudo snap install clockify-cli ``` #### Using `go install` ```sh go install github.com/lucassabreu/clockify-cli/cmd/clockify-cli@latest ``` The installed application for a default `go` installation should be located on your [$GOBIN path][go-envs]. You can add `$GOBIN` to your `$PATH`, or move it to a directory listed on your `$PATH` (e.g.: `/usr/local/bin`). [go-envs]: https://pkg.go.dev/cmd/go#hdr-Environment_variables #### Using `nix`/[NUR](http://github.com/nix-community/NUR) Add this input, overlay and package into your flake: ```nix # ... inputs = { nur = { url = "github:nix-community/NUR"; inputs.nixpkgs.follows = "nixpkgs"; }; }; # ... pkgs = import nixpkgs { inherit system; overlays = [ nur.overlays.default ]; }; # ... environment.systemPackages = with pkgs; [ nur.repos.lucassabreu.clockify-cli ]; ``` #### By Hand Go to the [releases page](https://github.com/lucassabreu/clockify-cli/releases) and download the pre-compiled binary that fits your system. Changelog --------- [Changelog](./CHANGELOG.md) License ------- [Apache License](LICENSE) ================================================ FILE: api/client.go ================================================ package api import ( "context" "encoding/hex" "fmt" "net/http" "net/url" "regexp" "strings" "time" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/strhlp" "github.com/pkg/errors" ) // Client will help to access Clockify API type Client interface { // SetDebugLogger when set will output the responses of requests to the // logger SetDebugLogger(logger Logger) Client // SetInfoLogger when set will output which requests and params are used to // the logger SetInfoLogger(logger Logger) Client GetWorkspace(GetWorkspace) (dto.Workspace, error) GetWorkspaces(GetWorkspaces) ([]dto.Workspace, error) GetMe() (dto.User, error) GetUser(GetUser) (dto.User, error) WorkspaceUsers(WorkspaceUsersParam) ([]dto.User, error) AddClient(AddClientParam) (dto.Client, error) GetClients(GetClientsParam) ([]dto.Client, error) // GetProjects get all project of a workspace GetProjects(GetProjectsParam) ([]dto.Project, error) // GetProject get a single Project, if exists GetProject(GetProjectParam) (*dto.Project, error) // AddProject creates a new project AddProject(AddProjectParam) (dto.Project, error) // UpdateProject changes basic information about the project UpdateProject(UpdateProjectParam) (dto.Project, error) // UpdateProjectUserCostRate will update the hourly rate of a user on a // project UpdateProjectUserBillableRate(UpdateProjectUserRateParam) ( dto.Project, error) // UpdateProjectUserCostRate will update the cost of a user on a project UpdateProjectUserCostRate(UpdateProjectUserRateParam) ( dto.Project, error) // UpdateProjectEstimate change how the estime of a project is measured UpdateProjectEstimate(UpdateProjectEstimateParam) (dto.Project, error) // UpdateProjectMemberships changes who has access to add time entries to // the project UpdateProjectMemberships(UpdateProjectMembershipsParam) (dto.Project, error) // UpdateProjectTemplate changes if a project is a template or not UpdateProjectTemplate(UpdateProjectTemplateParam) (dto.Project, error) // DeleteProject removes a project forever DeleteProject(DeleteProjectParam) (dto.Project, error) AddTask(AddTaskParam) (dto.Task, error) DeleteTask(DeleteTaskParam) (dto.Task, error) GetTask(GetTaskParam) (dto.Task, error) GetTasks(GetTasksParam) ([]dto.Task, error) UpdateTask(UpdateTaskParam) (dto.Task, error) GetTag(GetTagParam) (*dto.Tag, error) GetTags(GetTagsParam) ([]dto.Tag, error) ChangeInvoiced(ChangeInvoicedParam) error CreateTimeEntry(CreateTimeEntryParam) (dto.TimeEntryImpl, error) DeleteTimeEntry(DeleteTimeEntryParam) error GetHydratedTimeEntry(GetTimeEntryParam) (*dto.TimeEntry, error) GetHydratedTimeEntryInProgress(GetTimeEntryInProgressParam) (*dto.TimeEntry, error) GetTimeEntry(GetTimeEntryParam) (*dto.TimeEntryImpl, error) GetTimeEntryInProgress(GetTimeEntryInProgressParam) (*dto.TimeEntryImpl, error) GetUserTimeEntries(GetUserTimeEntriesParam) ([]dto.TimeEntryImpl, error) GetUsersHydratedTimeEntries(GetUserTimeEntriesParam) ([]dto.TimeEntry, error) Log(LogParam) ([]dto.TimeEntry, error) LogRange(LogRangeParam) ([]dto.TimeEntry, error) UpdateTimeEntry(UpdateTimeEntryParam) (dto.TimeEntryImpl, error) Out(OutParam) error } type client struct { baseURL *url.URL http.Client debugLogger Logger infoLogger Logger requestTickets chan struct{} } // BASE_URL is the Clockify API base URL const BASE_URL = "https://api.clockify.me/api" // REQUEST_RATE_LIMIT maximum number of requests per second const REQUEST_RATE_LIMIT = 50 // ErrorMissingAPIKey returned if X-Api-Key is missing var ErrorMissingAPIKey = errors.New("api Key must be informed") // ErrorMissingAPIURL returned if base url is missing var ErrorMissingAPIURL = errors.New("api URL must be informed") func NewClientFromUrlAndKey( apiKey, urlString string, ) (Client, error) { if apiKey == "" { return nil, errors.WithStack(ErrorMissingAPIKey) } if urlString == "" { return nil, errors.WithStack(ErrorMissingAPIURL) } u, err := url.Parse(urlString) if err != nil { return nil, errors.WithStack(err) } return &client{ baseURL: u, Client: http.Client{ Transport: transport{ apiKey: apiKey, next: http.DefaultTransport, }, }, requestTickets: startRequestTick(REQUEST_RATE_LIMIT), }, nil } // NewClient create a new Client, based on: https://clockify.github.io/clockify_api_docs/ func NewClient(apiKey string) (Client, error) { return NewClientFromUrlAndKey( apiKey, BASE_URL, ) } func startRequestTick(limit int) chan struct{} { ch := make(chan struct{}, limit) running := true release := func() { i := len(ch) for i < limit { if !running { return } i = i + 1 ch <- struct{}{} } } go func() { release() for { select { case <-time.After(time.Second): go release() case <-context.Background().Done(): running = false defer close(ch) return } } }() return ch } // GetWorkspaces will be used to filter the workspaces type GetWorkspaces struct { Name string } // Workspaces list all the user's workspaces func (c *client) GetWorkspaces(f GetWorkspaces) ([]dto.Workspace, error) { var w []dto.Workspace r, err := c.NewRequest("GET", "v1/workspaces", nil) if err != nil { return w, err } _, err = c.Do(r, &w, "GetWorkspaces") if err != nil { return w, errors.Wrap(err, "get workspaces") } if f.Name == "" { return w, nil } var ws []dto.Workspace n := strhlp.Normalize(strings.TrimSpace(f.Name)) for i := 0; i < len(w); i++ { if strings.Contains(strhlp.Normalize(w[i].Name), n) { ws = append(ws, w[i]) } } return ws, nil } type field string const ( workspaceField = field("workspace") userIDField = field("user id") userOrGroupIDField = field("user or group") projectField = field("project id") timeEntryIDField = field("time entry id") nameField = field("name") taskIDField = field("task id") estimateMethodField = field("estimate method") estimateTypeField = field("estimate type") resetOptionField = field("reset option") ) // RequiredFieldError indicates that a field should be filled, but was not type RequiredFieldError struct { Field string } func (e RequiredFieldError) Error() string { return e.Field + " is required" } func required(values map[field]string) error { for f := range values { if values[f] == "" { return RequiredFieldError{Field: string(f)} } } return nil } var regexId = regexp.MustCompile("^[a-fA-F0-9]{24}$") // IsValidID checks if a string looks like a valid ID func IsValidID(id string) bool { return regexId.MatchString(id) } // InvalidIDError indicates that a field should be a valid ID, but it is not type InvalidIDError struct { Field string ID string } func (e InvalidIDError) Error() string { return e.Field + " (\"" + e.ID + "\") is not valid ID" } func checkIDs(ids map[field]string) error { for field, id := range ids { if !IsValidID(id) { return InvalidIDError{Field: string(field), ID: id} } } return nil } func checkWorkspace(workspace string) error { ids := map[field]string{workspaceField: workspace} if err := required(ids); err != nil { return err } return checkIDs(ids) } func wrapError(err *error, message string, args ...interface{}) { if err == nil { return } *err = errors.Wrapf(*err, message, args...) } type EntityNotFound struct { EntityName string ID string } func (e EntityNotFound) Error() string { return e.EntityName + " with id " + e.ID + " was not found" } func (e EntityNotFound) Unwrap() error { return dto.Error{Code: 404, Message: e.Error()} } type GetWorkspace struct { ID string } func (c *client) GetWorkspace(p GetWorkspace) (dto.Workspace, error) { var err error defer wrapError(&err, "get workspace %s", p.ID) if err = checkWorkspace(p.ID); err != nil { return dto.Workspace{}, errors.WithStack(err) } ws, err := c.GetWorkspaces(GetWorkspaces{}) if err != nil { return dto.Workspace{}, err } for i := 0; i < len(ws); i++ { if ws[i].ID == p.ID { return ws[i], nil } } err = EntityNotFound{ EntityName: "workspace", ID: p.ID, } return dto.Workspace{}, err } // WorkspaceUsersParam params to query workspace users type WorkspaceUsersParam struct { Workspace string Email string PaginationParam } // WorkspaceUsers all users in a Workspace func (c *client) WorkspaceUsers(p WorkspaceUsersParam) (users []dto.User, err error) { defer wrapError(&err, "get users") if err := checkWorkspace(p.Workspace); err != nil { return users, err } users, err = paginate[dto.User]( c, "GET", fmt.Sprintf("v1/workspaces/%s/users", p.Workspace), p.PaginationParam, dto.WorkspaceUsersRequest{ Email: p.Email, }, "WorkspaceUsers", ) return } // PaginationParam parameters about pagination type PaginationParam struct { AllPages bool Page int PageSize int } // AllPages sets the query to retrieve all pages func AllPages() PaginationParam { return PaginationParam{AllPages: true} } // LogParam params to query entries type LogParam struct { Workspace string UserID string Date time.Time PaginationParam } // Log list time entries from a date func (c *client) Log(p LogParam) ([]dto.TimeEntry, error) { c.infof("Log - Date Param: %s", p.Date) d := p.Date.Round(time.Hour) d = d.Add(time.Hour * time.Duration(d.Hour()) * -1) return c.LogRange(LogRangeParam{ Workspace: p.Workspace, UserID: p.UserID, FirstDate: d, LastDate: d.Add(time.Hour * 24), PaginationParam: p.PaginationParam, }) } // LogRangeParam params to query entries type LogRangeParam struct { Workspace string UserID string FirstDate time.Time LastDate time.Time Description string ProjectID string TagIDs []string PaginationParam } // LogRange list time entries by date range func (c *client) LogRange(p LogRangeParam) ([]dto.TimeEntry, error) { c.infof("LogRange - First Date Param: %s | Last Date Param: %s", p.FirstDate, p.LastDate) return c.GetUsersHydratedTimeEntries(GetUserTimeEntriesParam{ Workspace: p.Workspace, UserID: p.UserID, Start: &p.FirstDate, End: &p.LastDate, Description: p.Description, ProjectID: p.ProjectID, TagIDs: p.TagIDs, PaginationParam: p.PaginationParam, }) } type GetUserTimeEntriesParam struct { Workspace string UserID string OnlyInProgress *bool Start *time.Time End *time.Time Description string ProjectID string TagIDs []string PaginationParam } // GetUserTimeEntries will list the time entries of a user on a workspace, can be paginated func (c *client) GetUserTimeEntries(p GetUserTimeEntriesParam) ([]dto.TimeEntryImpl, error) { return getUserTimeEntriesImpl[dto.TimeEntryImpl](c, p, false) } // GetUsersHydratedTimeEntries will list hydrated time entries of a user on a workspace, can be paginated func (c *client) GetUsersHydratedTimeEntries(p GetUserTimeEntriesParam) ([]dto.TimeEntry, error) { timeEntries, err := getUserTimeEntriesImpl[dto.TimeEntry](c, p, true) if err != nil { return timeEntries, err } user, err := c.GetUser(GetUser{p.Workspace, p.UserID}) if err != nil { return timeEntries, err } for i := 0; i < len(timeEntries); i++ { timeEntries[i].User = &user } return timeEntries, err } func getUserTimeEntriesImpl[K dto.TimeEntry | dto.TimeEntryImpl]( c *client, p GetUserTimeEntriesParam, hydrated bool, ) (tes []K, err error) { defer wrapError(&err, "get time entries from user \"%s\"", p.UserID) ids := map[field]string{ workspaceField: p.Workspace, userIDField: p.UserID, } if err := required(ids); err != nil { return tes, err } if err := checkIDs(ids); err != nil { return tes, err } inProgressFilter := "nil" if p.OnlyInProgress != nil { if *p.OnlyInProgress { inProgressFilter = "true" } else { inProgressFilter = "false" } } c.infof( "GetUserTimeEntries - Workspace: %s | User: %s | In Progress: %s "+ "| Description: %s | Project: %s", p.Workspace, p.UserID, inProgressFilter, p.Description, p.ProjectID, ) r := dto.UserTimeEntriesRequest{ OnlyInProgress: p.OnlyInProgress, Hydrated: &hydrated, Description: p.Description, Project: p.ProjectID, TagIDs: p.TagIDs, } if p.Start != nil { r.Start = &dto.DateTime{Time: *p.Start} } if p.End != nil { r.End = &dto.DateTime{Time: *p.End} } tes, err = paginate[K]( c, "GET", fmt.Sprintf( "v1/workspaces/%s/user/%s/time-entries", p.Workspace, p.UserID, ), p.PaginationParam, r, "GetUserTimeEntries", ) return } func paginate[K any]( c *client, method, uri string, p PaginationParam, request dto.PaginatedRequest, name string, ) ([]K, error) { page := p.Page if p.AllPages { page = 1 } if p.PageSize == 0 { p.PageSize = 50 } var ls []K stop := false for !stop { r, err := c.NewRequest( method, uri, request.WithPagination(page, p.PageSize), ) if err != nil { return ls, err } var response []K _, err = c.Do(r, &response, name) if err != nil { return ls, err } count := len(response) if count > 0 { ls = append(ls, response...) } stop = count < p.PageSize || !p.AllPages page++ } return ls, nil } // GetTimeEntryInProgressParam params to query entries type GetTimeEntryInProgressParam struct { Workspace string UserID string } // GetTimeEntryInProgress show time entry in progress (if any) func (c *client) GetTimeEntryInProgress(p GetTimeEntryInProgressParam) (timeEntryImpl *dto.TimeEntryImpl, err error) { b := true ts, err := c.GetUserTimeEntries(GetUserTimeEntriesParam{ Workspace: p.Workspace, UserID: p.UserID, OnlyInProgress: &b, PaginationParam: PaginationParam{PageSize: 1}, }) if err != nil { return } if err == nil && len(ts) > 0 { timeEntryImpl = &ts[0] } return } // GetHydratedTimeEntryInProgress show hydrated time entry in progress (if any) func (c *client) GetHydratedTimeEntryInProgress(p GetTimeEntryInProgressParam) (timeEntry *dto.TimeEntry, err error) { b := true ts, err := c.GetUsersHydratedTimeEntries(GetUserTimeEntriesParam{ Workspace: p.Workspace, UserID: p.UserID, OnlyInProgress: &b, }) if err == nil && len(ts) > 0 { timeEntry = &ts[0] } return } // GetTimeEntryParam params to get a Time Entry type GetTimeEntryParam struct { Workspace string TimeEntryID string ConsiderDurationFormat bool } // GetTimeEntry will retrieve a Time Entry using its Workspace and ID func (c *client) GetTimeEntry(p GetTimeEntryParam) (timeEntry *dto.TimeEntryImpl, err error) { defer wrapError(&err, "get time entry \"%s\"", p.TimeEntryID) ids := map[field]string{ workspaceField: p.Workspace, timeEntryIDField: p.TimeEntryID, } if err = required(ids); err != nil { return nil, err } if err = checkIDs(ids); err != nil { return nil, err } r, err := c.NewRequest( "GET", fmt.Sprintf( "v1/workspaces/%s/time-entries/%s", p.Workspace, p.TimeEntryID, ), dto.GetTimeEntryRequest{ ConsiderDurationFormat: &p.ConsiderDurationFormat, }, ) if err != nil { return timeEntry, err } _, err = c.Do(r, &timeEntry, "GetTimeEntry") return timeEntry, err } func (c *client) GetHydratedTimeEntry(p GetTimeEntryParam) (timeEntry *dto.TimeEntry, err error) { defer wrapError(&err, "get hydrated time entry \"%s\"", p.TimeEntryID) ids := map[field]string{ workspaceField: p.Workspace, timeEntryIDField: p.TimeEntryID, } if err = required(ids); err != nil { return nil, err } if err = checkIDs(ids); err != nil { return nil, err } b := true r, err := c.NewRequest( "GET", fmt.Sprintf( "v1/workspaces/%s/time-entries/%s", p.Workspace, p.TimeEntryID, ), dto.GetTimeEntryRequest{ ConsiderDurationFormat: &p.ConsiderDurationFormat, Hydrated: &b, }, ) if err != nil { return timeEntry, err } _, err = c.Do(r, &timeEntry, "GetHydratedTimeEntry") return timeEntry, err } // GetTagParam params to find a tag type GetTagParam struct { Workspace string TagID string } // GetTag get a single tag, if it exists func (c *client) GetTag(p GetTagParam) (*dto.Tag, error) { tags, err := c.GetTags(GetTagsParam{ Workspace: p.Workspace, }) if err != nil { return nil, err } for i := 0; i < len(tags); i++ { if tags[i].ID == p.TagID { return &tags[i], nil } } return nil, errors.Errorf( "tag %s not found on workspace %s", p.TagID, p.Workspace) } // GetProjectParam params to get a Project type GetProjectParam struct { Workspace string ProjectID string Hydrate bool } // GetProject get a single Project, if exists func (c *client) GetProject(p GetProjectParam) (pr *dto.Project, err error) { defer wrapError(&err, "get project \"%s\"", p.ProjectID) ids := map[field]string{ workspaceField: p.Workspace, projectField: p.ProjectID, } if err = required(ids); err != nil { return pr, err } if err = checkIDs(ids); err != nil { return pr, err } r, err := c.NewRequest( "GET", fmt.Sprintf( "v1/workspaces/%s/projects/%s", p.Workspace, p.ProjectID, ), dto.GetProjectRequest{Hydrated: p.Hydrate}, ) if err != nil { return pr, err } _, err = c.Do(r, &pr, "GetProject") if p.Hydrate && pr != nil { pr.Hydrated = true } return pr, err } // GetUser params to get a user type GetUser struct { Workspace string UserID string } // GetUser filters the wanted user from the workspace users func (c *client) GetUser(p GetUser) (dto.User, error) { var err error defer wrapError(&err, "get user \"%s\"", p.UserID) ids := map[field]string{ workspaceField: p.Workspace, userIDField: p.UserID, } if err = required(ids); err != nil { return dto.User{}, err } if err = checkIDs(ids); err != nil { return dto.User{}, err } us, err := c.WorkspaceUsers(WorkspaceUsersParam{ Workspace: p.Workspace, PaginationParam: AllPages(), }) if err != nil { return dto.User{}, errors.Wrapf(err, "get user %s", p.UserID) } for i := 0; i < len(us); i++ { if us[i].ID == p.UserID { return us[i], nil } } err = EntityNotFound{ EntityName: "user", ID: p.UserID, } return dto.User{}, err } // GetMe get details about the user who created the token func (c *client) GetMe() (dto.User, error) { r, err := c.NewRequest("GET", "v1/user", nil) if err != nil { return dto.User{}, err } var user dto.User _, err = c.Do(r, &user, "GetMe") return user, err } // GetTasksParam param to find tasks of a project type GetTasksParam struct { Workspace string ProjectID string Active bool Name string PaginationParam } // GetTasks get tasks of a project func (c *client) GetTasks(p GetTasksParam) (ps []dto.Task, err error) { defer wrapError(&err, "get tasks from project \"%s\"", p.ProjectID) ids := map[field]string{ workspaceField: p.Workspace, projectField: p.ProjectID, } if err = required(ids); err != nil { return ps, err } if err = checkIDs(ids); err != nil { return ps, err } ps, err = paginate[dto.Task]( c, "GET", fmt.Sprintf( "v1/workspaces/%s/projects/%s/tasks", p.Workspace, p.ProjectID, ), p.PaginationParam, dto.GetTasksRequest{ Name: p.Name, Active: p.Active, }, "GetTasks", ) return ps, err } // GetTaskParam param to get a task on a project type GetTaskParam struct { Workspace string ProjectID string TaskID string } // GetTasks get tasks of a project func (c *client) GetTask(p GetTaskParam) (t dto.Task, err error) { defer wrapError(&err, "get task \"%s\"", p.TaskID) ids := map[field]string{ workspaceField: p.Workspace, projectField: p.ProjectID, taskIDField: p.TaskID, } if err = required(ids); err != nil { return t, err } if err = checkIDs(ids); err != nil { return t, err } r, err := c.NewRequest( "GET", fmt.Sprintf( "v1/workspaces/%s/projects/%s/tasks/%s", p.Workspace, p.ProjectID, p.TaskID, ), nil, ) if err != nil { return t, err } _, err = c.Do(r, &t, "GetTask") return t, err } type TaskStatus string const ( TaskStatusDefault = "" TaskStatusDone = "DONE" TaskStatusActive = "ACTIVE" ) // AddTaskParam param to add tasks to a project type AddTaskParam struct { Workspace string ProjectID string Name string AssigneeIDs *[]string Estimate *time.Duration Status TaskStatus Billable *bool } func (c *client) AddTask(p AddTaskParam) (task dto.Task, err error) { defer wrapError(&err, "add task to project \"%s\"", p.ProjectID) if err = required(map[field]string{ nameField: p.Name, workspaceField: p.Workspace, projectField: p.ProjectID, }); err != nil { return task, err } if err = checkIDs(map[field]string{ workspaceField: p.Workspace, projectField: p.ProjectID, }); err != nil { return task, err } r := dto.AddTaskRequest{ Name: p.Name, AssigneeIDs: p.AssigneeIDs, Billable: p.Billable, } if p.Status != TaskStatus("") { s := string(p.Status) r.Status = &s } if p.Estimate != nil { e := dto.Duration{Duration: *p.Estimate} r.Estimate = &e } req, err := c.NewRequest( "POST", fmt.Sprintf( "v1/workspaces/%s/projects/%s/tasks", p.Workspace, p.ProjectID, ), r, ) if err != nil { return task, err } _, err = c.Do(req, &task, "AddTask") return task, err } // UpdateTaskParam param to update tasks to a project type UpdateTaskParam struct { Workspace string ProjectID string TaskID string Name string AssigneeIDs *[]string Estimate *time.Duration Status TaskStatus Billable *bool } func (c *client) UpdateTask(p UpdateTaskParam) (task dto.Task, err error) { defer wrapError(&err, "update task \"%s\"", p.TaskID) if err = required(map[field]string{ nameField: p.Name, taskIDField: p.TaskID, workspaceField: p.Workspace, projectField: p.ProjectID, }); err != nil { return task, err } if err = checkIDs(map[field]string{ taskIDField: p.TaskID, workspaceField: p.Workspace, projectField: p.ProjectID, }); err != nil { return task, err } r := dto.UpdateTaskRequest{ Name: p.Name, AssigneeIDs: p.AssigneeIDs, Billable: p.Billable, } if p.Status != TaskStatus("") { s := string(p.Status) r.Status = &s } if p.Estimate != nil { e := dto.Duration{Duration: *p.Estimate} r.Estimate = &e } req, err := c.NewRequest( "PUT", fmt.Sprintf( "v1/workspaces/%s/projects/%s/tasks/%s", p.Workspace, p.ProjectID, p.TaskID, ), r, ) if err != nil { return task, err } _, err = c.Do(req, &task, "UpdateTask") return task, err } // DeleteTaskParam param to update tasks to a project type DeleteTaskParam struct { Workspace string ProjectID string TaskID string } func (c *client) DeleteTask(p DeleteTaskParam) (task dto.Task, err error) { defer wrapError(&err, "delete task \"%s\"", p.TaskID) ids := map[field]string{ taskIDField: p.TaskID, workspaceField: p.Workspace, projectField: p.ProjectID, } if err = required(ids); err != nil { return task, err } if err = checkIDs(ids); err != nil { return task, err } req, err := c.NewRequest( "DELETE", fmt.Sprintf( "v1/workspaces/%s/projects/%s/tasks/%s", p.Workspace, p.ProjectID, p.TaskID, ), nil, ) if err != nil { return task, err } _, err = c.Do(req, &task, "DeleteTask") return task, err } // CreateTimeEntryParam params to create a new time entry type CreateTimeEntryParam struct { Workspace string Start time.Time End *time.Time Billable *bool Description string ProjectID string TaskID string TagIDs []string } // CreateTimeEntry create a new time entry func (c *client) CreateTimeEntry(p CreateTimeEntryParam) ( t dto.TimeEntryImpl, err error) { defer wrapError(&err, "create time entry") if err = checkWorkspace(p.Workspace); err != nil { return t, err } var end *dto.DateTime if p.End != nil { end = &dto.DateTime{Time: *p.End} } r, err := c.NewRequest( "POST", fmt.Sprintf( "v1/workspaces/%s/time-entries", p.Workspace, ), dto.CreateTimeEntryRequest{ Start: dto.DateTime{Time: p.Start}, End: end, Billable: p.Billable, Description: p.Description, ProjectID: p.ProjectID, TaskID: p.TaskID, TagIDs: p.TagIDs, }, ) if err != nil { return t, err } _, err = c.Do(r, &t, "CreateTimeEntry") return t, err } // GetTagsParam params to get all tags of a workspace type GetTagsParam struct { Workspace string Name string Archived *bool PaginationParam } // GetTags get all tags of a workspace func (c *client) GetTags(p GetTagsParam) (ps []dto.Tag, err error) { defer wrapError(&err, "get tags") if err = checkWorkspace(p.Workspace); err != nil { return ps, err } ps, err = paginate[dto.Tag]( c, "GET", fmt.Sprintf( "v1/workspaces/%s/tags", p.Workspace, ), p.PaginationParam, dto.GetTagsRequest{ Name: p.Name, Archived: p.Archived, }, "GetTags", ) return ps, err } // GetClientsParam params to get all clients of a workspace type GetClientsParam struct { Workspace string Name string Archived *bool PaginationParam } // GetClients gets all clients of a workspace func (c *client) GetClients(p GetClientsParam) ( clients []dto.Client, err error) { defer wrapError(&err, "get clients") if err = checkWorkspace(p.Workspace); err != nil { return clients, err } clients, err = paginate[dto.Client]( c, "GET", fmt.Sprintf( "v1/workspaces/%s/clients", p.Workspace, ), p.PaginationParam, dto.GetClientsRequest{ Name: p.Name, Archived: p.Archived, }, "GetClients", ) return } type AddClientParam struct { Workspace string Name string } // AddClient adds a new client to a workspace func (c *client) AddClient(p AddClientParam) (client dto.Client, err error) { defer wrapError(&err, "add client") if err = required(map[field]string{ nameField: p.Name, workspaceField: p.Workspace, }); err != nil { return client, err } if err = checkIDs(map[field]string{ workspaceField: p.Workspace, }); err != nil { return client, err } req, err := c.NewRequest( "POST", fmt.Sprintf( "v1/workspaces/%s/clients", p.Workspace, ), dto.AddClientRequest{ Name: p.Name, }, ) if err != nil { return client, err } _, err = c.Do(req, &client, "AddClient") return client, err } // GetProjectsParam params to get all project of a workspace type GetProjectsParam struct { Workspace string Name string Clients []string Archived *bool Hydrate bool PaginationParam } // GetProjects get all project of a workspace func (c *client) GetProjects(p GetProjectsParam) (ps []dto.Project, err error) { defer wrapError(&err, "get projects") if err = checkWorkspace(p.Workspace); err != nil { return ps, err } ps, err = paginate[dto.Project]( c, "GET", fmt.Sprintf( "v1/workspaces/%s/projects", p.Workspace, ), p.PaginationParam, dto.GetProjectsRequest{ Name: p.Name, Archived: p.Archived, Clients: p.Clients, Hydrated: p.Hydrate, }, "GetProjects", ) if p.Hydrate { for i := range ps { ps[i].Hydrated = true } } return ps, err } type AddProjectParam struct { Workspace string Name string ClientId string Color string Note string Billable bool Public bool } func parseColor(c string) (string, error) { if !strings.HasPrefix(c, "#") { c = "#" + c } if len(c) != 4 && len(c) != 7 { return c, errors.New("color must have 3 (#000) or 6 (#ffffff) numbers") } if len(c) == 4 { c = string([]byte{'#', c[1], c[1], c[2], c[2], c[3], c[3]}) } if _, err := hex.DecodeString(c[1:]); err != nil { return c, errors.Wrap(err, "color \""+c+"\" is not a hex string") } return c, nil } // AddProject adds a new project to a workspace func (c *client) AddProject(p AddProjectParam) ( project dto.Project, err error) { defer wrapError(&err, "add project") if err = required(map[field]string{ nameField: p.Name, workspaceField: p.Workspace, }); err != nil { return project, err } if err = checkIDs(map[field]string{ workspaceField: p.Workspace, }); err != nil { return project, err } if p.Color != "" { p.Color, err = parseColor(p.Color) if err != nil { return } } req, err := c.NewRequest( "POST", fmt.Sprintf( "v1/workspaces/%s/projects", p.Workspace, ), dto.AddProjectRequest{ Name: p.Name, ClientId: p.ClientId, IsPublic: p.Public, Color: p.Color, Note: p.Note, Billable: p.Billable, Public: p.Public, }, ) if err != nil { return project, err } _, err = c.Do(req, &project, "AddProject") return project, err } // UpdateProjectParam sets the properties to change on a project // Workspace and ID are required type UpdateProjectParam struct { Workspace string ProjectID string Name string ClientId *string Color string Note *string Billable *bool Public *bool Archived *bool } // UpdateProject will change properties of a Project, leave the property as nil // or "empty" to not change it func (c *client) UpdateProject(p UpdateProjectParam) ( project dto.Project, err error) { defer wrapError(&err, "update project") if err = required(map[field]string{ projectField: p.ProjectID, workspaceField: p.Workspace, }); err != nil { return project, err } if err = checkIDs(map[field]string{ projectField: p.ProjectID, workspaceField: p.Workspace, }); err != nil { return project, err } if p.Color != "" { p.Color, err = parseColor(p.Color) if err != nil { return } } var name, color *string if p.Name != "" { name = &p.Name } if p.Color != "" { color = &p.Color } req, err := c.NewRequest( "PUT", "v1/workspaces/"+p.Workspace+"/projects/"+p.ProjectID, dto.UpdateProjectRequest{ Name: name, ClientId: p.ClientId, IsPublic: p.Public, Color: color, Note: p.Note, Billable: p.Billable, Archived: p.Archived, }, ) if err != nil { return project, err } _, err = c.Do(req, &project, "UpdateProject") return project, err } // UpdateMembership represents the membership of a User or User Group to a // project type UpdateMembership struct { UserOrGroupID string HourlyRateAmount int64 } // UpdateProjectMembershipsParam will change which users and groups have // access to the project type UpdateProjectMembershipsParam struct { Workspace string ProjectID string Memberships []UpdateMembership } // UpdateProjectMemberships changes who has access to add time entries to // the project func (c *client) UpdateProjectMemberships(p UpdateProjectMembershipsParam) ( pr dto.Project, err error) { defer wrapError(&err, "update project memberships") if err = required(map[field]string{ projectField: p.ProjectID, workspaceField: p.Workspace, }); err != nil { return } if err = checkIDs(map[field]string{ projectField: p.ProjectID, workspaceField: p.Workspace, }); err != nil { return } members := make([]dto.UpdateProjectMembership, len(p.Memberships)) for i := range p.Memberships { id := map[field]string{ userOrGroupIDField: p.Memberships[i].UserOrGroupID} if err = required(id); err != nil { return } if err = checkIDs(id); err != nil { return } members[i].UserID = p.Memberships[i].UserOrGroupID members[i].HourlyRate.Amount = p.Memberships[i].HourlyRateAmount } req, err := c.NewRequest( "PATCH", "v1/workspaces/"+p.Workspace+"/projects/"+p.ProjectID+"/memberships", dto.UpdateProjectMembershipsRequest{ Memberships: members, }, ) if err != nil { return pr, err } _, err = c.Do(req, &pr, "UpdateProjectMemberships") return pr, err } // UpdateProjectTemplateParam sets which project will be updated,and if it will // became a template or not type UpdateProjectTemplateParam struct { Workspace string ProjectID string Template bool } // UpdateProjectTemplate changes if a project is a template or not func (c *client) UpdateProjectTemplate(p UpdateProjectTemplateParam) ( pr dto.Project, err error) { defer wrapError(&err, "update project template") if err = required(map[field]string{ projectField: p.ProjectID, workspaceField: p.Workspace, }); err != nil { return } if err = checkIDs(map[field]string{ projectField: p.ProjectID, workspaceField: p.Workspace, }); err != nil { return } req, err := c.NewRequest( "PATCH", "v1/workspaces/"+p.Workspace+"/projects/"+p.ProjectID+"/template", dto.UpdateProjectTemplateRequest{ IsTemplate: p.Template, }, ) if err != nil { return pr, err } _, err = c.Do(req, &pr, "UpdateProjectTemplate") return pr, err } // UpdateProjectUserRateParam sets the parameters to update the billable/cost // rate, if Since is not nil, then all time entries after that time will be // updated to new rate type UpdateProjectUserRateParam struct { Workspace string ProjectID string UserID string Amount uint Since *time.Time } func (c *client) UpdateProjectUserBillableRate( p UpdateProjectUserRateParam) (project dto.Project, err error) { defer wrapError(&err, "update project user billable rate") if err = required(map[field]string{ projectField: p.ProjectID, workspaceField: p.Workspace, userIDField: p.UserID, }); err != nil { return } if err = checkIDs(map[field]string{ projectField: p.ProjectID, workspaceField: p.Workspace, userIDField: p.UserID, }); err != nil { return } var since *dto.DateTime if p.Since != nil { since = &dto.DateTime{Time: *p.Since} } req, err := c.NewRequest( "PUT", "v1/workspaces/"+p.Workspace+"/projects/"+p.ProjectID+ "/users/"+p.UserID+"/hourly-rate", dto.UpdateProjectUserRateRequest{ Amount: p.Amount, Since: since, }, ) if err != nil { return project, err } _, err = c.Do(req, &project, "UpdateProjectUserBillableRate") return project, err } func (c *client) UpdateProjectUserCostRate( p UpdateProjectUserRateParam) (project dto.Project, err error) { defer wrapError(&err, "update project user cost rate") if err = required(map[field]string{ projectField: p.ProjectID, workspaceField: p.Workspace, userIDField: p.UserID, }); err != nil { return } if err = checkIDs(map[field]string{ projectField: p.ProjectID, workspaceField: p.Workspace, userIDField: p.UserID, }); err != nil { return } var since *dto.DateTime if p.Since != nil { since = &dto.DateTime{Time: *p.Since} } req, err := c.NewRequest( "PUT", "v1/workspaces/"+p.Workspace+"/projects/"+p.ProjectID+ "/users/"+p.UserID+"/cost-rate", dto.UpdateProjectUserRateRequest{ Amount: p.Amount, Since: since, }, ) if err != nil { return project, err } _, err = c.Do(req, &project, "UpdateProjectUserCostRate") return project, err } // EstimateMethod are methods to estimate projects (none, budget and time) type EstimateMethod string const ( // EstimateMethodNone dont estimate the project EstimateMethodNone = EstimateMethod("none") // EstimateMethodTime estimate by time EstimateMethodTime = EstimateMethod("time") // EstimateMethodBudget estimate by budget EstimateMethodBudget = EstimateMethod("budget") ) // EstimateType sets if the estimate is for the role project or per task type EstimateType string const ( EstimateTypeProject = EstimateType("project") EstimateTypeTask = EstimateType("task") ) func (t EstimateType) toRequestType() *dto.EstimateType { switch t { case EstimateTypeTask: v := dto.EstimateTypeAuto return &v case EstimateTypeProject: v := dto.EstimateTypeManual return &v default: return nil } } // EstimateResetOption defines the period in which the estimates reset type EstimateResetOption string const ( EstimateResetOptionDefault = EstimateType("") EstimateResetOptionMonthly = EstimateResetOption("monthly") ) func (t EstimateResetOption) toRequestType() *dto.EstimateResetOption { switch t { case EstimateResetOptionMonthly: v := dto.EstimateResetOptionMonthly return &v default: return nil } } // UpdateProjectEstimateParam holds parameters to change project estimate type UpdateProjectEstimateParam struct { Workspace string ProjectID string Method EstimateMethod Type EstimateType ResetOption EstimateResetOption Estimate int64 } // UpdateProjectEstimate change how the estime of a project is measured func (c *client) UpdateProjectEstimate(p UpdateProjectEstimateParam) ( r dto.Project, err error) { defer wrapError(&err, "update project estimate") if err = required(map[field]string{ projectField: p.ProjectID, workspaceField: p.Workspace, estimateMethodField: string(p.Method), }); err != nil { return } if err = checkIDs(map[field]string{ projectField: p.ProjectID, workspaceField: p.Workspace, }); err != nil { return } if err = shouldBeOneOf(estimateMethodField, string(p.Method), []string{ string(EstimateMethodNone), string(EstimateMethodTime), string(EstimateMethodBudget), }); err != nil { return } if p.Method != EstimateMethodNone { if err = shouldBeOneOf(estimateTypeField, string(p.Type), []string{ string(EstimateTypeProject), string(EstimateTypeTask), }); err != nil { return } if err = shouldBeOneOf(resetOptionField, string(p.ResetOption), []string{ string(EstimateResetOptionDefault), string(EstimateResetOptionMonthly), }); err != nil { return } if p.Type != EstimateTypeProject { p.Estimate = 0 } else if p.Estimate <= 0 { err = errors.New( "estimate should be greater than zero for type project") return } } b := dto.UpdateProjectEstimateRequest{} if p.Method != EstimateMethodNone { be := dto.BaseEstimateRequest{ Active: true, Type: p.Type.toRequestType(), ResetOptions: p.ResetOption.toRequestType(), } switch p.Method { case EstimateMethodBudget: b.BudgetEstimate.BaseEstimateRequest = be if p.Estimate > 0 { e := uint64(p.Estimate) b.BudgetEstimate.Estimate = &e } case EstimateMethodTime: b.TimeEstimate.BaseEstimateRequest = be if p.Estimate > 0 { b.TimeEstimate.Estimate = &dto.Duration{ Duration: time.Duration(p.Estimate)} } } } req, err := c.NewRequest( "PATCH", "v1/workspaces/"+p.Workspace+"/projects/"+p.ProjectID+"/estimate", b, ) if err != nil { return } _, err = c.Do(req, &r, "UpdateProjectEstimate") return } // DeleteProjectParam identifies which project to delete type DeleteProjectParam struct { Workspace string ProjectID string } // DeleteProject removes a project forever func (c *client) DeleteProject(p DeleteProjectParam) ( pr dto.Project, err error) { defer wrapError(&err, "delete project") ids := map[field]string{ workspaceField: p.Workspace, projectField: p.ProjectID, } if err = required(ids); err != nil { return pr, err } if err = checkIDs(ids); err != nil { return pr, err } r, err := c.NewRequest( "DELETE", "v1/workspaces/"+p.Workspace+"/projects/"+p.ProjectID, nil, ) if err != nil { return pr, err } _, err = c.Do(r, &pr, "DeleteProject") return pr, err } // InvalidOptionError indicates that the parameter has a limited set of valid // values, and the one used is not one of them (see Options for the valid ones) type InvalidOptionError struct { Field string Options []string } func (i *InvalidOptionError) Error() string { return "valid options for " + i.Field + " are " + strhlp.ListForHumans(i.Options) } func shouldBeOneOf(f field, s string, o []string) error { if strhlp.InSlice(s, o) { return nil } return &InvalidOptionError{ Field: string(f), Options: o, } } // OutParam params to end the current time entry type OutParam struct { Workspace string UserID string End time.Time } // Out create a new time entry func (c *client) Out(p OutParam) (err error) { defer wrapError(&err, "end running time entry") ids := map[field]string{ workspaceField: p.Workspace, userIDField: p.UserID, } if err = required(ids); err != nil { return err } if err = checkIDs(ids); err != nil { return err } r, err := c.NewRequest( "PATCH", fmt.Sprintf( "v1/workspaces/%s/user/%s/time-entries", p.Workspace, p.UserID, ), dto.OutTimeEntryRequest{ End: dto.DateTime{Time: p.End}, }, ) if err != nil { return err } _, err = c.Do(r, nil, "Out") return err } // UpdateTimeEntryParam params to update a new time entry type UpdateTimeEntryParam struct { Workspace string TimeEntryID string Start time.Time End *time.Time Billable bool Description string ProjectID string TaskID string TagIDs []string } // UpdateTimeEntry update a time entry func (c *client) UpdateTimeEntry(p UpdateTimeEntryParam) ( t dto.TimeEntryImpl, err error) { defer wrapError(&err, "update time entry \"%s\"", p.TimeEntryID) ids := map[field]string{ workspaceField: p.Workspace, timeEntryIDField: p.TimeEntryID, } if err = required(ids); err != nil { return t, err } if err = checkIDs(ids); err != nil { return t, err } var end *dto.DateTime if p.End != nil { end = &dto.DateTime{Time: *p.End} } r, err := c.NewRequest( "PUT", fmt.Sprintf( "v1/workspaces/%s/time-entries/%s", p.Workspace, p.TimeEntryID, ), dto.UpdateTimeEntryRequest{ Start: dto.DateTime{Time: p.Start}, End: end, Billable: p.Billable, Description: p.Description, ProjectID: p.ProjectID, TaskID: p.TaskID, TagIDs: p.TagIDs, }, ) if err != nil { return t, err } _, err = c.Do(r, &t, "UpdateTimeEntry") return t, err } // DeleteTimeEntryParam params to update a new time entry type DeleteTimeEntryParam struct { Workspace string TimeEntryID string } // DeleteTimeEntry deletes a time entry func (c *client) DeleteTimeEntry(p DeleteTimeEntryParam) (err error) { defer wrapError(&err, "delete time entry \"%s\"", p.TimeEntryID) ids := map[field]string{ workspaceField: p.Workspace, timeEntryIDField: p.TimeEntryID, } if err = required(ids); err != nil { return err } if err = checkIDs(ids); err != nil { return err } r, err := c.NewRequest( "DELETE", fmt.Sprintf( "v1/workspaces/%s/time-entries/%s", p.Workspace, p.TimeEntryID, ), nil, ) if err != nil { return err } _, err = c.Do(r, nil, "DeleteTimeEntry") return err } type ChangeInvoicedParam struct { Workspace string TimeEntryIDs []string Invoiced bool } // ChangeInvoiced changes time entries to invoiced or not func (c *client) ChangeInvoiced(p ChangeInvoicedParam) error { r, err := c.NewRequest( "PATCH", fmt.Sprintf( "v1/workspaces/%s/time-entries/invoiced", p.Workspace, ), dto.ChangeTimeEntriesInvoicedRequest{ TimeEntryIDs: p.TimeEntryIDs, Invoiced: p.Invoiced, }, ) if err != nil { return err } _, err = c.Do(r, nil, "ChangeInvoiced") return err } ================================================ FILE: api/client_test.go ================================================ package api_test import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/lucassabreu/clockify-cli/api" "github.com/stretchr/testify/assert" ) var exampleID = "62f2af744a912b05acc7c79e" type testCase interface { getName() string getParam() interface{} getResult() interface{} getErr() string hasHttpCalls() bool getHttpCallFor(uri string) httpCall getPendingHttpCalls() []httpCall } type httpCall interface { getRequestMethod() string getRequestUrl() string getRequestBody() string getResponseStatus() int getResponseBody() string } func runClient(t *testing.T, tt testCase, fn func(api.Client, interface{}) (interface{}, error)) { t.Run(tt.getName(), func(t *testing.T) { httpCalled := false t.Cleanup(func() { if !tt.hasHttpCalls() { assert.False(t, httpCalled, "should not call api") return } assert.True(t, httpCalled, "should call api") }) s := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { httpCalled = true if !tt.hasHttpCalls() { t.Error("should not call api") w.WriteHeader(500) return } hc := tt.getHttpCallFor(r.URL.String()) if hc == nil { assert.FailNow(t, "should not call api "+r.URL.String()) w.WriteHeader(500) return } assert.Equal(t, hc.getRequestUrl(), r.URL.String()) assert.Equal(t, hc.getRequestMethod(), strings.ToLower(r.Method)) b, _ := io.ReadAll(r.Body) if hc.getRequestBody() != "" { var eMap, aMap map[string]interface{} assert.NoError(t, json.Unmarshal(b, &aMap)) assert.NoError(t, json.Unmarshal([]byte(hc.getRequestBody()), &eMap)) assert.Equal(t, eMap, aMap) } else { assert.Empty(t, string(b)) } w.WriteHeader(hc.getResponseStatus()) rb := hc.getResponseBody() if rb == "" { rb = "{}" } _, err := w.Write([]byte(rb)) assert.NoError(t, err) })) defer s.Close() c, _ := api.NewClientFromUrlAndKey( "a-key", s.URL, ) r, err := fn(c, tt.getParam()) if tt.getErr() != "" { if !assert.Error(t, err) { return } assert.Regexp(t, tt.getErr(), err.Error()) return } if !assert.NoError(t, err) || tt.getResult() == nil { return } assert.Equal(t, tt.getResult(), r) }) } type simpleTestCase struct { name string param interface{} result interface{} err string requestMethod string requestUrl string requestBody string responseStatus int responseBody string once bool } func (s *simpleTestCase) getRequestMethod() string { return s.requestMethod } func (s *simpleTestCase) getRequestUrl() string { return s.requestUrl } func (s *simpleTestCase) getRequestBody() string { return s.requestBody } func (s *simpleTestCase) getResponseStatus() int { return s.responseStatus } func (s *simpleTestCase) getResponseBody() string { return s.responseBody } func (s *simpleTestCase) getName() string { return s.name } func (s *simpleTestCase) getParam() interface{} { return s.param } func (s *simpleTestCase) getResult() interface{} { return s.result } func (s *simpleTestCase) getErr() string { return s.err } func (s *simpleTestCase) getHttpCallFor(_ string) httpCall { if !s.once { s.once = true return s } return nil } func (s *simpleTestCase) getPendingHttpCalls() []httpCall { if s.once { return []httpCall{} } return []httpCall{s} } func (s *simpleTestCase) hasHttpCalls() bool { return s.requestUrl != "" } type multiRequestTestCase struct { name string param interface{} err string result interface{} calls map[string]httpCall hasCalls bool } func (m *multiRequestTestCase) getName() string { return m.name } func (m *multiRequestTestCase) getParam() interface{} { return m.param } func (m *multiRequestTestCase) getResult() interface{} { return m.result } func (m *multiRequestTestCase) getErr() string { return m.err } func (m *multiRequestTestCase) hasHttpCalls() bool { return m.hasCalls } func (m *multiRequestTestCase) getHttpCallFor(uri string) httpCall { if !m.hasCalls { return nil } c := m.calls[uri] delete(m.calls, uri) return c } func (m *multiRequestTestCase) getPendingHttpCalls() []httpCall { if !m.hasCalls { return []httpCall{} } l := make([]httpCall, len(m.calls)) for _, c := range m.calls { l = append(l, c) } return l } func (m *multiRequestTestCase) addHttpCall(c httpCall) *multiRequestTestCase { if m.calls == nil { m.calls = make(map[string]httpCall) m.hasCalls = true } if _, ok := m.calls[c.getRequestUrl()]; ok { panic("http call for " + c.getRequestUrl() + " already exists") } m.calls[c.getRequestUrl()] = c return m } type httpRequest struct { method string url string body string status int response string } func (h *httpRequest) getRequestMethod() string { return h.method } func (h *httpRequest) getRequestUrl() string { return h.url } func (h *httpRequest) getRequestBody() string { return h.body } func (h *httpRequest) getResponseStatus() int { return h.status } func (h *httpRequest) getResponseBody() string { return h.response } ================================================ FILE: api/dto/dto.go ================================================ package dto import ( "fmt" "strings" "time" ) // Error api errors type Error struct { Message string `json:"message"` Code int `json:"code"` } func (e Error) Error() string { return fmt.Sprintf("%s (code: %d)", e.Message, e.Code) } // Workspace DTO type Workspace struct { ID string `json:"id"` Name string `json:"name"` ImageURL string `json:"imageUrl"` Settings WorkspaceSettings `json:"workspaceSettings"` HourlyRate Rate `json:"hourlyRate"` Memberships []Membership } // Membership DTO type Membership struct { HourlyRate *Rate `json:"hourlyRate"` CostRate *Rate `json:"costRate"` Status MembershipStatus `json:"membershipStatus"` Type string `json:"membershipType"` TargetID string `json:"targetId"` UserID string `json:"userId"` } // MembershipStatus possible Membership Status type MembershipStatus string // MembershipStatusPending membership is Pending const MembershipStatusPending = MembershipStatus("PENDING") // MembershipStatusActive membership is Active const MembershipStatusActive = MembershipStatus("ACTIVE") // MembershipStatusDeclined membership is Declined const MembershipStatusDeclined = MembershipStatus("DECLINED") // MembershipStatusInactive membership is Inactive const MembershipStatusInactive = MembershipStatus("INACTIVE") // WorkspaceSettings DTO type WorkspaceSettings struct { AdminOnlyPages []string `json:"adminOnlyPages"` AutomaticLock AutomaticLock `json:"automaticLock"` CanSeeTimeSheet bool `json:"canSeeTimeSheet"` DefaultBillableProjects bool `json:"defaultBillableProjects"` ForceDescription bool `json:"forceDescription"` ForceProjects bool `json:"forceProjects"` ForceTags bool `json:"forceTags"` ForceTasks bool `json:"forceTasks"` LockTimeEntries time.Time `json:"lockTimeEntries"` OnlyAdminsCreateProject bool `json:"onlyAdminsCreateProject"` OnlyAdminsCreateTag bool `json:"onlyAdminsCreateTag"` OnlyAdminsCreateTask bool `json:"onlyAdminsCreateTask"` OnlyAdminsSeeAllTimeEntries bool `json:"onlyAdminsSeeAllTimeEntries"` OnlyAdminsSeeBillableRates bool `json:"onlyAdminsSeeBillableRates"` OnlyAdminsSeeDashboard bool `json:"onlyAdminsSeeDashboard"` OnlyAdminsSeePublicProjectsEntries bool `json:"onlyAdminsSeePublicProjectsEntries"` ProjectFavorites bool `json:"projectFavorites"` ProjectGroupingLabel string `json:"projectGroupingLabel"` ProjectPickerSpecialFilter bool `json:"projectPickerSpecialFilter"` Round Round `json:"round"` TimeRoundingInReports bool `json:"timeRoundingInReports"` TrackTimeDownToSecond bool `json:"trackTimeDownToSecond"` IsProjectPublicByDefault bool `json:"isProjectPublicByDefault"` CanSeeTracker bool `json:"canSeeTracker"` FeatureSubscriptionType string `json:"featureSubscriptionType"` } // AutomaticLock DTO type AutomaticLock struct { ChangeDay string `json:"changeDay"` DayOfMonth int `json:"dayOfMonth"` FirstDay string `json:"firstDay"` OlderThanPeriod string `json:"olderThanPeriod"` OlderThanValue int `json:"olderThanValue"` Type string `json:"type"` } // Round DTO type Round struct { Minutes string `json:"minutes"` Round string `json:"round"` } // Rate DTO type Rate struct { Amount int64 `json:"amount"` Currency string `json:"currency,omitempty"` } // TimeEntry DTO type TimeEntry struct { ID string `json:"id"` Billable bool `json:"billable"` Description string `json:"description"` HourlyRate Rate `json:"hourlyRate"` IsLocked bool `json:"isLocked"` Project *Project `json:"project"` CustomFields []CustomField `json:"customFieldValues"` ProjectID string `json:"projectId"` Tags []Tag `json:"tags"` Task *Task `json:"task"` TimeInterval TimeInterval `json:"timeInterval"` TotalBillable int64 `json:"totalBillable"` User *User `json:"user"` WorkspaceID string `json:"workspaceId"` } // NewTimeInterval will create a TimeInterval from start and end times func NewTimeInterval(start time.Time, end *time.Time) TimeInterval { start = start.UTC() if end != nil { *end = end.UTC() } t := TimeInterval{ Start: start.UTC(), End: end, } if end == nil { t := time.Now().UTC() end = &t } t.Duration = Duration{end.Sub(t.Start)}.String() return t } // TimeInterval DTO type TimeInterval struct { Duration string `json:"duration"` End *time.Time `json:"end"` Start time.Time `json:"start"` } // Tag DTO type Tag struct { ID string `json:"id"` Name string `json:"name"` WorkspaceID string `json:"workspaceId"` } func (e Tag) GetID() string { return e.ID } func (e Tag) GetName() string { return e.Name } func (e Tag) String() string { return e.Name + " (" + e.ID + ")" } // TaskStatus task status type TaskStatus string // TaskStatusActive task is Active const TaskStatusActive = TaskStatus("ACTIVE") // TaskStatusDone task is Done const TaskStatusDone = TaskStatus("DONE") // Task DTO type Task struct { AssigneeIDs []string `json:"assigneeIds"` UserGroupIDs []string `json:"userGroupIds"` Estimate *Duration `json:"estimate"` ID string `json:"id"` Name string `json:"name"` ProjectID string `json:"projectId"` Billable bool `json:"billable"` HourlyRate *Rate `json:"hourlyRate"` CostRate *Rate `json:"costRate"` Status TaskStatus `json:"status"` Duration *Duration `json:"duration"` Favorite bool `json:"favorite"` } func (e Task) GetID() string { return e.ID } func (e Task) GetName() string { return e.Name } // Client DTO type Client struct { ID string `json:"id"` Name string `json:"name"` WorkspaceID string `json:"workspaceId"` Archived bool `json:"archived"` } func (e Client) GetID() string { return e.ID } func (e Client) GetName() string { return e.Name } // CustomField DTO type CustomField struct { CustomFieldID string `json:"customFieldId"` TimeEntryId string `json:"timeEntryId"` Name string `json:"name"` Type string `json:"type"` Value interface{} `json:"value"` } // ValueAsString converter for CustomFieldDTO /* Custom field `Value` can be either a string or an array of strings. This function is used to get the value always as string, using the `|` symbol as separator between each individual string. */ func (cf CustomField) ValueAsString() string { switch v := cf.Value.(type) { case string: return v case []interface{}: parts := make([]string, len(v)) for i, item := range v { parts[i] = fmt.Sprint(item) } return strings.Join(parts, "|") case []string: return strings.Join(v, "|") case nil: return "" default: return fmt.Sprint(v) } } // Project DTO type Project struct { WorkspaceID string `json:"workspaceId"` ID string `json:"id"` Name string `json:"name"` Note string `json:"note"` Color string `json:"color"` ClientID string `json:"clientId"` ClientName string `json:"clientName"` HourlyRate Rate `json:"hourlyRate"` CostRate *Rate `json:"costRate"` Billable bool `json:"billable"` TimeEstimate TimeEstimate `json:"timeEstimate"` BudgetEstimate BaseEstimate `json:"budgetEstimate"` Duration *Duration `json:"duration"` Archived bool `json:"archived"` Template bool `json:"template"` Public bool `json:"public"` Favorite bool `json:"favorite"` Memberships []Membership `json:"memberships"` // Hydrated indicates if the attributes CustomFields and Tasks are filled Hydrated bool `json:"-"` CustomFields []CustomField `json:"customFields,omitempty"` Tasks []Task `json:"tasks,omitempty"` } func (p Project) GetID() string { return p.ID } func (p Project) GetName() string { return p.Name } // EstimateType possible Estimate types type EstimateType string // EstimateTypeAuto estimate is Auto const EstimateTypeAuto = EstimateType("AUTO") // EstimateTypeManual estimate is Manual const EstimateTypeManual = EstimateType("MANUAL") // EstimateResetOption possible Estimate Reset Options type EstimateResetOption string // EstimateResetOptionMonthly estimate is Auto const EstimateResetOptionMonthly = EstimateResetOption("MONTHLY") // BaseEstimate DTO type BaseEstimate struct { Type EstimateType `json:"type"` Active bool `json:"active"` ResetOptions *EstimateResetOption `json:"resetOptions"` } // TimeEstimate DTO type TimeEstimate struct { BaseEstimate Estimate Duration `json:"estimate"` IncludeNonBillable bool `json:"includeNonBillable"` } // BudgetEstimate DTO type BudgetEstimate struct { BaseEstimate Estimate uint `json:"estimate"` } // UserStatus possible user status type UserStatus string // UserStatusActive when the user is Active const UserStatusActive = UserStatus("ACTIVE") // UserStatusPendingEmailVerification when the user is Pending Email Verification const UserStatusPendingEmailVerification = UserStatus("PENDING_EMAIL_VERIFICATION") // UserStatusDeleted when the user is Deleted const UserStatusDeleted = UserStatus("DELETED") // User DTO type User struct { ID string `json:"id"` ActiveWorkspace string `json:"activeWorkspace"` DefaultWorkspace string `json:"defaultWorkspace"` Email string `json:"email"` Memberships []Membership `json:"memberships"` Name string `json:"name"` ProfilePicture string `json:"profilePicture"` Settings UserSettings `json:"settings"` Status UserStatus `json:"status"` Roles *[]Role `json:"roles"` } func (e User) GetID() string { return e.ID } func (e User) GetName() string { return e.Name } // Role DTO type Role struct { Role string `json:"role"` Entities []RoleEntity `json:"entities"` } type RoleEntity struct { ID string `json:"id"` Name string `json:"name"` } // WeekStart when the week starts type WeekStart string // WeekStartMonday when start at Monday const WeekStartMonday = WeekStart("MONDAY") // WeekStartTuesday when start at Tuesday const WeekStartTuesday = WeekStart("TUESDAY") // WeekStartWednesday when start at Wednesday const WeekStartWednesday = WeekStart("WEDNESDAY") // WeekStartThursday when start at Thursday const WeekStartThursday = WeekStart("THURSDAY") // WeekStartFriday when start at Friday const WeekStartFriday = WeekStart("FRIDAY") // WeekStartSaturday when start at Saturday const WeekStartSaturday = WeekStart("SATURDAY") // WeekStartSunday when start at Sunday const WeekStartSunday = WeekStart("SUNDAY") // UserSettings DTO type UserSettings struct { DateFormat string `json:"dateFormat"` IsCompactViewOn bool `json:"isCompactViewOn"` LongRunning bool `json:"longRunning"` SendNewsletter bool `json:"sendNewsletter"` SummaryReportSettings SummaryReportSettings `json:"summaryReportSettings"` TimeFormat string `json:"timeFormat"` TimeTrackingManual bool `json:"timeTrackingManual"` TimeZone string `json:"timeZone"` WeekStart string `json:"weekStart"` WeeklyUpdates bool `json:"weeklyUpdates"` } // SummaryReportSettings DTO type SummaryReportSettings struct { Group string `json:"group"` Subgroup string `json:"subgroup"` } // InvitedUser DTO type InvitedUser struct { ID string `json:"id"` Email string `json:"email"` Invitation Invitation `json:"invitation"` Memberships []Membership `json:"memberships"` } // Invitation DTO type Invitation struct { Creation time.Time `json:"creation"` InvitationCode string `json:"invitationCode"` Membership Membership `json:"membership"` WorkspaceID string `json:"workspaceId"` WorkspaceName string `json:"workspaceName"` } // TimeEntriesList DTO type TimeEntriesList struct { AllEntriesCount int64 `json:"allEntriesCount"` GotAllEntries bool `json:"gotAllEntries"` TimeEntriesList []TimeEntryImpl `json:"timeEntriesList"` } // TimeEntryImpl DTO type TimeEntryImpl struct { Billable bool `json:"billable"` Description string `json:"description"` ID string `json:"id"` IsLocked bool `json:"isLocked"` ProjectID string `json:"projectId"` TagIDs []string `json:"tagIds"` TaskID string `json:"taskId"` TimeInterval TimeInterval `json:"timeInterval"` UserID string `json:"userId"` WorkspaceID string `json:"workspaceId"` } ================================================ FILE: api/dto/request.go ================================================ package dto import ( "encoding/json" "fmt" "net/url" "strconv" "strings" "time" "github.com/pkg/errors" ) // DateTime is a time presentation for parameters type DateTime struct { time.Time } // MarshalJSON converts DateTime correctly func (d DateTime) MarshalJSON() ([]byte, error) { return []byte(strconv.Quote(d.String())), nil } func (d DateTime) String() string { return d.Time.UTC().Format("2006-01-02T15:04:05Z") } // Duration is a time presentation for parameters type Duration struct { time.Duration } // MarshalJSON converts Duration correctly func (d Duration) MarshalJSON() ([]byte, error) { return []byte("\"" + d.String() + "\""), nil } // UnmarshalJSON converts a JSON value to Duration correctly func (d *Duration) UnmarshalJSON(b []byte) error { var s string if err := json.Unmarshal(b, &s); err != nil { return errors.Wrap(err, "unmarshal duration") } dc, err := StringToDuration(s) if err != nil { return err } *d = Duration{dc} return err } func StringToDuration(s string) (time.Duration, error) { if len(s) < 4 { return 0, errors.Errorf("duration %s is invalid", s) } var u, dc time.Duration var j, i int for ; i < len(s); i++ { switch s[i] { case 'P', 'T': j = i + 1 continue case 'H': u = time.Hour case 'M': u = time.Minute case 'S': u = time.Second default: continue } v, err := strconv.Atoi(s[j:i]) if err != nil { return 0, errors.Wrap(err, "cast cast "+s[j:i]+" to int") } dc = dc + time.Duration(v)*u j = i + 1 } return dc, nil } func (d Duration) String() string { s := d.Duration.String() i := strings.LastIndex(s, ".") if i > -1 { s = s[0:i] + "s" } return "PT" + strings.ToUpper(s) } func (dd Duration) HumanString() string { d := dd.Duration p := "" if d < 0 { p = "-" d = d * -1 } return p + fmt.Sprintf("%d:%02d:%02d", int64(d.Hours()), int64(d.Minutes())%60, int64(d.Seconds())%60) } type pagination struct { page int pageSize int } func newPagination(page, size int) pagination { return pagination{ page: page, pageSize: size, } } // AppendToQuery decorates the URL with pagination parameters func (p pagination) AppendToQuery(u *url.URL) *url.URL { v := u.Query() if p.page != 0 { v.Add("page", strconv.Itoa(p.page)) } if p.pageSize != 0 { v.Add("page-size", strconv.Itoa(p.pageSize)) } u.RawQuery = v.Encode() return u } type PaginatedRequest interface { WithPagination(page, size int) PaginatedRequest } // GetTimeEntryRequest to get a time entry type GetTimeEntryRequest struct { Hydrated *bool ConsiderDurationFormat *bool } // AppendToQuery decorates the URL with the query string needed for this Request func (r GetTimeEntryRequest) AppendToQuery(u *url.URL) *url.URL { v := u.Query() if r.Hydrated != nil && *r.Hydrated { v.Add("hydrated", "true") } if r.ConsiderDurationFormat != nil && *r.ConsiderDurationFormat { v.Add("consider-duration-format", "true") } u.RawQuery = v.Encode() return u } // UserTimeEntriesRequest to get entries of a user type UserTimeEntriesRequest struct { Description string Start *DateTime End *DateTime Project string Task string TagIDs []string ProjectRequired *bool TaskRequired *bool ConsiderDurationFormat *bool Hydrated *bool OnlyInProgress *bool pagination } // WithPagination add pagination to the UserTimeEntriesRequest func (r UserTimeEntriesRequest) WithPagination(page, size int) PaginatedRequest { r.pagination = newPagination(page, size) return r } // AppendToQuery decorates the URL with the query string needed for this Request func (r UserTimeEntriesRequest) AppendToQuery(u *url.URL) *url.URL { u = r.pagination.AppendToQuery(u) v := u.Query() if r.Start != nil { v.Add("start", r.Start.String()) } if r.End != nil { v.Add("end", r.End.String()) } addNotNil := func(b *bool, p string) { if b == nil { return } if *b { v.Add(p, "1") } else { v.Add(p, "0") } } addNotNil(r.ProjectRequired, "project-required") addNotNil(r.TaskRequired, "task-required") addNotNil(r.ConsiderDurationFormat, "consider-duration-format") addNotNil(r.Hydrated, "hydrated") addNotNil(r.OnlyInProgress, "in-progress") addNotEmpty := func(s string, p string) { if s == "" { return } v.Add(p, s) } addNotEmpty(r.Description, "description") addNotEmpty(r.Project, "project") addNotEmpty(r.Task, "task") for _, t := range r.TagIDs { addNotEmpty(t, "tags") } u.RawQuery = v.Encode() return u } // OutTimeEntryRequest to end the current time entry type OutTimeEntryRequest struct { End DateTime `json:"end"` } // CreateTimeEntryRequest to create a time entry is created type CreateTimeEntryRequest struct { Start DateTime `json:"start,omitempty"` End *DateTime `json:"end,omitempty"` Billable *bool `json:"billable,omitempty"` Description string `json:"description,omitempty"` ProjectID string `json:"projectId,omitempty"` TaskID string `json:"taskId,omitempty"` TagIDs []string `json:"tagIds,omitempty"` CustomFields []CustomFieldValue `json:"customFields,omitempty"` } // CustomFieldValue DTO type CustomFieldValue struct { CustomFieldID string `json:"customFieldId"` Status string `json:"status"` Name string `json:"name"` Type string `json:"type"` Value string `json:"value"` } // UpdateTimeEntryRequest to update a time entry type UpdateTimeEntryRequest struct { Start DateTime `json:"start,omitempty"` End *DateTime `json:"end,omitempty"` Billable bool `json:"billable,omitempty"` Description string `json:"description,omitempty"` ProjectID string `json:"projectId,omitempty"` TaskID string `json:"taskId,omitempty"` TagIDs []string `json:"tagIds,omitempty"` CustomFields []CustomFieldValue `json:"customFields,omitempty"` } type GetClientsRequest struct { Name string Archived *bool pagination } // WithPagination add pagination to the GetClientsRequest func (r GetClientsRequest) WithPagination(page, size int) PaginatedRequest { r.pagination = newPagination(page, size) return r } // AppendToQuery decorates the URL with the query string needed for this Request func (r GetClientsRequest) AppendToQuery(u *url.URL) *url.URL { u = r.pagination.AppendToQuery(u) v := u.Query() if r.Name != "" { v.Add("name", r.Name) } if r.Archived != nil { v.Add("archived", boolString[*r.Archived]) } u.RawQuery = v.Encode() return u } type AddClientRequest struct { Name string `json:"name"` } type GetProjectsRequest struct { Name string Archived *bool Clients []string Hydrated bool pagination } // WithPagination add pagination to the GetProjectRequest func (r GetProjectsRequest) WithPagination(page, size int) PaginatedRequest { r.pagination = newPagination(page, size) return r } var boolString = map[bool]string{ true: "true", false: "false", } // AppendToQuery decorates the URL with the query string needed for this Request func (r GetProjectsRequest) AppendToQuery(u *url.URL) *url.URL { u = r.pagination.AppendToQuery(u) v := u.Query() if r.Name != "" { v.Add("name", r.Name) } if r.Hydrated { v.Add("hydrated", "true") } if r.Archived != nil { v.Add("archived", boolString[*r.Archived]) } if len(r.Clients) > 0 { v.Add("clients", strings.Join(r.Clients, ",")) } u.RawQuery = v.Encode() return u } // GetProjectRequest query parameters to fetch a project type GetProjectRequest struct { Hydrated bool } // AppendToQuery decorates the URL with a query string func (r GetProjectRequest) AppendToQuery(u *url.URL) *url.URL { v := u.Query() if r.Hydrated { v.Add("hydrated", "true") } u.RawQuery = v.Encode() return u } // AddProjectRequest represents the parameters to create a project type AddProjectRequest struct { Name string `json:"name"` ClientId string `json:"clientId,omitempty"` IsPublic bool `json:"isPublic"` Color string `json:"color,omitempty"` Note string `json:"note,omitempty"` Billable bool `json:"billable"` Public bool `json:"public"` } // UpdateProjectRequest represents the parameters to update a project type UpdateProjectRequest struct { Name *string `json:"name,omitempty"` ClientId *string `json:"clientId,omitempty"` IsPublic *bool `json:"isPublic,omitempty"` Color *string `json:"color,omitempty"` Note *string `json:"note,omitempty"` Billable *bool `json:"billable,omitempty"` Archived *bool `json:"archived,omitempty"` } // UpdateProjectMembershipsRequest represents a request to change which users // and groups have access to a project type UpdateProjectMembershipsRequest struct { Memberships []UpdateProjectMembership `json:"memberships"` } // UpdateProjectMembership sets which user or group has access, and their // hourly rate type UpdateProjectMembership struct { UserID string `json:"userId"` HourlyRate Rate `json:"hourlyRate"` } // UpdateProjectTemplateRequest represents a request to change isTemplate flag // of a project type UpdateProjectTemplateRequest struct { IsTemplate bool `json:"isTemplate"` } type GetTagsRequest struct { Name string Archived *bool pagination } // WithPagination add pagination to the GetTagsRequest func (r GetTagsRequest) WithPagination(page, size int) PaginatedRequest { r.pagination = newPagination(page, size) return r } // AppendToQuery decorates the URL with the query string needed for this Request func (r GetTagsRequest) AppendToQuery(u *url.URL) *url.URL { u = r.pagination.AppendToQuery(u) v := u.Query() if r.Name != "" { v.Add("name", r.Name) } if r.Archived != nil { v.Add("archived", boolString[*r.Archived]) } u.RawQuery = v.Encode() return u } // GetTasksRequest represents the query filters to search tasks of a project type GetTasksRequest struct { Name string Active bool pagination } // WithPagination add pagination to the GetTasksRequest func (r GetTasksRequest) WithPagination(page, size int) PaginatedRequest { r.pagination = newPagination(page, size) return r } // AppendToQuery decorates the URL with the query string needed for this Request func (r GetTasksRequest) AppendToQuery(u *url.URL) *url.URL { u = r.pagination.AppendToQuery(u) v := u.Query() if r.Name != "" { v.Add("name", r.Name) } if r.Active { v.Add("is-active", "true") } u.RawQuery = v.Encode() return u } type AddTaskRequest struct { Name string `json:"name"` AssigneeIDs *[]string `json:"assigneeIds,omitempty"` Billable *bool `json:"billable,omitempty"` Estimate *Duration `json:"estimate,omitempty"` Status *string `json:"status,omitempty"` } type UpdateTaskRequest struct { Name string `json:"name"` AssigneeIDs *[]string `json:"assigneeIds,omitempty"` Billable *bool `json:"billable,omitempty"` Estimate *Duration `json:"estimate,omitempty"` Status *string `json:"status,omitempty"` } type ChangeTimeEntriesInvoicedRequest struct { TimeEntryIDs []string `json:"timeEntryIds"` Invoiced bool `json:"invoiced"` } type WorkspaceUsersRequest struct { Email string pagination } // WithPagination add pagination to the WorkspaceUsersRequest func (r WorkspaceUsersRequest) WithPagination(page, size int) PaginatedRequest { r.pagination = newPagination(page, size) return r } // AppendToQuery decorates the URL with the query string needed for this Request func (r WorkspaceUsersRequest) AppendToQuery(u *url.URL) *url.URL { u = r.pagination.AppendToQuery(u) v := u.Query() if r.Email != "" { v.Add("email", r.Email) } u.RawQuery = v.Encode() return u } // UpdateProjectUserRateRequest represents a request to change a user // billable rate on a project type UpdateProjectUserRateRequest struct { Amount uint `json:"amount"` Since *DateTime `json:"since,omitempty"` } // BaseEstimateRequest is basic information to estime a project type BaseEstimateRequest struct { Type *EstimateType `json:"type,omitempty"` Active bool `json:"active"` ResetOptions *EstimateResetOption `json:"resetOption,omitempty"` } // TimeEstimateRequest set parameters for time estimate on a project type TimeEstimateRequest struct { BaseEstimateRequest Estimate *Duration `json:"estimate,omitempty"` } // BudgetEstimateRequest set parameters for time estimate on a project type BudgetEstimateRequest struct { BaseEstimateRequest Estimate *uint64 `json:"estimate,omitempty"` } // UpdateProjectEstimateRequest represents a request to set a estimate of a // project type UpdateProjectEstimateRequest struct { TimeEstimate TimeEstimateRequest `json:"timeEstimate"` BudgetEstimate BudgetEstimateRequest `json:"budgetEstimate"` } ================================================ FILE: api/httpClient.go ================================================ package api import ( "bytes" "encoding/json" "io" "net/http" "net/url" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/pkg/errors" ) // QueryAppender an interface to identify if the parameters should be sent through the query or body type QueryAppender interface { AppendToQuery(*url.URL) *url.URL } // ErrorNotFound Not Found var ErrorNotFound = dto.Error{Message: "Nothing was found", Code: 404} // ErrorForbidden Forbidden var ErrorForbidden = dto.Error{Message: "Forbidden", Code: 403} // ErrorTooManyRequests Too Many Requests var ErrorTooManyRequests = dto.Error{Message: "Too Many Requests", Code: 429} type transport struct { apiKey string next http.RoundTripper } func (t transport) RoundTrip(r *http.Request) (*http.Response, error) { r.Header.Set("X-Api-Key", t.apiKey) return t.next.RoundTrip(r) } // NewRequest to be used in Client func (c *client) NewRequest(method, uri string, body interface{}) (*http.Request, error) { u, err := c.baseURL.Parse(c.baseURL.Path + "/" + uri) if err != nil { return nil, err } if qa, ok := body.(QueryAppender); ok { u = qa.AppendToQuery(u) } if method == "GET" { body = nil } var buf io.ReadWriter if body != nil { buf = new(bytes.Buffer) err := json.NewEncoder(buf).Encode(body) if err != nil { return nil, err } c.infof("request body: %s", buf.(*bytes.Buffer)) } req, err := http.NewRequest(method, u.String(), buf) if err != nil { return nil, err } if body != nil { req.Header.Set("Content-Type", "application/json") } req.Header.Set("Accept", "application/json") return req, nil } // Do executes a http.Request inside the Clockify's Client func (c *client) Do( req *http.Request, v interface{}, name string) (r *http.Response, err error) { <-c.requestTickets r, err = c.Client.Do(req) if err != nil { return r, err } defer func() { if e := r.Body.Close(); e != nil { err = e } }() buf := new(bytes.Buffer) _, err = io.Copy(buf, r.Body) if err != nil { return nil, errors.WithStack(err) } if c.debugLogger != nil { c.debugf("name: %s, method: %s, url: %s, status: %d, response: \"%s\"", name, req.Method, req.URL.String(), r.StatusCode, buf) } else { c.infof("name: %s, method: %s, url: %s, status: %d", name, req.Method, req.URL.String(), r.StatusCode) } decoder := json.NewDecoder(buf) if r.StatusCode < 200 || r.StatusCode > 300 { var apiErr dto.Error err = decoder.Decode(&apiErr) if err != nil && err != io.EOF { return r, errors.WithStack(err) } if r.StatusCode == 404 && apiErr.Message == "" { apiErr = ErrorNotFound } if r.StatusCode == 403 && apiErr.Message == "" { apiErr = ErrorForbidden } if r.StatusCode == 429 && apiErr.Message == "" { apiErr = ErrorTooManyRequests } if apiErr.Message == "" { apiErr.Message = "No response" } return r, errors.WithStack(apiErr) } if v == nil { return r, nil } if buf.Len() == 0 { return r, nil } return r, errors.WithStack(decoder.Decode(v)) } ================================================ FILE: api/logger.go ================================================ package api // Logger for the Client type Logger interface { Print(v ...interface{}) Printf(format string, v ...interface{}) Println(v ...interface{}) } // SetDebugLogger debug logger func (c *client) SetDebugLogger(logger Logger) Client { c.debugLogger = logger return c } func (c *client) debugf(format string, v ...interface{}) { if c.debugLogger == nil { return } c.debugLogger.Printf(format, v...) } // SetInfoLogger info logger func (c *client) SetInfoLogger(logger Logger) Client { c.infoLogger = logger return c } func (c *client) infof(format string, v ...interface{}) { if c.infoLogger == nil { return } c.infoLogger.Printf(format, v...) } ================================================ FILE: api/project_test.go ================================================ package api_test import ( "testing" "time" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" ) func TestUpdateProjectMemberships(t *testing.T) { exampleID2 := "62f2af744a912b05acc7c792" errPrefix := `update project memberships: ` uri := "/v1/workspaces/" + exampleID + "/projects/" + exampleID + "/memberships" tts := []testCase{ &simpleTestCase{ name: "requires workspace", param: api.UpdateProjectMembershipsParam{ProjectID: "p1"}, err: errPrefix + "workspace is required", }, &simpleTestCase{ name: "requires project", param: api.UpdateProjectMembershipsParam{Workspace: "w"}, err: errPrefix + "project id is required", }, &simpleTestCase{ name: "valid workspace", param: api.UpdateProjectMembershipsParam{ Workspace: "w", ProjectID: exampleID, }, err: errPrefix + "workspace .* is not valid ID", }, &simpleTestCase{ name: "valid project", param: api.UpdateProjectMembershipsParam{ Workspace: exampleID, ProjectID: "p1", }, err: errPrefix + "project .* is not valid ID", }, &simpleTestCase{ name: "valid user or groups", param: api.UpdateProjectMembershipsParam{ Workspace: exampleID, ProjectID: exampleID, Memberships: []api.UpdateMembership{{ UserOrGroupID: "ug", }}, }, err: errPrefix + "user or group .* is not valid ID", }, &simpleTestCase{ name: "required user or groups", param: api.UpdateProjectMembershipsParam{ Workspace: exampleID, ProjectID: exampleID, Memberships: []api.UpdateMembership{ {UserOrGroupID: ""}, }, }, err: errPrefix + `user or group is required`, }, &simpleTestCase{ name: "valid user or groups (second one)", param: api.UpdateProjectMembershipsParam{ Workspace: exampleID, ProjectID: exampleID, Memberships: []api.UpdateMembership{ {UserOrGroupID: exampleID}, {UserOrGroupID: "ug"}, }, }, err: errPrefix + `user or group \("ug"\) is not valid ID`, }, &simpleTestCase{ name: "simplest update", param: api.UpdateProjectMembershipsParam{ Workspace: exampleID, ProjectID: exampleID, }, result: dto.Project{ID: "p1", Name: "project 1"}, requestMethod: "patch", requestUrl: uri, requestBody: `{"memberships":[]}`, responseStatus: 200, responseBody: `{"id":"p1", "name": "project 1"}`, }, &simpleTestCase{ name: "update with members", param: api.UpdateProjectMembershipsParam{ Workspace: exampleID, ProjectID: exampleID, Memberships: []api.UpdateMembership{{ UserOrGroupID: exampleID, }}, }, result: dto.Project{ID: "p1", Name: "project 1"}, requestMethod: "patch", requestUrl: uri, requestBody: `{"memberships":[{ "userId":"` + exampleID + `", "hourlyRate":{"amount":0} }]}`, responseStatus: 200, responseBody: `{"id":"p1", "name": "project 1"}`, }, &simpleTestCase{ name: "update with many members", param: api.UpdateProjectMembershipsParam{ Workspace: exampleID, ProjectID: exampleID, Memberships: []api.UpdateMembership{ {UserOrGroupID: exampleID}, {UserOrGroupID: exampleID2, HourlyRateAmount: 10}, }, }, result: dto.Project{ID: "p1", Name: "project 1"}, requestMethod: "patch", requestUrl: uri, requestBody: `{"memberships":[ {"userId":"` + exampleID + `", "hourlyRate":{"amount":0}}, {"userId":"` + exampleID2 + `", "hourlyRate":{"amount":10}} ]}`, responseStatus: 200, responseBody: `{"id":"p1", "name": "project 1"}`, }, &simpleTestCase{ name: "error response", param: api.UpdateProjectMembershipsParam{ Workspace: exampleID, ProjectID: exampleID, }, requestMethod: "patch", requestUrl: uri, requestBody: `{"memberships":[]}`, responseStatus: 400, responseBody: `{"code": 10, "message":"error"}`, err: errPrefix + `error`, }, } for _, tt := range tts { runClient(t, tt, func(c api.Client, p interface{}) (interface{}, error) { return c.UpdateProjectMemberships( p.(api.UpdateProjectMembershipsParam)) }) } } func TestDeleteProject(t *testing.T) { errPrefix := `delete project: ` uri := "/v1/workspaces/" + exampleID + "/projects/" + exampleID tts := []testCase{ &simpleTestCase{ name: "requires workspace", param: api.DeleteProjectParam{ProjectID: "p1"}, err: errPrefix + "workspace is required", }, &simpleTestCase{ name: "requires project", param: api.DeleteProjectParam{Workspace: "w"}, err: errPrefix + "project id is required", }, &simpleTestCase{ name: "valid workspace", param: api.DeleteProjectParam{ Workspace: "w", ProjectID: exampleID, }, err: errPrefix + "workspace .* is not valid ID", }, &simpleTestCase{ name: "valid project", param: api.DeleteProjectParam{ Workspace: exampleID, ProjectID: "p1", }, err: errPrefix + "project .* is not valid ID", }, &simpleTestCase{ name: "delete", param: api.DeleteProjectParam{ Workspace: exampleID, ProjectID: exampleID, }, result: dto.Project{ID: "p1", Name: "project 1"}, requestMethod: "delete", requestUrl: uri, responseStatus: 200, responseBody: `{"id":"p1", "name": "project 1"}`, }, &simpleTestCase{ name: "error response", param: api.DeleteProjectParam{ Workspace: exampleID, ProjectID: exampleID, }, requestMethod: "delete", requestUrl: uri, responseStatus: 400, responseBody: `{"code": 10, "message":"error"}`, err: errPrefix + `error`, }, } for _, tt := range tts { runClient(t, tt, func(c api.Client, p interface{}) (interface{}, error) { return c.DeleteProject(p.(api.DeleteProjectParam)) }) } } func TestGetProject(t *testing.T) { errPrefix := `get project "\w*": ` uri := "/v1/workspaces/" + exampleID + "/projects/" + exampleID tts := []testCase{ &simpleTestCase{ name: "requires workspace", param: api.GetProjectParam{ProjectID: "p1"}, err: errPrefix + "workspace is required", }, &simpleTestCase{ name: "requires project", param: api.GetProjectParam{Workspace: "w"}, err: errPrefix + "project id is required", }, &simpleTestCase{ name: "valid workspace", param: api.GetProjectParam{ Workspace: "w", ProjectID: exampleID, }, err: errPrefix + "workspace .* is not valid ID", }, &simpleTestCase{ name: "valid project", param: api.GetProjectParam{ Workspace: exampleID, ProjectID: "p1", }, err: errPrefix + "project .* is not valid ID", }, &simpleTestCase{ name: "simple", param: api.GetProjectParam{ Workspace: exampleID, ProjectID: exampleID, }, result: &dto.Project{ID: "p1", Name: "project 1"}, requestMethod: "get", requestUrl: uri, responseStatus: 200, responseBody: `{"id":"p1", "name": "project 1"}`, }, &simpleTestCase{ name: "hydrated", param: api.GetProjectParam{ Workspace: exampleID, ProjectID: exampleID, Hydrate: true, }, result: &dto.Project{ID: "p1", Name: "project 1", Hydrated: true}, requestMethod: "get", requestUrl: uri + "?hydrated=true", responseStatus: 200, responseBody: `{"id":"p1", "name": "project 1"}`, }, &simpleTestCase{ name: "error response", param: api.GetProjectParam{ Workspace: exampleID, ProjectID: exampleID, }, requestMethod: "get", requestUrl: uri, responseStatus: 400, responseBody: `{"code": 10, "message":"error"}`, err: errPrefix + `error`, }, &simpleTestCase{ name: "not found", param: api.GetProjectParam{ Workspace: exampleID, ProjectID: exampleID, }, requestMethod: "get", requestUrl: uri, responseStatus: 404, responseBody: `{"code": 0, "message":"not found"}`, err: errPrefix + `not found`, }, } for _, tt := range tts { runClient(t, tt, func(c api.Client, p interface{}) (interface{}, error) { return c.GetProject(p.(api.GetProjectParam)) }) } } func TestGetProjects(t *testing.T) { errPrefix := "get projects: " uri := "/v1/workspaces/" + exampleID + "/projects" var l []dto.Project tts := []testCase{ &simpleTestCase{ name: "requires workspace", param: api.GetProjectsParam{}, err: errPrefix + "workspace is required", }, &simpleTestCase{ name: "valid workspace", param: api.GetProjectsParam{Workspace: "w"}, err: errPrefix + "workspace .* is not valid ID", }, (&multiRequestTestCase{ name: "get all pages, but find none", param: api.GetProjectsParam{ Workspace: exampleID, PaginationParam: api.AllPages(), }, result: l, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?page=1&page-size=50", status: 200, response: "[]", }), (&multiRequestTestCase{ name: "get all pages, find five", param: api.GetProjectsParam{ Workspace: exampleID, PaginationParam: api.PaginationParam{ PageSize: 2, AllPages: true, }, }, result: []dto.Project{ {ID: "p1"}, {ID: "p2"}, {ID: "p3"}, {ID: "p4"}, {ID: "p5"}, }, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?page=1&page-size=2", status: 200, response: `[{"id":"p1"},{"id":"p2"}]`, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?page=2&page-size=2", status: 200, response: `[{"id":"p3"},{"id":"p4"}]`, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?page=3&page-size=2", status: 200, response: `[{"id":"p5"}]`, }), (&multiRequestTestCase{ name: "get all pages, hydrated", param: api.GetProjectsParam{ Workspace: exampleID, Hydrate: true, PaginationParam: api.PaginationParam{ PageSize: 1, AllPages: true, }, }, result: []dto.Project{ {ID: "p1", Hydrated: true}, {ID: "p2", Hydrated: true}, }, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?hydrated=true&page=1&page-size=1", status: 200, response: `[{"id":"p1"}]`, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?hydrated=true&page=2&page-size=1", status: 200, response: `[{"id":"p2"}]`, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?hydrated=true&page=3&page-size=1", status: 200, response: `[]`, }), &simpleTestCase{ name: "all parameters", param: api.GetProjectsParam{ Workspace: exampleID, Hydrate: true, Name: "project", Clients: []string{"c1", "c2"}, PaginationParam: api.AllPages(), }, result: []dto.Project{{ ID: "p1", Name: "project 1", Hydrated: true}}, requestMethod: "get", requestUrl: uri + "?clients=c1%2Cc2&hydrated=true&name=project&" + "page=1&page-size=50", responseStatus: 200, responseBody: `[{"id":"p1", "name": "project 1"}]`, }, &simpleTestCase{ name: "error response", param: api.GetProjectsParam{ Workspace: exampleID, PaginationParam: api.PaginationParam{Page: 2}, }, requestMethod: "get", requestUrl: uri + "?page=2&page-size=50", responseStatus: 400, responseBody: `{"code": 10, "message":"error"}`, err: `get projects: error \(code: 10\)`, }, &simpleTestCase{ name: "missing duration", param: api.GetProjectsParam{ Workspace: exampleID, PaginationParam: api.AllPages(), }, result: []dto.Project{ { ID: "wod", Name: "without duration", Archived: true, Duration: nil, }, { ID: "wd", Name: "with duration", Archived: true, Duration: &dto.Duration{ Duration: 561*time.Hour + 13*time.Minute + 28*time.Second, }, }, }, requestMethod: "get", requestUrl: uri + "?page=1&page-size=50", responseStatus: 200, responseBody: `[{ "id":"wod", "name":"without duration", "archived":true, "duration":null, "note":"" }, { "id":"wd", "name":"with duration", "archived":true, "duration":"PT561H13M28S", "note":"" }]`, }, } for _, tt := range tts { runClient(t, tt, func(c api.Client, p interface{}) (interface{}, error) { return c.GetProjects(p.(api.GetProjectsParam)) }) } } func TestUpdateProjectTemplate(t *testing.T) { errPrefix := "update project template: " tts := []simpleTestCase{ { name: "workspace require", param: api.UpdateProjectTemplateParam{ ProjectID: exampleID, }, err: errPrefix + "workspace is required", }, { name: "project require", param: api.UpdateProjectTemplateParam{ Workspace: exampleID, }, err: errPrefix + "project id is required", }, { name: "valid workspace", param: api.UpdateProjectTemplateParam{ ProjectID: exampleID, Workspace: "w", }, err: errPrefix + "workspace .* is not valid ID", }, { name: "valid project", param: api.UpdateProjectTemplateParam{ ProjectID: "p", Workspace: exampleID, }, err: errPrefix + "project .* is not valid ID", }, { name: "into template", param: api.UpdateProjectTemplateParam{ ProjectID: exampleID, Workspace: exampleID, Template: true, }, result: dto.Project{ID: exampleID}, requestMethod: "patch", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID + "/template", requestBody: `{"isTemplate":true}`, responseStatus: 200, responseBody: `{"id":"` + exampleID + `"}`, }, { name: "not a template", param: api.UpdateProjectTemplateParam{ ProjectID: exampleID, Workspace: exampleID, Template: false, }, result: dto.Project{ID: exampleID}, requestMethod: "patch", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID + "/template", requestBody: `{"isTemplate":false}`, responseStatus: 200, responseBody: `{"id":"` + exampleID + `"}`, }, { name: "error", param: api.UpdateProjectTemplateParam{ ProjectID: exampleID, Workspace: exampleID, Template: false, }, err: errPrefix + "failed .code: 90.", requestMethod: "patch", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID + "/template", requestBody: `{"isTemplate":false}`, responseStatus: 400, responseBody: `{"message":"failed", "code": 90}`, }, } for i := range tts { runClient(t, &tts[i], func(c api.Client, p interface{}) (interface{}, error) { return c.UpdateProjectTemplate( p.(api.UpdateProjectTemplateParam)) }) } } func TestUpdateProjectEstimate(t *testing.T) { errPrefix := "update project estimate: " tts := []simpleTestCase{ { name: "workspace require", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Method: api.EstimateMethodNone, }, err: errPrefix + "workspace is required", }, { name: "project require", param: api.UpdateProjectEstimateParam{ Workspace: exampleID, Method: api.EstimateMethodNone, }, err: errPrefix + "project id is required", }, { name: "estimate method required", param: api.UpdateProjectEstimateParam{ Workspace: exampleID, ProjectID: exampleID, }, err: errPrefix + "estimate method is required", }, { name: "valid workspace", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: "w", Method: api.EstimateMethodNone, }, err: errPrefix + "workspace .* is not valid ID", }, { name: "valid project", param: api.UpdateProjectEstimateParam{ ProjectID: "p", Workspace: exampleID, Method: api.EstimateMethodNone, }, err: errPrefix + "project .* is not valid ID", }, { name: "valid method", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: exampleID, Method: "m", }, err: errPrefix + "valid options for estimate method are", }, { name: "type should be set for budget", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: exampleID, Method: api.EstimateMethodBudget, Type: "t", }, err: errPrefix + "valid options for estimate type are", }, { name: "valid reset option", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: exampleID, Method: api.EstimateMethodBudget, Type: api.EstimateTypeTask, ResetOption: "daily", }, err: errPrefix + "valid options for reset option are", }, { name: "type should be set for time", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: exampleID, Method: api.EstimateMethodTime, }, err: errPrefix + "valid options for estimate type are", }, { name: "estimate should be set for budget method & type project", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: exampleID, Method: api.EstimateMethodBudget, Type: api.EstimateTypeProject, }, err: errPrefix + "estimate should be greater than zero for type project", }, { name: "estimate should be set for time method & type project", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: exampleID, Method: api.EstimateMethodTime, Type: api.EstimateTypeProject, }, err: errPrefix + "estimate should be greater than zero for type project", }, { name: "estimate should be positive for time method & type project", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: exampleID, Method: api.EstimateMethodTime, Type: api.EstimateTypeProject, Estimate: -1, }, err: errPrefix + "estimate should be greater than zero for type project", }, { name: "estimate should be positive for time method & type project", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: exampleID, Method: api.EstimateMethodTime, Type: api.EstimateTypeProject, Estimate: -1, }, err: errPrefix + "estimate should be greater than zero for type project", }, { name: "set estimate with budget for project", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: exampleID, Method: api.EstimateMethodBudget, Type: api.EstimateTypeProject, Estimate: 1000, }, requestMethod: "patch", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID + "/estimate", requestBody: `{ "timeEstimate": {"active": false}, "budgetEstimate": { "active": true, "estimate": 1000, "type": "MANUAL" } }`, responseStatus: 200, }, { name: "set estimate with time for project", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: exampleID, Method: api.EstimateMethodTime, Type: api.EstimateTypeProject, Estimate: int64(time.Minute)*90 + int64(time.Second)*15, }, requestMethod: "patch", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID + "/estimate", requestBody: `{ "budgetEstimate": {"active": false}, "timeEstimate": { "active": true, "estimate": "PT1H30M15S", "type": "MANUAL" } }`, responseStatus: 200, }, { name: "set estimate to none for project", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: exampleID, Method: api.EstimateMethodNone, }, requestMethod: "patch", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID + "/estimate", requestBody: `{ "budgetEstimate": {"active": false}, "timeEstimate": {"active": false} }`, responseStatus: 200, }, { name: "set estimate with budget for tasks", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: exampleID, Method: api.EstimateMethodBudget, Type: api.EstimateTypeTask, Estimate: 1000, }, requestMethod: "patch", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID + "/estimate", requestBody: `{ "timeEstimate": {"active": false}, "budgetEstimate": { "active": true, "type": "AUTO" } }`, responseStatus: 200, }, { name: "set estimate with time for task", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: exampleID, Method: api.EstimateMethodTime, Type: api.EstimateTypeTask, Estimate: int64(time.Minute)*90 + int64(time.Second)*15, }, requestMethod: "patch", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID + "/estimate", requestBody: `{ "budgetEstimate": {"active": false}, "timeEstimate": { "active": true, "type": "AUTO" } }`, responseStatus: 200, }, { name: "set estimate with time for task, and monthly reset", param: api.UpdateProjectEstimateParam{ ProjectID: exampleID, Workspace: exampleID, Method: api.EstimateMethodTime, Type: api.EstimateTypeTask, ResetOption: api.EstimateResetOptionMonthly, }, requestMethod: "patch", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID + "/estimate", requestBody: `{ "budgetEstimate": {"active": false}, "timeEstimate": { "active": true, "type": "AUTO", "resetOption": "MONTHLY" } }`, responseStatus: 200, }, } for i := range tts { runClient(t, &tts[i], func(c api.Client, p interface{}) (interface{}, error) { return c.UpdateProjectEstimate( p.(api.UpdateProjectEstimateParam)) }) } } func TestUpdateProjectUserCostRate(t *testing.T) { testUpdateProjectUserRate(t, "update project user cost rate: ", "cost-rate", func(c api.Client, p interface{}) (interface{}, error) { return c.UpdateProjectUserCostRate( p.(api.UpdateProjectUserRateParam)) }) } func TestUpdateProjectUserBillableRate(t *testing.T) { testUpdateProjectUserRate(t, "update project user billable rate: ", "hourly-rate", func(c api.Client, p interface{}) (interface{}, error) { return c.UpdateProjectUserBillableRate( p.(api.UpdateProjectUserRateParam)) }) } func testUpdateProjectUserRate(t *testing.T, errPrefix, uriSufix string, fn func(api.Client, interface{}) (interface{}, error)) { since, _ := time.Parse("2006-01-02", "2022-02-02") tts := []simpleTestCase{ { name: "project is required", param: api.UpdateProjectUserRateParam{ Workspace: "w", UserID: "u", }, err: errPrefix + "project id is required", }, { name: "workspace is required", param: api.UpdateProjectUserRateParam{ ProjectID: "p-1", UserID: "u", }, err: errPrefix + "workspace is required", }, { name: "user is required", param: api.UpdateProjectUserRateParam{ ProjectID: "p-1", Workspace: "w", }, err: errPrefix + "user id is required", }, { name: "project should be a ID", param: api.UpdateProjectUserRateParam{ ProjectID: "p-1", Workspace: exampleID, UserID: exampleID, }, err: errPrefix + "project id (.*) is not valid", }, { name: "user should be a ID", param: api.UpdateProjectUserRateParam{ ProjectID: exampleID, Workspace: exampleID, UserID: "u-1", }, err: errPrefix + "user id (.*) is not valid", }, { name: "workspace should be a ID", param: api.UpdateProjectUserRateParam{ ProjectID: exampleID, Workspace: "w", UserID: exampleID, }, err: errPrefix + "workspace (.*) is not valid", }, { name: "only amount", param: api.UpdateProjectUserRateParam{ ProjectID: exampleID, Workspace: exampleID, UserID: exampleID, Amount: 10, }, requestMethod: "put", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID + "/users/" + exampleID + "/" + uriSufix, requestBody: `{"amount":10}`, responseStatus: 200, }, { name: "amount and since", param: api.UpdateProjectUserRateParam{ ProjectID: exampleID, Workspace: exampleID, UserID: exampleID, Amount: 10, Since: &since, }, requestMethod: "put", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID + "/users/" + exampleID + "/" + uriSufix, requestBody: `{"amount":10,"since":"2022-02-02T00:00:00Z"}`, err: errPrefix + "custom error.*code: 42", responseStatus: 400, responseBody: `{"message":"custom error","code":42}`, }, { name: "fail", param: api.UpdateProjectUserRateParam{ ProjectID: exampleID, Workspace: exampleID, UserID: exampleID, Amount: 10, Since: &since, }, requestMethod: "put", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID + "/users/" + exampleID + "/" + uriSufix, requestBody: `{"amount":10,"since":"2022-02-02T00:00:00Z"}`, err: errPrefix + "custom error.*code: 42", responseStatus: 400, responseBody: `{"message":"custom error","code":42}`, }, } for i := range tts { runClient(t, &tts[i], fn) } } func TestUpdateProject(t *testing.T) { bt := true bf := false n := "special" empty := "" tts := []simpleTestCase{ { name: "project is required", param: api.UpdateProjectParam{Workspace: "w"}, err: "update project: project id is required", }, { name: "workspace is required", param: api.UpdateProjectParam{ProjectID: "p-1"}, err: "update project: workspace is required", }, { name: "project should be a ID", param: api.UpdateProjectParam{ ProjectID: "p-1", Workspace: exampleID, }, err: "update project: project id (.*) is not valid", }, { name: "workspace should be a ID", param: api.UpdateProjectParam{ ProjectID: exampleID, Workspace: "w", }, err: "update project: workspace (.*) is not valid", }, { name: "color is not hex", param: api.UpdateProjectParam{ ProjectID: exampleID, Workspace: exampleID, Color: "#zzz", }, err: "update project: color .* is not a hex string", }, { name: "color must have 3 or 6 numbers (4)", param: api.UpdateProjectParam{ ProjectID: exampleID, Workspace: exampleID, Color: "#0000", }, err: "update project: color must have 3.*or 6.*numbers", }, { name: "color must have 3 or 6 numbers (2)", param: api.UpdateProjectParam{ ProjectID: exampleID, Workspace: exampleID, Color: "#00", }, err: "update project: color must have 3.*or 6.*numbers", }, { name: "empty update", param: api.UpdateProjectParam{ ProjectID: exampleID, Workspace: exampleID, }, requestMethod: "put", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID, requestBody: "{}", responseStatus: 200, }, { name: "full update", param: api.UpdateProjectParam{ ProjectID: exampleID, Workspace: exampleID, Name: "a new name", Public: &bt, Archived: &bf, Note: &n, ClientId: &exampleID, Color: "012345", Billable: &bt, }, requestMethod: "put", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID, requestBody: `{ "archived":false, "isPublic":true, "billable":true, "clientId":"` + exampleID + `", "note": "special", "color": "#012345", "name":"a new name" }`, responseStatus: 200, }, { name: "expand color and remove client", param: api.UpdateProjectParam{ ProjectID: exampleID, Workspace: exampleID, ClientId: &empty, Color: "#0f0", }, requestMethod: "put", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID, requestBody: `{ "clientId":"", "color": "#00ff00" }`, responseStatus: 200, }, { name: "report 404", param: api.UpdateProjectParam{ ProjectID: exampleID, Workspace: exampleID, }, err: "update project: Nothing was found .*404", requestMethod: "put", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID, requestBody: `{}`, responseStatus: 404, }, { name: "report 403", param: api.UpdateProjectParam{ ProjectID: exampleID, Workspace: exampleID, }, err: "update project: Forbidden.*403", requestMethod: "put", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID, requestBody: `{}`, responseStatus: 403, }, { name: "report no response", param: api.UpdateProjectParam{ ProjectID: exampleID, Workspace: exampleID, }, err: "update project: No response", requestMethod: "put", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID, requestBody: `{}`, responseStatus: 400, responseBody: `{}`, }, { name: "report error", param: api.UpdateProjectParam{ ProjectID: exampleID, Workspace: exampleID, }, err: "update project: custom error.*code: 42", requestMethod: "put", requestUrl: "/v1/workspaces/" + exampleID + "/projects/" + exampleID, requestBody: `{}`, responseStatus: 400, responseBody: `{"message":"custom error","code":42}`, }, } for i := range tts { runClient(t, &tts[i], func( c api.Client, p interface{}) (interface{}, error) { return c.UpdateProject(p.(api.UpdateProjectParam)) }) } } ================================================ FILE: api/tag_test.go ================================================ package api_test import ( "testing" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" ) func TestGetTags(t *testing.T) { errPrefix := `get tags.*: ` uri := "/v1/workspaces/" + exampleID + "/tags" var l []dto.Tag tts := []testCase{ &simpleTestCase{ name: "requires workspace", param: api.GetTagsParam{}, err: errPrefix + "workspace is required", }, &simpleTestCase{ name: "valid workspace", param: api.GetTagsParam{Workspace: "w"}, err: errPrefix + "workspace .* is not valid ID", }, (&multiRequestTestCase{ name: "get all pages, but find none", param: api.GetTagsParam{ Workspace: exampleID, PaginationParam: api.AllPages(), }, result: l, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?page=1&page-size=50", status: 200, response: "[]", }), (&multiRequestTestCase{ name: "get all pages, find five", param: api.GetTagsParam{ Workspace: exampleID, PaginationParam: api.PaginationParam{ PageSize: 2, AllPages: true, }, }, result: []dto.Tag{ {ID: "p1"}, {ID: "p2"}, {ID: "p3"}, {ID: "p4"}, {ID: "p5"}, }, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?page=1&page-size=2", status: 200, response: `[{"id":"p1"},{"id":"p2"}]`, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?page=2&page-size=2", status: 200, response: `[{"id":"p3"},{"id":"p4"}]`, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?page=3&page-size=2", status: 200, response: `[{"id":"p5"}]`, }), &simpleTestCase{ name: "all parameters", param: api.GetTagsParam{ Workspace: exampleID, Name: "tag", PaginationParam: api.AllPages(), }, result: []dto.Tag{{ID: "p1", Name: "tag 1"}}, requestMethod: "get", requestUrl: uri + "?name=tag&page=1&page-size=50", responseStatus: 200, responseBody: `[{"id":"p1", "name": "tag 1"}]`, }, &simpleTestCase{ name: "error response", param: api.GetTagsParam{ Workspace: exampleID, PaginationParam: api.PaginationParam{Page: 2}, }, requestMethod: "get", requestUrl: uri + "?page=2&page-size=50", responseStatus: 400, responseBody: `{"code": 10, "message":"error"}`, err: errPrefix + `error \(code: 10\)`, }, } for _, tt := range tts { runClient(t, tt, func(c api.Client, p interface{}) (interface{}, error) { return c.GetTags( p.(api.GetTagsParam)) }) } } ================================================ FILE: api/task_test.go ================================================ package api_test import ( "testing" "time" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" ) func TestGetTasks(t *testing.T) { errPrefix := `get tasks from project .*: ` uri := "/v1/workspaces/" + exampleID + "/projects/" + exampleID + "/tasks" var l []dto.Task tts := []testCase{ &simpleTestCase{ name: "requires workspace", param: api.GetTasksParam{ProjectID: exampleID}, err: errPrefix + "workspace is required", }, &simpleTestCase{ name: "valid workspace", param: api.GetTasksParam{Workspace: "w", ProjectID: exampleID}, err: errPrefix + "workspace .* is not valid ID", }, &simpleTestCase{ name: "requires project id", param: api.GetTasksParam{Workspace: exampleID}, err: errPrefix + "project id is required", }, &simpleTestCase{ name: "valid project id", param: api.GetTasksParam{ProjectID: "w", Workspace: exampleID}, err: errPrefix + "project id .* is not valid ID", }, (&multiRequestTestCase{ name: "get all pages, but find none", param: api.GetTasksParam{ Workspace: exampleID, ProjectID: exampleID, PaginationParam: api.AllPages(), }, result: l, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?page=1&page-size=50", status: 200, response: "[]", }), (&multiRequestTestCase{ name: "get all pages, find five", param: api.GetTasksParam{ Workspace: exampleID, ProjectID: exampleID, PaginationParam: api.PaginationParam{ PageSize: 2, AllPages: true, }, }, result: []dto.Task{ {ID: "p1"}, {ID: "p2"}, {ID: "p3"}, {ID: "p4"}, {ID: "p5"}, }, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?page=1&page-size=2", status: 200, response: `[{"id":"p1"},{"id":"p2"}]`, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?page=2&page-size=2", status: 200, response: `[{"id":"p3"},{"id":"p4"}]`, }). addHttpCall(&httpRequest{ method: "get", url: uri + "?page=3&page-size=2", status: 200, response: `[{"id":"p5"}]`, }), &simpleTestCase{ name: "all parameters", param: api.GetTasksParam{ Workspace: exampleID, ProjectID: exampleID, Name: "project", PaginationParam: api.AllPages(), }, result: []dto.Task{{ID: "p1", Name: "project 1"}}, requestMethod: "get", requestUrl: uri + "?name=project&page=1&page-size=50", responseStatus: 200, responseBody: `[{"id":"p1", "name": "project 1"}]`, }, &simpleTestCase{ name: "error response", param: api.GetTasksParam{ Workspace: exampleID, ProjectID: exampleID, PaginationParam: api.PaginationParam{Page: 2}, }, requestMethod: "get", requestUrl: uri + "?page=2&page-size=50", responseStatus: 400, responseBody: `{"code": 10, "message":"error"}`, err: `get tasks from project .*: error \(code: 10\)`, }, &simpleTestCase{ name: "missing estimate", param: api.GetTasksParam{ Workspace: exampleID, ProjectID: exampleID, PaginationParam: api.AllPages(), }, result: []dto.Task{ { ID: "wod", Name: "without durations", ProjectID: "p", Duration: nil, Estimate: nil, Status: api.TaskStatusDone, UserGroupIDs: []string{}, AssigneeIDs: []string{}, Billable: true, }, { ID: "wd", Name: "with durations", ProjectID: "p", Duration: &dto.Duration{ Duration: 120 * time.Hour, }, Estimate: &dto.Duration{ Duration: 120 * time.Hour, }, Status: api.TaskStatusActive, UserGroupIDs: []string{}, AssigneeIDs: []string{}, Billable: true, }, }, requestMethod: "get", requestUrl: uri + "?page=1&page-size=50", responseStatus: 200, responseBody: `[{ "id": "wod", "name": "without durations", "projectId": "p", "assigneeIds": [], "assigneeId": null, "userGroupIds": [], "estimate": null, "status": "DONE", "duration": null, "billable": true, "hourlyRate": null, "costRate": null },{ "id": "wd", "name": "with durations", "projectId": "p", "assigneeIds": [], "assigneeId": null, "userGroupIds": [], "estimate": "P120H", "status": "ACTIVE", "duration": "P120H", "billable": true, "hourlyRate": null, "costRate": null }]`, }, } for _, tt := range tts { runClient(t, tt, func(c api.Client, p interface{}) (interface{}, error) { return c.GetTasks( p.(api.GetTasksParam)) }) } } ================================================ FILE: api/timeentry_test.go ================================================ package api_test import ( "testing" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" . "github.com/lucassabreu/clockify-cli/internal/testhlp" "github.com/lucassabreu/clockify-cli/pkg/timehlp" ) func TestCreateTimeEntry(t *testing.T) { uri := "/v1/workspaces/" + exampleID + "/time-entries" end := MustParseTime(timehlp.SimplerTimeFormat, "2022-11-07 11:00") bTrue := true bFalse := false tts := []testCase{ &simpleTestCase{ name: "workspace is required", param: api.CreateTimeEntryParam{}, err: "workspace is required", }, &simpleTestCase{ name: "workspace is valid", param: api.CreateTimeEntryParam{ Workspace: "w", }, err: "workspace .* is not valid ID", }, &simpleTestCase{ name: "with just start time", param: api.CreateTimeEntryParam{ Workspace: exampleID, Start: MustParseTime(timehlp.SimplerTimeFormat, "2022-11-07 10:00"), }, requestMethod: "post", requestUrl: uri, requestBody: `{"start":"2022-11-07T10:00:00Z"}`, responseStatus: 200, responseBody: `{"id": "1"}`, result: dto.TimeEntryImpl{ID: "1"}, }, &simpleTestCase{ name: "with all options (billable)", param: api.CreateTimeEntryParam{ Workspace: exampleID, Start: MustParseTime(timehlp.SimplerTimeFormat, "2022-11-07 10:00"), End: &end, Billable: &bTrue, Description: "new entry", ProjectID: "p", TaskID: "t", TagIDs: []string{"tag1", "tag2"}, }, requestMethod: "post", requestUrl: uri, requestBody: `{ "start":"2022-11-07T10:00:00Z", "end":"2022-11-07T11:00:00Z", "billable": true, "description": "new entry", "projectId": "p", "taskId": "t", "tagIds": ["tag1","tag2"] }`, responseStatus: 200, responseBody: `{"id": "1"}`, result: dto.TimeEntryImpl{ID: "1"}, }, &simpleTestCase{ name: "not billable", param: api.CreateTimeEntryParam{ Workspace: exampleID, Start: MustParseTime(timehlp.SimplerTimeFormat, "2022-11-07 10:00"), Billable: &bFalse, Description: "new entry", ProjectID: "p", }, requestMethod: "post", requestUrl: uri, requestBody: `{ "start":"2022-11-07T10:00:00Z", "billable": false, "description": "new entry", "projectId": "p" }`, responseStatus: 200, responseBody: `{"id": "1"}`, result: dto.TimeEntryImpl{ID: "1"}, }, &simpleTestCase{ name: "error response", param: api.CreateTimeEntryParam{ Workspace: exampleID, Start: MustParseTime(timehlp.SimplerTimeFormat, "2022-11-07 10:00"), }, requestMethod: "post", requestUrl: uri, requestBody: `{"start":"2022-11-07T10:00:00Z"}`, responseStatus: 400, responseBody: `{"code": 10, "message":"error"}`, err: `error`, }, } for _, tt := range tts { runClient(t, tt, func(c api.Client, p interface{}) (interface{}, error) { return c.CreateTimeEntry( p.(api.CreateTimeEntryParam)) }) } } ================================================ FILE: cmd/clockify-cli/main.go ================================================ package main import ( "errors" "fmt" "os" "path" "strings" "github.com/AlecAivazis/survey/v2/terminal" "github.com/lucassabreu/clockify-cli/pkg/cmd" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" ) var ( version = "dev" commit = "none" date = "unknown" ) const ( exitOK = 0 exitError = 1 exitCancel = 2 ) func main() { exitCode := execute() os.Exit(exitCode) } func execute() int { f := cmdutil.NewFactory(cmdutil.Version{ Tag: version, Commit: commit, Date: date, }) rootCmd := cmd.NewCmdRoot(f) rootCmd.SetFlagErrorFunc(func(_ *cobra.Command, err error) error { return cmdutil.FlagErrorWrap(err) }) cmd := rootCmd err := bindViper(rootCmd) if err == nil { cmd, err = rootCmd.ExecuteC() } if err == nil { return exitOK } stderr := cmd.ErrOrStderr() if errors.Is(err, terminal.InterruptErr) { _, _ = fmt.Fprintln(stderr) return exitCancel } var flagError *cmdutil.FlagError if errors.As(err, &flagError) { _, _ = fmt.Fprintln(stderr, flagError.Error()) _, _ = fmt.Fprintln(stderr, cmd.UsageString()) return exitError } if f.Config().IsDebuging() { _, _ = fmt.Fprintf(stderr, "%+v\n", err) } else { _, _ = fmt.Fprintln(stderr, err.Error()) } return exitError } func bindViper(rootCmd *cobra.Command) error { envPrefix := "CLOCKIFY" bind := func(flag *pflag.Flag, conf, sufix string) error { if flag == nil { return nil } flag.Usage = flag.Usage + " (defaults to env $" + envPrefix + "_" + sufix + ")" return viper.BindPFlag(conf, flag) } var err error l := rootCmd.PersistentFlags().Lookup if err = bind(l("token"), cmdutil.CONF_TOKEN, "TOKEN"); err != nil { return err } err = bind(l("workspace"), cmdutil.CONF_WORKSPACE, "WORKSPACE") if err != nil { return err } if err = bind(l("user-id"), cmdutil.CONF_USER_ID, "USER_ID"); err != nil { return err } err = bind(l("log-level"), cmdutil.CONF_LOG_LEVEL, "LOG_LEVEL") if err != nil { return err } viper.RegisterAlias(cmdutil.CONF_ALLOW_NAME_FOR_ID, "allow-project-name") if err = bind(l("allow-name-for-id"), cmdutil.CONF_ALLOW_NAME_FOR_ID, "ALLOW_NAME_FOR_ID"); err != nil { return err } if err = bind(l("interactive"), cmdutil.CONF_INTERACTIVE, "INTERACTIVE"); err != nil { return err } if err = bind(l("interactive-page-size"), cmdutil.CONF_INTERACTIVE_PAGE_SIZE, "INTERACTIVE_PAGE_SIZE"); err != nil { return err } f := l("interactive") f.Usage = f.Usage + "\n" + "You can be disable it temporally by setting it to 0 " + "(-i=0 or " + envPrefix + "_INTERACTIVE=0)" var cfgFile = "" rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.config/clockify-cli/config.yaml)") var viperErr error rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { if viperErr != nil { return viperErr } if withTotals := cmd.Flags().Lookup("with-totals"); withTotals != nil { viper.SetDefault(cmdutil.CONF_SHOW_TOTAL_DURATION, true) if err := viper.BindPFlag( cmdutil.CONF_SHOW_TOTAL_DURATION, withTotals); err != nil { return err } } if flag := cmd.Flags().Lookup("allow-incomplete"); flag != nil { if err := bind(flag, cmdutil.CONF_ALLOW_INCOMPLETE, "ALLOW_INCOMPLETE"); err != nil { return err } } if flag := cmd.Flags().Lookup("tz"); flag != nil { if err := bind(flag, cmdutil.CONF_TIMEZONE, "TIMEZONE"); err != nil { return err } } return nil } cobra.OnInitialize(func() { if cfgFile != "" { viper.SetConfigFile(cfgFile) } else { home, err := homedir.Dir() if err != nil { viperErr = err return } viper.AddConfigPath(home) viper.AddConfigPath(path.Join(home, ".config")) viper.AddConfigPath(path.Join(home, ".config", "clockify-cli")) viper.SetConfigName(".clockify-cli") } viper.SetEnvPrefix(envPrefix) viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AutomaticEnv() err := viper.ReadInConfig() if errors.As(err, &viper.ConfigFileNotFoundError{}) { return } viperErr = err }) return nil } ================================================ FILE: cmd/gendocs/main.go ================================================ package main import ( "fmt" "os" "path" "path/filepath" "strings" "time" "github.com/lucassabreu/clockify-cli/pkg/cmd" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/spf13/cobra/doc" ) const gendocFrontmatterTemplate = `--- date: %s title: "%s" slug: %s url: %s weight: 40 --- ` func main() { if err := execute(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func execute() error { docdir := "site/content/commands" if len(os.Args) > 1 { docdir = os.Args[1] } if err := os.MkdirAll(docdir, os.ModePerm); err != nil { return err } now := time.Now().Format("2006-01-02") prepender := func(filename string) string { name := filepath.Base(filename) base := strings.TrimSuffix(name, path.Ext(name)) url := "/en/commands/" + strings.ToLower(base) + "/" return fmt.Sprintf(gendocFrontmatterTemplate, now, strings.ReplaceAll(base, "_", " "), base, url) } linkHandler := func(name string) string { base := strings.TrimSuffix(name, path.Ext(name)) return "/en/commands/" + strings.ToLower(base) + "/" } cmd := cmd.NewCmdRoot(cmdutil.NewFactory(cmdutil.Version{})) fmt.Println("Generating Hugo command-line documentation in", docdir, "...") err := doc.GenMarkdownTreeCustom(cmd, docdir, prepender, linkHandler) if err != nil { return err } fmt.Println("Done.") return nil } ================================================ FILE: cmd/release/main.go ================================================ package main import ( "bufio" "fmt" "os" "os/exec" "regexp" "strconv" "strings" "time" ) type Version struct { Major int Minor int Patch int } func (v Version) String() string { return fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch) } func bumpVersion(v Version, bumpType string) Version { switch bumpType { case "major": return Version{Major: v.Major + 1, Minor: 0, Patch: 0} case "minor": return Version{Major: v.Major, Minor: v.Minor + 1, Patch: 0} case "patch": return Version{Major: v.Major, Minor: v.Minor, Patch: v.Patch + 1} default: return v } } func findLatestVersion(content string) (Version, error) { re := regexp.MustCompile(`## \[v(\d+)\.(\d+)\.(\d+)\]`) matches := re.FindAllStringSubmatch(content, -1) if len(matches) == 0 { return Version{}, fmt.Errorf("no version found in CHANGELOG.md") } first := matches[0] major, _ := strconv.Atoi(first[1]) minor, _ := strconv.Atoi(first[2]) patch, _ := strconv.Atoi(first[3]) return Version{Major: major, Minor: minor, Patch: patch}, nil } func findUnreleasedSection(content string) (string, int, int) { scanner := bufio.NewScanner(strings.NewReader(content)) var lines []string inUnreleased := false startLine := -1 endLine := -1 for scanner.Scan() { line := scanner.Text() lines = append(lines, line) if strings.HasPrefix(line, "## [Unreleased]") { inUnreleased = true startLine = len(lines) - 1 continue } if inUnreleased && strings.HasPrefix(line, "## [") { endLine = len(lines) - 1 break } } if startLine == -1 { return "", -1, -1 } if endLine == -1 { endLine = len(lines) } section := strings.Join(lines[startLine:endLine], "\n") return section, startLine, endLine } func extractUnreleasedContent(unreleasedSection string) string { lines := strings.Split(unreleasedSection, "\n") if len(lines) <= 1 { return "" } var content []string for i := 1; i < len(lines); i++ { line := lines[i] if strings.HasPrefix(line, "## [") { break } content = append(content, line) } return strings.TrimSpace(strings.Join(content, "\n")) } func updateUnreleasedLink(content string, newVersion string) string { oldUnreleasedLink := regexp.MustCompile(`\[Unreleased\]:.*`).FindString(content) if oldUnreleasedLink == "" { return content } newLink := fmt.Sprintf("[Unreleased]: https://github.com/lucassabreu/clockify-cli/compare/%s...HEAD", newVersion) + "\n" + fmt.Sprintf("[%s]: https://github.com/lucassabreu/clockify-cli/releases/tag/%s", newVersion, newVersion) return strings.Replace(content, oldUnreleasedLink, newLink, 1) } func runGitCommand(args ...string) error { cmd := exec.Command(args[0], args[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = append(os.Environ(), "GIT_EDITOR=true", "GIT_SEQUENCE_EDITOR=true") return cmd.Run() } func main() { if len(os.Args) != 2 { fmt.Fprintln(os.Stderr, "Usage: release major|minor|patch") os.Exit(1) } bumpType := strings.ToLower(os.Args[1]) if bumpType != "major" && bumpType != "minor" && bumpType != "patch" { fmt.Fprintln(os.Stderr, "Error: argument must be major, minor, or patch") os.Exit(1) } changelogPath := "CHANGELOG.md" if _, err := os.Stat(changelogPath); os.IsNotExist(err) { changelogPath = "../../CHANGELOG.md" } contentBytes, err := os.ReadFile(changelogPath) if err != nil { fmt.Fprintf(os.Stderr, "Error reading %s: %v\n", changelogPath, err) os.Exit(1) } content := string(contentBytes) unreleasedSection, startLine, _ := findUnreleasedSection(content) if unreleasedSection == "" || startLine == -1 { fmt.Fprintln(os.Stderr, "Error: no [Unreleased] section found in CHANGELOG.md") os.Exit(1) } unreleasedContent := extractUnreleasedContent(unreleasedSection) if unreleasedContent == "" { fmt.Fprintln(os.Stderr, "Error: [Unreleased] section is empty, nothing to release") os.Exit(1) } latestVersion, err := findLatestVersion(content) if err != nil { fmt.Fprintf(os.Stderr, "Error finding latest version: %v\n", err) os.Exit(1) } newVersion := bumpVersion(latestVersion, bumpType) date := time.Now().Format("2006-01-02") fmt.Printf("Releasing %s (bumping %s from %s)\n", newVersion, bumpType, latestVersion) newVersionSection := fmt.Sprintf("## [%s] - %s\n\n%s\n", newVersion, date, unreleasedContent) lines := strings.Split(content, "\n") var unreleasedStartIdx, unreleasedEndIdx int for i, line := range lines { if strings.HasPrefix(line, "## [Unreleased]") { unreleasedStartIdx = i } if unreleasedStartIdx > 0 && strings.HasPrefix(line, "## [v") { unreleasedEndIdx = i break } } var newLines []string newLines = append(newLines, lines[:unreleasedStartIdx+1]...) newLines = append(newLines, "") newLines = append(newLines, newVersionSection) newLines = append(newLines, lines[unreleasedEndIdx:]...) newContent := strings.Join(newLines, "\n") newContent = updateUnreleasedLink(newContent, newVersion.String()) if err := os.WriteFile(changelogPath, []byte(newContent), 0644); err != nil { fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", changelogPath, err) os.Exit(1) } fmt.Printf("Updated %s\n", changelogPath) if err := runGitCommand("git", "add", changelogPath); err != nil { fmt.Fprintln(os.Stderr, "Error: git add failed") os.Exit(1) } commitMsg := fmt.Sprintf("release: %s", newVersion) if err := runGitCommand("git", "commit", "-m", commitMsg); err != nil { fmt.Fprintln(os.Stderr, "Error: git commit failed") os.Exit(1) } fmt.Printf("Created commit: %s\n", commitMsg) if err := runGitCommand("git", "tag", "-a", newVersion.String(), "-m", fmt.Sprintf("Release %s", newVersion)); err != nil { fmt.Fprintln(os.Stderr, "Error: git tag failed") os.Exit(1) } fmt.Printf("Created tag: %s\n", newVersion) fmt.Println("Release completed successfully!") } ================================================ FILE: docs/project-layout.md ================================================ # Clockify CLI Project Layout The project is organized in the following folders and important files: - [`cmd/`](../cmd) - `main` packages to build executable binaries. - [`docs/`](.) - documentation of the project for maintainers and contributors. - [`scripts/`](../scripts) - build and release scripts. - [`api/`](../api) - golang implementation of the Clockify API. - [`pkg/`](../pkg) - other packages that support the `api` or commands. - [`internal/`](../internal) - Go packages that are highly specific to this project - [`go.mod`](../go.mod) - external Go dependencies for this project. - [`Makefile`](../Makefile) - most of setup and maintenance actions for this project. ## Command line organization All CLI commands will be under [`pkg/cmd/`](../pkg/cmd) and the file naming convention is this: ``` pkg/cmd///.go ``` Following the same structure as the command path, so `clockify-cli client add` is at `pkg/cmd/client/add.go`, all command packages will have a function named `NewCmd` that will receive a `cmdutil.Factory` type and return a `*cobra.Command`. Specific logic for that command must be kept at the same package as it, and all subcommands must be registered on its parent package. So all subcommands of `client` will registered on the function `client.NewCmdClient()`. Output formatters must stay under the package [`pkg/output/`](../pkg/output) using the following file convention: ``` pkg/output//.go ``` Shared functionality for printing entities must be at the package [`pkg/outpututil/`](../pkg/outpututil). ### Steps do create a new command Say you will create a new command `delete` under `client`. 1. Create the package `pkg/cmd/client/delete/` 2. Create a function called `NewCmdDelete` on a file `delete.go` 1. This function must receive a [`cmdutil.Factory`][] struct and return a [`*cobra.Command`][] fully set. 3. Edit the entity root command at `pkg/cmd/client/client.go` to register it as a subcommand using the factory function previously created. If the entity root does not exist yet, then: 1. Create the file, and in it a function `NewCmdClient` that should receive `cmdutil.Factory` and return a [`*cobra.Command`][] with all its subcommands. 4. If is the first command of a entity: 1. Create a package called `pkg/output/client` 2. Implement output the five basic output formats `table` (default), `json`, `quiet` (only the ID), `template` ([Go template](https://pkg.go.dev/text/template)) and `csv`. Each one on a file by itself. ## Credits This document is based on the [project-layout.md from github/cli/cli][credit]. [credit]: https://github.com/cli/cli/blob/trunk/docs/project-layout.md [`*cobra.Command`]: https://pkg.go.dev/github.com/spf13/cobra#Command [`cmdutil.Factory`]: ../pkg/cmdutil/factory.go ================================================ FILE: go.mod ================================================ module github.com/lucassabreu/clockify-cli go 1.24 require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/MakeNowJust/heredoc v1.0.0 github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 github.com/creack/pty v1.1.17 github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec github.com/mitchellh/go-homedir v1.1.0 github.com/olekukonko/tablewriter v0.0.5 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.9.0 golang.org/x/sync v0.7.0 golang.org/x/term v0.20.0 golang.org/x/text v0.15.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect golang.org/x/sys v0.20.0 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) ================================================ FILE: go.sum ================================================ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4= golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: internal/consoletest/test.go ================================================ package consoletest import ( "bytes" "io" "testing" "time" "github.com/Netflix/go-expect" "github.com/hinshun/vt10x" pseudotty "github.com/creack/pty" ) // ExpectConsole is a helper to interact if the pseudo terminal on tests type ExpectConsole interface { ExpectEOF() ExpectString(string) Send(string) SendLine(string) } type console struct { t *testing.T c *expect.Console } func (c *console) ExpectEOF() { if _, err := c.c.ExpectEOF(); err != nil { c.t.Errorf("failed to ExpectEOF %v", err) } } func (c *console) ExpectString(s string) { if _, err := c.c.ExpectString(s); err != nil { c.t.Errorf("failed to ExpectString (%s) %v", s, err) } } func (c *console) Send(s string) { if _, err := c.c.Send(s); err != nil { c.t.Errorf("failed to Send %v", err) } } func (c *console) SendLine(s string) { if _, err := c.c.SendLine(s); err != nil { c.t.Errorf("failed to SendLine %v", err) } } // FileWriter is a simplification of the io.Stdout struct type FileWriter interface { io.Writer Fd() uintptr } // FileReader is a simplification of the io.Stdin struct type FileReader interface { io.Reader Fd() uintptr } // RunTestConsole simulates a terminal for interactive tests // This is mostly a adaptation of the RunTest function at // [survey_test.go](https://github.com/AlecAivazis/survey/blob/e47352f914346a910cc7e1ca9f65a7ac0674449a/survey_posix_test.go#L15), // but with interfaces exported to easy re-use on other packages. func RunTestConsole( t *testing.T, setup func(out FileWriter, in FileReader) error, procedure func(c ExpectConsole), ) { t.Parallel() pty, tty, err := pseudotty.Open() if err != nil { t.Fatalf("failed to open pseudotty: %v", err) } b := bytes.NewBufferString("") term := vt10x.New(vt10x.WithWriter(tty)) c, err := expect.NewConsole( expect.WithStdin(pty), expect.WithStdout(term), expect.WithStdout(b), expect.WithCloser(pty, tty), ) if err != nil { t.Fatalf("failed to create console: %v", err) } defer func() { _ = c.Close() }() finished := false t.Cleanup(func() { if finished { return } t.Error( "console test failed\n" + "current output:\n" + b.String() + "\n") }) donec := make(chan struct{}) go func() { defer close(donec) procedure(&console{c: c, t: t}) }() go func() { defer func() { _ = c.Tty().Close() }() if err = setup(c.Tty(), c.Tty()); err != nil { t.Error(err) return } }() select { case <-time.After(time.Second * 10): t.Error("console test timeout exceeded") case <-donec: finished = true } } ================================================ FILE: internal/mocks/gen.go ================================================ package mocks import ( "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" ) type Factory interface { cmdutil.Factory } type Config interface { cmdutil.Config } type Client interface { api.Client } ================================================ FILE: internal/mocks/mock_Client.go ================================================ // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" mock "github.com/stretchr/testify/mock" ) // NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockClient(t interface { mock.TestingT Cleanup(func()) }) *MockClient { mock := &MockClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // MockClient is an autogenerated mock type for the Client type type MockClient struct { mock.Mock } type MockClient_Expecter struct { mock *mock.Mock } func (_m *MockClient) EXPECT() *MockClient_Expecter { return &MockClient_Expecter{mock: &_m.Mock} } // AddClient provides a mock function for the type MockClient func (_mock *MockClient) AddClient(addClientParam api.AddClientParam) (dto.Client, error) { ret := _mock.Called(addClientParam) if len(ret) == 0 { panic("no return value specified for AddClient") } var r0 dto.Client var r1 error if returnFunc, ok := ret.Get(0).(func(api.AddClientParam) (dto.Client, error)); ok { return returnFunc(addClientParam) } if returnFunc, ok := ret.Get(0).(func(api.AddClientParam) dto.Client); ok { r0 = returnFunc(addClientParam) } else { r0 = ret.Get(0).(dto.Client) } if returnFunc, ok := ret.Get(1).(func(api.AddClientParam) error); ok { r1 = returnFunc(addClientParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_AddClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddClient' type MockClient_AddClient_Call struct { *mock.Call } // AddClient is a helper method to define mock.On call // - addClientParam api.AddClientParam func (_e *MockClient_Expecter) AddClient(addClientParam interface{}) *MockClient_AddClient_Call { return &MockClient_AddClient_Call{Call: _e.mock.On("AddClient", addClientParam)} } func (_c *MockClient_AddClient_Call) Run(run func(addClientParam api.AddClientParam)) *MockClient_AddClient_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.AddClientParam if args[0] != nil { arg0 = args[0].(api.AddClientParam) } run( arg0, ) }) return _c } func (_c *MockClient_AddClient_Call) Return(client dto.Client, err error) *MockClient_AddClient_Call { _c.Call.Return(client, err) return _c } func (_c *MockClient_AddClient_Call) RunAndReturn(run func(addClientParam api.AddClientParam) (dto.Client, error)) *MockClient_AddClient_Call { _c.Call.Return(run) return _c } // AddProject provides a mock function for the type MockClient func (_mock *MockClient) AddProject(addProjectParam api.AddProjectParam) (dto.Project, error) { ret := _mock.Called(addProjectParam) if len(ret) == 0 { panic("no return value specified for AddProject") } var r0 dto.Project var r1 error if returnFunc, ok := ret.Get(0).(func(api.AddProjectParam) (dto.Project, error)); ok { return returnFunc(addProjectParam) } if returnFunc, ok := ret.Get(0).(func(api.AddProjectParam) dto.Project); ok { r0 = returnFunc(addProjectParam) } else { r0 = ret.Get(0).(dto.Project) } if returnFunc, ok := ret.Get(1).(func(api.AddProjectParam) error); ok { r1 = returnFunc(addProjectParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_AddProject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddProject' type MockClient_AddProject_Call struct { *mock.Call } // AddProject is a helper method to define mock.On call // - addProjectParam api.AddProjectParam func (_e *MockClient_Expecter) AddProject(addProjectParam interface{}) *MockClient_AddProject_Call { return &MockClient_AddProject_Call{Call: _e.mock.On("AddProject", addProjectParam)} } func (_c *MockClient_AddProject_Call) Run(run func(addProjectParam api.AddProjectParam)) *MockClient_AddProject_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.AddProjectParam if args[0] != nil { arg0 = args[0].(api.AddProjectParam) } run( arg0, ) }) return _c } func (_c *MockClient_AddProject_Call) Return(project dto.Project, err error) *MockClient_AddProject_Call { _c.Call.Return(project, err) return _c } func (_c *MockClient_AddProject_Call) RunAndReturn(run func(addProjectParam api.AddProjectParam) (dto.Project, error)) *MockClient_AddProject_Call { _c.Call.Return(run) return _c } // AddTask provides a mock function for the type MockClient func (_mock *MockClient) AddTask(addTaskParam api.AddTaskParam) (dto.Task, error) { ret := _mock.Called(addTaskParam) if len(ret) == 0 { panic("no return value specified for AddTask") } var r0 dto.Task var r1 error if returnFunc, ok := ret.Get(0).(func(api.AddTaskParam) (dto.Task, error)); ok { return returnFunc(addTaskParam) } if returnFunc, ok := ret.Get(0).(func(api.AddTaskParam) dto.Task); ok { r0 = returnFunc(addTaskParam) } else { r0 = ret.Get(0).(dto.Task) } if returnFunc, ok := ret.Get(1).(func(api.AddTaskParam) error); ok { r1 = returnFunc(addTaskParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_AddTask_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddTask' type MockClient_AddTask_Call struct { *mock.Call } // AddTask is a helper method to define mock.On call // - addTaskParam api.AddTaskParam func (_e *MockClient_Expecter) AddTask(addTaskParam interface{}) *MockClient_AddTask_Call { return &MockClient_AddTask_Call{Call: _e.mock.On("AddTask", addTaskParam)} } func (_c *MockClient_AddTask_Call) Run(run func(addTaskParam api.AddTaskParam)) *MockClient_AddTask_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.AddTaskParam if args[0] != nil { arg0 = args[0].(api.AddTaskParam) } run( arg0, ) }) return _c } func (_c *MockClient_AddTask_Call) Return(task dto.Task, err error) *MockClient_AddTask_Call { _c.Call.Return(task, err) return _c } func (_c *MockClient_AddTask_Call) RunAndReturn(run func(addTaskParam api.AddTaskParam) (dto.Task, error)) *MockClient_AddTask_Call { _c.Call.Return(run) return _c } // ChangeInvoiced provides a mock function for the type MockClient func (_mock *MockClient) ChangeInvoiced(changeInvoicedParam api.ChangeInvoicedParam) error { ret := _mock.Called(changeInvoicedParam) if len(ret) == 0 { panic("no return value specified for ChangeInvoiced") } var r0 error if returnFunc, ok := ret.Get(0).(func(api.ChangeInvoicedParam) error); ok { r0 = returnFunc(changeInvoicedParam) } else { r0 = ret.Error(0) } return r0 } // MockClient_ChangeInvoiced_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChangeInvoiced' type MockClient_ChangeInvoiced_Call struct { *mock.Call } // ChangeInvoiced is a helper method to define mock.On call // - changeInvoicedParam api.ChangeInvoicedParam func (_e *MockClient_Expecter) ChangeInvoiced(changeInvoicedParam interface{}) *MockClient_ChangeInvoiced_Call { return &MockClient_ChangeInvoiced_Call{Call: _e.mock.On("ChangeInvoiced", changeInvoicedParam)} } func (_c *MockClient_ChangeInvoiced_Call) Run(run func(changeInvoicedParam api.ChangeInvoicedParam)) *MockClient_ChangeInvoiced_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.ChangeInvoicedParam if args[0] != nil { arg0 = args[0].(api.ChangeInvoicedParam) } run( arg0, ) }) return _c } func (_c *MockClient_ChangeInvoiced_Call) Return(err error) *MockClient_ChangeInvoiced_Call { _c.Call.Return(err) return _c } func (_c *MockClient_ChangeInvoiced_Call) RunAndReturn(run func(changeInvoicedParam api.ChangeInvoicedParam) error) *MockClient_ChangeInvoiced_Call { _c.Call.Return(run) return _c } // CreateTimeEntry provides a mock function for the type MockClient func (_mock *MockClient) CreateTimeEntry(createTimeEntryParam api.CreateTimeEntryParam) (dto.TimeEntryImpl, error) { ret := _mock.Called(createTimeEntryParam) if len(ret) == 0 { panic("no return value specified for CreateTimeEntry") } var r0 dto.TimeEntryImpl var r1 error if returnFunc, ok := ret.Get(0).(func(api.CreateTimeEntryParam) (dto.TimeEntryImpl, error)); ok { return returnFunc(createTimeEntryParam) } if returnFunc, ok := ret.Get(0).(func(api.CreateTimeEntryParam) dto.TimeEntryImpl); ok { r0 = returnFunc(createTimeEntryParam) } else { r0 = ret.Get(0).(dto.TimeEntryImpl) } if returnFunc, ok := ret.Get(1).(func(api.CreateTimeEntryParam) error); ok { r1 = returnFunc(createTimeEntryParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_CreateTimeEntry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateTimeEntry' type MockClient_CreateTimeEntry_Call struct { *mock.Call } // CreateTimeEntry is a helper method to define mock.On call // - createTimeEntryParam api.CreateTimeEntryParam func (_e *MockClient_Expecter) CreateTimeEntry(createTimeEntryParam interface{}) *MockClient_CreateTimeEntry_Call { return &MockClient_CreateTimeEntry_Call{Call: _e.mock.On("CreateTimeEntry", createTimeEntryParam)} } func (_c *MockClient_CreateTimeEntry_Call) Run(run func(createTimeEntryParam api.CreateTimeEntryParam)) *MockClient_CreateTimeEntry_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.CreateTimeEntryParam if args[0] != nil { arg0 = args[0].(api.CreateTimeEntryParam) } run( arg0, ) }) return _c } func (_c *MockClient_CreateTimeEntry_Call) Return(timeEntryImpl dto.TimeEntryImpl, err error) *MockClient_CreateTimeEntry_Call { _c.Call.Return(timeEntryImpl, err) return _c } func (_c *MockClient_CreateTimeEntry_Call) RunAndReturn(run func(createTimeEntryParam api.CreateTimeEntryParam) (dto.TimeEntryImpl, error)) *MockClient_CreateTimeEntry_Call { _c.Call.Return(run) return _c } // DeleteProject provides a mock function for the type MockClient func (_mock *MockClient) DeleteProject(deleteProjectParam api.DeleteProjectParam) (dto.Project, error) { ret := _mock.Called(deleteProjectParam) if len(ret) == 0 { panic("no return value specified for DeleteProject") } var r0 dto.Project var r1 error if returnFunc, ok := ret.Get(0).(func(api.DeleteProjectParam) (dto.Project, error)); ok { return returnFunc(deleteProjectParam) } if returnFunc, ok := ret.Get(0).(func(api.DeleteProjectParam) dto.Project); ok { r0 = returnFunc(deleteProjectParam) } else { r0 = ret.Get(0).(dto.Project) } if returnFunc, ok := ret.Get(1).(func(api.DeleteProjectParam) error); ok { r1 = returnFunc(deleteProjectParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_DeleteProject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteProject' type MockClient_DeleteProject_Call struct { *mock.Call } // DeleteProject is a helper method to define mock.On call // - deleteProjectParam api.DeleteProjectParam func (_e *MockClient_Expecter) DeleteProject(deleteProjectParam interface{}) *MockClient_DeleteProject_Call { return &MockClient_DeleteProject_Call{Call: _e.mock.On("DeleteProject", deleteProjectParam)} } func (_c *MockClient_DeleteProject_Call) Run(run func(deleteProjectParam api.DeleteProjectParam)) *MockClient_DeleteProject_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.DeleteProjectParam if args[0] != nil { arg0 = args[0].(api.DeleteProjectParam) } run( arg0, ) }) return _c } func (_c *MockClient_DeleteProject_Call) Return(project dto.Project, err error) *MockClient_DeleteProject_Call { _c.Call.Return(project, err) return _c } func (_c *MockClient_DeleteProject_Call) RunAndReturn(run func(deleteProjectParam api.DeleteProjectParam) (dto.Project, error)) *MockClient_DeleteProject_Call { _c.Call.Return(run) return _c } // DeleteTask provides a mock function for the type MockClient func (_mock *MockClient) DeleteTask(deleteTaskParam api.DeleteTaskParam) (dto.Task, error) { ret := _mock.Called(deleteTaskParam) if len(ret) == 0 { panic("no return value specified for DeleteTask") } var r0 dto.Task var r1 error if returnFunc, ok := ret.Get(0).(func(api.DeleteTaskParam) (dto.Task, error)); ok { return returnFunc(deleteTaskParam) } if returnFunc, ok := ret.Get(0).(func(api.DeleteTaskParam) dto.Task); ok { r0 = returnFunc(deleteTaskParam) } else { r0 = ret.Get(0).(dto.Task) } if returnFunc, ok := ret.Get(1).(func(api.DeleteTaskParam) error); ok { r1 = returnFunc(deleteTaskParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_DeleteTask_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteTask' type MockClient_DeleteTask_Call struct { *mock.Call } // DeleteTask is a helper method to define mock.On call // - deleteTaskParam api.DeleteTaskParam func (_e *MockClient_Expecter) DeleteTask(deleteTaskParam interface{}) *MockClient_DeleteTask_Call { return &MockClient_DeleteTask_Call{Call: _e.mock.On("DeleteTask", deleteTaskParam)} } func (_c *MockClient_DeleteTask_Call) Run(run func(deleteTaskParam api.DeleteTaskParam)) *MockClient_DeleteTask_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.DeleteTaskParam if args[0] != nil { arg0 = args[0].(api.DeleteTaskParam) } run( arg0, ) }) return _c } func (_c *MockClient_DeleteTask_Call) Return(task dto.Task, err error) *MockClient_DeleteTask_Call { _c.Call.Return(task, err) return _c } func (_c *MockClient_DeleteTask_Call) RunAndReturn(run func(deleteTaskParam api.DeleteTaskParam) (dto.Task, error)) *MockClient_DeleteTask_Call { _c.Call.Return(run) return _c } // DeleteTimeEntry provides a mock function for the type MockClient func (_mock *MockClient) DeleteTimeEntry(deleteTimeEntryParam api.DeleteTimeEntryParam) error { ret := _mock.Called(deleteTimeEntryParam) if len(ret) == 0 { panic("no return value specified for DeleteTimeEntry") } var r0 error if returnFunc, ok := ret.Get(0).(func(api.DeleteTimeEntryParam) error); ok { r0 = returnFunc(deleteTimeEntryParam) } else { r0 = ret.Error(0) } return r0 } // MockClient_DeleteTimeEntry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteTimeEntry' type MockClient_DeleteTimeEntry_Call struct { *mock.Call } // DeleteTimeEntry is a helper method to define mock.On call // - deleteTimeEntryParam api.DeleteTimeEntryParam func (_e *MockClient_Expecter) DeleteTimeEntry(deleteTimeEntryParam interface{}) *MockClient_DeleteTimeEntry_Call { return &MockClient_DeleteTimeEntry_Call{Call: _e.mock.On("DeleteTimeEntry", deleteTimeEntryParam)} } func (_c *MockClient_DeleteTimeEntry_Call) Run(run func(deleteTimeEntryParam api.DeleteTimeEntryParam)) *MockClient_DeleteTimeEntry_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.DeleteTimeEntryParam if args[0] != nil { arg0 = args[0].(api.DeleteTimeEntryParam) } run( arg0, ) }) return _c } func (_c *MockClient_DeleteTimeEntry_Call) Return(err error) *MockClient_DeleteTimeEntry_Call { _c.Call.Return(err) return _c } func (_c *MockClient_DeleteTimeEntry_Call) RunAndReturn(run func(deleteTimeEntryParam api.DeleteTimeEntryParam) error) *MockClient_DeleteTimeEntry_Call { _c.Call.Return(run) return _c } // GetClients provides a mock function for the type MockClient func (_mock *MockClient) GetClients(getClientsParam api.GetClientsParam) ([]dto.Client, error) { ret := _mock.Called(getClientsParam) if len(ret) == 0 { panic("no return value specified for GetClients") } var r0 []dto.Client var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetClientsParam) ([]dto.Client, error)); ok { return returnFunc(getClientsParam) } if returnFunc, ok := ret.Get(0).(func(api.GetClientsParam) []dto.Client); ok { r0 = returnFunc(getClientsParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dto.Client) } } if returnFunc, ok := ret.Get(1).(func(api.GetClientsParam) error); ok { r1 = returnFunc(getClientsParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetClients_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetClients' type MockClient_GetClients_Call struct { *mock.Call } // GetClients is a helper method to define mock.On call // - getClientsParam api.GetClientsParam func (_e *MockClient_Expecter) GetClients(getClientsParam interface{}) *MockClient_GetClients_Call { return &MockClient_GetClients_Call{Call: _e.mock.On("GetClients", getClientsParam)} } func (_c *MockClient_GetClients_Call) Run(run func(getClientsParam api.GetClientsParam)) *MockClient_GetClients_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetClientsParam if args[0] != nil { arg0 = args[0].(api.GetClientsParam) } run( arg0, ) }) return _c } func (_c *MockClient_GetClients_Call) Return(clients []dto.Client, err error) *MockClient_GetClients_Call { _c.Call.Return(clients, err) return _c } func (_c *MockClient_GetClients_Call) RunAndReturn(run func(getClientsParam api.GetClientsParam) ([]dto.Client, error)) *MockClient_GetClients_Call { _c.Call.Return(run) return _c } // GetHydratedTimeEntry provides a mock function for the type MockClient func (_mock *MockClient) GetHydratedTimeEntry(getTimeEntryParam api.GetTimeEntryParam) (*dto.TimeEntry, error) { ret := _mock.Called(getTimeEntryParam) if len(ret) == 0 { panic("no return value specified for GetHydratedTimeEntry") } var r0 *dto.TimeEntry var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryParam) (*dto.TimeEntry, error)); ok { return returnFunc(getTimeEntryParam) } if returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryParam) *dto.TimeEntry); ok { r0 = returnFunc(getTimeEntryParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*dto.TimeEntry) } } if returnFunc, ok := ret.Get(1).(func(api.GetTimeEntryParam) error); ok { r1 = returnFunc(getTimeEntryParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetHydratedTimeEntry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetHydratedTimeEntry' type MockClient_GetHydratedTimeEntry_Call struct { *mock.Call } // GetHydratedTimeEntry is a helper method to define mock.On call // - getTimeEntryParam api.GetTimeEntryParam func (_e *MockClient_Expecter) GetHydratedTimeEntry(getTimeEntryParam interface{}) *MockClient_GetHydratedTimeEntry_Call { return &MockClient_GetHydratedTimeEntry_Call{Call: _e.mock.On("GetHydratedTimeEntry", getTimeEntryParam)} } func (_c *MockClient_GetHydratedTimeEntry_Call) Run(run func(getTimeEntryParam api.GetTimeEntryParam)) *MockClient_GetHydratedTimeEntry_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetTimeEntryParam if args[0] != nil { arg0 = args[0].(api.GetTimeEntryParam) } run( arg0, ) }) return _c } func (_c *MockClient_GetHydratedTimeEntry_Call) Return(timeEntry *dto.TimeEntry, err error) *MockClient_GetHydratedTimeEntry_Call { _c.Call.Return(timeEntry, err) return _c } func (_c *MockClient_GetHydratedTimeEntry_Call) RunAndReturn(run func(getTimeEntryParam api.GetTimeEntryParam) (*dto.TimeEntry, error)) *MockClient_GetHydratedTimeEntry_Call { _c.Call.Return(run) return _c } // GetHydratedTimeEntryInProgress provides a mock function for the type MockClient func (_mock *MockClient) GetHydratedTimeEntryInProgress(getTimeEntryInProgressParam api.GetTimeEntryInProgressParam) (*dto.TimeEntry, error) { ret := _mock.Called(getTimeEntryInProgressParam) if len(ret) == 0 { panic("no return value specified for GetHydratedTimeEntryInProgress") } var r0 *dto.TimeEntry var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryInProgressParam) (*dto.TimeEntry, error)); ok { return returnFunc(getTimeEntryInProgressParam) } if returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryInProgressParam) *dto.TimeEntry); ok { r0 = returnFunc(getTimeEntryInProgressParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*dto.TimeEntry) } } if returnFunc, ok := ret.Get(1).(func(api.GetTimeEntryInProgressParam) error); ok { r1 = returnFunc(getTimeEntryInProgressParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetHydratedTimeEntryInProgress_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetHydratedTimeEntryInProgress' type MockClient_GetHydratedTimeEntryInProgress_Call struct { *mock.Call } // GetHydratedTimeEntryInProgress is a helper method to define mock.On call // - getTimeEntryInProgressParam api.GetTimeEntryInProgressParam func (_e *MockClient_Expecter) GetHydratedTimeEntryInProgress(getTimeEntryInProgressParam interface{}) *MockClient_GetHydratedTimeEntryInProgress_Call { return &MockClient_GetHydratedTimeEntryInProgress_Call{Call: _e.mock.On("GetHydratedTimeEntryInProgress", getTimeEntryInProgressParam)} } func (_c *MockClient_GetHydratedTimeEntryInProgress_Call) Run(run func(getTimeEntryInProgressParam api.GetTimeEntryInProgressParam)) *MockClient_GetHydratedTimeEntryInProgress_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetTimeEntryInProgressParam if args[0] != nil { arg0 = args[0].(api.GetTimeEntryInProgressParam) } run( arg0, ) }) return _c } func (_c *MockClient_GetHydratedTimeEntryInProgress_Call) Return(timeEntry *dto.TimeEntry, err error) *MockClient_GetHydratedTimeEntryInProgress_Call { _c.Call.Return(timeEntry, err) return _c } func (_c *MockClient_GetHydratedTimeEntryInProgress_Call) RunAndReturn(run func(getTimeEntryInProgressParam api.GetTimeEntryInProgressParam) (*dto.TimeEntry, error)) *MockClient_GetHydratedTimeEntryInProgress_Call { _c.Call.Return(run) return _c } // GetMe provides a mock function for the type MockClient func (_mock *MockClient) GetMe() (dto.User, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for GetMe") } var r0 dto.User var r1 error if returnFunc, ok := ret.Get(0).(func() (dto.User, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() dto.User); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(dto.User) } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetMe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMe' type MockClient_GetMe_Call struct { *mock.Call } // GetMe is a helper method to define mock.On call func (_e *MockClient_Expecter) GetMe() *MockClient_GetMe_Call { return &MockClient_GetMe_Call{Call: _e.mock.On("GetMe")} } func (_c *MockClient_GetMe_Call) Run(run func()) *MockClient_GetMe_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockClient_GetMe_Call) Return(user dto.User, err error) *MockClient_GetMe_Call { _c.Call.Return(user, err) return _c } func (_c *MockClient_GetMe_Call) RunAndReturn(run func() (dto.User, error)) *MockClient_GetMe_Call { _c.Call.Return(run) return _c } // GetProject provides a mock function for the type MockClient func (_mock *MockClient) GetProject(getProjectParam api.GetProjectParam) (*dto.Project, error) { ret := _mock.Called(getProjectParam) if len(ret) == 0 { panic("no return value specified for GetProject") } var r0 *dto.Project var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetProjectParam) (*dto.Project, error)); ok { return returnFunc(getProjectParam) } if returnFunc, ok := ret.Get(0).(func(api.GetProjectParam) *dto.Project); ok { r0 = returnFunc(getProjectParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*dto.Project) } } if returnFunc, ok := ret.Get(1).(func(api.GetProjectParam) error); ok { r1 = returnFunc(getProjectParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetProject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProject' type MockClient_GetProject_Call struct { *mock.Call } // GetProject is a helper method to define mock.On call // - getProjectParam api.GetProjectParam func (_e *MockClient_Expecter) GetProject(getProjectParam interface{}) *MockClient_GetProject_Call { return &MockClient_GetProject_Call{Call: _e.mock.On("GetProject", getProjectParam)} } func (_c *MockClient_GetProject_Call) Run(run func(getProjectParam api.GetProjectParam)) *MockClient_GetProject_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetProjectParam if args[0] != nil { arg0 = args[0].(api.GetProjectParam) } run( arg0, ) }) return _c } func (_c *MockClient_GetProject_Call) Return(project *dto.Project, err error) *MockClient_GetProject_Call { _c.Call.Return(project, err) return _c } func (_c *MockClient_GetProject_Call) RunAndReturn(run func(getProjectParam api.GetProjectParam) (*dto.Project, error)) *MockClient_GetProject_Call { _c.Call.Return(run) return _c } // GetProjects provides a mock function for the type MockClient func (_mock *MockClient) GetProjects(getProjectsParam api.GetProjectsParam) ([]dto.Project, error) { ret := _mock.Called(getProjectsParam) if len(ret) == 0 { panic("no return value specified for GetProjects") } var r0 []dto.Project var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetProjectsParam) ([]dto.Project, error)); ok { return returnFunc(getProjectsParam) } if returnFunc, ok := ret.Get(0).(func(api.GetProjectsParam) []dto.Project); ok { r0 = returnFunc(getProjectsParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dto.Project) } } if returnFunc, ok := ret.Get(1).(func(api.GetProjectsParam) error); ok { r1 = returnFunc(getProjectsParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetProjects_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProjects' type MockClient_GetProjects_Call struct { *mock.Call } // GetProjects is a helper method to define mock.On call // - getProjectsParam api.GetProjectsParam func (_e *MockClient_Expecter) GetProjects(getProjectsParam interface{}) *MockClient_GetProjects_Call { return &MockClient_GetProjects_Call{Call: _e.mock.On("GetProjects", getProjectsParam)} } func (_c *MockClient_GetProjects_Call) Run(run func(getProjectsParam api.GetProjectsParam)) *MockClient_GetProjects_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetProjectsParam if args[0] != nil { arg0 = args[0].(api.GetProjectsParam) } run( arg0, ) }) return _c } func (_c *MockClient_GetProjects_Call) Return(projects []dto.Project, err error) *MockClient_GetProjects_Call { _c.Call.Return(projects, err) return _c } func (_c *MockClient_GetProjects_Call) RunAndReturn(run func(getProjectsParam api.GetProjectsParam) ([]dto.Project, error)) *MockClient_GetProjects_Call { _c.Call.Return(run) return _c } // GetTag provides a mock function for the type MockClient func (_mock *MockClient) GetTag(getTagParam api.GetTagParam) (*dto.Tag, error) { ret := _mock.Called(getTagParam) if len(ret) == 0 { panic("no return value specified for GetTag") } var r0 *dto.Tag var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetTagParam) (*dto.Tag, error)); ok { return returnFunc(getTagParam) } if returnFunc, ok := ret.Get(0).(func(api.GetTagParam) *dto.Tag); ok { r0 = returnFunc(getTagParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*dto.Tag) } } if returnFunc, ok := ret.Get(1).(func(api.GetTagParam) error); ok { r1 = returnFunc(getTagParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetTag_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTag' type MockClient_GetTag_Call struct { *mock.Call } // GetTag is a helper method to define mock.On call // - getTagParam api.GetTagParam func (_e *MockClient_Expecter) GetTag(getTagParam interface{}) *MockClient_GetTag_Call { return &MockClient_GetTag_Call{Call: _e.mock.On("GetTag", getTagParam)} } func (_c *MockClient_GetTag_Call) Run(run func(getTagParam api.GetTagParam)) *MockClient_GetTag_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetTagParam if args[0] != nil { arg0 = args[0].(api.GetTagParam) } run( arg0, ) }) return _c } func (_c *MockClient_GetTag_Call) Return(tag *dto.Tag, err error) *MockClient_GetTag_Call { _c.Call.Return(tag, err) return _c } func (_c *MockClient_GetTag_Call) RunAndReturn(run func(getTagParam api.GetTagParam) (*dto.Tag, error)) *MockClient_GetTag_Call { _c.Call.Return(run) return _c } // GetTags provides a mock function for the type MockClient func (_mock *MockClient) GetTags(getTagsParam api.GetTagsParam) ([]dto.Tag, error) { ret := _mock.Called(getTagsParam) if len(ret) == 0 { panic("no return value specified for GetTags") } var r0 []dto.Tag var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetTagsParam) ([]dto.Tag, error)); ok { return returnFunc(getTagsParam) } if returnFunc, ok := ret.Get(0).(func(api.GetTagsParam) []dto.Tag); ok { r0 = returnFunc(getTagsParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dto.Tag) } } if returnFunc, ok := ret.Get(1).(func(api.GetTagsParam) error); ok { r1 = returnFunc(getTagsParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTags' type MockClient_GetTags_Call struct { *mock.Call } // GetTags is a helper method to define mock.On call // - getTagsParam api.GetTagsParam func (_e *MockClient_Expecter) GetTags(getTagsParam interface{}) *MockClient_GetTags_Call { return &MockClient_GetTags_Call{Call: _e.mock.On("GetTags", getTagsParam)} } func (_c *MockClient_GetTags_Call) Run(run func(getTagsParam api.GetTagsParam)) *MockClient_GetTags_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetTagsParam if args[0] != nil { arg0 = args[0].(api.GetTagsParam) } run( arg0, ) }) return _c } func (_c *MockClient_GetTags_Call) Return(tags []dto.Tag, err error) *MockClient_GetTags_Call { _c.Call.Return(tags, err) return _c } func (_c *MockClient_GetTags_Call) RunAndReturn(run func(getTagsParam api.GetTagsParam) ([]dto.Tag, error)) *MockClient_GetTags_Call { _c.Call.Return(run) return _c } // GetTask provides a mock function for the type MockClient func (_mock *MockClient) GetTask(getTaskParam api.GetTaskParam) (dto.Task, error) { ret := _mock.Called(getTaskParam) if len(ret) == 0 { panic("no return value specified for GetTask") } var r0 dto.Task var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetTaskParam) (dto.Task, error)); ok { return returnFunc(getTaskParam) } if returnFunc, ok := ret.Get(0).(func(api.GetTaskParam) dto.Task); ok { r0 = returnFunc(getTaskParam) } else { r0 = ret.Get(0).(dto.Task) } if returnFunc, ok := ret.Get(1).(func(api.GetTaskParam) error); ok { r1 = returnFunc(getTaskParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetTask_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTask' type MockClient_GetTask_Call struct { *mock.Call } // GetTask is a helper method to define mock.On call // - getTaskParam api.GetTaskParam func (_e *MockClient_Expecter) GetTask(getTaskParam interface{}) *MockClient_GetTask_Call { return &MockClient_GetTask_Call{Call: _e.mock.On("GetTask", getTaskParam)} } func (_c *MockClient_GetTask_Call) Run(run func(getTaskParam api.GetTaskParam)) *MockClient_GetTask_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetTaskParam if args[0] != nil { arg0 = args[0].(api.GetTaskParam) } run( arg0, ) }) return _c } func (_c *MockClient_GetTask_Call) Return(task dto.Task, err error) *MockClient_GetTask_Call { _c.Call.Return(task, err) return _c } func (_c *MockClient_GetTask_Call) RunAndReturn(run func(getTaskParam api.GetTaskParam) (dto.Task, error)) *MockClient_GetTask_Call { _c.Call.Return(run) return _c } // GetTasks provides a mock function for the type MockClient func (_mock *MockClient) GetTasks(getTasksParam api.GetTasksParam) ([]dto.Task, error) { ret := _mock.Called(getTasksParam) if len(ret) == 0 { panic("no return value specified for GetTasks") } var r0 []dto.Task var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetTasksParam) ([]dto.Task, error)); ok { return returnFunc(getTasksParam) } if returnFunc, ok := ret.Get(0).(func(api.GetTasksParam) []dto.Task); ok { r0 = returnFunc(getTasksParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dto.Task) } } if returnFunc, ok := ret.Get(1).(func(api.GetTasksParam) error); ok { r1 = returnFunc(getTasksParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetTasks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTasks' type MockClient_GetTasks_Call struct { *mock.Call } // GetTasks is a helper method to define mock.On call // - getTasksParam api.GetTasksParam func (_e *MockClient_Expecter) GetTasks(getTasksParam interface{}) *MockClient_GetTasks_Call { return &MockClient_GetTasks_Call{Call: _e.mock.On("GetTasks", getTasksParam)} } func (_c *MockClient_GetTasks_Call) Run(run func(getTasksParam api.GetTasksParam)) *MockClient_GetTasks_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetTasksParam if args[0] != nil { arg0 = args[0].(api.GetTasksParam) } run( arg0, ) }) return _c } func (_c *MockClient_GetTasks_Call) Return(tasks []dto.Task, err error) *MockClient_GetTasks_Call { _c.Call.Return(tasks, err) return _c } func (_c *MockClient_GetTasks_Call) RunAndReturn(run func(getTasksParam api.GetTasksParam) ([]dto.Task, error)) *MockClient_GetTasks_Call { _c.Call.Return(run) return _c } // GetTimeEntry provides a mock function for the type MockClient func (_mock *MockClient) GetTimeEntry(getTimeEntryParam api.GetTimeEntryParam) (*dto.TimeEntryImpl, error) { ret := _mock.Called(getTimeEntryParam) if len(ret) == 0 { panic("no return value specified for GetTimeEntry") } var r0 *dto.TimeEntryImpl var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryParam) (*dto.TimeEntryImpl, error)); ok { return returnFunc(getTimeEntryParam) } if returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryParam) *dto.TimeEntryImpl); ok { r0 = returnFunc(getTimeEntryParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*dto.TimeEntryImpl) } } if returnFunc, ok := ret.Get(1).(func(api.GetTimeEntryParam) error); ok { r1 = returnFunc(getTimeEntryParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetTimeEntry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTimeEntry' type MockClient_GetTimeEntry_Call struct { *mock.Call } // GetTimeEntry is a helper method to define mock.On call // - getTimeEntryParam api.GetTimeEntryParam func (_e *MockClient_Expecter) GetTimeEntry(getTimeEntryParam interface{}) *MockClient_GetTimeEntry_Call { return &MockClient_GetTimeEntry_Call{Call: _e.mock.On("GetTimeEntry", getTimeEntryParam)} } func (_c *MockClient_GetTimeEntry_Call) Run(run func(getTimeEntryParam api.GetTimeEntryParam)) *MockClient_GetTimeEntry_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetTimeEntryParam if args[0] != nil { arg0 = args[0].(api.GetTimeEntryParam) } run( arg0, ) }) return _c } func (_c *MockClient_GetTimeEntry_Call) Return(timeEntryImpl *dto.TimeEntryImpl, err error) *MockClient_GetTimeEntry_Call { _c.Call.Return(timeEntryImpl, err) return _c } func (_c *MockClient_GetTimeEntry_Call) RunAndReturn(run func(getTimeEntryParam api.GetTimeEntryParam) (*dto.TimeEntryImpl, error)) *MockClient_GetTimeEntry_Call { _c.Call.Return(run) return _c } // GetTimeEntryInProgress provides a mock function for the type MockClient func (_mock *MockClient) GetTimeEntryInProgress(getTimeEntryInProgressParam api.GetTimeEntryInProgressParam) (*dto.TimeEntryImpl, error) { ret := _mock.Called(getTimeEntryInProgressParam) if len(ret) == 0 { panic("no return value specified for GetTimeEntryInProgress") } var r0 *dto.TimeEntryImpl var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryInProgressParam) (*dto.TimeEntryImpl, error)); ok { return returnFunc(getTimeEntryInProgressParam) } if returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryInProgressParam) *dto.TimeEntryImpl); ok { r0 = returnFunc(getTimeEntryInProgressParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*dto.TimeEntryImpl) } } if returnFunc, ok := ret.Get(1).(func(api.GetTimeEntryInProgressParam) error); ok { r1 = returnFunc(getTimeEntryInProgressParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetTimeEntryInProgress_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTimeEntryInProgress' type MockClient_GetTimeEntryInProgress_Call struct { *mock.Call } // GetTimeEntryInProgress is a helper method to define mock.On call // - getTimeEntryInProgressParam api.GetTimeEntryInProgressParam func (_e *MockClient_Expecter) GetTimeEntryInProgress(getTimeEntryInProgressParam interface{}) *MockClient_GetTimeEntryInProgress_Call { return &MockClient_GetTimeEntryInProgress_Call{Call: _e.mock.On("GetTimeEntryInProgress", getTimeEntryInProgressParam)} } func (_c *MockClient_GetTimeEntryInProgress_Call) Run(run func(getTimeEntryInProgressParam api.GetTimeEntryInProgressParam)) *MockClient_GetTimeEntryInProgress_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetTimeEntryInProgressParam if args[0] != nil { arg0 = args[0].(api.GetTimeEntryInProgressParam) } run( arg0, ) }) return _c } func (_c *MockClient_GetTimeEntryInProgress_Call) Return(timeEntryImpl *dto.TimeEntryImpl, err error) *MockClient_GetTimeEntryInProgress_Call { _c.Call.Return(timeEntryImpl, err) return _c } func (_c *MockClient_GetTimeEntryInProgress_Call) RunAndReturn(run func(getTimeEntryInProgressParam api.GetTimeEntryInProgressParam) (*dto.TimeEntryImpl, error)) *MockClient_GetTimeEntryInProgress_Call { _c.Call.Return(run) return _c } // GetUser provides a mock function for the type MockClient func (_mock *MockClient) GetUser(getUser api.GetUser) (dto.User, error) { ret := _mock.Called(getUser) if len(ret) == 0 { panic("no return value specified for GetUser") } var r0 dto.User var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetUser) (dto.User, error)); ok { return returnFunc(getUser) } if returnFunc, ok := ret.Get(0).(func(api.GetUser) dto.User); ok { r0 = returnFunc(getUser) } else { r0 = ret.Get(0).(dto.User) } if returnFunc, ok := ret.Get(1).(func(api.GetUser) error); ok { r1 = returnFunc(getUser) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUser' type MockClient_GetUser_Call struct { *mock.Call } // GetUser is a helper method to define mock.On call // - getUser api.GetUser func (_e *MockClient_Expecter) GetUser(getUser interface{}) *MockClient_GetUser_Call { return &MockClient_GetUser_Call{Call: _e.mock.On("GetUser", getUser)} } func (_c *MockClient_GetUser_Call) Run(run func(getUser api.GetUser)) *MockClient_GetUser_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetUser if args[0] != nil { arg0 = args[0].(api.GetUser) } run( arg0, ) }) return _c } func (_c *MockClient_GetUser_Call) Return(user dto.User, err error) *MockClient_GetUser_Call { _c.Call.Return(user, err) return _c } func (_c *MockClient_GetUser_Call) RunAndReturn(run func(getUser api.GetUser) (dto.User, error)) *MockClient_GetUser_Call { _c.Call.Return(run) return _c } // GetUserTimeEntries provides a mock function for the type MockClient func (_mock *MockClient) GetUserTimeEntries(getUserTimeEntriesParam api.GetUserTimeEntriesParam) ([]dto.TimeEntryImpl, error) { ret := _mock.Called(getUserTimeEntriesParam) if len(ret) == 0 { panic("no return value specified for GetUserTimeEntries") } var r0 []dto.TimeEntryImpl var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetUserTimeEntriesParam) ([]dto.TimeEntryImpl, error)); ok { return returnFunc(getUserTimeEntriesParam) } if returnFunc, ok := ret.Get(0).(func(api.GetUserTimeEntriesParam) []dto.TimeEntryImpl); ok { r0 = returnFunc(getUserTimeEntriesParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dto.TimeEntryImpl) } } if returnFunc, ok := ret.Get(1).(func(api.GetUserTimeEntriesParam) error); ok { r1 = returnFunc(getUserTimeEntriesParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetUserTimeEntries_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserTimeEntries' type MockClient_GetUserTimeEntries_Call struct { *mock.Call } // GetUserTimeEntries is a helper method to define mock.On call // - getUserTimeEntriesParam api.GetUserTimeEntriesParam func (_e *MockClient_Expecter) GetUserTimeEntries(getUserTimeEntriesParam interface{}) *MockClient_GetUserTimeEntries_Call { return &MockClient_GetUserTimeEntries_Call{Call: _e.mock.On("GetUserTimeEntries", getUserTimeEntriesParam)} } func (_c *MockClient_GetUserTimeEntries_Call) Run(run func(getUserTimeEntriesParam api.GetUserTimeEntriesParam)) *MockClient_GetUserTimeEntries_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetUserTimeEntriesParam if args[0] != nil { arg0 = args[0].(api.GetUserTimeEntriesParam) } run( arg0, ) }) return _c } func (_c *MockClient_GetUserTimeEntries_Call) Return(timeEntryImpls []dto.TimeEntryImpl, err error) *MockClient_GetUserTimeEntries_Call { _c.Call.Return(timeEntryImpls, err) return _c } func (_c *MockClient_GetUserTimeEntries_Call) RunAndReturn(run func(getUserTimeEntriesParam api.GetUserTimeEntriesParam) ([]dto.TimeEntryImpl, error)) *MockClient_GetUserTimeEntries_Call { _c.Call.Return(run) return _c } // GetUsersHydratedTimeEntries provides a mock function for the type MockClient func (_mock *MockClient) GetUsersHydratedTimeEntries(getUserTimeEntriesParam api.GetUserTimeEntriesParam) ([]dto.TimeEntry, error) { ret := _mock.Called(getUserTimeEntriesParam) if len(ret) == 0 { panic("no return value specified for GetUsersHydratedTimeEntries") } var r0 []dto.TimeEntry var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetUserTimeEntriesParam) ([]dto.TimeEntry, error)); ok { return returnFunc(getUserTimeEntriesParam) } if returnFunc, ok := ret.Get(0).(func(api.GetUserTimeEntriesParam) []dto.TimeEntry); ok { r0 = returnFunc(getUserTimeEntriesParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dto.TimeEntry) } } if returnFunc, ok := ret.Get(1).(func(api.GetUserTimeEntriesParam) error); ok { r1 = returnFunc(getUserTimeEntriesParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetUsersHydratedTimeEntries_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUsersHydratedTimeEntries' type MockClient_GetUsersHydratedTimeEntries_Call struct { *mock.Call } // GetUsersHydratedTimeEntries is a helper method to define mock.On call // - getUserTimeEntriesParam api.GetUserTimeEntriesParam func (_e *MockClient_Expecter) GetUsersHydratedTimeEntries(getUserTimeEntriesParam interface{}) *MockClient_GetUsersHydratedTimeEntries_Call { return &MockClient_GetUsersHydratedTimeEntries_Call{Call: _e.mock.On("GetUsersHydratedTimeEntries", getUserTimeEntriesParam)} } func (_c *MockClient_GetUsersHydratedTimeEntries_Call) Run(run func(getUserTimeEntriesParam api.GetUserTimeEntriesParam)) *MockClient_GetUsersHydratedTimeEntries_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetUserTimeEntriesParam if args[0] != nil { arg0 = args[0].(api.GetUserTimeEntriesParam) } run( arg0, ) }) return _c } func (_c *MockClient_GetUsersHydratedTimeEntries_Call) Return(timeEntrys []dto.TimeEntry, err error) *MockClient_GetUsersHydratedTimeEntries_Call { _c.Call.Return(timeEntrys, err) return _c } func (_c *MockClient_GetUsersHydratedTimeEntries_Call) RunAndReturn(run func(getUserTimeEntriesParam api.GetUserTimeEntriesParam) ([]dto.TimeEntry, error)) *MockClient_GetUsersHydratedTimeEntries_Call { _c.Call.Return(run) return _c } // GetWorkspace provides a mock function for the type MockClient func (_mock *MockClient) GetWorkspace(getWorkspace api.GetWorkspace) (dto.Workspace, error) { ret := _mock.Called(getWorkspace) if len(ret) == 0 { panic("no return value specified for GetWorkspace") } var r0 dto.Workspace var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetWorkspace) (dto.Workspace, error)); ok { return returnFunc(getWorkspace) } if returnFunc, ok := ret.Get(0).(func(api.GetWorkspace) dto.Workspace); ok { r0 = returnFunc(getWorkspace) } else { r0 = ret.Get(0).(dto.Workspace) } if returnFunc, ok := ret.Get(1).(func(api.GetWorkspace) error); ok { r1 = returnFunc(getWorkspace) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetWorkspace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkspace' type MockClient_GetWorkspace_Call struct { *mock.Call } // GetWorkspace is a helper method to define mock.On call // - getWorkspace api.GetWorkspace func (_e *MockClient_Expecter) GetWorkspace(getWorkspace interface{}) *MockClient_GetWorkspace_Call { return &MockClient_GetWorkspace_Call{Call: _e.mock.On("GetWorkspace", getWorkspace)} } func (_c *MockClient_GetWorkspace_Call) Run(run func(getWorkspace api.GetWorkspace)) *MockClient_GetWorkspace_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetWorkspace if args[0] != nil { arg0 = args[0].(api.GetWorkspace) } run( arg0, ) }) return _c } func (_c *MockClient_GetWorkspace_Call) Return(workspace dto.Workspace, err error) *MockClient_GetWorkspace_Call { _c.Call.Return(workspace, err) return _c } func (_c *MockClient_GetWorkspace_Call) RunAndReturn(run func(getWorkspace api.GetWorkspace) (dto.Workspace, error)) *MockClient_GetWorkspace_Call { _c.Call.Return(run) return _c } // GetWorkspaces provides a mock function for the type MockClient func (_mock *MockClient) GetWorkspaces(getWorkspaces api.GetWorkspaces) ([]dto.Workspace, error) { ret := _mock.Called(getWorkspaces) if len(ret) == 0 { panic("no return value specified for GetWorkspaces") } var r0 []dto.Workspace var r1 error if returnFunc, ok := ret.Get(0).(func(api.GetWorkspaces) ([]dto.Workspace, error)); ok { return returnFunc(getWorkspaces) } if returnFunc, ok := ret.Get(0).(func(api.GetWorkspaces) []dto.Workspace); ok { r0 = returnFunc(getWorkspaces) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dto.Workspace) } } if returnFunc, ok := ret.Get(1).(func(api.GetWorkspaces) error); ok { r1 = returnFunc(getWorkspaces) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_GetWorkspaces_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkspaces' type MockClient_GetWorkspaces_Call struct { *mock.Call } // GetWorkspaces is a helper method to define mock.On call // - getWorkspaces api.GetWorkspaces func (_e *MockClient_Expecter) GetWorkspaces(getWorkspaces interface{}) *MockClient_GetWorkspaces_Call { return &MockClient_GetWorkspaces_Call{Call: _e.mock.On("GetWorkspaces", getWorkspaces)} } func (_c *MockClient_GetWorkspaces_Call) Run(run func(getWorkspaces api.GetWorkspaces)) *MockClient_GetWorkspaces_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.GetWorkspaces if args[0] != nil { arg0 = args[0].(api.GetWorkspaces) } run( arg0, ) }) return _c } func (_c *MockClient_GetWorkspaces_Call) Return(workspaces []dto.Workspace, err error) *MockClient_GetWorkspaces_Call { _c.Call.Return(workspaces, err) return _c } func (_c *MockClient_GetWorkspaces_Call) RunAndReturn(run func(getWorkspaces api.GetWorkspaces) ([]dto.Workspace, error)) *MockClient_GetWorkspaces_Call { _c.Call.Return(run) return _c } // Log provides a mock function for the type MockClient func (_mock *MockClient) Log(logParam api.LogParam) ([]dto.TimeEntry, error) { ret := _mock.Called(logParam) if len(ret) == 0 { panic("no return value specified for Log") } var r0 []dto.TimeEntry var r1 error if returnFunc, ok := ret.Get(0).(func(api.LogParam) ([]dto.TimeEntry, error)); ok { return returnFunc(logParam) } if returnFunc, ok := ret.Get(0).(func(api.LogParam) []dto.TimeEntry); ok { r0 = returnFunc(logParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dto.TimeEntry) } } if returnFunc, ok := ret.Get(1).(func(api.LogParam) error); ok { r1 = returnFunc(logParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_Log_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Log' type MockClient_Log_Call struct { *mock.Call } // Log is a helper method to define mock.On call // - logParam api.LogParam func (_e *MockClient_Expecter) Log(logParam interface{}) *MockClient_Log_Call { return &MockClient_Log_Call{Call: _e.mock.On("Log", logParam)} } func (_c *MockClient_Log_Call) Run(run func(logParam api.LogParam)) *MockClient_Log_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.LogParam if args[0] != nil { arg0 = args[0].(api.LogParam) } run( arg0, ) }) return _c } func (_c *MockClient_Log_Call) Return(timeEntrys []dto.TimeEntry, err error) *MockClient_Log_Call { _c.Call.Return(timeEntrys, err) return _c } func (_c *MockClient_Log_Call) RunAndReturn(run func(logParam api.LogParam) ([]dto.TimeEntry, error)) *MockClient_Log_Call { _c.Call.Return(run) return _c } // LogRange provides a mock function for the type MockClient func (_mock *MockClient) LogRange(logRangeParam api.LogRangeParam) ([]dto.TimeEntry, error) { ret := _mock.Called(logRangeParam) if len(ret) == 0 { panic("no return value specified for LogRange") } var r0 []dto.TimeEntry var r1 error if returnFunc, ok := ret.Get(0).(func(api.LogRangeParam) ([]dto.TimeEntry, error)); ok { return returnFunc(logRangeParam) } if returnFunc, ok := ret.Get(0).(func(api.LogRangeParam) []dto.TimeEntry); ok { r0 = returnFunc(logRangeParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dto.TimeEntry) } } if returnFunc, ok := ret.Get(1).(func(api.LogRangeParam) error); ok { r1 = returnFunc(logRangeParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_LogRange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogRange' type MockClient_LogRange_Call struct { *mock.Call } // LogRange is a helper method to define mock.On call // - logRangeParam api.LogRangeParam func (_e *MockClient_Expecter) LogRange(logRangeParam interface{}) *MockClient_LogRange_Call { return &MockClient_LogRange_Call{Call: _e.mock.On("LogRange", logRangeParam)} } func (_c *MockClient_LogRange_Call) Run(run func(logRangeParam api.LogRangeParam)) *MockClient_LogRange_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.LogRangeParam if args[0] != nil { arg0 = args[0].(api.LogRangeParam) } run( arg0, ) }) return _c } func (_c *MockClient_LogRange_Call) Return(timeEntrys []dto.TimeEntry, err error) *MockClient_LogRange_Call { _c.Call.Return(timeEntrys, err) return _c } func (_c *MockClient_LogRange_Call) RunAndReturn(run func(logRangeParam api.LogRangeParam) ([]dto.TimeEntry, error)) *MockClient_LogRange_Call { _c.Call.Return(run) return _c } // Out provides a mock function for the type MockClient func (_mock *MockClient) Out(outParam api.OutParam) error { ret := _mock.Called(outParam) if len(ret) == 0 { panic("no return value specified for Out") } var r0 error if returnFunc, ok := ret.Get(0).(func(api.OutParam) error); ok { r0 = returnFunc(outParam) } else { r0 = ret.Error(0) } return r0 } // MockClient_Out_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Out' type MockClient_Out_Call struct { *mock.Call } // Out is a helper method to define mock.On call // - outParam api.OutParam func (_e *MockClient_Expecter) Out(outParam interface{}) *MockClient_Out_Call { return &MockClient_Out_Call{Call: _e.mock.On("Out", outParam)} } func (_c *MockClient_Out_Call) Run(run func(outParam api.OutParam)) *MockClient_Out_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.OutParam if args[0] != nil { arg0 = args[0].(api.OutParam) } run( arg0, ) }) return _c } func (_c *MockClient_Out_Call) Return(err error) *MockClient_Out_Call { _c.Call.Return(err) return _c } func (_c *MockClient_Out_Call) RunAndReturn(run func(outParam api.OutParam) error) *MockClient_Out_Call { _c.Call.Return(run) return _c } // SetDebugLogger provides a mock function for the type MockClient func (_mock *MockClient) SetDebugLogger(logger api.Logger) api.Client { ret := _mock.Called(logger) if len(ret) == 0 { panic("no return value specified for SetDebugLogger") } var r0 api.Client if returnFunc, ok := ret.Get(0).(func(api.Logger) api.Client); ok { r0 = returnFunc(logger) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(api.Client) } } return r0 } // MockClient_SetDebugLogger_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDebugLogger' type MockClient_SetDebugLogger_Call struct { *mock.Call } // SetDebugLogger is a helper method to define mock.On call // - logger api.Logger func (_e *MockClient_Expecter) SetDebugLogger(logger interface{}) *MockClient_SetDebugLogger_Call { return &MockClient_SetDebugLogger_Call{Call: _e.mock.On("SetDebugLogger", logger)} } func (_c *MockClient_SetDebugLogger_Call) Run(run func(logger api.Logger)) *MockClient_SetDebugLogger_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.Logger if args[0] != nil { arg0 = args[0].(api.Logger) } run( arg0, ) }) return _c } func (_c *MockClient_SetDebugLogger_Call) Return(client api.Client) *MockClient_SetDebugLogger_Call { _c.Call.Return(client) return _c } func (_c *MockClient_SetDebugLogger_Call) RunAndReturn(run func(logger api.Logger) api.Client) *MockClient_SetDebugLogger_Call { _c.Call.Return(run) return _c } // SetInfoLogger provides a mock function for the type MockClient func (_mock *MockClient) SetInfoLogger(logger api.Logger) api.Client { ret := _mock.Called(logger) if len(ret) == 0 { panic("no return value specified for SetInfoLogger") } var r0 api.Client if returnFunc, ok := ret.Get(0).(func(api.Logger) api.Client); ok { r0 = returnFunc(logger) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(api.Client) } } return r0 } // MockClient_SetInfoLogger_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetInfoLogger' type MockClient_SetInfoLogger_Call struct { *mock.Call } // SetInfoLogger is a helper method to define mock.On call // - logger api.Logger func (_e *MockClient_Expecter) SetInfoLogger(logger interface{}) *MockClient_SetInfoLogger_Call { return &MockClient_SetInfoLogger_Call{Call: _e.mock.On("SetInfoLogger", logger)} } func (_c *MockClient_SetInfoLogger_Call) Run(run func(logger api.Logger)) *MockClient_SetInfoLogger_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.Logger if args[0] != nil { arg0 = args[0].(api.Logger) } run( arg0, ) }) return _c } func (_c *MockClient_SetInfoLogger_Call) Return(client api.Client) *MockClient_SetInfoLogger_Call { _c.Call.Return(client) return _c } func (_c *MockClient_SetInfoLogger_Call) RunAndReturn(run func(logger api.Logger) api.Client) *MockClient_SetInfoLogger_Call { _c.Call.Return(run) return _c } // UpdateProject provides a mock function for the type MockClient func (_mock *MockClient) UpdateProject(updateProjectParam api.UpdateProjectParam) (dto.Project, error) { ret := _mock.Called(updateProjectParam) if len(ret) == 0 { panic("no return value specified for UpdateProject") } var r0 dto.Project var r1 error if returnFunc, ok := ret.Get(0).(func(api.UpdateProjectParam) (dto.Project, error)); ok { return returnFunc(updateProjectParam) } if returnFunc, ok := ret.Get(0).(func(api.UpdateProjectParam) dto.Project); ok { r0 = returnFunc(updateProjectParam) } else { r0 = ret.Get(0).(dto.Project) } if returnFunc, ok := ret.Get(1).(func(api.UpdateProjectParam) error); ok { r1 = returnFunc(updateProjectParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_UpdateProject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateProject' type MockClient_UpdateProject_Call struct { *mock.Call } // UpdateProject is a helper method to define mock.On call // - updateProjectParam api.UpdateProjectParam func (_e *MockClient_Expecter) UpdateProject(updateProjectParam interface{}) *MockClient_UpdateProject_Call { return &MockClient_UpdateProject_Call{Call: _e.mock.On("UpdateProject", updateProjectParam)} } func (_c *MockClient_UpdateProject_Call) Run(run func(updateProjectParam api.UpdateProjectParam)) *MockClient_UpdateProject_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.UpdateProjectParam if args[0] != nil { arg0 = args[0].(api.UpdateProjectParam) } run( arg0, ) }) return _c } func (_c *MockClient_UpdateProject_Call) Return(project dto.Project, err error) *MockClient_UpdateProject_Call { _c.Call.Return(project, err) return _c } func (_c *MockClient_UpdateProject_Call) RunAndReturn(run func(updateProjectParam api.UpdateProjectParam) (dto.Project, error)) *MockClient_UpdateProject_Call { _c.Call.Return(run) return _c } // UpdateProjectEstimate provides a mock function for the type MockClient func (_mock *MockClient) UpdateProjectEstimate(updateProjectEstimateParam api.UpdateProjectEstimateParam) (dto.Project, error) { ret := _mock.Called(updateProjectEstimateParam) if len(ret) == 0 { panic("no return value specified for UpdateProjectEstimate") } var r0 dto.Project var r1 error if returnFunc, ok := ret.Get(0).(func(api.UpdateProjectEstimateParam) (dto.Project, error)); ok { return returnFunc(updateProjectEstimateParam) } if returnFunc, ok := ret.Get(0).(func(api.UpdateProjectEstimateParam) dto.Project); ok { r0 = returnFunc(updateProjectEstimateParam) } else { r0 = ret.Get(0).(dto.Project) } if returnFunc, ok := ret.Get(1).(func(api.UpdateProjectEstimateParam) error); ok { r1 = returnFunc(updateProjectEstimateParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_UpdateProjectEstimate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateProjectEstimate' type MockClient_UpdateProjectEstimate_Call struct { *mock.Call } // UpdateProjectEstimate is a helper method to define mock.On call // - updateProjectEstimateParam api.UpdateProjectEstimateParam func (_e *MockClient_Expecter) UpdateProjectEstimate(updateProjectEstimateParam interface{}) *MockClient_UpdateProjectEstimate_Call { return &MockClient_UpdateProjectEstimate_Call{Call: _e.mock.On("UpdateProjectEstimate", updateProjectEstimateParam)} } func (_c *MockClient_UpdateProjectEstimate_Call) Run(run func(updateProjectEstimateParam api.UpdateProjectEstimateParam)) *MockClient_UpdateProjectEstimate_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.UpdateProjectEstimateParam if args[0] != nil { arg0 = args[0].(api.UpdateProjectEstimateParam) } run( arg0, ) }) return _c } func (_c *MockClient_UpdateProjectEstimate_Call) Return(project dto.Project, err error) *MockClient_UpdateProjectEstimate_Call { _c.Call.Return(project, err) return _c } func (_c *MockClient_UpdateProjectEstimate_Call) RunAndReturn(run func(updateProjectEstimateParam api.UpdateProjectEstimateParam) (dto.Project, error)) *MockClient_UpdateProjectEstimate_Call { _c.Call.Return(run) return _c } // UpdateProjectMemberships provides a mock function for the type MockClient func (_mock *MockClient) UpdateProjectMemberships(updateProjectMembershipsParam api.UpdateProjectMembershipsParam) (dto.Project, error) { ret := _mock.Called(updateProjectMembershipsParam) if len(ret) == 0 { panic("no return value specified for UpdateProjectMemberships") } var r0 dto.Project var r1 error if returnFunc, ok := ret.Get(0).(func(api.UpdateProjectMembershipsParam) (dto.Project, error)); ok { return returnFunc(updateProjectMembershipsParam) } if returnFunc, ok := ret.Get(0).(func(api.UpdateProjectMembershipsParam) dto.Project); ok { r0 = returnFunc(updateProjectMembershipsParam) } else { r0 = ret.Get(0).(dto.Project) } if returnFunc, ok := ret.Get(1).(func(api.UpdateProjectMembershipsParam) error); ok { r1 = returnFunc(updateProjectMembershipsParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_UpdateProjectMemberships_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateProjectMemberships' type MockClient_UpdateProjectMemberships_Call struct { *mock.Call } // UpdateProjectMemberships is a helper method to define mock.On call // - updateProjectMembershipsParam api.UpdateProjectMembershipsParam func (_e *MockClient_Expecter) UpdateProjectMemberships(updateProjectMembershipsParam interface{}) *MockClient_UpdateProjectMemberships_Call { return &MockClient_UpdateProjectMemberships_Call{Call: _e.mock.On("UpdateProjectMemberships", updateProjectMembershipsParam)} } func (_c *MockClient_UpdateProjectMemberships_Call) Run(run func(updateProjectMembershipsParam api.UpdateProjectMembershipsParam)) *MockClient_UpdateProjectMemberships_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.UpdateProjectMembershipsParam if args[0] != nil { arg0 = args[0].(api.UpdateProjectMembershipsParam) } run( arg0, ) }) return _c } func (_c *MockClient_UpdateProjectMemberships_Call) Return(project dto.Project, err error) *MockClient_UpdateProjectMemberships_Call { _c.Call.Return(project, err) return _c } func (_c *MockClient_UpdateProjectMemberships_Call) RunAndReturn(run func(updateProjectMembershipsParam api.UpdateProjectMembershipsParam) (dto.Project, error)) *MockClient_UpdateProjectMemberships_Call { _c.Call.Return(run) return _c } // UpdateProjectTemplate provides a mock function for the type MockClient func (_mock *MockClient) UpdateProjectTemplate(updateProjectTemplateParam api.UpdateProjectTemplateParam) (dto.Project, error) { ret := _mock.Called(updateProjectTemplateParam) if len(ret) == 0 { panic("no return value specified for UpdateProjectTemplate") } var r0 dto.Project var r1 error if returnFunc, ok := ret.Get(0).(func(api.UpdateProjectTemplateParam) (dto.Project, error)); ok { return returnFunc(updateProjectTemplateParam) } if returnFunc, ok := ret.Get(0).(func(api.UpdateProjectTemplateParam) dto.Project); ok { r0 = returnFunc(updateProjectTemplateParam) } else { r0 = ret.Get(0).(dto.Project) } if returnFunc, ok := ret.Get(1).(func(api.UpdateProjectTemplateParam) error); ok { r1 = returnFunc(updateProjectTemplateParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_UpdateProjectTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateProjectTemplate' type MockClient_UpdateProjectTemplate_Call struct { *mock.Call } // UpdateProjectTemplate is a helper method to define mock.On call // - updateProjectTemplateParam api.UpdateProjectTemplateParam func (_e *MockClient_Expecter) UpdateProjectTemplate(updateProjectTemplateParam interface{}) *MockClient_UpdateProjectTemplate_Call { return &MockClient_UpdateProjectTemplate_Call{Call: _e.mock.On("UpdateProjectTemplate", updateProjectTemplateParam)} } func (_c *MockClient_UpdateProjectTemplate_Call) Run(run func(updateProjectTemplateParam api.UpdateProjectTemplateParam)) *MockClient_UpdateProjectTemplate_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.UpdateProjectTemplateParam if args[0] != nil { arg0 = args[0].(api.UpdateProjectTemplateParam) } run( arg0, ) }) return _c } func (_c *MockClient_UpdateProjectTemplate_Call) Return(project dto.Project, err error) *MockClient_UpdateProjectTemplate_Call { _c.Call.Return(project, err) return _c } func (_c *MockClient_UpdateProjectTemplate_Call) RunAndReturn(run func(updateProjectTemplateParam api.UpdateProjectTemplateParam) (dto.Project, error)) *MockClient_UpdateProjectTemplate_Call { _c.Call.Return(run) return _c } // UpdateProjectUserBillableRate provides a mock function for the type MockClient func (_mock *MockClient) UpdateProjectUserBillableRate(updateProjectUserRateParam api.UpdateProjectUserRateParam) (dto.Project, error) { ret := _mock.Called(updateProjectUserRateParam) if len(ret) == 0 { panic("no return value specified for UpdateProjectUserBillableRate") } var r0 dto.Project var r1 error if returnFunc, ok := ret.Get(0).(func(api.UpdateProjectUserRateParam) (dto.Project, error)); ok { return returnFunc(updateProjectUserRateParam) } if returnFunc, ok := ret.Get(0).(func(api.UpdateProjectUserRateParam) dto.Project); ok { r0 = returnFunc(updateProjectUserRateParam) } else { r0 = ret.Get(0).(dto.Project) } if returnFunc, ok := ret.Get(1).(func(api.UpdateProjectUserRateParam) error); ok { r1 = returnFunc(updateProjectUserRateParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_UpdateProjectUserBillableRate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateProjectUserBillableRate' type MockClient_UpdateProjectUserBillableRate_Call struct { *mock.Call } // UpdateProjectUserBillableRate is a helper method to define mock.On call // - updateProjectUserRateParam api.UpdateProjectUserRateParam func (_e *MockClient_Expecter) UpdateProjectUserBillableRate(updateProjectUserRateParam interface{}) *MockClient_UpdateProjectUserBillableRate_Call { return &MockClient_UpdateProjectUserBillableRate_Call{Call: _e.mock.On("UpdateProjectUserBillableRate", updateProjectUserRateParam)} } func (_c *MockClient_UpdateProjectUserBillableRate_Call) Run(run func(updateProjectUserRateParam api.UpdateProjectUserRateParam)) *MockClient_UpdateProjectUserBillableRate_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.UpdateProjectUserRateParam if args[0] != nil { arg0 = args[0].(api.UpdateProjectUserRateParam) } run( arg0, ) }) return _c } func (_c *MockClient_UpdateProjectUserBillableRate_Call) Return(project dto.Project, err error) *MockClient_UpdateProjectUserBillableRate_Call { _c.Call.Return(project, err) return _c } func (_c *MockClient_UpdateProjectUserBillableRate_Call) RunAndReturn(run func(updateProjectUserRateParam api.UpdateProjectUserRateParam) (dto.Project, error)) *MockClient_UpdateProjectUserBillableRate_Call { _c.Call.Return(run) return _c } // UpdateProjectUserCostRate provides a mock function for the type MockClient func (_mock *MockClient) UpdateProjectUserCostRate(updateProjectUserRateParam api.UpdateProjectUserRateParam) (dto.Project, error) { ret := _mock.Called(updateProjectUserRateParam) if len(ret) == 0 { panic("no return value specified for UpdateProjectUserCostRate") } var r0 dto.Project var r1 error if returnFunc, ok := ret.Get(0).(func(api.UpdateProjectUserRateParam) (dto.Project, error)); ok { return returnFunc(updateProjectUserRateParam) } if returnFunc, ok := ret.Get(0).(func(api.UpdateProjectUserRateParam) dto.Project); ok { r0 = returnFunc(updateProjectUserRateParam) } else { r0 = ret.Get(0).(dto.Project) } if returnFunc, ok := ret.Get(1).(func(api.UpdateProjectUserRateParam) error); ok { r1 = returnFunc(updateProjectUserRateParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_UpdateProjectUserCostRate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateProjectUserCostRate' type MockClient_UpdateProjectUserCostRate_Call struct { *mock.Call } // UpdateProjectUserCostRate is a helper method to define mock.On call // - updateProjectUserRateParam api.UpdateProjectUserRateParam func (_e *MockClient_Expecter) UpdateProjectUserCostRate(updateProjectUserRateParam interface{}) *MockClient_UpdateProjectUserCostRate_Call { return &MockClient_UpdateProjectUserCostRate_Call{Call: _e.mock.On("UpdateProjectUserCostRate", updateProjectUserRateParam)} } func (_c *MockClient_UpdateProjectUserCostRate_Call) Run(run func(updateProjectUserRateParam api.UpdateProjectUserRateParam)) *MockClient_UpdateProjectUserCostRate_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.UpdateProjectUserRateParam if args[0] != nil { arg0 = args[0].(api.UpdateProjectUserRateParam) } run( arg0, ) }) return _c } func (_c *MockClient_UpdateProjectUserCostRate_Call) Return(project dto.Project, err error) *MockClient_UpdateProjectUserCostRate_Call { _c.Call.Return(project, err) return _c } func (_c *MockClient_UpdateProjectUserCostRate_Call) RunAndReturn(run func(updateProjectUserRateParam api.UpdateProjectUserRateParam) (dto.Project, error)) *MockClient_UpdateProjectUserCostRate_Call { _c.Call.Return(run) return _c } // UpdateTask provides a mock function for the type MockClient func (_mock *MockClient) UpdateTask(updateTaskParam api.UpdateTaskParam) (dto.Task, error) { ret := _mock.Called(updateTaskParam) if len(ret) == 0 { panic("no return value specified for UpdateTask") } var r0 dto.Task var r1 error if returnFunc, ok := ret.Get(0).(func(api.UpdateTaskParam) (dto.Task, error)); ok { return returnFunc(updateTaskParam) } if returnFunc, ok := ret.Get(0).(func(api.UpdateTaskParam) dto.Task); ok { r0 = returnFunc(updateTaskParam) } else { r0 = ret.Get(0).(dto.Task) } if returnFunc, ok := ret.Get(1).(func(api.UpdateTaskParam) error); ok { r1 = returnFunc(updateTaskParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_UpdateTask_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateTask' type MockClient_UpdateTask_Call struct { *mock.Call } // UpdateTask is a helper method to define mock.On call // - updateTaskParam api.UpdateTaskParam func (_e *MockClient_Expecter) UpdateTask(updateTaskParam interface{}) *MockClient_UpdateTask_Call { return &MockClient_UpdateTask_Call{Call: _e.mock.On("UpdateTask", updateTaskParam)} } func (_c *MockClient_UpdateTask_Call) Run(run func(updateTaskParam api.UpdateTaskParam)) *MockClient_UpdateTask_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.UpdateTaskParam if args[0] != nil { arg0 = args[0].(api.UpdateTaskParam) } run( arg0, ) }) return _c } func (_c *MockClient_UpdateTask_Call) Return(task dto.Task, err error) *MockClient_UpdateTask_Call { _c.Call.Return(task, err) return _c } func (_c *MockClient_UpdateTask_Call) RunAndReturn(run func(updateTaskParam api.UpdateTaskParam) (dto.Task, error)) *MockClient_UpdateTask_Call { _c.Call.Return(run) return _c } // UpdateTimeEntry provides a mock function for the type MockClient func (_mock *MockClient) UpdateTimeEntry(updateTimeEntryParam api.UpdateTimeEntryParam) (dto.TimeEntryImpl, error) { ret := _mock.Called(updateTimeEntryParam) if len(ret) == 0 { panic("no return value specified for UpdateTimeEntry") } var r0 dto.TimeEntryImpl var r1 error if returnFunc, ok := ret.Get(0).(func(api.UpdateTimeEntryParam) (dto.TimeEntryImpl, error)); ok { return returnFunc(updateTimeEntryParam) } if returnFunc, ok := ret.Get(0).(func(api.UpdateTimeEntryParam) dto.TimeEntryImpl); ok { r0 = returnFunc(updateTimeEntryParam) } else { r0 = ret.Get(0).(dto.TimeEntryImpl) } if returnFunc, ok := ret.Get(1).(func(api.UpdateTimeEntryParam) error); ok { r1 = returnFunc(updateTimeEntryParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_UpdateTimeEntry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateTimeEntry' type MockClient_UpdateTimeEntry_Call struct { *mock.Call } // UpdateTimeEntry is a helper method to define mock.On call // - updateTimeEntryParam api.UpdateTimeEntryParam func (_e *MockClient_Expecter) UpdateTimeEntry(updateTimeEntryParam interface{}) *MockClient_UpdateTimeEntry_Call { return &MockClient_UpdateTimeEntry_Call{Call: _e.mock.On("UpdateTimeEntry", updateTimeEntryParam)} } func (_c *MockClient_UpdateTimeEntry_Call) Run(run func(updateTimeEntryParam api.UpdateTimeEntryParam)) *MockClient_UpdateTimeEntry_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.UpdateTimeEntryParam if args[0] != nil { arg0 = args[0].(api.UpdateTimeEntryParam) } run( arg0, ) }) return _c } func (_c *MockClient_UpdateTimeEntry_Call) Return(timeEntryImpl dto.TimeEntryImpl, err error) *MockClient_UpdateTimeEntry_Call { _c.Call.Return(timeEntryImpl, err) return _c } func (_c *MockClient_UpdateTimeEntry_Call) RunAndReturn(run func(updateTimeEntryParam api.UpdateTimeEntryParam) (dto.TimeEntryImpl, error)) *MockClient_UpdateTimeEntry_Call { _c.Call.Return(run) return _c } // WorkspaceUsers provides a mock function for the type MockClient func (_mock *MockClient) WorkspaceUsers(workspaceUsersParam api.WorkspaceUsersParam) ([]dto.User, error) { ret := _mock.Called(workspaceUsersParam) if len(ret) == 0 { panic("no return value specified for WorkspaceUsers") } var r0 []dto.User var r1 error if returnFunc, ok := ret.Get(0).(func(api.WorkspaceUsersParam) ([]dto.User, error)); ok { return returnFunc(workspaceUsersParam) } if returnFunc, ok := ret.Get(0).(func(api.WorkspaceUsersParam) []dto.User); ok { r0 = returnFunc(workspaceUsersParam) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]dto.User) } } if returnFunc, ok := ret.Get(1).(func(api.WorkspaceUsersParam) error); ok { r1 = returnFunc(workspaceUsersParam) } else { r1 = ret.Error(1) } return r0, r1 } // MockClient_WorkspaceUsers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WorkspaceUsers' type MockClient_WorkspaceUsers_Call struct { *mock.Call } // WorkspaceUsers is a helper method to define mock.On call // - workspaceUsersParam api.WorkspaceUsersParam func (_e *MockClient_Expecter) WorkspaceUsers(workspaceUsersParam interface{}) *MockClient_WorkspaceUsers_Call { return &MockClient_WorkspaceUsers_Call{Call: _e.mock.On("WorkspaceUsers", workspaceUsersParam)} } func (_c *MockClient_WorkspaceUsers_Call) Run(run func(workspaceUsersParam api.WorkspaceUsersParam)) *MockClient_WorkspaceUsers_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 api.WorkspaceUsersParam if args[0] != nil { arg0 = args[0].(api.WorkspaceUsersParam) } run( arg0, ) }) return _c } func (_c *MockClient_WorkspaceUsers_Call) Return(users []dto.User, err error) *MockClient_WorkspaceUsers_Call { _c.Call.Return(users, err) return _c } func (_c *MockClient_WorkspaceUsers_Call) RunAndReturn(run func(workspaceUsersParam api.WorkspaceUsersParam) ([]dto.User, error)) *MockClient_WorkspaceUsers_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/mocks/mock_Config.go ================================================ // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "time" mock "github.com/stretchr/testify/mock" "golang.org/x/text/language" ) // NewMockConfig creates a new instance of MockConfig. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockConfig(t interface { mock.TestingT Cleanup(func()) }) *MockConfig { mock := &MockConfig{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // MockConfig is an autogenerated mock type for the Config type type MockConfig struct { mock.Mock } type MockConfig_Expecter struct { mock *mock.Mock } func (_m *MockConfig) EXPECT() *MockConfig_Expecter { return &MockConfig_Expecter{mock: &_m.Mock} } // All provides a mock function for the type MockConfig func (_mock *MockConfig) All() map[string]interface{} { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for All") } var r0 map[string]interface{} if returnFunc, ok := ret.Get(0).(func() map[string]interface{}); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(map[string]interface{}) } } return r0 } // MockConfig_All_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'All' type MockConfig_All_Call struct { *mock.Call } // All is a helper method to define mock.On call func (_e *MockConfig_Expecter) All() *MockConfig_All_Call { return &MockConfig_All_Call{Call: _e.mock.On("All")} } func (_c *MockConfig_All_Call) Run(run func()) *MockConfig_All_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConfig_All_Call) Return(stringToIfaceVal map[string]interface{}) *MockConfig_All_Call { _c.Call.Return(stringToIfaceVal) return _c } func (_c *MockConfig_All_Call) RunAndReturn(run func() map[string]interface{}) *MockConfig_All_Call { _c.Call.Return(run) return _c } // Get provides a mock function for the type MockConfig func (_mock *MockConfig) Get(s string) interface{} { ret := _mock.Called(s) if len(ret) == 0 { panic("no return value specified for Get") } var r0 interface{} if returnFunc, ok := ret.Get(0).(func(string) interface{}); ok { r0 = returnFunc(s) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(interface{}) } } return r0 } // MockConfig_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' type MockConfig_Get_Call struct { *mock.Call } // Get is a helper method to define mock.On call // - s string func (_e *MockConfig_Expecter) Get(s interface{}) *MockConfig_Get_Call { return &MockConfig_Get_Call{Call: _e.mock.On("Get", s)} } func (_c *MockConfig_Get_Call) Run(run func(s string)) *MockConfig_Get_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *MockConfig_Get_Call) Return(ifaceVal interface{}) *MockConfig_Get_Call { _c.Call.Return(ifaceVal) return _c } func (_c *MockConfig_Get_Call) RunAndReturn(run func(s string) interface{}) *MockConfig_Get_Call { _c.Call.Return(run) return _c } // GetBool provides a mock function for the type MockConfig func (_mock *MockConfig) GetBool(s string) bool { ret := _mock.Called(s) if len(ret) == 0 { panic("no return value specified for GetBool") } var r0 bool if returnFunc, ok := ret.Get(0).(func(string) bool); ok { r0 = returnFunc(s) } else { r0 = ret.Get(0).(bool) } return r0 } // MockConfig_GetBool_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBool' type MockConfig_GetBool_Call struct { *mock.Call } // GetBool is a helper method to define mock.On call // - s string func (_e *MockConfig_Expecter) GetBool(s interface{}) *MockConfig_GetBool_Call { return &MockConfig_GetBool_Call{Call: _e.mock.On("GetBool", s)} } func (_c *MockConfig_GetBool_Call) Run(run func(s string)) *MockConfig_GetBool_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *MockConfig_GetBool_Call) Return(b bool) *MockConfig_GetBool_Call { _c.Call.Return(b) return _c } func (_c *MockConfig_GetBool_Call) RunAndReturn(run func(s string) bool) *MockConfig_GetBool_Call { _c.Call.Return(run) return _c } // GetInt provides a mock function for the type MockConfig func (_mock *MockConfig) GetInt(s string) int { ret := _mock.Called(s) if len(ret) == 0 { panic("no return value specified for GetInt") } var r0 int if returnFunc, ok := ret.Get(0).(func(string) int); ok { r0 = returnFunc(s) } else { r0 = ret.Get(0).(int) } return r0 } // MockConfig_GetInt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetInt' type MockConfig_GetInt_Call struct { *mock.Call } // GetInt is a helper method to define mock.On call // - s string func (_e *MockConfig_Expecter) GetInt(s interface{}) *MockConfig_GetInt_Call { return &MockConfig_GetInt_Call{Call: _e.mock.On("GetInt", s)} } func (_c *MockConfig_GetInt_Call) Run(run func(s string)) *MockConfig_GetInt_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *MockConfig_GetInt_Call) Return(n int) *MockConfig_GetInt_Call { _c.Call.Return(n) return _c } func (_c *MockConfig_GetInt_Call) RunAndReturn(run func(s string) int) *MockConfig_GetInt_Call { _c.Call.Return(run) return _c } // GetString provides a mock function for the type MockConfig func (_mock *MockConfig) GetString(s string) string { ret := _mock.Called(s) if len(ret) == 0 { panic("no return value specified for GetString") } var r0 string if returnFunc, ok := ret.Get(0).(func(string) string); ok { r0 = returnFunc(s) } else { r0 = ret.Get(0).(string) } return r0 } // MockConfig_GetString_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetString' type MockConfig_GetString_Call struct { *mock.Call } // GetString is a helper method to define mock.On call // - s string func (_e *MockConfig_Expecter) GetString(s interface{}) *MockConfig_GetString_Call { return &MockConfig_GetString_Call{Call: _e.mock.On("GetString", s)} } func (_c *MockConfig_GetString_Call) Run(run func(s string)) *MockConfig_GetString_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *MockConfig_GetString_Call) Return(s1 string) *MockConfig_GetString_Call { _c.Call.Return(s1) return _c } func (_c *MockConfig_GetString_Call) RunAndReturn(run func(s string) string) *MockConfig_GetString_Call { _c.Call.Return(run) return _c } // GetStringSlice provides a mock function for the type MockConfig func (_mock *MockConfig) GetStringSlice(s string) []string { ret := _mock.Called(s) if len(ret) == 0 { panic("no return value specified for GetStringSlice") } var r0 []string if returnFunc, ok := ret.Get(0).(func(string) []string); ok { r0 = returnFunc(s) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]string) } } return r0 } // MockConfig_GetStringSlice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetStringSlice' type MockConfig_GetStringSlice_Call struct { *mock.Call } // GetStringSlice is a helper method to define mock.On call // - s string func (_e *MockConfig_Expecter) GetStringSlice(s interface{}) *MockConfig_GetStringSlice_Call { return &MockConfig_GetStringSlice_Call{Call: _e.mock.On("GetStringSlice", s)} } func (_c *MockConfig_GetStringSlice_Call) Run(run func(s string)) *MockConfig_GetStringSlice_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } run( arg0, ) }) return _c } func (_c *MockConfig_GetStringSlice_Call) Return(strings []string) *MockConfig_GetStringSlice_Call { _c.Call.Return(strings) return _c } func (_c *MockConfig_GetStringSlice_Call) RunAndReturn(run func(s string) []string) *MockConfig_GetStringSlice_Call { _c.Call.Return(run) return _c } // GetWorkWeekdays provides a mock function for the type MockConfig func (_mock *MockConfig) GetWorkWeekdays() []string { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for GetWorkWeekdays") } var r0 []string if returnFunc, ok := ret.Get(0).(func() []string); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]string) } } return r0 } // MockConfig_GetWorkWeekdays_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkWeekdays' type MockConfig_GetWorkWeekdays_Call struct { *mock.Call } // GetWorkWeekdays is a helper method to define mock.On call func (_e *MockConfig_Expecter) GetWorkWeekdays() *MockConfig_GetWorkWeekdays_Call { return &MockConfig_GetWorkWeekdays_Call{Call: _e.mock.On("GetWorkWeekdays")} } func (_c *MockConfig_GetWorkWeekdays_Call) Run(run func()) *MockConfig_GetWorkWeekdays_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConfig_GetWorkWeekdays_Call) Return(strings []string) *MockConfig_GetWorkWeekdays_Call { _c.Call.Return(strings) return _c } func (_c *MockConfig_GetWorkWeekdays_Call) RunAndReturn(run func() []string) *MockConfig_GetWorkWeekdays_Call { _c.Call.Return(run) return _c } // InteractivePageSize provides a mock function for the type MockConfig func (_mock *MockConfig) InteractivePageSize() int { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for InteractivePageSize") } var r0 int if returnFunc, ok := ret.Get(0).(func() int); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(int) } return r0 } // MockConfig_InteractivePageSize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InteractivePageSize' type MockConfig_InteractivePageSize_Call struct { *mock.Call } // InteractivePageSize is a helper method to define mock.On call func (_e *MockConfig_Expecter) InteractivePageSize() *MockConfig_InteractivePageSize_Call { return &MockConfig_InteractivePageSize_Call{Call: _e.mock.On("InteractivePageSize")} } func (_c *MockConfig_InteractivePageSize_Call) Run(run func()) *MockConfig_InteractivePageSize_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConfig_InteractivePageSize_Call) Return(n int) *MockConfig_InteractivePageSize_Call { _c.Call.Return(n) return _c } func (_c *MockConfig_InteractivePageSize_Call) RunAndReturn(run func() int) *MockConfig_InteractivePageSize_Call { _c.Call.Return(run) return _c } // IsAllowNameForID provides a mock function for the type MockConfig func (_mock *MockConfig) IsAllowNameForID() bool { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for IsAllowNameForID") } var r0 bool if returnFunc, ok := ret.Get(0).(func() bool); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(bool) } return r0 } // MockConfig_IsAllowNameForID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsAllowNameForID' type MockConfig_IsAllowNameForID_Call struct { *mock.Call } // IsAllowNameForID is a helper method to define mock.On call func (_e *MockConfig_Expecter) IsAllowNameForID() *MockConfig_IsAllowNameForID_Call { return &MockConfig_IsAllowNameForID_Call{Call: _e.mock.On("IsAllowNameForID")} } func (_c *MockConfig_IsAllowNameForID_Call) Run(run func()) *MockConfig_IsAllowNameForID_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConfig_IsAllowNameForID_Call) Return(b bool) *MockConfig_IsAllowNameForID_Call { _c.Call.Return(b) return _c } func (_c *MockConfig_IsAllowNameForID_Call) RunAndReturn(run func() bool) *MockConfig_IsAllowNameForID_Call { _c.Call.Return(run) return _c } // IsDebuging provides a mock function for the type MockConfig func (_mock *MockConfig) IsDebuging() bool { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for IsDebuging") } var r0 bool if returnFunc, ok := ret.Get(0).(func() bool); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(bool) } return r0 } // MockConfig_IsDebuging_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsDebuging' type MockConfig_IsDebuging_Call struct { *mock.Call } // IsDebuging is a helper method to define mock.On call func (_e *MockConfig_Expecter) IsDebuging() *MockConfig_IsDebuging_Call { return &MockConfig_IsDebuging_Call{Call: _e.mock.On("IsDebuging")} } func (_c *MockConfig_IsDebuging_Call) Run(run func()) *MockConfig_IsDebuging_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConfig_IsDebuging_Call) Return(b bool) *MockConfig_IsDebuging_Call { _c.Call.Return(b) return _c } func (_c *MockConfig_IsDebuging_Call) RunAndReturn(run func() bool) *MockConfig_IsDebuging_Call { _c.Call.Return(run) return _c } // IsInteractive provides a mock function for the type MockConfig func (_mock *MockConfig) IsInteractive() bool { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for IsInteractive") } var r0 bool if returnFunc, ok := ret.Get(0).(func() bool); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(bool) } return r0 } // MockConfig_IsInteractive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsInteractive' type MockConfig_IsInteractive_Call struct { *mock.Call } // IsInteractive is a helper method to define mock.On call func (_e *MockConfig_Expecter) IsInteractive() *MockConfig_IsInteractive_Call { return &MockConfig_IsInteractive_Call{Call: _e.mock.On("IsInteractive")} } func (_c *MockConfig_IsInteractive_Call) Run(run func()) *MockConfig_IsInteractive_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConfig_IsInteractive_Call) Return(b bool) *MockConfig_IsInteractive_Call { _c.Call.Return(b) return _c } func (_c *MockConfig_IsInteractive_Call) RunAndReturn(run func() bool) *MockConfig_IsInteractive_Call { _c.Call.Return(run) return _c } // IsSearchProjectWithClientsName provides a mock function for the type MockConfig func (_mock *MockConfig) IsSearchProjectWithClientsName() bool { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for IsSearchProjectWithClientsName") } var r0 bool if returnFunc, ok := ret.Get(0).(func() bool); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(bool) } return r0 } // MockConfig_IsSearchProjectWithClientsName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsSearchProjectWithClientsName' type MockConfig_IsSearchProjectWithClientsName_Call struct { *mock.Call } // IsSearchProjectWithClientsName is a helper method to define mock.On call func (_e *MockConfig_Expecter) IsSearchProjectWithClientsName() *MockConfig_IsSearchProjectWithClientsName_Call { return &MockConfig_IsSearchProjectWithClientsName_Call{Call: _e.mock.On("IsSearchProjectWithClientsName")} } func (_c *MockConfig_IsSearchProjectWithClientsName_Call) Run(run func()) *MockConfig_IsSearchProjectWithClientsName_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConfig_IsSearchProjectWithClientsName_Call) Return(b bool) *MockConfig_IsSearchProjectWithClientsName_Call { _c.Call.Return(b) return _c } func (_c *MockConfig_IsSearchProjectWithClientsName_Call) RunAndReturn(run func() bool) *MockConfig_IsSearchProjectWithClientsName_Call { _c.Call.Return(run) return _c } // Language provides a mock function for the type MockConfig func (_mock *MockConfig) Language() language.Tag { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Language") } var r0 language.Tag if returnFunc, ok := ret.Get(0).(func() language.Tag); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(language.Tag) } return r0 } // MockConfig_Language_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Language' type MockConfig_Language_Call struct { *mock.Call } // Language is a helper method to define mock.On call func (_e *MockConfig_Expecter) Language() *MockConfig_Language_Call { return &MockConfig_Language_Call{Call: _e.mock.On("Language")} } func (_c *MockConfig_Language_Call) Run(run func()) *MockConfig_Language_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConfig_Language_Call) Return(tag language.Tag) *MockConfig_Language_Call { _c.Call.Return(tag) return _c } func (_c *MockConfig_Language_Call) RunAndReturn(run func() language.Tag) *MockConfig_Language_Call { _c.Call.Return(run) return _c } // LogLevel provides a mock function for the type MockConfig func (_mock *MockConfig) LogLevel() string { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for LogLevel") } var r0 string if returnFunc, ok := ret.Get(0).(func() string); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(string) } return r0 } // MockConfig_LogLevel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogLevel' type MockConfig_LogLevel_Call struct { *mock.Call } // LogLevel is a helper method to define mock.On call func (_e *MockConfig_Expecter) LogLevel() *MockConfig_LogLevel_Call { return &MockConfig_LogLevel_Call{Call: _e.mock.On("LogLevel")} } func (_c *MockConfig_LogLevel_Call) Run(run func()) *MockConfig_LogLevel_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConfig_LogLevel_Call) Return(s string) *MockConfig_LogLevel_Call { _c.Call.Return(s) return _c } func (_c *MockConfig_LogLevel_Call) RunAndReturn(run func() string) *MockConfig_LogLevel_Call { _c.Call.Return(run) return _c } // Save provides a mock function for the type MockConfig func (_mock *MockConfig) Save() error { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Save") } var r0 error if returnFunc, ok := ret.Get(0).(func() error); ok { r0 = returnFunc() } else { r0 = ret.Error(0) } return r0 } // MockConfig_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save' type MockConfig_Save_Call struct { *mock.Call } // Save is a helper method to define mock.On call func (_e *MockConfig_Expecter) Save() *MockConfig_Save_Call { return &MockConfig_Save_Call{Call: _e.mock.On("Save")} } func (_c *MockConfig_Save_Call) Run(run func()) *MockConfig_Save_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConfig_Save_Call) Return(err error) *MockConfig_Save_Call { _c.Call.Return(err) return _c } func (_c *MockConfig_Save_Call) RunAndReturn(run func() error) *MockConfig_Save_Call { _c.Call.Return(run) return _c } // SetBool provides a mock function for the type MockConfig func (_mock *MockConfig) SetBool(s string, b bool) { _mock.Called(s, b) return } // MockConfig_SetBool_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetBool' type MockConfig_SetBool_Call struct { *mock.Call } // SetBool is a helper method to define mock.On call // - s string // - b bool func (_e *MockConfig_Expecter) SetBool(s interface{}, b interface{}) *MockConfig_SetBool_Call { return &MockConfig_SetBool_Call{Call: _e.mock.On("SetBool", s, b)} } func (_c *MockConfig_SetBool_Call) Run(run func(s string, b bool)) *MockConfig_SetBool_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } var arg1 bool if args[1] != nil { arg1 = args[1].(bool) } run( arg0, arg1, ) }) return _c } func (_c *MockConfig_SetBool_Call) Return() *MockConfig_SetBool_Call { _c.Call.Return() return _c } func (_c *MockConfig_SetBool_Call) RunAndReturn(run func(s string, b bool)) *MockConfig_SetBool_Call { _c.Run(run) return _c } // SetInt provides a mock function for the type MockConfig func (_mock *MockConfig) SetInt(s string, n int) { _mock.Called(s, n) return } // MockConfig_SetInt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetInt' type MockConfig_SetInt_Call struct { *mock.Call } // SetInt is a helper method to define mock.On call // - s string // - n int func (_e *MockConfig_Expecter) SetInt(s interface{}, n interface{}) *MockConfig_SetInt_Call { return &MockConfig_SetInt_Call{Call: _e.mock.On("SetInt", s, n)} } func (_c *MockConfig_SetInt_Call) Run(run func(s string, n int)) *MockConfig_SetInt_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } var arg1 int if args[1] != nil { arg1 = args[1].(int) } run( arg0, arg1, ) }) return _c } func (_c *MockConfig_SetInt_Call) Return() *MockConfig_SetInt_Call { _c.Call.Return() return _c } func (_c *MockConfig_SetInt_Call) RunAndReturn(run func(s string, n int)) *MockConfig_SetInt_Call { _c.Run(run) return _c } // SetLanguage provides a mock function for the type MockConfig func (_mock *MockConfig) SetLanguage(tag language.Tag) { _mock.Called(tag) return } // MockConfig_SetLanguage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetLanguage' type MockConfig_SetLanguage_Call struct { *mock.Call } // SetLanguage is a helper method to define mock.On call // - tag language.Tag func (_e *MockConfig_Expecter) SetLanguage(tag interface{}) *MockConfig_SetLanguage_Call { return &MockConfig_SetLanguage_Call{Call: _e.mock.On("SetLanguage", tag)} } func (_c *MockConfig_SetLanguage_Call) Run(run func(tag language.Tag)) *MockConfig_SetLanguage_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 language.Tag if args[0] != nil { arg0 = args[0].(language.Tag) } run( arg0, ) }) return _c } func (_c *MockConfig_SetLanguage_Call) Return() *MockConfig_SetLanguage_Call { _c.Call.Return() return _c } func (_c *MockConfig_SetLanguage_Call) RunAndReturn(run func(tag language.Tag)) *MockConfig_SetLanguage_Call { _c.Run(run) return _c } // SetString provides a mock function for the type MockConfig func (_mock *MockConfig) SetString(s string, s1 string) { _mock.Called(s, s1) return } // MockConfig_SetString_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetString' type MockConfig_SetString_Call struct { *mock.Call } // SetString is a helper method to define mock.On call // - s string // - s1 string func (_e *MockConfig_Expecter) SetString(s interface{}, s1 interface{}) *MockConfig_SetString_Call { return &MockConfig_SetString_Call{Call: _e.mock.On("SetString", s, s1)} } func (_c *MockConfig_SetString_Call) Run(run func(s string, s1 string)) *MockConfig_SetString_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } var arg1 string if args[1] != nil { arg1 = args[1].(string) } run( arg0, arg1, ) }) return _c } func (_c *MockConfig_SetString_Call) Return() *MockConfig_SetString_Call { _c.Call.Return() return _c } func (_c *MockConfig_SetString_Call) RunAndReturn(run func(s string, s1 string)) *MockConfig_SetString_Call { _c.Run(run) return _c } // SetStringSlice provides a mock function for the type MockConfig func (_mock *MockConfig) SetStringSlice(s string, strings []string) { _mock.Called(s, strings) return } // MockConfig_SetStringSlice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetStringSlice' type MockConfig_SetStringSlice_Call struct { *mock.Call } // SetStringSlice is a helper method to define mock.On call // - s string // - strings []string func (_e *MockConfig_Expecter) SetStringSlice(s interface{}, strings interface{}) *MockConfig_SetStringSlice_Call { return &MockConfig_SetStringSlice_Call{Call: _e.mock.On("SetStringSlice", s, strings)} } func (_c *MockConfig_SetStringSlice_Call) Run(run func(s string, strings []string)) *MockConfig_SetStringSlice_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } var arg1 []string if args[1] != nil { arg1 = args[1].([]string) } run( arg0, arg1, ) }) return _c } func (_c *MockConfig_SetStringSlice_Call) Return() *MockConfig_SetStringSlice_Call { _c.Call.Return() return _c } func (_c *MockConfig_SetStringSlice_Call) RunAndReturn(run func(s string, strings []string)) *MockConfig_SetStringSlice_Call { _c.Run(run) return _c } // SetTimeZone provides a mock function for the type MockConfig func (_mock *MockConfig) SetTimeZone(location *time.Location) { _mock.Called(location) return } // MockConfig_SetTimeZone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetTimeZone' type MockConfig_SetTimeZone_Call struct { *mock.Call } // SetTimeZone is a helper method to define mock.On call // - location *time.Location func (_e *MockConfig_Expecter) SetTimeZone(location interface{}) *MockConfig_SetTimeZone_Call { return &MockConfig_SetTimeZone_Call{Call: _e.mock.On("SetTimeZone", location)} } func (_c *MockConfig_SetTimeZone_Call) Run(run func(location *time.Location)) *MockConfig_SetTimeZone_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 *time.Location if args[0] != nil { arg0 = args[0].(*time.Location) } run( arg0, ) }) return _c } func (_c *MockConfig_SetTimeZone_Call) Return() *MockConfig_SetTimeZone_Call { _c.Call.Return() return _c } func (_c *MockConfig_SetTimeZone_Call) RunAndReturn(run func(location *time.Location)) *MockConfig_SetTimeZone_Call { _c.Run(run) return _c } // TimeZone provides a mock function for the type MockConfig func (_mock *MockConfig) TimeZone() *time.Location { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for TimeZone") } var r0 *time.Location if returnFunc, ok := ret.Get(0).(func() *time.Location); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*time.Location) } } return r0 } // MockConfig_TimeZone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TimeZone' type MockConfig_TimeZone_Call struct { *mock.Call } // TimeZone is a helper method to define mock.On call func (_e *MockConfig_Expecter) TimeZone() *MockConfig_TimeZone_Call { return &MockConfig_TimeZone_Call{Call: _e.mock.On("TimeZone")} } func (_c *MockConfig_TimeZone_Call) Run(run func()) *MockConfig_TimeZone_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConfig_TimeZone_Call) Return(location *time.Location) *MockConfig_TimeZone_Call { _c.Call.Return(location) return _c } func (_c *MockConfig_TimeZone_Call) RunAndReturn(run func() *time.Location) *MockConfig_TimeZone_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/mocks/mock_Factory.go ================================================ // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package mocks import ( "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/ui" mock "github.com/stretchr/testify/mock" ) // NewMockFactory creates a new instance of MockFactory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockFactory(t interface { mock.TestingT Cleanup(func()) }) *MockFactory { mock := &MockFactory{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // MockFactory is an autogenerated mock type for the Factory type type MockFactory struct { mock.Mock } type MockFactory_Expecter struct { mock *mock.Mock } func (_m *MockFactory) EXPECT() *MockFactory_Expecter { return &MockFactory_Expecter{mock: &_m.Mock} } // Client provides a mock function for the type MockFactory func (_mock *MockFactory) Client() (api.Client, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Client") } var r0 api.Client var r1 error if returnFunc, ok := ret.Get(0).(func() (api.Client, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() api.Client); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(api.Client) } } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // MockFactory_Client_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Client' type MockFactory_Client_Call struct { *mock.Call } // Client is a helper method to define mock.On call func (_e *MockFactory_Expecter) Client() *MockFactory_Client_Call { return &MockFactory_Client_Call{Call: _e.mock.On("Client")} } func (_c *MockFactory_Client_Call) Run(run func()) *MockFactory_Client_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockFactory_Client_Call) Return(client api.Client, err error) *MockFactory_Client_Call { _c.Call.Return(client, err) return _c } func (_c *MockFactory_Client_Call) RunAndReturn(run func() (api.Client, error)) *MockFactory_Client_Call { _c.Call.Return(run) return _c } // Config provides a mock function for the type MockFactory func (_mock *MockFactory) Config() cmdutil.Config { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Config") } var r0 cmdutil.Config if returnFunc, ok := ret.Get(0).(func() cmdutil.Config); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(cmdutil.Config) } } return r0 } // MockFactory_Config_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Config' type MockFactory_Config_Call struct { *mock.Call } // Config is a helper method to define mock.On call func (_e *MockFactory_Expecter) Config() *MockFactory_Config_Call { return &MockFactory_Config_Call{Call: _e.mock.On("Config")} } func (_c *MockFactory_Config_Call) Run(run func()) *MockFactory_Config_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockFactory_Config_Call) Return(config cmdutil.Config) *MockFactory_Config_Call { _c.Call.Return(config) return _c } func (_c *MockFactory_Config_Call) RunAndReturn(run func() cmdutil.Config) *MockFactory_Config_Call { _c.Call.Return(run) return _c } // GetUserID provides a mock function for the type MockFactory func (_mock *MockFactory) GetUserID() (string, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for GetUserID") } var r0 string var r1 error if returnFunc, ok := ret.Get(0).(func() (string, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() string); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(string) } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // MockFactory_GetUserID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserID' type MockFactory_GetUserID_Call struct { *mock.Call } // GetUserID is a helper method to define mock.On call func (_e *MockFactory_Expecter) GetUserID() *MockFactory_GetUserID_Call { return &MockFactory_GetUserID_Call{Call: _e.mock.On("GetUserID")} } func (_c *MockFactory_GetUserID_Call) Run(run func()) *MockFactory_GetUserID_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockFactory_GetUserID_Call) Return(s string, err error) *MockFactory_GetUserID_Call { _c.Call.Return(s, err) return _c } func (_c *MockFactory_GetUserID_Call) RunAndReturn(run func() (string, error)) *MockFactory_GetUserID_Call { _c.Call.Return(run) return _c } // GetWorkspace provides a mock function for the type MockFactory func (_mock *MockFactory) GetWorkspace() (dto.Workspace, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for GetWorkspace") } var r0 dto.Workspace var r1 error if returnFunc, ok := ret.Get(0).(func() (dto.Workspace, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() dto.Workspace); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(dto.Workspace) } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // MockFactory_GetWorkspace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkspace' type MockFactory_GetWorkspace_Call struct { *mock.Call } // GetWorkspace is a helper method to define mock.On call func (_e *MockFactory_Expecter) GetWorkspace() *MockFactory_GetWorkspace_Call { return &MockFactory_GetWorkspace_Call{Call: _e.mock.On("GetWorkspace")} } func (_c *MockFactory_GetWorkspace_Call) Run(run func()) *MockFactory_GetWorkspace_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockFactory_GetWorkspace_Call) Return(workspace dto.Workspace, err error) *MockFactory_GetWorkspace_Call { _c.Call.Return(workspace, err) return _c } func (_c *MockFactory_GetWorkspace_Call) RunAndReturn(run func() (dto.Workspace, error)) *MockFactory_GetWorkspace_Call { _c.Call.Return(run) return _c } // GetWorkspaceID provides a mock function for the type MockFactory func (_mock *MockFactory) GetWorkspaceID() (string, error) { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for GetWorkspaceID") } var r0 string var r1 error if returnFunc, ok := ret.Get(0).(func() (string, error)); ok { return returnFunc() } if returnFunc, ok := ret.Get(0).(func() string); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(string) } if returnFunc, ok := ret.Get(1).(func() error); ok { r1 = returnFunc() } else { r1 = ret.Error(1) } return r0, r1 } // MockFactory_GetWorkspaceID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkspaceID' type MockFactory_GetWorkspaceID_Call struct { *mock.Call } // GetWorkspaceID is a helper method to define mock.On call func (_e *MockFactory_Expecter) GetWorkspaceID() *MockFactory_GetWorkspaceID_Call { return &MockFactory_GetWorkspaceID_Call{Call: _e.mock.On("GetWorkspaceID")} } func (_c *MockFactory_GetWorkspaceID_Call) Run(run func()) *MockFactory_GetWorkspaceID_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockFactory_GetWorkspaceID_Call) Return(s string, err error) *MockFactory_GetWorkspaceID_Call { _c.Call.Return(s, err) return _c } func (_c *MockFactory_GetWorkspaceID_Call) RunAndReturn(run func() (string, error)) *MockFactory_GetWorkspaceID_Call { _c.Call.Return(run) return _c } // UI provides a mock function for the type MockFactory func (_mock *MockFactory) UI() ui.UI { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for UI") } var r0 ui.UI if returnFunc, ok := ret.Get(0).(func() ui.UI); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(ui.UI) } } return r0 } // MockFactory_UI_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UI' type MockFactory_UI_Call struct { *mock.Call } // UI is a helper method to define mock.On call func (_e *MockFactory_Expecter) UI() *MockFactory_UI_Call { return &MockFactory_UI_Call{Call: _e.mock.On("UI")} } func (_c *MockFactory_UI_Call) Run(run func()) *MockFactory_UI_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockFactory_UI_Call) Return(uI ui.UI) *MockFactory_UI_Call { _c.Call.Return(uI) return _c } func (_c *MockFactory_UI_Call) RunAndReturn(run func() ui.UI) *MockFactory_UI_Call { _c.Call.Return(run) return _c } // Version provides a mock function for the type MockFactory func (_mock *MockFactory) Version() cmdutil.Version { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Version") } var r0 cmdutil.Version if returnFunc, ok := ret.Get(0).(func() cmdutil.Version); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(cmdutil.Version) } return r0 } // MockFactory_Version_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Version' type MockFactory_Version_Call struct { *mock.Call } // Version is a helper method to define mock.On call func (_e *MockFactory_Expecter) Version() *MockFactory_Version_Call { return &MockFactory_Version_Call{Call: _e.mock.On("Version")} } func (_c *MockFactory_Version_Call) Run(run func()) *MockFactory_Version_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockFactory_Version_Call) Return(version cmdutil.Version) *MockFactory_Version_Call { _c.Call.Return(version) return _c } func (_c *MockFactory_Version_Call) RunAndReturn(run func() cmdutil.Version) *MockFactory_Version_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/mocks/simple_config.go ================================================ package mocks import ( "time" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "golang.org/x/text/language" ) // SimpleConfig is used to set configs for tests were changing the config or // accessing them with Get and All is not important type SimpleConfig struct { WorkweekDays []string Interactive bool InteractivePageSizeNumber int AllowNameForID bool UserID string Workspace string Token string AllowIncomplete bool ShowTask bool DescriptionAutocomplete bool DescriptionAutocompleteDays int ShowTotalDuration bool LogLevelValue string AllowArchivedTags bool SearchProjectWithClientsName bool LanguageTag language.Tag TimeZoneLoc *time.Location } func (d *SimpleConfig) GetBool(n string) bool { switch n { case cmdutil.CONF_INTERACTIVE: return d.Interactive case cmdutil.CONF_ALLOW_NAME_FOR_ID: return d.AllowNameForID case cmdutil.CONF_ALLOW_INCOMPLETE: return d.AllowIncomplete case cmdutil.CONF_SHOW_TASKS: return d.ShowTask case cmdutil.CONF_DESCR_AUTOCOMP: return d.DescriptionAutocomplete case cmdutil.CONF_SHOW_TOTAL_DURATION: return d.ShowTotalDuration case cmdutil.CONF_ALLOW_ARCHIVED_TAGS: return d.AllowArchivedTags default: return false } } func (*SimpleConfig) SetBool(_ string, _ bool) { panic("should not call") } func (d *SimpleConfig) GetInt(n string) int { switch n { case cmdutil.CONF_DESCR_AUTOCOMP_DAYS: return d.DescriptionAutocompleteDays case cmdutil.CONF_INTERACTIVE_PAGE_SIZE: return d.InteractivePageSize() default: return 0 } } func (*SimpleConfig) SetInt(_ string, _ int) { panic("should not call") } func (d *SimpleConfig) GetString(n string) string { switch n { case cmdutil.CONF_USER_ID: return d.UserID case cmdutil.CONF_WORKSPACE: return d.Workspace case cmdutil.CONF_TOKEN: return d.Token case cmdutil.CONF_LOG_LEVEL: return d.LogLevelValue default: return "" } } func (*SimpleConfig) SetString(_, _ string) { panic("should not call") } func (d *SimpleConfig) GetStringSlice(n string) []string { switch n { case cmdutil.CONF_WORKWEEK_DAYS: return d.WorkweekDays default: return []string{} } } func (*SimpleConfig) SetStringSlice(_ string, _ []string) { panic("should not call") } func (d *SimpleConfig) IsDebuging() bool { return d.LogLevel() == cmdutil.LOG_LEVEL_DEBUG } func (d *SimpleConfig) IsAllowNameForID() bool { return d.AllowNameForID } func (d *SimpleConfig) IsInteractive() bool { return d.Interactive } func (d *SimpleConfig) GetWorkWeekdays() []string { return d.WorkweekDays } func (d *SimpleConfig) SetLanguage(l language.Tag) { d.LanguageTag = l } func (d *SimpleConfig) Language() language.Tag { return d.LanguageTag } func (*SimpleConfig) Get(_ string) interface{} { panic("should not call") } func (*SimpleConfig) All() map[string]interface{} { panic("should not call") } func (d *SimpleConfig) LogLevel() string { return d.LogLevelValue } // TimeZone which time zone to use for showing date & time func (s *SimpleConfig) TimeZone() *time.Location { if s.TimeZoneLoc == nil { s.TimeZoneLoc = time.UTC } return s.TimeZoneLoc } // SetTimeZone changes the timezone used for dates func (s *SimpleConfig) SetTimeZone(loc *time.Location) { s.TimeZoneLoc = loc } // IsSearchProjectWithClientsName defines if the project name for ID should // include the client's name func (s *SimpleConfig) IsSearchProjectWithClientsName() bool { return s.SearchProjectWithClientsName } // InteractivePageSize sets how many items are shown when prompting // projects func (s *SimpleConfig) InteractivePageSize() int { return s.InteractivePageSizeNumber } func (*SimpleConfig) Save() error { panic("should not call") } ================================================ FILE: internal/testhlp/helper.go ================================================ package testhlp import "time" // MustParseTime will parse a string as time.Time or panic func MustParseTime(l, v string) time.Time { t, err := time.Parse(l, v) if err == nil { return t } panic(err) } ================================================ FILE: netlify.toml ================================================ [build.environment] GO_VERSION = "1.24" HUGO_VERSION = "0.156.0" [build] command = "make site-build" publish = "site/public" ================================================ FILE: pkg/cmd/client/add/add.go ================================================ package add import ( "io" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/client/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/spf13/cobra" ) // NewCmdAdd represents the add command func NewCmdAdd( f cmdutil.Factory, report func(io.Writer, *util.OutputFlags, dto.Client) error, ) *cobra.Command { of := util.OutputFlags{} cmd := &cobra.Command{ Use: "add", Aliases: []string{"new", "create"}, Short: "Adds a new client to the Clockify workspace", Example: heredoc.Docf(` $ %[1]s --name Special +--------------------------+---------+----------+ | ID | NAME | ARCHIVED | +--------------------------+---------+----------+ | eeeeeeeeeeeeeeeeeeeeeeee | Special | NO | +--------------------------+---------+----------+ $ %[1]s --name "Very Special" --quiet aaaaaaaaaaaaaaaaaaaaaaaa $ %[1]s --name "Special" # same name as existing one add client: Client with name 'Special' already exists (code: 501) `, "clockify-cli client add"), RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } w, err := f.GetWorkspaceID() if err != nil { return err } c, err := f.Client() if err != nil { return err } name, _ := cmd.Flags().GetString("name") cl, err := c.AddClient(api.AddClientParam{ Workspace: w, Name: name, }) if err != nil { return err } out := cmd.OutOrStdout() if report != nil { return report(out, &of, cl) } return util.Report([]dto.Client{cl}, out, of) }, } cmd.Flags().StringP("name", "n", "", "the name of the new client") _ = cmd.MarkFlagRequired("name") util.AddReportFlags(cmd, &of) return cmd } ================================================ FILE: pkg/cmd/client/add/add_test.go ================================================ package add_test import ( "errors" "io" "testing" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/client/add" "github.com/lucassabreu/clockify-cli/pkg/cmd/client/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/stretchr/testify/assert" ) func TestCmdAdd(t *testing.T) { tts := []struct { name string args []string factory func(*testing.T) cmdutil.Factory err string }{ { name: "only one format", args: []string{"--format={}", "-q", "-j", "-n=OK"}, err: "flags can't be used together.*format.*json.*quiet", factory: func(t *testing.T) cmdutil.Factory { return mocks.NewMockFactory(t) }, }, { name: "name required", err: `"name" not set`, factory: func(t *testing.T) cmdutil.Factory { return mocks.NewMockFactory(t) }, }, { name: "client error", err: "client error", args: []string{"-n=a"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(nil, errors.New("client error")) return f }, }, { name: "workspace error", err: "workspace error", args: []string{"-n=a"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID"). Return("", errors.New("workspace error")) return f }, }, { name: "http error", err: "http error", args: []string{"-n=error"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) c.On("AddClient", api.AddClientParam{ Workspace: "w", Name: "error", }). Return(dto.Client{}, errors.New("http error")) return f }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { cmd := add.NewCmdAdd(tt.factory(t), func(io.Writer, *util.OutputFlags, dto.Client) error { t.Error("should not get here") return nil }) cmd.SilenceUsage = true cmd.SetArgs(tt.args) _, err := cmd.ExecuteC() if tt.err == "" { assert.NoError(t, err) return } assert.Error(t, err) assert.Regexp(t, tt.err, err.Error()) }) } } func TestCmdAddReport(t *testing.T) { cl := dto.Client{Name: "Coderockr"} tts := []struct { name string args []string assert func(*testing.T, *util.OutputFlags, dto.Client) }{ { name: "report quiet", args: []string{"-q"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Client) { assert.True(t, of.Quiet) }, }, { name: "report json", args: []string{"--json"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Client) { assert.True(t, of.JSON) }, }, { name: "report format", args: []string{"--format={{.ID}}"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Client) { assert.Equal(t, "{{.ID}}", of.Format) }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) f.On("GetWorkspaceID"). Return("w", nil) c.On("AddClient", api.AddClientParam{ Workspace: "w", Name: "rockr", }). Return(cl, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) cmd := add.NewCmdAdd(f, func( _ io.Writer, of *util.OutputFlags, u dto.Client) error { called = true assert.Equal(t, cl, u) tt.assert(t, of, u) return nil }) cmd.SilenceUsage = true cmd.SetArgs(append(tt.args, "-n=rockr")) _, err := cmd.ExecuteC() assert.NoError(t, err) }) } } ================================================ FILE: pkg/cmd/client/client.go ================================================ package client import ( "github.com/lucassabreu/clockify-cli/pkg/cmd/client/add" "github.com/lucassabreu/clockify-cli/pkg/cmd/client/list" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/spf13/cobra" ) // NewCmdClient represents the client command func NewCmdClient(f cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "client", Aliases: []string{"clients"}, Short: "Work with Clockify clients", } cmd.AddCommand(list.NewCmdList(f, nil)) cmd.AddCommand(add.NewCmdAdd(f, nil)) return cmd } ================================================ FILE: pkg/cmd/client/list/list.go ================================================ package list import ( "io" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/client/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/spf13/cobra" ) // NewCmdList represents the list command func NewCmdList( f cmdutil.Factory, report func(io.Writer, *util.OutputFlags, []dto.Client) error, ) *cobra.Command { of := util.OutputFlags{} var archived, notArchived bool cmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "List clients from a Clockify workspace", Example: heredoc.Docf(` $ %[1]s +--------------------------+----------+----------+ | ID | NAME | ARCHIVED | +--------------------------+----------+----------+ | 6202634a28782767054eec26 | Client 1 | NO | | 62964b36bb48532a70730dbe | Client 2 | YES | +--------------------------+----------+----------+ $ %[1]s --archived --csv 62964b36bb48532a70730dbe,Client 2,true $ %[1]s --not-archived --format "<{{ .Name }}>" $ %[1]s --name "1" --quiet 6202634a28782767054eec26 `, "clockify-cli client list"), RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } if err := cmdutil.XorFlag(map[string]bool{ "archived": archived, "not-archived": notArchived, }); err != nil { return err } p := api.GetClientsParam{ PaginationParam: api.AllPages(), } var err error if p.Workspace, err = f.GetWorkspaceID(); err != nil { return err } c, err := f.Client() if err != nil { return err } p.Name, _ = cmd.Flags().GetString("name") if archived || notArchived { p.Archived = &archived } clients, err := c.GetClients(p) if err != nil { return err } if report != nil { return report(cmd.OutOrStdout(), &of, clients) } return util.Report(clients, cmd.OutOrStdout(), of) }, } cmd.Flags().StringP("name", "n", "", "will be used to filter the tag by name") cmd.Flags().BoolVarP( ¬Archived, "not-archived", "", false, "list only active projects") cmd.Flags().BoolVarP( &archived, "archived", "", false, "list only archived projects") util.AddReportFlags(cmd, &of) return cmd } ================================================ FILE: pkg/cmd/client/list/list_test.go ================================================ package list_test import ( "errors" "io" "testing" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/client/list" "github.com/lucassabreu/clockify-cli/pkg/cmd/client/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/stretchr/testify/assert" ) type report func(io.Writer, *util.OutputFlags, []dto.Client) error func TestCmdList(t *testing.T) { defReport := func(io.Writer, *util.OutputFlags, []dto.Client) error { return errors.New("should not call") } cs := []dto.Client{{Name: "Coderockr"}} tts := []struct { name string args []string factory func(*testing.T) (cmdutil.Factory, report) err string }{ { name: "only one format", args: []string{"--format={}", "-q", "-j"}, err: "flags can't be used together.*format.*json.*quiet", factory: func(t *testing.T) (cmdutil.Factory, report) { return mocks.NewMockFactory(t), defReport }, }, { name: "archived or not", args: []string{"--archived", "--not-archived"}, err: "flags can't be used together.*archived.*not-archived", factory: func(t *testing.T) (cmdutil.Factory, report) { return mocks.NewMockFactory(t), defReport }, }, { name: "client error", err: "client error", factory: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(nil, errors.New("client error")) return f, defReport }, }, { name: "workspace error", err: "workspace error", factory: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID"). Return("", errors.New("workspace error")) return f, defReport }, }, { name: "http error", err: "http error", factory: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) c.On("GetClients", api.GetClientsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Client{}, errors.New("http error")) return f, defReport }, }, { name: "only archived", args: []string{"--archived"}, factory: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) f.On("GetWorkspaceID"). Return("w", nil) b := true c.On("GetClients", api.GetClientsParam{ Workspace: "w", Archived: &b, PaginationParam: api.AllPages(), }). Return(cs, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) return f, func( _ io.Writer, of *util.OutputFlags, l []dto.Client) error { called = true assert.Equal(t, cs, l) return nil } }, }, { name: "not archived", args: []string{"--not-archived"}, factory: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) f.On("GetWorkspaceID"). Return("w", nil) b := false c.On("GetClients", api.GetClientsParam{ Workspace: "w", Archived: &b, PaginationParam: api.AllPages(), }). Return(cs, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) return f, func( _ io.Writer, of *util.OutputFlags, l []dto.Client) error { called = true assert.Equal(t, cs, l) return nil } }, }, { name: "report quiet", args: []string{"--name=rockr", "-q"}, factory: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) f.On("GetWorkspaceID"). Return("w", nil) c.On("GetClients", api.GetClientsParam{ Workspace: "w", Name: "rockr", PaginationParam: api.AllPages(), }). Return(cs, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) return f, func( _ io.Writer, of *util.OutputFlags, u []dto.Client) error { called = true assert.Equal(t, cs, u) assert.True(t, of.Quiet) return nil } }, }, { name: "report json", args: []string{"--json"}, factory: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) f.On("GetWorkspaceID"). Return("w", nil) c.On("GetClients", api.GetClientsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return(cs, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) return f, func( _ io.Writer, of *util.OutputFlags, u []dto.Client) error { called = true assert.Equal(t, cs, u) assert.True(t, of.JSON) return nil } }, }, { name: "report format", args: []string{"--format={{.Name}}"}, factory: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) f.On("GetWorkspaceID"). Return("w", nil) c.On("GetClients", api.GetClientsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return(cs, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) return f, func( _ io.Writer, of *util.OutputFlags, u []dto.Client) error { called = true assert.Equal(t, cs, u) assert.Equal(t, "{{.Name}}", of.Format) return nil } }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { cmd := list.NewCmdList(tt.factory(t)) cmd.SilenceUsage = true cmd.SetArgs(tt.args) _, err := cmd.ExecuteC() if tt.err == "" { assert.NoError(t, err) return } assert.Error(t, err) assert.Regexp(t, tt.err, err.Error()) }) } } ================================================ FILE: pkg/cmd/client/util/util.go ================================================ package util import ( "io" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" output "github.com/lucassabreu/clockify-cli/pkg/output/client" "github.com/spf13/cobra" ) // OutputFlags sets how to print out a list of clients type OutputFlags struct { Format string CSV bool JSON bool Quiet bool } func (of OutputFlags) Check() error { return cmdutil.XorFlag(map[string]bool{ "format": of.Format != "", "json": of.JSON, "csv": of.CSV, "quiet": of.Quiet, }) } // AddReportFlags adds the default output flags for clients func AddReportFlags(cmd *cobra.Command, of *OutputFlags) { cmd.Flags().StringVarP(&of.Format, "format", "f", "", "golang text/template format to be applied on each Client") cmd.Flags().BoolVarP(&of.JSON, "json", "j", false, "print as JSON") cmd.Flags().BoolVarP(&of.CSV, "csv", "v", false, "print as CSV") cmd.Flags().BoolVarP(&of.Quiet, "quiet", "q", false, "only display ids") } // Report prints out the clients func Report(cs []dto.Client, out io.Writer, of OutputFlags) error { switch { case of.JSON: return output.ClientsJSONPrint(cs, out) case of.CSV: return output.ClientsCSVPrint(cs, out) case of.Format != "": return output.ClientPrintWithTemplate(of.Format)(cs, out) case of.Quiet: return output.ClientPrintQuietly(cs, out) default: return output.ClientPrint(cs, out) } } ================================================ FILE: pkg/cmd/completion/completion.go ================================================ package completion import ( "fmt" "io" "strings" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/spf13/cobra" ) const ( bash = "bash" zsh = "zsh" fish = "fish" powershell = "powershell" ) // NewCmdCompletion represents the completion command func NewCmdCompletion() *cobra.Command { args := cmdcompl.ValidArgsSlide{bash, zsh, fish, powershell} cmd := &cobra.Command{ Use: "completion " + args.IntoUse(), Short: "Generate completion script", DisableFlagsInUseLine: true, ValidArgs: args.OnlyArgs(), Args: cobra.MatchAll( cobra.OnlyValidArgs, cobra.ExactArgs(1), ), RunE: func(cmd *cobra.Command, args []string) error { out := cmd.OutOrStdout() switch strings.ToLower(args[0]) { case bash: return cmd.Root().GenBashCompletion(out) case zsh: return genZshCompletion(cmd, out) case fish: return cmd.Root().GenFishCompletion(out, false) case powershell: return cmd.Root().GenPowerShellCompletion(out) default: return nil } }, } cmd.Long = heredoc.Docf(` To load completions for every session, execute once: #### Linux (Bash): %[1]s $ clockify-cli completion %[2]s > /etc/bash_cmdcompl.d/clockify-cli %[1]s #### Linux (Shell): %[1]s $ clockify-cli completion %[2]s > /etc/bash_cmdcompl.d/clockify-cli %[1]s #### MacOS: %[1]s $ clockify-cli completion %[2]s > /usr/local/etc/bash_cmdcompl.d/clockify-cli %[1]s #### Zsh: To load completions for each session, add this line to your ~/.zshrc: %[1]s source <(clockify-cli completion %[3]s) %[1]s You will need to start a new shell for this setup to take effect. #### Fish: To load completions for each session, execute once: %[1]s $ clockify-cli completion %[4]s > ~/.config/fish/completions/clockify-cli.fish %[1]s`, "```", bash, zsh, fish) return cmd } func genZshCompletion(cmd *cobra.Command, w io.Writer) error { if _, err := fmt.Fprintln(w, "autoload -U compinit; compinit"); err != nil { return err } if err := cmd.Root().GenZshCompletion(w); err != nil { return err } _, err := fmt.Fprintln(w, "compdef _clockify-cli clockify-cli") return err } ================================================ FILE: pkg/cmd/config/config.go ================================================ package config import ( "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/pkg/cmd/config/get" initialize "github.com/lucassabreu/clockify-cli/pkg/cmd/config/init" "github.com/lucassabreu/clockify-cli/pkg/cmd/config/list" "github.com/lucassabreu/clockify-cli/pkg/cmd/config/set" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/spf13/cobra" ) var validParameters = cmdcompl.ValidArgsMap{ cmdutil.CONF_TOKEN: "clockify's token", cmdutil.CONF_WORKSPACE: "workspace to be used", cmdutil.CONF_USER_ID: "user id from the token", cmdutil.CONF_ALLOW_NAME_FOR_ID: "allow to input the name of the entity " + "instead of its ID (projects, clients, tasks, users and tags)", cmdutil.CONF_INTERACTIVE: "show interactive mode", cmdutil.CONF_WORKWEEK_DAYS: "days of the week were your expected to " + "work (use comma to set multiple)", cmdutil.CONF_ALLOW_INCOMPLETE: "should allow starting time entries with " + "missing required values", cmdutil.CONF_SHOW_TASKS: "should show an extra column with the task " + "description", cmdutil.CONF_SHOW_CLIENT: "should show an extra column with the client " + "description", cmdutil.CONF_DESCR_AUTOCOMP: "autocomplete description looking at " + "recent time entries", cmdutil.CONF_DESCR_AUTOCOMP_DAYS: "how many days should be considered " + "for the description autocomplete", cmdutil.CONF_SHOW_TOTAL_DURATION: "adds a totals line on time entry " + "reports with the sum of the time entries duration", cmdutil.CONF_LOG_LEVEL: "how much logs should be shown values: " + "none , error , info and debug", cmdutil.CONF_ALLOW_ARCHIVED_TAGS: "should allow and suggest archived tags", cmdutil.CONF_LANGUAGE: "which language to use for number " + "formatting", cmdutil.CONF_TIMEZONE: "which timezone to use to input/output time", cmdutil.CONF_API_URL: "custom Clockify API base URL (for segregated tenants)", } // NewCmdConfig represents the config command func NewCmdConfig(f cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "config", Short: "Manages CLI configuration", Args: cobra.MaximumNArgs(0), Example: heredoc.Doc(` # cli will guide you to configure the CLI $ clockify-cli config init # token is the minimum information required for the CLI to work $ clockify-cli set token # you can see your current parameters using: $ clockify-cli get # if you wanna see the value of token parameter: $ clockify-cli get token `), Long: heredoc.Doc(` Changes or shows configuration settings for clockify-cli These are the parameters manageable: `) + validParameters.Long(), } cmd.AddCommand(initialize.NewCmdInit(f)) cmd.AddCommand(set.NewCmdSet(f, validParameters)) cmd.AddCommand(get.NewCmdGet(f, validParameters)) cmd.AddCommand(list.NewCmdList(f)) return cmd } ================================================ FILE: pkg/cmd/config/get/get.go ================================================ package get import ( "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/pkg/cmd/config/util" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/spf13/cobra" ) func NewCmdGet( f cmdutil.Factory, validParameters cmdcompl.ValidArgsMap, ) *cobra.Command { var format string cmd := &cobra.Command{ Use: "get ", Short: "Retrieves one parameter set by the user", Example: heredoc.Docf(` $ %[1]s token Yamdas569 $ %[1]s workweek-days --format=json ["monday","tuesday","wednesday","thursday","friday"] `, "clockify-cli config get"), Args: cobra.MatchAll( cmdutil.RequiredNamedArgs("param"), cobra.ExactArgs(1), ), ValidArgs: validParameters.IntoValidArgs(), RunE: func(cmd *cobra.Command, args []string) error { return util.Report( cmd.OutOrStdout(), format, f.Config().Get(args[0])) }, } _ = util.AddReportFlags(cmd, &format) return cmd } ================================================ FILE: pkg/cmd/config/get/get_test.go ================================================ package get_test import ( "bytes" "errors" "testing" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/config/get" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) func newCmd(f cmdutil.Factory) *cobra.Command { cmd := get.NewCmdGet( f, cmdcompl.ValidArgsMap{}, ) cmd.SilenceErrors = true cmd.SilenceUsage = true b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetErr(b) return cmd } func TestGetCmdArgs(t *testing.T) { tcs := []struct { name string args []string err error }{ { name: "none", args: []string{}, err: errors.New("requires arg param"), }, { name: "two", args: []string{"param1", "param2"}, err: errors.New("accepts 1 arg(s), received 2"), }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { cmd := newCmd(mocks.NewMockFactory(t)) cmd.SetArgs(tc.args) _, err := cmd.ExecuteC() if assert.Error(t, err) { assert.Equal(t, err.Error(), tc.err.Error()) } }) } } func TestGetCmdRun(t *testing.T) { tcs := []struct { name string args []string config func(*testing.T) cmdutil.Config output string err error }{ { name: "token with default format", args: []string{"token"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("Get", "token").Once().Return("") return c }, output: "\n", }, { name: "token with json format", args: []string{"token", "--format=json"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("Get", "token").Once().Return("token-value") return c }, output: `"token-value"`, }, { name: "workdays default format", args: []string{"workdays"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("Get", "workdays").Once().Return([]string{ "monday", "tuesday", "sunday", }) return c }, output: heredoc.Doc(` - monday - tuesday - sunday `), }, { name: "workdays json format", args: []string{"workdays", "--format", "json"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("Get", "workdays").Once().Return([]string{ "monday", "tuesday", "sunday", }) return c }, output: `["monday","tuesday","sunday"]`, }, { name: "user.id default format", args: []string{"user.id"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("Get", "user.id").Once().Return("someuserid") return c }, output: "someuserid\n", }, { name: "user default format", args: []string{"user"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("Get", "user").Once().Return(map[string]string{ "id": "someuserid", }) return c }, output: "id: someuserid\n", }, { name: "user json format", args: []string{"user", "-f=JSON"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("Get", "user").Once().Return(map[string]string{ "id": "someuserid", }) return c }, output: `{"id":"someuserid"}`, }, { name: "invalid format", args: []string{"user", "--format", "tmol"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("Get", "user").Return(map[string]string{ "id": "someuserid", }) return c }, output: ``, err: errors.New("invalid format"), }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { f := mocks.NewMockFactory(t) f.On("Config").Once().Return(tc.config(t)) cmd := newCmd(f) cmd.SetArgs(tc.args) out := cmd.OutOrStdout().(*bytes.Buffer) _, err := cmd.ExecuteC() assert.Equal(t, tc.output, out.String()) if tc.err == nil { assert.NoError(t, err) return } if !assert.Error(t, err) { return } assert.Equal(t, err.Error(), tc.err.Error()) }) } } ================================================ FILE: pkg/cmd/config/init/init.go ================================================ package init import ( "fmt" "strings" "time" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/ui" "github.com/lucassabreu/clockify-cli/strhlp" "github.com/spf13/cobra" "golang.org/x/text/language" ) func queue( tasks ...func() error, ) error { for _, t := range tasks { if err := t(); err != nil { return err } } return nil } // NewCmdInit executes and initialization of the config func NewCmdInit(f cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "init", Short: "Setups the CLI parameters and behavior", Long: "Setups the CLI parameters with tokens, default workspace, " + "user and behaviors", Args: cobra.ExactArgs(0), RunE: func(_ *cobra.Command, _ []string) error { i := f.UI() config := f.Config() apiURL := config.GetString(cmdutil.CONF_API_URL) if apiURL == "" { apiURL = api.BASE_URL } var err error if apiURL, err = i.AskForText("Clockify API URL:", ui.WithDefault(apiURL), ui.WithHelp("If you need a specific URL, you can find it at "+ "https://clockify.me/help/getting-started/data-regions#data-residency-and-api, "+ "or leave it empty for the default."), ); err != nil { return err } if apiURL == api.BASE_URL { apiURL = "" } config.SetString(cmdutil.CONF_API_URL, apiURL) token := "" if token, err = i.AskForText("User Generated Token:", ui.WithDefault(config.GetString(cmdutil.CONF_TOKEN)), ui.WithHelp("Can be generated at "+ "https://app.clockify.me/manage-api-keys"), ); err != nil { return err } config.SetString(cmdutil.CONF_TOKEN, token) c, err := f.Client() if err != nil { return err } if err := queue( func() error { return setWorkspace(c, config, i) }, func() error { return setUser(c, config, i) }, updateFlag( i, config, cmdutil.CONF_ALLOW_NAME_FOR_ID, "Should try to find projects/clients/users/tasks/tags by their names?", ), func() error { if !config.IsAllowNameForID() { return nil } return updateFlag(i, config, cmdutil.CONF_SEARCH_PROJECTS_WITH_CLIENT_NAME, `Should search projects looking into their `+ `client's name too?`, )() }, updateFlag(i, config, cmdutil.CONF_INTERACTIVE, `Should use "Interactive Mode" by default?`, ), updateInt(i, config, cmdutil.CONF_INTERACTIVE_PAGE_SIZE, "How many items should be shown when asking for "+ "projects, tasks or tags?"), func() error { return setWeekdays(config, i) }, updateFlag(i, config, cmdutil.CONF_ALLOW_INCOMPLETE, `Should allow starting time entries with incomplete data?`, ), updateFlag(i, config, cmdutil.CONF_SHOW_TASKS, `Should show task on time entries as a separated column?`, ), updateFlag(i, config, cmdutil.CONF_SHOW_CUSTOM_FIELDS, `Should show custom fields on time entries as a separated column?`, ), updateFlag(i, config, cmdutil.CONF_SHOW_CLIENT, `Should show client on time entries as a separated column?`, ), updateFlag(i, config, cmdutil.CONF_SHOW_TOTAL_DURATION, `Should show a line with the sum of `+ `the time entries duration?`, ), updateFlag(i, config, cmdutil.CONF_DESCR_AUTOCOMP, `Allow description suggestions using `+ `recent time entries' descriptions?`, ), func() error { if !config.GetBool(cmdutil.CONF_DESCR_AUTOCOMP) { config.SetInt(cmdutil.CONF_DESCR_AUTOCOMP_DAYS, 0) return nil } return updateInt( i, config, cmdutil.CONF_DESCR_AUTOCOMP_DAYS, `How many days should be used for a time entry to be `+ `"recent"?`, )() }, updateFlag(i, config, cmdutil.CONF_ALLOW_ARCHIVED_TAGS, "Should suggest and allow creating time entries "+ "with archived tags?", ), setLanguage(i, config), setTimezone(i, config), ); err != nil { return err } return config.Save() }, } return cmd } func setTimezone(i ui.UI, config cmdutil.Config) func() error { return func() error { tzname, err := i.AskForValidText("What is your preferred timezone:", func(s string) error { _, err := time.LoadLocation(s) return err }, ui.WithHelp("Should be 'Local' to use the systems timezone, UTC "+ "or valid TZ identifier from the IANA TZ database "+ "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones"), ui.WithDefault(config.TimeZone().String()), ) if err != nil { return err } tz, _ := time.LoadLocation(tzname) config.SetTimeZone(tz) return nil } } func setLanguage(i ui.UI, config cmdutil.Config) func() error { return func() error { suggestLanguages := []string{ language.English.String(), language.German.String(), language.Afrikaans.String(), language.Chinese.String(), language.Portuguese.String(), } lang, err := i.AskForValidText("What is your preferred language:", func(s string) error { _, err := language.Parse(s) return err }, ui.WithHelp("Accepts any IETF language tag "+ "https://en.wikipedia.org/wiki/IETF_language_tag"), ui.WithSuggestion(func(toComplete string) []string { return strhlp.Filter( strhlp.IsSimilar(toComplete), suggestLanguages, ) }), ui.WithDefault(config.Language().String()), ) if err != nil { return err } config.SetLanguage(language.MustParse(lang)) return nil } } func setWeekdays(config cmdutil.Config, i ui.UI) (err error) { workweekDays := config.GetStringSlice(cmdutil.CONF_WORKWEEK_DAYS) if workweekDays, err = i.AskManyFromOptions( "Which days of the week do you work?", cmdutil.GetWeekdays(), workweekDays, nil, ); err != nil { return err } config.SetStringSlice(cmdutil.CONF_WORKWEEK_DAYS, workweekDays) return nil } func setUser(c api.Client, config cmdutil.Config, i ui.UI) error { users, err := c.WorkspaceUsers(api.WorkspaceUsersParam{ Workspace: config.GetString(cmdutil.CONF_WORKSPACE), PaginationParam: api.AllPages(), }) if err != nil { return err } userID := config.GetString(cmdutil.CONF_USER_ID) dUser := "" usersString := make([]string, len(users)) for i := range users { usersString[i] = fmt.Sprintf("%s - %s", users[i].ID, users[i].Name) if users[i].ID == userID { dUser = usersString[i] } } if userID, err = i.AskFromOptions( "Choose your user:", usersString, dUser); err != nil { return err } config.SetString(cmdutil.CONF_USER_ID, strings.TrimSpace(userID[0:strings.Index(userID, " - ")])) return nil } func setWorkspace(c api.Client, config cmdutil.Config, i ui.UI) error { ws, err := c.GetWorkspaces(api.GetWorkspaces{}) if err != nil { return err } dWorkspace := "" wsString := make([]string, len(ws)) for i := range ws { wsString[i] = fmt.Sprintf("%s - %s", ws[i].ID, ws[i].Name) if ws[i].ID == config.GetString(cmdutil.CONF_WORKSPACE) { dWorkspace = wsString[i] } } w := "" if w, err = i.AskFromOptions("Choose default Workspace:", wsString, dWorkspace); err != nil { return err } config.SetString(cmdutil.CONF_WORKSPACE, strings.TrimSpace(w[0:strings.Index(w, " - ")])) return err } func updateInt(ui ui.UI, config cmdutil.Config, param, desc string, ) func() error { return func() error { value := config.GetInt(param) value, err := ui.AskForInt(desc, value) if err != nil { return err } config.SetInt(param, value) return nil } } func updateFlag( ui ui.UI, config cmdutil.Config, param, description string, ) func() error { return func() (err error) { b := config.GetBool(param) if b, err = ui.Confirm(description, b); err != nil { return } config.SetBool(param, b) return } } ================================================ FILE: pkg/cmd/config/init/init_test.go ================================================ package init_test import ( "errors" "strings" "testing" "time" "github.com/AlecAivazis/survey/v2/terminal" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/consoletest" "github.com/lucassabreu/clockify-cli/internal/mocks" ini "github.com/lucassabreu/clockify-cli/pkg/cmd/config/init" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/ui" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "golang.org/x/text/language" ) func setStringFn(config *mocks.MockConfig, name, value string) *mock.Call { r := "" config.On("GetString", name). Return(func(string) string { v := r r = value return v }) return config.On("SetString", name, value) } func setBoolFn(config *mocks.MockConfig, name string, first, value bool) *mock.Call { r := first config.On("GetBool", name). Return(func(string) bool { v := r r = value return v }) return config.On("SetBool", name, value) } func TestInitCmd(t *testing.T) { consoletest.RunTestConsole(t, func(out consoletest.FileWriter, in consoletest.FileReader) error { f := mocks.NewMockFactory(t) config := mocks.NewMockConfig(t) client := mocks.NewMockClient(t) f.EXPECT().Config().Return(config) config.EXPECT().GetString(cmdutil.CONF_API_URL).Return("") config.EXPECT().SetString(cmdutil.CONF_API_URL, "").Once() config.EXPECT().GetString(cmdutil.CONF_TOKEN).Return("") config.EXPECT().SetString(cmdutil.CONF_TOKEN, "new token") f.EXPECT().Client().Return(client, nil) client.EXPECT().GetWorkspaces(api.GetWorkspaces{}). Return([]dto.Workspace{ {ID: "1", Name: "First"}, {ID: "2", Name: "Second"}, }, nil) call := setStringFn(config, cmdutil.CONF_WORKSPACE, "2") client.EXPECT().WorkspaceUsers(api.WorkspaceUsersParam{ Workspace: "2", PaginationParam: api.AllPages(), }). NotBefore(call). Return([]dto.User{ {ID: "user-1", Name: "John Due"}, {ID: "user-2", Name: "Joana D'ark"}, }, nil) setStringFn(config, cmdutil.CONF_USER_ID, "user-1") setBoolFn(config, cmdutil.CONF_ALLOW_NAME_FOR_ID, false, true). Run(func(args mock.Arguments) { config.EXPECT().IsAllowNameForID().Return(true) }) setBoolFn(config, cmdutil.CONF_SEARCH_PROJECTS_WITH_CLIENT_NAME, false, true) setBoolFn(config, cmdutil.CONF_INTERACTIVE, false, false) config.EXPECT().GetInt(cmdutil.CONF_INTERACTIVE_PAGE_SIZE). Return(7) config.EXPECT(). SetInt(cmdutil.CONF_INTERACTIVE_PAGE_SIZE, 10) config.EXPECT().GetStringSlice(cmdutil.CONF_WORKWEEK_DAYS). Return([]string{}) config.EXPECT().SetStringSlice(cmdutil.CONF_WORKWEEK_DAYS, []string{ strings.ToLower(time.Sunday.String()), strings.ToLower(time.Tuesday.String()), strings.ToLower(time.Thursday.String()), strings.ToLower(time.Friday.String()), strings.ToLower(time.Saturday.String()), }) setBoolFn(config, cmdutil.CONF_ALLOW_INCOMPLETE, false, false) setBoolFn(config, cmdutil.CONF_SHOW_TASKS, true, true) setBoolFn(config, cmdutil.CONF_SHOW_CUSTOM_FIELDS, true, true) setBoolFn(config, cmdutil.CONF_SHOW_CLIENT, true, true) setBoolFn(config, cmdutil.CONF_SHOW_TOTAL_DURATION, true, true) setBoolFn(config, cmdutil.CONF_DESCR_AUTOCOMP, false, true) config.EXPECT().GetInt(cmdutil.CONF_DESCR_AUTOCOMP_DAYS).Return(0) config.EXPECT().SetInt(cmdutil.CONF_DESCR_AUTOCOMP_DAYS, 10) setBoolFn(config, cmdutil.CONF_ALLOW_ARCHIVED_TAGS, true, false) config.EXPECT().Language().Return(language.English) config.EXPECT().SetLanguage(language.German) config.EXPECT().TimeZone().Return(time.Local) config.EXPECT().SetTimeZone(mock.Anything). Run(func(tz *time.Location) { assert.Equal(t, tz.String(), "America/Bahia") }) config.EXPECT().Save().Once().Return(nil) f.EXPECT().UI().Return(ui.NewUI(in, out, out)) _, err := ini.NewCmdInit(f).ExecuteC() return err }, func(c consoletest.ExpectConsole) { c.ExpectString("Clockify API URL:") c.SendLine("") c.ExpectString("https://api.clockify.me/api") c.ExpectString("Token:") c.SendLine("new token") c.ExpectString("new token") c.ExpectString("Choose default Workspace:") c.ExpectString("First") c.ExpectString("Second") c.SendLine("sec") c.ExpectString("Second") c.ExpectString("Choose your user:") c.ExpectString("John Due") c.ExpectString("Joana") c.SendLine("due") c.ExpectString("John Due") c.ExpectString("Should try to find") c.ExpectString("by their names?") c.SendLine("y") c.ExpectString("Yes") c.ExpectString("search projects looking into their client's name") c.SendLine("y") c.ExpectString("Yes") c.ExpectString("Interactive Mode\" by default?") c.SendLine("n") c.ExpectString("No") c.ExpectString("How many items should be shown when asking for " + "projects, tasks or tags?") c.ExpectString("7") c.SendLine("10") c.ExpectString("Which days of the week do you work?") c.ExpectString("sunday") c.ExpectString("monday") c.ExpectString("tuesday") c.ExpectString("wednesday") c.ExpectString("thursday") c.ExpectString("friday") c.ExpectString("saturday") c.Send(string(terminal.KeySpace)) c.Send(string(terminal.KeyArrowDown)) c.Send(string(terminal.KeyArrowDown)) c.Send(string(terminal.KeySpace)) c.Send(string(terminal.KeyArrowDown)) c.Send(string(terminal.KeyArrowDown)) c.Send(string(terminal.KeyArrowDown)) c.Send(string(terminal.KeySpace)) c.Send(string(terminal.KeyArrowUp)) c.Send(string(terminal.KeySpace)) c.Send("sat") c.Send(string(terminal.KeySpace)) c.SendLine("") c.ExpectString("sunday, tuesday, thursday, friday, saturday") c.ExpectString("incomplete data?") c.SendLine("") c.ExpectString("No") c.ExpectString("show task on time entries") c.SendLine("") c.ExpectString("Yes") c.ExpectString("show custom fields") c.SendLine("") c.ExpectString("Yes") c.ExpectString("show client on time entries") c.SendLine("") c.ExpectString("Yes") c.ExpectString("sum of the time entries duration?") c.SendLine("yes") c.ExpectString("Yes") c.ExpectString("descriptions?") c.SendLine("YES") c.ExpectString("Yes") c.ExpectString("How many days") c.SendLine("10") c.ExpectString("archived tags?") c.SendLine("n") c.ExpectString("No") c.ExpectString("preferred language") c.Send("e") c.Send(string(terminal.KeyTab)) c.SendLine(string(terminal.KeyTab)) c.ExpectString("preferred timezone:") c.SendLine("America/Bahia") c.ExpectString("America/Bahia") c.ExpectEOF() }) } func TestInitCmdCtrlC(t *testing.T) { consoletest.RunTestConsole(t, func(out consoletest.FileWriter, in consoletest.FileReader) error { f := mocks.NewMockFactory(t) config := mocks.NewMockConfig(t) f.EXPECT().Config().Return(config) config.EXPECT().GetString(cmdutil.CONF_API_URL).Return("") f.EXPECT().UI().Return(ui.NewUI(in, out, out)) _, err := ini.NewCmdInit(f).ExecuteC() if !assert.Error(t, err) { return errors.New("should have failed") } assert.ErrorIs(t, err, terminal.InterruptErr) return nil }, func(c consoletest.ExpectConsole) { c.ExpectString("Clockify API URL:") c.Send(string(terminal.KeyInterrupt)) c.ExpectEOF() }) } func TestInitCmdCtrlCAtToken(t *testing.T) { consoletest.RunTestConsole(t, func(out consoletest.FileWriter, in consoletest.FileReader) error { f := mocks.NewMockFactory(t) config := mocks.NewMockConfig(t) f.EXPECT().Config().Return(config) config.EXPECT().GetString(cmdutil.CONF_API_URL).Return("") config.EXPECT().SetString(cmdutil.CONF_API_URL, "").Once() config.EXPECT().GetString(cmdutil.CONF_TOKEN).Return("") f.EXPECT().UI().Return(ui.NewUI(in, out, out)) _, err := ini.NewCmdInit(f).ExecuteC() if !assert.Error(t, err) { return errors.New("should have failed") } assert.ErrorIs(t, err, terminal.InterruptErr) return nil }, func(c consoletest.ExpectConsole) { c.ExpectString("Clockify API URL:") c.SendLine("") c.ExpectString("https://api.clockify.me/api") c.ExpectString("Token:") c.Send(string(terminal.KeyInterrupt)) c.ExpectEOF() }) } ================================================ FILE: pkg/cmd/config/list/list.go ================================================ package list import ( "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/pkg/cmd/config/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/spf13/cobra" ) // NewCmdList creates the config list command func NewCmdList(f cmdutil.Factory) *cobra.Command { var format string cmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "List all parameters set by the user", Example: heredoc.Doc(` $ clockify-cli config list allow-incomplete: false allow-name-for-id: true allow-project-name: true debug: false description-autocomplete: true description-autocomplete-days: 15 interactive: true no-closing: false show-task: false show-custom-fields: false show-total-duration: true token: Yamdas569 user: id: ffffffffffffffffffffffff workspace: eeeeeeeeeeeeeeeeeeeeeeee workweek-days: - monday - tuesday - wednesday - thursday - friday `), Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return util.Report(cmd.OutOrStdout(), format, f.Config().All()) }, } _ = util.AddReportFlags(cmd, &format) return cmd } ================================================ FILE: pkg/cmd/config/list/list_test.go ================================================ package list_test import ( "bytes" "errors" "testing" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/pkg/cmd/config/list" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/stretchr/testify/assert" ) func TestListCmd(t *testing.T) { tts := []struct { name string args []string config func(t *testing.T) cmdutil.Config expectedOutput string err error }{ { name: "no args", args: []string{"param"}, err: errors.New(`unknown command "param" for "list"`), config: func(t *testing.T) cmdutil.Config { return nil }, }, { name: "default format", args: []string{}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("All").Once().Return(map[string]interface{}{ "token": "value", "user": map[string]string{"id": "user.id"}, }) return c }, expectedOutput: heredoc.Doc(` token: value user: id: user.id `), }, { name: "json format", args: []string{"--format=json"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("All").Once().Return(map[string]interface{}{ "token": "value", "user": map[string]string{"id": "user.id"}, }) return c }, expectedOutput: `{"token":"value","user":{"id":"user.id"}}`, }, { name: "invalid format", args: []string{"--format=tmol"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("All").Once().Return(map[string]interface{}{}) return c }, err: errors.New("invalid format"), }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) if c := tt.config(t); c != nil { f.On("Config").Return(c) } cmd := list.NewCmdList(f) cmd.SilenceErrors = true cmd.SilenceUsage = true b := bytes.NewBufferString("") cmd.SetOut(b) cmd.SetErr(b) cmd.SetArgs(tt.args) _, err := cmd.ExecuteC() if tt.err != nil && assert.Error(t, err) { assert.EqualError(t, err, tt.err.Error()) return } assert.NoError(t, err) assert.Equal(t, tt.expectedOutput, b.String()) }) } } ================================================ FILE: pkg/cmd/config/set/set.go ================================================ package set import ( "fmt" "strings" "time" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/strhlp" "github.com/spf13/cobra" "golang.org/x/text/language" ) // NewCmdSet will update the value of one parameter func NewCmdSet( f cmdutil.Factory, validParameters cmdcompl.ValidArgsMap, ) *cobra.Command { cmd := &cobra.Command{ Use: "set ", Args: cobra.MatchAll( cmdutil.RequiredNamedArgs("param", "value"), cobra.ExactArgs(2), ), ValidArgs: validParameters.IntoValidArgs(), Short: "Changes the value of one parameter", Example: heredoc.Docf(` $ %[1]s token "Yamdas569" $ %[1]s workweek-days monday,tuesday,wednesday,thursday,friday $ %[1]s show-task true $ %[1]s show-custom-fields true $ %[1]s user.id 4564d5a6s4d54a5s4dasd5 `, "clockify-cli config set"), RunE: func(cmd *cobra.Command, args []string) error { param := args[0] value := args[1] config := f.Config() switch param { case cmdutil.CONF_WORKWEEK_DAYS: ws := strings.Split(strings.ToLower(value), ",") ws = strhlp.Filter( func(s string) bool { return strhlp.Search(s, cmdutil.GetWeekdays()) != -1 }, ws, ) config.SetStringSlice(param, ws) case cmdutil.CONF_LANGUAGE: lang, err := language.Parse(value) if err != nil { return fmt.Errorf( "%s is not a valid language: %w", value, err) } config.SetLanguage(lang) case cmdutil.CONF_TIMEZONE: tz, err := time.LoadLocation(value) if err != nil { return fmt.Errorf( "%s is not a valid timezone: %w", value, err) } config.SetTimeZone(tz) default: config.SetString(param, value) } return config.Save() }, } return cmd } ================================================ FILE: pkg/cmd/config/set/set_test.go ================================================ package set_test import ( "bytes" "testing" "time" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/config/set" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/stretchr/testify/assert" "golang.org/x/text/language" ) func TestSetCmdArgs(t *testing.T) { tt := map[string][]string{ "zero": []string{}, "one": []string{"param"}, "three": []string{"param", "value", "other value"}, } for name := range tt { t.Run(name, func(t *testing.T) { cmd := set.NewCmdSet( mocks.NewMockFactory(t), cmdcompl.ValidArgsMap{}, ) b := bytes.NewBufferString("") cmd.SetArgs(tt[name]) cmd.SetErr(b) cmd.SetOut(b) _, err := cmd.ExecuteC() assert.Error(t, err) }) } } func TestSetCmdRun(t *testing.T) { ts := []struct { name string args []string config func(t *testing.T) cmdutil.Config }{ { name: "set token", args: []string{"token", "some value"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("SetString", "token", "some value").Return(nil).Once() c.On("Save").Once().Return(nil) return c }, }, { name: "set weekdays", args: []string{cmdutil.CONF_WORKWEEK_DAYS, "SUNDAY,SATURDAY"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("SetStringSlice", cmdutil.CONF_WORKWEEK_DAYS, []string{"sunday", "saturday"}). Return(nil).Once() c.On("Save").Once().Return(nil) return c }, }, { name: "set wrong weekdays", args: []string{cmdutil.CONF_WORKWEEK_DAYS, "monday,sunday,june"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("SetStringSlice", cmdutil.CONF_WORKWEEK_DAYS, []string{"monday", "sunday"}). Return(nil).Once() c.On("Save").Once().Return(nil) return c }, }, { name: "set show client", args: []string{cmdutil.CONF_SHOW_CLIENT, "true"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.On("SetString", cmdutil.CONF_SHOW_CLIENT, "true"). Return(nil).Once() c.On("Save").Once().Return(nil) return c }, }, { name: "set language", args: []string{cmdutil.CONF_LANGUAGE, "pt-br"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.EXPECT().SetLanguage(language.BrazilianPortuguese).Once() c.EXPECT().Save().Once().Return(nil) return c }, }, { name: "set language (iso 639)", args: []string{cmdutil.CONF_LANGUAGE, "pt"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) c.EXPECT().SetLanguage(language.Portuguese).Once() c.EXPECT().Save().Once().Return(nil) return c }, }, { name: "set timezone", args: []string{cmdutil.CONF_TIMEZONE, "America/Sao_Paulo"}, config: func(t *testing.T) cmdutil.Config { c := mocks.NewMockConfig(t) tz, _ := time.LoadLocation("America/Sao_Paulo") c.EXPECT().SetTimeZone(tz).Once() c.EXPECT().Save().Once().Return(nil) return c }, }, } for _, tc := range ts { t.Run(tc.name, func(t *testing.T) { c := tc.config(t) f := mocks.NewMockFactory(t) f.On("Config").Return(c) cmd := set.NewCmdSet( f, cmdcompl.ValidArgsMap{}, ) b := bytes.NewBufferString("") cmd.SetArgs(tc.args) cmd.SetErr(b) cmd.SetOut(b) _, err := cmd.ExecuteC() assert.NoError(t, err) }) } } func TestSetCmdShouldFail(t *testing.T) { ts := []struct { name string args []string err string }{ { name: "set language", args: []string{cmdutil.CONF_LANGUAGE, "klingon"}, err: "klingon is not a valid language.*", }, { name: "set timezone", args: []string{cmdutil.CONF_TIMEZONE, "Murica"}, err: "Murica is not a valid timezone.*", }, { name: "set timezone no caps", args: []string{cmdutil.CONF_TIMEZONE, "america/sao_paulo"}, err: `america/sao_paulo is not a valid timezone`, }, } for _, tc := range ts { t.Run(tc.name, func(t *testing.T) { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(mocks.NewMockConfig(t)) cmd := set.NewCmdSet(f, cmdcompl.ValidArgsMap{}) b := bytes.NewBufferString("") cmd.SetArgs(tc.args) cmd.SetErr(b) cmd.SetOut(b) _, err := cmd.ExecuteC() if !assert.Error(t, err) { return } assert.Regexp(t, tc.err, err.Error()) }) } } ================================================ FILE: pkg/cmd/config/util/util.go ================================================ package util import ( "encoding/json" "errors" "io" "strings" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) const FormatYAML = "yaml" const FormatJSON = "json" // AddReportFlags adds the format flag func AddReportFlags(cmd *cobra.Command, format *string) error { cmd.Flags().StringVarP(format, "format", "f", FormatYAML, "output format") return cmdcompl.AddFixedSuggestionsToFlag(cmd, "format", cmdcompl.ValidArgsSlide{FormatYAML, FormatJSON}) } // Report prints the value as YAML or JSON func Report(out io.Writer, format string, v interface{}) error { format = strings.ToLower(format) var b []byte switch format { case FormatJSON: b, _ = json.Marshal(v) case FormatYAML: b, _ = yaml.Marshal(v) default: return errors.New("invalid format") } _, err := out.Write(b) return err } ================================================ FILE: pkg/cmd/project/add/add.go ================================================ package add import ( "crypto/rand" "encoding/hex" "io" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/search" "github.com/spf13/cobra" ) // NewCmdAdd represents the add command func NewCmdAdd( f cmdutil.Factory, report func(io.Writer, *util.OutputFlags, dto.Project) error, ) *cobra.Command { of := util.OutputFlags{} p := api.AddProjectParam{} randomColor := false cmd := &cobra.Command{ Use: "add", Aliases: []string{"new", "create"}, Short: "Adds a project to the Clockify workspace", Example: heredoc.Docf(` $ %[1]s --name "New One" +--------------------------+---------+--------+ | ID | NAME | CLIENT | +--------------------------+---------+--------+ | 62a8b52d67f40258719037f2 | New One | | +--------------------------+---------+--------+ $ %[1]s --name=Other -q 62a8b59067f40258719038fc $ %[1]s --name "Other" --client="Uber" --csv --color=#fff id,name,client.id,client.name 62a8b607027fe4592ef1520b,Other,62964b36bb48532a70730dbe,Uber Special $ %[1]s --name Other --random-color add project: Other project for client Uber Special already exists. (code: 501) $ %[1]s --name "Something" --client="Uber" --color=#fff the following flags can't be used together: color and random-color $ %[1]s --name "Something" --client="Uber" the following flags can't be used together: color and random-color `, "clockify-cli project add"), RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } if err := cmdutil.XorFlag(map[string]bool{ "color": p.Color != "", "random-color": randomColor, }); err != nil { return err } var err error p.Workspace, err = f.GetWorkspaceID() if err != nil { return err } c, err := f.Client() if err != nil { return err } if p.ClientId != "" && f.Config().IsAllowNameForID() { cs, err := search.GetClientsByName( c, p.Workspace, []string{p.ClientId}) if err != nil { return err } p.ClientId = cs[0] } if randomColor { bytes := make([]byte, 3) if _, err := rand.Read(bytes); err != nil { return err } p.Color = "#" + hex.EncodeToString(bytes) } project, err := c.AddProject(p) if err != nil { return err } out := cmd.OutOrStdout() if report != nil { return report(out, &of, project) } return util.ReportOne(project, out, of) }, } cmd.Flags().StringVarP(&p.Name, "name", "n", "", "name of the new project") _ = cmd.MarkFlagRequired("name") cmd.Flags().StringVarP(&p.Color, "color", "c", "", "color of the new project") cmd.Flags().BoolVar(&randomColor, "random-color", false, "use a random color for the project") cmd.Flags().StringVarP(&p.Note, "note", "N", "", "note for the new project") cmd.Flags().StringVar(&p.ClientId, "client", "", "the id/name of the client the new project will go under") cmd.Flags().BoolVarP(&p.Public, "public", "p", false, "make the new project public") cmd.Flags().BoolVarP(&p.Billable, "billable", "b", false, "make the new project as billable") util.AddReportFlags(cmd, &of) return cmd } ================================================ FILE: pkg/cmd/project/add/add_test.go ================================================ package add_test import ( "errors" "io" "regexp" "testing" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/add" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) func TestCmdAdd(t *testing.T) { tts := []struct { name string args []string factory func(*testing.T) cmdutil.Factory report func(*testing.T) func( io.Writer, *util.OutputFlags, dto.Project) error err string }{ { name: "only one format", args: []string{"--format={}", "-q", "-j", "-n=OK"}, err: "flags can't be used together.*format.*json.*quiet", factory: func(t *testing.T) cmdutil.Factory { return mocks.NewMockFactory(t) }, }, { name: "random-color or color", args: []string{"--color=f00", "--random-color", "-n=OK"}, err: "flags can't be used together.*color.*random-color", factory: func(t *testing.T) cmdutil.Factory { return mocks.NewMockFactory(t) }, }, { name: "name required", err: `"name" not set`, factory: func(t *testing.T) cmdutil.Factory { return mocks.NewMockFactory(t) }, }, { name: "client error", err: "client error", args: []string{"-n=a"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(nil, errors.New("client error")) return f }, }, { name: "workspace error", err: "workspace error", args: []string{"-n=a"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID"). Return("", errors.New("workspace error")) return f }, }, { name: "lookup client", err: "no client", args: []string{"-n=error", "--client=rockr"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) c.On("GetClients", api.GetClientsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Client{}, errors.New("no client")) return f }, }, { name: "http error", err: "http error", args: []string{"-n=error"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) c.On("AddProject", api.AddProjectParam{ Workspace: "w", Name: "error", }). Return(dto.Project{}, errors.New("http error")) return f }, }, { name: "add project", args: []string{ "--name=Clockify", "--client=self", "--color=f00", "--note", "This one", "--public", "--billable", }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) c.On("GetClients", api.GetClientsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Client{{ID: "c-1", Name: "Myself"}}, nil) c.On("AddProject", api.AddProjectParam{ Workspace: "w", Name: "Clockify", ClientId: "c-1", Note: "This one", Public: true, Billable: true, Color: "f00", }). Return(dto.Project{ID: "project-id"}, nil) return f }, report: func(t *testing.T) func( io.Writer, *util.OutputFlags, dto.Project) error { called := false t.Cleanup(func() { assert.True(t, called) }) return func( w io.Writer, of *util.OutputFlags, p dto.Project) error { called = true assert.Equal(t, "project-id", p.ID) return nil } }, }, { name: "add with random color", args: []string{ "--name=Clockify", "--client=c-id", "--random-color", "--note", "This one", "--public", "--billable", }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(false) c.On("AddProject", mock.AnythingOfType("api.AddProjectParam")). Run(func(args mock.Arguments) { p := args.Get(0).(api.AddProjectParam) assert.Equal(t, p.Workspace, "w") assert.Equal(t, p.Name, "Clockify") assert.Equal(t, p.ClientId, "c-id") assert.Equal(t, p.Note, "This one") assert.Equal(t, p.Public, true) assert.Equal(t, p.Billable, true) assert.Regexp(t, regexp.MustCompile("#[0-9a-f]{6}"), p.Color) }). Return(dto.Project{ID: "project-id"}, nil) return f }, report: func(t *testing.T) func( io.Writer, *util.OutputFlags, dto.Project) error { called := false t.Cleanup(func() { assert.True(t, called) }) return func( w io.Writer, of *util.OutputFlags, p dto.Project) error { called = true assert.Equal(t, "project-id", p.ID) return nil } }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { r := func(io.Writer, *util.OutputFlags, dto.Project) error { assert.Fail(t, "failed") return nil } if tt.report != nil { r = tt.report(t) } cmd := add.NewCmdAdd(tt.factory(t), r) cmd.SilenceUsage = true cmd.SetArgs(tt.args) _, err := cmd.ExecuteC() if tt.err == "" { assert.NoError(t, err) return } assert.Error(t, err) assert.Regexp(t, tt.err, err.Error()) }) } } func TestCmdAddReport(t *testing.T) { pr := dto.Project{Name: "Coderockr"} tts := []struct { name string args []string assert func(*testing.T, *util.OutputFlags, dto.Project) }{ { name: "report quiet", args: []string{"-q"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Project) { assert.True(t, of.Quiet) }, }, { name: "report json", args: []string{"--json"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Project) { assert.True(t, of.JSON) }, }, { name: "report format", args: []string{"--format={{.ID}}"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Project) { assert.Equal(t, "{{.ID}}", of.Format) }, }, { name: "report csv", args: []string{"--csv"}, assert: func(t *testing.T, of *util.OutputFlags, _ dto.Project) { assert.True(t, of.CSV) }, }, { name: "report default", assert: func(t *testing.T, of *util.OutputFlags, _ dto.Project) { assert.False(t, of.CSV) assert.False(t, of.JSON) assert.False(t, of.Quiet) assert.True(t, of.Format == "") }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) f.On("GetWorkspaceID"). Return("w", nil) c.On("AddProject", api.AddProjectParam{ Workspace: "w", Name: "rockr", }). Return(pr, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) cmd := add.NewCmdAdd(f, func( _ io.Writer, of *util.OutputFlags, u dto.Project) error { called = true assert.Equal(t, pr, u) tt.assert(t, of, u) return nil }) cmd.SilenceUsage = true cmd.SetArgs(append(tt.args, "-n=rockr")) _, err := cmd.ExecuteC() assert.NoError(t, err) }) } } ================================================ FILE: pkg/cmd/project/edit/edit.go ================================================ package edit import ( "errors" "io" "strings" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/search" "github.com/lucassabreu/clockify-cli/strhlp" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" ) // NewCmdEdit updates a project func NewCmdEdit( f cmdutil.Factory, report func(io.Writer, *util.OutputFlags, []dto.Project) error, ) *cobra.Command { of := util.OutputFlags{} cmd := &cobra.Command{ Use: "edit ...", Aliases: []string{"update"}, Short: "Edit a project", Example: heredoc.Docf(` # set a client form the project $ clockify-cli project edit cli --client Myself +--------------------------+--------------+--------------------------------+ | ID | NAME | CLIENT | +--------------------------+--------------+--------------------------------+ | 621948458cb9606d934ebb1c | Clockify Cli | Myself | | | | (6202634a28782767054eec26) | +--------------------------+--------------+--------------------------------+ # remove client from a project $ clockify-cli project edit cli --no-client +--------------------------+--------------+--------+ | ID | NAME | CLIENT | +--------------------------+--------------+--------+ | 621948458cb9606d934ebb1c | Clockify Cli | | +--------------------------+--------------+--------+ # change name, color and make public $ clockify-cli project 62f19c254a912b05acc6d6cf \ --name First --public --color #0f0 \ --format "{{.Name}} | {{.Public}} | {{.Color}}" First | true | #00ff00 # change to not billable, archived and leave a note $ clockify-cli project second --not-billable --archived \ --note "$(cat notes.txt)" \ --format 'n: {{.Name}}\nb: {{.Billable}}\na: {{.Archived}}\nn:\n{{ .Note }}' n: Noted b: false a: false n: one line two lines three lines # archive multiple projects $ clockify-cli project first second \ --archived \ --format "{{.Name}} | {{.Archived}}" First | true Second | true `), Args: cmdutil.RequiredNamedArgs("project"), RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } if err := cmdutil.XorFlagSet( cmd.Flags(), "billable", "not-billable"); err != nil { return err } if err := cmdutil.XorFlagSet( cmd.Flags(), "private", "public"); err != nil { return err } if err := cmdutil.XorFlagSet( cmd.Flags(), "no-client", "client"); err != nil { return err } if err := cmdutil.XorFlagSet( cmd.Flags(), "archived", "active"); err != nil { return err } if len(args) > 1 && cmd.Flags().Changed("name") { return errors.New( "`--name` can't be changed for multiple projects") } w, err := f.GetWorkspaceID() if err != nil { return err } c, err := f.Client() if err != nil { return err } ids := strhlp.Unique(strhlp.Map(strings.TrimSpace, args)) var client *string if cmd.Flags().Changed("client") { id, _ := cmd.Flags().GetString("client") client = &id } else if cmd.Flags().Changed("no-client") { id := "" client = &id } if f.Config().IsAllowNameForID() { if ids, err = search.GetProjectsByName( c, f.Config(), w, "", ids); err != nil { return err } if client != nil && *client != "" { if *client, err = search.GetClientByName( c, w, *client); err != nil { return err } } } p := api.UpdateProjectParam{ Workspace: w, ClientId: client, } p.Name, _ = cmd.Flags().GetString("name") p.Color, _ = cmd.Flags().GetString("color") if cmd.Flags().Changed("note") { n, _ := cmd.Flags().GetString("note") p.Note = &n } if cmd.Flags().Changed("billable") || cmd.Flags().Changed("not-billable") { b, _ := cmd.Flags().GetBool("billable") p.Billable = &b } if cmd.Flags().Changed("archived") || cmd.Flags().Changed("active") { b, _ := cmd.Flags().GetBool("archived") p.Archived = &b } if cmd.Flags().Changed("public") || cmd.Flags().Changed("private") { b, _ := cmd.Flags().GetBool("public") p.Public = &b } var g errgroup.Group projects := make([]dto.Project, len(ids)) for i := 0; i < len(ids); i++ { j := i g.Go(func() error { cp := p cp.ProjectID = ids[j] projects[j], err = c.UpdateProject(cp) return err }) } if err := g.Wait(); err != nil { return err } if report == nil { return util.Report(projects, cmd.OutOrStdout(), of) } return report(cmd.OutOrStdout(), &of, projects) }, } cmd.Flags().StringP("name", "n", "", "name of the project") cmd.Flags().StringP("color", "c", "", "color of the projects") cmd.Flags().StringP("note", "N", "", "note for the projects") cmd.Flags().String("client", "", "the id/name of the client the projects will go under") cmd.Flags().Bool("no-client", false, "set projects as not having clients") cmd.Flags().BoolP("public", "p", false, "set projects as public") cmd.Flags().BoolP("private", "P", false, "set the projects as private") cmd.Flags().BoolP("billable", "b", false, "set the projects as billable") cmd.Flags().BoolP("not-billable", "B", false, "set the projects as not billable") cmd.Flags().BoolP("archived", "A", false, "set projects as archived") cmd.Flags().BoolP("active", "a", false, "set the projects as active") util.AddReportFlags(cmd, &of) return cmd } ================================================ FILE: pkg/cmd/project/edit/edit_test.go ================================================ package edit_test import ( "errors" "io" "testing" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/edit" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/stretchr/testify/assert" ) type report func(io.Writer, *util.OutputFlags, []dto.Project) error func TestEditCmd(t *testing.T) { tts := []struct { name string args []string err string params func(*testing.T) (cmdutil.Factory, report) }{ { name: "project is required", err: "requires arg project", params: func(t *testing.T) (cmdutil.Factory, report) { return mocks.NewMockFactory(t), nil }, }, { name: "can only change a project name", err: "`--name` can't be changed for multiple projects", args: []string{"cli", "edit", "-n=wrong"}, params: func(t *testing.T) (cmdutil.Factory, report) { return mocks.NewMockFactory(t), nil }, }, { name: "only one format", args: []string{"--format={}", "-q", "-j", "cli"}, err: "flags can't be used together.*format.*json.*quiet", params: func(t *testing.T) (cmdutil.Factory, report) { return mocks.NewMockFactory(t), nil }, }, { name: "billable or not", args: []string{"--billable", "--not-billable", "cli"}, err: "flags can't be used together.*billable.*not-billable", params: func(t *testing.T) (cmdutil.Factory, report) { return mocks.NewMockFactory(t), nil }, }, { name: "active or archived", args: []string{"--active", "--archived", "cli"}, err: "flags can't be used together.*active.*archived", params: func(t *testing.T) (cmdutil.Factory, report) { return mocks.NewMockFactory(t), nil }, }, { name: "client and no client", args: []string{"--client=myself", "--no-client", "cli"}, err: "flags can't be used together.*client.*no-client", params: func(t *testing.T) (cmdutil.Factory, report) { return mocks.NewMockFactory(t), nil }, }, { name: "public or private", args: []string{"--private", "--public", "cli"}, err: "flags can't be used together.*private.*public", params: func(t *testing.T) (cmdutil.Factory, report) { return mocks.NewMockFactory(t), nil }, }, { name: "workspace error", err: "error", args: []string{"cli"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID").Return("", errors.New("error")) return f, nil }, }, { name: "client error", err: "error", args: []string{"cli"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID").Return("w", nil) f.On("Client").Return(nil, errors.New("error")) return f, nil }, }, { name: "lookup project error", err: "No project with id or name", args: []string{"cli", "second"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID").Return("w", nil) cf := &mocks.SimpleConfig{AllowNameForID: true} f.On("Config").Return(cf) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Project{}, nil) return f, nil }, }, { name: "fail to update second", args: []string{"cli", "second", "--public", "--billable", "--archived", "--no-client"}, err: "error", params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID").Return("w", nil) cf := &mocks.SimpleConfig{} f.On("Config").Return(cf) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) b := true client := "" c.On("UpdateProject", api.UpdateProjectParam{ Workspace: "w", ProjectID: "cli", ClientId: &client, Public: &b, Billable: &b, Archived: &b, }).Return(dto.Project{}, nil) c.On("UpdateProject", api.UpdateProjectParam{ Workspace: "w", ProjectID: "second", ClientId: &client, Public: &b, Billable: &b, Archived: &b, }).Return(dto.Project{}, errors.New("error")) return f, nil }, }, { name: "update projects", args: []string{"cli", "second", "--private", "--not-billable", "--active", "--note=active, but not billable", "--client=myself"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID").Return("w", nil) cf := &mocks.SimpleConfig{AllowNameForID: true} f.On("Config").Return(cf) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Project{ {ID: "p-1", Name: "Clockify CLI"}, {ID: "p-2", Name: "Second"}, }, nil) c.On("GetClients", api.GetClientsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Client{ {ID: "c-1", Name: "Myself"}, }, nil) b := false client := "c-1" n := "active, but not billable" c.On("UpdateProject", api.UpdateProjectParam{ Workspace: "w", ProjectID: "p-1", ClientId: &client, Public: &b, Billable: &b, Archived: &b, Note: &n, }).Return(dto.Project{ID: "cli"}, nil) c.On("UpdateProject", api.UpdateProjectParam{ Workspace: "w", ProjectID: "p-2", ClientId: &client, Public: &b, Billable: &b, Archived: &b, Note: &n, }).Return(dto.Project{ID: "edit"}, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) return f, func( w io.Writer, of *util.OutputFlags, p []dto.Project) error { called = true assert.Len(t, p, 2) return nil } }, }, { name: "change name and color", args: []string{"first", "--name=First Project", "--client=myself", "--color=0f0"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID").Return("w", nil) cf := &mocks.SimpleConfig{AllowNameForID: true} f.On("Config").Return(cf) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Project{ {ID: "p-1", Name: "First"}, }, nil) c.On("GetClients", api.GetClientsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Client{ {ID: "c-1", Name: "Myself"}, }, nil) client := "c-1" c.On("UpdateProject", api.UpdateProjectParam{ Workspace: "w", ProjectID: "p-1", ClientId: &client, Name: "First Project", Color: "0f0", }).Return(dto.Project{ID: "first"}, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) return f, func( w io.Writer, of *util.OutputFlags, p []dto.Project) error { called = true assert.Len(t, p, 1) return nil } }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f, r := tt.params(t) if r == nil { r = func(io.Writer, *util.OutputFlags, []dto.Project) error { t.Error("should not be called") return nil } } cmd := edit.NewCmdEdit(f, r) cmd.SilenceUsage = true cmd.SetArgs(tt.args) _, err := cmd.ExecuteC() if tt.err == "" { assert.NoError(t, err) return } if !assert.Error(t, err) { return } assert.Regexp(t, tt.err, err.Error()) }) } } func TestEditCmdReport(t *testing.T) { pr := dto.Project{Name: "Coderockr"} tts := []struct { name string args []string assert func(*testing.T, *util.OutputFlags, []dto.Project) }{ { name: "report quiet", args: []string{"-q"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) { assert.True(t, of.Quiet) }, }, { name: "report json", args: []string{"--json"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) { assert.True(t, of.JSON) }, }, { name: "report format", args: []string{"--format={{.ID}}"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) { assert.Equal(t, "{{.ID}}", of.Format) }, }, { name: "report csv", args: []string{"--csv"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) { assert.True(t, of.CSV) }, }, { name: "report default", assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) { assert.False(t, of.CSV) assert.False(t, of.JSON) assert.False(t, of.Quiet) assert.True(t, of.Format == "") }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) f.On("GetWorkspaceID"). Return("w", nil) cf := &mocks.SimpleConfig{AllowNameForID: false} f.On("Config").Return(cf) c.On("UpdateProject", api.UpdateProjectParam{ Workspace: "w", ProjectID: "p-1", Name: "Myself", }). Return(pr, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) cmd := edit.NewCmdEdit(f, func( _ io.Writer, of *util.OutputFlags, u []dto.Project) error { called = true assert.Contains(t, u, pr) tt.assert(t, of, u) return nil }) cmd.SilenceUsage = true cmd.SetArgs(append(tt.args, "-n=Myself", "p-1")) _, err := cmd.ExecuteC() assert.NoError(t, err) }) } } ================================================ FILE: pkg/cmd/project/get/get.go ================================================ package get import ( "errors" "io" "strings" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/util" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/search" "github.com/spf13/cobra" ) // NewCmdGet looks for a project with the informed ID func NewCmdGet( f cmdutil.Factory, report func(io.Writer, *util.OutputFlags, dto.Project) error, ) *cobra.Command { of := util.OutputFlags{} p := api.GetProjectParam{} cmd := &cobra.Command{ Use: "get", Args: cmdutil.RequiredNamedArgs("project"), ValidArgsFunction: cmdcompl.CombineSuggestionsToArgs( cmdcomplutil.NewProjectAutoComplete(f, f.Config())), Short: "Get a project on a Clockify workspace", Example: heredoc.Docf(` $ %[1]s 621948458cb9606d934ebb1c +--------------------------+-------------------+-----------------------------------------+ | ID | NAME | CLIENT | +--------------------------+-------------------+-----------------------------------------+ | 621948458cb9606d934ebb1c | Clockify Cli | Special (6202634a28782767054eec26) | +--------------------------+-------------------+-----------------------------------------+ $ %[1]s cli -q 621948458cb9606d934ebb1c $ %[1]s other --format '{{.Name}} - {{ .Color }} | {{ .ClientID }}' Other - #03A9F4 | 6202634a28782767054eec26 `, "clockify-cli project get"), RunE: func(cmd *cobra.Command, args []string) (err error) { if p.ProjectID = strings.TrimSpace(args[0]); p.ProjectID == "" { return errors.New("project id should not be empty") } if err := of.Check(); err != nil { return err } if p.Workspace, err = f.GetWorkspaceID(); err != nil { return err } c, err := f.Client() if err != nil { return err } if f.Config().IsAllowNameForID() { if p.ProjectID, err = search.GetProjectByName( c, f.Config(), p.Workspace, p.ProjectID, ""); err != nil { return err } } project, err := c.GetProject(p) if err != nil { return err } if project == nil { return api.EntityNotFound{ EntityName: "project", ID: args[0], } } if report != nil { return report(cmd.OutOrStdout(), &of, *project) } return util.ReportOne(*project, cmd.OutOrStdout(), of) }, } cmd.Flags().BoolVarP( &p.Hydrate, "hydrated", "H", false, "projects will have custom fields, tasks and memberships "+ "filled for json and format outputs") util.AddReportFlags(cmd, &of) return cmd } ================================================ FILE: pkg/cmd/project/get/get_test.go ================================================ package get_test import ( "errors" "io" "testing" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/get" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/stretchr/testify/assert" ) func TestCmdGet(t *testing.T) { shouldCall := func(t *testing.T) func( io.Writer, *util.OutputFlags, dto.Project) error { called := false t.Cleanup(func() { assert.True(t, called) }) return func(w io.Writer, of *util.OutputFlags, p dto.Project) error { called = true return nil } } tts := []struct { name string args []string factory func(*testing.T) cmdutil.Factory report func(*testing.T) func( io.Writer, *util.OutputFlags, dto.Project) error err string }{ { name: "only one format", args: []string{"--format={}", "-q", "-j", "p1"}, err: "flags can't be used together.*format.*json.*quiet", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) return f }, }, { name: "workspace error", err: "workspace error", args: []string{"p1"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.EXPECT().GetWorkspaceID(). Return("", errors.New("workspace error")) return f }, }, { name: "client error", err: "client error", args: []string{"p1"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.EXPECT().GetWorkspaceID(). Return("w", nil) f.EXPECT().Client().Return(nil, errors.New("client error")) return f }, }, { name: "http error", err: "http error", args: []string{"p1"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) cf := mocks.NewMockConfig(t) f.EXPECT().Config().Return(cf) cf.EXPECT().IsAllowNameForID().Return(false) c := mocks.NewMockClient(t) f.EXPECT().Client().Return(c, nil) f.EXPECT().GetWorkspaceID().Return("w", nil) c.EXPECT().GetProject(api.GetProjectParam{ Workspace: "w", ProjectID: "p1", }). Return(nil, errors.New("http error")) return f }, }, { name: "by id", args: []string{"p1"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().GetWorkspaceID(). Return("w", nil) cf := mocks.NewMockConfig(t) f.EXPECT().Config().Return(cf) cf.EXPECT().IsAllowNameForID().Return(false) c := mocks.NewMockClient(t) f.EXPECT().Client().Return(c, nil) c.EXPECT().GetProject(api.GetProjectParam{ Workspace: "w", ProjectID: "p1", }). Return(&dto.Project{}, nil) return f }, report: shouldCall, }, { name: "by name", args: []string{ "project", }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().GetWorkspaceID(). Return("w", nil) cf := mocks.NewMockConfig(t) f.EXPECT().Config().Return(cf) cf.EXPECT().IsAllowNameForID().Return(true) cf.EXPECT().IsSearchProjectWithClientsName().Return(true) c := mocks.NewMockClient(t) f.EXPECT().Client().Return(c, nil) c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Project{{Name: "project", ID: "p1"}}, nil) c.EXPECT().GetProject(api.GetProjectParam{ Workspace: "w", ProjectID: "p1", }). Return(&dto.Project{Name: "project", ID: "p1"}, nil) return f }, report: shouldCall, }, { name: "hydrated", args: []string{"-H", "project"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().GetWorkspaceID(). Return("w", nil) cf := mocks.NewMockConfig(t) f.EXPECT().Config().Return(cf) cf.EXPECT().IsAllowNameForID().Return(true) cf.EXPECT().IsSearchProjectWithClientsName().Return(true) c := mocks.NewMockClient(t) f.EXPECT().Client().Return(c, nil) c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Project{{Name: "project", ID: "p1"}}, nil) c.EXPECT().GetProject(api.GetProjectParam{ Workspace: "w", ProjectID: "p1", Hydrate: true, }). Return(&dto.Project{ Name: "project", ID: "p1", Hydrated: true}, nil) return f }, report: shouldCall, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { r := func(io.Writer, *util.OutputFlags, dto.Project) error { assert.Fail(t, "failed") return nil } if tt.report != nil { r = tt.report(t) } cmd := get.NewCmdGet(tt.factory(t), r) cmd.SilenceUsage = true cmd.SetArgs(tt.args) _, err := cmd.ExecuteC() if tt.err == "" { assert.NoError(t, err) return } assert.Error(t, err) assert.Regexp(t, tt.err, err.Error()) }) } } func TestCmdGetReport(t *testing.T) { pr := dto.Project{Name: "Coderockr"} tts := []struct { name string args []string assert func(*testing.T, *util.OutputFlags, dto.Project) }{ { name: "report quiet", args: []string{"-q"}, assert: func(t *testing.T, of *util.OutputFlags, _ dto.Project) { assert.True(t, of.Quiet) }, }, { name: "report json", args: []string{"--json"}, assert: func(t *testing.T, of *util.OutputFlags, _ dto.Project) { assert.True(t, of.JSON) }, }, { name: "report format", args: []string{"--format={{.ID}}"}, assert: func(t *testing.T, of *util.OutputFlags, _ dto.Project) { assert.Equal(t, "{{.ID}}", of.Format) }, }, { name: "report csv", args: []string{"--csv"}, assert: func(t *testing.T, of *util.OutputFlags, _ dto.Project) { assert.True(t, of.CSV) }, }, { name: "report default", assert: func(t *testing.T, of *util.OutputFlags, _ dto.Project) { assert.False(t, of.CSV) assert.False(t, of.JSON) assert.False(t, of.Quiet) assert.True(t, of.Format == "") }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.EXPECT().Client().Return(c, nil) f.EXPECT().GetWorkspaceID().Return("w", nil) cf := mocks.NewMockConfig(t) f.EXPECT().Config().Return(cf) cf.EXPECT().IsAllowNameForID().Return(false) c.EXPECT().GetProject(api.GetProjectParam{ Workspace: "w", ProjectID: "p1", }). Return(&pr, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) cmd := get.NewCmdGet(f, func( _ io.Writer, of *util.OutputFlags, u dto.Project) error { called = true assert.Equal(t, pr, u) tt.assert(t, of, u) return nil }) cmd.SilenceUsage = true cmd.SetArgs(append(tt.args, "p1")) _, err := cmd.ExecuteC() assert.NoError(t, err) }) } } ================================================ FILE: pkg/cmd/project/list/list.go ================================================ package list import ( "io" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/util" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/search" "github.com/spf13/cobra" ) // NewCmdList builds command to list projects func NewCmdList( f cmdutil.Factory, report func(io.Writer, *util.OutputFlags, []dto.Project) error, ) *cobra.Command { of := util.OutputFlags{} p := api.GetProjectsParam{ PaginationParam: api.AllPages(), } var archived, notArchived bool cmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "List projects on a Clockify workspace", Example: heredoc.Docf(` $ %[1]s +--------------------------+-------------------+-----------------------------------------+ | ID | NAME | CLIENT | +--------------------------+-------------------+-----------------------------------------+ | 621948458cb9606d934ebb1c | Clockify Cli | Special (6202634a28782767054eec26) | | 62a8b52d67f40258719037f2 | New One | | | 62a8b59067f40258719038fc | Other | | | 62a8b607027fe4592ef1520b | Other | Uber Special (62964b36bb48532a70730dbe) | | 62894c3ed2df9d2867dc750b | Something Newer | Special (6202634a28782767054eec26) | +--------------------------+-------------------+-----------------------------------------+ $ %[1]s --clients=uber +--------------------------+-------------------+-----------------------------------------+ | ID | NAME | CLIENT | +--------------------------+-------------------+-----------------------------------------+ | 62a8b607027fe4592ef1520b | Other | Uber Special (62964b36bb48532a70730dbe) | +--------------------------+-------------------+-----------------------------------------+ $ %[1]s --clients=uber --clients=special -q 621948458cb9606d934ebb1c 62a8b607027fe4592ef1520b $ %[1]s --name=other --format '{{.Name}} - {{ .Color }} | {{ .ClientID }}' Other - #607D8B | Other - #03A9F4 | 6202634a28782767054eec26 $ %[1]s --archived +--------------------------+-------------------+-----------------------------------------+ | ID | NAME | CLIENT | +--------------------------+-------------------+-----------------------------------------+ | 62894c3ed2df9d2867dc750b | Something Newer | Special (6202634a28782767054eec26) | +--------------------------+-------------------+-----------------------------------------+ `, "clockify-cli project list"), RunE: func(cmd *cobra.Command, args []string) (err error) { if err := of.Check(); err != nil { return err } if err := cmdutil.XorFlag(map[string]bool{ "archived": archived, "not-archived": notArchived, }); err != nil { return err } if p.Workspace, err = f.GetWorkspaceID(); err != nil { return err } c, err := f.Client() if err != nil { return err } if len(p.Clients) > 0 && f.Config().IsAllowNameForID() { if p.Clients, err = search.GetClientsByName( c, p.Workspace, p.Clients); err != nil { return err } } if archived || notArchived { p.Archived = &archived } projects, err := c.GetProjects(p) if err != nil { return err } if report != nil { return report(cmd.OutOrStdout(), &of, projects) } return util.Report(projects, cmd.OutOrStdout(), of) }, } cmd.Flags().StringVarP(&p.Name, "name", "n", "", "will be used to filter the project by name") cmd.Flags().StringSliceVarP(&p.Clients, "clients", "c", []string{}, "will be used to filter the project by client id/name") _ = cmdcompl.AddSuggestionsToFlag(cmd, "clients", cmdcomplutil.NewClientAutoComplete(f)) cmd.Flags().BoolVarP( ¬Archived, "not-archived", "", false, "list only active projects") cmd.Flags().BoolVarP( &archived, "archived", "", false, "list only archived projects") cmd.Flags().BoolVarP( &p.Hydrate, "hydrated", "H", false, "projects will have custom fields, tasks and memberships "+ "filled for json and format outputs") util.AddReportFlags(cmd, &of) return cmd } ================================================ FILE: pkg/cmd/project/list/list_test.go ================================================ package list_test import ( "errors" "io" "testing" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/list" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/stretchr/testify/assert" ) func TestCmdList(t *testing.T) { shouldCall := func(t *testing.T) func( io.Writer, *util.OutputFlags, []dto.Project) error { called := false t.Cleanup(func() { assert.True(t, called) }) return func(w io.Writer, of *util.OutputFlags, p []dto.Project) error { called = true return nil } } tts := []struct { name string args []string factory func(*testing.T) cmdutil.Factory report func(*testing.T) func( io.Writer, *util.OutputFlags, []dto.Project) error err string }{ { name: "only one format", args: []string{"--format={}", "-q", "-j"}, err: "flags can't be used together.*format.*json.*quiet", factory: func(t *testing.T) cmdutil.Factory { return mocks.NewMockFactory(t) }, }, { name: "archived or not-archived", args: []string{"--archived", "--not-archived"}, err: "flags can't be used together.*archived.*not-archived", factory: func(t *testing.T) cmdutil.Factory { return mocks.NewMockFactory(t) }, }, { name: "workspace error", err: "workspace error", args: []string{"-n=a"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().GetWorkspaceID(). Return("", errors.New("workspace error")) return f }, }, { name: "client error", err: "client error", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().GetWorkspaceID(). Return("w", nil) f.EXPECT().Client().Return(nil, errors.New("client error")) return f }, }, { name: "lookup client", err: "no client", args: []string{"--clients=rockr"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.EXPECT().GetWorkspaceID(). Return("w", nil) f.EXPECT().Client().Return(c, nil) f.EXPECT() cf := mocks.NewMockConfig(t) f.EXPECT().Config().Return(cf) cf.EXPECT().IsAllowNameForID().Return(true) c.EXPECT().GetClients(api.GetClientsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Client{}, errors.New("no client")) return f }, }, { name: "client not found", err: "No client with id or name containing 'other' was found", args: []string{"--clients=rockr", "--clients=other"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.EXPECT().GetWorkspaceID(). Return("w", nil) f.EXPECT().Client().Return(c, nil) cf := mocks.NewMockConfig(t) f.EXPECT().Config().Return(cf) cf.EXPECT().IsAllowNameForID().Return(true) c.EXPECT().GetClients(api.GetClientsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Client{{ID: "c1", Name: "Coderockr"}}, nil) return f }, }, { name: "http error", err: "http error", args: []string{"-n=error"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.EXPECT().GetWorkspaceID(). Return("w", nil) f.EXPECT().Client().Return(c, nil) c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: "w", Name: "error", Clients: []string{}, PaginationParam: api.AllPages(), }). Return([]dto.Project{}, errors.New("http error")) return f }, }, { name: "archived", args: []string{ "--name=cli", "--clients=rockr", "--clients", "other", "--archived", }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().GetWorkspaceID(). Return("w", nil) cf := mocks.NewMockConfig(t) f.EXPECT().Config().Return(cf) cf.EXPECT().IsAllowNameForID().Return(true) c := mocks.NewMockClient(t) f.EXPECT().Client().Return(c, nil) c.EXPECT().GetClients(api.GetClientsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Client{ {ID: "c1", Name: "Coderockr"}, {ID: "c2", Name: "Other"}, }, nil) b := true c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: "w", Name: "cli", Clients: []string{"c1", "c2"}, Archived: &b, PaginationParam: api.AllPages(), }). Return([]dto.Project{}, nil) return f }, report: shouldCall, }, { name: "not archived", args: []string{ "--name=cli", "--not-archived", }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().GetWorkspaceID(). Return("w", nil) c := mocks.NewMockClient(t) f.EXPECT().Client().Return(c, nil) b := false c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: "w", Name: "cli", Clients: []string{}, Archived: &b, PaginationParam: api.AllPages(), }). Return([]dto.Project{}, nil) return f }, report: shouldCall, }, { name: "hydrated", args: []string{ "--hydrated", }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().GetWorkspaceID(). Return("w", nil) c := mocks.NewMockClient(t) f.EXPECT().Client().Return(c, nil) c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: "w", Clients: []string{}, Hydrate: true, PaginationParam: api.AllPages(), }). Return([]dto.Project{}, nil) return f }, report: shouldCall, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { r := func(io.Writer, *util.OutputFlags, []dto.Project) error { assert.Fail(t, "failed") return nil } if tt.report != nil { r = tt.report(t) } cmd := list.NewCmdList(tt.factory(t), r) cmd.SilenceUsage = true cmd.SetArgs(tt.args) _, err := cmd.ExecuteC() if tt.err == "" { assert.NoError(t, err) return } assert.Error(t, err) assert.Regexp(t, tt.err, err.Error()) }) } } func TestCmdListReport(t *testing.T) { pr := []dto.Project{{Name: "Coderockr"}} tts := []struct { name string args []string assert func(*testing.T, *util.OutputFlags, []dto.Project) }{ { name: "report quiet", args: []string{"-q"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) { assert.True(t, of.Quiet) }, }, { name: "report json", args: []string{"--json"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) { assert.True(t, of.JSON) }, }, { name: "report format", args: []string{"--format={{.ID}}"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) { assert.Equal(t, "{{.ID}}", of.Format) }, }, { name: "report csv", args: []string{"--csv"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) { assert.True(t, of.CSV) }, }, { name: "report default", assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) { assert.False(t, of.CSV) assert.False(t, of.JSON) assert.False(t, of.Quiet) assert.True(t, of.Format == "") }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.EXPECT().Client().Return(c, nil) f.EXPECT().GetWorkspaceID(). Return("w", nil) c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: "w", Clients: []string{}, PaginationParam: api.AllPages(), }). Return(pr, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) cmd := list.NewCmdList(f, func( _ io.Writer, of *util.OutputFlags, u []dto.Project) error { called = true assert.Equal(t, pr, u) tt.assert(t, of, u) return nil }) cmd.SilenceUsage = true cmd.SetArgs(tt.args) _, err := cmd.ExecuteC() assert.NoError(t, err) }) } } ================================================ FILE: pkg/cmd/project/project.go ================================================ package project import ( "github.com/lucassabreu/clockify-cli/pkg/cmd/project/add" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/edit" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/get" "github.com/lucassabreu/clockify-cli/pkg/cmd/project/list" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/spf13/cobra" ) // NewCmdProject represents the project command func NewCmdProject(f cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "project", Aliases: []string{"projects"}, Short: "Work with Clockify projects", } cmd.AddCommand(list.NewCmdList(f, nil)) cmd.AddCommand(get.NewCmdGet(f, nil)) cmd.AddCommand(add.NewCmdAdd(f, nil)) cmd.AddCommand(edit.NewCmdEdit(f, nil)) return cmd } ================================================ FILE: pkg/cmd/project/util/util.go ================================================ package util import ( "io" "os" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/output/project" "github.com/spf13/cobra" ) // OutputFlags defines how to print the project type OutputFlags struct { JSON bool CSV bool Quiet bool Format string } func (of OutputFlags) Check() error { return cmdutil.XorFlag(map[string]bool{ "format": of.Format != "", "json": of.JSON, "csv": of.CSV, "quiet": of.Quiet, }) } // AddReportFlags adds the common flags to print projects func AddReportFlags(cmd *cobra.Command, of *OutputFlags) { cmd.Flags().StringVarP(&of.Format, "format", "f", "", "golang text/template format to be applied on each Project") cmd.Flags().BoolVarP(&of.JSON, "json", "j", false, "print as JSON") cmd.Flags().BoolVarP(&of.CSV, "csv", "v", false, "print as CSV") cmd.Flags().BoolVarP(&of.Quiet, "quiet", "q", false, "only display ids") } // Report will print the projects as set by the flags func Report(list []dto.Project, out io.Writer, f OutputFlags) error { switch { case f.JSON: return project.ProjectsJSONPrint(list, out) case f.CSV: return project.ProjectsCSVPrint(list, out) case f.Quiet: return project.ProjectPrintQuietly(list, out) case f.Format != "": return project.ProjectPrintWithTemplate(f.Format)(list, out) default: return project.ProjectPrint(list, os.Stdout) } } // ReportOne will print a project as set by the flags func ReportOne(p dto.Project, out io.Writer, f OutputFlags) error { switch { case f.JSON: return project.ProjectJSONPrint(p, out) default: return Report([]dto.Project{p}, os.Stdout, f) } } ================================================ FILE: pkg/cmd/root.go ================================================ package cmd import ( "github.com/lucassabreu/clockify-cli/pkg/cmd/client" "github.com/lucassabreu/clockify-cli/pkg/cmd/completion" "github.com/lucassabreu/clockify-cli/pkg/cmd/config" "github.com/lucassabreu/clockify-cli/pkg/cmd/project" "github.com/lucassabreu/clockify-cli/pkg/cmd/tag" "github.com/lucassabreu/clockify-cli/pkg/cmd/task" timeentry "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry" "github.com/lucassabreu/clockify-cli/pkg/cmd/user" "github.com/lucassabreu/clockify-cli/pkg/cmd/user/me" "github.com/lucassabreu/clockify-cli/pkg/cmd/version" "github.com/lucassabreu/clockify-cli/pkg/cmd/workspace" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/spf13/cobra" ) // NewCmdRoot creates the base command when called without any subcommands func NewCmdRoot(f cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "clockify-cli", Short: "Allow to integrate with Clockify through terminal", SilenceErrors: true, SilenceUsage: true, } cmd.PersistentFlags().StringP("token", "t", "", "clockify's token\nCan be generated here: "+ "https://clockify.me/user/settings#generateApiKeyBtn") cmd.PersistentFlags().StringP("workspace", "w", "", "workspace to be used") _ = cmdcompl.AddSuggestionsToFlag(cmd, "workspace", cmdcomplutil.NewWorspaceAutoComplete(f)) cmd.PersistentFlags().StringP("user-id", "u", "", "user id from the token") _ = cmdcompl.AddSuggestionsToFlag(cmd, "user-id", cmdcomplutil.NewUserAutoComplete(f)) cmd.PersistentFlags().BoolP("interactive", "i", false, "will prompt you to confirm/complement commands input before "+ "executing the action ") cmd.PersistentFlags().IntP("interactive-page-size", "L", 7, "will set how many items will be shown on interactive mode") cmd.PersistentFlags().BoolP("allow-name-for-id", "", false, "allow use of project/client/tag's name when id is asked") cmd.PersistentFlags().String( "log-level", cmdutil.LOG_LEVEL_NONE, "set log level") _ = cmdcompl.AddFixedSuggestionsToFlag(cmd, "log-level", cmdcompl.ValidArgsSlide{ cmdutil.LOG_LEVEL_NONE, cmdutil.LOG_LEVEL_DEBUG, cmdutil.LOG_LEVEL_INFO, }) _ = cmd.MarkFlagRequired("token") cmd.AddCommand(version.NewCmdVersion(f)) cmd.AddCommand(config.NewCmdConfig(f)) cmd.AddCommand(workspace.NewCmdWorkspace(f)) cmd.AddCommand(user.NewCmdUser(f, nil)) cmd.AddCommand(me.NewCmdMe(f, nil)) cmd.AddCommand(client.NewCmdClient(f)) cmd.AddCommand(project.NewCmdProject(f)) cmd.AddCommand(task.NewCmdTask(f)) cmd.AddCommand(tag.NewCmdTag(f)) cmd.AddCommand(timeentry.NewCmdTimeEntry(f)...) cmd.AddCommand(completion.NewCmdCompletion()) return cmd } ================================================ FILE: pkg/cmd/tag/tag.go ================================================ package tag import ( "os" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" output "github.com/lucassabreu/clockify-cli/pkg/output/tag" "github.com/spf13/cobra" ) // NewCmdTag represents the tags command func NewCmdTag(f cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "tag", Aliases: []string{"tags"}, Short: "List tags on Clockify", Example: heredoc.Docf(` $ %[1]s +--------------------------+------------------+ | ID | NAME | +--------------------------+------------------+ | 62194867edaba27d0a45b464 | Code Review | | 6219485e8cb9606d934ebb5f | Meeting | | 621948708cb9606d934ebba7 | Pair Programming | | 6143b768195e5c503960a775 | Special Tag | +--------------------------+------------------+ $ %[1]s --name code -q 62194867edaba27d0a45b464 $ %[1]s --format "{{.Name}}" -archived Archived Tag `, "clockify-cli tag"), RunE: func(cmd *cobra.Command, args []string) error { format, _ := cmd.Flags().GetString("format") quiet, _ := cmd.Flags().GetBool("quiet") if err := cmdutil.XorFlag(map[string]bool{ "format": format != "", "quiet": quiet, }); err != nil { return err } archived, _ := cmd.Flags().GetBool("archived") name, _ := cmd.Flags().GetString("name") tags, err := getTags(f, name, archived) if err != nil { return err } out := cmd.OutOrStdout() if format != "" { return output.TagPrintWithTemplate(format)(tags, out) } if quiet { return output.TagPrintQuietly(tags, out) } return output.TagPrint(tags, os.Stdout) }, } cmd.Flags().StringP("name", "n", "", "will be used to filter the tag by name") cmd.Flags().StringP("format", "f", "", "golang text/template format to be applied on each Tag") cmd.Flags().BoolP("quiet", "q", false, "only display ids") cmd.Flags().BoolP("archived", "", false, "only display archived tags") return cmd } func getTags(f cmdutil.Factory, name string, archived bool) ([]dto.Tag, error) { c, err := f.Client() if err != nil { return []dto.Tag{}, err } w, err := f.GetWorkspaceID() if err != nil { return []dto.Tag{}, err } return c.GetTags(api.GetTagsParam{ Workspace: w, Name: name, Archived: &archived, PaginationParam: api.AllPages(), }) } ================================================ FILE: pkg/cmd/task/add/add.go ================================================ package add import ( "io" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/spf13/cobra" ) // NewCmdAdd represents the add command func NewCmdAdd( f cmdutil.Factory, report func(io.Writer, *util.OutputFlags, dto.Task) error, ) *cobra.Command { of := util.OutputFlags{} cmd := &cobra.Command{ Use: "add", Short: "Adds a new task to a project on Clockify", Long: heredoc.Doc(` Adds a new active task to a project on Clockify, also allows to assign users to it at the same time Tasks will be created as billable or not depending on the project settings. If you set a estimate for the task, but the project is set as manual estimation, then it will have no effect on Clockify. `), Example: heredoc.Docf(` $ %[1]s -p special --name="Very Important" +--------------------------+----------------+--------+ | ID | NAME | STATUS | +--------------------------+----------------+--------+ | 62aa5d7049445270d7b979d6 | Very Important | ACTIVE | +--------------------------+----------------+--------+ $ %[1]s -p special --name="Very Cool" --assign john@example.com | \ jq '.[] |.assigneeIds' --compact-output ["dddddddddddddddddddddddd"] $ %[1]s -p special --name Billable --billable --quiet 62ab129e4ebb4f143c8e8622 $ %[1]s -p special --name "Not Billable" --not-billable --csv id,name,status 62ab145ec22de9759e6f6e36,Not Billable,ACTIVE $ %[1]s -p special --name 'With 1H to Make' --estimate 1 +--------------------------+-----------------+--------+ | ID | NAME | STATUS | +--------------------------+-----------------+--------+ | 62aa5d7049445270d7b979d6 | With 1H to Make | ACTIVE | +--------------------------+-----------------+--------+ `, "clockify-cli task add"), RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } fl, err := util.TaskReadFlags(cmd, f) if err != nil { return err } c, err := f.Client() if err != nil { return err } task, err := c.AddTask(api.AddTaskParam{ Workspace: fl.Workspace, ProjectID: fl.ProjectID, Name: fl.Name, Estimate: fl.Estimate, AssigneeIDs: fl.AssigneeIDs, Billable: fl.Billable, }) if err != nil { return err } if report != nil { return report(cmd.OutOrStdout(), &of, task) } return util.TaskReport(cmd, of, task) }, } util.TaskAddReportFlags(cmd, &of) util.TaskAddPropFlags(cmd, f) _ = cmd.MarkFlagRequired("name") return cmd } ================================================ FILE: pkg/cmd/task/add/add_test.go ================================================ package add_test import ( "errors" "io" "testing" "time" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/add" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/stretchr/testify/assert" ) func TestCmdAdd(t *testing.T) { shouldCall := func(t *testing.T) func( io.Writer, *util.OutputFlags, dto.Task) error { called := false t.Cleanup(func() { assert.True(t, called) }) return func( w io.Writer, of *util.OutputFlags, tk dto.Task) error { called = true assert.Equal(t, "t-id", tk.ID) return nil } } dFactory := func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) return f } tts := []struct { name string args []string factory func(*testing.T) cmdutil.Factory report func(*testing.T) func( io.Writer, *util.OutputFlags, dto.Task) error err string }{ { name: "only one format", args: []string{"--format={}", "-q", "-j", "-n=OK", "-p=OK"}, err: "flags can't be used together.*format.*json.*quiet", factory: dFactory, }, { name: "billable or not", args: []string{"--billable", "--not-billable", "-n=OK", "-p=OK"}, err: "flags can't be used together.*billable.*not-billable", factory: dFactory, }, { name: "assignee or no assignee", args: []string{"--assignee=l", "--no-assignee", "-n=OK", "-p=OK"}, err: "flags can't be used together.*assignee.*no-assignee", factory: dFactory, }, { name: "name required", args: []string{"-p=OK"}, err: `"name" not set`, factory: dFactory, }, { name: "project required", args: []string{"-n=OK"}, err: `"project" not set`, factory: dFactory, }, { name: "client error", err: "client error", args: []string{"-n=a", "-p=b"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID"). Return("w", nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.On("Client").Return(nil, errors.New("client error")) return f }, }, { name: "workspace error", err: "workspace error", args: []string{"-n=a", "-p=b"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.On("GetWorkspaceID"). Return("", errors.New("workspace error")) return f }, }, { name: "lookup project", err: "no project", args: []string{"-n=error", "-p=cli"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, }) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Project{}, errors.New("no project")) return f }, }, { name: "lookup user", err: "no user", args: []string{"-n=error", "-p=cli", "-A=who"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, }) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Project{{Name: "Cli"}}, nil) c.On("WorkspaceUsers", api.WorkspaceUsersParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.User{}, errors.New("no user")) return f }, }, { name: "http error", err: "http error", args: []string{"-n=error", "-p=ok"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) c.On("AddTask", api.AddTaskParam{ Workspace: "w", ProjectID: "ok", Name: "error", }). Return(dto.Task{}, errors.New("http error")) return f }, }, { name: "add billable task", args: []string{ "--name=Add", "--project=cli", "--billable", "--estimate", "32", }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, }) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return( []dto.Project{{ID: "p-1", Name: "Clockify CLI"}}, nil) b := true e := time.Hour * 32 c.On("AddTask", api.AddTaskParam{ Workspace: "w", Name: "Add", ProjectID: "p-1", Billable: &b, Estimate: &e, }). Return(dto.Task{ID: "t-id"}, nil) return f }, report: shouldCall, }, { name: "add non-billable task", args: []string{ "-n", "Add Task", "--project=p-1", "--assignee", "lucas", "--assignee=john", "--not-billable", }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, }) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return( []dto.Project{{ID: "p-1", Name: "Clockify CLI"}}, nil) c.On("WorkspaceUsers", api.WorkspaceUsersParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return( []dto.User{ {ID: "u-1", Name: "Lucas Abreu"}, {ID: "u-2", Name: "John Due"}, }, nil) b := false as := []string{"u-1", "u-2"} c.On("AddTask", api.AddTaskParam{ Workspace: "w", Name: "Add Task", ProjectID: "p-1", AssigneeIDs: &as, Billable: &b, }). Return(dto.Task{ID: "t-id"}, nil) return f }, report: shouldCall, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { r := func(io.Writer, *util.OutputFlags, dto.Task) error { assert.Fail(t, "failed") return nil } if tt.report != nil { r = tt.report(t) } cmd := add.NewCmdAdd(tt.factory(t), r) cmd.SilenceUsage = true cmd.SetArgs(tt.args) _, err := cmd.ExecuteC() if tt.err == "" { assert.NoError(t, err) return } assert.Error(t, err) assert.Regexp(t, tt.err, err.Error()) }) } } func TestCmdAddReport(t *testing.T) { pr := dto.Task{Name: "Coderockr"} tts := []struct { name string args []string assert func(*testing.T, *util.OutputFlags, dto.Task) }{ { name: "report quiet", args: []string{"-q"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Task) { assert.True(t, of.Quiet) }, }, { name: "report json", args: []string{"--json"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Task) { assert.True(t, of.JSON) }, }, { name: "report format", args: []string{"--format={{.ID}}"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Task) { assert.Equal(t, "{{.ID}}", of.Format) }, }, { name: "report csv", args: []string{"--csv"}, assert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) { assert.True(t, of.CSV) }, }, { name: "report default", assert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) { assert.False(t, of.CSV) assert.False(t, of.JSON) assert.False(t, of.Quiet) assert.True(t, of.Format == "") }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) f.On("GetWorkspaceID"). Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(false) c.On("AddTask", api.AddTaskParam{ Workspace: "w", ProjectID: "p-1", Name: "Task Add", }). Return(pr, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) cmd := add.NewCmdAdd(f, func( _ io.Writer, of *util.OutputFlags, u dto.Task) error { called = true assert.Equal(t, pr, u) tt.assert(t, of, u) return nil }) cmd.SilenceUsage = true cmd.SetArgs(append(tt.args, "-n", "Task Add", "-p=p-1")) _, err := cmd.ExecuteC() assert.NoError(t, err) }) } } ================================================ FILE: pkg/cmd/task/delete/delete.go ================================================ package del import ( "errors" "io" "strings" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/util" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/search" "github.com/spf13/cobra" ) // NewCmdDelete represents the close command func NewCmdDelete( f cmdutil.Factory, report func(io.Writer, *util.OutputFlags, dto.Task) error, ) *cobra.Command { of := util.OutputFlags{} cmd := &cobra.Command{ Use: "delete ", Aliases: []string{"remove", "rm", "del"}, Args: cmdutil.RequiredNamedArgs("task"), ValidArgsFunction: cmdcompl.CombineSuggestionsToArgs( cmdcomplutil.NewTaskAutoComplete(f, false)), Short: "Deletes a task from a project on Clockify", Long: heredoc.Doc(` Deletes a task from a project on Clockify This action can't be reverted, and all time entries using this task will revert to not having one `), Example: heredoc.Doc(` $ clockify-cli task delete -p "special" very +--------------------------+----------------+--------+ | ID | NAME | STATUS | +--------------------------+----------------+--------+ | 62aa5d7049445270d7b979d6 | Very Important | ACTIVE | +--------------------------+----------------+--------+ $ clockify-cli task delete -p "special" 62aa4eed49445270d7b9666c +--------------------------+----------+--------+ | ID | NAME | STATUS | +--------------------------+----------+--------+ | 62aa4eed49445270d7b9666c | Inactive | DONE | +--------------------------+----------+--------+ $ clockify-cli task delete -p "special" 62aa4eed49445270d7b9666c No task with id or name containing '62aa4eed49445270d7b9666c' was found `), RunE: func(cmd *cobra.Command, args []string) error { project, _ := cmd.Flags().GetString("project") task := strings.TrimSpace(args[0]) if project == "" || task == "" { return errors.New("project and task id should not be empty") } w, err := f.GetWorkspaceID() if err != nil { return err } c, err := f.Client() if err != nil { return err } if f.Config().IsAllowNameForID() { if project, err = search.GetProjectByName( c, f.Config(), w, project, ""); err != nil { return err } if task, err = search.GetTaskByName( c, api.GetTasksParam{Workspace: w, ProjectID: project}, task, ); err != nil { return err } } t, err := c.DeleteTask(api.DeleteTaskParam{ Workspace: w, ProjectID: project, TaskID: task, }) if err != nil { return err } if report == nil { return util.TaskReport(cmd, of, t) } return report(cmd.OutOrStdout(), &of, t) }, } cmdutil.AddProjectFlags(cmd, f) util.TaskAddReportFlags(cmd, &of) return cmd } ================================================ FILE: pkg/cmd/task/delete/delete_test.go ================================================ package del_test import ( "errors" "io" "testing" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" del "github.com/lucassabreu/clockify-cli/pkg/cmd/task/delete" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/stretchr/testify/assert" ) type report func(io.Writer, *util.OutputFlags, dto.Task) error func TestCmdDelete(t *testing.T) { tts := []struct { name string err string args []string params func(*testing.T) ( cmdutil.Factory, report, ) }{ { name: "task is required", args: []string{}, err: "requires arg task", params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) return f, nil }, }, { name: "project is required", err: "flag.*project.*not set", args: []string{"task-id"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) return f, nil }, }, { name: "workspace error", err: "w error", args: []string{"task-id", "-p", "p-1"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.On("GetWorkspaceID").Return("", errors.New("w error")) return f, nil }, }, { name: "client error", err: "c error", args: []string{"task-id", "-p", "p-1"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.On("GetWorkspaceID").Return("w", nil) f.On("Client").Return(nil, errors.New("c error")) return f, nil }, }, { name: "project lookup error", err: "No project with id or name containing.*p-1", args: []string{"task-id", "-p", "p-1"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID").Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) cf.On("IsSearchProjectWithClientsName").Return(true) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Project{}, nil) return f, nil }, }, { name: "task lookup error", err: "No task with id or name containing.*task-id", args: []string{"task-id", "-p", "p-1"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID").Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) cf.On("IsSearchProjectWithClientsName").Return(true) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Project{{ID: "p-1"}}, nil) c.On("GetTasks", api.GetTasksParam{ Workspace: "w", ProjectID: "p-1", PaginationParam: api.AllPages(), }).Return([]dto.Task{}, nil) return f, nil }, }, { name: "task delete error", err: "http error", args: []string{"delete", "-p", "p-1"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID").Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) cf.On("IsSearchProjectWithClientsName").Return(true) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Project{{ID: "p-1"}}, nil) te := dto.Task{ ID: "task-id", Name: "Delete Task", ProjectID: "p-1", } c.On("GetTasks", api.GetTasksParam{ Workspace: "w", ProjectID: "p-1", PaginationParam: api.AllPages(), }).Return([]dto.Task{te}, nil) c.On("DeleteTask", api.DeleteTaskParam{ Workspace: "w", ProjectID: "p-1", TaskID: "task-id", }).Return(te, errors.New("http error")) return f, nil }, }, { name: "task delete ", args: []string{"delete", "-p", "p-1"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID").Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) cf.On("IsSearchProjectWithClientsName").Return(true) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Project{{ID: "p-1"}}, nil) te := dto.Task{ ID: "task-id", Name: "Delete Task", ProjectID: "p-1", } c.On("GetTasks", api.GetTasksParam{ Workspace: "w", ProjectID: "p-1", PaginationParam: api.AllPages(), }).Return([]dto.Task{te}, nil) c.On("DeleteTask", api.DeleteTaskParam{ Workspace: "w", ProjectID: "p-1", TaskID: "task-id", }).Return(te, nil) called := false t.Cleanup(func() { assert.True(t, called) }) return f, func( _ io.Writer, _ *util.OutputFlags, tr dto.Task) error { called = true assert.Equal(t, te, tr) return nil } }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f, r := tt.params(t) if r == nil { r = func(w io.Writer, of *util.OutputFlags, _ dto.Task) error { t.Error("should not be called") return nil } } cmd := del.NewCmdDelete(f, r) cmd.SilenceUsage = true cmd.SetArgs(tt.args) _, err := cmd.ExecuteC() if tt.err == "" { assert.NoError(t, err) return } assert.Error(t, err) assert.Regexp(t, tt.err, err.Error()) }) } } func TestCmdDeleteReport(t *testing.T) { pr := dto.Task{Name: "Coderockr"} tts := []struct { name string args []string assert func(*testing.T, *util.OutputFlags, dto.Task) }{ { name: "report quiet", args: []string{"-q"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Task) { assert.True(t, of.Quiet) }, }, { name: "report json", args: []string{"--json"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Task) { assert.True(t, of.JSON) }, }, { name: "report format", args: []string{"--format={{.ID}}"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Task) { assert.Equal(t, "{{.ID}}", of.Format) }, }, { name: "report csv", args: []string{"--csv"}, assert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) { assert.True(t, of.CSV) }, }, { name: "report default", assert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) { assert.False(t, of.CSV) assert.False(t, of.JSON) assert.False(t, of.Quiet) assert.True(t, of.Format == "") }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) f.On("GetWorkspaceID"). Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(false) c.On("DeleteTask", api.DeleteTaskParam{ Workspace: "w", ProjectID: "p-1", TaskID: "t-1", }). Return(pr, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) cmd := del.NewCmdDelete(f, func( _ io.Writer, of *util.OutputFlags, u dto.Task) error { called = true assert.Equal(t, pr, u) tt.assert(t, of, u) return nil }) cmd.SilenceUsage = true cmd.SetArgs(append(tt.args, "t-1", "-p=p-1")) _, err := cmd.ExecuteC() assert.NoError(t, err) }) } } ================================================ FILE: pkg/cmd/task/done/done.go ================================================ package done import ( "errors" "io" "strings" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/util" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/search" "github.com/lucassabreu/clockify-cli/strhlp" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" ) // NewCmdDone represents the close command func NewCmdDone( f cmdutil.Factory, report func(io.Writer, *util.OutputFlags, []dto.Task) error, ) *cobra.Command { of := util.OutputFlags{} cmd := &cobra.Command{ Use: "done ...", Aliases: []string{"mark-as-done", "end"}, Args: cmdutil.RequiredNamedArgs("task"), ValidArgsFunction: cmdcompl.CombineSuggestionsToArgs( cmdcomplutil.NewTaskAutoComplete(f, true)), Short: "Edits a task to done", Long: "Edits a task to done, similar to doing `task edit --done`", Example: heredoc.Docf(` $ %[1]s ls +--------------------------+--------+--------+ | ID | NAME | STATUS | +--------------------------+--------+--------+ | 62adfcc8c22de9759e739d66 | Five | ACTIVE | | 62adfcc4c22de9759e739d64 | Four | ACTIVE | | 62adfcb649445270d7becfca | Three | ACTIVE | | 62adfcb149445270d7becfc8 | Second | ACTIVE | | 62adfcaa4ebb4f143c92bf8b | First | ACTIVE | +--------------------------+--------+--------+ $ %[1]s done first second 62adfcb649445270d7becfca +--------------------------+--------+--------+ | ID | NAME | STATUS | +--------------------------+--------+--------+ | 62adfcaa4ebb4f143c92bf8b | First | DONE | | 62adfcb149445270d7becfc8 | Second | DONE | | 62adfcb649445270d7becfca | Three | DONE | +--------------------------+--------+--------+ $ %[1]s done four id,name,status 62adfcc4c22de9759e739d64,Four,DONE $ %[1]s done five No active task with id or name containing 'five' was found `, "clockify-cli task -p cli"), RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } project, _ := cmd.Flags().GetString("project") project = strings.TrimSpace(project) if project == "" { return errors.New("project should not be empty") } ids := strhlp.Map(strings.TrimSpace, args) if strhlp.Search("", ids) != -1 { return errors.New("task id/name should not be empty") } workspace, err := f.GetWorkspaceID() if err != nil { return err } c, err := f.Client() if err != nil { return err } if f.Config().IsAllowNameForID() { if project, err = search.GetProjectByName( c, f.Config(), workspace, project, ""); err != nil { return err } if ids, err = search.GetTasksByName( c, api.GetTasksParam{ Workspace: workspace, ProjectID: project, Active: true, }, ids, ); err != nil { var errNF search.ErrNotFound if errors.As(err, &errNF) { return errors.New( "No active task with id or name containing '" + errNF.Reference + "' was found") } return err } } tasks := make([]dto.Task, len(ids)) var g errgroup.Group for i := 0; i < len(ids); i++ { j := i g.Go(func() error { t, err := c.GetTask(api.GetTaskParam{ Workspace: workspace, ProjectID: project, TaskID: ids[j], }) if err != nil { return err } tasks[j], err = c.UpdateTask(api.UpdateTaskParam{ Workspace: workspace, ProjectID: t.ProjectID, TaskID: t.ID, Name: t.Name, Status: api.TaskStatusDone, }) return err }) } if err := g.Wait(); err != nil { return err } if report == nil { return util.TaskReport(cmd, of, tasks...) } return report(cmd.OutOrStdout(), &of, tasks) }, } cmdutil.AddProjectFlags(cmd, f) util.TaskAddReportFlags(cmd, &of) return cmd } ================================================ FILE: pkg/cmd/task/done/done_test.go ================================================ package done_test import ( "errors" "io" "testing" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/done" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/stretchr/testify/assert" ) func TestCmdDone(t *testing.T) { te := dto.Task{ID: "task-id", Name: "Task with ID", ProjectID: "p-1"} shouldCall := func(t *testing.T) func( io.Writer, *util.OutputFlags, []dto.Task) error { called := false t.Cleanup(func() { assert.True(t, called) }) return func( w io.Writer, of *util.OutputFlags, tr []dto.Task) error { called = true assert.Len(t, tr, 1) assert.Equal(t, te, tr[0]) return nil } } dFactory := func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) return f } tts := []struct { name string args []string factory func(*testing.T) cmdutil.Factory report func(*testing.T) func( io.Writer, *util.OutputFlags, []dto.Task) error err string }{ { name: "task id required", args: []string{"-p=cli"}, err: `requires arg task`, factory: dFactory, }, { name: "project required", args: []string{"task"}, err: `"project" not set`, factory: dFactory, }, { name: "project not empty", args: []string{"task", "-p= "}, err: `project should not be empty`, factory: dFactory, }, { name: "task id not empty", args: []string{" ", "-p=cli"}, err: `task id/name should not be empty`, factory: dFactory, }, { name: "task id not empty (nice try)", args: []string{"not-empty", " ", "-p=cli"}, err: `task id/name should not be empty`, factory: dFactory, }, { name: "only one format", args: []string{"--format={}", "-q", "-j", "-p=OK", "done"}, err: "flags can't be used together.*format.*json.*quiet", factory: dFactory, }, { name: "client error", err: "client error", args: []string{"done", "-p=b"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(nil, errors.New("client error")) return f }, }, { name: "workspace error", err: "workspace error", args: []string{"done", "-p=b"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.On("GetWorkspaceID"). Return("", errors.New("workspace error")) return f }, }, { name: "lookup project", err: "no project", args: []string{"done", "-p=cli"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Project{}, errors.New("no project")) return f }, }, { name: "cant find task", err: "No active task with id or name.*done", args: []string{"done", "-p=p-1"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) cf.On("IsSearchProjectWithClientsName").Return(true) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Project{{ID: "p-1"}}, nil) c.On("GetTasks", api.GetTasksParam{ Workspace: "w", ProjectID: "p-1", Active: true, PaginationParam: api.AllPages(), }). Return([]dto.Task{}, nil) return f }, }, { name: "fail to find", err: "something went wrong", args: []string{"task 1", "task 2", "-p=cli"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) cf.On("IsSearchProjectWithClientsName").Return(true) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Project{{ID: "p-1", Name: "Cli"}}, nil) ts := []dto.Task{ {ID: "t-1", ProjectID: "p-1", Name: "Task 1"}, {ID: "t-2", ProjectID: "p-1", Name: "Task 2"}, } c.On("GetTasks", api.GetTasksParam{ Workspace: "w", ProjectID: "p-1", Active: true, PaginationParam: api.AllPages(), }). Return(ts, nil) c.On("GetTask", api.GetTaskParam{ Workspace: "w", ProjectID: ts[0].ProjectID, TaskID: ts[0].ID, }). Return(ts[0], nil) c.On("UpdateTask", api.UpdateTaskParam{ Workspace: "w", ProjectID: ts[0].ProjectID, TaskID: ts[0].ID, Name: ts[0].Name, Status: api.TaskStatusDone, }). Return(ts[0], nil) c.On("GetTask", api.GetTaskParam{ Workspace: "w", ProjectID: ts[1].ProjectID, TaskID: ts[1].ID, }). Return(ts[1], errors.New("something went wrong")) return f }, }, { name: "fail second update", err: "something went wrong", args: []string{"task 1", "task 2", "-p=cli"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) cf.On("IsSearchProjectWithClientsName").Return(true) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Project{{ID: "p-1", Name: "Cli"}}, nil) ts := []dto.Task{ {ID: "t-1", ProjectID: "p-1", Name: "Task 1"}, {ID: "t-2", ProjectID: "p-1", Name: "Task 2"}, } c.On("GetTasks", api.GetTasksParam{ Workspace: "w", ProjectID: "p-1", Active: true, PaginationParam: api.AllPages(), }). Return(ts, nil) c.On("GetTask", api.GetTaskParam{ Workspace: "w", ProjectID: ts[0].ProjectID, TaskID: ts[0].ID, }). Return(ts[0], nil) c.On("UpdateTask", api.UpdateTaskParam{ Workspace: "w", ProjectID: ts[0].ProjectID, TaskID: ts[0].ID, Name: ts[0].Name, Status: api.TaskStatusDone, }). Return(ts[0], nil) c.On("GetTask", api.GetTaskParam{ Workspace: "w", ProjectID: ts[1].ProjectID, TaskID: ts[1].ID, }). Return(ts[1], nil) c.On("UpdateTask", api.UpdateTaskParam{ Workspace: "w", ProjectID: ts[1].ProjectID, TaskID: ts[1].ID, Name: ts[1].Name, Status: api.TaskStatusDone, }). Return(ts[0], errors.New("something went wrong")) return f }, }, { name: "done", args: []string{te.ID, "-p=p-1"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(false) c.On("GetTask", api.GetTaskParam{ Workspace: "w", ProjectID: te.ProjectID, TaskID: te.ID, }). Return(te, nil) c.On("UpdateTask", api.UpdateTaskParam{ Workspace: "w", ProjectID: te.ProjectID, TaskID: te.ID, Name: te.Name, Status: api.TaskStatusDone, }). Return(te, nil) return f }, report: shouldCall, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { r := func(io.Writer, *util.OutputFlags, []dto.Task) error { assert.Fail(t, "failed") return nil } if tt.report != nil { r = tt.report(t) } cmd := done.NewCmdDone(tt.factory(t), r) cmd.SilenceUsage = true cmd.SetArgs(tt.args) _, err := cmd.ExecuteC() if tt.err == "" { assert.NoError(t, err) return } assert.Error(t, err) assert.Regexp(t, tt.err, err.Error()) }) } } func TestCmdDoneReport(t *testing.T) { tasks := []dto.Task{ {ID: "t-1", ProjectID: "p-1", Name: "Done Report"}, {ID: "t-2", ProjectID: "p-1", Name: "Done Cmd"}, } tts := []struct { name string args []string assert func(*testing.T, *util.OutputFlags, []dto.Task) }{ { name: "report quiet", args: []string{"-q"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) { assert.True(t, of.Quiet) }, }, { name: "report json", args: []string{"--json"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) { assert.True(t, of.JSON) }, }, { name: "report format", args: []string{"--format={{.ID}}"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) { assert.Equal(t, "{{.ID}}", of.Format) }, }, { name: "report csv", args: []string{"--csv"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) { assert.True(t, of.CSV) }, }, { name: "report default", assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) { assert.False(t, of.CSV) assert.False(t, of.JSON) assert.False(t, of.Quiet) assert.True(t, of.Format == "") }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) f.On("GetWorkspaceID"). Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(false) fn := func(t dto.Task) { c.On("GetTask", api.GetTaskParam{ Workspace: "w", ProjectID: t.ProjectID, TaskID: t.ID, }). Return(t, nil) c.On("UpdateTask", api.UpdateTaskParam{ Workspace: "w", ProjectID: t.ProjectID, TaskID: t.ID, Name: t.Name, Status: api.TaskStatusDone, }). Return(t, nil) } fn(tasks[0]) fn(tasks[1]) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) cmd := done.NewCmdDone(f, func( _ io.Writer, of *util.OutputFlags, l []dto.Task) error { called = true assert.Contains(t, l, tasks[0]) assert.Contains(t, l, tasks[1]) tt.assert(t, of, l) return nil }) cmd.SilenceUsage = true cmd.SetArgs(append(tt.args, "t-1", "-p=p-1", "t-2")) _, err := cmd.ExecuteC() assert.NoError(t, err) }) } } ================================================ FILE: pkg/cmd/task/edit/edit.go ================================================ package edit import ( "errors" "io" "strings" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/util" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/search" "github.com/spf13/cobra" ) // NewCmdEdit represents the close command func NewCmdEdit( f cmdutil.Factory, report func(io.Writer, *util.OutputFlags, dto.Task) error, ) *cobra.Command { of := util.OutputFlags{} cmd := &cobra.Command{ Use: "edit ", Aliases: []string{"update"}, Args: cmdutil.RequiredNamedArgs("task"), ValidArgsFunction: cmdcompl.CombineSuggestionsToArgs( cmdcomplutil.NewTaskAutoComplete(f, false)), Short: "Edit a task from a project on Clockify", Long: heredoc.Doc(` Edits a task on a Clockify's project, allowing to change the name, estimated time, assignees, status and billable settings. If you set a estimate for the task, but the project is set as manual estimation, then it will have no effect on Clockify. `), Example: heredoc.Docf(` $ %[1]s -p special 62aa5d7049445270d7b979d6 --name="Very Important" +--------------------------+----------------+--------+ | ID | NAME | STATUS | +--------------------------+----------------+--------+ | 62aa5d7049445270d7b979d6 | Very Important | ACTIVE | +--------------------------+----------------+--------+ $ %[1]s -p special 'important' --assign john@example.com | \ jq '.[] |.assigneeIds' --compact-output ["dddddddddddddddddddddddd"] $ %[1]s -p special important --billable --quiet 62aa5d7049445270d7b979d6 $ %[1]s -p special important --not-billable --csv id,name,status 62aa5d7049445270d7b979d6,Very Important,ACTIVE $ %[1]s -p special very --estimate 1 --done +--------------------------+----------------+--------+ | ID | NAME | STATUS | +--------------------------+----------------+--------+ | 62aa5d7049445270d7b979d6 | Very Important | DONE | +--------------------------+----------------+--------+ $ %[1]s -p special 'very i' --active --format --no-assignee \ --format '{{.Name}} | {{.Status}} | {{ .AssigneeIDs }}' Very Important | ACTIVE | [] `, "clockify-cli task edit"), RunE: func(cmd *cobra.Command, args []string) error { task := strings.TrimSpace(args[0]) if task == "" { return errors.New("task id should not be empty") } if err := of.Check(); err != nil { return err } fl, err := util.TaskReadFlags(cmd, f) if err != nil { return err } c, err := f.Client() if err != nil { return err } if f.Config().IsAllowNameForID() { if task, err = search.GetTaskByName( c, api.GetTasksParam{ Workspace: fl.Workspace, ProjectID: fl.ProjectID}, task); err != nil { return err } } p := api.UpdateTaskParam{ Workspace: fl.Workspace, ProjectID: fl.ProjectID, TaskID: task, Name: fl.Name, Estimate: fl.Estimate, AssigneeIDs: fl.AssigneeIDs, Billable: fl.Billable, } if !cmd.Flags().Changed("name") { t, err := c.GetTask(api.GetTaskParam{ Workspace: fl.Workspace, ProjectID: fl.ProjectID, TaskID: task, }) if err != nil { return err } p.Name = t.Name } switch { case cmd.Flags().Changed("active"): p.Status = api.TaskStatusActive case cmd.Flags().Changed("done"): p.Status = api.TaskStatusDone default: p.Status = api.TaskStatusDefault } t, err := c.UpdateTask(p) if err != nil { return err } if report == nil { return util.TaskReport(cmd, of, t) } return report(cmd.OutOrStdout(), &of, t) }, } util.TaskAddPropFlags(cmd, f) cmd.Flags().Bool("done", false, "sets the task as done") cmd.Flags().Bool("active", false, "sets the task as active") util.TaskAddReportFlags(cmd, &of) return cmd } ================================================ FILE: pkg/cmd/task/edit/edit_test.go ================================================ package edit_test import ( "errors" "io" "testing" "time" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/edit" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/stretchr/testify/assert" ) func TestCmdEdit(t *testing.T) { te := dto.Task{ID: "task-id"} shouldCall := func(t *testing.T) func( io.Writer, *util.OutputFlags, dto.Task) error { called := false t.Cleanup(func() { assert.True(t, called) }) return func( w io.Writer, of *util.OutputFlags, tr dto.Task) error { called = true assert.Equal(t, te, tr) return nil } } dFactory := func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) return f } tts := []struct { name string args []string factory func(*testing.T) cmdutil.Factory report func(*testing.T) func( io.Writer, *util.OutputFlags, dto.Task) error err string }{ { name: "task id required", args: []string{"-p=cli"}, err: `requires arg task`, factory: dFactory, }, { name: "project required", args: []string{"task"}, err: `"project" not set`, factory: dFactory, }, { name: "task id not empty", args: []string{" ", "-p=cli"}, err: `task id should not be empty`, factory: dFactory, }, { name: "only one format", args: []string{"--format={}", "-q", "-j", "-p=OK", "edit"}, err: "flags can't be used together.*format.*json.*quiet", factory: dFactory, }, { name: "billable or not", args: []string{"--billable", "--not-billable", "edit", "-p=OK"}, err: "flags can't be used together.*billable.*not-billable", factory: dFactory, }, { name: "assignee or no assignee", args: []string{"--assignee=l", "--no-assignee", "edit", "-p=OK"}, err: "flags can't be used together.*assignee.*no-assignee", factory: dFactory, }, { name: "client error", err: "client error", args: []string{"edit", "-p=b"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID"). Return("w", nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.On("Client").Return(nil, errors.New("client error")) return f }, }, { name: "workspace error", err: "workspace error", args: []string{"edit", "-p=b"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.On("GetWorkspaceID"). Return("", errors.New("workspace error")) return f }, }, { name: "lookup project", err: "no project", args: []string{"edit", "-p=cli"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Project{}, errors.New("no project")) return f }, }, { name: "lookup user", err: "no user", args: []string{"edit", "-p=cli", "-A=who"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, }) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Project{{Name: "Cli"}}, nil) c.On("WorkspaceUsers", api.WorkspaceUsersParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.User{}, errors.New("no user")) return f }, }, { name: "lookup task", err: "No task with id or name.*edit", args: []string{"edit", "-p=cli"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, }) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Project{{ID: "p-1", Name: "Cli"}}, nil) c.On("GetTasks", api.GetTasksParam{ Workspace: "w", ProjectID: "p-1", PaginationParam: api.AllPages(), }). Return([]dto.Task{{Name: "other one"}}, nil) return f }, }, { name: "http error", err: "http error", args: []string{"task-id", "-p=ok", "-n=new"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(false) c.On("UpdateTask", api.UpdateTaskParam{ Workspace: "w", TaskID: "task-id", ProjectID: "ok", Name: "new", }). Return(dto.Task{}, errors.New("http error")) return f }, }, { name: "edit billable task", args: []string{ "edit", "--name=Edit", "--project=cli", "--billable", "--no-assignee", "--estimate", "32", }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, }) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return( []dto.Project{{ID: "p-1", Name: "Clockify CLI"}}, nil) c.On("GetTasks", api.GetTasksParam{ Workspace: "w", ProjectID: "p-1", PaginationParam: api.AllPages(), }).Return( []dto.Task{{ID: "t-1", Name: "Edit Command"}}, nil) b := true e := time.Hour * 32 var us []string c.On("UpdateTask", api.UpdateTaskParam{ Workspace: "w", TaskID: "t-1", Name: "Edit", ProjectID: "p-1", Billable: &b, Estimate: &e, AssigneeIDs: &us, }). Return(te, nil) return f }, report: shouldCall, }, { name: "edit non-billable task", args: []string{ "edit", "--project=p-1", "--assignee", "lucas", "--assignee=john", "--not-billable", }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, }) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return( []dto.Project{{ID: "p-1", Name: "Clockify CLI"}}, nil) ts := dto.Task{ID: "t-1", Name: "Edit Command"} c.On("GetTasks", api.GetTasksParam{ Workspace: "w", ProjectID: "p-1", PaginationParam: api.AllPages(), }).Return( []dto.Task{ts}, nil) c.On("WorkspaceUsers", api.WorkspaceUsersParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.User{ {ID: "u-1", Name: "Lucas Abreu"}, {ID: "u-2", Name: "John Due"}, }, nil) c.On("GetTask", api.GetTaskParam{ Workspace: "w", ProjectID: "p-1", TaskID: "t-1", }).Return(ts, nil) b := false as := []string{"u-1", "u-2"} c.On("UpdateTask", api.UpdateTaskParam{ Workspace: "w", TaskID: "t-1", Name: "Edit Command", ProjectID: "p-1", AssigneeIDs: &as, Billable: &b, }). Return(te, nil) return f }, report: shouldCall, }, { name: "edit no allow name for id", args: []string{ "t-1", "--project=p-1", "--assignee", "u-1", "--assignee=u-2", "--not-billable", }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("GetWorkspaceID"). Return("w", nil) f.On("Client").Return(c, nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(false) ts := dto.Task{ID: "t-1", Name: "Edit Command"} c.On("GetTask", api.GetTaskParam{ Workspace: "w", ProjectID: "p-1", TaskID: "t-1", }).Return(ts, nil) b := false as := []string{"u-1", "u-2"} c.On("UpdateTask", api.UpdateTaskParam{ Workspace: "w", TaskID: "t-1", Name: "Edit Command", ProjectID: "p-1", AssigneeIDs: &as, Billable: &b, }). Return(te, nil) return f }, report: shouldCall, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { r := func(io.Writer, *util.OutputFlags, dto.Task) error { assert.Fail(t, "failed") return nil } if tt.report != nil { r = tt.report(t) } cmd := edit.NewCmdEdit(tt.factory(t), r) cmd.SilenceUsage = true cmd.SetArgs(tt.args) _, err := cmd.ExecuteC() if tt.err == "" { assert.NoError(t, err) return } assert.Error(t, err) assert.Regexp(t, tt.err, err.Error()) }) } } func TestCmdEditReport(t *testing.T) { pr := dto.Task{Name: "Coderockr"} tts := []struct { name string args []string assert func(*testing.T, *util.OutputFlags, dto.Task) }{ { name: "report quiet", args: []string{"-q"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Task) { assert.True(t, of.Quiet) }, }, { name: "report json", args: []string{"--json"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Task) { assert.True(t, of.JSON) }, }, { name: "report format", args: []string{"--format={{.ID}}"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Task) { assert.Equal(t, "{{.ID}}", of.Format) }, }, { name: "report csv", args: []string{"--csv"}, assert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) { assert.True(t, of.CSV) }, }, { name: "report default", assert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) { assert.False(t, of.CSV) assert.False(t, of.JSON) assert.False(t, of.Quiet) assert.True(t, of.Format == "") }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) f.On("GetWorkspaceID"). Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(false) c.On("UpdateTask", api.UpdateTaskParam{ Workspace: "w", ProjectID: "p-1", TaskID: "t-1", Name: "Task Edit", }). Return(pr, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) cmd := edit.NewCmdEdit(f, func( _ io.Writer, of *util.OutputFlags, u dto.Task) error { called = true assert.Equal(t, pr, u) tt.assert(t, of, u) return nil }) cmd.SilenceUsage = true cmd.SetArgs(append(tt.args, "t-1", "-n", "Task Edit", "-p=p-1")) _, err := cmd.ExecuteC() assert.NoError(t, err) }) } } ================================================ FILE: pkg/cmd/task/list/list.go ================================================ package list import ( "io" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/search" "github.com/spf13/cobra" ) // NewCmdList represents the list command func NewCmdList( f cmdutil.Factory, report func(io.Writer, *util.OutputFlags, []dto.Task) error, ) *cobra.Command { of := util.OutputFlags{} cmd := &cobra.Command{ Use: "list", Short: "List tasks in a Clockify project", Example: heredoc.Docf(` $ %[1]s --project special +--------------------------+----------+--------+ | ID | NAME | STATUS | +--------------------------+----------+--------+ | 62aa4eed49445270d7b9666c | Inactive | DONE | | 62aa4ee64ebb4f143c8d5225 | Second | ACTIVE | | 62aa4ea2c22de9759e6e3a0e | First | ACTIVE | +--------------------------+----------+--------+ $ %[1]s --project special --active --quiet 62aa4ee64ebb4f143c8d5225 62aa4ea2c22de9759e6e3a0e $ %[1]s --project special --name inact --csv id,name,status 62aa4eed49445270d7b9666c,Inactive,DONE `, "clockify-cli task list"), Aliases: []string{"ls"}, RunE: func(cmd *cobra.Command, args []string) error { workspace, err := f.GetWorkspaceID() if err != nil { return err } c, err := f.Client() if err != nil { return err } p := api.GetTasksParam{ Workspace: workspace, PaginationParam: api.AllPages(), } p.Active, _ = cmd.Flags().GetBool("active") p.Name, _ = cmd.Flags().GetString("name") p.ProjectID, _ = cmd.Flags().GetString("project") if f.Config().IsAllowNameForID() && p.ProjectID != "" { if p.ProjectID, err = search.GetProjectByName( c, f.Config(), workspace, p.ProjectID, ""); err != nil { return err } } tasks, err := c.GetTasks(p) if err != nil { return err } if report == nil { return util.TaskReport(cmd, of, tasks...) } return report(cmd.OutOrStdout(), &of, tasks) }, } cmd.Flags().StringP("name", "n", "", "will be used to filter the tag by name") cmd.Flags().BoolP("active", "a", false, "display only active tasks") util.TaskAddReportFlags(cmd, &of) cmdutil.AddProjectFlags(cmd, f) return cmd } ================================================ FILE: pkg/cmd/task/list/list_test.go ================================================ package list_test import ( "errors" "io" "testing" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/list" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/stretchr/testify/assert" ) type report func(io.Writer, *util.OutputFlags, []dto.Task) error func TestCmdList(t *testing.T) { tts := []struct { name string err string args []string params func(*testing.T) (cmdutil.Factory, report) }{ { name: "missing project", err: "required flag.*project", params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) return f, nil }, }, { name: "workspace error", err: "error", args: []string{"-p=p-1"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.On("GetWorkspaceID").Return("", errors.New("error")) return f, nil }, }, { name: "client error", err: "error", args: []string{"-p=p-1"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.On("GetWorkspaceID").Return("w", nil) f.On("Client").Return(nil, errors.New("error")) return f, nil }, }, { name: "project lookup error", err: "No project with id or name containing.*p-1", args: []string{"-p=p-1"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID").Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) cf.On("IsSearchProjectWithClientsName").Return(false) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Project{}, nil) return f, nil }, }, { name: "list error", err: "error", args: []string{"-p=p-1"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID").Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(false) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetTasks", api.GetTasksParam{ Workspace: "w", ProjectID: "p-1", PaginationParam: api.AllPages(), }).Return([]dto.Task{}, errors.New("error")) return f, nil }, }, { name: "list active with name", args: []string{"-p=p-1", "--active", "-n=list"}, params: func(t *testing.T) (cmdutil.Factory, report) { f := mocks.NewMockFactory(t) f.On("GetWorkspaceID").Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) cf.On("IsSearchProjectWithClientsName").Return(false) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Project{ {ID: "p-1", Name: "Cli"}, }, nil) l := []dto.Task{ {ID: "1", Name: "List"}, {ID: "2", Name: "List Tasks"}, } c.On("GetTasks", api.GetTasksParam{ Workspace: "w", ProjectID: "p-1", Active: true, Name: "list", PaginationParam: api.AllPages(), }).Return(l, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) return f, func( w io.Writer, of *util.OutputFlags, r []dto.Task) error { called = true assert.Equal(t, l, r) return nil } }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f, r := tt.params(t) if r == nil { r = func( _ io.Writer, of *util.OutputFlags, l []dto.Task) error { t.Error("should not be called") return nil } } cmd := list.NewCmdList(f, r) cmd.SilenceUsage = true cmd.SetArgs(tt.args) _, err := cmd.ExecuteC() if tt.err == "" { assert.NoError(t, err) return } assert.Error(t, err) assert.Regexp(t, tt.err, err.Error()) }) } } func TestCmdListReport(t *testing.T) { tasks := []dto.Task{ {ID: "t-1", ProjectID: "p-1", Name: "List Report"}, {ID: "t-2", ProjectID: "p-1", Name: "List Cmd"}, } tts := []struct { name string args []string assert func(*testing.T, *util.OutputFlags, []dto.Task) }{ { name: "report quiet", args: []string{"-q"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) { assert.True(t, of.Quiet) }, }, { name: "report json", args: []string{"--json"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) { assert.True(t, of.JSON) }, }, { name: "report format", args: []string{"--format={{.ID}}"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) { assert.Equal(t, "{{.ID}}", of.Format) }, }, { name: "report csv", args: []string{"--csv"}, assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) { assert.True(t, of.CSV) }, }, { name: "report default", assert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) { assert.False(t, of.CSV) assert.False(t, of.JSON) assert.False(t, of.Quiet) assert.True(t, of.Format == "") }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) f.On("GetWorkspaceID"). Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(false) c.On("GetTasks", api.GetTasksParam{ Workspace: "w", ProjectID: "p-1", PaginationParam: api.AllPages(), }). Return(tasks, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) cmd := list.NewCmdList(f, func( _ io.Writer, of *util.OutputFlags, l []dto.Task) error { called = true assert.Equal(t, l, tasks) tt.assert(t, of, l) return nil }) cmd.SilenceUsage = true cmd.SetArgs(append(tt.args, "t-1", "-p=p-1", "t-2")) _, err := cmd.ExecuteC() assert.NoError(t, err) }) } } ================================================ FILE: pkg/cmd/task/quick-add/quick-add.go ================================================ package quickadd import ( "io" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/search" "github.com/lucassabreu/clockify-cli/strhlp" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" ) // NewCmdQuickAdd will add multiple tasks to a project, but only setting its // name func NewCmdQuickAdd( f cmdutil.Factory, report func(io.Writer, *util.OutputFlags, []dto.Task) error, ) *cobra.Command { of := util.OutputFlags{} cmd := &cobra.Command{ Use: "quick-add ...", Aliases: []string{"quick"}, Short: "Adds tasks to a project on Clockify", Args: cmdutil.RequiredNamedArgs("name"), Long: "Adds a new active tasks to a project on Clockify, " + "but only allow setting their names.", Example: heredoc.Docf(` $ %[1]s -p special "Very Important" +--------------------------+----------------+--------+ | ID | NAME | STATUS | +--------------------------+----------------+--------+ | 62aa5d7049445270d7b979d6 | Very Important | ACTIVE | +--------------------------+----------------+--------+ $ %[1]s -p special "Very Cool" -q dddddddddddddddddddddddd $ %[1]s -p special Billable "Not Billable" --csv id,name,status 62ab145ec22de9759e6f6e35,Billable,ACTIVE 62ab145ec22de9759e6f6e36,Not Billable,ACTIVE `, "clockify-cli task quick-add"), RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } c, err := f.Client() if err != nil { return err } w, err := f.GetWorkspaceID() if err != nil { return err } p, _ := cmd.Flags().GetString("project") if f.Config().IsAllowNameForID() { if p, err = search.GetProjectByName( c, f.Config(), w, p, ""); err != nil { return err } } names := strhlp.Unique(args) tasks := make([]dto.Task, len(names)) g := errgroup.Group{} for j := range names { i := j g.Go(func() error { tasks[i], err = c.AddTask(api.AddTaskParam{ Workspace: w, ProjectID: p, Name: names[i], }) return err }) } if err := g.Wait(); err != nil { return err } if report != nil { return report(cmd.OutOrStdout(), &of, tasks) } return util.TaskReport(cmd, of, tasks...) }, } cmdutil.AddProjectFlags(cmd, f) util.TaskAddReportFlags(cmd, &of) return cmd } ================================================ FILE: pkg/cmd/task/quick-add/quick-add_test.go ================================================ package quickadd_test import ( "errors" "io" "testing" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" quickadd "github.com/lucassabreu/clockify-cli/pkg/cmd/task/quick-add" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/stretchr/testify/assert" ) func TestCmdQuickAdd(t *testing.T) { shouldCall := func(t *testing.T) func( io.Writer, *util.OutputFlags, []dto.Task) error { called := false t.Cleanup(func() { assert.True(t, called) }) return func( _ io.Writer, _ *util.OutputFlags, ts []dto.Task) error { called = true return nil } } dFactory := func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) return f } tts := []struct { name string args []string factory func(*testing.T) cmdutil.Factory report func(*testing.T) func( io.Writer, *util.OutputFlags, []dto.Task) error err string }{ { name: "only one format", args: []string{"--format={}", "-q", "-j", "OK", "-p=OK"}, err: "flags can't be used together.*format.*json.*quiet", factory: dFactory, }, { name: "name required", args: []string{"-p=OK"}, err: `requires arg name`, factory: dFactory, }, { name: "project required", args: []string{"OK"}, err: `"project" not set`, factory: dFactory, }, { name: "client error", err: "client error", args: []string{"a", "-p=b"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.EXPECT().Client().Return(nil, errors.New("client error")) return f }, }, { name: "workspace error", err: "workspace error", args: []string{"a", "-p=b"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.EXPECT().Client().Return(mocks.NewMockClient(t), nil) f.EXPECT().GetWorkspaceID(). Return("", errors.New("workspace error")) return f }, }, { name: "lookup project", err: "no project", args: []string{"error", "-p=cli"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.EXPECT().GetWorkspaceID(). Return("w", nil) f.EXPECT().Client().Return(c, nil) cf := mocks.NewMockConfig(t) f.EXPECT().Config().Return(cf) cf.EXPECT().IsAllowNameForID().Return(true) c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Project{}, errors.New("no project")) return f }, }, { name: "http error", err: "http error", args: []string{"error", "-p=ok"}, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.EXPECT().GetWorkspaceID(). Return("w", nil) f.EXPECT().Client().Return(c, nil) cf := mocks.NewMockConfig(t) f.EXPECT().Config().Return(cf) cf.EXPECT().IsAllowNameForID().Return(false) c.EXPECT().AddTask(api.AddTaskParam{ Workspace: "w", ProjectID: "ok", Name: "error", }). Return(dto.Task{}, errors.New("http error")) return f }, }, { name: "add one task", args: []string{ "--project=cli", "Add", }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.EXPECT().GetWorkspaceID(). Return("w", nil) f.EXPECT().Client().Return(c, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, }) c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return( []dto.Project{{ID: "p-1", Name: "Clockify CLI"}}, nil) c.EXPECT().AddTask(api.AddTaskParam{ Workspace: "w", Name: "Add", ProjectID: "p-1", }). Return(dto.Task{ID: "t-id"}, nil) return f }, report: shouldCall, }, { name: "add multiple tasks", args: []string{ "--project=cli", "Task 00", "Task 01", "Task 02", }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.EXPECT().GetWorkspaceID(). Return("w", nil) f.EXPECT().Client().Return(c, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, }) c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return( []dto.Project{{ID: "p-1", Name: "Clockify CLI"}}, nil) c.EXPECT().AddTask(api.AddTaskParam{ Workspace: "w", Name: "Task 00", ProjectID: "p-1", }). Return(dto.Task{ID: "00"}, nil) c.EXPECT().AddTask(api.AddTaskParam{ Workspace: "w", Name: "Task 01", ProjectID: "p-1", }). Return(dto.Task{ID: "01"}, nil) c.EXPECT().AddTask(api.AddTaskParam{ Workspace: "w", Name: "Task 02", ProjectID: "p-1", }). Return(dto.Task{ID: "02"}, nil) return f }, report: shouldCall, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { r := func(io.Writer, *util.OutputFlags, []dto.Task) error { assert.Fail(t, "failed") return nil } if tt.report != nil { r = tt.report(t) } cmd := quickadd.NewCmdQuickAdd(tt.factory(t), r) cmd.SilenceUsage = true cmd.SetArgs(tt.args) _, err := cmd.ExecuteC() if tt.err == "" { assert.NoError(t, err) return } assert.Error(t, err) assert.Regexp(t, tt.err, err.Error()) }) } } func TestCmdQuickAddReport(t *testing.T) { pr := dto.Task{Name: "Coderockr"} tts := []struct { name string args []string assert func(*testing.T, *util.OutputFlags, dto.Task) }{ { name: "report quiet", args: []string{"-q"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Task) { assert.True(t, of.Quiet) }, }, { name: "report json", args: []string{"--json"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Task) { assert.True(t, of.JSON) }, }, { name: "report format", args: []string{"--format={{.ID}}"}, assert: func(t *testing.T, of *util.OutputFlags, c dto.Task) { assert.Equal(t, "{{.ID}}", of.Format) }, }, { name: "report csv", args: []string{"--csv"}, assert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) { assert.True(t, of.CSV) }, }, { name: "report default", assert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) { assert.False(t, of.CSV) assert.False(t, of.JSON) assert.False(t, of.Quiet) assert.True(t, of.Format == "") }, }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) c := mocks.NewMockClient(t) f.EXPECT().Client().Return(c, nil) f.EXPECT().GetWorkspaceID(). Return("w", nil) cf := mocks.NewMockConfig(t) f.EXPECT().Config().Return(cf) cf.EXPECT().IsAllowNameForID().Return(false) c.EXPECT().AddTask(api.AddTaskParam{ Workspace: "w", ProjectID: "p-1", Name: "Task Add", }). Return(pr, nil) called := false t.Cleanup(func() { assert.True(t, called, "was not called") }) cmd := quickadd.NewCmdQuickAdd(f, func( _ io.Writer, of *util.OutputFlags, ts []dto.Task) error { u := ts[0] called = true assert.Equal(t, pr, u) tt.assert(t, of, u) return nil }) cmd.SilenceUsage = true cmd.SetArgs(append(tt.args, "Task Add", "-p=p-1")) _, err := cmd.ExecuteC() assert.NoError(t, err) }) } } ================================================ FILE: pkg/cmd/task/task.go ================================================ package task import ( "github.com/lucassabreu/clockify-cli/pkg/cmd/task/add" del "github.com/lucassabreu/clockify-cli/pkg/cmd/task/delete" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/done" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/edit" "github.com/lucassabreu/clockify-cli/pkg/cmd/task/list" quickadd "github.com/lucassabreu/clockify-cli/pkg/cmd/task/quick-add" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/spf13/cobra" ) // NewCmdTask represents the client command func NewCmdTask(f cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "task", Aliases: []string{"tasks"}, Short: "Work with Clockify tasks", } cmd.AddCommand(list.NewCmdList(f, nil)) cmd.AddCommand(add.NewCmdAdd(f, nil)) cmd.AddCommand(quickadd.NewCmdQuickAdd(f, nil)) cmd.AddCommand(edit.NewCmdEdit(f, nil)) cmd.AddCommand(del.NewCmdDelete(f, nil)) cmd.AddCommand(done.NewCmdDone(f, nil)) return cmd } ================================================ FILE: pkg/cmd/task/util/read-flags.go ================================================ package util import ( "time" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/search" "github.com/spf13/cobra" ) // TaskAddPropFlags add common flags expected on editing a task func TaskAddPropFlags(cmd *cobra.Command, f cmdutil.Factory) { cmd.Flags().StringP("name", "n", "", "new name of the task") cmd.Flags().Int32P("estimate", "E", 0, "estimation on hours") cmd.Flags().Bool("billable", false, "sets the task as billable") cmd.Flags().Bool("not-billable", false, "sets the task as not billable") cmd.Flags().StringSliceP("assignee", "A", []string{}, "list of users that are assigned to this task") _ = cmdcompl.AddSuggestionsToFlag(cmd, "assignee", cmdcomplutil.NewUserAutoComplete(f)) cmd.Flags().Bool("no-assignee", false, "cleans the assignee list") cmdutil.AddProjectFlags(cmd, f) } // FlagsDTO holds data about editing or creating a Task type FlagsDTO struct { Workspace string ProjectID string Name string Estimate *time.Duration AssigneeIDs *[]string Billable *bool } // TaskReadFlags read the common flags expected when editing a task func TaskReadFlags(cmd *cobra.Command, f cmdutil.Factory) (p FlagsDTO, err error) { if err := cmdutil.XorFlag(map[string]bool{ "assignee": cmd.Flags().Changed("assignee"), "no-assignee": cmd.Flags().Changed("no-assignee"), }); err != nil { return p, err } if err := cmdutil.XorFlag(map[string]bool{ "billable": cmd.Flags().Changed("billable"), "not-billable": cmd.Flags().Changed("not-billable"), }); err != nil { return p, err } if p.Workspace, err = f.GetWorkspaceID(); err != nil { return } p.ProjectID, _ = cmd.Flags().GetString("project") p.Name, _ = cmd.Flags().GetString("name") if cmd.Flags().Changed("estimate") { e, _ := cmd.Flags().GetInt32("estimate") d := time.Duration(e) * time.Hour p.Estimate = &d } if cmd.Flags().Changed("assignee") { assignees, _ := cmd.Flags().GetStringSlice("assignee") p.AssigneeIDs = &assignees } if f.Config().IsAllowNameForID() { c, err := f.Client() if err != nil { return p, err } if p.ProjectID, err = search.GetProjectByName( c, f.Config(), p.Workspace, p.ProjectID, ""); err != nil { return p, err } if p.AssigneeIDs != nil { as := *p.AssigneeIDs if as, err = search.GetUsersByName( c, p.Workspace, as); err != nil { return p, err } p.AssigneeIDs = &as } } if cmd.Flags().Changed("no-assignee") { var a []string p.AssigneeIDs = &a } switch { case cmd.Flags().Changed("billable"): b := true p.Billable = &b case cmd.Flags().Changed("not-billable"): b := false p.Billable = &b } return } ================================================ FILE: pkg/cmd/task/util/report.go ================================================ package util import ( "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/output/task" "github.com/spf13/cobra" ) // OutputFlags sets how the tasks will be printed type OutputFlags struct { Format string JSON bool CSV bool Quiet bool } // Check guaranties that only one type of output is chosen func (of OutputFlags) Check() error { return cmdutil.XorFlag(map[string]bool{ "format": of.Format != "", "json": of.JSON, "csv": of.CSV, "quiet": of.Quiet, }) } // TaskAddReportFlags will add common format flags used for tasks func TaskAddReportFlags(cmd *cobra.Command, of *OutputFlags) { cmd.Flags().StringVarP(&of.Format, "format", "f", "", "golang text/template format to be applied on each Client") cmd.Flags().BoolVarP(&of.JSON, "json", "j", false, "print as JSON") cmd.Flags().BoolVarP(&of.CSV, "csv", "v", false, "print as CSV") cmd.Flags().BoolVarP(&of.Quiet, "quiet", "q", false, "only display ids") } // TaskReport will output the task as set by the flags func TaskReport(cmd *cobra.Command, of OutputFlags, tasks ...dto.Task) error { out := cmd.OutOrStdout() switch { case of.JSON: return task.TasksJSONPrint(tasks, out) case of.CSV: return task.TasksCSVPrint(tasks, out) case of.Quiet: return task.TaskPrintQuietly(tasks, out) case of.Format != "": return task.TaskPrintWithTemplate(of.Format)(tasks, out) default: return task.TaskPrint(tasks, out) } } ================================================ FILE: pkg/cmd/time-entry/clone/clone.go ================================================ package clone import ( "strings" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" timeentry "github.com/lucassabreu/clockify-cli/pkg/output/time-entry" "github.com/lucassabreu/clockify-cli/pkg/timeentryhlp" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/spf13/cobra" ) // NewCmdClone represents the clone command func NewCmdClone(f cmdutil.Factory) *cobra.Command { of := util.OutputFlags{TimeFormat: timeentry.TimeFormatSimple} cmd := &cobra.Command{ Use: "clone " + "{ | " + timeentryhlp.AliasLast + "| ^ }", Short: "Copy a time entry and starts it ", Long: heredoc.Docf(` Copy a time entry and starts it. Running time entry will be stopped using the start time of this new entry. If you don't want to stop them, use the flag %[1]s--no-closing%[1]s. If you want to clone the last (running) time entry you can use "%[2]s" instead of its ID. Also if you want to clone the one previous to it, you can use "^2", for the before that "^3" and so on. The rules defined in the workspace and project will be checked before creating it. `, "`", timeentryhlp.AliasLast) + "\n" + util.HelpTimeEntryNowIfNotSet + "The same applies to end time (`--when-to-close`).\n\n" + util.HelpInteractiveByDefault + "\n" + util.HelpTimeInputOnTimeEntry + "\n" + util.HelpNamesForIds + "\n" + util.HelpMoreInfoAboutStarting + "\n" + util.HelpMoreInfoAboutPrinting, Example: heredoc.Docf(` $ %[1]s in --project cli --tag dev -d "Adding docs to clone" --task "clone" --md ID: %[2]s62ae4b304ebb4f143c931d50%[2]s Billable: %[2]syes%[2]s Locked: %[2]sno%[2]s Project: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s) Task: Clone Command (%[2]s62ae4af04ebb4f143c931d2e%[2]s) Interval: %[2]s2022-06-18 22:01:16%[2]s until %[2]snow%[2]s Description: > Adding docs to clone Tags: * Development (%[2]s62ae28b72518aa18da2acb49%[2]s) $ %[1]s clone last -d "Adding examples to clone" --md ID: %[2]s62ae4b304ebb4f143c931d50%[2]s Billable: %[2]syes%[2]s Locked: %[2]sno%[2]s Project: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s) Task: Clone Command (%[2]s62ae4af04ebb4f143c931d2e%[2]s) Interval: %[2]s2022-06-18 22:11:16%[2]s until %[2]snow%[2]s Description: > Adding examples to clone Tags: * Development (%[2]s62ae28b72518aa18da2acb49%[2]s) $ %[1]s clone last -d "Adding examples to in" -T pair --task "in command" --md ID: %[2]s62ae4dfe4ebb4f143c932106%[2]s Billable: %[2]syes%[2]s Locked: %[2]sno%[2]s Project: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s) Task: In Command (%[2]s62ae29e62518aa18da2acd14%[2]s) Interval: %[2]s2022-06-18 22:13:14%[2]s until %[2]snow%[2]s Description: > Adding examples to in Tags: * Pair Programming (%[2]s621948708cb9606d934ebba7%[2]s) `, "clockify-cli", "`"), Args: cmdutil.RequiredNamedArgs("time entry id"), ValidArgs: []string{timeentryhlp.AliasLast}, RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } var err error var w, u string if w, err = f.GetWorkspaceID(); err != nil { return err } if u, err = f.GetUserID(); err != nil { return err } c, err := f.Client() if err != nil { return err } id := strings.ToLower(strings.TrimSpace(args[0])) if id == timeentryhlp.AliasLast { id = timeentryhlp.AliasLatest } tec, err := timeentryhlp.GetTimeEntry(c, w, u, id) if err != nil { return err } tec.UserID = u tec.TimeInterval = dto.NewTimeInterval(timehlp.Now(), nil) noClosing, _ := cmd.Flags().GetBool("no-closing") dc := util.NewDescriptionCompleter(f) te := util.TimeEntryImplToDTO(tec) if te, err = util.Do( te, util.FillTimeEntryWithFlags(cmd.Flags()), func(tec util.TimeEntryDTO) (util.TimeEntryDTO, error) { if noClosing { return tec, nil } return util.ValidateClosingTimeEntry(f)(tec) }, util.GetAllowNameForIDsFn(f.Config(), c), util.GetPropsInteractiveFn(dc, f), util.GetDatesInteractiveFn(f), util.GetValidateTimeEntryFn(f), func(tec util.TimeEntryDTO) (util.TimeEntryDTO, error) { if noClosing { return tec, nil } return util.OutInProgressFn(c)(tec) }, util.CreateTimeEntryFn(c), ); err != nil { return err } return util.PrintTimeEntryImpl( util.TimeEntryDTOToImpl(te), f, cmd.OutOrStdout(), of) }, } util.AddTimeEntryFlags(cmd, f, &of) util.AddTimeEntryDateFlags(cmd) cmd.Flags().BoolP("no-closing", "", false, "don't close any active time entry") return cmd } ================================================ FILE: pkg/cmd/time-entry/delete/delete.go ================================================ package del import ( "errors" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/timeentryhlp" "github.com/spf13/cobra" ) // NewCmdDelete represents the delete command func NewCmdDelete(f cmdutil.Factory) *cobra.Command { va := cmdcompl.ValidArgsSlide{timeentryhlp.AliasCurrent, timeentryhlp.AliasLast} cmd := &cobra.Command{ Use: "delete { | " + va.IntoUseOptions() + " }...", Aliases: []string{"del", "rm", "remove"}, Args: cmdutil.RequiredNamedArgs("time entry id"), ValidArgs: va.IntoValidArgs(), Short: `Delete time entry(ies), use id "` + timeentryhlp.AliasCurrent + `" to apply to time entry in progress`, Long: heredoc.Docf(` Delete time entries If you want to delete the current (running) time entry you can use "%s" instead of its ID. **Important**: this action can't be reverted, once the time entry is deleted its ID is lost. `, timeentryhlp.AliasCurrent, ), Example: heredoc.Docf(` # trying to delete a time entry that does not exist, or from other workspace $ %[1]s 62af70d849445270d7c09fbc delete time entry "62af70d849445270d7c09fbc": TIMEENTRY with id 62af70d849445270d7c09fbc doesn't belong to WORKSPACE with id cccccccccccccccccccccccc (code: 501) # deleting the running time entry $ %[1]s current # no output # deleting the last time entry $ %[1]s last # no output # deleting multiple time entries $ %[1]s 62b5b51085815e619d7ae18d 62b5d55185815e619d7af928 # no output `, "clockify-cli delete"), RunE: func(cmd *cobra.Command, args []string) error { var err error var w, u string if w, err = f.GetWorkspaceID(); err != nil { return err } if u, err = f.GetUserID(); err != nil { return err } c, err := f.Client() if err != nil { return err } for i := range args { p := api.DeleteTimeEntryParam{ Workspace: w, TimeEntryID: args[i], } if p.TimeEntryID == timeentryhlp.AliasCurrent { te, err := c.GetTimeEntryInProgress( api.GetTimeEntryInProgressParam{ Workspace: p.Workspace, UserID: u, }) if err != nil { return err } if te == nil { return errors.New("there is no time entry in progress") } p.TimeEntryID = te.ID } if p.TimeEntryID == timeentryhlp.AliasLast { te, err := timeentryhlp.GetLatestEntryEntry(c, p.Workspace, u) if err != nil { return err } p.TimeEntryID = te.ID } if err := c.DeleteTimeEntry(p); err != nil { return err } } return nil }, } return cmd } ================================================ FILE: pkg/cmd/time-entry/edit/edit.go ================================================ package edit import ( "io" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" output "github.com/lucassabreu/clockify-cli/pkg/output/time-entry" "github.com/lucassabreu/clockify-cli/pkg/timeentryhlp" "github.com/spf13/cobra" ) // NewCmdEdit represents the edit command func NewCmdEdit( f cmdutil.Factory, report func(dto.TimeEntryImpl, io.Writer, util.OutputFlags) error, ) *cobra.Command { of := util.OutputFlags{TimeFormat: output.TimeFormatSimple} va := cmdcompl.ValidArgsSlide{ timeentryhlp.AliasCurrent, timeentryhlp.AliasLast} cmd := &cobra.Command{ Use: "edit { | " + va.IntoUseOptions() + " | ^n }", Aliases: []string{"update"}, Args: cobra.MatchAll( cmdutil.RequiredNamedArgs("time entry id"), cobra.ExactArgs(1), ), ValidArgs: va.IntoValidArgs(), Short: `Edit a time entry`, Long: heredoc.Docf(` Edit a time entry. Only the inputs sent thought flags will be changed, any other properties will remain the same. %s %s %s %s %s `, util.HelpTimeEntriesAliasForEdit, util.HelpInteractiveByDefault, util.HelpDateTimeFormats, util.HelpNamesForIds, util.HelpMoreInfoAboutPrinting, ), Example: heredoc.Docf(` # starting a time entry $ %[1]s in --project cli --tag dev -d "Adding docs to edit" --task "edit" --md ID: %[2]s62ae4b304ebb4f143c931d50%[2]s Billable: %[2]syes%[2]s Locked: %[2]sno%[2]s Project: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s) Task: Edit Command (%[2]s62ae4af04ebb4f143c931d2e%[2]s) Interval: %[2]s2022-06-18 22:01:16%[2]s until %[2]snow%[2]s Description: > Adding docs to edit Tags: * Development (%[2]s62ae28b72518aa18da2acb49%[2]s) # changing the description on the running time entry $ %[1]s edit current -d "Adding examples to edit" --md ID: %[2]s62ae4b304ebb4f143c931d50%[2]s Billable: %[2]syes%[2]s Locked: %[2]sno%[2]s Project: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s) Task: Edit Command (%[2]s62ae4af04ebb4f143c931d2e%[2]s) Interval: %[2]s2022-06-18 22:01:16%[2]s until %[2]snow%[2]s Description: > Adding examples to edit Tags: * Development (%[2]s62ae28b72518aa18da2acb49%[2]s) # change the description, task, and tags $ %[1]s edit -d "Adding examples to edit" -T pair --task "in command" --md ID: %[2]s62ae4b304ebb4f143c931d50%[2]s Billable: %[2]syes%[2]s Locked: %[2]sno%[2]s Project: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s) Task: In Command (%[2]s62ae29e62518aa18da2acd14%[2]s) Interval: %[2]s2022-06-18 22:13:14%[2]s until %[2]snow%[2]s Description: > Adding examples to edit Tags: * Pair Programming (%[2]s621948708cb9606d934ebba7%[2]s) `, "clockify-cli", "`"), RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } c, err := f.Client() if err != nil { return err } userID, err := f.GetUserID() if err != nil { return err } w, err := f.GetWorkspaceID() if err != nil { return err } tei, err := timeentryhlp.GetTimeEntry( c, w, userID, args[0], ) if err != nil { return err } te := util.TimeEntryImplToDTO(tei) dc := util.NewDescriptionCompleter(f) if te, err = util.Do( te, util.FillTimeEntryWithFlags(cmd.Flags()), util.GetAllowNameForIDsFn(f.Config(), c), util.GetPropsInteractiveFn(dc, f), util.GetDatesInteractiveFn(f), util.GetValidateTimeEntryFn(f), ); err != nil { return err } if tei, err = c.UpdateTimeEntry(api.UpdateTimeEntryParam{ Workspace: te.Workspace, TimeEntryID: te.ID, Description: te.Description, Start: te.Start, End: te.End, Billable: *te.Billable, ProjectID: te.ProjectID, TaskID: te.TaskID, TagIDs: te.TagIDs, }); err != nil { return err } return report(tei, cmd.OutOrStdout(), of) }, } util.AddTimeEntryFlags(cmd, f, &of) cmd.Flags().StringP("when", "s", "", "when the entry should be started") cmd.Flags().StringP("when-to-close", "e", "", "when the entry should be closed (same formats as `when`)") return cmd } ================================================ FILE: pkg/cmd/time-entry/edit/edit_test.go ================================================ package edit_test import ( "bytes" "io" "testing" "time" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/edit" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util" "github.com/stretchr/testify/assert" ) func TestNewCmdEditWhenChangingProjectOrTask(t *testing.T) { w := dto.Workspace{ID: "w"} te := dto.TimeEntryImpl{ WorkspaceID: w.ID, ID: "timeentryid", Description: "Something", ProjectID: "oldproj", TaskID: "oldtask", TimeInterval: dto.TimeInterval{ Start: time.Now(), }, } tts := []struct { name string args []string project *dto.Project updateParam api.UpdateTimeEntryParam }{ { name: "should remove task, when changing project", args: []string{"-p", "newproj"}, project: &dto.Project{ID: "newproj", Name: "newproj"}, updateParam: api.UpdateTimeEntryParam{ Workspace: te.WorkspaceID, TimeEntryID: te.ID, Start: te.TimeInterval.Start, End: te.TimeInterval.End, Billable: te.Billable, Description: te.Description, ProjectID: "newproj", TaskID: "", TagIDs: te.TagIDs, }, }, { name: "should remove task, when removing project", args: []string{"-p", ""}, updateParam: api.UpdateTimeEntryParam{ Workspace: te.WorkspaceID, TimeEntryID: te.ID, Start: te.TimeInterval.Start, End: te.TimeInterval.End, Billable: te.Billable, Description: te.Description, ProjectID: "", TaskID: "", TagIDs: te.TagIDs, }, }, { name: "should change project and task", args: []string{"--task", "newtask", "-p=newproj"}, project: &dto.Project{ID: "newproj", Name: "newproj"}, updateParam: api.UpdateTimeEntryParam{ Workspace: te.WorkspaceID, TimeEntryID: te.ID, Start: te.TimeInterval.Start, End: te.TimeInterval.End, Billable: te.Billable, Description: te.Description, ProjectID: "newproj", TaskID: "newtask", TagIDs: te.TagIDs, }, }, } for i := range tts { tt := &tts[i] t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) f.EXPECT().GetUserID().Return("u", nil) f.EXPECT().GetWorkspace().Return(w, nil) f.EXPECT().GetWorkspaceID().Return(w.ID, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, }) c := mocks.NewMockClient(t) f.EXPECT().Client().Return(c, nil) c.EXPECT().GetTimeEntryInProgress(api.GetTimeEntryInProgressParam{ Workspace: "w", UserID: "u", }). Return(&te, nil) p := tt.project if p != nil { c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: w.ID, PaginationParam: api.AllPages(), }). Return([]dto.Project{*p}, nil) c.EXPECT().GetProject(api.GetProjectParam{ Workspace: w.ID, ProjectID: p.ID, }). Return(p, nil) } if tt.updateParam.TaskID != "" { c.EXPECT().GetTasks(api.GetTasksParam{ Workspace: w.ID, ProjectID: tt.updateParam.ProjectID, Active: true, PaginationParam: api.AllPages(), }). Return([]dto.Task{{ID: tt.updateParam.TaskID}}, nil) } c.EXPECT().UpdateTimeEntry(tt.updateParam). Return(te, nil) called := false cmd := edit.NewCmdEdit(f, func( _ dto.TimeEntryImpl, _ io.Writer, _ util.OutputFlags) error { called = true return nil }) cmd.SilenceUsage = true cmd.SilenceErrors = true out := bytes.NewBufferString("") cmd.SetOut(out) cmd.SetErr(out) cmd.SetArgs(append(tt.args, "current", "-q")) _, err := cmd.ExecuteC() if assert.NoError(t, err) { t.Cleanup(func() { assert.True(t, called) }) return } t.Fatalf("err: %s", err) }) } } ================================================ FILE: pkg/cmd/time-entry/edit-multipple/edit-multiple.go ================================================ package editmultiple import ( "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" output "github.com/lucassabreu/clockify-cli/pkg/output/time-entry" "github.com/lucassabreu/clockify-cli/pkg/timeentryhlp" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/spf13/cobra" ) // NewCmdEditMultiple represents the editMultiple command func NewCmdEditMultiple(f cmdutil.Factory) *cobra.Command { of := util.OutputFlags{TimeFormat: output.TimeFormatSimple} cmd := &cobra.Command{ Use: "edit-multiple { | " + timeentryhlp.AliasCurrent + " | " + timeentryhlp.AliasLast + " }...", Aliases: []string{ "update-multiple", "multi-edit", "multi-update", "mult-edit", "mult-update", }, Args: cobra.MatchAll( cmdutil.RequiredNamedArgs("time entry id"), cobra.MinimumNArgs(2), ), ValidArgs: []string{timeentryhlp.AliasLast, timeentryhlp.AliasCurrent}, Short: `Edit multiple time entries at once`, Long: heredoc.Docf(` Edit multiple time entries at once. This command does not allow to edit when the time entries start or ended, because different time entries will have different start and end times. Except on interactive mode where the values informed, even if not changed will be applied to all entries (except for Start and End time). If you wanna edit only some properties, than use the flags without interactive mode, only the input sent thought the flags will be changed. %s %s %s %s `, util.HelpTimeEntriesAliasForEdit, util.HelpInteractiveByDefault, util.HelpNamesForIds, util.HelpMoreInfoAboutPrinting, ), Example: heredoc.Docf(` # just to help show the data $ export F="{{.ID}} :: {{ .Description }} When: {{ fdt .TimeInterval.Start }} util {{ ft (.TimeInterval.End | now) }} Task: {{ .Task.Name }} ({{ .Project.Name}}) Tags: {{ .Tags }} " $ %[1]s report --format "$F" 62af667c4ebb4f143c9482bb :: Edit multiple entries When: 2022-06-19 18:10:01 util 18:10:15 Task: Edit Command (Clockify Cli) Tags: [Development (62ae28b72518aa18da2acb49)] 62af668b49445270d7c092e4 :: Adding examples When: 2022-06-19 18:10:15 util 18:29:32 Task: Edit Command (Clockify Cli) Tags: [Development (62ae28b72518aa18da2acb49)] 62af6b0f4ebb4f143c94880e :: More examples When: 2022-06-19 18:29:32 util 18:38:12 Task: Edit Command (Clockify Cli) Tags: [Development (62ae28b72518aa18da2acb49)] # change all to use other task $ %[1]s edit-multiple -i=0 -f "$F" current last ^2 --task multiple 62af6b0f4ebb4f143c94880e :: More examples When: 2022-06-19 18:29:32 util 18:43:04 Task: Edit Multiple Command (Clockify Cli) Tags: [Development (62ae28b72518aa18da2acb49)] 62af668b49445270d7c092e4 :: Adding examples When: 2022-06-19 18:10:15 util 18:29:32 Task: Edit Multiple Command (Clockify Cli) Tags: [Development (62ae28b72518aa18da2acb49)] 62af668b49445270d7c092e4 :: Adding examples When: 2022-06-19 18:10:15 util 18:29:32 Task: Edit Multiple Command (Clockify Cli) Tags: [Development (62ae28b72518aa18da2acb49)] `, "clockify-cli"), RunE: func(cmd *cobra.Command, args []string) error { var err error var w, u string if w, err = f.GetWorkspaceID(); err != nil { return err } if u, err = f.GetUserID(); err != nil { return err } c, err := f.Client() if err != nil { return err } teis := make([]util.TimeEntryDTO, len(args)) for i := range args { t, err := timeentryhlp.GetTimeEntry(c, w, u, args[i]) if err != nil { return err } teis[i] = util.TimeEntryImplToDTO(t) } tei := teis[0] editFn := func(tei util.TimeEntryDTO) (util.TimeEntryDTO, error) { t, err := c.UpdateTimeEntry(api.UpdateTimeEntryParam{ Workspace: tei.Workspace, TimeEntryID: tei.ID, Description: tei.Description, Start: tei.Start, End: tei.End, Billable: *tei.Billable, ProjectID: tei.ProjectID, TaskID: tei.TaskID, TagIDs: tei.TagIDs, }) return util.TimeEntryImplToDTO(t), err } fn := func(input util.TimeEntryDTO) (util.TimeEntryDTO, error) { var err error for i, tei := range teis { input.Start = tei.Start input.End = tei.End input.ID = tei.ID if tei, err = editFn(input); err != nil { return input, err } teis[i] = tei } return input, err } if !f.Config().IsInteractive() { fn = func(input util.TimeEntryDTO) (util.TimeEntryDTO, error) { c := cmd.Flags().Changed for i, tei := range teis { if c("project") { tei.ProjectID = input.ProjectID } if c("description") { tei.Description = input.Description } if c("task") { tei.TaskID = input.TaskID } if c("tag") || c("tags") { tei.TagIDs = input.TagIDs } if c("not-billable") { tei.Billable = input.Billable } teis[i] = tei if _, err = editFn(tei); err != nil { return tei, err } } return input, nil } } dc := util.NewDescriptionCompleter(f) if _, err = util.Do( tei, util.FillTimeEntryWithFlags(cmd.Flags()), util.GetAllowNameForIDsFn(f.Config(), c), util.GetPropsInteractiveFn(dc, f), util.GetValidateTimeEntryFn(f), fn, ); err != nil { return err } tes := make([]dto.TimeEntry, len(teis)) var t *dto.TimeEntry for i, tei := range teis { t, err = c.GetHydratedTimeEntry(api.GetTimeEntryParam{ TimeEntryID: tei.ID, Workspace: tei.Workspace, }) if err != nil { return err } tes[i] = *t } return util.PrintTimeEntries(tes, cmd.OutOrStdout(), f.Config(), of) }, } util.AddTimeEntryFlags(cmd, f, &of) util.AddPrintMultipleTimeEntriesFlags(cmd) return cmd } ================================================ FILE: pkg/cmd/time-entry/in/in.go ================================================ package in import ( "io" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" output "github.com/lucassabreu/clockify-cli/pkg/output/time-entry" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/spf13/cobra" ) // NewCmdIn represents the in command func NewCmdIn( f cmdutil.Factory, report func(dto.TimeEntryImpl, io.Writer, util.OutputFlags) error, ) *cobra.Command { of := util.OutputFlags{TimeFormat: output.TimeFormatSimple} cmd := &cobra.Command{ Use: "in [] []", Short: "Create a new Clockify time entry ", Long: heredoc.Doc(` Create a new Clockify time entry Running time entry will be stopped using the start time of this new entry. `) + "\n" + util.HelpTimeEntryNowIfNotSet + "\n" + util.HelpInteractiveByDefault + "\n" + util.HelpTimeInputOnTimeEntry + "\n" + util.HelpNamesForIds + "\n" + util.HelpValidateIncomplete + "\n" + util.HelpMoreInfoAboutPrinting, Args: cobra.MaximumNArgs(2), ValidArgsFunction: cmdcompl.CombineSuggestionsToArgs( cmdcomplutil.NewProjectAutoComplete(f, f.Config())), Aliases: []string{"start"}, Example: heredoc.Docf(` # start a timer with project and description, starting now $ %[1]s -i=0 "Clockify CLI" "Documenting in command" +--------------------------+----------+----------+---------+--------------+------------------------+------+ | ID | START | END | DUR | PROJECT | DESCRIPTION | TAGS | +--------------------------+----------+----------+---------+--------------+------------------------+------+ | 62ae2744c22de9759e73d038 | 13:28:01 | 13:28:04 | 0:00:03 | Clockify Cli | Documenting in command | | +--------------------------+----------+----------+---------+--------------+------------------------+------+ # start a timer with description, starting at 14:00 $ %[1]s -i=0 -d "Documenting in command" -s "14:00" +--------------------------+----------+----------+---------+---------+------------------------+------+ | ID | START | END | DUR | PROJECT | DESCRIPTION | TAGS | +--------------------------+----------+----------+---------+---------+------------------------+------+ | 62ae27cd49445270d7bf0333 | 14:00:00 | 14:30:21 | 0:30:21 | | Documenting in command | | +--------------------------+----------+----------+---------+---------+------------------------+------+ # start a timer with description, project and tags, starting 10 min ago $ %[1]s -i=0 -p 621948458cb9606d934ebb1c -d "Documenting in command" -s -10m --tag dev +--------------------------+----------+----------+---------+--------------+------------------------+--------------------------------+ | ID | START | END | DUR | PROJECT | DESCRIPTION | TAGS | +--------------------------+----------+----------+---------+--------------+------------------------+--------------------------------+ | 62ae29104ebb4f143c92f458 | 14:25:41 | 14:35:44 | 0:10:03 | Clockify Cli | Documenting in command | Development | | | | | | | | (62ae28b72518aa18da2acb49) | +--------------------------+----------+----------+---------+--------------+------------------------+--------------------------------+ # start a timer with description, project and task, starting at 10 min, but only showing its ID $ %[1]s -i=0 -p 621948458cb9606d934ebb1c -d "Documenting in command" -s -10m --task "in command" 62ae29fdc22de9759e73d343 # start a timer without description, with task and project $ %[1]s -i=0 -p 621948458cb9606d934ebb1c -s -10m --task "in command" 62ae29fdc22de9759e73d343 # start a timer interactively $ %[1]s -i ? Choose your project: 621948458cb9606d934ebb1c - Clockify Cli | Client: Myself (6202634a28782767054eec26) ? Choose your task: 62ae29e62518aa18da2acd14 - In Command ? Description: Adding more examples ? Choose your tags: 62ae28b72518aa18da2acb49 - Development, 621948708cb9606d934ebba7 - Pair Programming ? Start: now ? End (leave it blank for empty): +--------------------------+----------+----------+---------+--------------+----------------------+-----------------------------------------+ | ID | START | END | DUR | PROJECT | DESCRIPTION | TAGS | +--------------------------+----------+----------+---------+--------------+----------------------+-----------------------------------------+ | 62ae37b84ebb4f143c930523 | 17:38:14 | 17:38:17 | 0:00:03 | Clockify Cli | Adding more examples | Pair Programming | | | | | | | | (621948708cb9606d934ebba7) Development | | | | | | | | (62ae28b72518aa18da2acb49) | +--------------------------+----------+----------+---------+--------------+----------------------+-----------------------------------------+ `, "clockify-cli in"), RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } var err error tei := util.TimeEntryDTO{ Start: timehlp.Now(), } if tei.Workspace, err = f.GetWorkspaceID(); err != nil { return err } if tei.UserID, err = f.GetUserID(); err != nil { return err } c, err := f.Client() if err != nil { return err } if len(args) > 0 { tei.ProjectID = args[0] } if len(args) > 1 { tei.Description = args[1] } dc := util.NewDescriptionCompleter(f) if tei, err = util.Do( tei, util.FillTimeEntryWithFlags(cmd.Flags()), util.ValidateClosingTimeEntry(f), util.GetAllowNameForIDsFn(f.Config(), c), util.GetPropsInteractiveFn(dc, f), util.GetDatesInteractiveFn(f), util.FillMissingBillableFn(c), util.GetValidateTimeEntryFn(f), util.OutInProgressFn(c), util.CreateTimeEntryFn(c), ); err != nil { return err } return report( util.TimeEntryDTOToImpl(tei), cmd.OutOrStdout(), of) }, } util.AddTimeEntryFlags(cmd, f, &of) util.AddTimeEntryDateFlags(cmd) return cmd } ================================================ FILE: pkg/cmd/time-entry/in/in_test.go ================================================ package in_test import ( "bytes" "errors" "io" "testing" "time" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/in" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/stretchr/testify/assert" ) var w = dto.Workspace{ID: "w"} func TestNewCmdIn_ShouldBeBothBillableAndNotBillable(t *testing.T) { f := mocks.NewMockFactory(t) f.EXPECT().GetUserID().Return("u", nil) f.EXPECT().GetWorkspaceID().Return(w.ID, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) c := mocks.NewMockClient(t) f.EXPECT().Client().Return(c, nil) called := false cmd := in.NewCmdIn(f, func( _ dto.TimeEntryImpl, _ io.Writer, _ util.OutputFlags) error { called = true return nil }) cmd.SilenceUsage = true cmd.SilenceErrors = true out := bytes.NewBufferString("") cmd.SetOut(out) cmd.SetErr(out) cmd.SetArgs([]string{"--billable", "--not-billable"}) _, err := cmd.ExecuteC() if assert.Error(t, err) { assert.False(t, called) flagErr := &cmdutil.FlagError{} assert.ErrorAs(t, err, &flagErr) return } t.Fatal("should've failed") } func TestNewCmdIn_ShouldNotSetBillable_WhenNotAsked(t *testing.T) { bTrue := true bFalse := false tts := []struct { name string args []string param api.CreateTimeEntryParam }{ { name: "should be nil", args: []string{"-s=08:00"}, param: api.CreateTimeEntryParam{ Workspace: w.ID, Start: timehlp.Today().Add(8 * time.Hour), Billable: nil, }, }, { name: "should be billable", args: []string{"-s=08:00", "--billable"}, param: api.CreateTimeEntryParam{ Workspace: w.ID, Start: timehlp.Today().Add(8 * time.Hour), Billable: &bTrue, }, }, { name: "should not be billable", args: []string{"-s=08:00", "--not-billable"}, param: api.CreateTimeEntryParam{ Workspace: w.ID, Start: timehlp.Today().Add(8 * time.Hour), Billable: &bFalse, }, }, } for i := range tts { tt := &tts[i] t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) f.EXPECT().GetUserID().Return("u", nil) f.EXPECT().GetWorkspace().Return(w, nil) f.EXPECT().GetWorkspaceID().Return(w.ID, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, }) c := mocks.NewMockClient(t) f.EXPECT().Client().Return(c, nil) c.EXPECT().GetTimeEntryInProgress(api.GetTimeEntryInProgressParam{ Workspace: w.ID, UserID: "u", }). Return(nil, nil) c.EXPECT().Out(api.OutParam{ Workspace: w.ID, UserID: "u", End: tt.param.Start, }).Return(api.ErrorNotFound) c.EXPECT().CreateTimeEntry(tt.param). Return(dto.TimeEntryImpl{ID: "te"}, nil) called := false cmd := in.NewCmdIn(f, func( _ dto.TimeEntryImpl, _ io.Writer, _ util.OutputFlags) error { called = true return nil }) cmd.SilenceUsage = true cmd.SilenceErrors = true out := bytes.NewBufferString("") cmd.SetOut(out) cmd.SetErr(out) cmd.SetArgs(append(tt.args, "-q")) _, err := cmd.ExecuteC() if assert.NoError(t, err) { t.Cleanup(func() { assert.True(t, called) }) return } t.Fatalf("err: %s", err) }) } } func TestNewCmdIn_ShouldLookupProject_WithAndWithoutClient(t *testing.T) { defaultStart := timehlp.Today().Add(8 * time.Hour) bFalse := false projects := []dto.Project{ {ID: "p1", Name: "first", ClientID: "c1", ClientName: "other"}, {ID: "p2", Name: "second", ClientID: "c2", ClientName: "me"}, {ID: "p3", Name: "second", ClientID: "c3", ClientName: "clockify"}, {ID: "p4", Name: "third"}, {ID: "p5", Name: "notonclient", ClientID: "c3", ClientName: "clockify"}, } tts := []struct { name string args []string param api.CreateTimeEntryParam err error }{ { name: "only project", args: []string{"-s=08:00", "-p=first"}, param: api.CreateTimeEntryParam{ Workspace: w.ID, Start: defaultStart, ProjectID: projects[0].ID, Billable: &bFalse, }, }, { name: "project and client", args: []string{"-s=08:00", "-p=second", "-c=me"}, param: api.CreateTimeEntryParam{ Workspace: w.ID, Start: defaultStart, ProjectID: projects[1].ID, Billable: &bFalse, }, }, { name: "project and other client", args: []string{"-s=08:00", "-p=second", "-c=clockify"}, param: api.CreateTimeEntryParam{ Workspace: w.ID, Start: defaultStart, ProjectID: projects[2].ID, Billable: &bFalse, }, }, { name: "project without client", args: []string{"-s=08:00", "-p=third"}, param: api.CreateTimeEntryParam{ Workspace: w.ID, Start: defaultStart, ProjectID: projects[3].ID, Billable: &bFalse, }, }, { name: "project does not exist", args: []string{"-s=08:00", "-p=notfound"}, err: errors.New( "No project with id or name containing 'notfound' " + "was found"), }, { name: "project does not exist in this client", args: []string{"-s=08:00", "-p=notonclient", "-c=me"}, err: errors.New( "No project with id or name containing 'notonclient' " + "was found for client 'me'"), }, { name: "project with client name does not exist", args: []string{"-s=08:00", "-p", "notonclient me"}, err: errors.New( "No project with id or name containing 'notonclient me' " + "was found"), }, { name: "project and client's name", args: []string{"-s=08:00", "-p", "sec me"}, param: api.CreateTimeEntryParam{ Workspace: w.ID, Start: defaultStart, ProjectID: projects[1].ID, Billable: &bFalse, }, }, { name: "project and client's name (other)", args: []string{"-s=08:00", "-p", "sec cloc"}, param: api.CreateTimeEntryParam{ Workspace: w.ID, Start: defaultStart, ProjectID: projects[2].ID, Billable: &bFalse, }, }, } for i := range tts { tt := &tts[i] t.Run(tt.name, func(t *testing.T) { f := mocks.NewMockFactory(t) f.EXPECT().GetUserID().Return("u", nil) f.EXPECT().GetWorkspaceID().Return(w.ID, nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, SearchProjectWithClientsName: true, }) c := mocks.NewMockClient(t) f.EXPECT().Client().Return(c, nil) c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: w.ID, PaginationParam: api.AllPages(), }). Return(projects, nil) c.EXPECT().GetTimeEntryInProgress(api.GetTimeEntryInProgressParam{ Workspace: w.ID, UserID: "u", }). Return(nil, nil) if tt.err == nil { c.EXPECT().GetProject(api.GetProjectParam{ Workspace: w.ID, ProjectID: tt.param.ProjectID, }). Return(&dto.Project{ID: tt.param.ProjectID}, nil) f.EXPECT().GetWorkspace().Return(w, nil) c.EXPECT().Out(api.OutParam{ Workspace: w.ID, UserID: "u", End: tt.param.Start, }).Return(api.ErrorNotFound) c.EXPECT().CreateTimeEntry(tt.param). Return(dto.TimeEntryImpl{ID: "te"}, nil) } called := false cmd := in.NewCmdIn(f, func( _ dto.TimeEntryImpl, _ io.Writer, _ util.OutputFlags) error { called = true return nil }) cmd.SilenceUsage = true cmd.SilenceErrors = true out := bytes.NewBufferString("") cmd.SetOut(out) cmd.SetErr(out) cmd.SetArgs(append(tt.args, "-q")) _, err := cmd.ExecuteC() if tt.err != nil { assert.EqualError(t, err, tt.err.Error()) return } t.Cleanup(func() { assert.True(t, called) }) if assert.NoError(t, err) { return } t.Fatalf("err: %s", err) }) } } ================================================ FILE: pkg/cmd/time-entry/invoiced/invoiced.go ================================================ package invoiced import ( "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" output "github.com/lucassabreu/clockify-cli/pkg/output/time-entry" "github.com/lucassabreu/clockify-cli/pkg/timeentryhlp" "github.com/lucassabreu/clockify-cli/strhlp" "github.com/spf13/cobra" ) // NewCmdInvoiced represents invoiced command func NewCmdInvoiced(f cmdutil.Factory) []*cobra.Command { of := util.OutputFlags{TimeFormat: output.TimeFormatSimple} addCmd := func(cmd *cobra.Command) *cobra.Command { util.AddPrintTimeEntriesFlags(cmd, &of) util.AddPrintMultipleTimeEntriesFlags(cmd) return cmd } va := cmdcompl.ValidArgsSlide{ timeentryhlp.AliasLast, timeentryhlp.AliasCurrent} use := "{ | " + va.IntoUseOptions() + " }..." args := cmdutil.RequiredNamedArgs("time entry id") return []*cobra.Command{ addCmd(&cobra.Command{ Use: "mark-invoiced " + use, Short: "Marks times entries as invoiced", Long: "Marks times entries as invoiced\n\n" + util.HelpMoreInfoAboutPrinting, Example: heredoc.Docf(` # when the workspace does not allow invoicing $ %[1]s 62b49641f4b27f4ed7d20e75 Forbidden (code: 403) # set the running time entry as invoiced $ %[1]s current --quiet 62b49641f4b27f4ed7d20e75 # setting multiple time entries as invoiced $ %[1]s 62b5b51085815e619d7ae18d 62b5d55185815e619d7af928 --quiet 62b5b51085815e619d7ae18d 62b5d55185815e619d7af928 `, "clockify-cli mark-invoiced"), Args: args, ValidArgs: va, RunE: changeInvoiced(f, &of, true), }), addCmd(&cobra.Command{ Use: "mark-not-invoiced " + use, Short: "Mark times entries as not invoiced", Long: "Mark times entries as not invoiced\n\n" + util.HelpMoreInfoAboutPrinting, Example: heredoc.Docf(` # when the workspace does not allow invoicing $ %[1]s 62b49641f4b27f4ed7d20e75 Forbidden (code: 403) # set the running time entry as not invoiced $ %[1]s current --quiet 62b49641f4b27f4ed7d20e75 # setting multiple time entries as not invoiced $ %[1]s 62b5b51085815e619d7ae18d 62b5d55185815e619d7af928 --quiet 62b5b51085815e619d7ae18d 62b5d55185815e619d7af928 `, "clockify-cli mark-not-invoiced"), Args: args, ValidArgs: va, RunE: changeInvoiced(f, &of, false), }), } } func changeInvoiced( f cmdutil.Factory, of *util.OutputFlags, invoiced bool, ) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } var err error var w, u string if w, err = f.GetWorkspaceID(); err != nil { return err } if u, err = f.GetUserID(); err != nil { return err } c, err := f.Client() if err != nil { return err } args = strhlp.Unique(args) tes := make([]dto.TimeEntry, len(args)) for i, id := range args { if id == timeentryhlp.AliasCurrent || id == timeentryhlp.AliasLast { tei, err := timeentryhlp.GetTimeEntry(c, w, u, id) if err != nil { return err } id = tei.ID args[i] = id } te, err := c.GetHydratedTimeEntry(api.GetTimeEntryParam{ Workspace: w, TimeEntryID: id, }) if err != nil { return err } tes[i] = *te } if err := c.ChangeInvoiced(api.ChangeInvoicedParam{ Workspace: w, TimeEntryIDs: args, Invoiced: invoiced, }); err != nil { return err } return util.PrintTimeEntries(tes, cmd.OutOrStdout(), f.Config(), *of) } } ================================================ FILE: pkg/cmd/time-entry/manual/manual.go ================================================ package manual import ( "fmt" "time" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" output "github.com/lucassabreu/clockify-cli/pkg/output/time-entry" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/spf13/cobra" ) // NewCmdManual represents the manual command func NewCmdManual(f cmdutil.Factory) *cobra.Command { of := util.OutputFlags{TimeFormat: output.TimeFormatSimple} cmd := &cobra.Command{ Use: "manual [] [] [] []", Short: "Create a new complete time entry", Long: heredoc.Doc(` Create a new complete time entry with start and end. This command will not stop running time entries. The rules defined in the workspace and project will be checked before creating it. `) + "\n" + util.HelpTimeEntryNowIfNotSet + "The same applies to end time (`--when-to-close`).\n\n" + util.HelpInteractiveByDefault + "\n" + util.HelpTimeInputOnTimeEntry + "\n" + util.HelpNamesForIds + "\n" + util.HelpMoreInfoAboutStarting + "\n" + util.HelpMoreInfoAboutPrinting, Args: cobra.MaximumNArgs(4), ValidArgsFunction: cmdcompl.CombineSuggestionsToArgs( cmdcomplutil.NewProjectAutoComplete(f, f.Config())), RunE: func(cmd *cobra.Command, args []string) error { var whenToCloseDate time.Time var err error tei := util.TimeEntryDTO{ Start: timehlp.Now(), } if tei.Workspace, err = f.GetWorkspaceID(); err != nil { return err } if tei.UserID, err = f.GetUserID(); err != nil { return err } c, err := f.Client() if err != nil { return err } if len(args) > 0 { tei.ProjectID = args[0] } if len(args) > 1 { tei.Start, err = timehlp.ConvertToTime(args[1]) if err != nil { return fmt.Errorf( "fail to convert when to start: %w", err) } } if len(args) > 2 { whenToCloseDate, err = timehlp.ConvertToTime(args[2]) if err != nil { return fmt.Errorf( "fail to convert when to end: %w", err) } tei.End = &whenToCloseDate } if len(args) > 3 { tei.Description = args[3] } dc := util.NewDescriptionCompleter(f) if tei, err = util.Do( tei, util.FillTimeEntryWithFlags(cmd.Flags()), func(tei util.TimeEntryDTO) (util.TimeEntryDTO, error) { if tei.End != nil { return tei, nil } now, _ := timehlp.ConvertToTime(timehlp.NowTimeFormat) tei.End = &now return tei, nil }, util.GetAllowNameForIDsFn(f.Config(), c), util.GetPropsInteractiveFn(dc, f), util.GetDatesInteractiveFn(f), util.ValidateClosingTimeEntry(f), util.CreateTimeEntryFn(c), ); err != nil { return err } return util.PrintTimeEntryImpl( util.TimeEntryDTOToImpl(tei), f, cmd.OutOrStdout(), of) }, } util.AddTimeEntryFlags(cmd, f, &of) util.AddTimeEntryDateFlags(cmd) return cmd } ================================================ FILE: pkg/cmd/time-entry/out/out.go ================================================ package out import ( "errors" "time" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" output "github.com/lucassabreu/clockify-cli/pkg/output/time-entry" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/spf13/cobra" ) // NewCmdOut represents the out command func NewCmdOut(f cmdutil.Factory) *cobra.Command { of := util.OutputFlags{TimeFormat: output.TimeFormatSimple} cmd := &cobra.Command{ Use: "out", Short: "Stops the running time entry", Long: heredoc.Docf(` Stops the running time entry. If no value is set on %[1]s--when%[1]s, then current time will be used. When setting the end time you can use any of the following formats to set it: %[2]s Use %[1]sclockify-cli edit current%[1]s to edit any properties before ending it. %[3]s `, "`", util.HelpDateTimeFormats, util.HelpMoreInfoAboutPrinting, ), Example: heredoc.Docf(` # stop running time entry with current time $ %[1]s out --md ID: %[2]s62af6b0f4ebb4f143c94880e%[2]s Billable: %[2]syes%[2]s Locked: %[2]sno%[2]s Project: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s) Task: Out Command (%[2]s62af66454ebb4f143c948263%[2]s) Interval: %[2]s2022-06-19 18:29:32%[2]s until %[2]s2022-06-19 18:52:13%[2]s Description: > Adding examples Tags: * Development (%[2]s62ae28b72518aa18da2acb49%[2]s) # clone last and stopping it in 10 minutes $ %[1]s clone last -i=0 -d 'More examples' -q 62af70d849445270d7c09fbd $ %[1]s out --when +10m --md ID: %[2]s62af70d849445270d7c09fbd%[2]s Billable: %[2]syes%[2]s Locked: %[2]sno%[2]s Project: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s) Task: Out Command (%[2]s62af666349445270d7c09285%[2]s) Interval: %[2]s2022-06-19 18:54:12%[2]s until %[2]s2022-06-19 19:08:26%[2]s Description: > More examples Tags: * Development (%[2]s62ae28b72518aa18da2acb49%[2]s) `, "clockify-cli", "`"), RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } var whenDate time.Time var err error whenString, _ := cmd.Flags().GetString("when") if whenDate, err = timehlp.ConvertToTime(whenString); err != nil { return err } c, err := f.Client() if err != nil { return err } userID, err := f.GetUserID() if err != nil { return err } w, err := f.GetWorkspaceID() if err != nil { return err } te, err := c.GetHydratedTimeEntryInProgress( api.GetTimeEntryInProgressParam{ Workspace: w, UserID: userID, }) if te == nil && err == nil { return errors.New("no time entry in progress") } if err != nil { return err } if err = c.Out(api.OutParam{ Workspace: w, UserID: userID, End: whenDate, }); err != nil { return err } te.TimeInterval.End = &whenDate return util.PrintTimeEntry(te, cmd.OutOrStdout(), f.Config(), of) }, } util.AddPrintTimeEntriesFlags(cmd, &of) cmd.Flags().String("when", time.Now().Format(timehlp.FullTimeFormat), "when the entry should be closed, "+ "if not informed will use current time") return cmd } ================================================ FILE: pkg/cmd/time-entry/report/last-day/last-day.go ================================================ package lastday import ( "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/timeentryhlp" "github.com/spf13/cobra" ) // NewCmdLastDay represents the report last-day command func NewCmdLastDay(f cmdutil.Factory) *cobra.Command { of := util.NewReportFlags() cmd := &cobra.Command{ Use: "last-day", Short: "List time entries from last day were a time entry was created", RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } w, err := f.GetWorkspaceID() if err != nil { return err } u, err := f.GetUserID() if err != nil { return err } c, err := f.Client() if err != nil { return err } te, err := timeentryhlp.GetLatestEntryEntry(c, w, u) if err != nil { return err } return util.ReportWithRange( f, te.TimeInterval.Start, te.TimeInterval.Start, cmd.OutOrStdout(), of) }, } cmd.Long = cmd.Short + "\n\n" + util.HelpNamesForIds + "\n" + util.HelpMoreInfoAboutPrinting util.AddReportFlags(f, cmd, &of) return cmd } ================================================ FILE: pkg/cmd/time-entry/report/last-month/last-month.go ================================================ package lastmonth import ( "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/spf13/cobra" ) // NewCmdLastMonth represents the report last-month command func NewCmdLastMonth(f cmdutil.Factory) *cobra.Command { of := util.NewReportFlags() cmd := &cobra.Command{ Use: "last-month", Short: "List all time entries in last month", RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } first, last := timehlp.GetMonthRange( timehlp.Today().AddDate(0, -1, 0)) return util.ReportWithRange(f, first, last, cmd.OutOrStdout(), of) }, } cmd.Long = cmd.Short + "\n\n" + util.HelpNamesForIds + "\n" + util.HelpMoreInfoAboutPrinting util.AddReportFlags(f, cmd, &of) return cmd } ================================================ FILE: pkg/cmd/time-entry/report/last-week/last-week.go ================================================ package lastweek import ( "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/spf13/cobra" ) // NewCmdLastWeek represents the report last-week command func NewCmdLastWeek(f cmdutil.Factory) *cobra.Command { of := util.NewReportFlags() cmd := &cobra.Command{ Use: "last-week", Short: "List all time entries in last week", RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } first, last := timehlp.GetWeekRange( timehlp.TruncateDate(timehlp.Today()).AddDate(0, 0, -7)) return util.ReportWithRange(f, first, last, cmd.OutOrStdout(), of) }, } cmd.Long = cmd.Short + "\n\n" + util.HelpNamesForIds + "\n" + util.HelpMoreInfoAboutPrinting util.AddReportFlags(f, cmd, &of) return cmd } ================================================ FILE: pkg/cmd/time-entry/report/last-week-day/last-week-day.go ================================================ package lastweekday import ( "errors" "strings" "time" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/lucassabreu/clockify-cli/strhlp" "github.com/spf13/cobra" ) // NewCmdLastWeekDay represents the report last working week day command func NewCmdLastWeekDay(f cmdutil.Factory) *cobra.Command { of := util.NewReportFlags() cmd := &cobra.Command{ Use: "last-week-day", Short: "List time entries from last week day", Long: heredoc.Docf(` List time entries from last week day For the CLI to know which days of the week you are expected to work, you will need to set them. This can be done using: $ clockify-cli config init Or more directly by running the set command as follows: $ clockify-cli config set workweek-days monday,tuesday,wednesday,thursday,friday %s %s `, util.HelpNamesForIds, util.HelpMoreInfoAboutPrinting, ), RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } workweek := f.Config().GetWorkWeekdays() if len(workweek) == 0 { return errors.New("no workweek days were set") } day := timehlp.Today().Add(-1) if strhlp.Search( strings.ToLower(day.Weekday().String()), workweek) != -1 { return util.ReportWithRange(f, day, day, cmd.OutOrStdout(), of) } dayWeekday := int(day.Weekday()) if dayWeekday == int(time.Sunday) { dayWeekday = int(time.Saturday + 1) } lastWeekDay := int(time.Sunday) for _, w := range workweek { i := strhlp.Search(w, cmdutil.GetWeekdays()) if i > lastWeekDay && i < dayWeekday { lastWeekDay = i } } day = day.Add( time.Duration(-24*(dayWeekday-lastWeekDay)) * time.Hour) return util.ReportWithRange(f, day, day, cmd.OutOrStdout(), of) }, } util.AddReportFlags(f, cmd, &of) return cmd } ================================================ FILE: pkg/cmd/time-entry/report/report.go ================================================ package report import ( "time" "github.com/MakeNowJust/heredoc" lastday "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/last-day" lastmonth "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/last-month" lastweek "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/last-week" lastweekday "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/last-week-day" thismonth "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/this-month" thisweek "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/this-week" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/today" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/yesterday" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/spf13/cobra" ) // NewCmdReport represents the reports command func NewCmdReport(f cmdutil.Factory) *cobra.Command { of := util.NewReportFlags() cmd := &cobra.Command{ Use: "report [] []", Short: "List all time entries for a given date range", Long: heredoc.Docf(` List all time entries for a given date range If no parameter is set, shows today's time entries Aliases today/now can be used for argument to represent current date Alias yesterday can be used for argument to represent previous date To choose a specific date to start or end use the format "2006-01-02" %s All the subcommands have the same flags to filter and format the time entries, but will act as aliases to relative date ranges. `, util.HelpNamesForIds), Example: heredoc.Docf(` # reporting all time entries from today $ %[1]s +--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+ | ID | START | END | DUR | PROJECT | DESCRIPTION | TAGS | +--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+ | 62b87a9785815e619d7ce02e | 2022-06-26 12:25:56 | 2022-06-26 12:26:47 | 0:00:51 | Clockify Cli | Example for today | Development | | | | | | | | (62ae28b72518aa18da2acb49) | +--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+ | 62b87abb85815e619d7ce034 | 2022-06-26 12:26:47 | 2022-06-26 13:00:00 | 0:33:13 | Clockify Cli | Example for today (second one) | Development | | | | | | | | (62ae28b72518aa18da2acb49) | +--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+ | TOTAL | | | 0:34:04 | | | | +--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+ # reporting all time entries from 2022-06-24 to today $ %[1]s 2022-06-24 today +--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+ | ID | START | END | DUR | PROJECT | DESCRIPTION | TAGS | +--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+ | 62b8ce7185815e619d7d0a82 | 2022-06-24 08:00:00 | 2022-06-24 09:00:00 | 1:00:00 | Clockify Cli | Example for before yesterday | Development | | | | | | | | (62ae28b72518aa18da2acb49) | +--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+ | 62b8ce1edba0da0f21e7e688 | 2022-06-25 08:00:00 | 2022-06-25 09:00:00 | 1:00:00 | Clockify Cli | Example for yesterday | Development | | | | | | | | (62ae28b72518aa18da2acb49) | +--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+ | 62b87a9785815e619d7ce02e | 2022-06-26 12:25:56 | 2022-06-26 12:26:47 | 0:00:51 | Clockify Cli | Example for today | Development | | | | | | | | (62ae28b72518aa18da2acb49) | +--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+ | 62b87abb85815e619d7ce034 | 2022-06-26 12:26:47 | 2022-06-26 13:00:00 | 0:33:13 | Clockify Cli | Example for today (second one) | Development | | | | | | | | (62ae28b72518aa18da2acb49) | +--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+ | TOTAL | | | 2:34:04 | | | | +--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+ # when there are no entries for the range $ %[1]s 1999-01-01 +-------+-------+-----+---------+---------+-------------+------+ | ID | START | END | DUR | PROJECT | DESCRIPTION | TAGS | +-------+-------+-----+---------+---------+-------------+------+ | TOTAL | | | 0:00:00 | | | | +-------+-------+-----+---------+---------+-------------+------+ # format output with golang template $ %[1]s 2022-06-23 --format "{{.ID}} - {{ .TimeInterval.Duration }} - {{ pad .Project.Name 12 }} - {{ .Description }}" 62b8d162984dba2c06699e3f - PT1H - Clockify Cli - First example for report 62b8d195dba0da0f21e7e85d - PT1H - Special - Lunch break 62b8d207dba0da0f21e7e868 - PT1H - Clockify Cli - After lunch # the default functions from the text/template package from Go are available, and the following functions are also allowed: # # - formatDateTime(time.Time) => format date/times with %[3]s # - formatTime(time.Time) => format date/times with %[4]s # - json(interface{}) => encodes a value to json # - now(time.Time) => returns the argument or now if nil # - pad(s string, size int) => adds spaces to the end of a string until its length meets the size # - since(s time.Time, [e time.Time]) => returns the time difference between the first and second time (or now if not set) # - until(e time.Time, [s time.Time]) => returns the time difference between the second and first time (or now if not set) # - yaml(interface{}) => encodes a value to yaml # show time spent on the project "Clockify CLI" as float $ %[1]s 2022-06-23 --duration-float -p "clockify cli" 2.000000 # show time spent on the project "Clockify CLI" with "lunch" on description $ %[1]s 2022-06-23 --duration-formatted -p "clockify cli" -d lunch 1:00:00 # show ids from time entries from project "clockify cli" $ %[1]s 2022-06-23 -p "clockify cli" --quiet 62b8d162984dba2c06699e3f 62b8d207dba0da0f21e7e868 # show time entries from project "special" as markdown $ %[1]s 2022-06-23 -p "clockify cli" --quiet ID: %[2]s63b8d195dba0da0f21e7e85d%[2]s Billable: %[2]sno%[2]s Locked: %[2]sno%[2]s Project: Special (%[2]s6202680228782767055ef004%[2]s) Interval: %[2]s2023-06-23 15:00:00%[2]s until %[2]s2022-06-23 16:00:00%[2]s Description: > Lunch break Tags: * Meeting (%[2]s6219486e8cb9606d934ebb5f%[2]s) # csv format output $ %[1]s --csv id,description,project.id,project.name,task.id,task.name,start,end,duration,user.id,user.email,user.name,tags...,customFields... 62b87a9785815e619d7ce02e,Example for today,621948458cb9606d934ebb1c,Clockify Cli,62b87a7e984dba2c0669724d,Report Command,2022-06-26 12:25:56,2022-06-26 12:26:47,0:00:51,5c6bf21db079873a55facc08,joe@due.com,John Due,Development (62ae28b72518aa18da2acb49),Example custom field(5e1147fe8c526f38930d57b8)=value 62b87abb85815e619d7ce034,Example for today (second one),621948458cb9606d934ebb1c,Clockify Cli,62b87a7e984dba2c0669724d,Report Command,2022-06-26 12:26:47,2022-06-26 13:00:00,0:33:13,5c6bf21db079873a55facc08,joe@due.com,John Due,Development (62ae28b72518aa18da2acb49), Example custom field(5e1147fe8c526f38930d57b8)=value `, "clockify-cli report", "`", timehlp.FullTimeFormat, timehlp.OnlyTimeFormat, ), Args: cobra.MaximumNArgs(2), Aliases: []string{"log"}, RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } var err error start := timehlp.Today() if len(args) > 0 { start, err = time.Parse("2006-01-02", args[0]) if err != nil { return err } } end := start if len(args) > 1 { if args[1] == "now" || args[1] == "today" { end = timehlp.Today() } else if args[1] == "yesterday" { end = timehlp.Today().Add(-1) } else if end, err = time.Parse( "2006-01-02", args[1]); err != nil { return err } } return util.ReportWithRange( f, start, end, cmd.OutOrStdout(), of) }, } cmd.AddCommand(thismonth.NewCmdThisMonth(f)) cmd.AddCommand(lastmonth.NewCmdLastMonth(f)) cmd.AddCommand(thisweek.NewCmdThisWeek(f)) cmd.AddCommand(lastweek.NewCmdLastWeek(f)) cmd.AddCommand(lastday.NewCmdLastDay(f)) cmd.AddCommand(lastweekday.NewCmdLastWeekDay(f)) cmd.AddCommand(today.NewCmdToday(f)) cmd.AddCommand(yesterday.NewCmdYesterday(f)) util.AddReportFlags(f, cmd, &of) _ = cmd.MarkFlagRequired("workspace") _ = cmd.MarkFlagRequired("user-id") return cmd } ================================================ FILE: pkg/cmd/time-entry/report/this-month/this-month.go ================================================ package thismonth import ( "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/spf13/cobra" ) // NewCmdThisMonth represents the reports this-month command func NewCmdThisMonth(f cmdutil.Factory) *cobra.Command { of := util.NewReportFlags() cmd := &cobra.Command{ Use: "this-month", Short: "List all time entries in this month", RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } first, last := timehlp.GetMonthRange(timehlp.Today()) return util.ReportWithRange(f, first, last, cmd.OutOrStdout(), of) }, } cmd.Long = cmd.Short + "\n\n" + util.HelpNamesForIds + "\n" + util.HelpMoreInfoAboutPrinting util.AddReportFlags(f, cmd, &of) return cmd } ================================================ FILE: pkg/cmd/time-entry/report/this-week/this-week.go ================================================ package thisweek import ( "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/spf13/cobra" ) // NewCmdThisWeek represents the report this-week command func NewCmdThisWeek(f cmdutil.Factory) *cobra.Command { of := util.NewReportFlags() cmd := &cobra.Command{ Use: "this-week", Short: "List all time entries in this week", RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } first, last := timehlp.GetWeekRange(timehlp.Today()) return util.ReportWithRange(f, first, last, cmd.OutOrStdout(), of) }, } cmd.Long = cmd.Short + "\n\n" + util.HelpNamesForIds + "\n" + util.HelpMoreInfoAboutPrinting util.AddReportFlags(f, cmd, &of) return cmd } ================================================ FILE: pkg/cmd/time-entry/report/today/today.go ================================================ package today import ( "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/spf13/cobra" ) // NewCmdToday represents report today command func NewCmdToday(f cmdutil.Factory) *cobra.Command { of := util.NewReportFlags() cmd := &cobra.Command{ Use: "today", Short: "List all time entries created today", RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } today := timehlp.Today() return util.ReportWithRange(f, today, today, cmd.OutOrStdout(), of) }, } cmd.Long = cmd.Short + "\n\n" + util.HelpNamesForIds + "\n" + util.HelpMoreInfoAboutPrinting util.AddReportFlags(f, cmd, &of) return cmd } ================================================ FILE: pkg/cmd/time-entry/report/today/today_test.go ================================================ package today_test import ( "bytes" "errors" "strings" "testing" "time" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/today" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/stretchr/testify/assert" ) func TestCmdToday(t *testing.T) { first := time.Now() first = time.Date( first.Year(), first.Month(), first.Day(), 0, 0, 0, 0, time.UTC, ) last := first.AddDate(0, 0, 1) tts := []struct { name string args string err error expected string factory func(*testing.T) cmdutil.Factory }{ { name: "error on multi format", args: "--format {} --json --csv -q --md " + "--duration-float --duration-formatted", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) return f }, err: errors.New( "the following flags can't be used together: " + "`csv`, `duration-float`, `duration-formatted`, " + "`format`, `json`, `md` and `quiet`", ), }, { name: "all of them, but only ids", args: "-q", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.On("GetUserID").Return("user-id", nil) f.On("GetWorkspaceID").Return("w-id", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("LogRange", api.LogRangeParam{ Workspace: "w-id", UserID: "user-id", FirstDate: first, LastDate: last, TagIDs: []string{}, PaginationParam: api.AllPages(), }). Return( []dto.TimeEntry{{ID: "time-entry-id"}}, nil, ) return f }, expected: "time-entry-id\n", }, { name: "all of them, but fails", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) f.On("GetUserID").Return("user-id", nil) f.On("GetWorkspaceID").Return("w-id", nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("LogRange", api.LogRangeParam{ Workspace: "w-id", UserID: "user-id", FirstDate: first, LastDate: last, TagIDs: []string{}, PaginationParam: api.AllPages(), }). Return( []dto.TimeEntry{}, errors.New("failed"), ) return f }, err: errors.New("failed"), }, { name: "only project x, no results", args: "--project x", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("user-id", nil) f.On("GetWorkspaceID").Return("w-id", nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, }) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w-id", PaginationParam: api.AllPages(), }). Return( []dto.Project{{ ID: "project-id", Name: "xpecial", }}, nil, ) c.On("LogRange", api.LogRangeParam{ Workspace: "w-id", UserID: "user-id", FirstDate: first, LastDate: last, ProjectID: "project-id", TagIDs: []string{}, PaginationParam: api.AllPages(), }). Return( []dto.TimeEntry{}, errors.New("failed"), ) return f }, err: errors.New("failed"), }, { name: "only with desc on description", args: "--description desc -q", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("user-id", nil) f.On("GetWorkspaceID").Return("w-id", nil) f.On("Config").Return(&mocks.SimpleConfig{}) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("LogRange", api.LogRangeParam{ Workspace: "w-id", UserID: "user-id", FirstDate: first, LastDate: last, Description: "desc", TagIDs: []string{}, PaginationParam: api.AllPages(), }). Return( []dto.TimeEntry{ {ID: "time-entry-1"}, {ID: "time-entry-2"}, }, nil, ) return f }, expected: heredoc.Doc(` time-entry-1 time-entry-2 `), }, { name: "report only the first time entry", args: "--limit 2 --page 10 -q", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("user-id", nil) f.On("GetWorkspaceID").Return("w-id", nil) f.On("Config").Return(&mocks.SimpleConfig{}) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("LogRange", api.LogRangeParam{ Workspace: "w-id", UserID: "user-id", FirstDate: first, LastDate: last, TagIDs: []string{}, PaginationParam: api.PaginationParam{ Page: 10, PageSize: 2, }, }). Return( []dto.TimeEntry{ {ID: "time-entry-1"}, {ID: "time-entry-2"}, }, nil, ) return f }, expected: heredoc.Doc(` time-entry-1 time-entry-2 `), }, } for i := range tts { tt := tts[i] t.Run(tt.name, func(t *testing.T) { cmd := today.NewCmdToday(tt.factory(t)) cmd.SilenceUsage = true cmd.SilenceErrors = true cmd.SetArgs(strings.Split(tt.args, " ")) out := bytes.NewBufferString("") cmd.SetOut(out) cmd.SetErr(out) _, err := cmd.ExecuteC() if tt.err != nil { assert.Error(t, err) assert.EqualError(t, err, tt.err.Error()) return } assert.Equal(t, tt.expected, out.String()) assert.NoError(t, err) }) } } ================================================ FILE: pkg/cmd/time-entry/report/util/report.go ================================================ package util import ( "errors" "io" "sort" "time" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/search" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" ) const ( HelpNamesForIds = util.HelpNamesForIds HelpMoreInfoAboutPrinting = util.HelpMoreInfoAboutPrinting ) // ReportFlags reads the "shared" flags for report commands type ReportFlags struct { util.OutputFlags FillMissingDates bool Limit int Page int Billable bool NotBillable bool Description string Client string Projects []string TagIDs []string } // Check will assure that there is no conflicting flag values func (rf ReportFlags) Check() error { if err := rf.OutputFlags.Check(); err != nil { return err } if rf.Page > 0 && rf.Limit <= 0 { return cmdutil.FlagErrorWrap( errors.New("page can't be used without limit")) } if err := cmdutil.XorFlag(map[string]bool{ "limit": rf.Limit > 0, "fill-missing-dates": rf.FillMissingDates, }); err != nil { return err } return cmdutil.XorFlag(map[string]bool{ "billable": rf.Billable, "not-billable": rf.NotBillable, }) } // NewReportFlags helps creating a util.ReportFlags for report commands func NewReportFlags() ReportFlags { return ReportFlags{ Limit: 0, OutputFlags: util.OutputFlags{ TimeFormat: timehlp.FullTimeFormat, }, } } // AddReportFlags add flags for print out the time entries func AddReportFlags( f cmdutil.Factory, cmd *cobra.Command, rf *ReportFlags, ) { util.AddPrintTimeEntriesFlags(cmd, &rf.OutputFlags) util.AddPrintMultipleTimeEntriesFlags(cmd) cmd.Flags().IntVarP(&rf.Page, "page", "P", 0, "set which page to return") cmd.Flags().IntVarP(&rf.Limit, "limit", "l", 0, "Only look for this quantity of time entries") cmd.Flags().BoolVarP(&rf.FillMissingDates, "fill-missing-dates", "e", false, "Add empty lines for dates without time entries") cmd.Flags().StringVarP(&rf.Description, "description", "d", "", "will filter time entries that contains this on the description field") cmd.Flags().StringSliceVarP(&rf.Projects, "project", "p", []string{}, "Will filter time entries using this project") _ = cmdcompl.AddSuggestionsToFlag(cmd, "project", cmdcomplutil.NewProjectAutoComplete(f, f.Config())) cmd.Flags().StringVarP(&rf.Client, "client", "c", "", "Will filter projects from this client") _ = cmdcompl.AddSuggestionsToFlag(cmd, "project", cmdcomplutil.NewProjectAutoComplete(f, f.Config())) cmd.Flags().StringSliceVarP(&rf.TagIDs, "tag", "T", []string{}, "Will filter time entries using these tags") _ = cmdcompl.AddSuggestionsToFlag(cmd, "tag", cmdcomplutil.NewTagAutoComplete(f)) cmd.Flags().BoolVar(&rf.Billable, "billable", false, "Will filter time entries that are billable") cmd.Flags().BoolVar(&rf.NotBillable, "not-billable", false, "Will filter time entries that are not billable") } // ReportWithRange fetches and prints out time entries func ReportWithRange( f cmdutil.Factory, start, end time.Time, out io.Writer, rf ReportFlags, ) error { userId, err := f.GetUserID() if err != nil { return err } workspace, err := f.GetWorkspaceID() if err != nil { return err } c, err := f.Client() if err != nil { return err } cnf := f.Config() if len(rf.Projects) != 0 { if f.Config().IsAllowNameForID() { if rf.Projects, err = search.GetProjectsByName( c, cnf, workspace, rf.Client, rf.Projects); err != nil { return err } } } else if rf.Client != "" { if f.Config().IsAllowNameForID() { if rf.Client, err = search.GetClientByName( c, workspace, rf.Client); err != nil { return err } } ps, err := c.GetProjects(api.GetProjectsParam{ Workspace: workspace, Clients: []string{rf.Client}, Hydrate: false, PaginationParam: api.AllPages(), }) if err != nil { return err } rf.Projects = make([]string, len(ps)) for i := range ps { rf.Projects[i] = ps[i].ID } } if len(rf.TagIDs) > 0 && f.Config().IsAllowNameForID() { if rf.TagIDs, err = search.GetTagsByName( c, workspace, rf.TagIDs); err != nil { return err } } if len(rf.Projects) == 0 { rf.Projects = []string{""} } start = timehlp.TruncateDate(start) end = timehlp.TruncateDate(end).Add(time.Hour * 24) wg := errgroup.Group{} logs := make([][]dto.TimeEntry, len(rf.Projects)) pages := api.AllPages() if rf.Limit > 0 { pages = api.PaginationParam{ Page: 1, PageSize: rf.Limit, } if rf.Page > 0 { pages.Page = rf.Page } } for i := range rf.Projects { i := i wg.Go(func() error { var err error logs[i], err = c.LogRange(api.LogRangeParam{ Workspace: workspace, UserID: userId, FirstDate: start, LastDate: end, Description: rf.Description, ProjectID: rf.Projects[i], TagIDs: rf.TagIDs, PaginationParam: pages, }) return err }) } if err = wg.Wait(); err != nil { return err } log := make([]dto.TimeEntry, 0) for i := range logs { log = append(log, logs[i]...) } if rf.Billable || rf.NotBillable { log = filterBilling(log, rf.Billable) } sort.Slice(log, func(i, j int) bool { return log[j].TimeInterval.Start.After( log[i].TimeInterval.Start, ) }) if rf.Limit > 0 && len(log) > rf.Limit { log = log[len(log)-rf.Limit:] } if rf.FillMissingDates && len(log) > 0 { l := log log = make([]dto.TimeEntry, 0, len(l)) log = append(log, fillMissing(start, l[0].TimeInterval.Start)...) nextDay := start for i := range l { log = append(log, fillMissing(nextDay, l[i].TimeInterval.Start)...) log = append(log, l[i]) nextDay = l[i].TimeInterval.Start.Add( time.Duration(24-l[i].TimeInterval.Start.Hour()) * time.Hour) } log = append(log, fillMissing(nextDay, end)...) } return util.PrintTimeEntries( log, out, cnf, rf.OutputFlags) } func filterBilling(l []dto.TimeEntry, billable bool) []dto.TimeEntry { r := make([]dto.TimeEntry, 0, len(l)) for i := 0; i < len(l); i++ { if l[i].Billable == billable { r = append(r, l[i]) } } return r } func fillMissing(first, last time.Time) []dto.TimeEntry { first = timehlp.TruncateDate(first) last = timehlp.TruncateDate(last) d := int(last.Sub(first).Hours() / 24) if d <= 0 { return []dto.TimeEntry{} } missing := make([]dto.TimeEntry, d) for i := 0; i < d; i++ { t := missing[i] ti := first.AddDate(0, 0, i) t.TimeInterval.Start = ti t.TimeInterval.End = &ti missing[i] = t } return missing } ================================================ FILE: pkg/cmd/time-entry/report/util/report_flag_test.go ================================================ package util_test import ( "testing" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util" "github.com/stretchr/testify/assert" ) func TestReportFlags_Check(t *testing.T) { tts := map[string]struct { rf util.ReportFlags err string }{ "just billable": { rf: util.ReportFlags{ Billable: true, NotBillable: false, }, }, "just not-billable": { rf: util.ReportFlags{ Billable: false, NotBillable: true, }, }, "only one billable": { rf: util.ReportFlags{ Billable: true, NotBillable: true, }, err: "can't be used together.*billable.*not-billable", }, "just client": { rf: util.ReportFlags{ Client: "me", }, }, "just projects": { rf: util.ReportFlags{ Projects: []string{"mine"}, }, }, "client and project": { rf: util.ReportFlags{ Client: "me", Projects: []string{"mine"}, }, }, "fill missing dates": { rf: util.ReportFlags{ FillMissingDates: true, }, }, "limit": { rf: util.ReportFlags{ Limit: 10, }, }, "only limit or fill missing": { rf: util.ReportFlags{ Limit: 10, FillMissingDates: true, }, err: "can't be used together.*fill-missing-dates.*limit", }, "limit and page": { rf: util.ReportFlags{ Limit: 10, Page: 10, }, }, "page needs limit": { rf: util.ReportFlags{ Page: 10, }, err: "page can't be used without limit", }, } for name, tt := range tts { t.Run(name, func(t *testing.T) { err := tt.rf.Check() if tt.err == "" { assert.NoError(t, err) return } if !assert.Error(t, err) { return } assert.Regexp(t, tt.err, err.Error()) }) } } ================================================ FILE: pkg/cmd/time-entry/report/util/reportwithrange_test.go ================================================ package util_test import ( "bytes" "errors" "testing" "time" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/internal/mocks" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/stretchr/testify/assert" ) func newDate(s string) time.Time { date, _ := time.ParseInLocation("2006-01-02", s, time.UTC) return date } func TestReportWithRange(t *testing.T) { date := newDate("2006-01-02") first := time.Date( date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC, ) last := first.AddDate(0, 0, 3) tts := []struct { name string factory func(*testing.T) cmdutil.Factory flags func(*testing.T) util.ReportFlags expected string err string }{ { name: "no user", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("", errors.New("no user")) return f }, flags: func(t *testing.T) util.ReportFlags { return util.NewReportFlags() }, err: "no user", }, { name: "no workspace", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("", errors.New("no workspace")) return f }, flags: func(t *testing.T) util.ReportFlags { return util.NewReportFlags() }, err: "no workspace", }, { name: "no client", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) f.On("Client").Return(nil, errors.New("no client")) return f }, flags: func(t *testing.T) util.ReportFlags { return util.NewReportFlags() }, err: "no client", }, { name: "http error project", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Project{}, errors.New("http error")) return f }, flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.Projects = []string{"p"} return rf }, err: "http error", }, { name: "invalid project", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) cf.On("IsSearchProjectWithClientsName").Return(false) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Project{{Name: "right"}}, nil) return f }, flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.Projects = []string{"wrong"} return rf }, err: "No project.*wrong' was found", }, { name: "invalid client", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Project{{Name: "right"}}, nil) return f }, flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.Client = "right" rf.Projects = []string{"wrong"} return rf }, err: "No client.*right' was found", }, { name: "invalid project for client", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) cf.On("IsSearchProjectWithClientsName").Return(false) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return( []dto.Project{{ Name: "right", ClientName: "right", ClientID: "r1", }}, nil) return f }, flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.Client = "right" rf.Projects = []string{"wrong"} return rf }, err: "No project.*wrong' was found for client 'right'", }, { name: "range http error", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) cf := mocks.NewMockConfig(t) f.On("Config").Return(cf) cf.On("IsAllowNameForID").Return(true) cf.On("IsSearchProjectWithClientsName").Return(false) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("GetProjects", api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Project{ { ID: "p", Name: "right", ClientName: "right", ClientID: "c1", }, { ID: "p", Name: "right", ClientName: "wrong", ClientID: "c2", }, }, nil) c.On("LogRange", api.LogRangeParam{ Workspace: "w", UserID: "u", ProjectID: "p", FirstDate: first, LastDate: last, PaginationParam: api.AllPages(), }). Return([]dto.TimeEntry{}, errors.New("http error")) return f }, flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.Projects = []string{"right"} rf.Client = "right" return rf }, err: "http error", }, { name: "project and description", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: false, }) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("LogRange", api.LogRangeParam{ Workspace: "w", UserID: "u", ProjectID: "p", Description: "desc", FirstDate: first, LastDate: last, PaginationParam: api.AllPages(), }).Return([]dto.TimeEntry{ {ID: "time-entry-1", TimeInterval: dto.TimeInterval{Start: last}}, {ID: "time-entry-2", TimeInterval: dto.TimeInterval{Start: first}}, }, nil) return f }, flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.Projects = []string{"p"} rf.Description = "desc" rf.Quiet = true return rf }, expected: heredoc.Doc(` time-entry-2 time-entry-1 `), }, { name: "fill missing dates", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) f.On("Config").Return(&mocks.SimpleConfig{}) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("LogRange", api.LogRangeParam{ Workspace: "w", UserID: "u", FirstDate: first, LastDate: last, PaginationParam: api.AllPages(), }).Return([]dto.TimeEntry{ {ID: "time-entry-1", TimeInterval: dto.TimeInterval{ Start: newDate("2006-01-04")}}, {ID: "time-entry-2", TimeInterval: dto.TimeInterval{ Start: newDate("2006-01-01")}}, }, nil) return f }, flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.FillMissingDates = true rf.Format = "{{.ID}};{{ .TimeInterval.Start.Format " + `"2006-01-02"` + " }}" return rf }, expected: heredoc.Doc(` time-entry-2;2006-01-01 ;2006-01-02 ;2006-01-03 time-entry-1;2006-01-04 `), }, { name: "billable only", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("LogRange", api.LogRangeParam{ Workspace: "w", UserID: "u", FirstDate: first, LastDate: last, PaginationParam: api.AllPages(), }).Return([]dto.TimeEntry{ {ID: "time-entry-1", Billable: true}, {ID: "time-entry-2", Billable: false}, {ID: "time-entry-3", Billable: true}, }, nil) return f }, flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.Billable = true rf.Quiet = true return rf }, expected: heredoc.Doc(` time-entry-1 time-entry-3 `), }, { name: "not billable only", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{}) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("LogRange", api.LogRangeParam{ Workspace: "w", UserID: "u", FirstDate: first, LastDate: last, PaginationParam: api.AllPages(), }).Return([]dto.TimeEntry{ {ID: "time-entry-1", Billable: true}, {ID: "time-entry-2", Billable: false}, {ID: "time-entry-3", Billable: true}, }, nil) return f }, flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.NotBillable = true rf.Quiet = true return rf }, expected: heredoc.Doc(` time-entry-2 `), }, { name: "not billable & tag cli only", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) f.EXPECT().Config().Return(&mocks.SimpleConfig{ AllowNameForID: true, }) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) tag := dto.Tag{ID: "t1", Name: "Client"} c.On("GetTags", api.GetTagsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Tag{tag}, nil) c.On("LogRange", api.LogRangeParam{ Workspace: "w", UserID: "u", FirstDate: first, LastDate: last, TagIDs: []string{tag.ID}, PaginationParam: api.AllPages(), }).Return([]dto.TimeEntry{ {ID: "te-1", Tags: []dto.Tag{tag}, Billable: true}, {ID: "te-2", Tags: []dto.Tag{tag}, Billable: false}, {ID: "te-3", Tags: []dto.Tag{tag}, Billable: true}, {ID: "te-4", Tags: []dto.Tag{tag}, Billable: false}, }, nil) return f }, flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.NotBillable = true rf.Quiet = true rf.TagIDs = []string{"cli"} return rf }, expected: heredoc.Doc(` te-2 te-4 `), }, { name: "multiple projects", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) f.EXPECT().Config().Return( &mocks.SimpleConfig{AllowNameForID: true}) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: "w", PaginationParam: api.AllPages(), }).Return([]dto.Project{ {ID: "p1", Name: "p1"}, {ID: "p2", Name: "p2"}, {ID: "p3", Name: "p3"}, }, nil) c.EXPECT().LogRange(api.LogRangeParam{ Workspace: "w", UserID: "u", ProjectID: "p1", FirstDate: first, LastDate: last, PaginationParam: api.AllPages(), }).Return([]dto.TimeEntry{ {ID: "te-1", TimeInterval: dto.TimeInterval{ Start: first, }, }, {ID: "te-3", TimeInterval: dto.TimeInterval{ Start: first.Add(time.Duration(2)), }, }, }, nil) c.EXPECT().LogRange(api.LogRangeParam{ Workspace: "w", UserID: "u", ProjectID: "p2", FirstDate: first, LastDate: last, PaginationParam: api.AllPages(), }).Return([]dto.TimeEntry{ {ID: "te-2", TimeInterval: dto.TimeInterval{ Start: first.Add(time.Duration(1)), }, }, {ID: "te-4", TimeInterval: dto.TimeInterval{ Start: first.Add(time.Duration(3)), }, }, }, nil) return f }, flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.Quiet = true rf.Projects = []string{"p1", "p2"} return rf }, expected: heredoc.Doc(` te-1 te-2 te-3 te-4 `), }, { name: "projects form a client", flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.Quiet = true rf.Client = "me" return rf }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) f.EXPECT().Config().Return( &mocks.SimpleConfig{AllowNameForID: true}) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.EXPECT().GetClients(api.GetClientsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Client{ {ID: "c1", Name: "me"}, {ID: "c2", Name: "you"}, }, nil) c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: "w", Clients: []string{"c1"}, PaginationParam: api.AllPages(), }).Return([]dto.Project{ {ID: "p1", Name: "p1", ClientID: "c1", ClientName: "me"}, {ID: "p3", Name: "p3", ClientID: "c1", ClientName: "me"}, }, nil) c.EXPECT().LogRange(api.LogRangeParam{ Workspace: "w", UserID: "u", ProjectID: "p1", FirstDate: first, LastDate: last, PaginationParam: api.AllPages(), }).Return([]dto.TimeEntry{ {ID: "te-1", TimeInterval: dto.TimeInterval{ Start: first, }, }, {ID: "te-3", TimeInterval: dto.TimeInterval{ Start: first.Add(time.Duration(2)), }, }, }, nil) c.EXPECT().LogRange(api.LogRangeParam{ Workspace: "w", UserID: "u", ProjectID: "p3", FirstDate: first, LastDate: last, PaginationParam: api.AllPages(), }).Return([]dto.TimeEntry{ {ID: "te-2", TimeInterval: dto.TimeInterval{ Start: first.Add(time.Duration(1)), }, }, }, nil) return f }, expected: heredoc.Doc(` te-1 te-2 te-3 `), }, { name: "change timezone", factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) tz, _ := time.LoadLocation("America/Sao_Paulo") f.EXPECT().Config().Return(&mocks.SimpleConfig{ TimeZoneLoc: tz, AllowNameForID: false, }) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.On("LogRange", api.LogRangeParam{ Workspace: "w", UserID: "u", ProjectID: "p", Description: "desc", FirstDate: first, LastDate: last, PaginationParam: api.AllPages(), }).Return([]dto.TimeEntry{ {ID: "time-entry-1", TimeInterval: dto.TimeInterval{Start: last}}, {ID: "time-entry-2", TimeInterval: dto.TimeInterval{Start: first}}, }, nil) return f }, flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.Projects = []string{"p"} rf.Description = "desc" rf.Quiet = true rf.Format = `{{ .TimeInterval.Start.Format "` + timehlp.FullTimeFormat + `" }}` return rf }, expected: heredoc.Doc(` 2006-01-01 22:00:00 2006-01-04 22:00:00 `), }, { name: "limit number of time entries", flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.Limit = 2 rf.Quiet = true return rf }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) f.EXPECT().Config().Return( &mocks.SimpleConfig{AllowNameForID: true}) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.EXPECT().LogRange(api.LogRangeParam{ Workspace: "w", UserID: "u", FirstDate: first, LastDate: last, PaginationParam: api.PaginationParam{ Page: 1, PageSize: 2, }, }).Return([]dto.TimeEntry{ {ID: "te-1", TimeInterval: dto.TimeInterval{ Start: first, }, }, {ID: "te-3", TimeInterval: dto.TimeInterval{ Start: first.Add(time.Duration(2)), }, }, }, nil) return f }, expected: heredoc.Doc(` te-1 te-3 `), }, { name: "limit number of time entries with client filter", flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.Limit = 2 rf.Client = "me" rf.Quiet = true return rf }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) f.EXPECT().Config().Return( &mocks.SimpleConfig{AllowNameForID: true}) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.EXPECT().GetClients(api.GetClientsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Client{ {ID: "c1", Name: "me"}, {ID: "c2", Name: "you"}, }, nil) c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: "w", Clients: []string{"c1"}, PaginationParam: api.AllPages(), }).Return([]dto.Project{ {ID: "p1", Name: "p1", ClientID: "c1", ClientName: "me"}, {ID: "p3", Name: "p3", ClientID: "c1", ClientName: "me"}, }, nil) p := api.PaginationParam{Page: 1, PageSize: 2} c.EXPECT().LogRange(api.LogRangeParam{ Workspace: "w", UserID: "u", ProjectID: "p1", FirstDate: first, LastDate: last, PaginationParam: p, }).Return([]dto.TimeEntry{ {ID: "te-1", TimeInterval: dto.TimeInterval{ Start: first, }, }, {ID: "te-3", TimeInterval: dto.TimeInterval{ Start: first.Add(time.Duration(2)), }, }, }, nil) c.EXPECT().LogRange(api.LogRangeParam{ Workspace: "w", UserID: "u", ProjectID: "p3", FirstDate: first, LastDate: last, PaginationParam: p, }).Return([]dto.TimeEntry{ {ID: "te-2", TimeInterval: dto.TimeInterval{ Start: first.Add(time.Duration(1)), }, }, }, nil) return f }, expected: heredoc.Doc(` te-2 te-3 `), }, { name: "only a limited page", flags: func(t *testing.T) util.ReportFlags { rf := util.NewReportFlags() rf.Limit = 4 rf.Page = 12 rf.Client = "me" rf.Quiet = true return rf }, factory: func(t *testing.T) cmdutil.Factory { f := mocks.NewMockFactory(t) f.On("GetUserID").Return("u", nil) f.On("GetWorkspaceID").Return("w", nil) f.EXPECT().Config().Return( &mocks.SimpleConfig{AllowNameForID: true}) c := mocks.NewMockClient(t) f.On("Client").Return(c, nil) c.EXPECT().GetClients(api.GetClientsParam{ Workspace: "w", PaginationParam: api.AllPages(), }). Return([]dto.Client{ {ID: "c1", Name: "me"}, {ID: "c2", Name: "you"}, }, nil) c.EXPECT().GetProjects(api.GetProjectsParam{ Workspace: "w", Clients: []string{"c1"}, PaginationParam: api.AllPages(), }).Return([]dto.Project{ {ID: "p1", Name: "p1", ClientID: "c1", ClientName: "me"}, {ID: "p3", Name: "p3", ClientID: "c1", ClientName: "me"}, }, nil) p := api.PaginationParam{Page: 12, PageSize: 4} c.EXPECT().LogRange(api.LogRangeParam{ Workspace: "w", UserID: "u", ProjectID: "p1", FirstDate: first, LastDate: last, PaginationParam: p, }).Return([]dto.TimeEntry{ {ID: "te-1", TimeInterval: dto.TimeInterval{ Start: first, }, }, {ID: "te-3", TimeInterval: dto.TimeInterval{ Start: first.Add(time.Duration(2)), }, }, }, nil) c.EXPECT().LogRange(api.LogRangeParam{ Workspace: "w", UserID: "u", ProjectID: "p3", FirstDate: first, LastDate: last, PaginationParam: p, }).Return([]dto.TimeEntry{ {ID: "te-2", TimeInterval: dto.TimeInterval{ Start: first.Add(time.Duration(1)), }, }, {ID: "te-4", TimeInterval: dto.TimeInterval{ Start: first.Add(time.Duration(3)), }, }, {ID: "te-5", TimeInterval: dto.TimeInterval{ Start: first.Add(time.Duration(4)), }, }, }, nil) return f }, expected: heredoc.Doc(` te-2 te-3 te-4 te-5 `), }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { b := bytes.NewBufferString("") err := util.ReportWithRange( tt.factory(t), date, date.AddDate(0, 0, 2), b, tt.flags(t), ) if tt.err != "" { if assert.Error(t, err) { assert.Regexp(t, tt.err, err.Error()) } return } assert.NoError(t, err) assert.Equal(t, tt.expected, b.String()) }) } } ================================================ FILE: pkg/cmd/time-entry/report/yesterday/yesterday.go ================================================ package yesterday import ( "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/spf13/cobra" ) // NewCmdYesterday represents report today command func NewCmdYesterday(f cmdutil.Factory) *cobra.Command { of := util.NewReportFlags() cmd := &cobra.Command{ Use: "yesterday", Short: "List all time entries created yesterday", RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } day := timehlp.Today().Add(-1) return util.ReportWithRange(f, day, day, cmd.OutOrStdout(), of) }, } cmd.Long = cmd.Short + "\n\n" + util.HelpNamesForIds + "\n" + util.HelpMoreInfoAboutPrinting util.AddReportFlags(f, cmd, &of) return cmd } ================================================ FILE: pkg/cmd/time-entry/show/show.go ================================================ package show import ( "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/timeentryhlp" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/spf13/cobra" ) // NewCmdShow represents the show command func NewCmdShow(f cmdutil.Factory) *cobra.Command { of := util.OutputFlags{TimeFormat: timehlp.FullTimeFormat} va := cmdcompl.ValidArgsSlide{ timeentryhlp.AliasCurrent, timeentryhlp.AliasLast} cmd := &cobra.Command{ Use: "show [ | " + va.IntoUseOptions() + " | ^n ]", ValidArgs: va.IntoValidArgs(), Args: cobra.MaximumNArgs(1), Short: "Show information about one time entry.", Long: heredoc.Docf(` Show information about one time entry. If no time entry ID is informed it shows the running it exists. To show the last ended time entry you can use "%s" for it, for the one before that you can use "^2", for the previous "^3" and so on. %s `, timeentryhlp.AliasLast, util.HelpMoreInfoAboutPrinting, ), Example: heredoc.Docf(` # trying to show running time entry, when there is none $ %[1]s looking for running time entry: time entry was not found # show the last time entry (ended) $ %[1]s last -q 62af70d849445270d7c09fbd # show the time entry before the last one $ %[1]s ^2 -q 62af668b49445270d7c092e4 `, "clockify-cli show"), RunE: func(cmd *cobra.Command, args []string) error { if err := of.Check(); err != nil { return err } userID, err := f.GetUserID() if err != nil { return err } w, err := f.GetWorkspaceID() if err != nil { return err } id := timeentryhlp.AliasCurrent if len(args) > 0 { id = args[0] } c, err := f.Client() if err != nil { return err } tei, err := timeentryhlp.GetTimeEntry(c, w, userID, id) if err != nil { return err } return util.PrintTimeEntryImpl(tei, f, cmd.OutOrStdout(), of) }, } util.AddPrintTimeEntriesFlags(cmd, &of) _ = cmd.MarkFlagRequired("workspace") _ = cmd.MarkFlagRequired("user-id") return cmd } ================================================ FILE: pkg/cmd/time-entry/split/split.go ================================================ package split import ( "errors" "fmt" "io" "time" "github.com/MakeNowJust/heredoc" "github.com/lucassabreu/clockify-cli/api" "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util" "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" "github.com/lucassabreu/clockify-cli/pkg/cmdutil" "github.com/lucassabreu/clockify-cli/pkg/timeentryhlp" "github.com/lucassabreu/clockify-cli/pkg/timehlp" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" ) func NewCmdSplit( f cmdutil.Factory, report func([]dto.TimeEntry, io.Writer, util.OutputFlags) error, ) *cobra.Command { of := util.OutputFlags{TimeFormat: timehlp.OnlyTimeFormat} va := cmdcompl.ValidArgsSlide{ timeentryhlp.AliasCurrent, timeentryhlp.AliasLast, timeentryhlp.AliasLatest, } cmd := &cobra.Command{ Use: "split { | " + va.IntoUseOptions() + " | ^n } " + "