[
  {
    "path": ".deepsource.toml",
    "content": "version = 1\n\n[[analyzers]]\nname = \"go\"\nenabled = true\n\n  [analyzers.meta]\n  import_root = \"github.com/lucassabreu/clockify-cli\"\n  dependencies_vendored = false\n\n[[analyzers]]\nname = \"test-coverage\"\nenabled = true\n"
  },
  {
    "path": ".github/workflows/golangci-lint.yml",
    "content": "name: golangci-lint\non:\n  push:\n    tags:\n      - v*\n    branches:\n      - main\n  pull_request:\npermissions:\n  contents: read\n  # Optional: allow read access to pull request. Use with `only-new-issues` option.\n  # pull-requests: read\njobs:\n  golangci:\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-go@v6\n        with:\n          go-version: 1.24\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v9\n        with:\n          version: latest\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: goreleaser\n\non:\n  pull_request:\n  push:\n    tags:\n      - \"*\"\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - name: checkout\n        uses: actions/checkout@v6\n      - name: go-setup\n        uses: actions/setup-go@v6\n        with:\n          go-version: 1.24\n      - name: install snapcraft\n        uses: samuelmeuli/action-snapcraft@v3\n      - name: install nix\n        uses: cachix/install-nix-action@v31\n      - name: goreleaser-setup\n        uses: goreleaser/goreleaser-action@v6\n        with:\n          distribution: goreleaser\n          version: latest\n          install-only: true\n      - if: startsWith(github.ref, 'refs/tags/')\n        name: release a new version\n        run: |\n          make release \"tag=${GITHUB_REF#refs/tags/}\"\n        env:\n          GITHUB_TOKEN: ${{ secrets.GH_TOKEN_GORELEASER }}\n          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}\n      - if: startsWith(github.ref, 'refs/tags/') == false\n        name: test releasing a snapshot version\n        run: make release SNAPSHOT=1 tag=Unreleased\n      - if: startsWith(github.ref, 'refs/tags/')\n        name: trigger Netlify deploy with new release\n        run: |\n          curl -vs -X POST \"https://api.netlify.com/build_hooks/${NETLIFY_HOOK}\" \\\n            --data-urlencode \"trigger_title=triggered+by github actions (tag: ${GITHUB_REF#refs/tags/})\" \\\n            --data-urlencode \"trigger_branch=main\"\n        env:\n          NETLIFY_HOOK: ${{ secrets.NETLIFY_HOOK }}\n"
  },
  {
    "path": ".github/workflows/test-unit.yaml",
    "content": "name: Unit Tests\non:\n  pull_request:\n  push:\n    tags:\n      - v*\n    branches:\n      - main\njobs:\n  tests:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Check out code into the Go module directory\n        uses: actions/checkout@v6\n\n      - uses: actions/setup-go@v6\n        with:\n          go-version: 1.24\n\n      - name: Get dependencies\n        run: |\n          go mod download\n          go install gotest.tools/gotestsum@latest\n\n      - name: Generate coverage report\n        run: |\n          gotestsum --format dots -- \\\n            -coverprofile=coverage.txt \\\n            -covermode=atomic \\\n            ./...\n\n      - name: Upload coverage report\n        uses: codecov/codecov-action@v5\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          files: ./coverage.txt\n          flags: unittests\n\n      - name: Report test coverage to DeepSource\n        uses: deepsourcelabs/test-coverage-action@master\n        with:\n          key: go\n          coverage-file: ./coverage.txt\n          dsn: ${{ secrets.DEEPSOURCE_DSN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "build/\ndist/\nsnap.login\n/clockify-cli\nsite/content/commands/\nsite/public/\nsite/content/license\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"site/themes/hugo-theme-relearn\"]\n\tpath = site/themes/hugo-theme-relearn\n\turl = https://github.com/McShelby/hugo-theme-relearn.git\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "version: 2\n\nbuilds:\n  - env:\n      - CGO_ENABLED=0\n    goos:\n      - windows\n      - linux\n      - darwin\n    hooks:\n      pre:\n        - go mod download\n    main: ./cmd/clockify-cli\n\narchives:\n  - id: default\n    name_template: >-\n      {{- .ProjectName }}_\n      {{- title .Os }}_\n      {{- if eq .Arch \"amd64\" }}x86_64\n      {{- else if eq .Arch \"386\" }}i386\n      {{- else }}{{ .Arch }}{{ end }}\n      {{- if .Arm }}v{{ .Arm }}{{ end -}}\n    format_overrides:\n      - goos: windows\n        formats: [zip]\n    files:\n      - LICENSE\n\nchecksum:\n  name_template: \"checksums.txt\"\n\nsnapshot:\n  version_template: \"{{ .Tag }}-next\"\n\nchangelog:\n  sort: asc\n  filters:\n    exclude:\n      - \"^docs:\"\n      - \"^test:\"\n\nsnapcrafts:\n  - name: clockify-cli\n    summary: Helps to interact with Clockfy's API\n    description: Helps to interact with Clockfy's API\n\n    grade: stable\n    publish: true\n    confinement: strict\n\n    apps:\n      clockify-cli:\n        plugs: [\"network\"]\n\nhomebrew_casks:\n  - name: clockify-cli\n    repository:\n      owner: lucassabreu\n      name: homebrew-tap\n    homepage: https://github.com/lucassabreu/clockify-cli\n    description: Helps to interact with Clockfy's API\n\nnix:\n  - name: clockify-cli\n\n    goamd64: v1\n\n    # The project name and current git tag are used in the format string.\n    #\n    # Templates: allowed.\n    commit_msg_template: \"{{ .ProjectName }}: {{ .Tag }}\"\n\n    # Your app's homepage.\n    #\n    # Templates: allowed.\n    # Default: inferred from global metadata.\n    homepage: \"https://clockify-cli.netlify.app/\"\n\n    # Your app's description.\n    #\n    # Templates: allowed.\n    # Default: inferred from global metadata.\n    description: \"A simple cli to manage your time entries on Clockify from terminal\"\n\n    license: \"asl20\"\n\n    # Repository to push the generated files to.\n    repository:\n      # Repository owner.\n      #\n      # Templates: allowed.\n      owner: lucassabreu\n\n      # Repository name.\n      #\n      # Templates: allowed.\n      name: nur-packages\n\n      # Optionally a branch can be provided.\n      #\n      # Default: default repository branch.\n      # Templates: allowed.\n      branch: main\n"
  },
  {
    "path": ".mockery.yaml",
    "content": "dir: internal/mocks\ntemplate: testify\ntemplate-data:\n  unroll-variadic: true\npackages:\n  github.com/lucassabreu/clockify-cli/internal/mocks:\n    interfaces:\n      Client:\n        configs:\n          - filename: \"mock_Client.go\"\n      Config:\n        configs:\n          - filename: \"mock_Config.go\"\n      Factory:\n        configs:\n          - filename: \"mock_Factory.go\"\n"
  },
  {
    "path": ".nvimrc",
    "content": "set spell\nset spelllang=en\nset textwidth=79\nset colorcolumn=80\nlet g:goyo_width = 103\n\nautocmd FileType markdown setlocal ts=2 sts=2 sw=2 expandtab textwidth=99 colorcolumn=100\nautocmd FileType markdown setlocal nofoldenable\nautocmd BufRead,BufNewFile *.md setlocal spell wrap\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n## [v0.63.0] - 2026-03-26\n\n### Added\n\n- when not informed in the command new time entries will read the `billable` flag from the Task or Project set\n\n### Fixed\n\n- updated github workflows to use the newest versions of the actions\n- removed unused code\n- fixed assorted lint errors\n\n### Thanks\n\nThank you to [@reva](https://github.com/reva) for the improvements on\n[#292](https://github.com/lucassabreu/clockify-cli/pull/292).\n\n## [v0.62.0] - 2026-03-20\n\n### Added\n\n- prompt for API URL on `config init` command to allow configuring different Clockify datacenters\n- new script cmd/release/main.go to help with new releases\n\n### Fixed\n\n- changelog was not showing on the site\n\n## [v0.61.1] - 2026-02-21\n\n### Fixed\n\n- when initializing the config the folder might not exist yet\n\n## [v0.61.0] - 2026-02-21\n\n### Added\n\n- support to config file to be at `$HOME/.config` also, instead of just `$HOME`.\n\n### Changed\n\n- hugo theme to be compatible with newer versions\n\n## [v0.60.0] - 2026-02-11\n\n### Added\n\n- support to set which api to use with the client, this became necessary because of the EU datacenters\n\n### Thanks\n\nThank you to [@mbosc](https://github.com/mbosc) for the improvements on\n[#285](https://github.com/lucassabreu/clockify-cli/pull/285).\n\n## [v0.59.0] - 2026-01-20\n\n### Changed\n\n- change url for API token creation\n\n### Thanks\n\nThank you to [@davidsneighbour](https://github.com/davidsneighbour) for the implementing\nthe improvements on [#283](https://github.com/lucassabreu/clockify-cli/pull/283).\n\n## [v0.58.0] - 2025-11-24\n\n### Added\n\n- reporting custom fields for the time entries\n\n### Thanks\n\nThank you to [@calebtrepowski](https://github.com/calebtrepowski) for the implementing\nthe improvements on [#282](https://github.com/lucassabreu/clockify-cli/pull/282).\n\n## [v0.57.0] - 2025-10-14\n\n### Added\n\n- documentation about nix packages\n\n### Thanks\n\nThank you to [@Sekky61](https://github.com/Sekky61) for the information\non Issue [#280](https://github.com/lucassabreu/clockify-cli/pull/280).\n\n## [v0.56.2] - 2025-09-30\n\n### Fixed\n\n- NUR repository name was wrong\n\n## [v0.56.1] - 2025-09-30\n\n### Fixed\n\n- license is required\n\n## [v0.56.0] - 2025-09-30\n\n### Added\n\n- support for nix packages\n\n## [v0.55.2] - 2025-07-28\n\n### Fixed\n\n- update README section about installing using homebrew\n\n## [v0.55.1] - 2025-07-28\n\n### Fixed\n\n- migration from homebrew Formula to Casks needed fixing\n\n## [v0.55.0] - 2025-06-26\n\n### Added\n\n- support to limit how many time entries should be listed on the `report` commands, and choose which page to\n    show\n\n## [v0.54.2] - 2025-06-25\n\n### Fixed\n\n- deepsource suggestions\n- `last` alias on `show`, `clone`, `edit` and `edit-multiple` would select future time entries if they\n    existed, now only time entries started before now will be considered\n\n## [v0.54.1] - 2025-06-20\n\n### Fixed\n\n- when config \"show-client\" was on, printing time entries without projects were breaking the cli\n- goreleaser config deprecations\n- installing snapcraft from apt does not work anymore\n\n### Thanks\n\nThank you to [@melluh](https://github.com/melluh) for fixing the bug\non PR [#275](https://github.com/lucassabreu/clockify-cli/pull/275).\n\n## [v0.54.0] - 2024-06-15\n\n### Changed\n\n- markdown output now tries to resemble the time entry calendar dialog\n\n## [v0.53.1] - 2024-06-14\n\n### Fixed\n\n- was printing the language before the duration as float\n\n## [v0.53.0] - 2024-06-14\n\n### Added\n\n- new config `lang` to allow setting the number format to be used when printing\n- support to using client's name or id for autocompletion on bash\n- new config `timezone` to allow setting which timezone to use when reporting the time of a time entry\n\n## [v0.52.0] - 2024-06-02\n\n### Added\n\n- new command `split` to allow break a time entry into others with break points\n\n## [v0.51.1] - 2024-05-30\n\n### Fixed\n\n- when using `show-client` without `show-task` column headers became unaligned\n\n## [v0.51.0] - 2024-05-29\n\n### Added\n- new config `show-client` that sets the reports/output of time entries to show its client, if exists\n\n## [v0.50.1] - 2024-05-25\n\n### Fixed\n\n- snapcraft requires explicit confinement\n\n## [v0.50.0] - 2024-05-25\n\n### Added\n- more unit tests\n\n### Changed\n\n- using throttle/ticket providing system to limit requests per second to the clockify's api to prevent the\n    error message: `Too Many Requests (code: 429)`\n- upgrade go version to 1.19\n\n## [v0.49.0] - 2024-03-29\n\n### Added\n\n- report subcommands now allowing passing multiple projects to search/filter\n- report subcommands now will search all the time entries of a client with the flag `--client` without using\n  `--project`\n\n## [v0.48.2] - 2024-02-22\n\n### Fixed\n\n- using name for id options with `[` in the name makes the cli panic\n\n## [v0.48.1] - 2024-02-16\n\n### Fixed\n\n- match how strings are compared when using `allow-name-for-id` and filtering on interactive mode.\n\n## [v0.48.0] - 2024-02-16\n\n### Added\n\n- new config `search-project-with-client` to set whether or not the cli should lookup projects using the\n  client's name too\n\n## [v0.47.0] - 2024-02-09\n\n### Added\n\n- new flag `--client` to filter projects by client when managing time entries\n\n### Changed\n\n- `mockey` update and its configuration has changed\n- github actions steps updated to node20\n\n## [v0.46.0] - 2023-12-06\n\n### Added\n\n- support for the formats `HMM` and `HHMM` for time input\n\n### Fixed\n\n- update github actions workflows to use newer actions\n\n### Thanks\n\nThank you to [@aVolpe](https://github.com/aVolpe) for implementing new time formats as input\non PR [#251](https://github.com/lucassabreu/clockify-cli/pull/251).\n\n## [v0.45.0] - 2023-08-05\n\n### Added\n\n- new function `since` to be used on the time entry format to help working with time\n- new function `until` to be used on the time entry format to help working with time\n\n## [v0.44.2] - 2023-04-04\n\n### Fixed\n\n- when searching for task names with special characters (\"-\" for example), this was fixed for the filter\n\n## [v0.44.1] - 2023-03-06\n\n### Fixed\n\n- time entries were created as billable without user input.\n- bump golang.org/x/text from 0.3.7 to 0.3.8 ([#244](https://github.com/lucassabreu/clockify-cli/pull/244))\n- `go get` is not a supported option to install the cli\n  ([#245](https://github.com/lucassabreu/clockify-cli/pull/245))\n\n### Added\n\n- test coverage for interactive mode components and `in` command.\n\n### Thanks\n\nThank you to [@diegoquintanav](https://github.com/diegoquintanav) for reporting and fixing the documentation\non PR [#245](https://github.com/lucassabreu/clockify-cli/pull/245).\n\n## [v0.44.0] - 2022-12-18\n\n### Added\n\n- new flag and config `interactive-page-size` to set how many entries should be shown on select prompts.\n- new command `task quick-add` to easily create multiple tasks on a project.\n\n## [v0.43.0] - 2022-12-13\n\n### Added\n\n- support to `last` alias when deleting a time entry.\n\n### Thanks\n\nThank you to [@jjnilton](https://github.com/jjnilton) for fixing the issue\n[#229](https://github.com/lucassabreu/clockify-cli/issues/229) on PR\n[#238](https://github.com/lucassabreu/clockify-cli/pull/238).\n\n## [v0.42.2] - 2022-12-06\n\n### Fixed\n\n- `duration` and `estimate` of a task can be `null`, and when the `estimate` where null the cli was failing\n   with: \"duration null is invalid\"\n\n## [v0.42.1] - 2022-12-01\n\n### Fixed\n\n- when the `duration` of a project is `null`, a error \"duration null is invalid\" was blocking the use of the\n  cli\n\n## [v0.42.0] - 2022-11-09\n\n### Added\n\n- test help function `runClient` to mock calls to Clockfy's API\n- added the following methods on `api.Client` (with test coverage)\n  + UpdateProjectUserBillableRate\n  + UpdateProjectUserCostRate\n  + UpdateProjectEstimate\n  + UpdateProjectMemberships\n  + UpdateProjectTemplate\n  + DeleteProject\n- added flag `hydrated` at `project list` to get \"enriched\" projects with their custom fields, tasks and\n  memberships in one call, these can be accessed using the `format` or `json` output formats.\n- new command `project get` to show a project using its ID or name.\n\n### Fixed\n\n- when running `edit` for a time entry that had a task to change its project, the command was failing if no\n  task set, because it tried to find the task from the older project in the new one.\n\n## [v0.41.0] - 2022-08-31\n\n### Added\n\n- new parameter called `allow-archived-tags` to allow selection of archived tags.\n- new flag `tag` on `report` commands, this will filter the time entries with all the tags informed.\n\n## [v0.40.0] - 2022-08-09\n\n### Added\n\n- test coverage for `task` commands\n- new method `UpdateProject` on `api.Client` to update a project\n- new command `project edit` to allow batch editing multiple projects\n\n### Fixed\n\n- using `\\t` and `\\n` on format output will behaviour as expected\n\n## [v0.39.0] - 2022-07-31\n\n### Added\n\n- flags `--billable` and `--not-billable` to report commands to filter time entries that are\n  billable or not respectively\n\n## [v0.38.4] - 2022-07-27\n\n### Fixed\n\n- fixing pagination for time entry listing\n\n## [v0.38.3] - 2022-07-26\n\n### Fixed\n\n- `config init` tests were broken\n- `report today` was using local timezone, which created the wrong range of time for the api\n\n## [v0.38.2] - 2022-07-26\n\n### Added\n\n- tests for pkg/cmd/config\n- created a helper pkg for interactive console testing\n- tests for pkg/cmd/version\n- add codecov to pull requests\n\n### Changed\n\n- `api.Client` changed into a interface to easy testing\n- user reports now show the user timezone\n- flag `debug` dropped in favor of `log-level` to allow a finer control of the output for reporting\n  bugs.\n\n### Fixed\n\n- `Client.WorkspaceUsers` was not paginating over the results, this created bugs on `config init`\n  and `user list`\n- `make dist` was building all system to the same file\n\n### Thanks\n\nThank you to [@mhogerheijde](https://github.com/mhogerheijde) for fixing the issue\n[#204](https://github.com/lucassabreu/clockify-cli/issues/204).\n\n## [v0.38.1] - 2022-07-05\n\n### Added\n\n- link to LICENSE added to README.md\n- link to CHANGELOG added to README.md\n\n### Changed\n\n- function `bool2str` substituted with a map to appease deepsource.io\n- change task prompt to not list tasks that are inactive.\n\n### Fixed\n\n- `in` command on interactive mode was exiting when the user tried to start a timer on a project\n  were they don't have direct access to (only by their group). This is a bug on the API, but a\n  fix was done to not block the users.\n\n## [v0.38.0] - 2022-07-01\n\n### Added\n\n- badge with amount of downloads from github releases\n- most of the commands have better descriptions explaining flag usages and command examples.\n- document describing the [project layout][] and where to add or find files.\n- document with [how to contribute][contribute] to the project\n- site preview on branches going to `main`.\n- added `golang-lint` as a Github Action on every PR.\n- functions `json`, `yaml` and `pad` add to all golang template formatters.\n\n### Changed\n\n- site specific files moved from `docs/` to `site/` to free docs folder for actual documentation.\n- moved `cmd/*` and `internal/output/*` into the new locations as stated on [project layout][]\n- new `cmdutil.Factory` interface to work as a \"service locator\" so sharing some states, behaviours\n  and \"services\" can be easier.\n- project dependencies were updated.\n- memory and performance improvements\n- `config --init` changed to `config init` to better organized the commands logic.\n- site home page changed to better explain how to setup the project and to direct to new documents.\n\n### Fixed\n\n- `report` commands could fail to list time entries closer to midnight because of timezone\n    differences.\n\n## [v0.37.0] - 2022-05-17\n\n### Added\n\n- build windows binaries\n\n## [v0.36.2] - 2022-05-13\n\n### Fixed\n\n- `edit-multiple` was not updating time entries without interactive mode.\n\n## [v0.36.1] - 2022-05-10\n\n### Fixed\n\n- `clone` command was using the start time of the copied time entry to close the current one\n  instead of the start time of new one being created.\n\n## [v0.36.0] - 2022-05-09\n\n### Added\n\n- support for relative time for time parameters, can be +1:40, or +1h40m\n\n### Fixed\n\n- negative duration was printed broken, now it show as a valid negative duration\n\n### Changed\n\n- new error types from required fields and invalid entity ids.\n- all errors have a minimal context to help on support.\n- `manageEntry` refactored to not have as many control flags\n- reduce copy of objects on loops\n\n## [v0.35.1] - 2022-05-04\n\n### Fixed\n\n- fake \"not found\" errors will have more context for the message\n\n### Changed\n\n- all `api/client.go` will have stack traces.\n\n## [v0.35.0] - 2022-05-03\n\n### Added\n\n- `task edit` command allows changing a existing task and changing its status.\n- `task delete` command allows removing a existing task.\n- `task done` command is a helper for `task edit --done`.\n\n### Changed\n\n- `task add` command now accepts `assignees` and `estimate` for task creation\n- `end` argument of `report` command accepts the alias `yesterday` for previous date.\n\n## [v0.34.0] - 2022-04-27\n\n### Changed\n\n- `end` argument of `report` command accepts the aliases `now` and `today` for current date.\n\n## [v0.33.1] - 2022-04-25\n\n### Fixed\n\n- enabling `show-task` config were hiding the description column for table report format.\n\n## [v0.33.0] - 2022-04-21\n\n### Added\n\n- flag to filter projects on `report` command.\n\n## [v0.32.2] - 2022-02-25\n\n### Fixed\n\n- examples on `README` were out of date with current commands and outputs.\n- short description of `show` and `report` were too long.\n\n## [v0.32.1] - 2022-02-25\n\n### Removed\n\n- `log` subcommand removed (deprecated since [v0.28.0])\n- `log in-progress` subcommand removed (deprecated since [v0.29.0])\n\n### Changed\n\n- `report` subcommand allows calls using the alias `log`\n\n## [v0.32.0] - 2022-02-14\n\n### Added\n\n- new options `--random-color` when creating a project, to auto-generate a color for the project.\n\n### Thanks\n\nThank you to [@NoF0rte](https://github.com/NoF0rte) for these improvements to the CLI.\n\n## [v0.31.0] - 2022-02-08\n\n### Added\n\n- new commands `task add` and `task list` to manage tasks on projects\n- new commands `client add` and `client list` to manage clients\n- new command `project add` create projects\n\n### Changed\n\n- command `project list` has new parameter `clients` to filter only the projects related to the\n  clients informed.\n\n### Thanks\n\nThank you to [@NoF0rte](https://github.com/NoF0rte) for these improvements to the CLI.\n\n## [v0.30.1] - 2022-01-17\n\n### Fixed\n\n- `manual` subcommand was allowing creation of open time entries.\n\n## [v0.30.0] - 2022-01-15\n\n### Changed\n\n- if creation of incomplete time entries is not allowed, the commands will verify if the project is\n  active or not.\n- when closing a running time entry before creating a new one, the client will validate it before\n  asking information on interactive mode.\n\n### Fixed\n\n- archived projects were being shown as options to select in interactive mode, now only active are\n  shown.\n\n## [v0.29.0] - 2022-01-12\n\n### Changed\n\n- `show` subcommand has its parameter as optional, and shows current time entry by default when the\n  parameter is omitted.\n- `report` subcommand has its parameters as optional, and use `today` as value when none is set.\n- `log in-progress` subcommand is deprecated in favor of `show`/`show current`\n\n## [v0.28.0] - 2022-01-10\n\n### Changed\n\n- `log` subcommand deprecated in favor of `report`\n- default `report` now allows to set only one date of the range, in this situation it will treat\n  start and end date as being the same.\n\n### Added\n\n- new `report today` to show only the time entries from today, with `report` options\n- new `report yesterday` to show only the time entries from yesterday, with `report` options\n\n### Fixed\n\n- golang commands were wrong\n- there was a output on report subcommands breaking the format\n- changelog release links were wrong\n\n## [v0.27.1] - 2021-12-31\n\n### Fixed\n\n- `report last-month` was failing to create a valid range time because it was not truncated to 0\n    hours.\n\n## [v0.27.0] - 2021-12-31\n\n### Changed\n\n- `formatTimeEntry` renamed into `printTimeEntry`, and simplified to just call `printTimeEntries`\n  with a list containing the time entry informed.\n- go version on `go.mod` updated to 1.17\n\n### Added\n\n- all subcommands that can print more than one time entry will print the total duration for that\n  listing, this can be disabled with the `config` subcommand.\n- `report` subcommands now have a `description` flag to filter time entries that contains text on\n  its description.\n- all subcommands that output time entries now have two new formats: `duration-formatted` and\n  `duration-float`, that do sum all durations of the time entries and print only the sum, formatted\n  as time or as \"floaty-hour\", respectively\n\n### Fixed\n\n- `report` subcommands which required pagination on the requests to the api were not doing so, the\n  time entry list shown by this command was incomplete.\n\n## [v0.26.1] - 2021-12-07\n\n### Changed\n\n- hide \"interrupted\" error from the output\n\n### Fixed\n\n- removed `println` in the code breaking the component.\n- prevent error when no time has been chosen as output for `AskForDateTime`\n\n## [v0.26.0] - 2021-11-02\n\n### Added\n\n- add description suggestion using the recent time entries.\n\n### Changed\n\n- some code and style fixes detected by [deepsource](https://deepsource.io/)\n- refactored date-time flags into its own function.\n- refactored ask for date-time helper function to not have control flags.\n\n## [v0.25.0] - 2021-10-08\n\n### Fixed\n\n- `report` subcommands were showing only the time not the date when the time entry was created.\n- `--quiet` help was wrong, it said \"print as json\", but it prints only the id.\n\n### Added\n\n- project color is used to \"render\" project name on the terminal, if the output is being piped or\n  redirected then colors will be ignored to prevent problems and miss-interpretation of the output.\n- `show` subcommand prints details about time entries without having to list of the time entries of\n  a given date.\n- `edit`, `edit-multiple`, `show`, `clone` support \"^n\" expression to select a time entry to act\n  on, \"^0\" is the same as \"current\", \"^1\" is the same as \"last\", \"^2\" chooses the time entry before\n  the last one, etc.\n- new `md` (markdown) format to print time entries\n\n## [v0.24.1] - 2021-09-20\n\n### Fixed\n\n- `out` subcommand was not setting the user to look on ending the time entry.\n- listing subcommands didn't show \"hydrated\" information about time entries,\n  `GetUsersHydratedTimeEntries` was not telling the api to return hydrated data.\n\n### Added\n\n- all client method calls now validated for required fields, this makes easier to see bugs and\n  prevent errors to creping up into releases.\n\n## [v0.24.0] - 2021-09-18\n\n### Added\n\n- new commands `mask-invoiced` and `mark-not-invoiced` created to allow users to set this\n  information using the cli.\n\n### Changed\n\n- creation/update/out of time entries is made using the current api, instead of the old one\n- listing of workspaces and users is made using the current api, instead of the old one\n- all specific calls for the api for listing time entries were refactored to use a main function to\n  request then, the client methods still exist and maintain the same inputs/outputs, but are calling\n  the same function instead of reimplementing the call every time\n- getting of a project now uses the current api\n- debug messages of requests now show a \"name\" on it to help identify what where the intention of\n  the call\n\n### Removed\n\n- client method for recent time entries was not listed as a valid api, so its is now removed.\n\n## [v0.23.1] - 2021-09-17\n\n### Fixed\n\n- `last` and `current` aliases were failing to find and select the right time entry, it is a problem\n  with the old api for getting \"recent time entries\", fixed by [@zerodahero](https://github.com/zerodahero)\n\n## [v0.23.0] - 2021-09-16\n\n### Added\n\n- client uses current api to retrieve all tasks of a project\n- interactive mode support to select tasks\n- name or id support for tasks\n- terminal auto-complete support for `task` flag\n- new config `show-task` that sets the reports/output of time entries to show its task, if exists\n\n### Fixed\n\n- package `golang.org/x/crypto/ssh/terminal` was deprecated, substituted by `golang.org/x/term`\n\n### Removed\n\n- output formatters for `dto.TimeEntryImpl` were not being used.\n\n## [v0.22.0] - 2021-09-05\n\n### Changed\n\n- use new go version (1.17)\n- custom `changed` function is the same as using `Flags.Changed`, changed to use just the later\n- use `hydrated` parameter on \"get time entry\" endpoint instead of getting details individually\n- change in progress time entry using the current api\n- using \"Hydrated\" instead of \"Full\" to be consistent will the api\n\n### Fixed\n\n- remove default message for 404 errors from the api\n- `edit-multiple` without interactive mode were not working with the `allow-name-for-id` flag.\n\n## [v0.21.0] - 2021-08-16\n\n### Fixed\n\n- deploy to Netlify was not being triggered after release build, making the html documentation always wrong.\n- using terminal size of stdout file descriptor, this may fix problems on windows to print reports.\n- special characters will be ignored when looking for a project or tag with similar name.\n\n### Added\n\n- `--interactive` flag now describes how to disable it (suggestion from [#115](https://github.com/lucassabreu/clockify-cli/issues/115))\n- example to create a time entry using only flags no README.\n- keep the same options to print/output on all commands that show time entries.\n- support for names for id for tags\n\n### Changed\n\n- improved output examples to better resemble real output.\n- updated go dependencies\n- `reports` package renamed to `internal/output`, to prevent usage from other packages and solve ambiguity\n  with `report` command and `report api` (to come)\n- flag `allow-project-name` now will be called `allow-name-for-id` to account for other entities that would\n  benefit from using their names instead of their ids\n\n### Removed\n\n- features about integration with github:issues, azure dev and trello will not be implemented, at least not\n  in a foreseeable future.\n\n## [v0.20.0] - 2021-08-10\n\n### Changed\n\n- `manual` and `in` commands now support the use of `--project`, `--description`, `--when` and `--when-to-close`\n  flags besides existing positional arguments (now optional even without interactive mode).\n\n### Added\n- shorthand names for flags `when`, `when-to-close`, `description`, `project` and `tag`\n\n## [v0.19.5] - 2021-08-03\n\n### Fixed\n\n- select UI component can fail to return a valid option if the default value were not in the list, to prevent\n  that if the default value is empty or not in the list, no default value will be set.\n\n## [v0.19.4] - 2021-07-21\n\n### Fixed\n\n- `edit` command were resetting the start time to \"now\" if the user didn't set the `--when` flag.\n- `when` and `when-to-close` flags on `edit` help had the wrong description.\n\n## [v0.19.3] - 2021-07-20\n\n### Fixed\n\n- `clone` should create a open time entry by default.\n\n### Changed\n\n- `delete` command accepts multiple ids instead of just one.\n\n## [v0.19.2] - 2021-07-20\n\n### Fixed\n\n- `in` and `clone` commands were starting at 0001-01-01 because the default value of the flag was not being read.\n\n## [v0.19.1] - 2021-07-19\n\n### Fixed\n\n- `README` now contains updated help output.\n- `edit-multiple` help should be capitalized.\n\n## [v0.19.0] - 2021-07-19\n\n### Added\n\n- subcommand `edit-multiple` allows the user to edit all properties (except for the time interval) of multiple time entries\n  simultaneously. when not in interactive mode the user can choose exactly which properties to change and to keep.\n\n### Changed\n\n- flags used for creation and edition of time entries are now centralized into three functions `addFlagsForTimeEntryCreation`\n  to add flags used to create time entries, `addFlagsForTimeEntryEdit` for flags used on edition, and\n  `fillTimeEntryWithFlags` to replicated the flag values into the time entry.\n\n### Deprecated\n\n- flag `end-at` on edit subcommand will be removed in favor of `when-to-close` to be consistent with other subcommands.\n- flag `tags` on many subcommands will be removed in favor of `tag` to imply that its one by flag.\n\n## [v0.18.1] - 2021-07-12\n\n### Fixed\n\n- when the input for start time is cancelled (ctrl+c), clockify-cli was blocking the user by looping\n  on the field until a valid date-time string was used, or the process were killed.\n\n### Changed\n\n- library `github.com/AlecAivazis/survey` updated to the latest version.\n- `README` updated to show new configurations.\n\n## [v0.18.0] - 2021-07-08\n\n### Added\n\n- commands `in`, `clone` and `manual` will show a new \"None\" option on the projects list on the\n  interactive mode if the workspace allows time entries without projects.\n- config `allow-incomplete` allows the user to set if they want to create \"incomplete time entries\"\n  or to validated then before creation. Flag `--allow-incomplete` and environment variable\n  `CLOCKIFY_ALLOW_INCOMPLETE` can be used for the same purpose. by default time entries will be\n  validated.\n\n### Changed\n\n- commands `in` and `clone` when creating an \"open\" time entry will not validate if the workspace\n  requires a project or not, allowing the creation of open incomplete/invalid time entries, similar\n  to the browser application.\n- `newEntry` function changed to `manageEntry` and will allow a callback to deal with the filled and\n  validated time entry instead of always creating a new one, that way same code that were duplicated\n  between it and the `edit` command can be united.\n\n### Fixed\n\n- `no-closing` configuration was removed, because was not used anymore.\n\n## [v0.17.2] - 2021-06-17\n\n### Fixed\n\n- goreleaser needs a GitHub token with more permissions to create the homebrew Formulae.\n\n## [v0.17.1] - 2021-06-16\n\n### Changed\n\n- changing travis ci for gihub actions, seens easier to use and one less login to handle\n\n## [v0.17.0] - 2021-06-16\n\n### Added\n\n- command `report last-day`, this command will list time entries from the last day the user worked.\n- command `report last-week-day`, this command will look for the last day were the user should\n  have worked (based on the new config `workweek-days`) and list the time entries for that day.\n- config `workweek-days` for the user to set which days of the week they work. it can be set\n  interactively.\n\n## [v0.16.1] - 2021-06-16\n\n### Fixed\n\n- interactive selection of project would panic if the list were empty (filtering can empty the list)\n  and pressing enter. now will return as \"no project selected\".\n\n### Changed\n\n- `workspaces` command is now named `workspace`, `workspaces` still supported\n- `workspace` default print format now shows the workspace marked as \"default\"\n\n## [v0.16.0] - 2021-05-14\n\n### Added\n\n- `project list` can print the projects as JSON and CSV.\n- `project list` command default print format shows the client name and id\n\n## [v0.15.1] - 2020-09-30\n\n### Fixed\n\n- if the workspace has more the one page of projects, in interactive mode, only the first page was\n  being shown. now fixed to run over all pages to fill the list.\n\n### Added\n\n- \"Getting Started\" section on README.md to help new users to setup theirs environment.\n\n## [v0.15.0] - 2020-09-12\n\n### Added\n\n- support for command line completion on `fish`, `bash` and `zsh` for subcommands and flag name's\n- command line completion for arguments and flags for Tags, Projects, Workspaces and Users.\n- alias `remove` to command `delete`\n\n### Changed\n\n- using the API `v1` version to get tags available to a workspace.\n- `api.Client.Workspaces` renamed to `api.Client.GetWorkspaces` to follow pattern used on other\n  functions.\n- command `config`, `config set` and `config init` combined to be only one command `config`\n- improvements on help of many commands to show usable values.\n- `github.com/spf13/cobra` updated to latest possible current version to use completion improvements\n  not yet released\n- \"interactive mode\" functions moved to a separate package.\n\n## [v0.14.1] - 2020-09-09\n\n### Fixed\n\n- the project select on interactive mode was not respecting the \"default\" project when cloning\n  or informed through flags/parameters\n\n## [v0.14.0] - 2020-09-08\n\n### Changed\n\n- ask for \"interactive mode\" and \"auto-closing\" global configurations on `config init` command.\n\n## [v0.13.0] - 2020-09-08\n\n### Added\n\n- select and multi-select interactive now support \"glob like\" expressions to filter a option\n\n### Changed\n\n- client name of a project is shown on interactive mode to help identify the project.\n\n### Fixed\n\n- select and multi-select options now support \"non-english\" characters like \"á\" by converting then to a ASCII equivalent character.\n\n## [v0.12.2] - 2020-09-04\n\n### Fixed\n\n- flag `--token` help was not showing the right env var name.\n\n## [v0.12.1] - 2020-08-22\n\n### Added\n\n- \"How to install\" section on README to help new users to understand which options are available.\n\n### Fixed\n\n- improving the \"homebrew tap\" to allow installation using: `brew install lucassabreu/tap/clockify-cli`\n\n## [v0.12.0] - 2020-08-31\n\n### Added\n\n- support to homebrew for macOs users.\n\n## [v0.11.0] - 2020-08-22\n\n### Added\n\n- new `delete` command to remove a existing time entry from a workspace.\n- `edit` command support to interactive mode.\n\n### Fixed\n\n- when cloning a time entry, using interactive mode, the tags selected were not being respected.\n- `edit` command was removing all data from time-entry if the flag to fill the field was not being set.\n\n## [v0.10.1] - 2020-08-10\n\n### Fixed\n\n- `in` and `manual` command were showing a error \"Project '' informed was not found\", even\n  when no project id/name is informed, this is now fixed.\n\n## [v0.10.0] - 2020-08-07\n\n### Added\n\n- `clone` command now allow to change the project and description on the\n  time entry creation, interactive mode already had this possibility\n- new flag `archived` on `project list` to list archived projects\n- a new global config `allow-project-name` that, when enabled, allow the user to the project\n  name (or parts of it) to be used where the project id is asked.\n- common function to get all pages on a paginated request, to not reimplement it, and guarantee\n  all entities are being used/returned.\n\n### Fixed\n\n- `clone` sub-command was not asking to confirm the tags when the original time entry already\n  had some.\n- `clone` command now will respect flags `--tags` and `--when-to-close`.\n- \"billable\" attribute was not being cloned\n- keep the current CHANGELOG when extracting the last tag\n- some grammatic errors (\"applyied\" => applied)\n- remove mentions to GitHub or Trello token, until integration is implemented\n\n## [v0.9.0] - 2020-07-20\n\n### Added\n\n- new sub-command `version` to allow a quick way to know which version is installed\n- sub-command `report` now supports `this-week` and `last-week` as time range aliases\n  listing respectively all entries which start this week, and all entries that happened\n  on previous week.\n\n### Changed\n\n- all relevant errors now have stack trace with then, which will be printed when the\n  flag `--debug` is used.\n- error reporting now centralized, removing the need for a helper function in each\n  sub-command\n- `report`command default output (table) with show in which day the times entries were made.\n\n## [v0.8.1] - 2020-07-09\n\n### Fixed\n\n- `clone` sub-command was not working because the `no-closing` viper config was being\n  connected with a non-existing `--no-closing` flag in the `in` sub-command, that does\n  not exist anymore.\n\n## [v0.8.0] - 2020-07-08\n\n### Added\n\n- created a new sub-command `manual` that will allow to create \"completed\" time entries\n  in a more easy way.\n- created a new flag `--when-to-close` on `in` and `clone` to set close time for the\n  time entry being started (if wanted).\n\n### Changed\n\n- `clone` sub-command allows the flag `--no-closing` and will have the same flags as\n  `in` to set start and end time (if wanted)\n- `in` sub-command will always stops time entries that are open in the moment of the\n  sub-command call.\n- some helps and messages were improved to better describe what the command does\n\n### Removed\n\n- flags `--trello-token` and `--github-token` were removed because they are not\n  currently used and may give false impressions about the cli\n\n### Fixed\n\n- some code for the in and clone sub-commands were duplicated, now they are in `newEntry`\n  function that they both used.\n\n## [v0.7.2] - 2020-06-21\n\n### Fixed\n\n- using JSON to notify Netlify, to prevent \"malformed url errors\"\n\n## [v0.7.1] - 2020-06-21\n\n### Fixed\n\n- snapcraft build/release problems after Travis config update\n\n## [v0.7.0] - 2020-06-21\n\n### Added\n\n- build every pull request as a snapshot to check if it is not failing\n- command to auto-generated hugo formatted markdown files from the commands\n- implemented a site to better help people to understand what the CLI does,\n  without having to download it (live on: https://clockify-cli.netlify.app/)\n\n### Changed\n\n- improved headers on the CHANGELOG to better represent the hierarchies\n- moved `in clone` to be just `clone`\n\n### Fixed\n\n- missing release links for the title on the CHANGELOG\n- filling the brackets on the LICENSE file\n\n## [v0.6.1] - 2020-06-16\n\n### Added\n\n- `config` command can print the \"global\" parameters in `json` or `yaml`\n- `config` now accepts a argument, which is the name of the parameter,\n  when informed only this parameter will be printed\n\n## [v0.6.0] - 2020-06-16\n\n### Added\n\n- some badges, who does not like they?\n\n### Fixed\n\n- help was showing `CLOCKIFY_WROKSPACE` as env var for workspace, the right name is\n  `CLOCKIFY_WORKSPACE`\n- fixed some `golint` warnings\n\n### Changed\n\n- go mod dependencies updated\n- `snapcraft` package only requires network\n\n### Removed\n\n- Removed `GetCurrentUser` in favor of `GetMe` to be closer to the APIs format\n\n## [v0.5.0] - 2020-06-15\n\n### Changed\n\n- `in`, `log` and `report` now don't require you to inform a \"user-id\", if none is set,\n  than will get the user id from the token used to access the api\n\n### Added\n\n- `me` command returns information about the user who owns the token used to access\n  the clockify's api\n\n## [v0.4.0] - 2020-06-01\n\n### Added\n\n- table format will show time entry tags\n\n### Changed\n\n- when adding fake entries with `--fill-missing-dates`, will set end time as equal\n  to start time, so the duration will be 0 seconds\n\n## [v0.3.2] - 2020-05-22\n\n### Changed\n\n- printing duration as \"h:mm:ss\" instead of the Go's default format,\n  because is more user and sheet applications friendly.\n\n## [v0.3.1] - 2020-04-01\n\n### Fixed\n\n- fixed `--no-closing` being ignored\n- interactive flow of `clone` was keeping previous time interval\n\n## [v0.3.0] - 2020-04-01\n\n### Fixed\n\n- minor grammar bug fixes\n\n### Changed\n\n- improvements to the code moving interactive logic of the \"in\" command into `cmd/common.go`\n- \"in clone\" is now interactive and will ask the user to confirm the time entry data before\n  creating it.\n\n## [v0.2.2] - 2020-03-18\n\n### Fixed\n\n- the endpoint `workspaces/<workspace-id>/tags/<tag-id>` does not exist anymore, instead the\n  `api.Client` will get all tags of the workspace (`api.Client.GetTags`) and filter the response\n  to find the tag by its id.\n\n## [v0.2.1] - 2020-03-02\n\n### Fixed\n\n- `clockify-cli report` parameter `--fill-missing-dates`, was not working\n\n## [v0.2.0] - 2020-03-02\n\n### Added\n\n- `clockify-cli report --fill-missing-dates` when this parameters is set, if there\n  are dates from the range informed, will be created \"stub\" entries to better show\n  that are missing entries.\n\n## [v0.1.7] - 2020-02-03\n\n### Added\n\n- `api.Client` now supports getting one specific time entry from a workspace,\n  without the need to paginate through all time entries to find it (`GetTimeEntry`\n  function).\n\n### Fixed\n\n- `clockify-cli report` was not getting all pages from the period, implemented\n  support for pagination and to get \"all pages\" at once into `Client.Log` and\n  `Client.LogRange`\n\n### Changed\n\n- updated README, so it shows the `--help` output as it is now\n\n## [v0.1.6] - 2020-02-03\n\n### Fixed\n\n- fixed bug after Clockify's API changed, where `user` and `project` are not\n  automatically provided by the \"time-entries\" endpoint, unless sending\n  an extra parameter `hydrated=true`, and `user` is not provided anymore, so\n  now we find it using the user id from the function filter\n\n## [v0.1.5] - 2020-01-08\n\n### Fixed\n- fixed bug on the `log` commands, where the previous api url is not available\n  anymore, now using `v1/workspace/{workspace}/user/{user}/times-entries`\n- spelling of some words fixed and improving some aspects of the code\n\n### Changed\n- `go.mod` updated\n\n### Added\n- seamless support for query parameters using the interface `QueryAppender`\n- support for retrieving the current user of the token (`v1/user`) in the API client.\n- `.nvimrc` added to provide spell check\n\n## [v0.1.4] - 2019-08-05\n\n### Added\n- Permissions to `snap` installation, so configuration file can be used\n\n## [v0.1.3] - 2019-08-02\n\n### Changed\n- Set `publish` to `true` so it will be sent to `snapcraft`\n\n## [v0.1.2] - 2019-08-02\n\n### Added\n- Add release to snapcraft by the name `clockify-cli`\n- Add command `clockify-cli report` implemented to generate bigger exports. CSV, JSON,\n`gofmt` and table formats allowed in this command.\n\n## [v0.1.1] - 2019-06-10\n\n### Changed\n- The list returned by the `log` command will the sorted starting from the oldest\ntime entry.\n\n## [v0.1.0] - 2019-04-08\n\n### Added\n- Add `goreleaser` to manage binary and releases of the command\n- `clockify-cli in` asks user about new entry information when `interactive` is\n  enabled\n- Command `clockify-cli config init` allows to start a fresh setup, creating a\n  configuration file\n- Command `clockify-cli config set` updates/creates one configuration key into the\n  configuration file\n- `clockify-cli in` commands now allow more flexible time format inputs, can be:\n  hh:mm, hh:mm:ss, yyyy-mm-dd hh:mm or yyyy-mm-dd hh:mm:ss\n- Command `clockify-cli out` implemented, it will close any pending time entry,\n  and show the last entry info when closing it with success\n- Command `clockify-cli in clone` implemented, to allow creation of new time\n  entries based on existing ones, it also close pending ones, if any\n- Command `clockify-cli project list` was implemented, it allows to list the\n  projects of a workspace, format the return to table, json, and just id. Helps\n  with script automation\n- Using https://github.com/spf13/viper to link environment variables and configuration\n  files with the global flags. User can set variables `CLOCKIFY_TOKEN`,\n  `CLOCKIFY_WORKSPACE` and `CLOCKIFY_USER_ID` instead of using the command flags\n- Command `clockify-cli tags` created, to list workspace tags\n- Command `clockify-cli in` implemented, to allow creation of new time entries,\n  it also close pending ones, if any\n- Command `clockify-cli edit <id>` implemented, to allow updates on time entries,\n  including the in-progress one using the id: \"current\n- `--debug` option to allow better understanding of the requests\n- Command `clockify-cli log in-progress` implemented, with options to format the\n  output, and in the TimeEntry format, instead of TimeEntryImpl\n- Command `clockify-cli log` implemented, with options to format the output,\n  will require the user for now\n- Package `dto` created to hold all payload objects\n- Package `api.Client` to call Clockfy's API\n- Command `clockify-cli workspaces` created, with options to format the output\n- Command `clockify-cli workspaces users` created, with options to format the\n  output to allow retrieving the user's ID\n\n## [v0.0.1] - 2019-03-03\n### Added\n- This CHANGELOG file to hopefully serve as an evolving example of a\n  standardized open source project CHANGELOG.\n- README now show which features are expected, and that nothings is done yet\n- Golang CLI using [cobra](https://github.com/spf13/cobra)\n- Makefile to help setup actions\n\n[Unreleased]: https://github.com/lucassabreu/clockify-cli/compare/v0.63.0...HEAD\n[v0.63.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.63.0\n[v0.62.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.62.0\n[v0.61.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.61.1\n[v0.61.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.61.0\n[v0.60.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.60.0\n[v0.59.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.59.0\n[v0.58.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.58.0\n[v0.57.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.57.0\n[v0.56.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.56.2\n[v0.56.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.56.1\n[v0.56.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.56.0\n[v0.55.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.55.2\n[v0.55.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.55.1\n[v0.55.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.55.0\n[v0.54.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.54.2\n[v0.54.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.54.1\n[v0.54.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.54.0\n[v0.53.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.53.1\n[v0.53.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.53.0\n[v0.52.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.52.0\n[v0.51.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.51.1\n[v0.51.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.51.0\n[v0.50.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.50.1\n[v0.50.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.50.0\n[v0.49.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.49.0\n[v0.48.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.48.2\n[v0.48.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.48.1\n[v0.48.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.48.0\n[v0.47.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.47.0\n[v0.46.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.46.0\n[v0.45.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.45.0\n[v0.44.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.44.2\n[v0.44.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.44.1\n[v0.44.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.44.0\n[v0.43.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.43.0\n[v0.42.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.42.2\n[v0.42.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.42.1\n[v0.42.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.42.0\n[v0.41.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.41.0\n[v0.40.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.40.0\n[v0.39.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.39.0\n[v0.38.4]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.38.4\n[v0.38.3]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.38.3\n[v0.38.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.38.2\n[v0.38.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.38.1\n[v0.38.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.38.0\n[v0.37.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.37.0\n[v0.36.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.36.2\n[v0.36.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.36.1\n[v0.36.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.36.0\n[v0.35.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.35.1\n[v0.35.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.35.0\n[v0.34.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.34.0\n[v0.33.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.33.1\n[v0.33.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.33.0\n[v0.32.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.32.2\n[v0.32.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.32.1\n[v0.32.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.32.0\n[v0.31.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.31.0\n[v0.30.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.30.1\n[v0.30.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.30.0\n[v0.29.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.29.0\n[v0.28.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.28.0\n[v0.27.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.27.1\n[v0.27.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.27.0\n[v0.26.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.26.1\n[v0.26.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.26.0\n[v0.25.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.25.0\n[v0.24.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.24.1\n[v0.24.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.24.0\n[v0.23.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.23.1\n[v0.23.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.23.0\n[v0.22.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.22.0\n[v0.21.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.21.0\n[v0.20.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.20.0\n[v0.19.5]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.19.5\n[v0.19.4]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.19.4\n[v0.19.3]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.19.3\n[v0.19.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.19.2\n[v0.19.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.19.1\n[v0.19.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.19.0\n[v0.18.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.18.1\n[v0.18.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.18.0\n[v0.17.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.17.2\n[v0.17.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.17.1\n[v0.17.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.17.0\n[v0.16.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.16.1\n[v0.16.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.16.0\n[v0.15.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.15.1\n[v0.15.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.15.0\n[v0.14.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.14.1\n[v0.14.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.14.0\n[v0.13.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.13.0\n[v0.12.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.12.1\n[v0.12.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.12.0\n[v0.11.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.11.0\n[v0.10.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.10.1\n[v0.10.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.10.0\n[v0.9.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.9.0\n[v0.8.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.8.1\n[v0.8.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.8.0\n[v0.7.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.7.2\n[v0.7.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.7.1\n[v0.7.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.7.0\n[v0.6.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.6.1\n[v0.6.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.6.0\n[v0.5.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.5.0\n[v0.4.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.4.0\n[v0.3.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.3.2\n[v0.3.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.3.1\n[v0.3.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.3.0\n[v0.2.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.2.2\n[v0.2.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.2.1\n[v0.2.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.2.0\n[v0.1.7]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.7\n[v0.1.6]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.6\n[v0.1.5]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.5\n[v0.1.4]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.4\n[v0.1.3]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.3\n[v0.1.2]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.2\n[v0.1.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.1\n[v0.1.0]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.1.0\n[v0.0.1]: https://github.com/lucassabreu/clockify-cli/releases/tag/v0.0.1\n[project layout]: https://github.com/lucassabreu/clockify-cli/blob/main/docs/project-layout.md\n[contribute]: https://github.com/lucassabreu/clockify-cli/blob/feat/factory/CONTRIBUTING.md\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nThank you for the interest in Contributing to Clockify CLI.\n\nWe accept pull requests for bug fixes and features (preferably that were discussed on an issue\nbefore). Also opening issues with feature requests and reporting bugs are very important\ncontributions.\n\nPlease do:\n\n- Check in the [issues][issues] if the [bug][bugs] or [feature request][enhancement] has not been submitted.\n- Open an issue if things aren't working as expected.\n- Open an issue to propose new features or improvements on existing ones.\n- Open a pull request to fix a [bug][bugs].\n- Open a pull request for any open issue labelled [`type: enhancement`][enhancement].\n\nPlease avoid:\n\n- Opening pull requests for issues marked as `blocked`.\n\nAll enhancement and bug issues are marked with a `level` label, it may help you know the\nsize/complexity of it.\n\n## Building the project\n\nPrerequisites:\n- Go 1.19+\n\nRun `make deps-install` to install the packages used by the project.\n\nRun `make deps-upgrade` if you need to upgrade all of them, run `go help get` to see how to update\nindividual ones.\n\nTo build your changes into a binary run `make dist`, all three versions (Windows, Mac and Linux)\nwill be created under the `dist/` folder.\n\nYou can also just run `go run cmd/clockify-cli/main.go` to execute the source directly.\n\nSee the [project layout documentation][project layout] to know where to find and create specific\ncomponents.\n\n## Submitting a pull request\n\nContributions to this project are [released][legal] to the public under the\n[project's open source license][license]. By participating in this project you agree to abide by\nits terms.\n\nWe generate manual pages from source on every release. You do not need to submit pull requests for\ndocumentation specifically; manual pages for commands will automatically get updated after your\npull requests gets accepted.\n\n### With [`gh`][gh]\n\n1. Clone this repository\n2. Make and commit your changes.\n3. Submit a pull request: `gh pr create --web`\n4. In its body link which issue it is related, if there is one\n\n### Without `gh`\n\n1. [Fork the repository][fork]\n2. Make and commit your changes\n3. [Open a pull request][open-pr]\n4. In its body link which issue it is related, if there is one\n\n## Resources\n\n- [How to Contribute to Open Source][]\n- [Using Pull Requests][]\n- [GitHub Help][]\n\n## Credits\n\nThis document is based on the [CONTRIBUTING.md from github/cli/cli][credit].\n\n[fork]: https://github.com/lucassabreu/clockify-cli/fork\n[open-pr]: https://github.com/lucassabreu/clockify-cli/compare\n[credit]: https://github.com/cli/cli/blob/trunk/.github/CONTRIBUTING.md\n[issues]: https://github.com/lucassabreu/clockify-cli/issues\n[bugs]: https://github.com/lucassabreu/clockify-cli/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+bug%22\n[enhancement]: https://github.com/lucassabreu/clockify-cli/issues?q=is%3Aissue+is%3Aopen+label%3A%22type%3A+enhancement%22\n[project layout]: ./docs/project-layout.md\n[gh]: https://github.com/cli/cli\n[legal]: https://docs.github.com/en/free-pro-team@latest/github/site-policy/github-terms-of-service#6-contributions-under-repository-license\n[license]: ./LICENSE\n[How to Contribute to Open Source]: https://opensource.guide/how-to-contribute/\n[Using Pull Requests]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/about-pull-requests\n[GitHub Help]: https://docs.github.com/\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2020 Lucas dos Santos Abreu <lucas.s.abreu@gmail.com>\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Makefile",
    "content": "export GO111MODULE=on\nMAIN_PKG=./cmd/clockify-cli\n\n# Absolutely awesome: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html\nhelp: ## show this help\n\t@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = \":.*?## \"}; {printf \"\\033[36m%-15s\\033[0m %s\\n\", $$1, $$2}'\n\nclean: ## clean all buildable files\n\trm -rf dist\n\ndeps-install: ## install golang dependencies\n\tgo mod download\n\ndeps-upgrade: ## upgrade go dependencies\n\tgo get -u -v $(MAIN_PKG)\n\tgo mod tidy\n\nbuild: clean dist\n\ndist: deps-install dist/darwin dist/linux dist/windows ## build all cli versions (default)\n\ndist-internal:\n\tmkdir -p dist/$(goos)\n\tGOOS=$(goos) GOARCH=$(goarch) go build -o dist/$(goos)/clockify-cli $(MAIN_PKG)\n\ndist/darwin:\n\tmake dist-internal goos=darwin goarch=amd64\n\ndist/linux:\n\tmake dist-internal goos=linux goarch=amd64\n\ndist/windows:\n\tmake dist-internal goos=windows goarch=amd64\n\ngo-install: deps-install ## install dev version\n\tgo install $(MAIN_PKG)\n\ngo-generate: deps-install ## recreates generate files\n\tgo install github.com/vektra/mockery/v3@v3.4.0\n\tmockery\n\ntest-install: deps-install go-generate\n\tgo install gotest.tools/gotestsum@latest\n\ntest: test-install ## runs all tests\n\tgotestsum --format dots-v2\n\ntest_coverprofile=coverage.txt\ntest_covermode=atomic\ntest-coverage: test-install ## runs all tests and output coverage\n\tgotestsum --format dots-v2 -- \\\n\t\t-coverprofile=$(coverprofile) \\\n\t\t-covermode=$(covermode) \\\n\t\t./...\n\ntest-watch: test-install ## runs all tests and watch changes\n\tgotestsum --format testname --watch -- -failfast\n\ngoreleaser-test: tag=Unreleased\ngoreleaser-test: release\n\nifeq ($(tag),Unreleased)\nSNAPSHOT=1\nendif\ntag=\nrelease: ## releases a tagged version\n\tsed \"/^## \\[$(tag)/, /^## \\[/!d\" CHANGELOG.md | tail -n +2 | head -n -2 > /tmp/rn.md\n\tcurl -sL https://git.io/goreleaser | bash /dev/stdin --release-notes /tmp/rn.md \\\n\t\t--clean $(if $(SNAPSHOT),--snapshot --skip=publish,)\nifneq ($(SNAPSHOT),1)\n\tcurl -X POST -d '{\"trigger_branch\":\"$(tag)\",\"trigger_title\":\"Releasing $(tag)\"}' https://api.netlify.com/build_hooks/5eef4f99028bddbb4093e4c6 -v\nendif\n\nsite/themes/hugo-theme-relearn/.git:\n\tgit submodule update --init\n\nsite-build: site/themes/hugo-theme-relearn/.git ## generates command documents and builds the site\n\t./scripts/site-build\n\nsite-serve: site-build ## builds the site, and serves it locally\n\tcd site && hugo serve\n\ncreate-release-minor: ## create a new minor release\n\tgo run cmd/release/main.go minor\n\ncreate-release-patch: ## create a new patch release\n\tgo run cmd/release/main.go patch\n\n"
  },
  {
    "path": "README.md",
    "content": "![Clockify CLI](https://repository-images.githubusercontent.com/173476481/3445a278-9bb9-49e9-8c99-d10c76574489)\n============\n\nA simple cli to manage your time entries and projects on Clockify from terminal\n\n[![Release](https://img.shields.io/github/release/lucassabreu/clockify-cli.svg?classes=badges)](https://github.com/lucassabreu/clockify-cli/releases/latest)\n[![GitHub all releases](https://img.shields.io/github/downloads/lucassabreu/clockify-cli/total)](https://github.com/lucassabreu/clockify-cli/releases)\n[![clockify-cli](https://snapcraft.io//clockify-cli/badge.svg?classes=badges)](https://snapcraft.io/clockify-cli)\n[![Build Status](https://github.com/lucassabreu/clockify-cli/actions/workflows/release.yml/badge.svg?classes=badges)](.github/workflows/release.yml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/lucassabreu/clockify-cli?classes=badges)](https://goreportcard.com/report/github.com/lucassabreu/clockify-cli)\n[![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)\n[![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)\n\nDocumentation\n-------------\n\nSee the [project site](https://clockify-cli.netlify.app/) for the how to setup and use this CLI.\n\nSee more information about the sub-commands at: https://clockify-cli.netlify.app/en/commands/clockify-cli/\n\nContributing\n------------\n\nOn how to help improve the tool, suggest new features or report bugs, please take a look at the\n[CONTRIBUTING.md](CONTRIBUTING.md).\n\nFeatures\n--------\n\n* [x] List time entries from a day\n  * [x] List in progress entry\n* [x] Report time entries using a date range\n  * [x] Inform date range as parameters\n  * [x] \"auto filter\" for last month\n  * [x] \"auto filter\" for this month\n* [x] Start a new time entry\n  * [x] Cloning last time entry\n  * [x] Ask input interactively\n* [x] Stop the last entry\n* [x] List workspace projects\n* [x] List Clockify Workspaces\n* [x] List Clockify Workspaces Users\n* [x] List Clockify Tags\n* [x] Edit time entry\n* [x] Configuration management\n  * [x] Initialize configuration\n  * [x] Update individual configuration\n  * [x] Show current configuration\n\nHow to install [![Powered By: GoReleaser](https://img.shields.io/badge/powered%20by-goreleaser-green.svg?classes=badges)](https://github.com/goreleaser)\n--------------\n\n#### Using [`homebrew`](https://brew.sh/):\n\n```sh\nbrew install --cask lucassabreu/tap/clockify-cli\n```\n\n#### Using [`snapcraft`](https://snapcraft.io/clockify-cli)\n\n```sh\nsudo snap install clockify-cli\n```\n\n#### Using `go install`\n\n```sh\ngo install github.com/lucassabreu/clockify-cli/cmd/clockify-cli@latest\n```\n\nThe 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`).\n\n[go-envs]: https://pkg.go.dev/cmd/go#hdr-Environment_variables\n\n#### Using `nix`/[NUR](http://github.com/nix-community/NUR)\n\nAdd this input, overlay and package into your flake:\n\n```nix\n# ...\n\n  inputs = {\n    nur = {\n      url = \"github:nix-community/NUR\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n  };\n\n# ...\n\n  pkgs = import nixpkgs {\n    inherit system;\n    overlays = [ nur.overlays.default ];\n  };\n\n# ...\n\n  environment.systemPackages = with pkgs; [\n    nur.repos.lucassabreu.clockify-cli\n  ];\n```\n\n#### By Hand\n\nGo to the [releases page](https://github.com/lucassabreu/clockify-cli/releases) and download the pre-compiled\nbinary that fits your system.\n\nChangelog\n---------\n\n[Changelog](./CHANGELOG.md)\n\nLicense\n-------\n\n[Apache License](LICENSE)\n"
  },
  {
    "path": "api/client.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/pkg/errors\"\n)\n\n// Client will help to access Clockify API\ntype Client interface {\n\t// SetDebugLogger when set will output the responses of requests to the\n\t// logger\n\tSetDebugLogger(logger Logger) Client\n\t// SetInfoLogger when set will output which requests and params are used to\n\t// the logger\n\tSetInfoLogger(logger Logger) Client\n\n\tGetWorkspace(GetWorkspace) (dto.Workspace, error)\n\tGetWorkspaces(GetWorkspaces) ([]dto.Workspace, error)\n\n\tGetMe() (dto.User, error)\n\tGetUser(GetUser) (dto.User, error)\n\tWorkspaceUsers(WorkspaceUsersParam) ([]dto.User, error)\n\n\tAddClient(AddClientParam) (dto.Client, error)\n\tGetClients(GetClientsParam) ([]dto.Client, error)\n\n\t// GetProjects get all project of a workspace\n\tGetProjects(GetProjectsParam) ([]dto.Project, error)\n\t// GetProject get a single Project, if exists\n\tGetProject(GetProjectParam) (*dto.Project, error)\n\t// AddProject creates a new project\n\tAddProject(AddProjectParam) (dto.Project, error)\n\t// UpdateProject changes basic information about the project\n\tUpdateProject(UpdateProjectParam) (dto.Project, error)\n\t// UpdateProjectUserCostRate will update the hourly rate of a user on a\n\t// project\n\tUpdateProjectUserBillableRate(UpdateProjectUserRateParam) (\n\t\tdto.Project, error)\n\t// UpdateProjectUserCostRate will update the cost of a user on a project\n\tUpdateProjectUserCostRate(UpdateProjectUserRateParam) (\n\t\tdto.Project, error)\n\t// UpdateProjectEstimate change how the estime of a project is measured\n\tUpdateProjectEstimate(UpdateProjectEstimateParam) (dto.Project, error)\n\t// UpdateProjectMemberships changes who has access to add time entries to\n\t// the project\n\tUpdateProjectMemberships(UpdateProjectMembershipsParam) (dto.Project, error)\n\t// UpdateProjectTemplate changes if a project is a template or not\n\tUpdateProjectTemplate(UpdateProjectTemplateParam) (dto.Project, error)\n\t// DeleteProject removes a project forever\n\tDeleteProject(DeleteProjectParam) (dto.Project, error)\n\n\tAddTask(AddTaskParam) (dto.Task, error)\n\tDeleteTask(DeleteTaskParam) (dto.Task, error)\n\tGetTask(GetTaskParam) (dto.Task, error)\n\tGetTasks(GetTasksParam) ([]dto.Task, error)\n\tUpdateTask(UpdateTaskParam) (dto.Task, error)\n\n\tGetTag(GetTagParam) (*dto.Tag, error)\n\tGetTags(GetTagsParam) ([]dto.Tag, error)\n\n\tChangeInvoiced(ChangeInvoicedParam) error\n\tCreateTimeEntry(CreateTimeEntryParam) (dto.TimeEntryImpl, error)\n\tDeleteTimeEntry(DeleteTimeEntryParam) error\n\tGetHydratedTimeEntry(GetTimeEntryParam) (*dto.TimeEntry, error)\n\tGetHydratedTimeEntryInProgress(GetTimeEntryInProgressParam) (*dto.TimeEntry, error)\n\tGetTimeEntry(GetTimeEntryParam) (*dto.TimeEntryImpl, error)\n\tGetTimeEntryInProgress(GetTimeEntryInProgressParam) (*dto.TimeEntryImpl, error)\n\tGetUserTimeEntries(GetUserTimeEntriesParam) ([]dto.TimeEntryImpl, error)\n\tGetUsersHydratedTimeEntries(GetUserTimeEntriesParam) ([]dto.TimeEntry, error)\n\tLog(LogParam) ([]dto.TimeEntry, error)\n\tLogRange(LogRangeParam) ([]dto.TimeEntry, error)\n\tUpdateTimeEntry(UpdateTimeEntryParam) (dto.TimeEntryImpl, error)\n\tOut(OutParam) error\n}\n\ntype client struct {\n\tbaseURL *url.URL\n\thttp.Client\n\tdebugLogger    Logger\n\tinfoLogger     Logger\n\trequestTickets chan struct{}\n}\n\n// BASE_URL is the Clockify API base URL\nconst BASE_URL = \"https://api.clockify.me/api\"\n\n// REQUEST_RATE_LIMIT maximum number of requests per second\nconst REQUEST_RATE_LIMIT = 50\n\n// ErrorMissingAPIKey returned if X-Api-Key is missing\nvar ErrorMissingAPIKey = errors.New(\"api Key must be informed\")\n\n// ErrorMissingAPIURL returned if base url is missing\nvar ErrorMissingAPIURL = errors.New(\"api URL must be informed\")\n\nfunc NewClientFromUrlAndKey(\n\tapiKey,\n\turlString string,\n) (Client, error) {\n\tif apiKey == \"\" {\n\t\treturn nil, errors.WithStack(ErrorMissingAPIKey)\n\t}\n\n\tif urlString == \"\" {\n\t\treturn nil, errors.WithStack(ErrorMissingAPIURL)\n\t}\n\n\tu, err := url.Parse(urlString)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\n\treturn &client{\n\t\tbaseURL: u,\n\t\tClient: http.Client{\n\t\t\tTransport: transport{\n\t\t\t\tapiKey: apiKey,\n\t\t\t\tnext:   http.DefaultTransport,\n\t\t\t},\n\t\t},\n\t\trequestTickets: startRequestTick(REQUEST_RATE_LIMIT),\n\t}, nil\n}\n\n// NewClient create a new Client, based on: https://clockify.github.io/clockify_api_docs/\nfunc NewClient(apiKey string) (Client, error) {\n\treturn NewClientFromUrlAndKey(\n\t\tapiKey,\n\t\tBASE_URL,\n\t)\n}\n\nfunc startRequestTick(limit int) chan struct{} {\n\tch := make(chan struct{}, limit)\n\n\trunning := true\n\trelease := func() {\n\t\ti := len(ch)\n\t\tfor i < limit {\n\t\t\tif !running {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ti = i + 1\n\t\t\tch <- struct{}{}\n\t\t}\n\t}\n\n\tgo func() {\n\t\trelease()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\tgo release()\n\t\t\tcase <-context.Background().Done():\n\t\t\t\trunning = false\n\t\t\t\tdefer close(ch)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn ch\n}\n\n// GetWorkspaces will be used to filter the workspaces\ntype GetWorkspaces struct {\n\tName string\n}\n\n// Workspaces list all the user's workspaces\nfunc (c *client) GetWorkspaces(f GetWorkspaces) ([]dto.Workspace, error) {\n\tvar w []dto.Workspace\n\n\tr, err := c.NewRequest(\"GET\", \"v1/workspaces\", nil)\n\tif err != nil {\n\t\treturn w, err\n\t}\n\n\t_, err = c.Do(r, &w, \"GetWorkspaces\")\n\n\tif err != nil {\n\t\treturn w, errors.Wrap(err, \"get workspaces\")\n\t}\n\n\tif f.Name == \"\" {\n\t\treturn w, nil\n\t}\n\n\tvar ws []dto.Workspace\n\n\tn := strhlp.Normalize(strings.TrimSpace(f.Name))\n\tfor i := 0; i < len(w); i++ {\n\t\tif strings.Contains(strhlp.Normalize(w[i].Name), n) {\n\t\t\tws = append(ws, w[i])\n\t\t}\n\t}\n\n\treturn ws, nil\n}\n\ntype field string\n\nconst (\n\tworkspaceField      = field(\"workspace\")\n\tuserIDField         = field(\"user id\")\n\tuserOrGroupIDField  = field(\"user or group\")\n\tprojectField        = field(\"project id\")\n\ttimeEntryIDField    = field(\"time entry id\")\n\tnameField           = field(\"name\")\n\ttaskIDField         = field(\"task id\")\n\testimateMethodField = field(\"estimate method\")\n\testimateTypeField   = field(\"estimate type\")\n\tresetOptionField    = field(\"reset option\")\n)\n\n// RequiredFieldError indicates that a field should be filled, but was not\ntype RequiredFieldError struct {\n\tField string\n}\n\nfunc (e RequiredFieldError) Error() string {\n\treturn e.Field + \" is required\"\n}\n\nfunc required(values map[field]string) error {\n\tfor f := range values {\n\t\tif values[f] == \"\" {\n\t\t\treturn RequiredFieldError{Field: string(f)}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nvar regexId = regexp.MustCompile(\"^[a-fA-F0-9]{24}$\")\n\n// IsValidID checks if a string looks like a valid ID\nfunc IsValidID(id string) bool {\n\treturn regexId.MatchString(id)\n}\n\n// InvalidIDError indicates that a field should be a valid ID, but it is not\ntype InvalidIDError struct {\n\tField string\n\tID    string\n}\n\nfunc (e InvalidIDError) Error() string {\n\treturn e.Field + \" (\\\"\" + e.ID + \"\\\") is not valid ID\"\n}\n\nfunc checkIDs(ids map[field]string) error {\n\tfor field, id := range ids {\n\t\tif !IsValidID(id) {\n\t\t\treturn InvalidIDError{Field: string(field), ID: id}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc checkWorkspace(workspace string) error {\n\tids := map[field]string{workspaceField: workspace}\n\tif err := required(ids); err != nil {\n\t\treturn err\n\t}\n\n\treturn checkIDs(ids)\n}\n\nfunc wrapError(err *error, message string, args ...interface{}) {\n\tif err == nil {\n\t\treturn\n\t}\n\t*err = errors.Wrapf(*err, message, args...)\n}\n\ntype EntityNotFound struct {\n\tEntityName string\n\tID         string\n}\n\nfunc (e EntityNotFound) Error() string {\n\treturn e.EntityName + \" with id \" + e.ID + \" was not found\"\n}\n\nfunc (e EntityNotFound) Unwrap() error {\n\treturn dto.Error{Code: 404, Message: e.Error()}\n}\n\ntype GetWorkspace struct {\n\tID string\n}\n\nfunc (c *client) GetWorkspace(p GetWorkspace) (dto.Workspace, error) {\n\tvar err error\n\tdefer wrapError(&err, \"get workspace %s\", p.ID)\n\n\tif err = checkWorkspace(p.ID); err != nil {\n\t\treturn dto.Workspace{}, errors.WithStack(err)\n\t}\n\n\tws, err := c.GetWorkspaces(GetWorkspaces{})\n\tif err != nil {\n\t\treturn dto.Workspace{}, err\n\t}\n\n\tfor i := 0; i < len(ws); i++ {\n\t\tif ws[i].ID == p.ID {\n\t\t\treturn ws[i], nil\n\t\t}\n\t}\n\n\terr = EntityNotFound{\n\t\tEntityName: \"workspace\",\n\t\tID:         p.ID,\n\t}\n\n\treturn dto.Workspace{}, err\n}\n\n// WorkspaceUsersParam params to query workspace users\ntype WorkspaceUsersParam struct {\n\tWorkspace string\n\tEmail     string\n\n\tPaginationParam\n}\n\n// WorkspaceUsers all users in a Workspace\nfunc (c *client) WorkspaceUsers(p WorkspaceUsersParam) (users []dto.User, err error) {\n\tdefer wrapError(&err, \"get users\")\n\n\tif err := checkWorkspace(p.Workspace); err != nil {\n\t\treturn users, err\n\t}\n\n\tusers, err = paginate[dto.User](\n\t\tc,\n\t\t\"GET\",\n\t\tfmt.Sprintf(\"v1/workspaces/%s/users\", p.Workspace),\n\t\tp.PaginationParam,\n\t\tdto.WorkspaceUsersRequest{\n\t\t\tEmail: p.Email,\n\t\t},\n\t\t\"WorkspaceUsers\",\n\t)\n\n\treturn\n}\n\n// PaginationParam parameters about pagination\ntype PaginationParam struct {\n\tAllPages bool\n\tPage     int\n\tPageSize int\n}\n\n// AllPages sets the query to retrieve all pages\nfunc AllPages() PaginationParam {\n\treturn PaginationParam{AllPages: true}\n}\n\n// LogParam params to query entries\ntype LogParam struct {\n\tWorkspace string\n\tUserID    string\n\tDate      time.Time\n\tPaginationParam\n}\n\n// Log list time entries from a date\nfunc (c *client) Log(p LogParam) ([]dto.TimeEntry, error) {\n\tc.infof(\"Log - Date Param: %s\", p.Date)\n\n\td := p.Date.Round(time.Hour)\n\td = d.Add(time.Hour * time.Duration(d.Hour()) * -1)\n\n\treturn c.LogRange(LogRangeParam{\n\t\tWorkspace:       p.Workspace,\n\t\tUserID:          p.UserID,\n\t\tFirstDate:       d,\n\t\tLastDate:        d.Add(time.Hour * 24),\n\t\tPaginationParam: p.PaginationParam,\n\t})\n}\n\n// LogRangeParam params to query entries\ntype LogRangeParam struct {\n\tWorkspace   string\n\tUserID      string\n\tFirstDate   time.Time\n\tLastDate    time.Time\n\tDescription string\n\tProjectID   string\n\tTagIDs      []string\n\tPaginationParam\n}\n\n// LogRange list time entries by date range\nfunc (c *client) LogRange(p LogRangeParam) ([]dto.TimeEntry, error) {\n\tc.infof(\"LogRange - First Date Param: %s | Last Date Param: %s\", p.FirstDate, p.LastDate)\n\n\treturn c.GetUsersHydratedTimeEntries(GetUserTimeEntriesParam{\n\t\tWorkspace:       p.Workspace,\n\t\tUserID:          p.UserID,\n\t\tStart:           &p.FirstDate,\n\t\tEnd:             &p.LastDate,\n\t\tDescription:     p.Description,\n\t\tProjectID:       p.ProjectID,\n\t\tTagIDs:          p.TagIDs,\n\t\tPaginationParam: p.PaginationParam,\n\t})\n}\n\ntype GetUserTimeEntriesParam struct {\n\tWorkspace      string\n\tUserID         string\n\tOnlyInProgress *bool\n\tStart          *time.Time\n\tEnd            *time.Time\n\tDescription    string\n\tProjectID      string\n\tTagIDs         []string\n\n\tPaginationParam\n}\n\n// GetUserTimeEntries will list the time entries of a user on a workspace, can be paginated\nfunc (c *client) GetUserTimeEntries(p GetUserTimeEntriesParam) ([]dto.TimeEntryImpl, error) {\n\treturn getUserTimeEntriesImpl[dto.TimeEntryImpl](c, p, false)\n\n}\n\n// GetUsersHydratedTimeEntries will list hydrated time entries of a user on a workspace, can be paginated\nfunc (c *client) GetUsersHydratedTimeEntries(p GetUserTimeEntriesParam) ([]dto.TimeEntry, error) {\n\ttimeEntries, err := getUserTimeEntriesImpl[dto.TimeEntry](c, p, true)\n\n\tif err != nil {\n\t\treturn timeEntries, err\n\t}\n\n\tuser, err := c.GetUser(GetUser{p.Workspace, p.UserID})\n\tif err != nil {\n\t\treturn timeEntries, err\n\t}\n\n\tfor i := 0; i < len(timeEntries); i++ {\n\t\ttimeEntries[i].User = &user\n\t}\n\n\treturn timeEntries, err\n}\n\nfunc getUserTimeEntriesImpl[K dto.TimeEntry | dto.TimeEntryImpl](\n\tc *client,\n\tp GetUserTimeEntriesParam,\n\thydrated bool,\n) (tes []K, err error) {\n\tdefer wrapError(&err, \"get time entries from user \\\"%s\\\"\", p.UserID)\n\n\tids := map[field]string{\n\t\tworkspaceField: p.Workspace,\n\t\tuserIDField:    p.UserID,\n\t}\n\n\tif err := required(ids); err != nil {\n\t\treturn tes, err\n\t}\n\n\tif err := checkIDs(ids); err != nil {\n\t\treturn tes, err\n\t}\n\n\tinProgressFilter := \"nil\"\n\tif p.OnlyInProgress != nil {\n\t\tif *p.OnlyInProgress {\n\t\t\tinProgressFilter = \"true\"\n\t\t} else {\n\t\t\tinProgressFilter = \"false\"\n\t\t}\n\t}\n\n\tc.infof(\n\t\t\"GetUserTimeEntries - Workspace: %s | User: %s | In Progress: %s \"+\n\t\t\t\"| Description: %s | Project: %s\",\n\t\tp.Workspace,\n\t\tp.UserID,\n\t\tinProgressFilter,\n\t\tp.Description,\n\t\tp.ProjectID,\n\t)\n\n\tr := dto.UserTimeEntriesRequest{\n\t\tOnlyInProgress: p.OnlyInProgress,\n\t\tHydrated:       &hydrated,\n\t\tDescription:    p.Description,\n\t\tProject:        p.ProjectID,\n\t\tTagIDs:         p.TagIDs,\n\t}\n\n\tif p.Start != nil {\n\t\tr.Start = &dto.DateTime{Time: *p.Start}\n\t}\n\n\tif p.End != nil {\n\t\tr.End = &dto.DateTime{Time: *p.End}\n\t}\n\n\ttes, err = paginate[K](\n\t\tc,\n\t\t\"GET\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/user/%s/time-entries\",\n\t\t\tp.Workspace,\n\t\t\tp.UserID,\n\t\t),\n\t\tp.PaginationParam,\n\t\tr,\n\t\t\"GetUserTimeEntries\",\n\t)\n\n\treturn\n}\n\nfunc paginate[K any](\n\tc *client,\n\tmethod, uri string,\n\tp PaginationParam,\n\trequest dto.PaginatedRequest,\n\tname string,\n) ([]K, error) {\n\tpage := p.Page\n\tif p.AllPages {\n\t\tpage = 1\n\t}\n\n\tif p.PageSize == 0 {\n\t\tp.PageSize = 50\n\t}\n\n\tvar ls []K\n\tstop := false\n\tfor !stop {\n\t\tr, err := c.NewRequest(\n\t\t\tmethod,\n\t\t\turi,\n\t\t\trequest.WithPagination(page, p.PageSize),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn ls, err\n\t\t}\n\n\t\tvar response []K\n\t\t_, err = c.Do(r, &response, name)\n\t\tif err != nil {\n\t\t\treturn ls, err\n\t\t}\n\n\t\tcount := len(response)\n\t\tif count > 0 {\n\t\t\tls = append(ls, response...)\n\t\t}\n\n\t\tstop = count < p.PageSize || !p.AllPages\n\t\tpage++\n\t}\n\treturn ls, nil\n}\n\n// GetTimeEntryInProgressParam params to query entries\ntype GetTimeEntryInProgressParam struct {\n\tWorkspace string\n\tUserID    string\n}\n\n// GetTimeEntryInProgress show time entry in progress (if any)\nfunc (c *client) GetTimeEntryInProgress(p GetTimeEntryInProgressParam) (timeEntryImpl *dto.TimeEntryImpl, err error) {\n\tb := true\n\tts, err := c.GetUserTimeEntries(GetUserTimeEntriesParam{\n\t\tWorkspace:       p.Workspace,\n\t\tUserID:          p.UserID,\n\t\tOnlyInProgress:  &b,\n\t\tPaginationParam: PaginationParam{PageSize: 1},\n\t})\n\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif err == nil && len(ts) > 0 {\n\t\ttimeEntryImpl = &ts[0]\n\t}\n\treturn\n}\n\n// GetHydratedTimeEntryInProgress show hydrated time entry in progress (if any)\nfunc (c *client) GetHydratedTimeEntryInProgress(p GetTimeEntryInProgressParam) (timeEntry *dto.TimeEntry, err error) {\n\tb := true\n\tts, err := c.GetUsersHydratedTimeEntries(GetUserTimeEntriesParam{\n\t\tWorkspace:      p.Workspace,\n\t\tUserID:         p.UserID,\n\t\tOnlyInProgress: &b,\n\t})\n\tif err == nil && len(ts) > 0 {\n\t\ttimeEntry = &ts[0]\n\t}\n\treturn\n}\n\n// GetTimeEntryParam params to get a Time Entry\ntype GetTimeEntryParam struct {\n\tWorkspace              string\n\tTimeEntryID            string\n\tConsiderDurationFormat bool\n}\n\n// GetTimeEntry will retrieve a Time Entry using its Workspace and ID\nfunc (c *client) GetTimeEntry(p GetTimeEntryParam) (timeEntry *dto.TimeEntryImpl, err error) {\n\tdefer wrapError(&err, \"get time entry \\\"%s\\\"\", p.TimeEntryID)\n\n\tids := map[field]string{\n\t\tworkspaceField:   p.Workspace,\n\t\ttimeEntryIDField: p.TimeEntryID,\n\t}\n\n\tif err = required(ids); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = checkIDs(ids); err != nil {\n\t\treturn nil, err\n\t}\n\n\tr, err := c.NewRequest(\n\t\t\"GET\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/time-entries/%s\",\n\t\t\tp.Workspace,\n\t\t\tp.TimeEntryID,\n\t\t),\n\t\tdto.GetTimeEntryRequest{\n\t\t\tConsiderDurationFormat: &p.ConsiderDurationFormat,\n\t\t},\n\t)\n\n\tif err != nil {\n\t\treturn timeEntry, err\n\t}\n\n\t_, err = c.Do(r, &timeEntry, \"GetTimeEntry\")\n\treturn timeEntry, err\n}\n\nfunc (c *client) GetHydratedTimeEntry(p GetTimeEntryParam) (timeEntry *dto.TimeEntry, err error) {\n\tdefer wrapError(&err, \"get hydrated time entry \\\"%s\\\"\", p.TimeEntryID)\n\n\tids := map[field]string{\n\t\tworkspaceField:   p.Workspace,\n\t\ttimeEntryIDField: p.TimeEntryID,\n\t}\n\n\tif err = required(ids); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = checkIDs(ids); err != nil {\n\t\treturn nil, err\n\t}\n\n\tb := true\n\tr, err := c.NewRequest(\n\t\t\"GET\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/time-entries/%s\",\n\t\t\tp.Workspace,\n\t\t\tp.TimeEntryID,\n\t\t),\n\t\tdto.GetTimeEntryRequest{\n\t\t\tConsiderDurationFormat: &p.ConsiderDurationFormat,\n\t\t\tHydrated:               &b,\n\t\t},\n\t)\n\n\tif err != nil {\n\t\treturn timeEntry, err\n\t}\n\n\t_, err = c.Do(r, &timeEntry, \"GetHydratedTimeEntry\")\n\treturn timeEntry, err\n}\n\n// GetTagParam params to find a tag\ntype GetTagParam struct {\n\tWorkspace string\n\tTagID     string\n}\n\n// GetTag get a single tag, if it exists\nfunc (c *client) GetTag(p GetTagParam) (*dto.Tag, error) {\n\ttags, err := c.GetTags(GetTagsParam{\n\t\tWorkspace: p.Workspace,\n\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor i := 0; i < len(tags); i++ {\n\t\tif tags[i].ID == p.TagID {\n\t\t\treturn &tags[i], nil\n\t\t}\n\t}\n\n\treturn nil, errors.Errorf(\n\t\t\"tag %s not found on workspace %s\", p.TagID, p.Workspace)\n}\n\n// GetProjectParam params to get a Project\ntype GetProjectParam struct {\n\tWorkspace string\n\tProjectID string\n\tHydrate   bool\n}\n\n// GetProject get a single Project, if exists\nfunc (c *client) GetProject(p GetProjectParam) (pr *dto.Project, err error) {\n\tdefer wrapError(&err, \"get project \\\"%s\\\"\", p.ProjectID)\n\n\tids := map[field]string{\n\t\tworkspaceField: p.Workspace,\n\t\tprojectField:   p.ProjectID,\n\t}\n\n\tif err = required(ids); err != nil {\n\t\treturn pr, err\n\t}\n\n\tif err = checkIDs(ids); err != nil {\n\t\treturn pr, err\n\t}\n\n\tr, err := c.NewRequest(\n\t\t\"GET\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/projects/%s\",\n\t\t\tp.Workspace,\n\t\t\tp.ProjectID,\n\t\t),\n\t\tdto.GetProjectRequest{Hydrated: p.Hydrate},\n\t)\n\n\tif err != nil {\n\t\treturn pr, err\n\t}\n\n\t_, err = c.Do(r, &pr, \"GetProject\")\n\tif p.Hydrate && pr != nil {\n\t\tpr.Hydrated = true\n\t}\n\n\treturn pr, err\n}\n\n// GetUser params to get a user\ntype GetUser struct {\n\tWorkspace string\n\tUserID    string\n}\n\n// GetUser filters the wanted user from the workspace users\nfunc (c *client) GetUser(p GetUser) (dto.User, error) {\n\tvar err error\n\tdefer wrapError(&err, \"get user \\\"%s\\\"\", p.UserID)\n\n\tids := map[field]string{\n\t\tworkspaceField: p.Workspace,\n\t\tuserIDField:    p.UserID,\n\t}\n\n\tif err = required(ids); err != nil {\n\t\treturn dto.User{}, err\n\t}\n\n\tif err = checkIDs(ids); err != nil {\n\t\treturn dto.User{}, err\n\t}\n\n\tus, err := c.WorkspaceUsers(WorkspaceUsersParam{\n\t\tWorkspace:       p.Workspace,\n\t\tPaginationParam: AllPages(),\n\t})\n\tif err != nil {\n\t\treturn dto.User{}, errors.Wrapf(err, \"get user %s\", p.UserID)\n\t}\n\n\tfor i := 0; i < len(us); i++ {\n\t\tif us[i].ID == p.UserID {\n\t\t\treturn us[i], nil\n\t\t}\n\t}\n\n\terr = EntityNotFound{\n\t\tEntityName: \"user\",\n\t\tID:         p.UserID,\n\t}\n\treturn dto.User{}, err\n}\n\n// GetMe get details about the user who created the token\nfunc (c *client) GetMe() (dto.User, error) {\n\tr, err := c.NewRequest(\"GET\", \"v1/user\", nil)\n\n\tif err != nil {\n\t\treturn dto.User{}, err\n\t}\n\n\tvar user dto.User\n\t_, err = c.Do(r, &user, \"GetMe\")\n\treturn user, err\n}\n\n// GetTasksParam param to find tasks of a project\ntype GetTasksParam struct {\n\tWorkspace string\n\tProjectID string\n\tActive    bool\n\tName      string\n\n\tPaginationParam\n}\n\n// GetTasks get tasks of a project\nfunc (c *client) GetTasks(p GetTasksParam) (ps []dto.Task, err error) {\n\tdefer wrapError(&err, \"get tasks from project \\\"%s\\\"\", p.ProjectID)\n\n\tids := map[field]string{\n\t\tworkspaceField: p.Workspace,\n\t\tprojectField:   p.ProjectID,\n\t}\n\n\tif err = required(ids); err != nil {\n\t\treturn ps, err\n\t}\n\n\tif err = checkIDs(ids); err != nil {\n\t\treturn ps, err\n\t}\n\n\tps, err = paginate[dto.Task](\n\t\tc,\n\t\t\"GET\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/projects/%s/tasks\",\n\t\t\tp.Workspace,\n\t\t\tp.ProjectID,\n\t\t),\n\t\tp.PaginationParam,\n\t\tdto.GetTasksRequest{\n\t\t\tName:   p.Name,\n\t\t\tActive: p.Active,\n\t\t},\n\t\t\"GetTasks\",\n\t)\n\treturn ps, err\n}\n\n// GetTaskParam param to get a task on a project\ntype GetTaskParam struct {\n\tWorkspace string\n\tProjectID string\n\tTaskID    string\n}\n\n// GetTasks get tasks of a project\nfunc (c *client) GetTask(p GetTaskParam) (t dto.Task, err error) {\n\tdefer wrapError(&err, \"get task \\\"%s\\\"\", p.TaskID)\n\n\tids := map[field]string{\n\t\tworkspaceField: p.Workspace,\n\t\tprojectField:   p.ProjectID,\n\t\ttaskIDField:    p.TaskID,\n\t}\n\n\tif err = required(ids); err != nil {\n\t\treturn t, err\n\t}\n\n\tif err = checkIDs(ids); err != nil {\n\t\treturn t, err\n\t}\n\n\tr, err := c.NewRequest(\n\t\t\"GET\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/projects/%s/tasks/%s\",\n\t\t\tp.Workspace,\n\t\t\tp.ProjectID,\n\t\t\tp.TaskID,\n\t\t),\n\t\tnil,\n\t)\n\n\tif err != nil {\n\t\treturn t, err\n\t}\n\n\t_, err = c.Do(r, &t, \"GetTask\")\n\treturn t, err\n}\n\ntype TaskStatus string\n\nconst (\n\tTaskStatusDefault = \"\"\n\tTaskStatusDone    = \"DONE\"\n\tTaskStatusActive  = \"ACTIVE\"\n)\n\n// AddTaskParam param to add tasks to a project\ntype AddTaskParam struct {\n\tWorkspace   string\n\tProjectID   string\n\tName        string\n\tAssigneeIDs *[]string\n\tEstimate    *time.Duration\n\tStatus      TaskStatus\n\tBillable    *bool\n}\n\nfunc (c *client) AddTask(p AddTaskParam) (task dto.Task, err error) {\n\tdefer wrapError(&err, \"add task to project \\\"%s\\\"\", p.ProjectID)\n\n\tif err = required(map[field]string{\n\t\tnameField:      p.Name,\n\t\tworkspaceField: p.Workspace,\n\t\tprojectField:   p.ProjectID,\n\t}); err != nil {\n\t\treturn task, err\n\t}\n\n\tif err = checkIDs(map[field]string{\n\t\tworkspaceField: p.Workspace,\n\t\tprojectField:   p.ProjectID,\n\t}); err != nil {\n\t\treturn task, err\n\t}\n\n\tr := dto.AddTaskRequest{\n\t\tName:        p.Name,\n\t\tAssigneeIDs: p.AssigneeIDs,\n\t\tBillable:    p.Billable,\n\t}\n\n\tif p.Status != TaskStatus(\"\") {\n\t\ts := string(p.Status)\n\t\tr.Status = &s\n\t}\n\n\tif p.Estimate != nil {\n\t\te := dto.Duration{Duration: *p.Estimate}\n\t\tr.Estimate = &e\n\t}\n\n\treq, err := c.NewRequest(\n\t\t\"POST\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/projects/%s/tasks\",\n\t\t\tp.Workspace,\n\t\t\tp.ProjectID,\n\t\t),\n\t\tr,\n\t)\n\n\tif err != nil {\n\t\treturn task, err\n\t}\n\n\t_, err = c.Do(req, &task, \"AddTask\")\n\treturn task, err\n}\n\n// UpdateTaskParam param to update tasks to a project\ntype UpdateTaskParam struct {\n\tWorkspace   string\n\tProjectID   string\n\tTaskID      string\n\tName        string\n\tAssigneeIDs *[]string\n\tEstimate    *time.Duration\n\tStatus      TaskStatus\n\tBillable    *bool\n}\n\nfunc (c *client) UpdateTask(p UpdateTaskParam) (task dto.Task, err error) {\n\tdefer wrapError(&err, \"update task \\\"%s\\\"\", p.TaskID)\n\n\tif err = required(map[field]string{\n\t\tnameField:      p.Name,\n\t\ttaskIDField:    p.TaskID,\n\t\tworkspaceField: p.Workspace,\n\t\tprojectField:   p.ProjectID,\n\t}); err != nil {\n\t\treturn task, err\n\t}\n\n\tif err = checkIDs(map[field]string{\n\t\ttaskIDField:    p.TaskID,\n\t\tworkspaceField: p.Workspace,\n\t\tprojectField:   p.ProjectID,\n\t}); err != nil {\n\t\treturn task, err\n\t}\n\n\tr := dto.UpdateTaskRequest{\n\t\tName:        p.Name,\n\t\tAssigneeIDs: p.AssigneeIDs,\n\t\tBillable:    p.Billable,\n\t}\n\n\tif p.Status != TaskStatus(\"\") {\n\t\ts := string(p.Status)\n\t\tr.Status = &s\n\t}\n\n\tif p.Estimate != nil {\n\t\te := dto.Duration{Duration: *p.Estimate}\n\t\tr.Estimate = &e\n\t}\n\n\treq, err := c.NewRequest(\n\t\t\"PUT\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/projects/%s/tasks/%s\",\n\t\t\tp.Workspace,\n\t\t\tp.ProjectID,\n\t\t\tp.TaskID,\n\t\t),\n\t\tr,\n\t)\n\n\tif err != nil {\n\t\treturn task, err\n\t}\n\n\t_, err = c.Do(req, &task, \"UpdateTask\")\n\treturn task, err\n}\n\n// DeleteTaskParam param to update tasks to a project\ntype DeleteTaskParam struct {\n\tWorkspace string\n\tProjectID string\n\tTaskID    string\n}\n\nfunc (c *client) DeleteTask(p DeleteTaskParam) (task dto.Task, err error) {\n\tdefer wrapError(&err, \"delete task \\\"%s\\\"\", p.TaskID)\n\n\tids := map[field]string{\n\t\ttaskIDField:    p.TaskID,\n\t\tworkspaceField: p.Workspace,\n\t\tprojectField:   p.ProjectID,\n\t}\n\n\tif err = required(ids); err != nil {\n\t\treturn task, err\n\t}\n\n\tif err = checkIDs(ids); err != nil {\n\t\treturn task, err\n\t}\n\n\treq, err := c.NewRequest(\n\t\t\"DELETE\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/projects/%s/tasks/%s\",\n\t\t\tp.Workspace,\n\t\t\tp.ProjectID,\n\t\t\tp.TaskID,\n\t\t),\n\t\tnil,\n\t)\n\n\tif err != nil {\n\t\treturn task, err\n\t}\n\n\t_, err = c.Do(req, &task, \"DeleteTask\")\n\treturn task, err\n}\n\n// CreateTimeEntryParam params to create a new time entry\ntype CreateTimeEntryParam struct {\n\tWorkspace   string\n\tStart       time.Time\n\tEnd         *time.Time\n\tBillable    *bool\n\tDescription string\n\tProjectID   string\n\tTaskID      string\n\tTagIDs      []string\n}\n\n// CreateTimeEntry create a new time entry\nfunc (c *client) CreateTimeEntry(p CreateTimeEntryParam) (\n\tt dto.TimeEntryImpl, err error) {\n\tdefer wrapError(&err, \"create time entry\")\n\n\tif err = checkWorkspace(p.Workspace); err != nil {\n\t\treturn t, err\n\t}\n\n\tvar end *dto.DateTime\n\tif p.End != nil {\n\t\tend = &dto.DateTime{Time: *p.End}\n\t}\n\n\tr, err := c.NewRequest(\n\t\t\"POST\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/time-entries\",\n\t\t\tp.Workspace,\n\t\t),\n\t\tdto.CreateTimeEntryRequest{\n\t\t\tStart:       dto.DateTime{Time: p.Start},\n\t\t\tEnd:         end,\n\t\t\tBillable:    p.Billable,\n\t\t\tDescription: p.Description,\n\t\t\tProjectID:   p.ProjectID,\n\t\t\tTaskID:      p.TaskID,\n\t\t\tTagIDs:      p.TagIDs,\n\t\t},\n\t)\n\n\tif err != nil {\n\t\treturn t, err\n\t}\n\n\t_, err = c.Do(r, &t, \"CreateTimeEntry\")\n\treturn t, err\n}\n\n// GetTagsParam params to get all tags of a workspace\ntype GetTagsParam struct {\n\tWorkspace string\n\tName      string\n\tArchived  *bool\n\n\tPaginationParam\n}\n\n// GetTags get all tags of a workspace\nfunc (c *client) GetTags(p GetTagsParam) (ps []dto.Tag, err error) {\n\tdefer wrapError(&err, \"get tags\")\n\tif err = checkWorkspace(p.Workspace); err != nil {\n\t\treturn ps, err\n\t}\n\n\tps, err = paginate[dto.Tag](\n\t\tc,\n\t\t\"GET\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/tags\",\n\t\t\tp.Workspace,\n\t\t),\n\t\tp.PaginationParam,\n\t\tdto.GetTagsRequest{\n\t\t\tName:     p.Name,\n\t\t\tArchived: p.Archived,\n\t\t},\n\t\t\"GetTags\",\n\t)\n\treturn ps, err\n}\n\n// GetClientsParam params to get all clients of a workspace\ntype GetClientsParam struct {\n\tWorkspace string\n\tName      string\n\tArchived  *bool\n\n\tPaginationParam\n}\n\n// GetClients gets all clients of a workspace\nfunc (c *client) GetClients(p GetClientsParam) (\n\tclients []dto.Client, err error) {\n\tdefer wrapError(&err, \"get clients\")\n\n\tif err = checkWorkspace(p.Workspace); err != nil {\n\t\treturn clients, err\n\t}\n\n\tclients, err = paginate[dto.Client](\n\t\tc,\n\t\t\"GET\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/clients\",\n\t\t\tp.Workspace,\n\t\t),\n\t\tp.PaginationParam,\n\t\tdto.GetClientsRequest{\n\t\t\tName:     p.Name,\n\t\t\tArchived: p.Archived,\n\t\t},\n\n\t\t\"GetClients\",\n\t)\n\n\treturn\n}\n\ntype AddClientParam struct {\n\tWorkspace string\n\tName      string\n}\n\n// AddClient adds a new client to a workspace\nfunc (c *client) AddClient(p AddClientParam) (client dto.Client, err error) {\n\tdefer wrapError(&err, \"add client\")\n\n\tif err = required(map[field]string{\n\t\tnameField:      p.Name,\n\t\tworkspaceField: p.Workspace,\n\t}); err != nil {\n\t\treturn client, err\n\t}\n\n\tif err = checkIDs(map[field]string{\n\t\tworkspaceField: p.Workspace,\n\t}); err != nil {\n\t\treturn client, err\n\t}\n\n\treq, err := c.NewRequest(\n\t\t\"POST\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/clients\",\n\t\t\tp.Workspace,\n\t\t),\n\t\tdto.AddClientRequest{\n\t\t\tName: p.Name,\n\t\t},\n\t)\n\n\tif err != nil {\n\t\treturn client, err\n\t}\n\n\t_, err = c.Do(req, &client, \"AddClient\")\n\treturn client, err\n}\n\n// GetProjectsParam params to get all project of a workspace\ntype GetProjectsParam struct {\n\tWorkspace string\n\tName      string\n\tClients   []string\n\tArchived  *bool\n\tHydrate   bool\n\n\tPaginationParam\n}\n\n// GetProjects get all project of a workspace\nfunc (c *client) GetProjects(p GetProjectsParam) (ps []dto.Project, err error) {\n\tdefer wrapError(&err, \"get projects\")\n\n\tif err = checkWorkspace(p.Workspace); err != nil {\n\t\treturn ps, err\n\t}\n\n\tps, err = paginate[dto.Project](\n\t\tc,\n\t\t\"GET\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/projects\",\n\t\t\tp.Workspace,\n\t\t),\n\t\tp.PaginationParam,\n\t\tdto.GetProjectsRequest{\n\t\t\tName:     p.Name,\n\t\t\tArchived: p.Archived,\n\t\t\tClients:  p.Clients,\n\t\t\tHydrated: p.Hydrate,\n\t\t},\n\t\t\"GetProjects\",\n\t)\n\n\tif p.Hydrate {\n\t\tfor i := range ps {\n\t\t\tps[i].Hydrated = true\n\t\t}\n\t}\n\n\treturn ps, err\n}\n\ntype AddProjectParam struct {\n\tWorkspace string\n\tName      string\n\tClientId  string\n\tColor     string\n\tNote      string\n\tBillable  bool\n\tPublic    bool\n}\n\nfunc parseColor(c string) (string, error) {\n\tif !strings.HasPrefix(c, \"#\") {\n\t\tc = \"#\" + c\n\t}\n\n\tif len(c) != 4 && len(c) != 7 {\n\t\treturn c, errors.New(\"color must have 3 (#000) or 6 (#ffffff) numbers\")\n\t}\n\n\tif len(c) == 4 {\n\t\tc = string([]byte{'#', c[1], c[1], c[2], c[2], c[3], c[3]})\n\t}\n\n\tif _, err := hex.DecodeString(c[1:]); err != nil {\n\t\treturn c, errors.Wrap(err, \"color \\\"\"+c+\"\\\" is not a hex string\")\n\t}\n\n\treturn c, nil\n}\n\n// AddProject adds a new project to a workspace\nfunc (c *client) AddProject(p AddProjectParam) (\n\tproject dto.Project, err error) {\n\tdefer wrapError(&err, \"add project\")\n\n\tif err = required(map[field]string{\n\t\tnameField:      p.Name,\n\t\tworkspaceField: p.Workspace,\n\t}); err != nil {\n\t\treturn project, err\n\t}\n\n\tif err = checkIDs(map[field]string{\n\t\tworkspaceField: p.Workspace,\n\t}); err != nil {\n\t\treturn project, err\n\t}\n\n\tif p.Color != \"\" {\n\t\tp.Color, err = parseColor(p.Color)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\treq, err := c.NewRequest(\n\t\t\"POST\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/projects\",\n\t\t\tp.Workspace,\n\t\t),\n\t\tdto.AddProjectRequest{\n\t\t\tName:     p.Name,\n\t\t\tClientId: p.ClientId,\n\t\t\tIsPublic: p.Public,\n\t\t\tColor:    p.Color,\n\t\t\tNote:     p.Note,\n\t\t\tBillable: p.Billable,\n\t\t\tPublic:   p.Public,\n\t\t},\n\t)\n\n\tif err != nil {\n\t\treturn project, err\n\t}\n\n\t_, err = c.Do(req, &project, \"AddProject\")\n\treturn project, err\n}\n\n// UpdateProjectParam sets the properties to change on a project\n// Workspace and ID are required\ntype UpdateProjectParam struct {\n\tWorkspace string\n\tProjectID string\n\tName      string\n\tClientId  *string\n\tColor     string\n\tNote      *string\n\tBillable  *bool\n\tPublic    *bool\n\tArchived  *bool\n}\n\n// UpdateProject will change properties of a Project, leave the property as nil\n// or \"empty\" to not change it\nfunc (c *client) UpdateProject(p UpdateProjectParam) (\n\tproject dto.Project, err error) {\n\tdefer wrapError(&err, \"update project\")\n\n\tif err = required(map[field]string{\n\t\tprojectField:   p.ProjectID,\n\t\tworkspaceField: p.Workspace,\n\t}); err != nil {\n\t\treturn project, err\n\t}\n\n\tif err = checkIDs(map[field]string{\n\t\tprojectField:   p.ProjectID,\n\t\tworkspaceField: p.Workspace,\n\t}); err != nil {\n\t\treturn project, err\n\t}\n\n\tif p.Color != \"\" {\n\t\tp.Color, err = parseColor(p.Color)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\tvar name, color *string\n\tif p.Name != \"\" {\n\t\tname = &p.Name\n\t}\n\tif p.Color != \"\" {\n\t\tcolor = &p.Color\n\t}\n\n\treq, err := c.NewRequest(\n\t\t\"PUT\",\n\t\t\"v1/workspaces/\"+p.Workspace+\"/projects/\"+p.ProjectID,\n\t\tdto.UpdateProjectRequest{\n\t\t\tName:     name,\n\t\t\tClientId: p.ClientId,\n\t\t\tIsPublic: p.Public,\n\t\t\tColor:    color,\n\t\t\tNote:     p.Note,\n\t\t\tBillable: p.Billable,\n\t\t\tArchived: p.Archived,\n\t\t},\n\t)\n\n\tif err != nil {\n\t\treturn project, err\n\t}\n\n\t_, err = c.Do(req, &project, \"UpdateProject\")\n\treturn project, err\n}\n\n// UpdateMembership represents the membership of a User or User Group to a\n// project\ntype UpdateMembership struct {\n\tUserOrGroupID    string\n\tHourlyRateAmount int64\n}\n\n// UpdateProjectMembershipsParam will change which users and groups have\n// access to the project\ntype UpdateProjectMembershipsParam struct {\n\tWorkspace   string\n\tProjectID   string\n\tMemberships []UpdateMembership\n}\n\n// UpdateProjectMemberships changes who has access to add time entries to\n// the project\nfunc (c *client) UpdateProjectMemberships(p UpdateProjectMembershipsParam) (\n\tpr dto.Project, err error) {\n\tdefer wrapError(&err, \"update project memberships\")\n\n\tif err = required(map[field]string{\n\t\tprojectField:   p.ProjectID,\n\t\tworkspaceField: p.Workspace,\n\t}); err != nil {\n\t\treturn\n\t}\n\n\tif err = checkIDs(map[field]string{\n\t\tprojectField:   p.ProjectID,\n\t\tworkspaceField: p.Workspace,\n\t}); err != nil {\n\t\treturn\n\t}\n\n\tmembers := make([]dto.UpdateProjectMembership, len(p.Memberships))\n\tfor i := range p.Memberships {\n\t\tid := map[field]string{\n\t\t\tuserOrGroupIDField: p.Memberships[i].UserOrGroupID}\n\t\tif err = required(id); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tif err = checkIDs(id); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tmembers[i].UserID = p.Memberships[i].UserOrGroupID\n\t\tmembers[i].HourlyRate.Amount = p.Memberships[i].HourlyRateAmount\n\t}\n\n\treq, err := c.NewRequest(\n\t\t\"PATCH\",\n\t\t\"v1/workspaces/\"+p.Workspace+\"/projects/\"+p.ProjectID+\"/memberships\",\n\t\tdto.UpdateProjectMembershipsRequest{\n\t\t\tMemberships: members,\n\t\t},\n\t)\n\n\tif err != nil {\n\t\treturn pr, err\n\t}\n\n\t_, err = c.Do(req, &pr, \"UpdateProjectMemberships\")\n\treturn pr, err\n}\n\n// UpdateProjectTemplateParam sets which project will be updated,and if it will\n// became a template or not\ntype UpdateProjectTemplateParam struct {\n\tWorkspace string\n\tProjectID string\n\tTemplate  bool\n}\n\n// UpdateProjectTemplate changes if a project is a template or not\nfunc (c *client) UpdateProjectTemplate(p UpdateProjectTemplateParam) (\n\tpr dto.Project, err error) {\n\tdefer wrapError(&err, \"update project template\")\n\n\tif err = required(map[field]string{\n\t\tprojectField:   p.ProjectID,\n\t\tworkspaceField: p.Workspace,\n\t}); err != nil {\n\t\treturn\n\t}\n\n\tif err = checkIDs(map[field]string{\n\t\tprojectField:   p.ProjectID,\n\t\tworkspaceField: p.Workspace,\n\t}); err != nil {\n\t\treturn\n\t}\n\n\treq, err := c.NewRequest(\n\t\t\"PATCH\",\n\t\t\"v1/workspaces/\"+p.Workspace+\"/projects/\"+p.ProjectID+\"/template\",\n\t\tdto.UpdateProjectTemplateRequest{\n\t\t\tIsTemplate: p.Template,\n\t\t},\n\t)\n\n\tif err != nil {\n\t\treturn pr, err\n\t}\n\n\t_, err = c.Do(req, &pr, \"UpdateProjectTemplate\")\n\treturn pr, err\n}\n\n// UpdateProjectUserRateParam sets the parameters to update the billable/cost\n// rate, if Since is not nil, then all time entries after that time will be\n// updated to new rate\ntype UpdateProjectUserRateParam struct {\n\tWorkspace string\n\tProjectID string\n\tUserID    string\n\tAmount    uint\n\tSince     *time.Time\n}\n\nfunc (c *client) UpdateProjectUserBillableRate(\n\tp UpdateProjectUserRateParam) (project dto.Project, err error) {\n\tdefer wrapError(&err, \"update project user billable rate\")\n\n\tif err = required(map[field]string{\n\t\tprojectField:   p.ProjectID,\n\t\tworkspaceField: p.Workspace,\n\t\tuserIDField:    p.UserID,\n\t}); err != nil {\n\t\treturn\n\t}\n\n\tif err = checkIDs(map[field]string{\n\t\tprojectField:   p.ProjectID,\n\t\tworkspaceField: p.Workspace,\n\t\tuserIDField:    p.UserID,\n\t}); err != nil {\n\t\treturn\n\t}\n\n\tvar since *dto.DateTime\n\tif p.Since != nil {\n\t\tsince = &dto.DateTime{Time: *p.Since}\n\t}\n\n\treq, err := c.NewRequest(\n\t\t\"PUT\",\n\t\t\"v1/workspaces/\"+p.Workspace+\"/projects/\"+p.ProjectID+\n\t\t\t\"/users/\"+p.UserID+\"/hourly-rate\",\n\t\tdto.UpdateProjectUserRateRequest{\n\t\t\tAmount: p.Amount,\n\t\t\tSince:  since,\n\t\t},\n\t)\n\n\tif err != nil {\n\t\treturn project, err\n\t}\n\n\t_, err = c.Do(req, &project, \"UpdateProjectUserBillableRate\")\n\treturn project, err\n}\n\nfunc (c *client) UpdateProjectUserCostRate(\n\tp UpdateProjectUserRateParam) (project dto.Project, err error) {\n\tdefer wrapError(&err, \"update project user cost rate\")\n\n\tif err = required(map[field]string{\n\t\tprojectField:   p.ProjectID,\n\t\tworkspaceField: p.Workspace,\n\t\tuserIDField:    p.UserID,\n\t}); err != nil {\n\t\treturn\n\t}\n\n\tif err = checkIDs(map[field]string{\n\t\tprojectField:   p.ProjectID,\n\t\tworkspaceField: p.Workspace,\n\t\tuserIDField:    p.UserID,\n\t}); err != nil {\n\t\treturn\n\t}\n\n\tvar since *dto.DateTime\n\tif p.Since != nil {\n\t\tsince = &dto.DateTime{Time: *p.Since}\n\t}\n\n\treq, err := c.NewRequest(\n\t\t\"PUT\",\n\t\t\"v1/workspaces/\"+p.Workspace+\"/projects/\"+p.ProjectID+\n\t\t\t\"/users/\"+p.UserID+\"/cost-rate\",\n\t\tdto.UpdateProjectUserRateRequest{\n\t\t\tAmount: p.Amount,\n\t\t\tSince:  since,\n\t\t},\n\t)\n\n\tif err != nil {\n\t\treturn project, err\n\t}\n\n\t_, err = c.Do(req, &project, \"UpdateProjectUserCostRate\")\n\treturn project, err\n}\n\n// EstimateMethod are methods to estimate projects (none, budget and time)\ntype EstimateMethod string\n\nconst (\n\t// EstimateMethodNone dont estimate the project\n\tEstimateMethodNone = EstimateMethod(\"none\")\n\t// EstimateMethodTime estimate by time\n\tEstimateMethodTime = EstimateMethod(\"time\")\n\t// EstimateMethodBudget estimate by budget\n\tEstimateMethodBudget = EstimateMethod(\"budget\")\n)\n\n// EstimateType sets if the estimate is for the role project or per task\ntype EstimateType string\n\nconst (\n\tEstimateTypeProject = EstimateType(\"project\")\n\tEstimateTypeTask    = EstimateType(\"task\")\n)\n\nfunc (t EstimateType) toRequestType() *dto.EstimateType {\n\tswitch t {\n\tcase EstimateTypeTask:\n\t\tv := dto.EstimateTypeAuto\n\t\treturn &v\n\tcase EstimateTypeProject:\n\t\tv := dto.EstimateTypeManual\n\t\treturn &v\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// EstimateResetOption defines the period in which the estimates reset\ntype EstimateResetOption string\n\nconst (\n\tEstimateResetOptionDefault = EstimateType(\"\")\n\tEstimateResetOptionMonthly = EstimateResetOption(\"monthly\")\n)\n\nfunc (t EstimateResetOption) toRequestType() *dto.EstimateResetOption {\n\tswitch t {\n\tcase EstimateResetOptionMonthly:\n\t\tv := dto.EstimateResetOptionMonthly\n\t\treturn &v\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// UpdateProjectEstimateParam holds parameters to change project estimate\ntype UpdateProjectEstimateParam struct {\n\tWorkspace   string\n\tProjectID   string\n\tMethod      EstimateMethod\n\tType        EstimateType\n\tResetOption EstimateResetOption\n\tEstimate    int64\n}\n\n// UpdateProjectEstimate change how the estime of a project is measured\nfunc (c *client) UpdateProjectEstimate(p UpdateProjectEstimateParam) (\n\tr dto.Project, err error) {\n\tdefer wrapError(&err, \"update project estimate\")\n\n\tif err = required(map[field]string{\n\t\tprojectField:        p.ProjectID,\n\t\tworkspaceField:      p.Workspace,\n\t\testimateMethodField: string(p.Method),\n\t}); err != nil {\n\t\treturn\n\t}\n\n\tif err = checkIDs(map[field]string{\n\t\tprojectField:   p.ProjectID,\n\t\tworkspaceField: p.Workspace,\n\t}); err != nil {\n\t\treturn\n\t}\n\n\tif err = shouldBeOneOf(estimateMethodField, string(p.Method), []string{\n\t\tstring(EstimateMethodNone),\n\t\tstring(EstimateMethodTime),\n\t\tstring(EstimateMethodBudget),\n\t}); err != nil {\n\t\treturn\n\t}\n\n\tif p.Method != EstimateMethodNone {\n\t\tif err = shouldBeOneOf(estimateTypeField, string(p.Type), []string{\n\t\t\tstring(EstimateTypeProject),\n\t\t\tstring(EstimateTypeTask),\n\t\t}); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tif err = shouldBeOneOf(resetOptionField, string(p.ResetOption),\n\t\t\t[]string{\n\t\t\t\tstring(EstimateResetOptionDefault),\n\t\t\t\tstring(EstimateResetOptionMonthly),\n\t\t\t}); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tif p.Type != EstimateTypeProject {\n\t\t\tp.Estimate = 0\n\t\t} else if p.Estimate <= 0 {\n\t\t\terr = errors.New(\n\t\t\t\t\"estimate should be greater than zero for type project\")\n\t\t\treturn\n\t\t}\n\t}\n\n\tb := dto.UpdateProjectEstimateRequest{}\n\tif p.Method != EstimateMethodNone {\n\t\tbe := dto.BaseEstimateRequest{\n\t\t\tActive:       true,\n\t\t\tType:         p.Type.toRequestType(),\n\t\t\tResetOptions: p.ResetOption.toRequestType(),\n\t\t}\n\n\t\tswitch p.Method {\n\t\tcase EstimateMethodBudget:\n\t\t\tb.BudgetEstimate.BaseEstimateRequest = be\n\t\t\tif p.Estimate > 0 {\n\t\t\t\te := uint64(p.Estimate)\n\t\t\t\tb.BudgetEstimate.Estimate = &e\n\t\t\t}\n\t\tcase EstimateMethodTime:\n\t\t\tb.TimeEstimate.BaseEstimateRequest = be\n\t\t\tif p.Estimate > 0 {\n\t\t\t\tb.TimeEstimate.Estimate = &dto.Duration{\n\t\t\t\t\tDuration: time.Duration(p.Estimate)}\n\t\t\t}\n\t\t}\n\t}\n\n\treq, err := c.NewRequest(\n\t\t\"PATCH\",\n\t\t\"v1/workspaces/\"+p.Workspace+\"/projects/\"+p.ProjectID+\"/estimate\",\n\t\tb,\n\t)\n\n\tif err != nil {\n\t\treturn\n\t}\n\n\t_, err = c.Do(req, &r, \"UpdateProjectEstimate\")\n\n\treturn\n}\n\n// DeleteProjectParam identifies which project to delete\ntype DeleteProjectParam struct {\n\tWorkspace string\n\tProjectID string\n}\n\n// DeleteProject removes a project forever\nfunc (c *client) DeleteProject(p DeleteProjectParam) (\n\tpr dto.Project, err error) {\n\tdefer wrapError(&err, \"delete project\")\n\n\tids := map[field]string{\n\t\tworkspaceField: p.Workspace,\n\t\tprojectField:   p.ProjectID,\n\t}\n\n\tif err = required(ids); err != nil {\n\t\treturn pr, err\n\t}\n\n\tif err = checkIDs(ids); err != nil {\n\t\treturn pr, err\n\t}\n\n\tr, err := c.NewRequest(\n\t\t\"DELETE\",\n\t\t\"v1/workspaces/\"+p.Workspace+\"/projects/\"+p.ProjectID,\n\t\tnil,\n\t)\n\n\tif err != nil {\n\t\treturn pr, err\n\t}\n\n\t_, err = c.Do(r, &pr, \"DeleteProject\")\n\n\treturn pr, err\n}\n\n// InvalidOptionError indicates that the parameter has a limited set of valid\n// values, and the one used is not one of them (see Options for the valid ones)\ntype InvalidOptionError struct {\n\tField   string\n\tOptions []string\n}\n\nfunc (i *InvalidOptionError) Error() string {\n\treturn \"valid options for \" + i.Field + \" are \" + strhlp.ListForHumans(i.Options)\n}\n\nfunc shouldBeOneOf(f field, s string, o []string) error {\n\tif strhlp.InSlice(s, o) {\n\t\treturn nil\n\t}\n\n\treturn &InvalidOptionError{\n\t\tField:   string(f),\n\t\tOptions: o,\n\t}\n}\n\n// OutParam params to end the current time entry\ntype OutParam struct {\n\tWorkspace string\n\tUserID    string\n\tEnd       time.Time\n}\n\n// Out create a new time entry\nfunc (c *client) Out(p OutParam) (err error) {\n\tdefer wrapError(&err, \"end running time entry\")\n\n\tids := map[field]string{\n\t\tworkspaceField: p.Workspace,\n\t\tuserIDField:    p.UserID,\n\t}\n\n\tif err = required(ids); err != nil {\n\t\treturn err\n\t}\n\n\tif err = checkIDs(ids); err != nil {\n\t\treturn err\n\t}\n\n\tr, err := c.NewRequest(\n\t\t\"PATCH\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/user/%s/time-entries\",\n\t\t\tp.Workspace,\n\t\t\tp.UserID,\n\t\t),\n\t\tdto.OutTimeEntryRequest{\n\t\t\tEnd: dto.DateTime{Time: p.End},\n\t\t},\n\t)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = c.Do(r, nil, \"Out\")\n\treturn err\n}\n\n// UpdateTimeEntryParam params to update a new time entry\ntype UpdateTimeEntryParam struct {\n\tWorkspace   string\n\tTimeEntryID string\n\tStart       time.Time\n\tEnd         *time.Time\n\tBillable    bool\n\tDescription string\n\tProjectID   string\n\tTaskID      string\n\tTagIDs      []string\n}\n\n// UpdateTimeEntry update a time entry\nfunc (c *client) UpdateTimeEntry(p UpdateTimeEntryParam) (\n\tt dto.TimeEntryImpl, err error) {\n\tdefer wrapError(&err, \"update time entry \\\"%s\\\"\", p.TimeEntryID)\n\n\tids := map[field]string{\n\t\tworkspaceField:   p.Workspace,\n\t\ttimeEntryIDField: p.TimeEntryID,\n\t}\n\n\tif err = required(ids); err != nil {\n\t\treturn t, err\n\t}\n\n\tif err = checkIDs(ids); err != nil {\n\t\treturn t, err\n\t}\n\n\tvar end *dto.DateTime\n\tif p.End != nil {\n\t\tend = &dto.DateTime{Time: *p.End}\n\t}\n\n\tr, err := c.NewRequest(\n\t\t\"PUT\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/time-entries/%s\",\n\t\t\tp.Workspace,\n\t\t\tp.TimeEntryID,\n\t\t),\n\t\tdto.UpdateTimeEntryRequest{\n\t\t\tStart:       dto.DateTime{Time: p.Start},\n\t\t\tEnd:         end,\n\t\t\tBillable:    p.Billable,\n\t\t\tDescription: p.Description,\n\t\t\tProjectID:   p.ProjectID,\n\t\t\tTaskID:      p.TaskID,\n\t\t\tTagIDs:      p.TagIDs,\n\t\t},\n\t)\n\n\tif err != nil {\n\t\treturn t, err\n\t}\n\n\t_, err = c.Do(r, &t, \"UpdateTimeEntry\")\n\treturn t, err\n}\n\n// DeleteTimeEntryParam params to update a new time entry\ntype DeleteTimeEntryParam struct {\n\tWorkspace   string\n\tTimeEntryID string\n}\n\n// DeleteTimeEntry deletes a time entry\nfunc (c *client) DeleteTimeEntry(p DeleteTimeEntryParam) (err error) {\n\tdefer wrapError(&err, \"delete time entry \\\"%s\\\"\", p.TimeEntryID)\n\n\tids := map[field]string{\n\t\tworkspaceField:   p.Workspace,\n\t\ttimeEntryIDField: p.TimeEntryID,\n\t}\n\n\tif err = required(ids); err != nil {\n\t\treturn err\n\t}\n\n\tif err = checkIDs(ids); err != nil {\n\t\treturn err\n\t}\n\n\tr, err := c.NewRequest(\n\t\t\"DELETE\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/time-entries/%s\",\n\t\t\tp.Workspace,\n\t\t\tp.TimeEntryID,\n\t\t),\n\t\tnil,\n\t)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = c.Do(r, nil, \"DeleteTimeEntry\")\n\treturn err\n}\n\ntype ChangeInvoicedParam struct {\n\tWorkspace    string\n\tTimeEntryIDs []string\n\tInvoiced     bool\n}\n\n// ChangeInvoiced changes time entries to invoiced or not\nfunc (c *client) ChangeInvoiced(p ChangeInvoicedParam) error {\n\tr, err := c.NewRequest(\n\t\t\"PATCH\",\n\t\tfmt.Sprintf(\n\t\t\t\"v1/workspaces/%s/time-entries/invoiced\",\n\t\t\tp.Workspace,\n\t\t),\n\t\tdto.ChangeTimeEntriesInvoicedRequest{\n\t\t\tTimeEntryIDs: p.TimeEntryIDs,\n\t\t\tInvoiced:     p.Invoiced,\n\t\t},\n\t)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = c.Do(r, nil, \"ChangeInvoiced\")\n\treturn err\n}\n"
  },
  {
    "path": "api/client_test.go",
    "content": "package api_test\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar exampleID = \"62f2af744a912b05acc7c79e\"\n\ntype testCase interface {\n\tgetName() string\n\tgetParam() interface{}\n\tgetResult() interface{}\n\tgetErr() string\n\n\thasHttpCalls() bool\n\tgetHttpCallFor(uri string) httpCall\n\tgetPendingHttpCalls() []httpCall\n}\n\ntype httpCall interface {\n\tgetRequestMethod() string\n\tgetRequestUrl() string\n\tgetRequestBody() string\n\tgetResponseStatus() int\n\tgetResponseBody() string\n}\n\nfunc runClient(t *testing.T, tt testCase,\n\tfn func(api.Client, interface{}) (interface{}, error)) {\n\n\tt.Run(tt.getName(), func(t *testing.T) {\n\t\thttpCalled := false\n\t\tt.Cleanup(func() {\n\t\t\tif !tt.hasHttpCalls() {\n\t\t\t\tassert.False(t, httpCalled, \"should not call api\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.True(t, httpCalled, \"should call api\")\n\t\t})\n\t\ts := httptest.NewServer(\n\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\thttpCalled = true\n\t\t\t\tif !tt.hasHttpCalls() {\n\t\t\t\t\tt.Error(\"should not call api\")\n\t\t\t\t\tw.WriteHeader(500)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\thc := tt.getHttpCallFor(r.URL.String())\n\t\t\t\tif hc == nil {\n\t\t\t\t\tassert.FailNow(t, \"should not call api \"+r.URL.String())\n\t\t\t\t\tw.WriteHeader(500)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tassert.Equal(t, hc.getRequestUrl(), r.URL.String())\n\t\t\t\tassert.Equal(t,\n\t\t\t\t\thc.getRequestMethod(), strings.ToLower(r.Method))\n\n\t\t\t\tb, _ := io.ReadAll(r.Body)\n\t\t\t\tif hc.getRequestBody() != \"\" {\n\t\t\t\t\tvar eMap, aMap map[string]interface{}\n\t\t\t\t\tassert.NoError(t, json.Unmarshal(b, &aMap))\n\t\t\t\t\tassert.NoError(t,\n\t\t\t\t\t\tjson.Unmarshal([]byte(hc.getRequestBody()), &eMap))\n\n\t\t\t\t\tassert.Equal(t, eMap, aMap)\n\t\t\t\t} else {\n\t\t\t\t\tassert.Empty(t, string(b))\n\t\t\t\t}\n\n\t\t\t\tw.WriteHeader(hc.getResponseStatus())\n\t\t\t\trb := hc.getResponseBody()\n\t\t\t\tif rb == \"\" {\n\t\t\t\t\trb = \"{}\"\n\t\t\t\t}\n\t\t\t\t_, err := w.Write([]byte(rb))\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}))\n\t\tdefer s.Close()\n\n\t\tc, _ := api.NewClientFromUrlAndKey(\n\t\t\t\"a-key\",\n\t\t\ts.URL,\n\t\t)\n\n\t\tr, err := fn(c, tt.getParam())\n\t\tif tt.getErr() != \"\" {\n\t\t\tif !assert.Error(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Regexp(t, tt.getErr(), err.Error())\n\t\t\treturn\n\t\t}\n\n\t\tif !assert.NoError(t, err) || tt.getResult() == nil {\n\t\t\treturn\n\t\t}\n\t\tassert.Equal(t, tt.getResult(), r)\n\t})\n}\n\ntype simpleTestCase struct {\n\tname   string\n\tparam  interface{}\n\tresult interface{}\n\terr    string\n\n\trequestMethod string\n\trequestUrl    string\n\trequestBody   string\n\n\tresponseStatus int\n\tresponseBody   string\n\n\tonce bool\n}\n\nfunc (s *simpleTestCase) getRequestMethod() string {\n\treturn s.requestMethod\n}\n\nfunc (s *simpleTestCase) getRequestUrl() string {\n\treturn s.requestUrl\n}\n\nfunc (s *simpleTestCase) getRequestBody() string {\n\treturn s.requestBody\n}\n\nfunc (s *simpleTestCase) getResponseStatus() int {\n\treturn s.responseStatus\n}\n\nfunc (s *simpleTestCase) getResponseBody() string {\n\treturn s.responseBody\n}\n\nfunc (s *simpleTestCase) getName() string {\n\treturn s.name\n}\n\nfunc (s *simpleTestCase) getParam() interface{} {\n\treturn s.param\n}\n\nfunc (s *simpleTestCase) getResult() interface{} {\n\treturn s.result\n}\n\nfunc (s *simpleTestCase) getErr() string {\n\treturn s.err\n}\n\nfunc (s *simpleTestCase) getHttpCallFor(_ string) httpCall {\n\tif !s.once {\n\t\ts.once = true\n\t\treturn s\n\t}\n\treturn nil\n}\n\nfunc (s *simpleTestCase) getPendingHttpCalls() []httpCall {\n\tif s.once {\n\t\treturn []httpCall{}\n\t}\n\n\treturn []httpCall{s}\n}\n\nfunc (s *simpleTestCase) hasHttpCalls() bool {\n\treturn s.requestUrl != \"\"\n}\n\ntype multiRequestTestCase struct {\n\tname  string\n\tparam interface{}\n\n\terr    string\n\tresult interface{}\n\n\tcalls    map[string]httpCall\n\thasCalls bool\n}\n\nfunc (m *multiRequestTestCase) getName() string {\n\treturn m.name\n}\n\nfunc (m *multiRequestTestCase) getParam() interface{} {\n\treturn m.param\n}\n\nfunc (m *multiRequestTestCase) getResult() interface{} {\n\treturn m.result\n}\n\nfunc (m *multiRequestTestCase) getErr() string {\n\treturn m.err\n}\n\nfunc (m *multiRequestTestCase) hasHttpCalls() bool {\n\treturn m.hasCalls\n}\n\nfunc (m *multiRequestTestCase) getHttpCallFor(uri string) httpCall {\n\tif !m.hasCalls {\n\t\treturn nil\n\t}\n\tc := m.calls[uri]\n\tdelete(m.calls, uri)\n\treturn c\n}\n\nfunc (m *multiRequestTestCase) getPendingHttpCalls() []httpCall {\n\tif !m.hasCalls {\n\t\treturn []httpCall{}\n\t}\n\tl := make([]httpCall, len(m.calls))\n\tfor _, c := range m.calls {\n\t\tl = append(l, c)\n\t}\n\treturn l\n}\n\nfunc (m *multiRequestTestCase) addHttpCall(c httpCall) *multiRequestTestCase {\n\tif m.calls == nil {\n\t\tm.calls = make(map[string]httpCall)\n\t\tm.hasCalls = true\n\t}\n\n\tif _, ok := m.calls[c.getRequestUrl()]; ok {\n\t\tpanic(\"http call for \" + c.getRequestUrl() + \" already exists\")\n\t}\n\tm.calls[c.getRequestUrl()] = c\n\treturn m\n}\n\ntype httpRequest struct {\n\tmethod string\n\turl    string\n\tbody   string\n\n\tstatus   int\n\tresponse string\n}\n\nfunc (h *httpRequest) getRequestMethod() string {\n\treturn h.method\n}\n\nfunc (h *httpRequest) getRequestUrl() string {\n\treturn h.url\n}\n\nfunc (h *httpRequest) getRequestBody() string {\n\treturn h.body\n}\n\nfunc (h *httpRequest) getResponseStatus() int {\n\treturn h.status\n}\n\nfunc (h *httpRequest) getResponseBody() string {\n\treturn h.response\n}\n"
  },
  {
    "path": "api/dto/dto.go",
    "content": "package dto\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Error api errors\ntype Error struct {\n\tMessage string `json:\"message\"`\n\tCode    int    `json:\"code\"`\n}\n\nfunc (e Error) Error() string {\n\treturn fmt.Sprintf(\"%s (code: %d)\", e.Message, e.Code)\n}\n\n// Workspace DTO\ntype Workspace struct {\n\tID          string            `json:\"id\"`\n\tName        string            `json:\"name\"`\n\tImageURL    string            `json:\"imageUrl\"`\n\tSettings    WorkspaceSettings `json:\"workspaceSettings\"`\n\tHourlyRate  Rate              `json:\"hourlyRate\"`\n\tMemberships []Membership\n}\n\n// Membership DTO\ntype Membership struct {\n\tHourlyRate *Rate            `json:\"hourlyRate\"`\n\tCostRate   *Rate            `json:\"costRate\"`\n\tStatus     MembershipStatus `json:\"membershipStatus\"`\n\tType       string           `json:\"membershipType\"`\n\tTargetID   string           `json:\"targetId\"`\n\tUserID     string           `json:\"userId\"`\n}\n\n// MembershipStatus possible Membership Status\ntype MembershipStatus string\n\n// MembershipStatusPending membership is Pending\nconst MembershipStatusPending = MembershipStatus(\"PENDING\")\n\n// MembershipStatusActive membership is Active\nconst MembershipStatusActive = MembershipStatus(\"ACTIVE\")\n\n// MembershipStatusDeclined membership is Declined\nconst MembershipStatusDeclined = MembershipStatus(\"DECLINED\")\n\n// MembershipStatusInactive membership is Inactive\nconst MembershipStatusInactive = MembershipStatus(\"INACTIVE\")\n\n// WorkspaceSettings DTO\ntype WorkspaceSettings struct {\n\tAdminOnlyPages                     []string      `json:\"adminOnlyPages\"`\n\tAutomaticLock                      AutomaticLock `json:\"automaticLock\"`\n\tCanSeeTimeSheet                    bool          `json:\"canSeeTimeSheet\"`\n\tDefaultBillableProjects            bool          `json:\"defaultBillableProjects\"`\n\tForceDescription                   bool          `json:\"forceDescription\"`\n\tForceProjects                      bool          `json:\"forceProjects\"`\n\tForceTags                          bool          `json:\"forceTags\"`\n\tForceTasks                         bool          `json:\"forceTasks\"`\n\tLockTimeEntries                    time.Time     `json:\"lockTimeEntries\"`\n\tOnlyAdminsCreateProject            bool          `json:\"onlyAdminsCreateProject\"`\n\tOnlyAdminsCreateTag                bool          `json:\"onlyAdminsCreateTag\"`\n\tOnlyAdminsCreateTask               bool          `json:\"onlyAdminsCreateTask\"`\n\tOnlyAdminsSeeAllTimeEntries        bool          `json:\"onlyAdminsSeeAllTimeEntries\"`\n\tOnlyAdminsSeeBillableRates         bool          `json:\"onlyAdminsSeeBillableRates\"`\n\tOnlyAdminsSeeDashboard             bool          `json:\"onlyAdminsSeeDashboard\"`\n\tOnlyAdminsSeePublicProjectsEntries bool          `json:\"onlyAdminsSeePublicProjectsEntries\"`\n\tProjectFavorites                   bool          `json:\"projectFavorites\"`\n\tProjectGroupingLabel               string        `json:\"projectGroupingLabel\"`\n\tProjectPickerSpecialFilter         bool          `json:\"projectPickerSpecialFilter\"`\n\tRound                              Round         `json:\"round\"`\n\tTimeRoundingInReports              bool          `json:\"timeRoundingInReports\"`\n\tTrackTimeDownToSecond              bool          `json:\"trackTimeDownToSecond\"`\n\tIsProjectPublicByDefault           bool          `json:\"isProjectPublicByDefault\"`\n\tCanSeeTracker                      bool          `json:\"canSeeTracker\"`\n\tFeatureSubscriptionType            string        `json:\"featureSubscriptionType\"`\n}\n\n// AutomaticLock DTO\ntype AutomaticLock struct {\n\tChangeDay       string `json:\"changeDay\"`\n\tDayOfMonth      int    `json:\"dayOfMonth\"`\n\tFirstDay        string `json:\"firstDay\"`\n\tOlderThanPeriod string `json:\"olderThanPeriod\"`\n\tOlderThanValue  int    `json:\"olderThanValue\"`\n\tType            string `json:\"type\"`\n}\n\n// Round DTO\ntype Round struct {\n\tMinutes string `json:\"minutes\"`\n\tRound   string `json:\"round\"`\n}\n\n// Rate DTO\ntype Rate struct {\n\tAmount   int64  `json:\"amount\"`\n\tCurrency string `json:\"currency,omitempty\"`\n}\n\n// TimeEntry DTO\ntype TimeEntry struct {\n\tID            string        `json:\"id\"`\n\tBillable      bool          `json:\"billable\"`\n\tDescription   string        `json:\"description\"`\n\tHourlyRate    Rate          `json:\"hourlyRate\"`\n\tIsLocked      bool          `json:\"isLocked\"`\n\tProject       *Project      `json:\"project\"`\n\tCustomFields  []CustomField `json:\"customFieldValues\"`\n\tProjectID     string        `json:\"projectId\"`\n\tTags          []Tag         `json:\"tags\"`\n\tTask          *Task         `json:\"task\"`\n\tTimeInterval  TimeInterval  `json:\"timeInterval\"`\n\tTotalBillable int64         `json:\"totalBillable\"`\n\tUser          *User         `json:\"user\"`\n\tWorkspaceID   string        `json:\"workspaceId\"`\n}\n\n// NewTimeInterval will create a TimeInterval from start and end times\nfunc NewTimeInterval(start time.Time, end *time.Time) TimeInterval {\n\tstart = start.UTC()\n\tif end != nil {\n\t\t*end = end.UTC()\n\t}\n\n\tt := TimeInterval{\n\t\tStart: start.UTC(),\n\t\tEnd:   end,\n\t}\n\n\tif end == nil {\n\t\tt := time.Now().UTC()\n\t\tend = &t\n\t}\n\n\tt.Duration = Duration{end.Sub(t.Start)}.String()\n\n\treturn t\n}\n\n// TimeInterval DTO\ntype TimeInterval struct {\n\tDuration string     `json:\"duration\"`\n\tEnd      *time.Time `json:\"end\"`\n\tStart    time.Time  `json:\"start\"`\n}\n\n// Tag DTO\ntype Tag struct {\n\tID          string `json:\"id\"`\n\tName        string `json:\"name\"`\n\tWorkspaceID string `json:\"workspaceId\"`\n}\n\nfunc (e Tag) GetID() string   { return e.ID }\nfunc (e Tag) GetName() string { return e.Name }\nfunc (e Tag) String() string  { return e.Name + \" (\" + e.ID + \")\" }\n\n// TaskStatus task status\ntype TaskStatus string\n\n// TaskStatusActive task is Active\nconst TaskStatusActive = TaskStatus(\"ACTIVE\")\n\n// TaskStatusDone task is Done\nconst TaskStatusDone = TaskStatus(\"DONE\")\n\n// Task DTO\ntype Task struct {\n\tAssigneeIDs  []string   `json:\"assigneeIds\"`\n\tUserGroupIDs []string   `json:\"userGroupIds\"`\n\tEstimate     *Duration  `json:\"estimate\"`\n\tID           string     `json:\"id\"`\n\tName         string     `json:\"name\"`\n\tProjectID    string     `json:\"projectId\"`\n\tBillable     bool       `json:\"billable\"`\n\tHourlyRate   *Rate      `json:\"hourlyRate\"`\n\tCostRate     *Rate      `json:\"costRate\"`\n\tStatus       TaskStatus `json:\"status\"`\n\tDuration     *Duration  `json:\"duration\"`\n\tFavorite     bool       `json:\"favorite\"`\n}\n\nfunc (e Task) GetID() string   { return e.ID }\nfunc (e Task) GetName() string { return e.Name }\n\n// Client DTO\ntype Client struct {\n\tID          string `json:\"id\"`\n\tName        string `json:\"name\"`\n\tWorkspaceID string `json:\"workspaceId\"`\n\tArchived    bool   `json:\"archived\"`\n}\n\nfunc (e Client) GetID() string   { return e.ID }\nfunc (e Client) GetName() string { return e.Name }\n\n// CustomField DTO\ntype CustomField struct {\n\tCustomFieldID string      `json:\"customFieldId\"`\n\tTimeEntryId   string      `json:\"timeEntryId\"`\n\tName          string      `json:\"name\"`\n\tType          string      `json:\"type\"`\n\tValue         interface{} `json:\"value\"`\n}\n\n// ValueAsString converter for CustomFieldDTO\n/*\n   Custom field `Value` can be either a string or an array of strings.\n   This function is used to get the value always as string, using the `|` symbol\n   as separator between each individual string.\n*/\nfunc (cf CustomField) ValueAsString() string {\n\tswitch v := cf.Value.(type) {\n\tcase string:\n\t\treturn v\n\tcase []interface{}:\n\t\tparts := make([]string, len(v))\n\t\tfor i, item := range v {\n\t\t\tparts[i] = fmt.Sprint(item)\n\t\t}\n\t\treturn strings.Join(parts, \"|\")\n\tcase []string:\n\t\treturn strings.Join(v, \"|\")\n\tcase nil:\n\t\treturn \"\"\n\tdefault:\n\t\treturn fmt.Sprint(v)\n\t}\n}\n\n// Project DTO\ntype Project struct {\n\tWorkspaceID string `json:\"workspaceId\"`\n\n\tID    string `json:\"id\"`\n\tName  string `json:\"name\"`\n\tNote  string `json:\"note\"`\n\tColor string `json:\"color\"`\n\n\tClientID   string `json:\"clientId\"`\n\tClientName string `json:\"clientName\"`\n\n\tHourlyRate Rate  `json:\"hourlyRate\"`\n\tCostRate   *Rate `json:\"costRate\"`\n\tBillable   bool  `json:\"billable\"`\n\n\tTimeEstimate   TimeEstimate `json:\"timeEstimate\"`\n\tBudgetEstimate BaseEstimate `json:\"budgetEstimate\"`\n\tDuration       *Duration    `json:\"duration\"`\n\n\tArchived bool `json:\"archived\"`\n\tTemplate bool `json:\"template\"`\n\tPublic   bool `json:\"public\"`\n\tFavorite bool `json:\"favorite\"`\n\n\tMemberships []Membership `json:\"memberships\"`\n\n\t// Hydrated indicates if the attributes CustomFields and Tasks are filled\n\tHydrated     bool          `json:\"-\"`\n\tCustomFields []CustomField `json:\"customFields,omitempty\"`\n\tTasks        []Task        `json:\"tasks,omitempty\"`\n}\n\nfunc (p Project) GetID() string   { return p.ID }\nfunc (p Project) GetName() string { return p.Name }\n\n// EstimateType possible Estimate types\ntype EstimateType string\n\n// EstimateTypeAuto estimate is Auto\nconst EstimateTypeAuto = EstimateType(\"AUTO\")\n\n// EstimateTypeManual estimate is Manual\nconst EstimateTypeManual = EstimateType(\"MANUAL\")\n\n// EstimateResetOption possible Estimate Reset Options\ntype EstimateResetOption string\n\n// EstimateResetOptionMonthly estimate is Auto\nconst EstimateResetOptionMonthly = EstimateResetOption(\"MONTHLY\")\n\n// BaseEstimate DTO\ntype BaseEstimate struct {\n\tType         EstimateType         `json:\"type\"`\n\tActive       bool                 `json:\"active\"`\n\tResetOptions *EstimateResetOption `json:\"resetOptions\"`\n}\n\n// TimeEstimate DTO\ntype TimeEstimate struct {\n\tBaseEstimate\n\tEstimate           Duration `json:\"estimate\"`\n\tIncludeNonBillable bool     `json:\"includeNonBillable\"`\n}\n\n// BudgetEstimate DTO\ntype BudgetEstimate struct {\n\tBaseEstimate\n\tEstimate uint `json:\"estimate\"`\n}\n\n// UserStatus possible user status\ntype UserStatus string\n\n// UserStatusActive when the user is Active\nconst UserStatusActive = UserStatus(\"ACTIVE\")\n\n// UserStatusPendingEmailVerification when the user is Pending Email Verification\nconst UserStatusPendingEmailVerification = UserStatus(\"PENDING_EMAIL_VERIFICATION\")\n\n// UserStatusDeleted when the user is Deleted\nconst UserStatusDeleted = UserStatus(\"DELETED\")\n\n// User DTO\ntype User struct {\n\tID               string       `json:\"id\"`\n\tActiveWorkspace  string       `json:\"activeWorkspace\"`\n\tDefaultWorkspace string       `json:\"defaultWorkspace\"`\n\tEmail            string       `json:\"email\"`\n\tMemberships      []Membership `json:\"memberships\"`\n\tName             string       `json:\"name\"`\n\tProfilePicture   string       `json:\"profilePicture\"`\n\tSettings         UserSettings `json:\"settings\"`\n\tStatus           UserStatus   `json:\"status\"`\n\tRoles            *[]Role      `json:\"roles\"`\n}\n\nfunc (e User) GetID() string   { return e.ID }\nfunc (e User) GetName() string { return e.Name }\n\n// Role DTO\ntype Role struct {\n\tRole     string       `json:\"role\"`\n\tEntities []RoleEntity `json:\"entities\"`\n}\n\ntype RoleEntity struct {\n\tID   string `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\n// WeekStart when the week starts\ntype WeekStart string\n\n// WeekStartMonday when start at Monday\nconst WeekStartMonday = WeekStart(\"MONDAY\")\n\n// WeekStartTuesday when start at Tuesday\nconst WeekStartTuesday = WeekStart(\"TUESDAY\")\n\n// WeekStartWednesday when start at Wednesday\nconst WeekStartWednesday = WeekStart(\"WEDNESDAY\")\n\n// WeekStartThursday when start at Thursday\nconst WeekStartThursday = WeekStart(\"THURSDAY\")\n\n// WeekStartFriday when start at Friday\nconst WeekStartFriday = WeekStart(\"FRIDAY\")\n\n// WeekStartSaturday when start at Saturday\nconst WeekStartSaturday = WeekStart(\"SATURDAY\")\n\n// WeekStartSunday when start at Sunday\nconst WeekStartSunday = WeekStart(\"SUNDAY\")\n\n// UserSettings DTO\ntype UserSettings struct {\n\tDateFormat            string                `json:\"dateFormat\"`\n\tIsCompactViewOn       bool                  `json:\"isCompactViewOn\"`\n\tLongRunning           bool                  `json:\"longRunning\"`\n\tSendNewsletter        bool                  `json:\"sendNewsletter\"`\n\tSummaryReportSettings SummaryReportSettings `json:\"summaryReportSettings\"`\n\tTimeFormat            string                `json:\"timeFormat\"`\n\tTimeTrackingManual    bool                  `json:\"timeTrackingManual\"`\n\tTimeZone              string                `json:\"timeZone\"`\n\tWeekStart             string                `json:\"weekStart\"`\n\tWeeklyUpdates         bool                  `json:\"weeklyUpdates\"`\n}\n\n// SummaryReportSettings DTO\ntype SummaryReportSettings struct {\n\tGroup    string `json:\"group\"`\n\tSubgroup string `json:\"subgroup\"`\n}\n\n// InvitedUser DTO\ntype InvitedUser struct {\n\tID          string       `json:\"id\"`\n\tEmail       string       `json:\"email\"`\n\tInvitation  Invitation   `json:\"invitation\"`\n\tMemberships []Membership `json:\"memberships\"`\n}\n\n// Invitation DTO\ntype Invitation struct {\n\tCreation       time.Time  `json:\"creation\"`\n\tInvitationCode string     `json:\"invitationCode\"`\n\tMembership     Membership `json:\"membership\"`\n\tWorkspaceID    string     `json:\"workspaceId\"`\n\tWorkspaceName  string     `json:\"workspaceName\"`\n}\n\n// TimeEntriesList DTO\ntype TimeEntriesList struct {\n\tAllEntriesCount int64           `json:\"allEntriesCount\"`\n\tGotAllEntries   bool            `json:\"gotAllEntries\"`\n\tTimeEntriesList []TimeEntryImpl `json:\"timeEntriesList\"`\n}\n\n// TimeEntryImpl DTO\ntype TimeEntryImpl struct {\n\tBillable     bool         `json:\"billable\"`\n\tDescription  string       `json:\"description\"`\n\tID           string       `json:\"id\"`\n\tIsLocked     bool         `json:\"isLocked\"`\n\tProjectID    string       `json:\"projectId\"`\n\tTagIDs       []string     `json:\"tagIds\"`\n\tTaskID       string       `json:\"taskId\"`\n\tTimeInterval TimeInterval `json:\"timeInterval\"`\n\tUserID       string       `json:\"userId\"`\n\tWorkspaceID  string       `json:\"workspaceId\"`\n}\n"
  },
  {
    "path": "api/dto/request.go",
    "content": "package dto\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// DateTime is a time presentation for parameters\ntype DateTime struct {\n\ttime.Time\n}\n\n// MarshalJSON converts DateTime correctly\nfunc (d DateTime) MarshalJSON() ([]byte, error) {\n\treturn []byte(strconv.Quote(d.String())), nil\n}\n\nfunc (d DateTime) String() string {\n\treturn d.Time.UTC().Format(\"2006-01-02T15:04:05Z\")\n}\n\n// Duration is a time presentation for parameters\ntype Duration struct {\n\ttime.Duration\n}\n\n// MarshalJSON converts Duration correctly\nfunc (d Duration) MarshalJSON() ([]byte, error) {\n\treturn []byte(\"\\\"\" + d.String() + \"\\\"\"), nil\n}\n\n// UnmarshalJSON converts a JSON value to Duration correctly\nfunc (d *Duration) UnmarshalJSON(b []byte) error {\n\tvar s string\n\tif err := json.Unmarshal(b, &s); err != nil {\n\t\treturn errors.Wrap(err, \"unmarshal duration\")\n\t}\n\n\tdc, err := StringToDuration(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t*d = Duration{dc}\n\treturn err\n}\n\nfunc StringToDuration(s string) (time.Duration, error) {\n\tif len(s) < 4 {\n\t\treturn 0, errors.Errorf(\"duration %s is invalid\", s)\n\t}\n\n\tvar u, dc time.Duration\n\tvar j, i int\n\tfor ; i < len(s); i++ {\n\t\tswitch s[i] {\n\t\tcase 'P', 'T':\n\t\t\tj = i + 1\n\t\t\tcontinue\n\t\tcase 'H':\n\t\t\tu = time.Hour\n\t\tcase 'M':\n\t\t\tu = time.Minute\n\t\tcase 'S':\n\t\t\tu = time.Second\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\n\t\tv, err := strconv.Atoi(s[j:i])\n\t\tif err != nil {\n\t\t\treturn 0, errors.Wrap(err, \"cast cast \"+s[j:i]+\" to int\")\n\t\t}\n\t\tdc = dc + time.Duration(v)*u\n\t\tj = i + 1\n\t}\n\n\treturn dc, nil\n}\n\nfunc (d Duration) String() string {\n\ts := d.Duration.String()\n\ti := strings.LastIndex(s, \".\")\n\tif i > -1 {\n\t\ts = s[0:i] + \"s\"\n\t}\n\n\treturn \"PT\" + strings.ToUpper(s)\n}\n\nfunc (dd Duration) HumanString() string {\n\td := dd.Duration\n\tp := \"\"\n\tif d < 0 {\n\t\tp = \"-\"\n\t\td = d * -1\n\t}\n\n\treturn p + fmt.Sprintf(\"%d:%02d:%02d\",\n\t\tint64(d.Hours()), int64(d.Minutes())%60, int64(d.Seconds())%60)\n}\n\ntype pagination struct {\n\tpage     int\n\tpageSize int\n}\n\nfunc newPagination(page, size int) pagination {\n\treturn pagination{\n\t\tpage:     page,\n\t\tpageSize: size,\n\t}\n}\n\n// AppendToQuery decorates the URL with pagination parameters\nfunc (p pagination) AppendToQuery(u *url.URL) *url.URL {\n\tv := u.Query()\n\n\tif p.page != 0 {\n\t\tv.Add(\"page\", strconv.Itoa(p.page))\n\t}\n\tif p.pageSize != 0 {\n\t\tv.Add(\"page-size\", strconv.Itoa(p.pageSize))\n\t}\n\n\tu.RawQuery = v.Encode()\n\n\treturn u\n}\n\ntype PaginatedRequest interface {\n\tWithPagination(page, size int) PaginatedRequest\n}\n\n// GetTimeEntryRequest to get a time entry\ntype GetTimeEntryRequest struct {\n\tHydrated               *bool\n\tConsiderDurationFormat *bool\n}\n\n// AppendToQuery decorates the URL with the query string needed for this Request\nfunc (r GetTimeEntryRequest) AppendToQuery(u *url.URL) *url.URL {\n\tv := u.Query()\n\tif r.Hydrated != nil && *r.Hydrated {\n\t\tv.Add(\"hydrated\", \"true\")\n\t}\n\tif r.ConsiderDurationFormat != nil && *r.ConsiderDurationFormat {\n\t\tv.Add(\"consider-duration-format\", \"true\")\n\t}\n\n\tu.RawQuery = v.Encode()\n\n\treturn u\n}\n\n// UserTimeEntriesRequest to get entries of a user\ntype UserTimeEntriesRequest struct {\n\tDescription string\n\tStart       *DateTime\n\tEnd         *DateTime\n\tProject     string\n\tTask        string\n\tTagIDs      []string\n\n\tProjectRequired        *bool\n\tTaskRequired           *bool\n\tConsiderDurationFormat *bool\n\tHydrated               *bool\n\tOnlyInProgress         *bool\n\n\tpagination\n}\n\n// WithPagination add pagination to the UserTimeEntriesRequest\nfunc (r UserTimeEntriesRequest) WithPagination(page, size int) PaginatedRequest {\n\tr.pagination = newPagination(page, size)\n\treturn r\n}\n\n// AppendToQuery decorates the URL with the query string needed for this Request\nfunc (r UserTimeEntriesRequest) AppendToQuery(u *url.URL) *url.URL {\n\tu = r.pagination.AppendToQuery(u)\n\tv := u.Query()\n\n\tif r.Start != nil {\n\t\tv.Add(\"start\", r.Start.String())\n\t}\n\n\tif r.End != nil {\n\t\tv.Add(\"end\", r.End.String())\n\t}\n\n\taddNotNil := func(b *bool, p string) {\n\t\tif b == nil {\n\t\t\treturn\n\t\t}\n\n\t\tif *b {\n\t\t\tv.Add(p, \"1\")\n\t\t} else {\n\t\t\tv.Add(p, \"0\")\n\t\t}\n\n\t}\n\n\taddNotNil(r.ProjectRequired, \"project-required\")\n\taddNotNil(r.TaskRequired, \"task-required\")\n\taddNotNil(r.ConsiderDurationFormat, \"consider-duration-format\")\n\taddNotNil(r.Hydrated, \"hydrated\")\n\taddNotNil(r.OnlyInProgress, \"in-progress\")\n\n\taddNotEmpty := func(s string, p string) {\n\t\tif s == \"\" {\n\t\t\treturn\n\t\t}\n\n\t\tv.Add(p, s)\n\t}\n\n\taddNotEmpty(r.Description, \"description\")\n\taddNotEmpty(r.Project, \"project\")\n\taddNotEmpty(r.Task, \"task\")\n\n\tfor _, t := range r.TagIDs {\n\t\taddNotEmpty(t, \"tags\")\n\t}\n\n\tu.RawQuery = v.Encode()\n\n\treturn u\n}\n\n// OutTimeEntryRequest to end the current time entry\ntype OutTimeEntryRequest struct {\n\tEnd DateTime `json:\"end\"`\n}\n\n// CreateTimeEntryRequest to create a time entry is created\ntype CreateTimeEntryRequest struct {\n\tStart        DateTime           `json:\"start,omitempty\"`\n\tEnd          *DateTime          `json:\"end,omitempty\"`\n\tBillable     *bool              `json:\"billable,omitempty\"`\n\tDescription  string             `json:\"description,omitempty\"`\n\tProjectID    string             `json:\"projectId,omitempty\"`\n\tTaskID       string             `json:\"taskId,omitempty\"`\n\tTagIDs       []string           `json:\"tagIds,omitempty\"`\n\tCustomFields []CustomFieldValue `json:\"customFields,omitempty\"`\n}\n\n// CustomFieldValue DTO\ntype CustomFieldValue struct {\n\tCustomFieldID string `json:\"customFieldId\"`\n\tStatus        string `json:\"status\"`\n\tName          string `json:\"name\"`\n\tType          string `json:\"type\"`\n\tValue         string `json:\"value\"`\n}\n\n// UpdateTimeEntryRequest to update a time entry\ntype UpdateTimeEntryRequest struct {\n\tStart        DateTime           `json:\"start,omitempty\"`\n\tEnd          *DateTime          `json:\"end,omitempty\"`\n\tBillable     bool               `json:\"billable,omitempty\"`\n\tDescription  string             `json:\"description,omitempty\"`\n\tProjectID    string             `json:\"projectId,omitempty\"`\n\tTaskID       string             `json:\"taskId,omitempty\"`\n\tTagIDs       []string           `json:\"tagIds,omitempty\"`\n\tCustomFields []CustomFieldValue `json:\"customFields,omitempty\"`\n}\n\ntype GetClientsRequest struct {\n\tName     string\n\tArchived *bool\n\n\tpagination\n}\n\n// WithPagination add pagination to the GetClientsRequest\nfunc (r GetClientsRequest) WithPagination(page, size int) PaginatedRequest {\n\tr.pagination = newPagination(page, size)\n\treturn r\n}\n\n// AppendToQuery decorates the URL with the query string needed for this Request\nfunc (r GetClientsRequest) AppendToQuery(u *url.URL) *url.URL {\n\tu = r.pagination.AppendToQuery(u)\n\n\tv := u.Query()\n\n\tif r.Name != \"\" {\n\t\tv.Add(\"name\", r.Name)\n\t}\n\n\tif r.Archived != nil {\n\t\tv.Add(\"archived\", boolString[*r.Archived])\n\t}\n\n\tu.RawQuery = v.Encode()\n\n\treturn u\n}\n\ntype AddClientRequest struct {\n\tName string `json:\"name\"`\n}\n\ntype GetProjectsRequest struct {\n\tName     string\n\tArchived *bool\n\tClients  []string\n\tHydrated bool\n\n\tpagination\n}\n\n// WithPagination add pagination to the GetProjectRequest\nfunc (r GetProjectsRequest) WithPagination(page, size int) PaginatedRequest {\n\tr.pagination = newPagination(page, size)\n\treturn r\n}\n\nvar boolString = map[bool]string{\n\ttrue:  \"true\",\n\tfalse: \"false\",\n}\n\n// AppendToQuery decorates the URL with the query string needed for this Request\nfunc (r GetProjectsRequest) AppendToQuery(u *url.URL) *url.URL {\n\tu = r.pagination.AppendToQuery(u)\n\n\tv := u.Query()\n\n\tif r.Name != \"\" {\n\t\tv.Add(\"name\", r.Name)\n\t}\n\n\tif r.Hydrated {\n\t\tv.Add(\"hydrated\", \"true\")\n\t}\n\n\tif r.Archived != nil {\n\t\tv.Add(\"archived\", boolString[*r.Archived])\n\t}\n\n\tif len(r.Clients) > 0 {\n\t\tv.Add(\"clients\", strings.Join(r.Clients, \",\"))\n\t}\n\n\tu.RawQuery = v.Encode()\n\n\treturn u\n}\n\n// GetProjectRequest query parameters to fetch a project\ntype GetProjectRequest struct {\n\tHydrated bool\n}\n\n// AppendToQuery decorates the URL with a query string\nfunc (r GetProjectRequest) AppendToQuery(u *url.URL) *url.URL {\n\n\tv := u.Query()\n\n\tif r.Hydrated {\n\t\tv.Add(\"hydrated\", \"true\")\n\t}\n\n\tu.RawQuery = v.Encode()\n\treturn u\n}\n\n// AddProjectRequest represents the parameters to create a project\ntype AddProjectRequest struct {\n\tName     string `json:\"name\"`\n\tClientId string `json:\"clientId,omitempty\"`\n\tIsPublic bool   `json:\"isPublic\"`\n\tColor    string `json:\"color,omitempty\"`\n\tNote     string `json:\"note,omitempty\"`\n\tBillable bool   `json:\"billable\"`\n\tPublic   bool   `json:\"public\"`\n}\n\n// UpdateProjectRequest represents the parameters to update a project\ntype UpdateProjectRequest struct {\n\tName     *string `json:\"name,omitempty\"`\n\tClientId *string `json:\"clientId,omitempty\"`\n\tIsPublic *bool   `json:\"isPublic,omitempty\"`\n\tColor    *string `json:\"color,omitempty\"`\n\tNote     *string `json:\"note,omitempty\"`\n\tBillable *bool   `json:\"billable,omitempty\"`\n\tArchived *bool   `json:\"archived,omitempty\"`\n}\n\n// UpdateProjectMembershipsRequest represents a request to change which users\n// and groups have access to a project\ntype UpdateProjectMembershipsRequest struct {\n\tMemberships []UpdateProjectMembership `json:\"memberships\"`\n}\n\n// UpdateProjectMembership sets which user or group has access, and their\n// hourly rate\ntype UpdateProjectMembership struct {\n\tUserID     string `json:\"userId\"`\n\tHourlyRate Rate   `json:\"hourlyRate\"`\n}\n\n// UpdateProjectTemplateRequest represents a request to change isTemplate flag\n// of a project\ntype UpdateProjectTemplateRequest struct {\n\tIsTemplate bool `json:\"isTemplate\"`\n}\n\ntype GetTagsRequest struct {\n\tName     string\n\tArchived *bool\n\n\tpagination\n}\n\n// WithPagination add pagination to the GetTagsRequest\nfunc (r GetTagsRequest) WithPagination(page, size int) PaginatedRequest {\n\tr.pagination = newPagination(page, size)\n\treturn r\n}\n\n// AppendToQuery decorates the URL with the query string needed for this Request\nfunc (r GetTagsRequest) AppendToQuery(u *url.URL) *url.URL {\n\tu = r.pagination.AppendToQuery(u)\n\n\tv := u.Query()\n\tif r.Name != \"\" {\n\t\tv.Add(\"name\", r.Name)\n\t}\n\n\tif r.Archived != nil {\n\t\tv.Add(\"archived\", boolString[*r.Archived])\n\t}\n\n\tu.RawQuery = v.Encode()\n\n\treturn u\n}\n\n// GetTasksRequest represents the query filters to search tasks of a project\ntype GetTasksRequest struct {\n\tName   string\n\tActive bool\n\n\tpagination\n}\n\n// WithPagination add pagination to the GetTasksRequest\nfunc (r GetTasksRequest) WithPagination(page, size int) PaginatedRequest {\n\tr.pagination = newPagination(page, size)\n\treturn r\n}\n\n// AppendToQuery decorates the URL with the query string needed for this Request\nfunc (r GetTasksRequest) AppendToQuery(u *url.URL) *url.URL {\n\tu = r.pagination.AppendToQuery(u)\n\n\tv := u.Query()\n\tif r.Name != \"\" {\n\t\tv.Add(\"name\", r.Name)\n\t}\n\n\tif r.Active {\n\t\tv.Add(\"is-active\", \"true\")\n\t}\n\n\tu.RawQuery = v.Encode()\n\n\treturn u\n}\n\ntype AddTaskRequest struct {\n\tName        string    `json:\"name\"`\n\tAssigneeIDs *[]string `json:\"assigneeIds,omitempty\"`\n\tBillable    *bool     `json:\"billable,omitempty\"`\n\tEstimate    *Duration `json:\"estimate,omitempty\"`\n\tStatus      *string   `json:\"status,omitempty\"`\n}\n\ntype UpdateTaskRequest struct {\n\tName        string    `json:\"name\"`\n\tAssigneeIDs *[]string `json:\"assigneeIds,omitempty\"`\n\tBillable    *bool     `json:\"billable,omitempty\"`\n\tEstimate    *Duration `json:\"estimate,omitempty\"`\n\tStatus      *string   `json:\"status,omitempty\"`\n}\n\ntype ChangeTimeEntriesInvoicedRequest struct {\n\tTimeEntryIDs []string `json:\"timeEntryIds\"`\n\tInvoiced     bool     `json:\"invoiced\"`\n}\n\ntype WorkspaceUsersRequest struct {\n\tEmail string\n\tpagination\n}\n\n// WithPagination add pagination to the WorkspaceUsersRequest\nfunc (r WorkspaceUsersRequest) WithPagination(page, size int) PaginatedRequest {\n\tr.pagination = newPagination(page, size)\n\treturn r\n}\n\n// AppendToQuery decorates the URL with the query string needed for this Request\nfunc (r WorkspaceUsersRequest) AppendToQuery(u *url.URL) *url.URL {\n\tu = r.pagination.AppendToQuery(u)\n\n\tv := u.Query()\n\n\tif r.Email != \"\" {\n\t\tv.Add(\"email\", r.Email)\n\t}\n\n\tu.RawQuery = v.Encode()\n\n\treturn u\n}\n\n// UpdateProjectUserRateRequest represents a request to change a user\n// billable rate on a project\ntype UpdateProjectUserRateRequest struct {\n\tAmount uint      `json:\"amount\"`\n\tSince  *DateTime `json:\"since,omitempty\"`\n}\n\n// BaseEstimateRequest is basic information to estime a project\ntype BaseEstimateRequest struct {\n\tType         *EstimateType        `json:\"type,omitempty\"`\n\tActive       bool                 `json:\"active\"`\n\tResetOptions *EstimateResetOption `json:\"resetOption,omitempty\"`\n}\n\n// TimeEstimateRequest set parameters for time estimate on a project\ntype TimeEstimateRequest struct {\n\tBaseEstimateRequest\n\tEstimate *Duration `json:\"estimate,omitempty\"`\n}\n\n// BudgetEstimateRequest set parameters for time estimate on a project\ntype BudgetEstimateRequest struct {\n\tBaseEstimateRequest\n\tEstimate *uint64 `json:\"estimate,omitempty\"`\n}\n\n// UpdateProjectEstimateRequest represents a request to set a estimate of a\n// project\ntype UpdateProjectEstimateRequest struct {\n\tTimeEstimate   TimeEstimateRequest   `json:\"timeEstimate\"`\n\tBudgetEstimate BudgetEstimateRequest `json:\"budgetEstimate\"`\n}\n"
  },
  {
    "path": "api/httpClient.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/pkg/errors\"\n)\n\n// QueryAppender an interface to identify if the parameters should be sent through the query or body\ntype QueryAppender interface {\n\tAppendToQuery(*url.URL) *url.URL\n}\n\n// ErrorNotFound Not Found\nvar ErrorNotFound = dto.Error{Message: \"Nothing was found\", Code: 404}\n\n// ErrorForbidden Forbidden\nvar ErrorForbidden = dto.Error{Message: \"Forbidden\", Code: 403}\n\n// ErrorTooManyRequests Too Many Requests\nvar ErrorTooManyRequests = dto.Error{Message: \"Too Many Requests\", Code: 429}\n\ntype transport struct {\n\tapiKey string\n\tnext   http.RoundTripper\n}\n\nfunc (t transport) RoundTrip(r *http.Request) (*http.Response, error) {\n\tr.Header.Set(\"X-Api-Key\", t.apiKey)\n\n\treturn t.next.RoundTrip(r)\n}\n\n// NewRequest to be used in Client\nfunc (c *client) NewRequest(method, uri string, body interface{}) (*http.Request, error) {\n\tu, err := c.baseURL.Parse(c.baseURL.Path + \"/\" + uri)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif qa, ok := body.(QueryAppender); ok {\n\t\tu = qa.AppendToQuery(u)\n\t}\n\n\tif method == \"GET\" {\n\t\tbody = nil\n\t}\n\n\tvar buf io.ReadWriter\n\tif body != nil {\n\t\tbuf = new(bytes.Buffer)\n\t\terr := json.NewEncoder(buf).Encode(body)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tc.infof(\"request body: %s\", buf.(*bytes.Buffer))\n\t}\n\n\treq, err := http.NewRequest(method, u.String(), buf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif body != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treturn req, nil\n}\n\n// Do executes a http.Request inside the Clockify's Client\nfunc (c *client) Do(\n\treq *http.Request, v interface{}, name string) (r *http.Response, err error) {\n\n\t<-c.requestTickets\n\n\tr, err = c.Client.Do(req)\n\tif err != nil {\n\t\treturn r, err\n\t}\n\tdefer func() {\n\t\tif e := r.Body.Close(); e != nil {\n\t\t\terr = e\n\t\t}\n\t}()\n\n\tbuf := new(bytes.Buffer)\n\n\t_, err = io.Copy(buf, r.Body)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\n\tif c.debugLogger != nil {\n\t\tc.debugf(\"name: %s, method: %s, url: %s, status: %d, response: \\\"%s\\\"\",\n\t\t\tname, req.Method, req.URL.String(), r.StatusCode, buf)\n\t} else {\n\t\tc.infof(\"name: %s, method: %s, url: %s, status: %d\",\n\t\t\tname, req.Method, req.URL.String(), r.StatusCode)\n\t}\n\n\tdecoder := json.NewDecoder(buf)\n\n\tif r.StatusCode < 200 || r.StatusCode > 300 {\n\t\tvar apiErr dto.Error\n\t\terr = decoder.Decode(&apiErr)\n\t\tif err != nil && err != io.EOF {\n\t\t\treturn r, errors.WithStack(err)\n\t\t}\n\n\t\tif r.StatusCode == 404 && apiErr.Message == \"\" {\n\t\t\tapiErr = ErrorNotFound\n\t\t}\n\n\t\tif r.StatusCode == 403 && apiErr.Message == \"\" {\n\t\t\tapiErr = ErrorForbidden\n\t\t}\n\n\t\tif r.StatusCode == 429 && apiErr.Message == \"\" {\n\t\t\tapiErr = ErrorTooManyRequests\n\t\t}\n\n\t\tif apiErr.Message == \"\" {\n\t\t\tapiErr.Message = \"No response\"\n\t\t}\n\n\t\treturn r, errors.WithStack(apiErr)\n\t}\n\n\tif v == nil {\n\t\treturn r, nil\n\t}\n\n\tif buf.Len() == 0 {\n\t\treturn r, nil\n\t}\n\n\treturn r, errors.WithStack(decoder.Decode(v))\n}\n"
  },
  {
    "path": "api/logger.go",
    "content": "package api\n\n// Logger for the Client\ntype Logger interface {\n\tPrint(v ...interface{})\n\tPrintf(format string, v ...interface{})\n\tPrintln(v ...interface{})\n}\n\n// SetDebugLogger debug logger\nfunc (c *client) SetDebugLogger(logger Logger) Client {\n\tc.debugLogger = logger\n\treturn c\n}\n\nfunc (c *client) debugf(format string, v ...interface{}) {\n\tif c.debugLogger == nil {\n\t\treturn\n\t}\n\n\tc.debugLogger.Printf(format, v...)\n}\n\n// SetInfoLogger info logger\nfunc (c *client) SetInfoLogger(logger Logger) Client {\n\tc.infoLogger = logger\n\treturn c\n}\n\nfunc (c *client) infof(format string, v ...interface{}) {\n\tif c.infoLogger == nil {\n\t\treturn\n\t}\n\n\tc.infoLogger.Printf(format, v...)\n}\n"
  },
  {
    "path": "api/project_test.go",
    "content": "package api_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\nfunc TestUpdateProjectMemberships(t *testing.T) {\n\texampleID2 := \"62f2af744a912b05acc7c792\"\n\terrPrefix := `update project memberships: `\n\turi := \"/v1/workspaces/\" + exampleID +\n\t\t\"/projects/\" + exampleID +\n\t\t\"/memberships\"\n\n\ttts := []testCase{\n\t\t&simpleTestCase{\n\t\t\tname:  \"requires workspace\",\n\t\t\tparam: api.UpdateProjectMembershipsParam{ProjectID: \"p1\"},\n\t\t\terr:   errPrefix + \"workspace is required\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname:  \"requires project\",\n\t\t\tparam: api.UpdateProjectMembershipsParam{Workspace: \"w\"},\n\t\t\terr:   errPrefix + \"project id is required\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"valid workspace\",\n\t\t\tparam: api.UpdateProjectMembershipsParam{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tProjectID: exampleID,\n\t\t\t},\n\t\t\terr: errPrefix + \"workspace .* is not valid ID\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"valid project\",\n\t\t\tparam: api.UpdateProjectMembershipsParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: \"p1\",\n\t\t\t},\n\t\t\terr: errPrefix + \"project .* is not valid ID\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"valid user or groups\",\n\t\t\tparam: api.UpdateProjectMembershipsParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tMemberships: []api.UpdateMembership{{\n\t\t\t\t\tUserOrGroupID: \"ug\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\terr: errPrefix + \"user or group .* is not valid ID\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"required user or groups\",\n\t\t\tparam: api.UpdateProjectMembershipsParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tMemberships: []api.UpdateMembership{\n\t\t\t\t\t{UserOrGroupID: \"\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: errPrefix + `user or group is required`,\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"valid user or groups (second one)\",\n\t\t\tparam: api.UpdateProjectMembershipsParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tMemberships: []api.UpdateMembership{\n\t\t\t\t\t{UserOrGroupID: exampleID},\n\t\t\t\t\t{UserOrGroupID: \"ug\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: errPrefix + `user or group \\(\"ug\"\\) is not valid ID`,\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"simplest update\",\n\t\t\tparam: api.UpdateProjectMembershipsParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t},\n\n\t\t\tresult: dto.Project{ID: \"p1\", Name: \"project 1\"},\n\n\t\t\trequestMethod: \"patch\",\n\t\t\trequestUrl:    uri,\n\t\t\trequestBody:   `{\"memberships\":[]}`,\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody:   `{\"id\":\"p1\", \"name\": \"project 1\"}`,\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"update with members\",\n\t\t\tparam: api.UpdateProjectMembershipsParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tMemberships: []api.UpdateMembership{{\n\t\t\t\t\tUserOrGroupID: exampleID,\n\t\t\t\t}},\n\t\t\t},\n\n\t\t\tresult: dto.Project{ID: \"p1\", Name: \"project 1\"},\n\n\t\t\trequestMethod: \"patch\",\n\t\t\trequestUrl:    uri,\n\t\t\trequestBody: `{\"memberships\":[{\n\t\t\t\t\"userId\":\"` + exampleID + `\", \"hourlyRate\":{\"amount\":0}\n\t\t\t}]}`,\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody:   `{\"id\":\"p1\", \"name\": \"project 1\"}`,\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"update with many members\",\n\t\t\tparam: api.UpdateProjectMembershipsParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tMemberships: []api.UpdateMembership{\n\t\t\t\t\t{UserOrGroupID: exampleID},\n\t\t\t\t\t{UserOrGroupID: exampleID2, HourlyRateAmount: 10},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\tresult: dto.Project{ID: \"p1\", Name: \"project 1\"},\n\n\t\t\trequestMethod: \"patch\",\n\t\t\trequestUrl:    uri,\n\t\t\trequestBody: `{\"memberships\":[\n\t\t\t\t{\"userId\":\"` + exampleID + `\", \"hourlyRate\":{\"amount\":0}},\n\t\t\t\t{\"userId\":\"` + exampleID2 + `\", \"hourlyRate\":{\"amount\":10}}\n\t\t\t]}`,\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody:   `{\"id\":\"p1\", \"name\": \"project 1\"}`,\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"error response\",\n\t\t\tparam: api.UpdateProjectMembershipsParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t},\n\n\t\t\trequestMethod: \"patch\",\n\t\t\trequestUrl:    uri,\n\t\t\trequestBody:   `{\"memberships\":[]}`,\n\n\t\t\tresponseStatus: 400,\n\t\t\tresponseBody:   `{\"code\": 10, \"message\":\"error\"}`,\n\n\t\t\terr: errPrefix + `error`,\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\trunClient(t, tt,\n\t\t\tfunc(c api.Client, p interface{}) (interface{}, error) {\n\t\t\t\treturn c.UpdateProjectMemberships(\n\t\t\t\t\tp.(api.UpdateProjectMembershipsParam))\n\t\t\t})\n\t}\n}\n\nfunc TestDeleteProject(t *testing.T) {\n\terrPrefix := `delete project: `\n\turi := \"/v1/workspaces/\" + exampleID + \"/projects/\" + exampleID\n\n\ttts := []testCase{\n\t\t&simpleTestCase{\n\t\t\tname:  \"requires workspace\",\n\t\t\tparam: api.DeleteProjectParam{ProjectID: \"p1\"},\n\t\t\terr:   errPrefix + \"workspace is required\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname:  \"requires project\",\n\t\t\tparam: api.DeleteProjectParam{Workspace: \"w\"},\n\t\t\terr:   errPrefix + \"project id is required\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"valid workspace\",\n\t\t\tparam: api.DeleteProjectParam{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tProjectID: exampleID,\n\t\t\t},\n\t\t\terr: errPrefix + \"workspace .* is not valid ID\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"valid project\",\n\t\t\tparam: api.DeleteProjectParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: \"p1\",\n\t\t\t},\n\t\t\terr: errPrefix + \"project .* is not valid ID\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"delete\",\n\t\t\tparam: api.DeleteProjectParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t},\n\n\t\t\tresult: dto.Project{ID: \"p1\", Name: \"project 1\"},\n\n\t\t\trequestMethod: \"delete\",\n\t\t\trequestUrl:    uri,\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody:   `{\"id\":\"p1\", \"name\": \"project 1\"}`,\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"error response\",\n\t\t\tparam: api.DeleteProjectParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t},\n\n\t\t\trequestMethod: \"delete\",\n\t\t\trequestUrl:    uri,\n\n\t\t\tresponseStatus: 400,\n\t\t\tresponseBody:   `{\"code\": 10, \"message\":\"error\"}`,\n\n\t\t\terr: errPrefix + `error`,\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\trunClient(t, tt,\n\t\t\tfunc(c api.Client, p interface{}) (interface{}, error) {\n\t\t\t\treturn c.DeleteProject(p.(api.DeleteProjectParam))\n\t\t\t})\n\t}\n}\n\nfunc TestGetProject(t *testing.T) {\n\terrPrefix := `get project \"\\w*\": `\n\turi := \"/v1/workspaces/\" + exampleID + \"/projects/\" + exampleID\n\n\ttts := []testCase{\n\t\t&simpleTestCase{\n\t\t\tname:  \"requires workspace\",\n\t\t\tparam: api.GetProjectParam{ProjectID: \"p1\"},\n\t\t\terr:   errPrefix + \"workspace is required\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname:  \"requires project\",\n\t\t\tparam: api.GetProjectParam{Workspace: \"w\"},\n\t\t\terr:   errPrefix + \"project id is required\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"valid workspace\",\n\t\t\tparam: api.GetProjectParam{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tProjectID: exampleID,\n\t\t\t},\n\t\t\terr: errPrefix + \"workspace .* is not valid ID\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"valid project\",\n\t\t\tparam: api.GetProjectParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: \"p1\",\n\t\t\t},\n\t\t\terr: errPrefix + \"project .* is not valid ID\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"simple\",\n\t\t\tparam: api.GetProjectParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t},\n\n\t\t\tresult: &dto.Project{ID: \"p1\", Name: \"project 1\"},\n\n\t\t\trequestMethod: \"get\",\n\t\t\trequestUrl:    uri,\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody:   `{\"id\":\"p1\", \"name\": \"project 1\"}`,\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"hydrated\",\n\t\t\tparam: api.GetProjectParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tHydrate:   true,\n\t\t\t},\n\n\t\t\tresult: &dto.Project{ID: \"p1\", Name: \"project 1\", Hydrated: true},\n\n\t\t\trequestMethod: \"get\",\n\t\t\trequestUrl:    uri + \"?hydrated=true\",\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody:   `{\"id\":\"p1\", \"name\": \"project 1\"}`,\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"error response\",\n\t\t\tparam: api.GetProjectParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t},\n\n\t\t\trequestMethod: \"get\",\n\t\t\trequestUrl:    uri,\n\n\t\t\tresponseStatus: 400,\n\t\t\tresponseBody:   `{\"code\": 10, \"message\":\"error\"}`,\n\n\t\t\terr: errPrefix + `error`,\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"not found\",\n\t\t\tparam: api.GetProjectParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t},\n\n\t\t\trequestMethod: \"get\",\n\t\t\trequestUrl:    uri,\n\n\t\t\tresponseStatus: 404,\n\t\t\tresponseBody:   `{\"code\": 0, \"message\":\"not found\"}`,\n\n\t\t\terr: errPrefix + `not found`,\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\trunClient(t, tt,\n\t\t\tfunc(c api.Client, p interface{}) (interface{}, error) {\n\t\t\t\treturn c.GetProject(p.(api.GetProjectParam))\n\t\t\t})\n\t}\n}\n\nfunc TestGetProjects(t *testing.T) {\n\terrPrefix := \"get projects: \"\n\turi := \"/v1/workspaces/\" + exampleID + \"/projects\"\n\tvar l []dto.Project\n\n\ttts := []testCase{\n\t\t&simpleTestCase{\n\t\t\tname:  \"requires workspace\",\n\t\t\tparam: api.GetProjectsParam{},\n\t\t\terr:   errPrefix + \"workspace is required\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname:  \"valid workspace\",\n\t\t\tparam: api.GetProjectsParam{Workspace: \"w\"},\n\t\t\terr:   errPrefix + \"workspace .* is not valid ID\",\n\t\t},\n\t\t(&multiRequestTestCase{\n\t\t\tname: \"get all pages, but find none\",\n\t\t\tparam: api.GetProjectsParam{\n\t\t\t\tWorkspace:       exampleID,\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t},\n\n\t\t\tresult: l,\n\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?page=1&page-size=50\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: \"[]\",\n\t\t\t}),\n\t\t(&multiRequestTestCase{\n\t\t\tname: \"get all pages, find five\",\n\t\t\tparam: api.GetProjectsParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tPaginationParam: api.PaginationParam{\n\t\t\t\t\tPageSize: 2,\n\t\t\t\t\tAllPages: true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\tresult: []dto.Project{\n\t\t\t\t{ID: \"p1\"},\n\t\t\t\t{ID: \"p2\"},\n\t\t\t\t{ID: \"p3\"},\n\t\t\t\t{ID: \"p4\"},\n\t\t\t\t{ID: \"p5\"},\n\t\t\t},\n\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?page=1&page-size=2\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: `[{\"id\":\"p1\"},{\"id\":\"p2\"}]`,\n\t\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?page=2&page-size=2\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: `[{\"id\":\"p3\"},{\"id\":\"p4\"}]`,\n\t\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?page=3&page-size=2\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: `[{\"id\":\"p5\"}]`,\n\t\t\t}),\n\t\t(&multiRequestTestCase{\n\t\t\tname: \"get all pages, hydrated\",\n\t\t\tparam: api.GetProjectsParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tHydrate:   true,\n\t\t\t\tPaginationParam: api.PaginationParam{\n\t\t\t\t\tPageSize: 1,\n\t\t\t\t\tAllPages: true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\tresult: []dto.Project{\n\t\t\t\t{ID: \"p1\", Hydrated: true},\n\t\t\t\t{ID: \"p2\", Hydrated: true},\n\t\t\t},\n\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?hydrated=true&page=1&page-size=1\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: `[{\"id\":\"p1\"}]`,\n\t\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?hydrated=true&page=2&page-size=1\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: `[{\"id\":\"p2\"}]`,\n\t\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?hydrated=true&page=3&page-size=1\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: `[]`,\n\t\t\t}),\n\t\t&simpleTestCase{\n\t\t\tname: \"all parameters\",\n\t\t\tparam: api.GetProjectsParam{\n\t\t\t\tWorkspace:       exampleID,\n\t\t\t\tHydrate:         true,\n\t\t\t\tName:            \"project\",\n\t\t\t\tClients:         []string{\"c1\", \"c2\"},\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t},\n\n\t\t\tresult: []dto.Project{{\n\t\t\t\tID: \"p1\", Name: \"project 1\", Hydrated: true}},\n\n\t\t\trequestMethod: \"get\",\n\t\t\trequestUrl: uri +\n\t\t\t\t\"?clients=c1%2Cc2&hydrated=true&name=project&\" +\n\t\t\t\t\"page=1&page-size=50\",\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody:   `[{\"id\":\"p1\", \"name\": \"project 1\"}]`,\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"error response\",\n\t\t\tparam: api.GetProjectsParam{\n\t\t\t\tWorkspace:       exampleID,\n\t\t\t\tPaginationParam: api.PaginationParam{Page: 2},\n\t\t\t},\n\n\t\t\trequestMethod: \"get\",\n\t\t\trequestUrl:    uri + \"?page=2&page-size=50\",\n\n\t\t\tresponseStatus: 400,\n\t\t\tresponseBody:   `{\"code\": 10, \"message\":\"error\"}`,\n\n\t\t\terr: `get projects: error \\(code: 10\\)`,\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"missing duration\",\n\t\t\tparam: api.GetProjectsParam{\n\t\t\t\tWorkspace:       exampleID,\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t},\n\n\t\t\tresult: []dto.Project{\n\t\t\t\t{\n\t\t\t\t\tID:       \"wod\",\n\t\t\t\t\tName:     \"without duration\",\n\t\t\t\t\tArchived: true,\n\t\t\t\t\tDuration: nil,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:       \"wd\",\n\t\t\t\t\tName:     \"with duration\",\n\t\t\t\t\tArchived: true,\n\t\t\t\t\tDuration: &dto.Duration{\n\t\t\t\t\t\tDuration: 561*time.Hour +\n\t\t\t\t\t\t\t13*time.Minute + 28*time.Second,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trequestMethod: \"get\",\n\t\t\trequestUrl:    uri + \"?page=1&page-size=50\",\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody: `[{\n\t\t\t\t\"id\":\"wod\",\n\t\t\t\t\"name\":\"without duration\",\n\t\t\t\t\"archived\":true,\n\t\t\t\t\"duration\":null,\n\t\t\t\t\"note\":\"\"\n\t\t\t}, {\n\t\t\t\t\"id\":\"wd\",\n\t\t\t\t\"name\":\"with duration\",\n\t\t\t\t\"archived\":true,\n\t\t\t\t\"duration\":\"PT561H13M28S\",\n\t\t\t\t\"note\":\"\"\n\t\t\t}]`,\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\trunClient(t, tt,\n\t\t\tfunc(c api.Client, p interface{}) (interface{}, error) {\n\t\t\t\treturn c.GetProjects(p.(api.GetProjectsParam))\n\t\t\t})\n\t}\n}\n\nfunc TestUpdateProjectTemplate(t *testing.T) {\n\terrPrefix := \"update project template: \"\n\ttts := []simpleTestCase{\n\t\t{\n\t\t\tname: \"workspace require\",\n\t\t\tparam: api.UpdateProjectTemplateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t},\n\n\t\t\terr: errPrefix + \"workspace is required\",\n\t\t},\n\t\t{\n\t\t\tname: \"project require\",\n\t\t\tparam: api.UpdateProjectTemplateParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t},\n\n\t\t\terr: errPrefix + \"project id is required\",\n\t\t},\n\t\t{\n\t\t\tname: \"valid workspace\",\n\t\t\tparam: api.UpdateProjectTemplateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: \"w\",\n\t\t\t},\n\n\t\t\terr: errPrefix + \"workspace .* is not valid ID\",\n\t\t},\n\t\t{\n\t\t\tname: \"valid project\",\n\t\t\tparam: api.UpdateProjectTemplateParam{\n\t\t\t\tProjectID: \"p\",\n\t\t\t\tWorkspace: exampleID,\n\t\t\t},\n\n\t\t\terr: errPrefix + \"project .* is not valid ID\",\n\t\t},\n\t\t{\n\t\t\tname: \"into template\",\n\t\t\tparam: api.UpdateProjectTemplateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tTemplate:  true,\n\t\t\t},\n\n\t\t\tresult: dto.Project{ID: exampleID},\n\n\t\t\trequestMethod: \"patch\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID + \"/template\",\n\t\t\trequestBody: `{\"isTemplate\":true}`,\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody:   `{\"id\":\"` + exampleID + `\"}`,\n\t\t},\n\t\t{\n\t\t\tname: \"not a template\",\n\t\t\tparam: api.UpdateProjectTemplateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tTemplate:  false,\n\t\t\t},\n\n\t\t\tresult: dto.Project{ID: exampleID},\n\n\t\t\trequestMethod: \"patch\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID + \"/template\",\n\t\t\trequestBody: `{\"isTemplate\":false}`,\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody:   `{\"id\":\"` + exampleID + `\"}`,\n\t\t},\n\t\t{\n\t\t\tname: \"error\",\n\t\t\tparam: api.UpdateProjectTemplateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tTemplate:  false,\n\t\t\t},\n\n\t\t\terr: errPrefix + \"failed .code: 90.\",\n\n\t\t\trequestMethod: \"patch\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID + \"/template\",\n\t\t\trequestBody: `{\"isTemplate\":false}`,\n\n\t\t\tresponseStatus: 400,\n\t\t\tresponseBody:   `{\"message\":\"failed\", \"code\": 90}`,\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\trunClient(t, &tts[i],\n\t\t\tfunc(c api.Client, p interface{}) (interface{}, error) {\n\t\t\t\treturn c.UpdateProjectTemplate(\n\t\t\t\t\tp.(api.UpdateProjectTemplateParam))\n\t\t\t})\n\t}\n}\n\nfunc TestUpdateProjectEstimate(t *testing.T) {\n\terrPrefix := \"update project estimate: \"\n\ttts := []simpleTestCase{\n\t\t{\n\t\t\tname: \"workspace require\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tMethod:    api.EstimateMethodNone,\n\t\t\t},\n\n\t\t\terr: errPrefix + \"workspace is required\",\n\t\t},\n\t\t{\n\t\t\tname: \"project require\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tMethod:    api.EstimateMethodNone,\n\t\t\t},\n\n\t\t\terr: errPrefix + \"project id is required\",\n\t\t},\n\t\t{\n\t\t\tname: \"estimate method required\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t},\n\n\t\t\terr: errPrefix + \"estimate method is required\",\n\t\t},\n\t\t{\n\t\t\tname: \"valid workspace\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tMethod:    api.EstimateMethodNone,\n\t\t\t},\n\n\t\t\terr: errPrefix + \"workspace .* is not valid ID\",\n\t\t},\n\t\t{\n\t\t\tname: \"valid project\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: \"p\",\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tMethod:    api.EstimateMethodNone,\n\t\t\t},\n\n\t\t\terr: errPrefix + \"project .* is not valid ID\",\n\t\t},\n\t\t{\n\t\t\tname: \"valid method\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tMethod:    \"m\",\n\t\t\t},\n\n\t\t\terr: errPrefix + \"valid options for estimate method are\",\n\t\t},\n\t\t{\n\t\t\tname: \"type should be set for budget\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tMethod:    api.EstimateMethodBudget,\n\t\t\t\tType:      \"t\",\n\t\t\t},\n\n\t\t\terr: errPrefix + \"valid options for estimate type are\",\n\t\t},\n\t\t{\n\t\t\tname: \"valid reset option\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID:   exampleID,\n\t\t\t\tWorkspace:   exampleID,\n\t\t\t\tMethod:      api.EstimateMethodBudget,\n\t\t\t\tType:        api.EstimateTypeTask,\n\t\t\t\tResetOption: \"daily\",\n\t\t\t},\n\n\t\t\terr: errPrefix + \"valid options for reset option are\",\n\t\t},\n\t\t{\n\t\t\tname: \"type should be set for time\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tMethod:    api.EstimateMethodTime,\n\t\t\t},\n\n\t\t\terr: errPrefix + \"valid options for estimate type are\",\n\t\t},\n\t\t{\n\t\t\tname: \"estimate should be set for budget method & type project\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tMethod:    api.EstimateMethodBudget,\n\t\t\t\tType:      api.EstimateTypeProject,\n\t\t\t},\n\n\t\t\terr: errPrefix +\n\t\t\t\t\"estimate should be greater than zero for type project\",\n\t\t},\n\t\t{\n\t\t\tname: \"estimate should be set for time method & type project\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tMethod:    api.EstimateMethodTime,\n\t\t\t\tType:      api.EstimateTypeProject,\n\t\t\t},\n\n\t\t\terr: errPrefix +\n\t\t\t\t\"estimate should be greater than zero for type project\",\n\t\t},\n\t\t{\n\t\t\tname: \"estimate should be positive for time method & type project\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tMethod:    api.EstimateMethodTime,\n\t\t\t\tType:      api.EstimateTypeProject,\n\t\t\t\tEstimate:  -1,\n\t\t\t},\n\n\t\t\terr: errPrefix +\n\t\t\t\t\"estimate should be greater than zero for type project\",\n\t\t},\n\t\t{\n\t\t\tname: \"estimate should be positive for time method & type project\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tMethod:    api.EstimateMethodTime,\n\t\t\t\tType:      api.EstimateTypeProject,\n\t\t\t\tEstimate:  -1,\n\t\t\t},\n\n\t\t\terr: errPrefix +\n\t\t\t\t\"estimate should be greater than zero for type project\",\n\t\t},\n\t\t{\n\t\t\tname: \"set estimate with budget for project\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tMethod:    api.EstimateMethodBudget,\n\t\t\t\tType:      api.EstimateTypeProject,\n\t\t\t\tEstimate:  1000,\n\t\t\t},\n\n\t\t\trequestMethod: \"patch\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID + \"/estimate\",\n\t\t\trequestBody: `{\n\t\t\t\t\"timeEstimate\": {\"active\": false},\n\t\t\t\t\"budgetEstimate\": {\n\t\t\t\t\t\"active\": true,\n\t\t\t\t\t\"estimate\": 1000,\n\t\t\t\t\t\"type\": \"MANUAL\"\n\t\t\t\t}\n\t\t\t}`,\n\n\t\t\tresponseStatus: 200,\n\t\t},\n\t\t{\n\t\t\tname: \"set estimate with time for project\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tMethod:    api.EstimateMethodTime,\n\t\t\t\tType:      api.EstimateTypeProject,\n\t\t\t\tEstimate:  int64(time.Minute)*90 + int64(time.Second)*15,\n\t\t\t},\n\n\t\t\trequestMethod: \"patch\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID + \"/estimate\",\n\t\t\trequestBody: `{\n\t\t\t\t\"budgetEstimate\": {\"active\": false},\n\t\t\t\t\"timeEstimate\": {\n\t\t\t\t\t\"active\": true,\n\t\t\t\t\t\"estimate\": \"PT1H30M15S\",\n\t\t\t\t\t\"type\": \"MANUAL\"\n\t\t\t\t}\n\t\t\t}`,\n\n\t\t\tresponseStatus: 200,\n\t\t},\n\t\t{\n\t\t\tname: \"set estimate to none for project\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tMethod:    api.EstimateMethodNone,\n\t\t\t},\n\n\t\t\trequestMethod: \"patch\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID + \"/estimate\",\n\t\t\trequestBody: `{\n\t\t\t\t\"budgetEstimate\": {\"active\": false},\n\t\t\t\t\"timeEstimate\": {\"active\": false}\n\t\t\t}`,\n\n\t\t\tresponseStatus: 200,\n\t\t},\n\t\t{\n\t\t\tname: \"set estimate with budget for tasks\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tMethod:    api.EstimateMethodBudget,\n\t\t\t\tType:      api.EstimateTypeTask,\n\t\t\t\tEstimate:  1000,\n\t\t\t},\n\n\t\t\trequestMethod: \"patch\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID + \"/estimate\",\n\t\t\trequestBody: `{\n\t\t\t\t\"timeEstimate\": {\"active\": false},\n\t\t\t\t\"budgetEstimate\": {\n\t\t\t\t\t\"active\": true,\n\t\t\t\t\t\"type\": \"AUTO\"\n\t\t\t\t}\n\t\t\t}`,\n\n\t\t\tresponseStatus: 200,\n\t\t},\n\t\t{\n\t\t\tname: \"set estimate with time for task\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tMethod:    api.EstimateMethodTime,\n\t\t\t\tType:      api.EstimateTypeTask,\n\t\t\t\tEstimate:  int64(time.Minute)*90 + int64(time.Second)*15,\n\t\t\t},\n\n\t\t\trequestMethod: \"patch\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID + \"/estimate\",\n\t\t\trequestBody: `{\n\t\t\t\t\"budgetEstimate\": {\"active\": false},\n\t\t\t\t\"timeEstimate\": {\n\t\t\t\t\t\"active\": true,\n\t\t\t\t\t\"type\": \"AUTO\"\n\t\t\t\t}\n\t\t\t}`,\n\n\t\t\tresponseStatus: 200,\n\t\t},\n\t\t{\n\t\t\tname: \"set estimate with time for task, and monthly reset\",\n\t\t\tparam: api.UpdateProjectEstimateParam{\n\t\t\t\tProjectID:   exampleID,\n\t\t\t\tWorkspace:   exampleID,\n\t\t\t\tMethod:      api.EstimateMethodTime,\n\t\t\t\tType:        api.EstimateTypeTask,\n\t\t\t\tResetOption: api.EstimateResetOptionMonthly,\n\t\t\t},\n\n\t\t\trequestMethod: \"patch\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID + \"/estimate\",\n\t\t\trequestBody: `{\n\t\t\t\t\"budgetEstimate\": {\"active\": false},\n\t\t\t\t\"timeEstimate\": {\n\t\t\t\t\t\"active\": true,\n\t\t\t\t\t\"type\": \"AUTO\",\n\t\t\t\t\t\"resetOption\": \"MONTHLY\"\n\t\t\t\t}\n\t\t\t}`,\n\n\t\t\tresponseStatus: 200,\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\trunClient(t, &tts[i],\n\t\t\tfunc(c api.Client, p interface{}) (interface{}, error) {\n\t\t\t\treturn c.UpdateProjectEstimate(\n\t\t\t\t\tp.(api.UpdateProjectEstimateParam))\n\t\t\t})\n\t}\n}\n\nfunc TestUpdateProjectUserCostRate(t *testing.T) {\n\ttestUpdateProjectUserRate(t,\n\t\t\"update project user cost rate: \",\n\t\t\"cost-rate\",\n\t\tfunc(c api.Client, p interface{}) (interface{}, error) {\n\t\t\treturn c.UpdateProjectUserCostRate(\n\t\t\t\tp.(api.UpdateProjectUserRateParam))\n\t\t})\n}\n\nfunc TestUpdateProjectUserBillableRate(t *testing.T) {\n\ttestUpdateProjectUserRate(t,\n\t\t\"update project user billable rate: \",\n\t\t\"hourly-rate\",\n\t\tfunc(c api.Client, p interface{}) (interface{}, error) {\n\t\t\treturn c.UpdateProjectUserBillableRate(\n\t\t\t\tp.(api.UpdateProjectUserRateParam))\n\t\t})\n}\n\nfunc testUpdateProjectUserRate(t *testing.T,\n\terrPrefix, uriSufix string,\n\tfn func(api.Client, interface{}) (interface{}, error)) {\n\tsince, _ := time.Parse(\"2006-01-02\", \"2022-02-02\")\n\ttts := []simpleTestCase{\n\t\t{\n\t\t\tname: \"project is required\",\n\t\t\tparam: api.UpdateProjectUserRateParam{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tUserID:    \"u\",\n\t\t\t},\n\t\t\terr: errPrefix + \"project id is required\",\n\t\t},\n\t\t{\n\t\t\tname: \"workspace is required\",\n\t\t\tparam: api.UpdateProjectUserRateParam{\n\t\t\t\tProjectID: \"p-1\",\n\t\t\t\tUserID:    \"u\",\n\t\t\t},\n\t\t\terr: errPrefix + \"workspace is required\",\n\t\t},\n\t\t{\n\t\t\tname: \"user is required\",\n\t\t\tparam: api.UpdateProjectUserRateParam{\n\t\t\t\tProjectID: \"p-1\",\n\t\t\t\tWorkspace: \"w\",\n\t\t\t},\n\t\t\terr: errPrefix + \"user id is required\",\n\t\t},\n\t\t{\n\t\t\tname: \"project should be a ID\",\n\t\t\tparam: api.UpdateProjectUserRateParam{\n\t\t\t\tProjectID: \"p-1\",\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tUserID:    exampleID,\n\t\t\t},\n\t\t\terr: errPrefix + \"project id (.*) is not valid\",\n\t\t},\n\t\t{\n\t\t\tname: \"user should be a ID\",\n\t\t\tparam: api.UpdateProjectUserRateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tUserID:    \"u-1\",\n\t\t\t},\n\t\t\terr: errPrefix + \"user id (.*) is not valid\",\n\t\t},\n\t\t{\n\t\t\tname: \"workspace should be a ID\",\n\t\t\tparam: api.UpdateProjectUserRateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tUserID:    exampleID,\n\t\t\t},\n\t\t\terr: errPrefix + \"workspace (.*) is not valid\",\n\t\t},\n\t\t{\n\t\t\tname: \"only amount\",\n\t\t\tparam: api.UpdateProjectUserRateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tUserID:    exampleID,\n\t\t\t\tAmount:    10,\n\t\t\t},\n\n\t\t\trequestMethod: \"put\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID +\n\t\t\t\t\"/users/\" + exampleID + \"/\" + uriSufix,\n\t\t\trequestBody: `{\"amount\":10}`,\n\n\t\t\tresponseStatus: 200,\n\t\t},\n\t\t{\n\t\t\tname: \"amount and since\",\n\t\t\tparam: api.UpdateProjectUserRateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tUserID:    exampleID,\n\t\t\t\tAmount:    10,\n\t\t\t\tSince:     &since,\n\t\t\t},\n\n\t\t\trequestMethod: \"put\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID +\n\t\t\t\t\"/users/\" + exampleID + \"/\" + uriSufix,\n\t\t\trequestBody: `{\"amount\":10,\"since\":\"2022-02-02T00:00:00Z\"}`,\n\n\t\t\terr:            errPrefix + \"custom error.*code: 42\",\n\t\t\tresponseStatus: 400,\n\t\t\tresponseBody:   `{\"message\":\"custom error\",\"code\":42}`,\n\t\t},\n\t\t{\n\t\t\tname: \"fail\",\n\t\t\tparam: api.UpdateProjectUserRateParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tUserID:    exampleID,\n\t\t\t\tAmount:    10,\n\t\t\t\tSince:     &since,\n\t\t\t},\n\n\t\t\trequestMethod: \"put\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID +\n\t\t\t\t\"/users/\" + exampleID + \"/\" + uriSufix,\n\t\t\trequestBody: `{\"amount\":10,\"since\":\"2022-02-02T00:00:00Z\"}`,\n\n\t\t\terr:            errPrefix + \"custom error.*code: 42\",\n\t\t\tresponseStatus: 400,\n\t\t\tresponseBody:   `{\"message\":\"custom error\",\"code\":42}`,\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\trunClient(t, &tts[i], fn)\n\t}\n}\n\nfunc TestUpdateProject(t *testing.T) {\n\tbt := true\n\tbf := false\n\tn := \"special\"\n\tempty := \"\"\n\ttts := []simpleTestCase{\n\t\t{\n\t\t\tname:  \"project is required\",\n\t\t\tparam: api.UpdateProjectParam{Workspace: \"w\"},\n\t\t\terr:   \"update project: project id is required\",\n\t\t},\n\t\t{\n\t\t\tname:  \"workspace is required\",\n\t\t\tparam: api.UpdateProjectParam{ProjectID: \"p-1\"},\n\t\t\terr:   \"update project: workspace is required\",\n\t\t},\n\t\t{\n\t\t\tname: \"project should be a ID\",\n\t\t\tparam: api.UpdateProjectParam{\n\t\t\t\tProjectID: \"p-1\",\n\t\t\t\tWorkspace: exampleID,\n\t\t\t},\n\t\t\terr: \"update project: project id (.*) is not valid\",\n\t\t},\n\t\t{\n\t\t\tname: \"workspace should be a ID\",\n\t\t\tparam: api.UpdateProjectParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: \"w\",\n\t\t\t},\n\t\t\terr: \"update project: workspace (.*) is not valid\",\n\t\t},\n\t\t{\n\t\t\tname: \"color is not hex\",\n\t\t\tparam: api.UpdateProjectParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tColor:     \"#zzz\",\n\t\t\t},\n\t\t\terr: \"update project: color .* is not a hex string\",\n\t\t},\n\t\t{\n\t\t\tname: \"color must have 3 or 6 numbers (4)\",\n\t\t\tparam: api.UpdateProjectParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tColor:     \"#0000\",\n\t\t\t},\n\t\t\terr: \"update project: color must have 3.*or 6.*numbers\",\n\t\t},\n\t\t{\n\t\t\tname: \"color must have 3 or 6 numbers (2)\",\n\t\t\tparam: api.UpdateProjectParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tColor:     \"#00\",\n\t\t\t},\n\t\t\terr: \"update project: color must have 3.*or 6.*numbers\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty update\",\n\t\t\tparam: api.UpdateProjectParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t},\n\n\t\t\trequestMethod: \"put\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID,\n\t\t\trequestBody: \"{}\",\n\n\t\t\tresponseStatus: 200,\n\t\t},\n\t\t{\n\t\t\tname: \"full update\",\n\t\t\tparam: api.UpdateProjectParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tName:      \"a new name\",\n\t\t\t\tPublic:    &bt,\n\t\t\t\tArchived:  &bf,\n\t\t\t\tNote:      &n,\n\t\t\t\tClientId:  &exampleID,\n\t\t\t\tColor:     \"012345\",\n\t\t\t\tBillable:  &bt,\n\t\t\t},\n\n\t\t\trequestMethod: \"put\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID,\n\t\t\trequestBody: `{\n\t\t\t\t\"archived\":false,\n\t\t\t\t\"isPublic\":true,\n\t\t\t\t\"billable\":true,\n\t\t\t\t\"clientId\":\"` + exampleID + `\",\n\t\t\t\t\"note\": \"special\",\n\t\t\t\t\"color\": \"#012345\",\n\t\t\t\t\"name\":\"a new name\"\n\t\t\t}`,\n\n\t\t\tresponseStatus: 200,\n\t\t},\n\t\t{\n\t\t\tname: \"expand color and remove client\",\n\t\t\tparam: api.UpdateProjectParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tClientId:  &empty,\n\t\t\t\tColor:     \"#0f0\",\n\t\t\t},\n\n\t\t\trequestMethod: \"put\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID,\n\t\t\trequestBody: `{\n\t\t\t\t\"clientId\":\"\",\n\t\t\t\t\"color\": \"#00ff00\"\n\t\t\t}`,\n\n\t\t\tresponseStatus: 200,\n\t\t},\n\t\t{\n\t\t\tname: \"report 404\",\n\t\t\tparam: api.UpdateProjectParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t},\n\t\t\terr: \"update project: Nothing was found .*404\",\n\n\t\t\trequestMethod: \"put\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID,\n\t\t\trequestBody: `{}`,\n\n\t\t\tresponseStatus: 404,\n\t\t},\n\t\t{\n\t\t\tname: \"report 403\",\n\t\t\tparam: api.UpdateProjectParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t},\n\t\t\terr: \"update project: Forbidden.*403\",\n\n\t\t\trequestMethod: \"put\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID,\n\t\t\trequestBody: `{}`,\n\n\t\t\tresponseStatus: 403,\n\t\t},\n\t\t{\n\t\t\tname: \"report no response\",\n\t\t\tparam: api.UpdateProjectParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t},\n\t\t\terr: \"update project: No response\",\n\n\t\t\trequestMethod: \"put\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID,\n\t\t\trequestBody: `{}`,\n\n\t\t\tresponseStatus: 400,\n\t\t\tresponseBody:   `{}`,\n\t\t},\n\t\t{\n\t\t\tname: \"report error\",\n\t\t\tparam: api.UpdateProjectParam{\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tWorkspace: exampleID,\n\t\t\t},\n\t\t\terr: \"update project: custom error.*code: 42\",\n\n\t\t\trequestMethod: \"put\",\n\t\t\trequestUrl: \"/v1/workspaces/\" + exampleID +\n\t\t\t\t\"/projects/\" + exampleID,\n\t\t\trequestBody: `{}`,\n\n\t\t\tresponseStatus: 400,\n\t\t\tresponseBody:   `{\"message\":\"custom error\",\"code\":42}`,\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\trunClient(t, &tts[i], func(\n\t\t\tc api.Client, p interface{}) (interface{}, error) {\n\t\t\treturn c.UpdateProject(p.(api.UpdateProjectParam))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "api/tag_test.go",
    "content": "package api_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\nfunc TestGetTags(t *testing.T) {\n\terrPrefix := `get tags.*: `\n\turi := \"/v1/workspaces/\" + exampleID +\n\t\t\"/tags\"\n\tvar l []dto.Tag\n\n\ttts := []testCase{\n\t\t&simpleTestCase{\n\t\t\tname:  \"requires workspace\",\n\t\t\tparam: api.GetTagsParam{},\n\t\t\terr:   errPrefix + \"workspace is required\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname:  \"valid workspace\",\n\t\t\tparam: api.GetTagsParam{Workspace: \"w\"},\n\t\t\terr:   errPrefix + \"workspace .* is not valid ID\",\n\t\t},\n\t\t(&multiRequestTestCase{\n\t\t\tname: \"get all pages, but find none\",\n\t\t\tparam: api.GetTagsParam{\n\t\t\t\tWorkspace:       exampleID,\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t},\n\n\t\t\tresult: l,\n\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?page=1&page-size=50\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: \"[]\",\n\t\t\t}),\n\t\t(&multiRequestTestCase{\n\t\t\tname: \"get all pages, find five\",\n\t\t\tparam: api.GetTagsParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tPaginationParam: api.PaginationParam{\n\t\t\t\t\tPageSize: 2,\n\t\t\t\t\tAllPages: true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\tresult: []dto.Tag{\n\t\t\t\t{ID: \"p1\"},\n\t\t\t\t{ID: \"p2\"},\n\t\t\t\t{ID: \"p3\"},\n\t\t\t\t{ID: \"p4\"},\n\t\t\t\t{ID: \"p5\"},\n\t\t\t},\n\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?page=1&page-size=2\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: `[{\"id\":\"p1\"},{\"id\":\"p2\"}]`,\n\t\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?page=2&page-size=2\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: `[{\"id\":\"p3\"},{\"id\":\"p4\"}]`,\n\t\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?page=3&page-size=2\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: `[{\"id\":\"p5\"}]`,\n\t\t\t}),\n\t\t&simpleTestCase{\n\t\t\tname: \"all parameters\",\n\t\t\tparam: api.GetTagsParam{\n\t\t\t\tWorkspace:       exampleID,\n\t\t\t\tName:            \"tag\",\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t},\n\n\t\t\tresult: []dto.Tag{{ID: \"p1\", Name: \"tag 1\"}},\n\n\t\t\trequestMethod: \"get\",\n\t\t\trequestUrl: uri +\n\t\t\t\t\"?name=tag&page=1&page-size=50\",\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody:   `[{\"id\":\"p1\", \"name\": \"tag 1\"}]`,\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"error response\",\n\t\t\tparam: api.GetTagsParam{\n\t\t\t\tWorkspace:       exampleID,\n\t\t\t\tPaginationParam: api.PaginationParam{Page: 2},\n\t\t\t},\n\n\t\t\trequestMethod: \"get\",\n\t\t\trequestUrl:    uri + \"?page=2&page-size=50\",\n\n\t\t\tresponseStatus: 400,\n\t\t\tresponseBody:   `{\"code\": 10, \"message\":\"error\"}`,\n\n\t\t\terr: errPrefix + `error \\(code: 10\\)`,\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\trunClient(t, tt,\n\t\t\tfunc(c api.Client, p interface{}) (interface{}, error) {\n\t\t\t\treturn c.GetTags(\n\t\t\t\t\tp.(api.GetTagsParam))\n\t\t\t})\n\t}\n\n}\n"
  },
  {
    "path": "api/task_test.go",
    "content": "package api_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\nfunc TestGetTasks(t *testing.T) {\n\terrPrefix := `get tasks from project .*: `\n\turi := \"/v1/workspaces/\" + exampleID +\n\t\t\"/projects/\" + exampleID +\n\t\t\"/tasks\"\n\tvar l []dto.Task\n\n\ttts := []testCase{\n\t\t&simpleTestCase{\n\t\t\tname:  \"requires workspace\",\n\t\t\tparam: api.GetTasksParam{ProjectID: exampleID},\n\t\t\terr:   errPrefix + \"workspace is required\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname:  \"valid workspace\",\n\t\t\tparam: api.GetTasksParam{Workspace: \"w\", ProjectID: exampleID},\n\t\t\terr:   errPrefix + \"workspace .* is not valid ID\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname:  \"requires project id\",\n\t\t\tparam: api.GetTasksParam{Workspace: exampleID},\n\t\t\terr:   errPrefix + \"project id is required\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname:  \"valid project id\",\n\t\t\tparam: api.GetTasksParam{ProjectID: \"w\", Workspace: exampleID},\n\t\t\terr:   errPrefix + \"project id .* is not valid ID\",\n\t\t},\n\t\t(&multiRequestTestCase{\n\t\t\tname: \"get all pages, but find none\",\n\t\t\tparam: api.GetTasksParam{\n\t\t\t\tWorkspace:       exampleID,\n\t\t\t\tProjectID:       exampleID,\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t},\n\n\t\t\tresult: l,\n\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?page=1&page-size=50\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: \"[]\",\n\t\t\t}),\n\t\t(&multiRequestTestCase{\n\t\t\tname: \"get all pages, find five\",\n\t\t\tparam: api.GetTasksParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tProjectID: exampleID,\n\t\t\t\tPaginationParam: api.PaginationParam{\n\t\t\t\t\tPageSize: 2,\n\t\t\t\t\tAllPages: true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\tresult: []dto.Task{\n\t\t\t\t{ID: \"p1\"},\n\t\t\t\t{ID: \"p2\"},\n\t\t\t\t{ID: \"p3\"},\n\t\t\t\t{ID: \"p4\"},\n\t\t\t\t{ID: \"p5\"},\n\t\t\t},\n\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?page=1&page-size=2\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: `[{\"id\":\"p1\"},{\"id\":\"p2\"}]`,\n\t\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?page=2&page-size=2\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: `[{\"id\":\"p3\"},{\"id\":\"p4\"}]`,\n\t\t\t}).\n\t\t\taddHttpCall(&httpRequest{\n\t\t\t\tmethod:   \"get\",\n\t\t\t\turl:      uri + \"?page=3&page-size=2\",\n\t\t\t\tstatus:   200,\n\t\t\t\tresponse: `[{\"id\":\"p5\"}]`,\n\t\t\t}),\n\t\t&simpleTestCase{\n\t\t\tname: \"all parameters\",\n\t\t\tparam: api.GetTasksParam{\n\t\t\t\tWorkspace:       exampleID,\n\t\t\t\tProjectID:       exampleID,\n\t\t\t\tName:            \"project\",\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t},\n\n\t\t\tresult: []dto.Task{{ID: \"p1\", Name: \"project 1\"}},\n\n\t\t\trequestMethod: \"get\",\n\t\t\trequestUrl: uri +\n\t\t\t\t\"?name=project&page=1&page-size=50\",\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody:   `[{\"id\":\"p1\", \"name\": \"project 1\"}]`,\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"error response\",\n\t\t\tparam: api.GetTasksParam{\n\t\t\t\tWorkspace:       exampleID,\n\t\t\t\tProjectID:       exampleID,\n\t\t\t\tPaginationParam: api.PaginationParam{Page: 2},\n\t\t\t},\n\n\t\t\trequestMethod: \"get\",\n\t\t\trequestUrl:    uri + \"?page=2&page-size=50\",\n\n\t\t\tresponseStatus: 400,\n\t\t\tresponseBody:   `{\"code\": 10, \"message\":\"error\"}`,\n\n\t\t\terr: `get tasks from project .*: error \\(code: 10\\)`,\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"missing estimate\",\n\t\t\tparam: api.GetTasksParam{\n\t\t\t\tWorkspace:       exampleID,\n\t\t\t\tProjectID:       exampleID,\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t},\n\n\t\t\tresult: []dto.Task{\n\t\t\t\t{\n\t\t\t\t\tID:           \"wod\",\n\t\t\t\t\tName:         \"without durations\",\n\t\t\t\t\tProjectID:    \"p\",\n\t\t\t\t\tDuration:     nil,\n\t\t\t\t\tEstimate:     nil,\n\t\t\t\t\tStatus:       api.TaskStatusDone,\n\t\t\t\t\tUserGroupIDs: []string{},\n\t\t\t\t\tAssigneeIDs:  []string{},\n\t\t\t\t\tBillable:     true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:        \"wd\",\n\t\t\t\t\tName:      \"with durations\",\n\t\t\t\t\tProjectID: \"p\",\n\t\t\t\t\tDuration: &dto.Duration{\n\t\t\t\t\t\tDuration: 120 * time.Hour,\n\t\t\t\t\t},\n\t\t\t\t\tEstimate: &dto.Duration{\n\t\t\t\t\t\tDuration: 120 * time.Hour,\n\t\t\t\t\t},\n\t\t\t\t\tStatus:       api.TaskStatusActive,\n\t\t\t\t\tUserGroupIDs: []string{},\n\t\t\t\t\tAssigneeIDs:  []string{},\n\t\t\t\t\tBillable:     true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trequestMethod: \"get\",\n\t\t\trequestUrl:    uri + \"?page=1&page-size=50\",\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody: `[{\n\t\t\t\t\"id\": \"wod\",\n\t\t\t\t\"name\": \"without durations\",\n\t\t\t\t\"projectId\": \"p\",\n\t\t\t\t\"assigneeIds\": [],\n\t\t\t\t\"assigneeId\": null,\n\t\t\t\t\"userGroupIds\": [],\n\t\t\t\t\"estimate\": null,\n\t\t\t\t\"status\": \"DONE\",\n\t\t\t\t\"duration\": null,\n\t\t\t\t\"billable\": true,\n\t\t\t\t\"hourlyRate\": null,\n\t\t\t\t\"costRate\": null\n\t\t\t},{\n\t\t\t\t\"id\": \"wd\",\n\t\t\t\t\"name\": \"with durations\",\n\t\t\t\t\"projectId\": \"p\",\n\t\t\t\t\"assigneeIds\": [],\n\t\t\t\t\"assigneeId\": null,\n\t\t\t\t\"userGroupIds\": [],\n\t\t\t\t\"estimate\": \"P120H\",\n\t\t\t\t\"status\": \"ACTIVE\",\n\t\t\t\t\"duration\": \"P120H\",\n\t\t\t\t\"billable\": true,\n\t\t\t\t\"hourlyRate\": null,\n\t\t\t\t\"costRate\": null\n\t\t\t}]`,\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\trunClient(t, tt,\n\t\t\tfunc(c api.Client, p interface{}) (interface{}, error) {\n\t\t\t\treturn c.GetTasks(\n\t\t\t\t\tp.(api.GetTasksParam))\n\t\t\t})\n\t}\n\n}\n"
  },
  {
    "path": "api/timeentry_test.go",
    "content": "package api_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t. \"github.com/lucassabreu/clockify-cli/internal/testhlp\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n)\n\nfunc TestCreateTimeEntry(t *testing.T) {\n\turi := \"/v1/workspaces/\" + exampleID + \"/time-entries\"\n\tend := MustParseTime(timehlp.SimplerTimeFormat, \"2022-11-07 11:00\")\n\tbTrue := true\n\tbFalse := false\n\n\ttts := []testCase{\n\t\t&simpleTestCase{\n\t\t\tname:  \"workspace is required\",\n\t\t\tparam: api.CreateTimeEntryParam{},\n\t\t\terr:   \"workspace is required\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"workspace is valid\",\n\t\t\tparam: api.CreateTimeEntryParam{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t},\n\t\t\terr: \"workspace .* is not valid ID\",\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"with just start time\",\n\t\t\tparam: api.CreateTimeEntryParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tStart: MustParseTime(timehlp.SimplerTimeFormat,\n\t\t\t\t\t\"2022-11-07 10:00\"),\n\t\t\t},\n\n\t\t\trequestMethod: \"post\",\n\t\t\trequestUrl:    uri,\n\t\t\trequestBody:   `{\"start\":\"2022-11-07T10:00:00Z\"}`,\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody:   `{\"id\": \"1\"}`,\n\n\t\t\tresult: dto.TimeEntryImpl{ID: \"1\"},\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"with all options (billable)\",\n\t\t\tparam: api.CreateTimeEntryParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tStart: MustParseTime(timehlp.SimplerTimeFormat,\n\t\t\t\t\t\"2022-11-07 10:00\"),\n\t\t\t\tEnd:         &end,\n\t\t\t\tBillable:    &bTrue,\n\t\t\t\tDescription: \"new entry\",\n\t\t\t\tProjectID:   \"p\",\n\t\t\t\tTaskID:      \"t\",\n\t\t\t\tTagIDs:      []string{\"tag1\", \"tag2\"},\n\t\t\t},\n\n\t\t\trequestMethod: \"post\",\n\t\t\trequestUrl:    uri,\n\t\t\trequestBody: `{\n\t\t\t\t\"start\":\"2022-11-07T10:00:00Z\",\n\t\t\t\t\"end\":\"2022-11-07T11:00:00Z\",\n\t\t\t\t\"billable\": true,\n\t\t\t\t\"description\": \"new entry\",\n\t\t\t\t\"projectId\": \"p\",\n\t\t\t\t\"taskId\": \"t\",\n\t\t\t\t\"tagIds\": [\"tag1\",\"tag2\"]\n\t\t\t}`,\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody:   `{\"id\": \"1\"}`,\n\n\t\t\tresult: dto.TimeEntryImpl{ID: \"1\"},\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"not billable\",\n\t\t\tparam: api.CreateTimeEntryParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tStart: MustParseTime(timehlp.SimplerTimeFormat,\n\t\t\t\t\t\"2022-11-07 10:00\"),\n\t\t\t\tBillable:    &bFalse,\n\t\t\t\tDescription: \"new entry\",\n\t\t\t\tProjectID:   \"p\",\n\t\t\t},\n\n\t\t\trequestMethod: \"post\",\n\t\t\trequestUrl:    uri,\n\t\t\trequestBody: `{\n\t\t\t\t\"start\":\"2022-11-07T10:00:00Z\",\n\t\t\t\t\"billable\": false,\n\t\t\t\t\"description\": \"new entry\",\n\t\t\t\t\"projectId\": \"p\"\n\t\t\t}`,\n\n\t\t\tresponseStatus: 200,\n\t\t\tresponseBody:   `{\"id\": \"1\"}`,\n\n\t\t\tresult: dto.TimeEntryImpl{ID: \"1\"},\n\t\t},\n\t\t&simpleTestCase{\n\t\t\tname: \"error response\",\n\t\t\tparam: api.CreateTimeEntryParam{\n\t\t\t\tWorkspace: exampleID,\n\t\t\t\tStart: MustParseTime(timehlp.SimplerTimeFormat,\n\t\t\t\t\t\"2022-11-07 10:00\"),\n\t\t\t},\n\n\t\t\trequestMethod: \"post\",\n\t\t\trequestUrl:    uri,\n\t\t\trequestBody:   `{\"start\":\"2022-11-07T10:00:00Z\"}`,\n\n\t\t\tresponseStatus: 400,\n\t\t\tresponseBody:   `{\"code\": 10, \"message\":\"error\"}`,\n\n\t\t\terr: `error`,\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\trunClient(t, tt,\n\t\t\tfunc(c api.Client, p interface{}) (interface{}, error) {\n\t\t\t\treturn c.CreateTimeEntry(\n\t\t\t\t\tp.(api.CreateTimeEntryParam))\n\t\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/clockify-cli/main.go",
    "content": "package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/AlecAivazis/survey/v2/terminal\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/mitchellh/go-homedir\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n\t\"github.com/spf13/viper\"\n)\n\nvar (\n\tversion = \"dev\"\n\tcommit  = \"none\"\n\tdate    = \"unknown\"\n)\n\nconst (\n\texitOK     = 0\n\texitError  = 1\n\texitCancel = 2\n)\n\nfunc main() {\n\texitCode := execute()\n\tos.Exit(exitCode)\n}\n\nfunc execute() int {\n\tf := cmdutil.NewFactory(cmdutil.Version{\n\t\tTag:    version,\n\t\tCommit: commit,\n\t\tDate:   date,\n\t})\n\n\trootCmd := cmd.NewCmdRoot(f)\n\trootCmd.SetFlagErrorFunc(func(_ *cobra.Command, err error) error {\n\t\treturn cmdutil.FlagErrorWrap(err)\n\t})\n\n\tcmd := rootCmd\n\terr := bindViper(rootCmd)\n\n\tif err == nil {\n\t\tcmd, err = rootCmd.ExecuteC()\n\t}\n\n\tif err == nil {\n\t\treturn exitOK\n\t}\n\n\tstderr := cmd.ErrOrStderr()\n\tif errors.Is(err, terminal.InterruptErr) {\n\t\t_, _ = fmt.Fprintln(stderr)\n\t\treturn exitCancel\n\t}\n\n\tvar flagError *cmdutil.FlagError\n\tif errors.As(err, &flagError) {\n\t\t_, _ = fmt.Fprintln(stderr, flagError.Error())\n\t\t_, _ = fmt.Fprintln(stderr, cmd.UsageString())\n\t\treturn exitError\n\t}\n\n\tif f.Config().IsDebuging() {\n\t\t_, _ = fmt.Fprintf(stderr, \"%+v\\n\", err)\n\t} else {\n\t\t_, _ = fmt.Fprintln(stderr, err.Error())\n\t}\n\n\treturn exitError\n}\n\nfunc bindViper(rootCmd *cobra.Command) error {\n\tenvPrefix := \"CLOCKIFY\"\n\tbind := func(flag *pflag.Flag, conf, sufix string) error {\n\t\tif flag == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tflag.Usage = flag.Usage +\n\t\t\t\" (defaults to env $\" + envPrefix + \"_\" + sufix + \")\"\n\n\t\treturn viper.BindPFlag(conf, flag)\n\t}\n\n\tvar err error\n\tl := rootCmd.PersistentFlags().Lookup\n\tif err = bind(l(\"token\"), cmdutil.CONF_TOKEN, \"TOKEN\"); err != nil {\n\t\treturn err\n\t}\n\n\terr = bind(l(\"workspace\"), cmdutil.CONF_WORKSPACE, \"WORKSPACE\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = bind(l(\"user-id\"), cmdutil.CONF_USER_ID, \"USER_ID\"); err != nil {\n\t\treturn err\n\t}\n\n\terr = bind(l(\"log-level\"), cmdutil.CONF_LOG_LEVEL, \"LOG_LEVEL\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tviper.RegisterAlias(cmdutil.CONF_ALLOW_NAME_FOR_ID, \"allow-project-name\")\n\tif err = bind(l(\"allow-name-for-id\"), cmdutil.CONF_ALLOW_NAME_FOR_ID,\n\t\t\"ALLOW_NAME_FOR_ID\"); err != nil {\n\t\treturn err\n\t}\n\n\tif err = bind(l(\"interactive\"), cmdutil.CONF_INTERACTIVE,\n\t\t\"INTERACTIVE\"); err != nil {\n\t\treturn err\n\t}\n\n\tif err = bind(l(\"interactive-page-size\"),\n\t\tcmdutil.CONF_INTERACTIVE_PAGE_SIZE,\n\t\t\"INTERACTIVE_PAGE_SIZE\"); err != nil {\n\t\treturn err\n\t}\n\n\tf := l(\"interactive\")\n\tf.Usage = f.Usage + \"\\n\" +\n\t\t\"You can be disable it temporally by setting it to 0 \" +\n\t\t\"(-i=0 or \" + envPrefix + \"_INTERACTIVE=0)\"\n\n\tvar cfgFile = \"\"\n\trootCmd.PersistentFlags().StringVar(&cfgFile, \"config\", \"\",\n\t\t\"config file (default is $HOME/.config/clockify-cli/config.yaml)\")\n\n\tvar viperErr error\n\trootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {\n\t\tif viperErr != nil {\n\t\t\treturn viperErr\n\t\t}\n\n\t\tif withTotals := cmd.Flags().Lookup(\"with-totals\"); withTotals != nil {\n\t\t\tviper.SetDefault(cmdutil.CONF_SHOW_TOTAL_DURATION, true)\n\t\t\tif err := viper.BindPFlag(\n\t\t\t\tcmdutil.CONF_SHOW_TOTAL_DURATION, withTotals); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif flag := cmd.Flags().Lookup(\"allow-incomplete\"); flag != nil {\n\t\t\tif err := bind(flag, cmdutil.CONF_ALLOW_INCOMPLETE,\n\t\t\t\t\"ALLOW_INCOMPLETE\"); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif flag := cmd.Flags().Lookup(\"tz\"); flag != nil {\n\t\t\tif err := bind(flag, cmdutil.CONF_TIMEZONE,\n\t\t\t\t\"TIMEZONE\"); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tcobra.OnInitialize(func() {\n\t\tif cfgFile != \"\" {\n\t\t\tviper.SetConfigFile(cfgFile)\n\t\t} else {\n\t\t\thome, err := homedir.Dir()\n\t\t\tif err != nil {\n\t\t\t\tviperErr = err\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tviper.AddConfigPath(home)\n\t\t\tviper.AddConfigPath(path.Join(home, \".config\"))\n\t\t\tviper.AddConfigPath(path.Join(home, \".config\", \"clockify-cli\"))\n\t\t\tviper.SetConfigName(\".clockify-cli\")\n\t\t}\n\n\t\tviper.SetEnvPrefix(envPrefix)\n\t\tviper.SetEnvKeyReplacer(strings.NewReplacer(\".\", \"_\"))\n\t\tviper.AutomaticEnv()\n\n\t\terr := viper.ReadInConfig()\n\t\tif errors.As(err, &viper.ConfigFileNotFoundError{}) {\n\t\t\treturn\n\t\t}\n\n\t\tviperErr = err\n\t})\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/gendocs/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra/doc\"\n)\n\nconst gendocFrontmatterTemplate = `---\ndate: %s\ntitle: \"%s\"\nslug: %s\nurl: %s\nweight: 40\n---\n`\n\nfunc main() {\n\tif err := execute(); err != nil {\n\t\tfmt.Fprintln(os.Stderr, err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc execute() error {\n\tdocdir := \"site/content/commands\"\n\tif len(os.Args) > 1 {\n\t\tdocdir = os.Args[1]\n\t}\n\n\tif err := os.MkdirAll(docdir, os.ModePerm); err != nil {\n\t\treturn err\n\t}\n\n\tnow := time.Now().Format(\"2006-01-02\")\n\tprepender := func(filename string) string {\n\t\tname := filepath.Base(filename)\n\t\tbase := strings.TrimSuffix(name, path.Ext(name))\n\t\turl := \"/en/commands/\" + strings.ToLower(base) + \"/\"\n\t\treturn fmt.Sprintf(gendocFrontmatterTemplate,\n\t\t\tnow, strings.ReplaceAll(base, \"_\", \" \"), base, url)\n\t}\n\n\tlinkHandler := func(name string) string {\n\t\tbase := strings.TrimSuffix(name, path.Ext(name))\n\t\treturn \"/en/commands/\" + strings.ToLower(base) + \"/\"\n\t}\n\n\tcmd := cmd.NewCmdRoot(cmdutil.NewFactory(cmdutil.Version{}))\n\n\tfmt.Println(\"Generating Hugo command-line documentation in\", docdir, \"...\")\n\terr := doc.GenMarkdownTreeCustom(cmd, docdir, prepender, linkHandler)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfmt.Println(\"Done.\")\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/release/main.go",
    "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype Version struct {\n\tMajor int\n\tMinor int\n\tPatch int\n}\n\nfunc (v Version) String() string {\n\treturn fmt.Sprintf(\"v%d.%d.%d\", v.Major, v.Minor, v.Patch)\n}\n\nfunc bumpVersion(v Version, bumpType string) Version {\n\tswitch bumpType {\n\tcase \"major\":\n\t\treturn Version{Major: v.Major + 1, Minor: 0, Patch: 0}\n\tcase \"minor\":\n\t\treturn Version{Major: v.Major, Minor: v.Minor + 1, Patch: 0}\n\tcase \"patch\":\n\t\treturn Version{Major: v.Major, Minor: v.Minor, Patch: v.Patch + 1}\n\tdefault:\n\t\treturn v\n\t}\n}\n\nfunc findLatestVersion(content string) (Version, error) {\n\tre := regexp.MustCompile(`## \\[v(\\d+)\\.(\\d+)\\.(\\d+)\\]`)\n\tmatches := re.FindAllStringSubmatch(content, -1)\n\tif len(matches) == 0 {\n\t\treturn Version{}, fmt.Errorf(\"no version found in CHANGELOG.md\")\n\t}\n\tfirst := matches[0]\n\tmajor, _ := strconv.Atoi(first[1])\n\tminor, _ := strconv.Atoi(first[2])\n\tpatch, _ := strconv.Atoi(first[3])\n\treturn Version{Major: major, Minor: minor, Patch: patch}, nil\n}\n\nfunc findUnreleasedSection(content string) (string, int, int) {\n\tscanner := bufio.NewScanner(strings.NewReader(content))\n\tvar lines []string\n\tinUnreleased := false\n\tstartLine := -1\n\tendLine := -1\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tlines = append(lines, line)\n\n\t\tif strings.HasPrefix(line, \"## [Unreleased]\") {\n\t\t\tinUnreleased = true\n\t\t\tstartLine = len(lines) - 1\n\t\t\tcontinue\n\t\t}\n\n\t\tif inUnreleased && strings.HasPrefix(line, \"## [\") {\n\t\t\tendLine = len(lines) - 1\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif startLine == -1 {\n\t\treturn \"\", -1, -1\n\t}\n\n\tif endLine == -1 {\n\t\tendLine = len(lines)\n\t}\n\n\tsection := strings.Join(lines[startLine:endLine], \"\\n\")\n\treturn section, startLine, endLine\n}\n\nfunc extractUnreleasedContent(unreleasedSection string) string {\n\tlines := strings.Split(unreleasedSection, \"\\n\")\n\tif len(lines) <= 1 {\n\t\treturn \"\"\n\t}\n\n\tvar content []string\n\tfor i := 1; i < len(lines); i++ {\n\t\tline := lines[i]\n\t\tif strings.HasPrefix(line, \"## [\") {\n\t\t\tbreak\n\t\t}\n\t\tcontent = append(content, line)\n\t}\n\n\treturn strings.TrimSpace(strings.Join(content, \"\\n\"))\n}\n\nfunc updateUnreleasedLink(content string, newVersion string) string {\n\toldUnreleasedLink := regexp.MustCompile(`\\[Unreleased\\]:.*`).FindString(content)\n\tif oldUnreleasedLink == \"\" {\n\t\treturn content\n\t}\n\n\tnewLink := fmt.Sprintf(\"[Unreleased]: https://github.com/lucassabreu/clockify-cli/compare/%s...HEAD\", newVersion) +\n\t\t\"\\n\" +\n\t\tfmt.Sprintf(\"[%s]: https://github.com/lucassabreu/clockify-cli/releases/tag/%s\", newVersion, newVersion)\n\treturn strings.Replace(content, oldUnreleasedLink, newLink, 1)\n}\n\nfunc runGitCommand(args ...string) error {\n\tcmd := exec.Command(args[0], args[1:]...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tcmd.Env = append(os.Environ(), \"GIT_EDITOR=true\", \"GIT_SEQUENCE_EDITOR=true\")\n\treturn cmd.Run()\n}\n\nfunc main() {\n\tif len(os.Args) != 2 {\n\t\tfmt.Fprintln(os.Stderr, \"Usage: release major|minor|patch\")\n\t\tos.Exit(1)\n\t}\n\n\tbumpType := strings.ToLower(os.Args[1])\n\tif bumpType != \"major\" && bumpType != \"minor\" && bumpType != \"patch\" {\n\t\tfmt.Fprintln(os.Stderr, \"Error: argument must be major, minor, or patch\")\n\t\tos.Exit(1)\n\t}\n\n\tchangelogPath := \"CHANGELOG.md\"\n\tif _, err := os.Stat(changelogPath); os.IsNotExist(err) {\n\t\tchangelogPath = \"../../CHANGELOG.md\"\n\t}\n\n\tcontentBytes, err := os.ReadFile(changelogPath)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Error reading %s: %v\\n\", changelogPath, err)\n\t\tos.Exit(1)\n\t}\n\tcontent := string(contentBytes)\n\n\tunreleasedSection, startLine, _ := findUnreleasedSection(content)\n\tif unreleasedSection == \"\" || startLine == -1 {\n\t\tfmt.Fprintln(os.Stderr, \"Error: no [Unreleased] section found in CHANGELOG.md\")\n\t\tos.Exit(1)\n\t}\n\n\tunreleasedContent := extractUnreleasedContent(unreleasedSection)\n\tif unreleasedContent == \"\" {\n\t\tfmt.Fprintln(os.Stderr, \"Error: [Unreleased] section is empty, nothing to release\")\n\t\tos.Exit(1)\n\t}\n\n\tlatestVersion, err := findLatestVersion(content)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Error finding latest version: %v\\n\", err)\n\t\tos.Exit(1)\n\t}\n\n\tnewVersion := bumpVersion(latestVersion, bumpType)\n\tdate := time.Now().Format(\"2006-01-02\")\n\n\tfmt.Printf(\"Releasing %s (bumping %s from %s)\\n\", newVersion, bumpType, latestVersion)\n\n\tnewVersionSection := fmt.Sprintf(\"## [%s] - %s\\n\\n%s\\n\", newVersion, date, unreleasedContent)\n\n\tlines := strings.Split(content, \"\\n\")\n\n\tvar unreleasedStartIdx, unreleasedEndIdx int\n\tfor i, line := range lines {\n\t\tif strings.HasPrefix(line, \"## [Unreleased]\") {\n\t\t\tunreleasedStartIdx = i\n\t\t}\n\t\tif unreleasedStartIdx > 0 && strings.HasPrefix(line, \"## [v\") {\n\t\t\tunreleasedEndIdx = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tvar newLines []string\n\tnewLines = append(newLines, lines[:unreleasedStartIdx+1]...)\n\tnewLines = append(newLines, \"\")\n\tnewLines = append(newLines, newVersionSection)\n\tnewLines = append(newLines, lines[unreleasedEndIdx:]...)\n\n\tnewContent := strings.Join(newLines, \"\\n\")\n\n\tnewContent = updateUnreleasedLink(newContent, newVersion.String())\n\n\tif err := os.WriteFile(changelogPath, []byte(newContent), 0644); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Error writing %s: %v\\n\", changelogPath, err)\n\t\tos.Exit(1)\n\t}\n\n\tfmt.Printf(\"Updated %s\\n\", changelogPath)\n\n\tif err := runGitCommand(\"git\", \"add\", changelogPath); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"Error: git add failed\")\n\t\tos.Exit(1)\n\t}\n\n\tcommitMsg := fmt.Sprintf(\"release: %s\", newVersion)\n\tif err := runGitCommand(\"git\", \"commit\", \"-m\", commitMsg); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"Error: git commit failed\")\n\t\tos.Exit(1)\n\t}\n\tfmt.Printf(\"Created commit: %s\\n\", commitMsg)\n\n\tif err := runGitCommand(\"git\", \"tag\", \"-a\", newVersion.String(), \"-m\", fmt.Sprintf(\"Release %s\", newVersion)); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"Error: git tag failed\")\n\t\tos.Exit(1)\n\t}\n\tfmt.Printf(\"Created tag: %s\\n\", newVersion)\n\n\tfmt.Println(\"Release completed successfully!\")\n}\n"
  },
  {
    "path": "docs/project-layout.md",
    "content": "# Clockify CLI Project Layout\n\nThe project is organized in the following folders and important files:\n\n- [`cmd/`](../cmd) - `main` packages to build executable binaries.\n- [`docs/`](.) - documentation of the project for maintainers and contributors.\n- [`scripts/`](../scripts) - build and release scripts.\n- [`api/`](../api) - golang implementation of the Clockify API.\n- [`pkg/`](../pkg) - other packages that support the `api` or commands.\n- [`internal/`](../internal) - Go packages that are highly specific to this project\n- [`go.mod`](../go.mod) - external Go dependencies for this project.\n- [`Makefile`](../Makefile) - most of setup and maintenance actions for this project.\n\n## Command line organization\n\nAll CLI commands will be under [`pkg/cmd/`](../pkg/cmd) and the file naming convention is this:\n\n```\npkg/cmd/<command>/<subcommand>/<subcommand>.go\n```\n\nFollowing the same structure as the command path, so `clockify-cli client add` is at\n`pkg/cmd/client/add.go`, all command packages will have a function named `NewCmd<subcommand>` that\nwill receive a `cmdutil.Factory` type and return a `*cobra.Command`.\n\nSpecific logic for that command must be kept at the same package as it, and all subcommands must be\nregistered on its parent package. So all subcommands of `client` will registered on the function\n`client.NewCmdClient()`.\n\nOutput formatters must stay under the package [`pkg/output/`](../pkg/output) using the following\nfile convention:\n\n```\npkg/output/<entity>/<format>.go\n```\n\nShared functionality for printing entities must be at the package\n[`pkg/outpututil/`](../pkg/outpututil).\n\n### Steps do create a new command\n\nSay you will create a new command `delete` under `client`.\n\n1. Create the package `pkg/cmd/client/delete/`\n2. Create a function called `NewCmdDelete` on a file `delete.go`\n    1. This function must receive a [`cmdutil.Factory`][] struct and\n       return a [`*cobra.Command`][] fully set.\n3. Edit the entity root command at `pkg/cmd/client/client.go` to register it as a subcommand using\n   the factory function previously created. If the entity root does not exist yet, then:\n    1. Create the file, and in it a function `NewCmdClient` that should receive `cmdutil.Factory`\n       and return a [`*cobra.Command`][] with all its subcommands.\n4. If is the first command of a entity:\n    1. Create a package called `pkg/output/client`\n    2. Implement output the five basic output formats `table` (default), `json`, `quiet` (only the\n       ID), `template` ([Go template](https://pkg.go.dev/text/template)) and `csv`. Each one on a\n       file by itself.\n\n## Credits\n\nThis document is based on the [project-layout.md from github/cli/cli][credit].\n\n[credit]: https://github.com/cli/cli/blob/trunk/docs/project-layout.md\n[`*cobra.Command`]: https://pkg.go.dev/github.com/spf13/cobra#Command\n[`cmdutil.Factory`]: ../pkg/cmdutil/factory.go\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/lucassabreu/clockify-cli\n\ngo 1.24\n\nrequire (\n\tgithub.com/AlecAivazis/survey/v2 v2.3.7\n\tgithub.com/MakeNowJust/heredoc v1.0.0\n\tgithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2\n\tgithub.com/creack/pty v1.1.17\n\tgithub.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec\n\tgithub.com/mitchellh/go-homedir v1.1.0\n\tgithub.com/olekukonko/tablewriter v0.0.5\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/spf13/cobra v1.8.0\n\tgithub.com/spf13/pflag v1.0.5\n\tgithub.com/spf13/viper v1.18.2\n\tgithub.com/stretchr/testify v1.9.0\n\tgolang.org/x/sync v0.7.0\n\tgolang.org/x/term v0.20.0\n\tgolang.org/x/text v0.15.0\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/fsnotify/fsnotify v1.7.0 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect\n\tgithub.com/magiconair/properties v1.8.7 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.15 // indirect\n\tgithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.2 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/sagikazarmark/locafero v0.4.0 // indirect\n\tgithub.com/sagikazarmark/slog-shim v0.1.0 // indirect\n\tgithub.com/sourcegraph/conc v0.3.0 // indirect\n\tgithub.com/spf13/afero v1.11.0 // indirect\n\tgithub.com/spf13/cast v1.6.0 // indirect\n\tgithub.com/stretchr/objx v0.5.2 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgolang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect\n\tgolang.org/x/sys v0.20.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect\n\tgopkg.in/ini.v1 v1.67.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=\ngithub.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=\ngithub.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=\ngithub.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=\ngithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=\ngithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=\ngithub.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=\ngithub.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=\ngithub.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=\ngithub.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=\ngithub.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=\ngithub.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=\ngithub.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=\ngithub.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=\ngithub.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=\ngithub.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=\ngithub.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=\ngithub.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=\ngithub.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=\ngithub.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=\ngithub.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=\ngithub.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=\ngithub.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=\ngithub.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=\ngithub.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=\ngithub.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=\ngithub.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4=\ngolang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=\ngolang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=\ngolang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "internal/consoletest/test.go",
    "content": "package consoletest\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/Netflix/go-expect\"\n\t\"github.com/hinshun/vt10x\"\n\n\tpseudotty \"github.com/creack/pty\"\n)\n\n// ExpectConsole is a helper to interact if the pseudo terminal on tests\ntype ExpectConsole interface {\n\tExpectEOF()\n\tExpectString(string)\n\tSend(string)\n\tSendLine(string)\n}\n\ntype console struct {\n\tt *testing.T\n\tc *expect.Console\n}\n\nfunc (c *console) ExpectEOF() {\n\tif _, err := c.c.ExpectEOF(); err != nil {\n\t\tc.t.Errorf(\"failed to ExpectEOF %v\", err)\n\t}\n}\n\nfunc (c *console) ExpectString(s string) {\n\tif _, err := c.c.ExpectString(s); err != nil {\n\t\tc.t.Errorf(\"failed to ExpectString (%s) %v\", s, err)\n\t}\n}\n\nfunc (c *console) Send(s string) {\n\tif _, err := c.c.Send(s); err != nil {\n\t\tc.t.Errorf(\"failed to Send %v\", err)\n\t}\n}\n\nfunc (c *console) SendLine(s string) {\n\tif _, err := c.c.SendLine(s); err != nil {\n\t\tc.t.Errorf(\"failed to SendLine %v\", err)\n\t}\n}\n\n// FileWriter is a simplification of the io.Stdout struct\ntype FileWriter interface {\n\tio.Writer\n\tFd() uintptr\n}\n\n// FileReader is a simplification of the io.Stdin struct\ntype FileReader interface {\n\tio.Reader\n\tFd() uintptr\n}\n\n// RunTestConsole simulates a terminal for interactive tests\n// This is mostly a adaptation of the RunTest function at\n// [survey_test.go](https://github.com/AlecAivazis/survey/blob/e47352f914346a910cc7e1ca9f65a7ac0674449a/survey_posix_test.go#L15),\n// but with interfaces exported to easy re-use on other packages.\nfunc RunTestConsole(\n\tt *testing.T,\n\tsetup func(out FileWriter, in FileReader) error,\n\tprocedure func(c ExpectConsole),\n) {\n\tt.Parallel()\n\n\tpty, tty, err := pseudotty.Open()\n\tif err != nil {\n\t\tt.Fatalf(\"failed to open pseudotty: %v\", err)\n\t}\n\n\tb := bytes.NewBufferString(\"\")\n\tterm := vt10x.New(vt10x.WithWriter(tty))\n\tc, err := expect.NewConsole(\n\t\texpect.WithStdin(pty),\n\t\texpect.WithStdout(term),\n\t\texpect.WithStdout(b),\n\t\texpect.WithCloser(pty, tty),\n\t)\n\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create console: %v\", err)\n\t}\n\tdefer func() { _ = c.Close() }()\n\n\tfinished := false\n\tt.Cleanup(func() {\n\t\tif finished {\n\t\t\treturn\n\t\t}\n\t\tt.Error(\n\t\t\t\"console test failed\\n\" +\n\t\t\t\t\"current output:\\n\" +\n\t\t\t\tb.String() + \"\\n\")\n\t})\n\n\tdonec := make(chan struct{})\n\tgo func() {\n\t\tdefer close(donec)\n\t\tprocedure(&console{c: c, t: t})\n\t}()\n\n\tgo func() {\n\t\tdefer func() { _ = c.Tty().Close() }()\n\t\tif err = setup(c.Tty(), c.Tty()); err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t}()\n\n\tselect {\n\tcase <-time.After(time.Second * 10):\n\t\tt.Error(\"console test timeout exceeded\")\n\tcase <-donec:\n\t\tfinished = true\n\t}\n}\n"
  },
  {
    "path": "internal/mocks/gen.go",
    "content": "package mocks\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n)\n\ntype Factory interface {\n\tcmdutil.Factory\n}\n\ntype Config interface {\n\tcmdutil.Config\n}\n\ntype Client interface {\n\tapi.Client\n}\n"
  },
  {
    "path": "internal/mocks/mock_Client.go",
    "content": "// Code generated by mockery; DO NOT EDIT.\n// github.com/vektra/mockery\n// template: testify\n\npackage mocks\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\tmock \"github.com/stretchr/testify/mock\"\n)\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewMockClient(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *MockClient {\n\tmock := &MockClient{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n\n// MockClient is an autogenerated mock type for the Client type\ntype MockClient struct {\n\tmock.Mock\n}\n\ntype MockClient_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *MockClient) EXPECT() *MockClient_Expecter {\n\treturn &MockClient_Expecter{mock: &_m.Mock}\n}\n\n// AddClient provides a mock function for the type MockClient\nfunc (_mock *MockClient) AddClient(addClientParam api.AddClientParam) (dto.Client, error) {\n\tret := _mock.Called(addClientParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for AddClient\")\n\t}\n\n\tvar r0 dto.Client\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.AddClientParam) (dto.Client, error)); ok {\n\t\treturn returnFunc(addClientParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.AddClientParam) dto.Client); ok {\n\t\tr0 = returnFunc(addClientParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Client)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.AddClientParam) error); ok {\n\t\tr1 = returnFunc(addClientParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_AddClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddClient'\ntype MockClient_AddClient_Call struct {\n\t*mock.Call\n}\n\n// AddClient is a helper method to define mock.On call\n//   - addClientParam api.AddClientParam\nfunc (_e *MockClient_Expecter) AddClient(addClientParam interface{}) *MockClient_AddClient_Call {\n\treturn &MockClient_AddClient_Call{Call: _e.mock.On(\"AddClient\", addClientParam)}\n}\n\nfunc (_c *MockClient_AddClient_Call) Run(run func(addClientParam api.AddClientParam)) *MockClient_AddClient_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.AddClientParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.AddClientParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_AddClient_Call) Return(client dto.Client, err error) *MockClient_AddClient_Call {\n\t_c.Call.Return(client, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_AddClient_Call) RunAndReturn(run func(addClientParam api.AddClientParam) (dto.Client, error)) *MockClient_AddClient_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// AddProject provides a mock function for the type MockClient\nfunc (_mock *MockClient) AddProject(addProjectParam api.AddProjectParam) (dto.Project, error) {\n\tret := _mock.Called(addProjectParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for AddProject\")\n\t}\n\n\tvar r0 dto.Project\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.AddProjectParam) (dto.Project, error)); ok {\n\t\treturn returnFunc(addProjectParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.AddProjectParam) dto.Project); ok {\n\t\tr0 = returnFunc(addProjectParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Project)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.AddProjectParam) error); ok {\n\t\tr1 = returnFunc(addProjectParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_AddProject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddProject'\ntype MockClient_AddProject_Call struct {\n\t*mock.Call\n}\n\n// AddProject is a helper method to define mock.On call\n//   - addProjectParam api.AddProjectParam\nfunc (_e *MockClient_Expecter) AddProject(addProjectParam interface{}) *MockClient_AddProject_Call {\n\treturn &MockClient_AddProject_Call{Call: _e.mock.On(\"AddProject\", addProjectParam)}\n}\n\nfunc (_c *MockClient_AddProject_Call) Run(run func(addProjectParam api.AddProjectParam)) *MockClient_AddProject_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.AddProjectParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.AddProjectParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_AddProject_Call) Return(project dto.Project, err error) *MockClient_AddProject_Call {\n\t_c.Call.Return(project, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_AddProject_Call) RunAndReturn(run func(addProjectParam api.AddProjectParam) (dto.Project, error)) *MockClient_AddProject_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// AddTask provides a mock function for the type MockClient\nfunc (_mock *MockClient) AddTask(addTaskParam api.AddTaskParam) (dto.Task, error) {\n\tret := _mock.Called(addTaskParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for AddTask\")\n\t}\n\n\tvar r0 dto.Task\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.AddTaskParam) (dto.Task, error)); ok {\n\t\treturn returnFunc(addTaskParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.AddTaskParam) dto.Task); ok {\n\t\tr0 = returnFunc(addTaskParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Task)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.AddTaskParam) error); ok {\n\t\tr1 = returnFunc(addTaskParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_AddTask_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddTask'\ntype MockClient_AddTask_Call struct {\n\t*mock.Call\n}\n\n// AddTask is a helper method to define mock.On call\n//   - addTaskParam api.AddTaskParam\nfunc (_e *MockClient_Expecter) AddTask(addTaskParam interface{}) *MockClient_AddTask_Call {\n\treturn &MockClient_AddTask_Call{Call: _e.mock.On(\"AddTask\", addTaskParam)}\n}\n\nfunc (_c *MockClient_AddTask_Call) Run(run func(addTaskParam api.AddTaskParam)) *MockClient_AddTask_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.AddTaskParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.AddTaskParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_AddTask_Call) Return(task dto.Task, err error) *MockClient_AddTask_Call {\n\t_c.Call.Return(task, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_AddTask_Call) RunAndReturn(run func(addTaskParam api.AddTaskParam) (dto.Task, error)) *MockClient_AddTask_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// ChangeInvoiced provides a mock function for the type MockClient\nfunc (_mock *MockClient) ChangeInvoiced(changeInvoicedParam api.ChangeInvoicedParam) error {\n\tret := _mock.Called(changeInvoicedParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for ChangeInvoiced\")\n\t}\n\n\tvar r0 error\n\tif returnFunc, ok := ret.Get(0).(func(api.ChangeInvoicedParam) error); ok {\n\t\tr0 = returnFunc(changeInvoicedParam)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\treturn r0\n}\n\n// MockClient_ChangeInvoiced_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChangeInvoiced'\ntype MockClient_ChangeInvoiced_Call struct {\n\t*mock.Call\n}\n\n// ChangeInvoiced is a helper method to define mock.On call\n//   - changeInvoicedParam api.ChangeInvoicedParam\nfunc (_e *MockClient_Expecter) ChangeInvoiced(changeInvoicedParam interface{}) *MockClient_ChangeInvoiced_Call {\n\treturn &MockClient_ChangeInvoiced_Call{Call: _e.mock.On(\"ChangeInvoiced\", changeInvoicedParam)}\n}\n\nfunc (_c *MockClient_ChangeInvoiced_Call) Run(run func(changeInvoicedParam api.ChangeInvoicedParam)) *MockClient_ChangeInvoiced_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.ChangeInvoicedParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.ChangeInvoicedParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_ChangeInvoiced_Call) Return(err error) *MockClient_ChangeInvoiced_Call {\n\t_c.Call.Return(err)\n\treturn _c\n}\n\nfunc (_c *MockClient_ChangeInvoiced_Call) RunAndReturn(run func(changeInvoicedParam api.ChangeInvoicedParam) error) *MockClient_ChangeInvoiced_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// CreateTimeEntry provides a mock function for the type MockClient\nfunc (_mock *MockClient) CreateTimeEntry(createTimeEntryParam api.CreateTimeEntryParam) (dto.TimeEntryImpl, error) {\n\tret := _mock.Called(createTimeEntryParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for CreateTimeEntry\")\n\t}\n\n\tvar r0 dto.TimeEntryImpl\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.CreateTimeEntryParam) (dto.TimeEntryImpl, error)); ok {\n\t\treturn returnFunc(createTimeEntryParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.CreateTimeEntryParam) dto.TimeEntryImpl); ok {\n\t\tr0 = returnFunc(createTimeEntryParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.TimeEntryImpl)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.CreateTimeEntryParam) error); ok {\n\t\tr1 = returnFunc(createTimeEntryParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_CreateTimeEntry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateTimeEntry'\ntype MockClient_CreateTimeEntry_Call struct {\n\t*mock.Call\n}\n\n// CreateTimeEntry is a helper method to define mock.On call\n//   - createTimeEntryParam api.CreateTimeEntryParam\nfunc (_e *MockClient_Expecter) CreateTimeEntry(createTimeEntryParam interface{}) *MockClient_CreateTimeEntry_Call {\n\treturn &MockClient_CreateTimeEntry_Call{Call: _e.mock.On(\"CreateTimeEntry\", createTimeEntryParam)}\n}\n\nfunc (_c *MockClient_CreateTimeEntry_Call) Run(run func(createTimeEntryParam api.CreateTimeEntryParam)) *MockClient_CreateTimeEntry_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.CreateTimeEntryParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.CreateTimeEntryParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_CreateTimeEntry_Call) Return(timeEntryImpl dto.TimeEntryImpl, err error) *MockClient_CreateTimeEntry_Call {\n\t_c.Call.Return(timeEntryImpl, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_CreateTimeEntry_Call) RunAndReturn(run func(createTimeEntryParam api.CreateTimeEntryParam) (dto.TimeEntryImpl, error)) *MockClient_CreateTimeEntry_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// DeleteProject provides a mock function for the type MockClient\nfunc (_mock *MockClient) DeleteProject(deleteProjectParam api.DeleteProjectParam) (dto.Project, error) {\n\tret := _mock.Called(deleteProjectParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for DeleteProject\")\n\t}\n\n\tvar r0 dto.Project\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.DeleteProjectParam) (dto.Project, error)); ok {\n\t\treturn returnFunc(deleteProjectParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.DeleteProjectParam) dto.Project); ok {\n\t\tr0 = returnFunc(deleteProjectParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Project)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.DeleteProjectParam) error); ok {\n\t\tr1 = returnFunc(deleteProjectParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_DeleteProject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteProject'\ntype MockClient_DeleteProject_Call struct {\n\t*mock.Call\n}\n\n// DeleteProject is a helper method to define mock.On call\n//   - deleteProjectParam api.DeleteProjectParam\nfunc (_e *MockClient_Expecter) DeleteProject(deleteProjectParam interface{}) *MockClient_DeleteProject_Call {\n\treturn &MockClient_DeleteProject_Call{Call: _e.mock.On(\"DeleteProject\", deleteProjectParam)}\n}\n\nfunc (_c *MockClient_DeleteProject_Call) Run(run func(deleteProjectParam api.DeleteProjectParam)) *MockClient_DeleteProject_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.DeleteProjectParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.DeleteProjectParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_DeleteProject_Call) Return(project dto.Project, err error) *MockClient_DeleteProject_Call {\n\t_c.Call.Return(project, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_DeleteProject_Call) RunAndReturn(run func(deleteProjectParam api.DeleteProjectParam) (dto.Project, error)) *MockClient_DeleteProject_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// DeleteTask provides a mock function for the type MockClient\nfunc (_mock *MockClient) DeleteTask(deleteTaskParam api.DeleteTaskParam) (dto.Task, error) {\n\tret := _mock.Called(deleteTaskParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for DeleteTask\")\n\t}\n\n\tvar r0 dto.Task\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.DeleteTaskParam) (dto.Task, error)); ok {\n\t\treturn returnFunc(deleteTaskParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.DeleteTaskParam) dto.Task); ok {\n\t\tr0 = returnFunc(deleteTaskParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Task)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.DeleteTaskParam) error); ok {\n\t\tr1 = returnFunc(deleteTaskParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_DeleteTask_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteTask'\ntype MockClient_DeleteTask_Call struct {\n\t*mock.Call\n}\n\n// DeleteTask is a helper method to define mock.On call\n//   - deleteTaskParam api.DeleteTaskParam\nfunc (_e *MockClient_Expecter) DeleteTask(deleteTaskParam interface{}) *MockClient_DeleteTask_Call {\n\treturn &MockClient_DeleteTask_Call{Call: _e.mock.On(\"DeleteTask\", deleteTaskParam)}\n}\n\nfunc (_c *MockClient_DeleteTask_Call) Run(run func(deleteTaskParam api.DeleteTaskParam)) *MockClient_DeleteTask_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.DeleteTaskParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.DeleteTaskParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_DeleteTask_Call) Return(task dto.Task, err error) *MockClient_DeleteTask_Call {\n\t_c.Call.Return(task, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_DeleteTask_Call) RunAndReturn(run func(deleteTaskParam api.DeleteTaskParam) (dto.Task, error)) *MockClient_DeleteTask_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// DeleteTimeEntry provides a mock function for the type MockClient\nfunc (_mock *MockClient) DeleteTimeEntry(deleteTimeEntryParam api.DeleteTimeEntryParam) error {\n\tret := _mock.Called(deleteTimeEntryParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for DeleteTimeEntry\")\n\t}\n\n\tvar r0 error\n\tif returnFunc, ok := ret.Get(0).(func(api.DeleteTimeEntryParam) error); ok {\n\t\tr0 = returnFunc(deleteTimeEntryParam)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\treturn r0\n}\n\n// MockClient_DeleteTimeEntry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteTimeEntry'\ntype MockClient_DeleteTimeEntry_Call struct {\n\t*mock.Call\n}\n\n// DeleteTimeEntry is a helper method to define mock.On call\n//   - deleteTimeEntryParam api.DeleteTimeEntryParam\nfunc (_e *MockClient_Expecter) DeleteTimeEntry(deleteTimeEntryParam interface{}) *MockClient_DeleteTimeEntry_Call {\n\treturn &MockClient_DeleteTimeEntry_Call{Call: _e.mock.On(\"DeleteTimeEntry\", deleteTimeEntryParam)}\n}\n\nfunc (_c *MockClient_DeleteTimeEntry_Call) Run(run func(deleteTimeEntryParam api.DeleteTimeEntryParam)) *MockClient_DeleteTimeEntry_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.DeleteTimeEntryParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.DeleteTimeEntryParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_DeleteTimeEntry_Call) Return(err error) *MockClient_DeleteTimeEntry_Call {\n\t_c.Call.Return(err)\n\treturn _c\n}\n\nfunc (_c *MockClient_DeleteTimeEntry_Call) RunAndReturn(run func(deleteTimeEntryParam api.DeleteTimeEntryParam) error) *MockClient_DeleteTimeEntry_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetClients provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetClients(getClientsParam api.GetClientsParam) ([]dto.Client, error) {\n\tret := _mock.Called(getClientsParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetClients\")\n\t}\n\n\tvar r0 []dto.Client\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetClientsParam) ([]dto.Client, error)); ok {\n\t\treturn returnFunc(getClientsParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetClientsParam) []dto.Client); ok {\n\t\tr0 = returnFunc(getClientsParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]dto.Client)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetClientsParam) error); ok {\n\t\tr1 = returnFunc(getClientsParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetClients_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetClients'\ntype MockClient_GetClients_Call struct {\n\t*mock.Call\n}\n\n// GetClients is a helper method to define mock.On call\n//   - getClientsParam api.GetClientsParam\nfunc (_e *MockClient_Expecter) GetClients(getClientsParam interface{}) *MockClient_GetClients_Call {\n\treturn &MockClient_GetClients_Call{Call: _e.mock.On(\"GetClients\", getClientsParam)}\n}\n\nfunc (_c *MockClient_GetClients_Call) Run(run func(getClientsParam api.GetClientsParam)) *MockClient_GetClients_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetClientsParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetClientsParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetClients_Call) Return(clients []dto.Client, err error) *MockClient_GetClients_Call {\n\t_c.Call.Return(clients, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetClients_Call) RunAndReturn(run func(getClientsParam api.GetClientsParam) ([]dto.Client, error)) *MockClient_GetClients_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetHydratedTimeEntry provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetHydratedTimeEntry(getTimeEntryParam api.GetTimeEntryParam) (*dto.TimeEntry, error) {\n\tret := _mock.Called(getTimeEntryParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetHydratedTimeEntry\")\n\t}\n\n\tvar r0 *dto.TimeEntry\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryParam) (*dto.TimeEntry, error)); ok {\n\t\treturn returnFunc(getTimeEntryParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryParam) *dto.TimeEntry); ok {\n\t\tr0 = returnFunc(getTimeEntryParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*dto.TimeEntry)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetTimeEntryParam) error); ok {\n\t\tr1 = returnFunc(getTimeEntryParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetHydratedTimeEntry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetHydratedTimeEntry'\ntype MockClient_GetHydratedTimeEntry_Call struct {\n\t*mock.Call\n}\n\n// GetHydratedTimeEntry is a helper method to define mock.On call\n//   - getTimeEntryParam api.GetTimeEntryParam\nfunc (_e *MockClient_Expecter) GetHydratedTimeEntry(getTimeEntryParam interface{}) *MockClient_GetHydratedTimeEntry_Call {\n\treturn &MockClient_GetHydratedTimeEntry_Call{Call: _e.mock.On(\"GetHydratedTimeEntry\", getTimeEntryParam)}\n}\n\nfunc (_c *MockClient_GetHydratedTimeEntry_Call) Run(run func(getTimeEntryParam api.GetTimeEntryParam)) *MockClient_GetHydratedTimeEntry_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetTimeEntryParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetTimeEntryParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetHydratedTimeEntry_Call) Return(timeEntry *dto.TimeEntry, err error) *MockClient_GetHydratedTimeEntry_Call {\n\t_c.Call.Return(timeEntry, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetHydratedTimeEntry_Call) RunAndReturn(run func(getTimeEntryParam api.GetTimeEntryParam) (*dto.TimeEntry, error)) *MockClient_GetHydratedTimeEntry_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetHydratedTimeEntryInProgress provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetHydratedTimeEntryInProgress(getTimeEntryInProgressParam api.GetTimeEntryInProgressParam) (*dto.TimeEntry, error) {\n\tret := _mock.Called(getTimeEntryInProgressParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetHydratedTimeEntryInProgress\")\n\t}\n\n\tvar r0 *dto.TimeEntry\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryInProgressParam) (*dto.TimeEntry, error)); ok {\n\t\treturn returnFunc(getTimeEntryInProgressParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryInProgressParam) *dto.TimeEntry); ok {\n\t\tr0 = returnFunc(getTimeEntryInProgressParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*dto.TimeEntry)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetTimeEntryInProgressParam) error); ok {\n\t\tr1 = returnFunc(getTimeEntryInProgressParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetHydratedTimeEntryInProgress_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetHydratedTimeEntryInProgress'\ntype MockClient_GetHydratedTimeEntryInProgress_Call struct {\n\t*mock.Call\n}\n\n// GetHydratedTimeEntryInProgress is a helper method to define mock.On call\n//   - getTimeEntryInProgressParam api.GetTimeEntryInProgressParam\nfunc (_e *MockClient_Expecter) GetHydratedTimeEntryInProgress(getTimeEntryInProgressParam interface{}) *MockClient_GetHydratedTimeEntryInProgress_Call {\n\treturn &MockClient_GetHydratedTimeEntryInProgress_Call{Call: _e.mock.On(\"GetHydratedTimeEntryInProgress\", getTimeEntryInProgressParam)}\n}\n\nfunc (_c *MockClient_GetHydratedTimeEntryInProgress_Call) Run(run func(getTimeEntryInProgressParam api.GetTimeEntryInProgressParam)) *MockClient_GetHydratedTimeEntryInProgress_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetTimeEntryInProgressParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetTimeEntryInProgressParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetHydratedTimeEntryInProgress_Call) Return(timeEntry *dto.TimeEntry, err error) *MockClient_GetHydratedTimeEntryInProgress_Call {\n\t_c.Call.Return(timeEntry, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetHydratedTimeEntryInProgress_Call) RunAndReturn(run func(getTimeEntryInProgressParam api.GetTimeEntryInProgressParam) (*dto.TimeEntry, error)) *MockClient_GetHydratedTimeEntryInProgress_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetMe provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetMe() (dto.User, error) {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetMe\")\n\t}\n\n\tvar r0 dto.User\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func() (dto.User, error)); ok {\n\t\treturn returnFunc()\n\t}\n\tif returnFunc, ok := ret.Get(0).(func() dto.User); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tr0 = ret.Get(0).(dto.User)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func() error); ok {\n\t\tr1 = returnFunc()\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetMe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMe'\ntype MockClient_GetMe_Call struct {\n\t*mock.Call\n}\n\n// GetMe is a helper method to define mock.On call\nfunc (_e *MockClient_Expecter) GetMe() *MockClient_GetMe_Call {\n\treturn &MockClient_GetMe_Call{Call: _e.mock.On(\"GetMe\")}\n}\n\nfunc (_c *MockClient_GetMe_Call) Run(run func()) *MockClient_GetMe_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetMe_Call) Return(user dto.User, err error) *MockClient_GetMe_Call {\n\t_c.Call.Return(user, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetMe_Call) RunAndReturn(run func() (dto.User, error)) *MockClient_GetMe_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetProject provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetProject(getProjectParam api.GetProjectParam) (*dto.Project, error) {\n\tret := _mock.Called(getProjectParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetProject\")\n\t}\n\n\tvar r0 *dto.Project\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetProjectParam) (*dto.Project, error)); ok {\n\t\treturn returnFunc(getProjectParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetProjectParam) *dto.Project); ok {\n\t\tr0 = returnFunc(getProjectParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*dto.Project)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetProjectParam) error); ok {\n\t\tr1 = returnFunc(getProjectParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetProject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProject'\ntype MockClient_GetProject_Call struct {\n\t*mock.Call\n}\n\n// GetProject is a helper method to define mock.On call\n//   - getProjectParam api.GetProjectParam\nfunc (_e *MockClient_Expecter) GetProject(getProjectParam interface{}) *MockClient_GetProject_Call {\n\treturn &MockClient_GetProject_Call{Call: _e.mock.On(\"GetProject\", getProjectParam)}\n}\n\nfunc (_c *MockClient_GetProject_Call) Run(run func(getProjectParam api.GetProjectParam)) *MockClient_GetProject_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetProjectParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetProjectParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetProject_Call) Return(project *dto.Project, err error) *MockClient_GetProject_Call {\n\t_c.Call.Return(project, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetProject_Call) RunAndReturn(run func(getProjectParam api.GetProjectParam) (*dto.Project, error)) *MockClient_GetProject_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetProjects provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetProjects(getProjectsParam api.GetProjectsParam) ([]dto.Project, error) {\n\tret := _mock.Called(getProjectsParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetProjects\")\n\t}\n\n\tvar r0 []dto.Project\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetProjectsParam) ([]dto.Project, error)); ok {\n\t\treturn returnFunc(getProjectsParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetProjectsParam) []dto.Project); ok {\n\t\tr0 = returnFunc(getProjectsParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]dto.Project)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetProjectsParam) error); ok {\n\t\tr1 = returnFunc(getProjectsParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetProjects_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProjects'\ntype MockClient_GetProjects_Call struct {\n\t*mock.Call\n}\n\n// GetProjects is a helper method to define mock.On call\n//   - getProjectsParam api.GetProjectsParam\nfunc (_e *MockClient_Expecter) GetProjects(getProjectsParam interface{}) *MockClient_GetProjects_Call {\n\treturn &MockClient_GetProjects_Call{Call: _e.mock.On(\"GetProjects\", getProjectsParam)}\n}\n\nfunc (_c *MockClient_GetProjects_Call) Run(run func(getProjectsParam api.GetProjectsParam)) *MockClient_GetProjects_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetProjectsParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetProjectsParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetProjects_Call) Return(projects []dto.Project, err error) *MockClient_GetProjects_Call {\n\t_c.Call.Return(projects, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetProjects_Call) RunAndReturn(run func(getProjectsParam api.GetProjectsParam) ([]dto.Project, error)) *MockClient_GetProjects_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetTag provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetTag(getTagParam api.GetTagParam) (*dto.Tag, error) {\n\tret := _mock.Called(getTagParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetTag\")\n\t}\n\n\tvar r0 *dto.Tag\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTagParam) (*dto.Tag, error)); ok {\n\t\treturn returnFunc(getTagParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTagParam) *dto.Tag); ok {\n\t\tr0 = returnFunc(getTagParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*dto.Tag)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetTagParam) error); ok {\n\t\tr1 = returnFunc(getTagParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetTag_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTag'\ntype MockClient_GetTag_Call struct {\n\t*mock.Call\n}\n\n// GetTag is a helper method to define mock.On call\n//   - getTagParam api.GetTagParam\nfunc (_e *MockClient_Expecter) GetTag(getTagParam interface{}) *MockClient_GetTag_Call {\n\treturn &MockClient_GetTag_Call{Call: _e.mock.On(\"GetTag\", getTagParam)}\n}\n\nfunc (_c *MockClient_GetTag_Call) Run(run func(getTagParam api.GetTagParam)) *MockClient_GetTag_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetTagParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetTagParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetTag_Call) Return(tag *dto.Tag, err error) *MockClient_GetTag_Call {\n\t_c.Call.Return(tag, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetTag_Call) RunAndReturn(run func(getTagParam api.GetTagParam) (*dto.Tag, error)) *MockClient_GetTag_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetTags provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetTags(getTagsParam api.GetTagsParam) ([]dto.Tag, error) {\n\tret := _mock.Called(getTagsParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetTags\")\n\t}\n\n\tvar r0 []dto.Tag\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTagsParam) ([]dto.Tag, error)); ok {\n\t\treturn returnFunc(getTagsParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTagsParam) []dto.Tag); ok {\n\t\tr0 = returnFunc(getTagsParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]dto.Tag)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetTagsParam) error); ok {\n\t\tr1 = returnFunc(getTagsParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetTags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTags'\ntype MockClient_GetTags_Call struct {\n\t*mock.Call\n}\n\n// GetTags is a helper method to define mock.On call\n//   - getTagsParam api.GetTagsParam\nfunc (_e *MockClient_Expecter) GetTags(getTagsParam interface{}) *MockClient_GetTags_Call {\n\treturn &MockClient_GetTags_Call{Call: _e.mock.On(\"GetTags\", getTagsParam)}\n}\n\nfunc (_c *MockClient_GetTags_Call) Run(run func(getTagsParam api.GetTagsParam)) *MockClient_GetTags_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetTagsParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetTagsParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetTags_Call) Return(tags []dto.Tag, err error) *MockClient_GetTags_Call {\n\t_c.Call.Return(tags, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetTags_Call) RunAndReturn(run func(getTagsParam api.GetTagsParam) ([]dto.Tag, error)) *MockClient_GetTags_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetTask provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetTask(getTaskParam api.GetTaskParam) (dto.Task, error) {\n\tret := _mock.Called(getTaskParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetTask\")\n\t}\n\n\tvar r0 dto.Task\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTaskParam) (dto.Task, error)); ok {\n\t\treturn returnFunc(getTaskParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTaskParam) dto.Task); ok {\n\t\tr0 = returnFunc(getTaskParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Task)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetTaskParam) error); ok {\n\t\tr1 = returnFunc(getTaskParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetTask_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTask'\ntype MockClient_GetTask_Call struct {\n\t*mock.Call\n}\n\n// GetTask is a helper method to define mock.On call\n//   - getTaskParam api.GetTaskParam\nfunc (_e *MockClient_Expecter) GetTask(getTaskParam interface{}) *MockClient_GetTask_Call {\n\treturn &MockClient_GetTask_Call{Call: _e.mock.On(\"GetTask\", getTaskParam)}\n}\n\nfunc (_c *MockClient_GetTask_Call) Run(run func(getTaskParam api.GetTaskParam)) *MockClient_GetTask_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetTaskParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetTaskParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetTask_Call) Return(task dto.Task, err error) *MockClient_GetTask_Call {\n\t_c.Call.Return(task, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetTask_Call) RunAndReturn(run func(getTaskParam api.GetTaskParam) (dto.Task, error)) *MockClient_GetTask_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetTasks provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetTasks(getTasksParam api.GetTasksParam) ([]dto.Task, error) {\n\tret := _mock.Called(getTasksParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetTasks\")\n\t}\n\n\tvar r0 []dto.Task\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTasksParam) ([]dto.Task, error)); ok {\n\t\treturn returnFunc(getTasksParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTasksParam) []dto.Task); ok {\n\t\tr0 = returnFunc(getTasksParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]dto.Task)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetTasksParam) error); ok {\n\t\tr1 = returnFunc(getTasksParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetTasks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTasks'\ntype MockClient_GetTasks_Call struct {\n\t*mock.Call\n}\n\n// GetTasks is a helper method to define mock.On call\n//   - getTasksParam api.GetTasksParam\nfunc (_e *MockClient_Expecter) GetTasks(getTasksParam interface{}) *MockClient_GetTasks_Call {\n\treturn &MockClient_GetTasks_Call{Call: _e.mock.On(\"GetTasks\", getTasksParam)}\n}\n\nfunc (_c *MockClient_GetTasks_Call) Run(run func(getTasksParam api.GetTasksParam)) *MockClient_GetTasks_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetTasksParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetTasksParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetTasks_Call) Return(tasks []dto.Task, err error) *MockClient_GetTasks_Call {\n\t_c.Call.Return(tasks, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetTasks_Call) RunAndReturn(run func(getTasksParam api.GetTasksParam) ([]dto.Task, error)) *MockClient_GetTasks_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetTimeEntry provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetTimeEntry(getTimeEntryParam api.GetTimeEntryParam) (*dto.TimeEntryImpl, error) {\n\tret := _mock.Called(getTimeEntryParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetTimeEntry\")\n\t}\n\n\tvar r0 *dto.TimeEntryImpl\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryParam) (*dto.TimeEntryImpl, error)); ok {\n\t\treturn returnFunc(getTimeEntryParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryParam) *dto.TimeEntryImpl); ok {\n\t\tr0 = returnFunc(getTimeEntryParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*dto.TimeEntryImpl)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetTimeEntryParam) error); ok {\n\t\tr1 = returnFunc(getTimeEntryParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetTimeEntry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTimeEntry'\ntype MockClient_GetTimeEntry_Call struct {\n\t*mock.Call\n}\n\n// GetTimeEntry is a helper method to define mock.On call\n//   - getTimeEntryParam api.GetTimeEntryParam\nfunc (_e *MockClient_Expecter) GetTimeEntry(getTimeEntryParam interface{}) *MockClient_GetTimeEntry_Call {\n\treturn &MockClient_GetTimeEntry_Call{Call: _e.mock.On(\"GetTimeEntry\", getTimeEntryParam)}\n}\n\nfunc (_c *MockClient_GetTimeEntry_Call) Run(run func(getTimeEntryParam api.GetTimeEntryParam)) *MockClient_GetTimeEntry_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetTimeEntryParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetTimeEntryParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetTimeEntry_Call) Return(timeEntryImpl *dto.TimeEntryImpl, err error) *MockClient_GetTimeEntry_Call {\n\t_c.Call.Return(timeEntryImpl, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetTimeEntry_Call) RunAndReturn(run func(getTimeEntryParam api.GetTimeEntryParam) (*dto.TimeEntryImpl, error)) *MockClient_GetTimeEntry_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetTimeEntryInProgress provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetTimeEntryInProgress(getTimeEntryInProgressParam api.GetTimeEntryInProgressParam) (*dto.TimeEntryImpl, error) {\n\tret := _mock.Called(getTimeEntryInProgressParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetTimeEntryInProgress\")\n\t}\n\n\tvar r0 *dto.TimeEntryImpl\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryInProgressParam) (*dto.TimeEntryImpl, error)); ok {\n\t\treturn returnFunc(getTimeEntryInProgressParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetTimeEntryInProgressParam) *dto.TimeEntryImpl); ok {\n\t\tr0 = returnFunc(getTimeEntryInProgressParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*dto.TimeEntryImpl)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetTimeEntryInProgressParam) error); ok {\n\t\tr1 = returnFunc(getTimeEntryInProgressParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetTimeEntryInProgress_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTimeEntryInProgress'\ntype MockClient_GetTimeEntryInProgress_Call struct {\n\t*mock.Call\n}\n\n// GetTimeEntryInProgress is a helper method to define mock.On call\n//   - getTimeEntryInProgressParam api.GetTimeEntryInProgressParam\nfunc (_e *MockClient_Expecter) GetTimeEntryInProgress(getTimeEntryInProgressParam interface{}) *MockClient_GetTimeEntryInProgress_Call {\n\treturn &MockClient_GetTimeEntryInProgress_Call{Call: _e.mock.On(\"GetTimeEntryInProgress\", getTimeEntryInProgressParam)}\n}\n\nfunc (_c *MockClient_GetTimeEntryInProgress_Call) Run(run func(getTimeEntryInProgressParam api.GetTimeEntryInProgressParam)) *MockClient_GetTimeEntryInProgress_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetTimeEntryInProgressParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetTimeEntryInProgressParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetTimeEntryInProgress_Call) Return(timeEntryImpl *dto.TimeEntryImpl, err error) *MockClient_GetTimeEntryInProgress_Call {\n\t_c.Call.Return(timeEntryImpl, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetTimeEntryInProgress_Call) RunAndReturn(run func(getTimeEntryInProgressParam api.GetTimeEntryInProgressParam) (*dto.TimeEntryImpl, error)) *MockClient_GetTimeEntryInProgress_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetUser provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetUser(getUser api.GetUser) (dto.User, error) {\n\tret := _mock.Called(getUser)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetUser\")\n\t}\n\n\tvar r0 dto.User\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetUser) (dto.User, error)); ok {\n\t\treturn returnFunc(getUser)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetUser) dto.User); ok {\n\t\tr0 = returnFunc(getUser)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.User)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetUser) error); ok {\n\t\tr1 = returnFunc(getUser)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUser'\ntype MockClient_GetUser_Call struct {\n\t*mock.Call\n}\n\n// GetUser is a helper method to define mock.On call\n//   - getUser api.GetUser\nfunc (_e *MockClient_Expecter) GetUser(getUser interface{}) *MockClient_GetUser_Call {\n\treturn &MockClient_GetUser_Call{Call: _e.mock.On(\"GetUser\", getUser)}\n}\n\nfunc (_c *MockClient_GetUser_Call) Run(run func(getUser api.GetUser)) *MockClient_GetUser_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetUser\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetUser)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetUser_Call) Return(user dto.User, err error) *MockClient_GetUser_Call {\n\t_c.Call.Return(user, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetUser_Call) RunAndReturn(run func(getUser api.GetUser) (dto.User, error)) *MockClient_GetUser_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetUserTimeEntries provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetUserTimeEntries(getUserTimeEntriesParam api.GetUserTimeEntriesParam) ([]dto.TimeEntryImpl, error) {\n\tret := _mock.Called(getUserTimeEntriesParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetUserTimeEntries\")\n\t}\n\n\tvar r0 []dto.TimeEntryImpl\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetUserTimeEntriesParam) ([]dto.TimeEntryImpl, error)); ok {\n\t\treturn returnFunc(getUserTimeEntriesParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetUserTimeEntriesParam) []dto.TimeEntryImpl); ok {\n\t\tr0 = returnFunc(getUserTimeEntriesParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]dto.TimeEntryImpl)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetUserTimeEntriesParam) error); ok {\n\t\tr1 = returnFunc(getUserTimeEntriesParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetUserTimeEntries_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserTimeEntries'\ntype MockClient_GetUserTimeEntries_Call struct {\n\t*mock.Call\n}\n\n// GetUserTimeEntries is a helper method to define mock.On call\n//   - getUserTimeEntriesParam api.GetUserTimeEntriesParam\nfunc (_e *MockClient_Expecter) GetUserTimeEntries(getUserTimeEntriesParam interface{}) *MockClient_GetUserTimeEntries_Call {\n\treturn &MockClient_GetUserTimeEntries_Call{Call: _e.mock.On(\"GetUserTimeEntries\", getUserTimeEntriesParam)}\n}\n\nfunc (_c *MockClient_GetUserTimeEntries_Call) Run(run func(getUserTimeEntriesParam api.GetUserTimeEntriesParam)) *MockClient_GetUserTimeEntries_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetUserTimeEntriesParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetUserTimeEntriesParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetUserTimeEntries_Call) Return(timeEntryImpls []dto.TimeEntryImpl, err error) *MockClient_GetUserTimeEntries_Call {\n\t_c.Call.Return(timeEntryImpls, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetUserTimeEntries_Call) RunAndReturn(run func(getUserTimeEntriesParam api.GetUserTimeEntriesParam) ([]dto.TimeEntryImpl, error)) *MockClient_GetUserTimeEntries_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetUsersHydratedTimeEntries provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetUsersHydratedTimeEntries(getUserTimeEntriesParam api.GetUserTimeEntriesParam) ([]dto.TimeEntry, error) {\n\tret := _mock.Called(getUserTimeEntriesParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetUsersHydratedTimeEntries\")\n\t}\n\n\tvar r0 []dto.TimeEntry\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetUserTimeEntriesParam) ([]dto.TimeEntry, error)); ok {\n\t\treturn returnFunc(getUserTimeEntriesParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetUserTimeEntriesParam) []dto.TimeEntry); ok {\n\t\tr0 = returnFunc(getUserTimeEntriesParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]dto.TimeEntry)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetUserTimeEntriesParam) error); ok {\n\t\tr1 = returnFunc(getUserTimeEntriesParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetUsersHydratedTimeEntries_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUsersHydratedTimeEntries'\ntype MockClient_GetUsersHydratedTimeEntries_Call struct {\n\t*mock.Call\n}\n\n// GetUsersHydratedTimeEntries is a helper method to define mock.On call\n//   - getUserTimeEntriesParam api.GetUserTimeEntriesParam\nfunc (_e *MockClient_Expecter) GetUsersHydratedTimeEntries(getUserTimeEntriesParam interface{}) *MockClient_GetUsersHydratedTimeEntries_Call {\n\treturn &MockClient_GetUsersHydratedTimeEntries_Call{Call: _e.mock.On(\"GetUsersHydratedTimeEntries\", getUserTimeEntriesParam)}\n}\n\nfunc (_c *MockClient_GetUsersHydratedTimeEntries_Call) Run(run func(getUserTimeEntriesParam api.GetUserTimeEntriesParam)) *MockClient_GetUsersHydratedTimeEntries_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetUserTimeEntriesParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetUserTimeEntriesParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetUsersHydratedTimeEntries_Call) Return(timeEntrys []dto.TimeEntry, err error) *MockClient_GetUsersHydratedTimeEntries_Call {\n\t_c.Call.Return(timeEntrys, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetUsersHydratedTimeEntries_Call) RunAndReturn(run func(getUserTimeEntriesParam api.GetUserTimeEntriesParam) ([]dto.TimeEntry, error)) *MockClient_GetUsersHydratedTimeEntries_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetWorkspace provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetWorkspace(getWorkspace api.GetWorkspace) (dto.Workspace, error) {\n\tret := _mock.Called(getWorkspace)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetWorkspace\")\n\t}\n\n\tvar r0 dto.Workspace\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetWorkspace) (dto.Workspace, error)); ok {\n\t\treturn returnFunc(getWorkspace)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetWorkspace) dto.Workspace); ok {\n\t\tr0 = returnFunc(getWorkspace)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Workspace)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetWorkspace) error); ok {\n\t\tr1 = returnFunc(getWorkspace)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetWorkspace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkspace'\ntype MockClient_GetWorkspace_Call struct {\n\t*mock.Call\n}\n\n// GetWorkspace is a helper method to define mock.On call\n//   - getWorkspace api.GetWorkspace\nfunc (_e *MockClient_Expecter) GetWorkspace(getWorkspace interface{}) *MockClient_GetWorkspace_Call {\n\treturn &MockClient_GetWorkspace_Call{Call: _e.mock.On(\"GetWorkspace\", getWorkspace)}\n}\n\nfunc (_c *MockClient_GetWorkspace_Call) Run(run func(getWorkspace api.GetWorkspace)) *MockClient_GetWorkspace_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetWorkspace\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetWorkspace)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetWorkspace_Call) Return(workspace dto.Workspace, err error) *MockClient_GetWorkspace_Call {\n\t_c.Call.Return(workspace, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetWorkspace_Call) RunAndReturn(run func(getWorkspace api.GetWorkspace) (dto.Workspace, error)) *MockClient_GetWorkspace_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetWorkspaces provides a mock function for the type MockClient\nfunc (_mock *MockClient) GetWorkspaces(getWorkspaces api.GetWorkspaces) ([]dto.Workspace, error) {\n\tret := _mock.Called(getWorkspaces)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetWorkspaces\")\n\t}\n\n\tvar r0 []dto.Workspace\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.GetWorkspaces) ([]dto.Workspace, error)); ok {\n\t\treturn returnFunc(getWorkspaces)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.GetWorkspaces) []dto.Workspace); ok {\n\t\tr0 = returnFunc(getWorkspaces)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]dto.Workspace)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.GetWorkspaces) error); ok {\n\t\tr1 = returnFunc(getWorkspaces)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_GetWorkspaces_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkspaces'\ntype MockClient_GetWorkspaces_Call struct {\n\t*mock.Call\n}\n\n// GetWorkspaces is a helper method to define mock.On call\n//   - getWorkspaces api.GetWorkspaces\nfunc (_e *MockClient_Expecter) GetWorkspaces(getWorkspaces interface{}) *MockClient_GetWorkspaces_Call {\n\treturn &MockClient_GetWorkspaces_Call{Call: _e.mock.On(\"GetWorkspaces\", getWorkspaces)}\n}\n\nfunc (_c *MockClient_GetWorkspaces_Call) Run(run func(getWorkspaces api.GetWorkspaces)) *MockClient_GetWorkspaces_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.GetWorkspaces\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.GetWorkspaces)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_GetWorkspaces_Call) Return(workspaces []dto.Workspace, err error) *MockClient_GetWorkspaces_Call {\n\t_c.Call.Return(workspaces, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_GetWorkspaces_Call) RunAndReturn(run func(getWorkspaces api.GetWorkspaces) ([]dto.Workspace, error)) *MockClient_GetWorkspaces_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Log provides a mock function for the type MockClient\nfunc (_mock *MockClient) Log(logParam api.LogParam) ([]dto.TimeEntry, error) {\n\tret := _mock.Called(logParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Log\")\n\t}\n\n\tvar r0 []dto.TimeEntry\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.LogParam) ([]dto.TimeEntry, error)); ok {\n\t\treturn returnFunc(logParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.LogParam) []dto.TimeEntry); ok {\n\t\tr0 = returnFunc(logParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]dto.TimeEntry)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.LogParam) error); ok {\n\t\tr1 = returnFunc(logParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_Log_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Log'\ntype MockClient_Log_Call struct {\n\t*mock.Call\n}\n\n// Log is a helper method to define mock.On call\n//   - logParam api.LogParam\nfunc (_e *MockClient_Expecter) Log(logParam interface{}) *MockClient_Log_Call {\n\treturn &MockClient_Log_Call{Call: _e.mock.On(\"Log\", logParam)}\n}\n\nfunc (_c *MockClient_Log_Call) Run(run func(logParam api.LogParam)) *MockClient_Log_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.LogParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.LogParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_Log_Call) Return(timeEntrys []dto.TimeEntry, err error) *MockClient_Log_Call {\n\t_c.Call.Return(timeEntrys, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_Log_Call) RunAndReturn(run func(logParam api.LogParam) ([]dto.TimeEntry, error)) *MockClient_Log_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// LogRange provides a mock function for the type MockClient\nfunc (_mock *MockClient) LogRange(logRangeParam api.LogRangeParam) ([]dto.TimeEntry, error) {\n\tret := _mock.Called(logRangeParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for LogRange\")\n\t}\n\n\tvar r0 []dto.TimeEntry\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.LogRangeParam) ([]dto.TimeEntry, error)); ok {\n\t\treturn returnFunc(logRangeParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.LogRangeParam) []dto.TimeEntry); ok {\n\t\tr0 = returnFunc(logRangeParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]dto.TimeEntry)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.LogRangeParam) error); ok {\n\t\tr1 = returnFunc(logRangeParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_LogRange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogRange'\ntype MockClient_LogRange_Call struct {\n\t*mock.Call\n}\n\n// LogRange is a helper method to define mock.On call\n//   - logRangeParam api.LogRangeParam\nfunc (_e *MockClient_Expecter) LogRange(logRangeParam interface{}) *MockClient_LogRange_Call {\n\treturn &MockClient_LogRange_Call{Call: _e.mock.On(\"LogRange\", logRangeParam)}\n}\n\nfunc (_c *MockClient_LogRange_Call) Run(run func(logRangeParam api.LogRangeParam)) *MockClient_LogRange_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.LogRangeParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.LogRangeParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_LogRange_Call) Return(timeEntrys []dto.TimeEntry, err error) *MockClient_LogRange_Call {\n\t_c.Call.Return(timeEntrys, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_LogRange_Call) RunAndReturn(run func(logRangeParam api.LogRangeParam) ([]dto.TimeEntry, error)) *MockClient_LogRange_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Out provides a mock function for the type MockClient\nfunc (_mock *MockClient) Out(outParam api.OutParam) error {\n\tret := _mock.Called(outParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Out\")\n\t}\n\n\tvar r0 error\n\tif returnFunc, ok := ret.Get(0).(func(api.OutParam) error); ok {\n\t\tr0 = returnFunc(outParam)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\treturn r0\n}\n\n// MockClient_Out_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Out'\ntype MockClient_Out_Call struct {\n\t*mock.Call\n}\n\n// Out is a helper method to define mock.On call\n//   - outParam api.OutParam\nfunc (_e *MockClient_Expecter) Out(outParam interface{}) *MockClient_Out_Call {\n\treturn &MockClient_Out_Call{Call: _e.mock.On(\"Out\", outParam)}\n}\n\nfunc (_c *MockClient_Out_Call) Run(run func(outParam api.OutParam)) *MockClient_Out_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.OutParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.OutParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_Out_Call) Return(err error) *MockClient_Out_Call {\n\t_c.Call.Return(err)\n\treturn _c\n}\n\nfunc (_c *MockClient_Out_Call) RunAndReturn(run func(outParam api.OutParam) error) *MockClient_Out_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// SetDebugLogger provides a mock function for the type MockClient\nfunc (_mock *MockClient) SetDebugLogger(logger api.Logger) api.Client {\n\tret := _mock.Called(logger)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for SetDebugLogger\")\n\t}\n\n\tvar r0 api.Client\n\tif returnFunc, ok := ret.Get(0).(func(api.Logger) api.Client); ok {\n\t\tr0 = returnFunc(logger)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(api.Client)\n\t\t}\n\t}\n\treturn r0\n}\n\n// MockClient_SetDebugLogger_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDebugLogger'\ntype MockClient_SetDebugLogger_Call struct {\n\t*mock.Call\n}\n\n// SetDebugLogger is a helper method to define mock.On call\n//   - logger api.Logger\nfunc (_e *MockClient_Expecter) SetDebugLogger(logger interface{}) *MockClient_SetDebugLogger_Call {\n\treturn &MockClient_SetDebugLogger_Call{Call: _e.mock.On(\"SetDebugLogger\", logger)}\n}\n\nfunc (_c *MockClient_SetDebugLogger_Call) Run(run func(logger api.Logger)) *MockClient_SetDebugLogger_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.Logger\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.Logger)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_SetDebugLogger_Call) Return(client api.Client) *MockClient_SetDebugLogger_Call {\n\t_c.Call.Return(client)\n\treturn _c\n}\n\nfunc (_c *MockClient_SetDebugLogger_Call) RunAndReturn(run func(logger api.Logger) api.Client) *MockClient_SetDebugLogger_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// SetInfoLogger provides a mock function for the type MockClient\nfunc (_mock *MockClient) SetInfoLogger(logger api.Logger) api.Client {\n\tret := _mock.Called(logger)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for SetInfoLogger\")\n\t}\n\n\tvar r0 api.Client\n\tif returnFunc, ok := ret.Get(0).(func(api.Logger) api.Client); ok {\n\t\tr0 = returnFunc(logger)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(api.Client)\n\t\t}\n\t}\n\treturn r0\n}\n\n// MockClient_SetInfoLogger_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetInfoLogger'\ntype MockClient_SetInfoLogger_Call struct {\n\t*mock.Call\n}\n\n// SetInfoLogger is a helper method to define mock.On call\n//   - logger api.Logger\nfunc (_e *MockClient_Expecter) SetInfoLogger(logger interface{}) *MockClient_SetInfoLogger_Call {\n\treturn &MockClient_SetInfoLogger_Call{Call: _e.mock.On(\"SetInfoLogger\", logger)}\n}\n\nfunc (_c *MockClient_SetInfoLogger_Call) Run(run func(logger api.Logger)) *MockClient_SetInfoLogger_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.Logger\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.Logger)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_SetInfoLogger_Call) Return(client api.Client) *MockClient_SetInfoLogger_Call {\n\t_c.Call.Return(client)\n\treturn _c\n}\n\nfunc (_c *MockClient_SetInfoLogger_Call) RunAndReturn(run func(logger api.Logger) api.Client) *MockClient_SetInfoLogger_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UpdateProject provides a mock function for the type MockClient\nfunc (_mock *MockClient) UpdateProject(updateProjectParam api.UpdateProjectParam) (dto.Project, error) {\n\tret := _mock.Called(updateProjectParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for UpdateProject\")\n\t}\n\n\tvar r0 dto.Project\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateProjectParam) (dto.Project, error)); ok {\n\t\treturn returnFunc(updateProjectParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateProjectParam) dto.Project); ok {\n\t\tr0 = returnFunc(updateProjectParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Project)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.UpdateProjectParam) error); ok {\n\t\tr1 = returnFunc(updateProjectParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_UpdateProject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateProject'\ntype MockClient_UpdateProject_Call struct {\n\t*mock.Call\n}\n\n// UpdateProject is a helper method to define mock.On call\n//   - updateProjectParam api.UpdateProjectParam\nfunc (_e *MockClient_Expecter) UpdateProject(updateProjectParam interface{}) *MockClient_UpdateProject_Call {\n\treturn &MockClient_UpdateProject_Call{Call: _e.mock.On(\"UpdateProject\", updateProjectParam)}\n}\n\nfunc (_c *MockClient_UpdateProject_Call) Run(run func(updateProjectParam api.UpdateProjectParam)) *MockClient_UpdateProject_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.UpdateProjectParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.UpdateProjectParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateProject_Call) Return(project dto.Project, err error) *MockClient_UpdateProject_Call {\n\t_c.Call.Return(project, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateProject_Call) RunAndReturn(run func(updateProjectParam api.UpdateProjectParam) (dto.Project, error)) *MockClient_UpdateProject_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UpdateProjectEstimate provides a mock function for the type MockClient\nfunc (_mock *MockClient) UpdateProjectEstimate(updateProjectEstimateParam api.UpdateProjectEstimateParam) (dto.Project, error) {\n\tret := _mock.Called(updateProjectEstimateParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for UpdateProjectEstimate\")\n\t}\n\n\tvar r0 dto.Project\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateProjectEstimateParam) (dto.Project, error)); ok {\n\t\treturn returnFunc(updateProjectEstimateParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateProjectEstimateParam) dto.Project); ok {\n\t\tr0 = returnFunc(updateProjectEstimateParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Project)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.UpdateProjectEstimateParam) error); ok {\n\t\tr1 = returnFunc(updateProjectEstimateParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_UpdateProjectEstimate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateProjectEstimate'\ntype MockClient_UpdateProjectEstimate_Call struct {\n\t*mock.Call\n}\n\n// UpdateProjectEstimate is a helper method to define mock.On call\n//   - updateProjectEstimateParam api.UpdateProjectEstimateParam\nfunc (_e *MockClient_Expecter) UpdateProjectEstimate(updateProjectEstimateParam interface{}) *MockClient_UpdateProjectEstimate_Call {\n\treturn &MockClient_UpdateProjectEstimate_Call{Call: _e.mock.On(\"UpdateProjectEstimate\", updateProjectEstimateParam)}\n}\n\nfunc (_c *MockClient_UpdateProjectEstimate_Call) Run(run func(updateProjectEstimateParam api.UpdateProjectEstimateParam)) *MockClient_UpdateProjectEstimate_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.UpdateProjectEstimateParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.UpdateProjectEstimateParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateProjectEstimate_Call) Return(project dto.Project, err error) *MockClient_UpdateProjectEstimate_Call {\n\t_c.Call.Return(project, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateProjectEstimate_Call) RunAndReturn(run func(updateProjectEstimateParam api.UpdateProjectEstimateParam) (dto.Project, error)) *MockClient_UpdateProjectEstimate_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UpdateProjectMemberships provides a mock function for the type MockClient\nfunc (_mock *MockClient) UpdateProjectMemberships(updateProjectMembershipsParam api.UpdateProjectMembershipsParam) (dto.Project, error) {\n\tret := _mock.Called(updateProjectMembershipsParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for UpdateProjectMemberships\")\n\t}\n\n\tvar r0 dto.Project\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateProjectMembershipsParam) (dto.Project, error)); ok {\n\t\treturn returnFunc(updateProjectMembershipsParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateProjectMembershipsParam) dto.Project); ok {\n\t\tr0 = returnFunc(updateProjectMembershipsParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Project)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.UpdateProjectMembershipsParam) error); ok {\n\t\tr1 = returnFunc(updateProjectMembershipsParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_UpdateProjectMemberships_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateProjectMemberships'\ntype MockClient_UpdateProjectMemberships_Call struct {\n\t*mock.Call\n}\n\n// UpdateProjectMemberships is a helper method to define mock.On call\n//   - updateProjectMembershipsParam api.UpdateProjectMembershipsParam\nfunc (_e *MockClient_Expecter) UpdateProjectMemberships(updateProjectMembershipsParam interface{}) *MockClient_UpdateProjectMemberships_Call {\n\treturn &MockClient_UpdateProjectMemberships_Call{Call: _e.mock.On(\"UpdateProjectMemberships\", updateProjectMembershipsParam)}\n}\n\nfunc (_c *MockClient_UpdateProjectMemberships_Call) Run(run func(updateProjectMembershipsParam api.UpdateProjectMembershipsParam)) *MockClient_UpdateProjectMemberships_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.UpdateProjectMembershipsParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.UpdateProjectMembershipsParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateProjectMemberships_Call) Return(project dto.Project, err error) *MockClient_UpdateProjectMemberships_Call {\n\t_c.Call.Return(project, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateProjectMemberships_Call) RunAndReturn(run func(updateProjectMembershipsParam api.UpdateProjectMembershipsParam) (dto.Project, error)) *MockClient_UpdateProjectMemberships_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UpdateProjectTemplate provides a mock function for the type MockClient\nfunc (_mock *MockClient) UpdateProjectTemplate(updateProjectTemplateParam api.UpdateProjectTemplateParam) (dto.Project, error) {\n\tret := _mock.Called(updateProjectTemplateParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for UpdateProjectTemplate\")\n\t}\n\n\tvar r0 dto.Project\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateProjectTemplateParam) (dto.Project, error)); ok {\n\t\treturn returnFunc(updateProjectTemplateParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateProjectTemplateParam) dto.Project); ok {\n\t\tr0 = returnFunc(updateProjectTemplateParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Project)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.UpdateProjectTemplateParam) error); ok {\n\t\tr1 = returnFunc(updateProjectTemplateParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_UpdateProjectTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateProjectTemplate'\ntype MockClient_UpdateProjectTemplate_Call struct {\n\t*mock.Call\n}\n\n// UpdateProjectTemplate is a helper method to define mock.On call\n//   - updateProjectTemplateParam api.UpdateProjectTemplateParam\nfunc (_e *MockClient_Expecter) UpdateProjectTemplate(updateProjectTemplateParam interface{}) *MockClient_UpdateProjectTemplate_Call {\n\treturn &MockClient_UpdateProjectTemplate_Call{Call: _e.mock.On(\"UpdateProjectTemplate\", updateProjectTemplateParam)}\n}\n\nfunc (_c *MockClient_UpdateProjectTemplate_Call) Run(run func(updateProjectTemplateParam api.UpdateProjectTemplateParam)) *MockClient_UpdateProjectTemplate_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.UpdateProjectTemplateParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.UpdateProjectTemplateParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateProjectTemplate_Call) Return(project dto.Project, err error) *MockClient_UpdateProjectTemplate_Call {\n\t_c.Call.Return(project, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateProjectTemplate_Call) RunAndReturn(run func(updateProjectTemplateParam api.UpdateProjectTemplateParam) (dto.Project, error)) *MockClient_UpdateProjectTemplate_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UpdateProjectUserBillableRate provides a mock function for the type MockClient\nfunc (_mock *MockClient) UpdateProjectUserBillableRate(updateProjectUserRateParam api.UpdateProjectUserRateParam) (dto.Project, error) {\n\tret := _mock.Called(updateProjectUserRateParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for UpdateProjectUserBillableRate\")\n\t}\n\n\tvar r0 dto.Project\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateProjectUserRateParam) (dto.Project, error)); ok {\n\t\treturn returnFunc(updateProjectUserRateParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateProjectUserRateParam) dto.Project); ok {\n\t\tr0 = returnFunc(updateProjectUserRateParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Project)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.UpdateProjectUserRateParam) error); ok {\n\t\tr1 = returnFunc(updateProjectUserRateParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_UpdateProjectUserBillableRate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateProjectUserBillableRate'\ntype MockClient_UpdateProjectUserBillableRate_Call struct {\n\t*mock.Call\n}\n\n// UpdateProjectUserBillableRate is a helper method to define mock.On call\n//   - updateProjectUserRateParam api.UpdateProjectUserRateParam\nfunc (_e *MockClient_Expecter) UpdateProjectUserBillableRate(updateProjectUserRateParam interface{}) *MockClient_UpdateProjectUserBillableRate_Call {\n\treturn &MockClient_UpdateProjectUserBillableRate_Call{Call: _e.mock.On(\"UpdateProjectUserBillableRate\", updateProjectUserRateParam)}\n}\n\nfunc (_c *MockClient_UpdateProjectUserBillableRate_Call) Run(run func(updateProjectUserRateParam api.UpdateProjectUserRateParam)) *MockClient_UpdateProjectUserBillableRate_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.UpdateProjectUserRateParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.UpdateProjectUserRateParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateProjectUserBillableRate_Call) Return(project dto.Project, err error) *MockClient_UpdateProjectUserBillableRate_Call {\n\t_c.Call.Return(project, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateProjectUserBillableRate_Call) RunAndReturn(run func(updateProjectUserRateParam api.UpdateProjectUserRateParam) (dto.Project, error)) *MockClient_UpdateProjectUserBillableRate_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UpdateProjectUserCostRate provides a mock function for the type MockClient\nfunc (_mock *MockClient) UpdateProjectUserCostRate(updateProjectUserRateParam api.UpdateProjectUserRateParam) (dto.Project, error) {\n\tret := _mock.Called(updateProjectUserRateParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for UpdateProjectUserCostRate\")\n\t}\n\n\tvar r0 dto.Project\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateProjectUserRateParam) (dto.Project, error)); ok {\n\t\treturn returnFunc(updateProjectUserRateParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateProjectUserRateParam) dto.Project); ok {\n\t\tr0 = returnFunc(updateProjectUserRateParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Project)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.UpdateProjectUserRateParam) error); ok {\n\t\tr1 = returnFunc(updateProjectUserRateParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_UpdateProjectUserCostRate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateProjectUserCostRate'\ntype MockClient_UpdateProjectUserCostRate_Call struct {\n\t*mock.Call\n}\n\n// UpdateProjectUserCostRate is a helper method to define mock.On call\n//   - updateProjectUserRateParam api.UpdateProjectUserRateParam\nfunc (_e *MockClient_Expecter) UpdateProjectUserCostRate(updateProjectUserRateParam interface{}) *MockClient_UpdateProjectUserCostRate_Call {\n\treturn &MockClient_UpdateProjectUserCostRate_Call{Call: _e.mock.On(\"UpdateProjectUserCostRate\", updateProjectUserRateParam)}\n}\n\nfunc (_c *MockClient_UpdateProjectUserCostRate_Call) Run(run func(updateProjectUserRateParam api.UpdateProjectUserRateParam)) *MockClient_UpdateProjectUserCostRate_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.UpdateProjectUserRateParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.UpdateProjectUserRateParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateProjectUserCostRate_Call) Return(project dto.Project, err error) *MockClient_UpdateProjectUserCostRate_Call {\n\t_c.Call.Return(project, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateProjectUserCostRate_Call) RunAndReturn(run func(updateProjectUserRateParam api.UpdateProjectUserRateParam) (dto.Project, error)) *MockClient_UpdateProjectUserCostRate_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UpdateTask provides a mock function for the type MockClient\nfunc (_mock *MockClient) UpdateTask(updateTaskParam api.UpdateTaskParam) (dto.Task, error) {\n\tret := _mock.Called(updateTaskParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for UpdateTask\")\n\t}\n\n\tvar r0 dto.Task\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateTaskParam) (dto.Task, error)); ok {\n\t\treturn returnFunc(updateTaskParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateTaskParam) dto.Task); ok {\n\t\tr0 = returnFunc(updateTaskParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Task)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.UpdateTaskParam) error); ok {\n\t\tr1 = returnFunc(updateTaskParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_UpdateTask_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateTask'\ntype MockClient_UpdateTask_Call struct {\n\t*mock.Call\n}\n\n// UpdateTask is a helper method to define mock.On call\n//   - updateTaskParam api.UpdateTaskParam\nfunc (_e *MockClient_Expecter) UpdateTask(updateTaskParam interface{}) *MockClient_UpdateTask_Call {\n\treturn &MockClient_UpdateTask_Call{Call: _e.mock.On(\"UpdateTask\", updateTaskParam)}\n}\n\nfunc (_c *MockClient_UpdateTask_Call) Run(run func(updateTaskParam api.UpdateTaskParam)) *MockClient_UpdateTask_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.UpdateTaskParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.UpdateTaskParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateTask_Call) Return(task dto.Task, err error) *MockClient_UpdateTask_Call {\n\t_c.Call.Return(task, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateTask_Call) RunAndReturn(run func(updateTaskParam api.UpdateTaskParam) (dto.Task, error)) *MockClient_UpdateTask_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UpdateTimeEntry provides a mock function for the type MockClient\nfunc (_mock *MockClient) UpdateTimeEntry(updateTimeEntryParam api.UpdateTimeEntryParam) (dto.TimeEntryImpl, error) {\n\tret := _mock.Called(updateTimeEntryParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for UpdateTimeEntry\")\n\t}\n\n\tvar r0 dto.TimeEntryImpl\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateTimeEntryParam) (dto.TimeEntryImpl, error)); ok {\n\t\treturn returnFunc(updateTimeEntryParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.UpdateTimeEntryParam) dto.TimeEntryImpl); ok {\n\t\tr0 = returnFunc(updateTimeEntryParam)\n\t} else {\n\t\tr0 = ret.Get(0).(dto.TimeEntryImpl)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.UpdateTimeEntryParam) error); ok {\n\t\tr1 = returnFunc(updateTimeEntryParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_UpdateTimeEntry_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateTimeEntry'\ntype MockClient_UpdateTimeEntry_Call struct {\n\t*mock.Call\n}\n\n// UpdateTimeEntry is a helper method to define mock.On call\n//   - updateTimeEntryParam api.UpdateTimeEntryParam\nfunc (_e *MockClient_Expecter) UpdateTimeEntry(updateTimeEntryParam interface{}) *MockClient_UpdateTimeEntry_Call {\n\treturn &MockClient_UpdateTimeEntry_Call{Call: _e.mock.On(\"UpdateTimeEntry\", updateTimeEntryParam)}\n}\n\nfunc (_c *MockClient_UpdateTimeEntry_Call) Run(run func(updateTimeEntryParam api.UpdateTimeEntryParam)) *MockClient_UpdateTimeEntry_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.UpdateTimeEntryParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.UpdateTimeEntryParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateTimeEntry_Call) Return(timeEntryImpl dto.TimeEntryImpl, err error) *MockClient_UpdateTimeEntry_Call {\n\t_c.Call.Return(timeEntryImpl, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_UpdateTimeEntry_Call) RunAndReturn(run func(updateTimeEntryParam api.UpdateTimeEntryParam) (dto.TimeEntryImpl, error)) *MockClient_UpdateTimeEntry_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// WorkspaceUsers provides a mock function for the type MockClient\nfunc (_mock *MockClient) WorkspaceUsers(workspaceUsersParam api.WorkspaceUsersParam) ([]dto.User, error) {\n\tret := _mock.Called(workspaceUsersParam)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for WorkspaceUsers\")\n\t}\n\n\tvar r0 []dto.User\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(api.WorkspaceUsersParam) ([]dto.User, error)); ok {\n\t\treturn returnFunc(workspaceUsersParam)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(api.WorkspaceUsersParam) []dto.User); ok {\n\t\tr0 = returnFunc(workspaceUsersParam)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]dto.User)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(api.WorkspaceUsersParam) error); ok {\n\t\tr1 = returnFunc(workspaceUsersParam)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockClient_WorkspaceUsers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WorkspaceUsers'\ntype MockClient_WorkspaceUsers_Call struct {\n\t*mock.Call\n}\n\n// WorkspaceUsers is a helper method to define mock.On call\n//   - workspaceUsersParam api.WorkspaceUsersParam\nfunc (_e *MockClient_Expecter) WorkspaceUsers(workspaceUsersParam interface{}) *MockClient_WorkspaceUsers_Call {\n\treturn &MockClient_WorkspaceUsers_Call{Call: _e.mock.On(\"WorkspaceUsers\", workspaceUsersParam)}\n}\n\nfunc (_c *MockClient_WorkspaceUsers_Call) Run(run func(workspaceUsersParam api.WorkspaceUsersParam)) *MockClient_WorkspaceUsers_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 api.WorkspaceUsersParam\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(api.WorkspaceUsersParam)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockClient_WorkspaceUsers_Call) Return(users []dto.User, err error) *MockClient_WorkspaceUsers_Call {\n\t_c.Call.Return(users, err)\n\treturn _c\n}\n\nfunc (_c *MockClient_WorkspaceUsers_Call) RunAndReturn(run func(workspaceUsersParam api.WorkspaceUsersParam) ([]dto.User, error)) *MockClient_WorkspaceUsers_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n"
  },
  {
    "path": "internal/mocks/mock_Config.go",
    "content": "// Code generated by mockery; DO NOT EDIT.\n// github.com/vektra/mockery\n// template: testify\n\npackage mocks\n\nimport (\n\t\"time\"\n\n\tmock \"github.com/stretchr/testify/mock\"\n\t\"golang.org/x/text/language\"\n)\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewMockConfig(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *MockConfig {\n\tmock := &MockConfig{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n\n// MockConfig is an autogenerated mock type for the Config type\ntype MockConfig struct {\n\tmock.Mock\n}\n\ntype MockConfig_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *MockConfig) EXPECT() *MockConfig_Expecter {\n\treturn &MockConfig_Expecter{mock: &_m.Mock}\n}\n\n// All provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) All() map[string]interface{} {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for All\")\n\t}\n\n\tvar r0 map[string]interface{}\n\tif returnFunc, ok := ret.Get(0).(func() map[string]interface{}); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(map[string]interface{})\n\t\t}\n\t}\n\treturn r0\n}\n\n// MockConfig_All_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'All'\ntype MockConfig_All_Call struct {\n\t*mock.Call\n}\n\n// All is a helper method to define mock.On call\nfunc (_e *MockConfig_Expecter) All() *MockConfig_All_Call {\n\treturn &MockConfig_All_Call{Call: _e.mock.On(\"All\")}\n}\n\nfunc (_c *MockConfig_All_Call) Run(run func()) *MockConfig_All_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_All_Call) Return(stringToIfaceVal map[string]interface{}) *MockConfig_All_Call {\n\t_c.Call.Return(stringToIfaceVal)\n\treturn _c\n}\n\nfunc (_c *MockConfig_All_Call) RunAndReturn(run func() map[string]interface{}) *MockConfig_All_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Get provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) Get(s string) interface{} {\n\tret := _mock.Called(s)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Get\")\n\t}\n\n\tvar r0 interface{}\n\tif returnFunc, ok := ret.Get(0).(func(string) interface{}); ok {\n\t\tr0 = returnFunc(s)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(interface{})\n\t\t}\n\t}\n\treturn r0\n}\n\n// MockConfig_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'\ntype MockConfig_Get_Call struct {\n\t*mock.Call\n}\n\n// Get is a helper method to define mock.On call\n//   - s string\nfunc (_e *MockConfig_Expecter) Get(s interface{}) *MockConfig_Get_Call {\n\treturn &MockConfig_Get_Call{Call: _e.mock.On(\"Get\", s)}\n}\n\nfunc (_c *MockConfig_Get_Call) Run(run func(s string)) *MockConfig_Get_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 string\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(string)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_Get_Call) Return(ifaceVal interface{}) *MockConfig_Get_Call {\n\t_c.Call.Return(ifaceVal)\n\treturn _c\n}\n\nfunc (_c *MockConfig_Get_Call) RunAndReturn(run func(s string) interface{}) *MockConfig_Get_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetBool provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) GetBool(s string) bool {\n\tret := _mock.Called(s)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetBool\")\n\t}\n\n\tvar r0 bool\n\tif returnFunc, ok := ret.Get(0).(func(string) bool); ok {\n\t\tr0 = returnFunc(s)\n\t} else {\n\t\tr0 = ret.Get(0).(bool)\n\t}\n\treturn r0\n}\n\n// MockConfig_GetBool_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBool'\ntype MockConfig_GetBool_Call struct {\n\t*mock.Call\n}\n\n// GetBool is a helper method to define mock.On call\n//   - s string\nfunc (_e *MockConfig_Expecter) GetBool(s interface{}) *MockConfig_GetBool_Call {\n\treturn &MockConfig_GetBool_Call{Call: _e.mock.On(\"GetBool\", s)}\n}\n\nfunc (_c *MockConfig_GetBool_Call) Run(run func(s string)) *MockConfig_GetBool_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 string\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(string)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_GetBool_Call) Return(b bool) *MockConfig_GetBool_Call {\n\t_c.Call.Return(b)\n\treturn _c\n}\n\nfunc (_c *MockConfig_GetBool_Call) RunAndReturn(run func(s string) bool) *MockConfig_GetBool_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetInt provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) GetInt(s string) int {\n\tret := _mock.Called(s)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetInt\")\n\t}\n\n\tvar r0 int\n\tif returnFunc, ok := ret.Get(0).(func(string) int); ok {\n\t\tr0 = returnFunc(s)\n\t} else {\n\t\tr0 = ret.Get(0).(int)\n\t}\n\treturn r0\n}\n\n// MockConfig_GetInt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetInt'\ntype MockConfig_GetInt_Call struct {\n\t*mock.Call\n}\n\n// GetInt is a helper method to define mock.On call\n//   - s string\nfunc (_e *MockConfig_Expecter) GetInt(s interface{}) *MockConfig_GetInt_Call {\n\treturn &MockConfig_GetInt_Call{Call: _e.mock.On(\"GetInt\", s)}\n}\n\nfunc (_c *MockConfig_GetInt_Call) Run(run func(s string)) *MockConfig_GetInt_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 string\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(string)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_GetInt_Call) Return(n int) *MockConfig_GetInt_Call {\n\t_c.Call.Return(n)\n\treturn _c\n}\n\nfunc (_c *MockConfig_GetInt_Call) RunAndReturn(run func(s string) int) *MockConfig_GetInt_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetString provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) GetString(s string) string {\n\tret := _mock.Called(s)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetString\")\n\t}\n\n\tvar r0 string\n\tif returnFunc, ok := ret.Get(0).(func(string) string); ok {\n\t\tr0 = returnFunc(s)\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\treturn r0\n}\n\n// MockConfig_GetString_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetString'\ntype MockConfig_GetString_Call struct {\n\t*mock.Call\n}\n\n// GetString is a helper method to define mock.On call\n//   - s string\nfunc (_e *MockConfig_Expecter) GetString(s interface{}) *MockConfig_GetString_Call {\n\treturn &MockConfig_GetString_Call{Call: _e.mock.On(\"GetString\", s)}\n}\n\nfunc (_c *MockConfig_GetString_Call) Run(run func(s string)) *MockConfig_GetString_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 string\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(string)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_GetString_Call) Return(s1 string) *MockConfig_GetString_Call {\n\t_c.Call.Return(s1)\n\treturn _c\n}\n\nfunc (_c *MockConfig_GetString_Call) RunAndReturn(run func(s string) string) *MockConfig_GetString_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetStringSlice provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) GetStringSlice(s string) []string {\n\tret := _mock.Called(s)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetStringSlice\")\n\t}\n\n\tvar r0 []string\n\tif returnFunc, ok := ret.Get(0).(func(string) []string); ok {\n\t\tr0 = returnFunc(s)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]string)\n\t\t}\n\t}\n\treturn r0\n}\n\n// MockConfig_GetStringSlice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetStringSlice'\ntype MockConfig_GetStringSlice_Call struct {\n\t*mock.Call\n}\n\n// GetStringSlice is a helper method to define mock.On call\n//   - s string\nfunc (_e *MockConfig_Expecter) GetStringSlice(s interface{}) *MockConfig_GetStringSlice_Call {\n\treturn &MockConfig_GetStringSlice_Call{Call: _e.mock.On(\"GetStringSlice\", s)}\n}\n\nfunc (_c *MockConfig_GetStringSlice_Call) Run(run func(s string)) *MockConfig_GetStringSlice_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 string\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(string)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_GetStringSlice_Call) Return(strings []string) *MockConfig_GetStringSlice_Call {\n\t_c.Call.Return(strings)\n\treturn _c\n}\n\nfunc (_c *MockConfig_GetStringSlice_Call) RunAndReturn(run func(s string) []string) *MockConfig_GetStringSlice_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetWorkWeekdays provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) GetWorkWeekdays() []string {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetWorkWeekdays\")\n\t}\n\n\tvar r0 []string\n\tif returnFunc, ok := ret.Get(0).(func() []string); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]string)\n\t\t}\n\t}\n\treturn r0\n}\n\n// MockConfig_GetWorkWeekdays_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkWeekdays'\ntype MockConfig_GetWorkWeekdays_Call struct {\n\t*mock.Call\n}\n\n// GetWorkWeekdays is a helper method to define mock.On call\nfunc (_e *MockConfig_Expecter) GetWorkWeekdays() *MockConfig_GetWorkWeekdays_Call {\n\treturn &MockConfig_GetWorkWeekdays_Call{Call: _e.mock.On(\"GetWorkWeekdays\")}\n}\n\nfunc (_c *MockConfig_GetWorkWeekdays_Call) Run(run func()) *MockConfig_GetWorkWeekdays_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_GetWorkWeekdays_Call) Return(strings []string) *MockConfig_GetWorkWeekdays_Call {\n\t_c.Call.Return(strings)\n\treturn _c\n}\n\nfunc (_c *MockConfig_GetWorkWeekdays_Call) RunAndReturn(run func() []string) *MockConfig_GetWorkWeekdays_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// InteractivePageSize provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) InteractivePageSize() int {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for InteractivePageSize\")\n\t}\n\n\tvar r0 int\n\tif returnFunc, ok := ret.Get(0).(func() int); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tr0 = ret.Get(0).(int)\n\t}\n\treturn r0\n}\n\n// MockConfig_InteractivePageSize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InteractivePageSize'\ntype MockConfig_InteractivePageSize_Call struct {\n\t*mock.Call\n}\n\n// InteractivePageSize is a helper method to define mock.On call\nfunc (_e *MockConfig_Expecter) InteractivePageSize() *MockConfig_InteractivePageSize_Call {\n\treturn &MockConfig_InteractivePageSize_Call{Call: _e.mock.On(\"InteractivePageSize\")}\n}\n\nfunc (_c *MockConfig_InteractivePageSize_Call) Run(run func()) *MockConfig_InteractivePageSize_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_InteractivePageSize_Call) Return(n int) *MockConfig_InteractivePageSize_Call {\n\t_c.Call.Return(n)\n\treturn _c\n}\n\nfunc (_c *MockConfig_InteractivePageSize_Call) RunAndReturn(run func() int) *MockConfig_InteractivePageSize_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// IsAllowNameForID provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) IsAllowNameForID() bool {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for IsAllowNameForID\")\n\t}\n\n\tvar r0 bool\n\tif returnFunc, ok := ret.Get(0).(func() bool); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tr0 = ret.Get(0).(bool)\n\t}\n\treturn r0\n}\n\n// MockConfig_IsAllowNameForID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsAllowNameForID'\ntype MockConfig_IsAllowNameForID_Call struct {\n\t*mock.Call\n}\n\n// IsAllowNameForID is a helper method to define mock.On call\nfunc (_e *MockConfig_Expecter) IsAllowNameForID() *MockConfig_IsAllowNameForID_Call {\n\treturn &MockConfig_IsAllowNameForID_Call{Call: _e.mock.On(\"IsAllowNameForID\")}\n}\n\nfunc (_c *MockConfig_IsAllowNameForID_Call) Run(run func()) *MockConfig_IsAllowNameForID_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_IsAllowNameForID_Call) Return(b bool) *MockConfig_IsAllowNameForID_Call {\n\t_c.Call.Return(b)\n\treturn _c\n}\n\nfunc (_c *MockConfig_IsAllowNameForID_Call) RunAndReturn(run func() bool) *MockConfig_IsAllowNameForID_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// IsDebuging provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) IsDebuging() bool {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for IsDebuging\")\n\t}\n\n\tvar r0 bool\n\tif returnFunc, ok := ret.Get(0).(func() bool); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tr0 = ret.Get(0).(bool)\n\t}\n\treturn r0\n}\n\n// MockConfig_IsDebuging_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsDebuging'\ntype MockConfig_IsDebuging_Call struct {\n\t*mock.Call\n}\n\n// IsDebuging is a helper method to define mock.On call\nfunc (_e *MockConfig_Expecter) IsDebuging() *MockConfig_IsDebuging_Call {\n\treturn &MockConfig_IsDebuging_Call{Call: _e.mock.On(\"IsDebuging\")}\n}\n\nfunc (_c *MockConfig_IsDebuging_Call) Run(run func()) *MockConfig_IsDebuging_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_IsDebuging_Call) Return(b bool) *MockConfig_IsDebuging_Call {\n\t_c.Call.Return(b)\n\treturn _c\n}\n\nfunc (_c *MockConfig_IsDebuging_Call) RunAndReturn(run func() bool) *MockConfig_IsDebuging_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// IsInteractive provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) IsInteractive() bool {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for IsInteractive\")\n\t}\n\n\tvar r0 bool\n\tif returnFunc, ok := ret.Get(0).(func() bool); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tr0 = ret.Get(0).(bool)\n\t}\n\treturn r0\n}\n\n// MockConfig_IsInteractive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsInteractive'\ntype MockConfig_IsInteractive_Call struct {\n\t*mock.Call\n}\n\n// IsInteractive is a helper method to define mock.On call\nfunc (_e *MockConfig_Expecter) IsInteractive() *MockConfig_IsInteractive_Call {\n\treturn &MockConfig_IsInteractive_Call{Call: _e.mock.On(\"IsInteractive\")}\n}\n\nfunc (_c *MockConfig_IsInteractive_Call) Run(run func()) *MockConfig_IsInteractive_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_IsInteractive_Call) Return(b bool) *MockConfig_IsInteractive_Call {\n\t_c.Call.Return(b)\n\treturn _c\n}\n\nfunc (_c *MockConfig_IsInteractive_Call) RunAndReturn(run func() bool) *MockConfig_IsInteractive_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// IsSearchProjectWithClientsName provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) IsSearchProjectWithClientsName() bool {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for IsSearchProjectWithClientsName\")\n\t}\n\n\tvar r0 bool\n\tif returnFunc, ok := ret.Get(0).(func() bool); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tr0 = ret.Get(0).(bool)\n\t}\n\treturn r0\n}\n\n// MockConfig_IsSearchProjectWithClientsName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsSearchProjectWithClientsName'\ntype MockConfig_IsSearchProjectWithClientsName_Call struct {\n\t*mock.Call\n}\n\n// IsSearchProjectWithClientsName is a helper method to define mock.On call\nfunc (_e *MockConfig_Expecter) IsSearchProjectWithClientsName() *MockConfig_IsSearchProjectWithClientsName_Call {\n\treturn &MockConfig_IsSearchProjectWithClientsName_Call{Call: _e.mock.On(\"IsSearchProjectWithClientsName\")}\n}\n\nfunc (_c *MockConfig_IsSearchProjectWithClientsName_Call) Run(run func()) *MockConfig_IsSearchProjectWithClientsName_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_IsSearchProjectWithClientsName_Call) Return(b bool) *MockConfig_IsSearchProjectWithClientsName_Call {\n\t_c.Call.Return(b)\n\treturn _c\n}\n\nfunc (_c *MockConfig_IsSearchProjectWithClientsName_Call) RunAndReturn(run func() bool) *MockConfig_IsSearchProjectWithClientsName_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Language provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) Language() language.Tag {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Language\")\n\t}\n\n\tvar r0 language.Tag\n\tif returnFunc, ok := ret.Get(0).(func() language.Tag); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tr0 = ret.Get(0).(language.Tag)\n\t}\n\treturn r0\n}\n\n// MockConfig_Language_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Language'\ntype MockConfig_Language_Call struct {\n\t*mock.Call\n}\n\n// Language is a helper method to define mock.On call\nfunc (_e *MockConfig_Expecter) Language() *MockConfig_Language_Call {\n\treturn &MockConfig_Language_Call{Call: _e.mock.On(\"Language\")}\n}\n\nfunc (_c *MockConfig_Language_Call) Run(run func()) *MockConfig_Language_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_Language_Call) Return(tag language.Tag) *MockConfig_Language_Call {\n\t_c.Call.Return(tag)\n\treturn _c\n}\n\nfunc (_c *MockConfig_Language_Call) RunAndReturn(run func() language.Tag) *MockConfig_Language_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// LogLevel provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) LogLevel() string {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for LogLevel\")\n\t}\n\n\tvar r0 string\n\tif returnFunc, ok := ret.Get(0).(func() string); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\treturn r0\n}\n\n// MockConfig_LogLevel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogLevel'\ntype MockConfig_LogLevel_Call struct {\n\t*mock.Call\n}\n\n// LogLevel is a helper method to define mock.On call\nfunc (_e *MockConfig_Expecter) LogLevel() *MockConfig_LogLevel_Call {\n\treturn &MockConfig_LogLevel_Call{Call: _e.mock.On(\"LogLevel\")}\n}\n\nfunc (_c *MockConfig_LogLevel_Call) Run(run func()) *MockConfig_LogLevel_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_LogLevel_Call) Return(s string) *MockConfig_LogLevel_Call {\n\t_c.Call.Return(s)\n\treturn _c\n}\n\nfunc (_c *MockConfig_LogLevel_Call) RunAndReturn(run func() string) *MockConfig_LogLevel_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Save provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) Save() error {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Save\")\n\t}\n\n\tvar r0 error\n\tif returnFunc, ok := ret.Get(0).(func() error); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\treturn r0\n}\n\n// MockConfig_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save'\ntype MockConfig_Save_Call struct {\n\t*mock.Call\n}\n\n// Save is a helper method to define mock.On call\nfunc (_e *MockConfig_Expecter) Save() *MockConfig_Save_Call {\n\treturn &MockConfig_Save_Call{Call: _e.mock.On(\"Save\")}\n}\n\nfunc (_c *MockConfig_Save_Call) Run(run func()) *MockConfig_Save_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_Save_Call) Return(err error) *MockConfig_Save_Call {\n\t_c.Call.Return(err)\n\treturn _c\n}\n\nfunc (_c *MockConfig_Save_Call) RunAndReturn(run func() error) *MockConfig_Save_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// SetBool provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) SetBool(s string, b bool) {\n\t_mock.Called(s, b)\n\treturn\n}\n\n// MockConfig_SetBool_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetBool'\ntype MockConfig_SetBool_Call struct {\n\t*mock.Call\n}\n\n// SetBool is a helper method to define mock.On call\n//   - s string\n//   - b bool\nfunc (_e *MockConfig_Expecter) SetBool(s interface{}, b interface{}) *MockConfig_SetBool_Call {\n\treturn &MockConfig_SetBool_Call{Call: _e.mock.On(\"SetBool\", s, b)}\n}\n\nfunc (_c *MockConfig_SetBool_Call) Run(run func(s string, b bool)) *MockConfig_SetBool_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 string\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(string)\n\t\t}\n\t\tvar arg1 bool\n\t\tif args[1] != nil {\n\t\t\targ1 = args[1].(bool)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t\targ1,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_SetBool_Call) Return() *MockConfig_SetBool_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *MockConfig_SetBool_Call) RunAndReturn(run func(s string, b bool)) *MockConfig_SetBool_Call {\n\t_c.Run(run)\n\treturn _c\n}\n\n// SetInt provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) SetInt(s string, n int) {\n\t_mock.Called(s, n)\n\treturn\n}\n\n// MockConfig_SetInt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetInt'\ntype MockConfig_SetInt_Call struct {\n\t*mock.Call\n}\n\n// SetInt is a helper method to define mock.On call\n//   - s string\n//   - n int\nfunc (_e *MockConfig_Expecter) SetInt(s interface{}, n interface{}) *MockConfig_SetInt_Call {\n\treturn &MockConfig_SetInt_Call{Call: _e.mock.On(\"SetInt\", s, n)}\n}\n\nfunc (_c *MockConfig_SetInt_Call) Run(run func(s string, n int)) *MockConfig_SetInt_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 string\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(string)\n\t\t}\n\t\tvar arg1 int\n\t\tif args[1] != nil {\n\t\t\targ1 = args[1].(int)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t\targ1,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_SetInt_Call) Return() *MockConfig_SetInt_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *MockConfig_SetInt_Call) RunAndReturn(run func(s string, n int)) *MockConfig_SetInt_Call {\n\t_c.Run(run)\n\treturn _c\n}\n\n// SetLanguage provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) SetLanguage(tag language.Tag) {\n\t_mock.Called(tag)\n\treturn\n}\n\n// MockConfig_SetLanguage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetLanguage'\ntype MockConfig_SetLanguage_Call struct {\n\t*mock.Call\n}\n\n// SetLanguage is a helper method to define mock.On call\n//   - tag language.Tag\nfunc (_e *MockConfig_Expecter) SetLanguage(tag interface{}) *MockConfig_SetLanguage_Call {\n\treturn &MockConfig_SetLanguage_Call{Call: _e.mock.On(\"SetLanguage\", tag)}\n}\n\nfunc (_c *MockConfig_SetLanguage_Call) Run(run func(tag language.Tag)) *MockConfig_SetLanguage_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 language.Tag\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(language.Tag)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_SetLanguage_Call) Return() *MockConfig_SetLanguage_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *MockConfig_SetLanguage_Call) RunAndReturn(run func(tag language.Tag)) *MockConfig_SetLanguage_Call {\n\t_c.Run(run)\n\treturn _c\n}\n\n// SetString provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) SetString(s string, s1 string) {\n\t_mock.Called(s, s1)\n\treturn\n}\n\n// MockConfig_SetString_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetString'\ntype MockConfig_SetString_Call struct {\n\t*mock.Call\n}\n\n// SetString is a helper method to define mock.On call\n//   - s string\n//   - s1 string\nfunc (_e *MockConfig_Expecter) SetString(s interface{}, s1 interface{}) *MockConfig_SetString_Call {\n\treturn &MockConfig_SetString_Call{Call: _e.mock.On(\"SetString\", s, s1)}\n}\n\nfunc (_c *MockConfig_SetString_Call) Run(run func(s string, s1 string)) *MockConfig_SetString_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 string\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(string)\n\t\t}\n\t\tvar arg1 string\n\t\tif args[1] != nil {\n\t\t\targ1 = args[1].(string)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t\targ1,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_SetString_Call) Return() *MockConfig_SetString_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *MockConfig_SetString_Call) RunAndReturn(run func(s string, s1 string)) *MockConfig_SetString_Call {\n\t_c.Run(run)\n\treturn _c\n}\n\n// SetStringSlice provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) SetStringSlice(s string, strings []string) {\n\t_mock.Called(s, strings)\n\treturn\n}\n\n// MockConfig_SetStringSlice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetStringSlice'\ntype MockConfig_SetStringSlice_Call struct {\n\t*mock.Call\n}\n\n// SetStringSlice is a helper method to define mock.On call\n//   - s string\n//   - strings []string\nfunc (_e *MockConfig_Expecter) SetStringSlice(s interface{}, strings interface{}) *MockConfig_SetStringSlice_Call {\n\treturn &MockConfig_SetStringSlice_Call{Call: _e.mock.On(\"SetStringSlice\", s, strings)}\n}\n\nfunc (_c *MockConfig_SetStringSlice_Call) Run(run func(s string, strings []string)) *MockConfig_SetStringSlice_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 string\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(string)\n\t\t}\n\t\tvar arg1 []string\n\t\tif args[1] != nil {\n\t\t\targ1 = args[1].([]string)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t\targ1,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_SetStringSlice_Call) Return() *MockConfig_SetStringSlice_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *MockConfig_SetStringSlice_Call) RunAndReturn(run func(s string, strings []string)) *MockConfig_SetStringSlice_Call {\n\t_c.Run(run)\n\treturn _c\n}\n\n// SetTimeZone provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) SetTimeZone(location *time.Location) {\n\t_mock.Called(location)\n\treturn\n}\n\n// MockConfig_SetTimeZone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetTimeZone'\ntype MockConfig_SetTimeZone_Call struct {\n\t*mock.Call\n}\n\n// SetTimeZone is a helper method to define mock.On call\n//   - location *time.Location\nfunc (_e *MockConfig_Expecter) SetTimeZone(location interface{}) *MockConfig_SetTimeZone_Call {\n\treturn &MockConfig_SetTimeZone_Call{Call: _e.mock.On(\"SetTimeZone\", location)}\n}\n\nfunc (_c *MockConfig_SetTimeZone_Call) Run(run func(location *time.Location)) *MockConfig_SetTimeZone_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\tvar arg0 *time.Location\n\t\tif args[0] != nil {\n\t\t\targ0 = args[0].(*time.Location)\n\t\t}\n\t\trun(\n\t\t\targ0,\n\t\t)\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_SetTimeZone_Call) Return() *MockConfig_SetTimeZone_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *MockConfig_SetTimeZone_Call) RunAndReturn(run func(location *time.Location)) *MockConfig_SetTimeZone_Call {\n\t_c.Run(run)\n\treturn _c\n}\n\n// TimeZone provides a mock function for the type MockConfig\nfunc (_mock *MockConfig) TimeZone() *time.Location {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for TimeZone\")\n\t}\n\n\tvar r0 *time.Location\n\tif returnFunc, ok := ret.Get(0).(func() *time.Location); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*time.Location)\n\t\t}\n\t}\n\treturn r0\n}\n\n// MockConfig_TimeZone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TimeZone'\ntype MockConfig_TimeZone_Call struct {\n\t*mock.Call\n}\n\n// TimeZone is a helper method to define mock.On call\nfunc (_e *MockConfig_Expecter) TimeZone() *MockConfig_TimeZone_Call {\n\treturn &MockConfig_TimeZone_Call{Call: _e.mock.On(\"TimeZone\")}\n}\n\nfunc (_c *MockConfig_TimeZone_Call) Run(run func()) *MockConfig_TimeZone_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConfig_TimeZone_Call) Return(location *time.Location) *MockConfig_TimeZone_Call {\n\t_c.Call.Return(location)\n\treturn _c\n}\n\nfunc (_c *MockConfig_TimeZone_Call) RunAndReturn(run func() *time.Location) *MockConfig_TimeZone_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n"
  },
  {
    "path": "internal/mocks/mock_Factory.go",
    "content": "// Code generated by mockery; DO NOT EDIT.\n// github.com/vektra/mockery\n// template: testify\n\npackage mocks\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/ui\"\n\tmock \"github.com/stretchr/testify/mock\"\n)\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewMockFactory(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *MockFactory {\n\tmock := &MockFactory{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n\n// MockFactory is an autogenerated mock type for the Factory type\ntype MockFactory struct {\n\tmock.Mock\n}\n\ntype MockFactory_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *MockFactory) EXPECT() *MockFactory_Expecter {\n\treturn &MockFactory_Expecter{mock: &_m.Mock}\n}\n\n// Client provides a mock function for the type MockFactory\nfunc (_mock *MockFactory) Client() (api.Client, error) {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Client\")\n\t}\n\n\tvar r0 api.Client\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func() (api.Client, error)); ok {\n\t\treturn returnFunc()\n\t}\n\tif returnFunc, ok := ret.Get(0).(func() api.Client); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(api.Client)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func() error); ok {\n\t\tr1 = returnFunc()\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockFactory_Client_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Client'\ntype MockFactory_Client_Call struct {\n\t*mock.Call\n}\n\n// Client is a helper method to define mock.On call\nfunc (_e *MockFactory_Expecter) Client() *MockFactory_Client_Call {\n\treturn &MockFactory_Client_Call{Call: _e.mock.On(\"Client\")}\n}\n\nfunc (_c *MockFactory_Client_Call) Run(run func()) *MockFactory_Client_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockFactory_Client_Call) Return(client api.Client, err error) *MockFactory_Client_Call {\n\t_c.Call.Return(client, err)\n\treturn _c\n}\n\nfunc (_c *MockFactory_Client_Call) RunAndReturn(run func() (api.Client, error)) *MockFactory_Client_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Config provides a mock function for the type MockFactory\nfunc (_mock *MockFactory) Config() cmdutil.Config {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Config\")\n\t}\n\n\tvar r0 cmdutil.Config\n\tif returnFunc, ok := ret.Get(0).(func() cmdutil.Config); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(cmdutil.Config)\n\t\t}\n\t}\n\treturn r0\n}\n\n// MockFactory_Config_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Config'\ntype MockFactory_Config_Call struct {\n\t*mock.Call\n}\n\n// Config is a helper method to define mock.On call\nfunc (_e *MockFactory_Expecter) Config() *MockFactory_Config_Call {\n\treturn &MockFactory_Config_Call{Call: _e.mock.On(\"Config\")}\n}\n\nfunc (_c *MockFactory_Config_Call) Run(run func()) *MockFactory_Config_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockFactory_Config_Call) Return(config cmdutil.Config) *MockFactory_Config_Call {\n\t_c.Call.Return(config)\n\treturn _c\n}\n\nfunc (_c *MockFactory_Config_Call) RunAndReturn(run func() cmdutil.Config) *MockFactory_Config_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetUserID provides a mock function for the type MockFactory\nfunc (_mock *MockFactory) GetUserID() (string, error) {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetUserID\")\n\t}\n\n\tvar r0 string\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func() (string, error)); ok {\n\t\treturn returnFunc()\n\t}\n\tif returnFunc, ok := ret.Get(0).(func() string); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func() error); ok {\n\t\tr1 = returnFunc()\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockFactory_GetUserID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserID'\ntype MockFactory_GetUserID_Call struct {\n\t*mock.Call\n}\n\n// GetUserID is a helper method to define mock.On call\nfunc (_e *MockFactory_Expecter) GetUserID() *MockFactory_GetUserID_Call {\n\treturn &MockFactory_GetUserID_Call{Call: _e.mock.On(\"GetUserID\")}\n}\n\nfunc (_c *MockFactory_GetUserID_Call) Run(run func()) *MockFactory_GetUserID_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockFactory_GetUserID_Call) Return(s string, err error) *MockFactory_GetUserID_Call {\n\t_c.Call.Return(s, err)\n\treturn _c\n}\n\nfunc (_c *MockFactory_GetUserID_Call) RunAndReturn(run func() (string, error)) *MockFactory_GetUserID_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetWorkspace provides a mock function for the type MockFactory\nfunc (_mock *MockFactory) GetWorkspace() (dto.Workspace, error) {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetWorkspace\")\n\t}\n\n\tvar r0 dto.Workspace\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func() (dto.Workspace, error)); ok {\n\t\treturn returnFunc()\n\t}\n\tif returnFunc, ok := ret.Get(0).(func() dto.Workspace); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tr0 = ret.Get(0).(dto.Workspace)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func() error); ok {\n\t\tr1 = returnFunc()\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockFactory_GetWorkspace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkspace'\ntype MockFactory_GetWorkspace_Call struct {\n\t*mock.Call\n}\n\n// GetWorkspace is a helper method to define mock.On call\nfunc (_e *MockFactory_Expecter) GetWorkspace() *MockFactory_GetWorkspace_Call {\n\treturn &MockFactory_GetWorkspace_Call{Call: _e.mock.On(\"GetWorkspace\")}\n}\n\nfunc (_c *MockFactory_GetWorkspace_Call) Run(run func()) *MockFactory_GetWorkspace_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockFactory_GetWorkspace_Call) Return(workspace dto.Workspace, err error) *MockFactory_GetWorkspace_Call {\n\t_c.Call.Return(workspace, err)\n\treturn _c\n}\n\nfunc (_c *MockFactory_GetWorkspace_Call) RunAndReturn(run func() (dto.Workspace, error)) *MockFactory_GetWorkspace_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// GetWorkspaceID provides a mock function for the type MockFactory\nfunc (_mock *MockFactory) GetWorkspaceID() (string, error) {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetWorkspaceID\")\n\t}\n\n\tvar r0 string\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func() (string, error)); ok {\n\t\treturn returnFunc()\n\t}\n\tif returnFunc, ok := ret.Get(0).(func() string); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func() error); ok {\n\t\tr1 = returnFunc()\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockFactory_GetWorkspaceID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkspaceID'\ntype MockFactory_GetWorkspaceID_Call struct {\n\t*mock.Call\n}\n\n// GetWorkspaceID is a helper method to define mock.On call\nfunc (_e *MockFactory_Expecter) GetWorkspaceID() *MockFactory_GetWorkspaceID_Call {\n\treturn &MockFactory_GetWorkspaceID_Call{Call: _e.mock.On(\"GetWorkspaceID\")}\n}\n\nfunc (_c *MockFactory_GetWorkspaceID_Call) Run(run func()) *MockFactory_GetWorkspaceID_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockFactory_GetWorkspaceID_Call) Return(s string, err error) *MockFactory_GetWorkspaceID_Call {\n\t_c.Call.Return(s, err)\n\treturn _c\n}\n\nfunc (_c *MockFactory_GetWorkspaceID_Call) RunAndReturn(run func() (string, error)) *MockFactory_GetWorkspaceID_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UI provides a mock function for the type MockFactory\nfunc (_mock *MockFactory) UI() ui.UI {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for UI\")\n\t}\n\n\tvar r0 ui.UI\n\tif returnFunc, ok := ret.Get(0).(func() ui.UI); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(ui.UI)\n\t\t}\n\t}\n\treturn r0\n}\n\n// MockFactory_UI_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UI'\ntype MockFactory_UI_Call struct {\n\t*mock.Call\n}\n\n// UI is a helper method to define mock.On call\nfunc (_e *MockFactory_Expecter) UI() *MockFactory_UI_Call {\n\treturn &MockFactory_UI_Call{Call: _e.mock.On(\"UI\")}\n}\n\nfunc (_c *MockFactory_UI_Call) Run(run func()) *MockFactory_UI_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockFactory_UI_Call) Return(uI ui.UI) *MockFactory_UI_Call {\n\t_c.Call.Return(uI)\n\treturn _c\n}\n\nfunc (_c *MockFactory_UI_Call) RunAndReturn(run func() ui.UI) *MockFactory_UI_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Version provides a mock function for the type MockFactory\nfunc (_mock *MockFactory) Version() cmdutil.Version {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Version\")\n\t}\n\n\tvar r0 cmdutil.Version\n\tif returnFunc, ok := ret.Get(0).(func() cmdutil.Version); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tr0 = ret.Get(0).(cmdutil.Version)\n\t}\n\treturn r0\n}\n\n// MockFactory_Version_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Version'\ntype MockFactory_Version_Call struct {\n\t*mock.Call\n}\n\n// Version is a helper method to define mock.On call\nfunc (_e *MockFactory_Expecter) Version() *MockFactory_Version_Call {\n\treturn &MockFactory_Version_Call{Call: _e.mock.On(\"Version\")}\n}\n\nfunc (_c *MockFactory_Version_Call) Run(run func()) *MockFactory_Version_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockFactory_Version_Call) Return(version cmdutil.Version) *MockFactory_Version_Call {\n\t_c.Call.Return(version)\n\treturn _c\n}\n\nfunc (_c *MockFactory_Version_Call) RunAndReturn(run func() cmdutil.Version) *MockFactory_Version_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n"
  },
  {
    "path": "internal/mocks/simple_config.go",
    "content": "package mocks\n\nimport (\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"golang.org/x/text/language\"\n)\n\n// SimpleConfig is used to set configs for tests were changing the config or\n// accessing them with Get and All is not important\ntype SimpleConfig struct {\n\tWorkweekDays                 []string\n\tInteractive                  bool\n\tInteractivePageSizeNumber    int\n\tAllowNameForID               bool\n\tUserID                       string\n\tWorkspace                    string\n\tToken                        string\n\tAllowIncomplete              bool\n\tShowTask                     bool\n\tDescriptionAutocomplete      bool\n\tDescriptionAutocompleteDays  int\n\tShowTotalDuration            bool\n\tLogLevelValue                string\n\tAllowArchivedTags            bool\n\tSearchProjectWithClientsName bool\n\tLanguageTag                  language.Tag\n\tTimeZoneLoc                  *time.Location\n}\n\nfunc (d *SimpleConfig) GetBool(n string) bool {\n\tswitch n {\n\tcase cmdutil.CONF_INTERACTIVE:\n\t\treturn d.Interactive\n\tcase cmdutil.CONF_ALLOW_NAME_FOR_ID:\n\t\treturn d.AllowNameForID\n\tcase cmdutil.CONF_ALLOW_INCOMPLETE:\n\t\treturn d.AllowIncomplete\n\tcase cmdutil.CONF_SHOW_TASKS:\n\t\treturn d.ShowTask\n\tcase cmdutil.CONF_DESCR_AUTOCOMP:\n\t\treturn d.DescriptionAutocomplete\n\tcase cmdutil.CONF_SHOW_TOTAL_DURATION:\n\t\treturn d.ShowTotalDuration\n\tcase cmdutil.CONF_ALLOW_ARCHIVED_TAGS:\n\t\treturn d.AllowArchivedTags\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc (*SimpleConfig) SetBool(_ string, _ bool) {\n\tpanic(\"should not call\")\n}\n\nfunc (d *SimpleConfig) GetInt(n string) int {\n\tswitch n {\n\tcase cmdutil.CONF_DESCR_AUTOCOMP_DAYS:\n\t\treturn d.DescriptionAutocompleteDays\n\tcase cmdutil.CONF_INTERACTIVE_PAGE_SIZE:\n\t\treturn d.InteractivePageSize()\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc (*SimpleConfig) SetInt(_ string, _ int) {\n\tpanic(\"should not call\")\n}\n\nfunc (d *SimpleConfig) GetString(n string) string {\n\tswitch n {\n\tcase cmdutil.CONF_USER_ID:\n\t\treturn d.UserID\n\tcase cmdutil.CONF_WORKSPACE:\n\t\treturn d.Workspace\n\tcase cmdutil.CONF_TOKEN:\n\t\treturn d.Token\n\tcase cmdutil.CONF_LOG_LEVEL:\n\t\treturn d.LogLevelValue\n\tdefault:\n\t\treturn \"\"\n\n\t}\n}\n\nfunc (*SimpleConfig) SetString(_, _ string) {\n\tpanic(\"should not call\")\n}\n\nfunc (d *SimpleConfig) GetStringSlice(n string) []string {\n\tswitch n {\n\tcase cmdutil.CONF_WORKWEEK_DAYS:\n\t\treturn d.WorkweekDays\n\tdefault:\n\t\treturn []string{}\n\t}\n}\n\nfunc (*SimpleConfig) SetStringSlice(_ string, _ []string) {\n\tpanic(\"should not call\")\n}\n\nfunc (d *SimpleConfig) IsDebuging() bool {\n\treturn d.LogLevel() == cmdutil.LOG_LEVEL_DEBUG\n}\n\nfunc (d *SimpleConfig) IsAllowNameForID() bool {\n\treturn d.AllowNameForID\n}\n\nfunc (d *SimpleConfig) IsInteractive() bool {\n\treturn d.Interactive\n}\n\nfunc (d *SimpleConfig) GetWorkWeekdays() []string {\n\treturn d.WorkweekDays\n}\n\nfunc (d *SimpleConfig) SetLanguage(l language.Tag) {\n\td.LanguageTag = l\n}\n\nfunc (d *SimpleConfig) Language() language.Tag {\n\treturn d.LanguageTag\n}\n\nfunc (*SimpleConfig) Get(_ string) interface{} {\n\tpanic(\"should not call\")\n}\n\nfunc (*SimpleConfig) All() map[string]interface{} {\n\tpanic(\"should not call\")\n}\n\nfunc (d *SimpleConfig) LogLevel() string {\n\treturn d.LogLevelValue\n}\n\n// TimeZone which time zone to use for showing date & time\nfunc (s *SimpleConfig) TimeZone() *time.Location {\n\tif s.TimeZoneLoc == nil {\n\t\ts.TimeZoneLoc = time.UTC\n\t}\n\n\treturn s.TimeZoneLoc\n}\n\n// SetTimeZone changes the timezone used for dates\nfunc (s *SimpleConfig) SetTimeZone(loc *time.Location) {\n\ts.TimeZoneLoc = loc\n}\n\n// IsSearchProjectWithClientsName defines if the project name for ID should\n// include the client's name\nfunc (s *SimpleConfig) IsSearchProjectWithClientsName() bool {\n\treturn s.SearchProjectWithClientsName\n}\n\n// InteractivePageSize sets how many items are shown when prompting\n// projects\nfunc (s *SimpleConfig) InteractivePageSize() int {\n\treturn s.InteractivePageSizeNumber\n}\n\nfunc (*SimpleConfig) Save() error {\n\tpanic(\"should not call\")\n}\n"
  },
  {
    "path": "internal/testhlp/helper.go",
    "content": "package testhlp\n\nimport \"time\"\n\n// MustParseTime will parse a string as time.Time or panic\nfunc MustParseTime(l, v string) time.Time {\n\tt, err := time.Parse(l, v)\n\tif err == nil {\n\t\treturn t\n\t}\n\tpanic(err)\n}\n"
  },
  {
    "path": "netlify.toml",
    "content": "[build.environment]\nGO_VERSION = \"1.24\"\nHUGO_VERSION = \"0.156.0\"\n\n[build]\n    command = \"make site-build\"\n    publish = \"site/public\"\n\n"
  },
  {
    "path": "pkg/cmd/client/add/add.go",
    "content": "package add\n\nimport (\n\t\"io\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/client/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdAdd represents the add command\nfunc NewCmdAdd(\n\tf cmdutil.Factory,\n\treport func(io.Writer, *util.OutputFlags, dto.Client) error,\n) *cobra.Command {\n\tof := util.OutputFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:     \"add\",\n\t\tAliases: []string{\"new\", \"create\"},\n\t\tShort:   \"Adds a new client to the Clockify workspace\",\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s --name Special\n\t\t\t+--------------------------+---------+----------+\n\t\t\t|            ID            |   NAME  | ARCHIVED |\n\t\t\t+--------------------------+---------+----------+\n\t\t\t| eeeeeeeeeeeeeeeeeeeeeeee | Special | NO       |\n\t\t\t+--------------------------+---------+----------+\n\n\t\t\t$ %[1]s --name \"Very Special\" --quiet\n\t\t\taaaaaaaaaaaaaaaaaaaaaaaa\n\n\t\t\t$ %[1]s --name \"Special\" # same name as existing one\n\t\t\tadd client: Client with name 'Special' already exists (code: 501)\n\t\t`, \"clockify-cli client add\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tw, err := f.GetWorkspaceID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tname, _ := cmd.Flags().GetString(\"name\")\n\t\t\tcl, err := c.AddClient(api.AddClientParam{\n\t\t\t\tWorkspace: w,\n\t\t\t\tName:      name,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tout := cmd.OutOrStdout()\n\n\t\t\tif report != nil {\n\t\t\t\treturn report(out, &of, cl)\n\t\t\t}\n\n\t\t\treturn util.Report([]dto.Client{cl}, out, of)\n\t\t},\n\t}\n\n\tcmd.Flags().StringP(\"name\", \"n\", \"\", \"the name of the new client\")\n\t_ = cmd.MarkFlagRequired(\"name\")\n\n\tutil.AddReportFlags(cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/client/add/add_test.go",
    "content": "package add_test\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/client/add\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/client/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCmdAdd(t *testing.T) {\n\ttts := []struct {\n\t\tname    string\n\t\targs    []string\n\t\tfactory func(*testing.T) cmdutil.Factory\n\t\terr     string\n\t}{\n\t\t{\n\t\t\tname: \"only one format\",\n\t\t\targs: []string{\"--format={}\", \"-q\", \"-j\", \"-n=OK\"},\n\t\t\terr:  \"flags can't be used together.*format.*json.*quiet\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\treturn mocks.NewMockFactory(t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"name required\",\n\t\t\terr:  `\"name\" not set`,\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\treturn mocks.NewMockFactory(t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"client error\",\n\t\t\terr:  \"client error\",\n\t\t\targs: []string{\"-n=a\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(nil, errors.New(\"client error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workspace error\",\n\t\t\terr:  \"workspace error\",\n\t\t\targs: []string{\"-n=a\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"\", errors.New(\"workspace error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"http error\",\n\t\t\terr:  \"http error\",\n\t\t\targs: []string{\"-n=error\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tc.On(\"AddClient\", api.AddClientParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tName:      \"error\",\n\t\t\t\t}).\n\t\t\t\t\tReturn(dto.Client{}, errors.New(\"http error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcmd := add.NewCmdAdd(tt.factory(t),\n\t\t\t\tfunc(io.Writer, *util.OutputFlags, dto.Client) error {\n\t\t\t\t\tt.Error(\"should not get here\")\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n}\n\nfunc TestCmdAddReport(t *testing.T) {\n\tcl := dto.Client{Name: \"Coderockr\"}\n\ttts := []struct {\n\t\tname   string\n\t\targs   []string\n\t\tassert func(*testing.T, *util.OutputFlags, dto.Client)\n\t}{\n\t\t{\n\t\t\tname: \"report quiet\",\n\t\t\targs: []string{\"-q\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Client) {\n\t\t\t\tassert.True(t, of.Quiet)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report json\",\n\t\t\targs: []string{\"--json\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Client) {\n\t\t\t\tassert.True(t, of.JSON)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report format\",\n\t\t\targs: []string{\"--format={{.ID}}\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Client) {\n\t\t\t\tassert.Equal(t, \"{{.ID}}\", of.Format)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\tReturn(\"w\", nil)\n\n\t\t\tc.On(\"AddClient\", api.AddClientParam{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tName:      \"rockr\",\n\t\t\t}).\n\t\t\t\tReturn(cl, nil)\n\n\t\t\tcalled := false\n\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\tcmd := add.NewCmdAdd(f, func(\n\t\t\t\t_ io.Writer, of *util.OutputFlags, u dto.Client) error {\n\t\t\t\tcalled = true\n\t\t\t\tassert.Equal(t, cl, u)\n\t\t\t\ttt.assert(t, of, u)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(append(tt.args, \"-n=rockr\"))\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/client/client.go",
    "content": "package client\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/client/add\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/client/list\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdClient represents the client command\nfunc NewCmdClient(f cmdutil.Factory) *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:     \"client\",\n\t\tAliases: []string{\"clients\"},\n\t\tShort:   \"Work with Clockify clients\",\n\t}\n\n\tcmd.AddCommand(list.NewCmdList(f, nil))\n\tcmd.AddCommand(add.NewCmdAdd(f, nil))\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/client/list/list.go",
    "content": "package list\n\nimport (\n\t\"io\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/client/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdList represents the list command\nfunc NewCmdList(\n\tf cmdutil.Factory,\n\treport func(io.Writer, *util.OutputFlags, []dto.Client) error,\n) *cobra.Command {\n\tof := util.OutputFlags{}\n\tvar archived, notArchived bool\n\tcmd := &cobra.Command{\n\t\tUse:     \"list\",\n\t\tAliases: []string{\"ls\"},\n\t\tShort:   \"List clients from a Clockify workspace\",\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s\n\t\t\t+--------------------------+----------+----------+\n\t\t\t|            ID            |   NAME   | ARCHIVED |\n\t\t\t+--------------------------+----------+----------+\n\t\t\t| 6202634a28782767054eec26 | Client 1 | NO       |\n\t\t\t| 62964b36bb48532a70730dbe | Client 2 | YES      |\n\t\t\t+--------------------------+----------+----------+\n\n\t\t\t$ %[1]s --archived --csv\n\t\t\t62964b36bb48532a70730dbe,Client 2,true\n\n\t\t\t$ %[1]s --not-archived --format \"<{{ .Name }}>\"\n\t\t\t<Client 1>\n\n\t\t\t$ %[1]s --name \"1\" --quiet\n\t\t\t6202634a28782767054eec26\n\t\t`, \"clockify-cli client list\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := cmdutil.XorFlag(map[string]bool{\n\t\t\t\t\"archived\":     archived,\n\t\t\t\t\"not-archived\": notArchived,\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tp := api.GetClientsParam{\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tif p.Workspace, err = f.GetWorkspaceID(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tp.Name, _ = cmd.Flags().GetString(\"name\")\n\t\t\tif archived || notArchived {\n\t\t\t\tp.Archived = &archived\n\t\t\t}\n\n\t\t\tclients, err := c.GetClients(p)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif report != nil {\n\t\t\t\treturn report(cmd.OutOrStdout(), &of, clients)\n\t\t\t}\n\n\t\t\treturn util.Report(clients, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tcmd.Flags().StringP(\"name\", \"n\", \"\",\n\t\t\"will be used to filter the tag by name\")\n\tcmd.Flags().BoolVarP(\n\t\t&notArchived, \"not-archived\", \"\", false, \"list only active projects\")\n\tcmd.Flags().BoolVarP(\n\t\t&archived, \"archived\", \"\", false, \"list only archived projects\")\n\n\tutil.AddReportFlags(cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/client/list/list_test.go",
    "content": "package list_test\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/client/list\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/client/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype report func(io.Writer, *util.OutputFlags, []dto.Client) error\n\nfunc TestCmdList(t *testing.T) {\n\tdefReport := func(io.Writer, *util.OutputFlags, []dto.Client) error {\n\t\treturn errors.New(\"should not call\")\n\t}\n\n\tcs := []dto.Client{{Name: \"Coderockr\"}}\n\ttts := []struct {\n\t\tname    string\n\t\targs    []string\n\t\tfactory func(*testing.T) (cmdutil.Factory, report)\n\t\terr     string\n\t}{\n\t\t{\n\t\t\tname: \"only one format\",\n\t\t\targs: []string{\"--format={}\", \"-q\", \"-j\"},\n\t\t\terr:  \"flags can't be used together.*format.*json.*quiet\",\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\treturn mocks.NewMockFactory(t), defReport\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"archived or not\",\n\t\t\targs: []string{\"--archived\", \"--not-archived\"},\n\t\t\terr:  \"flags can't be used together.*archived.*not-archived\",\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\treturn mocks.NewMockFactory(t), defReport\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"client error\",\n\t\t\terr:  \"client error\",\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(nil, errors.New(\"client error\"))\n\t\t\t\treturn f, defReport\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workspace error\",\n\t\t\terr:  \"workspace error\",\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"\", errors.New(\"workspace error\"))\n\t\t\t\treturn f, defReport\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"http error\",\n\t\t\terr:  \"http error\",\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tc.On(\"GetClients\", api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Client{}, errors.New(\"http error\"))\n\t\t\t\treturn f, defReport\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"only archived\",\n\t\t\targs: []string{\"--archived\"},\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tb := true\n\t\t\t\tc.On(\"GetClients\", api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tArchived:        &b,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(cs, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\t\treturn f, func(\n\t\t\t\t\t_ io.Writer, of *util.OutputFlags, l []dto.Client) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, cs, l)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"not archived\",\n\t\t\targs: []string{\"--not-archived\"},\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tb := false\n\t\t\t\tc.On(\"GetClients\", api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tArchived:        &b,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(cs, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\t\treturn f, func(\n\t\t\t\t\t_ io.Writer, of *util.OutputFlags, l []dto.Client) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, cs, l)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report quiet\",\n\t\t\targs: []string{\"--name=rockr\", \"-q\"},\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tc.On(\"GetClients\", api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tName:            \"rockr\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(cs, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\t\treturn f, func(\n\t\t\t\t\t_ io.Writer, of *util.OutputFlags, u []dto.Client) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, cs, u)\n\t\t\t\t\tassert.True(t, of.Quiet)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report json\",\n\t\t\targs: []string{\"--json\"},\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tc.On(\"GetClients\", api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(cs, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\t\treturn f, func(\n\t\t\t\t\t_ io.Writer, of *util.OutputFlags, u []dto.Client) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, cs, u)\n\t\t\t\t\tassert.True(t, of.JSON)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report format\",\n\t\t\targs: []string{\"--format={{.Name}}\"},\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tc.On(\"GetClients\", api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(cs, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\t\treturn f, func(\n\t\t\t\t\t_ io.Writer, of *util.OutputFlags, u []dto.Client) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, cs, u)\n\t\t\t\t\tassert.Equal(t, \"{{.Name}}\", of.Format)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcmd := list.NewCmdList(tt.factory(t))\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/client/util/util.go",
    "content": "package util\n\nimport (\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\toutput \"github.com/lucassabreu/clockify-cli/pkg/output/client\"\n\t\"github.com/spf13/cobra\"\n)\n\n// OutputFlags sets how to print out a list of clients\ntype OutputFlags struct {\n\tFormat string\n\tCSV    bool\n\tJSON   bool\n\tQuiet  bool\n}\n\nfunc (of OutputFlags) Check() error {\n\treturn cmdutil.XorFlag(map[string]bool{\n\t\t\"format\": of.Format != \"\",\n\t\t\"json\":   of.JSON,\n\t\t\"csv\":    of.CSV,\n\t\t\"quiet\":  of.Quiet,\n\t})\n}\n\n// AddReportFlags adds the default output flags for clients\nfunc AddReportFlags(cmd *cobra.Command, of *OutputFlags) {\n\tcmd.Flags().StringVarP(&of.Format, \"format\", \"f\", \"\",\n\t\t\"golang text/template format to be applied on each Client\")\n\tcmd.Flags().BoolVarP(&of.JSON, \"json\", \"j\", false, \"print as JSON\")\n\tcmd.Flags().BoolVarP(&of.CSV, \"csv\", \"v\", false, \"print as CSV\")\n\tcmd.Flags().BoolVarP(&of.Quiet, \"quiet\", \"q\", false, \"only display ids\")\n}\n\n// Report prints out the clients\nfunc Report(cs []dto.Client, out io.Writer, of OutputFlags) error {\n\tswitch {\n\tcase of.JSON:\n\t\treturn output.ClientsJSONPrint(cs, out)\n\tcase of.CSV:\n\t\treturn output.ClientsCSVPrint(cs, out)\n\tcase of.Format != \"\":\n\t\treturn output.ClientPrintWithTemplate(of.Format)(cs, out)\n\tcase of.Quiet:\n\t\treturn output.ClientPrintQuietly(cs, out)\n\tdefault:\n\t\treturn output.ClientPrint(cs, out)\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/completion/completion.go",
    "content": "package completion\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/spf13/cobra\"\n)\n\nconst (\n\tbash       = \"bash\"\n\tzsh        = \"zsh\"\n\tfish       = \"fish\"\n\tpowershell = \"powershell\"\n)\n\n// NewCmdCompletion represents the completion command\nfunc NewCmdCompletion() *cobra.Command {\n\targs := cmdcompl.ValidArgsSlide{bash, zsh, fish, powershell}\n\n\tcmd := &cobra.Command{\n\t\tUse:                   \"completion \" + args.IntoUse(),\n\t\tShort:                 \"Generate completion script\",\n\t\tDisableFlagsInUseLine: true,\n\t\tValidArgs:             args.OnlyArgs(),\n\t\tArgs: cobra.MatchAll(\n\t\t\tcobra.OnlyValidArgs,\n\t\t\tcobra.ExactArgs(1),\n\t\t),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tout := cmd.OutOrStdout()\n\t\t\tswitch strings.ToLower(args[0]) {\n\t\t\tcase bash:\n\t\t\t\treturn cmd.Root().GenBashCompletion(out)\n\t\t\tcase zsh:\n\t\t\t\treturn genZshCompletion(cmd, out)\n\t\t\tcase fish:\n\t\t\t\treturn cmd.Root().GenFishCompletion(out, false)\n\t\t\tcase powershell:\n\t\t\t\treturn cmd.Root().GenPowerShellCompletion(out)\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t},\n\t}\n\n\tcmd.Long = heredoc.Docf(`\n\t\tTo load completions for every session, execute once:\n\n\t\t#### Linux (Bash):\n\n\t\t%[1]s\n\t\t$ clockify-cli completion %[2]s > /etc/bash_cmdcompl.d/clockify-cli\n\t\t%[1]s\n\n\t\t#### Linux (Shell):\n\n\t\t%[1]s\n\t\t$ clockify-cli completion %[2]s > /etc/bash_cmdcompl.d/clockify-cli\n\t\t%[1]s\n\n\t\t#### MacOS:\n\n\t\t%[1]s\n\t\t$ clockify-cli completion %[2]s > /usr/local/etc/bash_cmdcompl.d/clockify-cli\n\t\t%[1]s\n\n\t\t#### Zsh:\n\n\t\tTo load completions for each session, add this line to your ~/.zshrc:\n\t\t%[1]s\n\t\tsource <(clockify-cli completion %[3]s)\n\t\t%[1]s\n\n\t\tYou will need to start a new shell for this setup to take effect.\n\n\t\t#### Fish:\n\t\tTo load completions for each session, execute once:\n\t\t%[1]s\n\t\t$ clockify-cli completion %[4]s > ~/.config/fish/completions/clockify-cli.fish\n\t\t%[1]s`, \"```\", bash, zsh, fish)\n\n\treturn cmd\n}\n\nfunc genZshCompletion(cmd *cobra.Command, w io.Writer) error {\n\tif _, err := fmt.Fprintln(w,\n\t\t\"autoload -U compinit; compinit\"); err != nil {\n\t\treturn err\n\t}\n\n\tif err := cmd.Root().GenZshCompletion(w); err != nil {\n\t\treturn err\n\t}\n\n\t_, err := fmt.Fprintln(w, \"compdef _clockify-cli clockify-cli\")\n\treturn err\n}\n"
  },
  {
    "path": "pkg/cmd/config/config.go",
    "content": "package config\n\nimport (\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/config/get\"\n\tinitialize \"github.com/lucassabreu/clockify-cli/pkg/cmd/config/init\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/config/list\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/config/set\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar validParameters = cmdcompl.ValidArgsMap{\n\tcmdutil.CONF_TOKEN:     \"clockify's token\",\n\tcmdutil.CONF_WORKSPACE: \"workspace to be used\",\n\tcmdutil.CONF_USER_ID:   \"user id from the token\",\n\tcmdutil.CONF_ALLOW_NAME_FOR_ID: \"allow to input the name of the entity \" +\n\t\t\"instead of its ID (projects, clients, tasks, users and tags)\",\n\tcmdutil.CONF_INTERACTIVE: \"show interactive mode\",\n\tcmdutil.CONF_WORKWEEK_DAYS: \"days of the week were your expected to \" +\n\t\t\"work (use comma to set multiple)\",\n\tcmdutil.CONF_ALLOW_INCOMPLETE: \"should allow starting time entries with \" +\n\t\t\"missing required values\",\n\tcmdutil.CONF_SHOW_TASKS: \"should show an extra column with the task \" +\n\t\t\"description\",\n\tcmdutil.CONF_SHOW_CLIENT: \"should show an extra column with the client \" +\n\t\t\"description\",\n\tcmdutil.CONF_DESCR_AUTOCOMP: \"autocomplete description looking at \" +\n\t\t\"recent time entries\",\n\tcmdutil.CONF_DESCR_AUTOCOMP_DAYS: \"how many days should be considered \" +\n\t\t\"for the description autocomplete\",\n\tcmdutil.CONF_SHOW_TOTAL_DURATION: \"adds a totals line on time entry \" +\n\t\t\"reports with the sum of the time entries duration\",\n\tcmdutil.CONF_LOG_LEVEL: \"how much logs should be shown values: \" +\n\t\t\"none , error , info and debug\",\n\tcmdutil.CONF_ALLOW_ARCHIVED_TAGS: \"should allow and suggest archived tags\",\n\tcmdutil.CONF_LANGUAGE: \"which language to use for number \" +\n\t\t\"formatting\",\n\tcmdutil.CONF_TIMEZONE: \"which timezone to use to input/output time\",\n\tcmdutil.CONF_API_URL:  \"custom Clockify API base URL (for segregated tenants)\",\n}\n\n// NewCmdConfig represents the config command\nfunc NewCmdConfig(f cmdutil.Factory) *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"config\",\n\t\tShort: \"Manages CLI configuration\",\n\t\tArgs:  cobra.MaximumNArgs(0),\n\t\tExample: heredoc.Doc(`\n\t\t\t# cli will guide you to configure the CLI\n\t\t\t$ clockify-cli config init\n\n\t\t\t# token is the minimum information required for the CLI to work\n\t\t\t$ clockify-cli set token <token>\n\n\t\t\t# you can see your current parameters using:\n\t\t\t$ clockify-cli get\n\n\t\t\t# if you wanna see the value of token parameter:\n\t\t\t$ clockify-cli get token\n\t\t`),\n\t\tLong: heredoc.Doc(`\n\t\t\tChanges or shows configuration settings for clockify-cli\n\n\t\t\tThese are the parameters manageable:\n\t\t`) + validParameters.Long(),\n\t}\n\n\tcmd.AddCommand(initialize.NewCmdInit(f))\n\tcmd.AddCommand(set.NewCmdSet(f, validParameters))\n\tcmd.AddCommand(get.NewCmdGet(f, validParameters))\n\tcmd.AddCommand(list.NewCmdList(f))\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/config/get/get.go",
    "content": "package get\n\nimport (\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/config/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc NewCmdGet(\n\tf cmdutil.Factory,\n\tvalidParameters cmdcompl.ValidArgsMap,\n) *cobra.Command {\n\tvar format string\n\tcmd := &cobra.Command{\n\t\tUse:   \"get <param>\",\n\t\tShort: \"Retrieves one parameter set by the user\",\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s token\n\t\t\tYamdas569\n\n\t\t\t$ %[1]s workweek-days --format=json\n\t\t\t[\"monday\",\"tuesday\",\"wednesday\",\"thursday\",\"friday\"]\n\t\t`, \"clockify-cli config get\"),\n\t\tArgs: cobra.MatchAll(\n\t\t\tcmdutil.RequiredNamedArgs(\"param\"),\n\t\t\tcobra.ExactArgs(1),\n\t\t),\n\t\tValidArgs: validParameters.IntoValidArgs(),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn util.Report(\n\t\t\t\tcmd.OutOrStdout(), format,\n\t\t\t\tf.Config().Get(args[0]))\n\t\t},\n\t}\n\n\t_ = util.AddReportFlags(cmd, &format)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/config/get/get_test.go",
    "content": "package get_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/config/get\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc newCmd(f cmdutil.Factory) *cobra.Command {\n\tcmd := get.NewCmdGet(\n\t\tf,\n\t\tcmdcompl.ValidArgsMap{},\n\t)\n\tcmd.SilenceErrors = true\n\tcmd.SilenceUsage = true\n\n\tb := bytes.NewBufferString(\"\")\n\tcmd.SetOut(b)\n\tcmd.SetErr(b)\n\treturn cmd\n}\n\nfunc TestGetCmdArgs(t *testing.T) {\n\ttcs := []struct {\n\t\tname string\n\t\targs []string\n\t\terr  error\n\t}{\n\t\t{\n\t\t\tname: \"none\",\n\t\t\targs: []string{},\n\t\t\terr:  errors.New(\"requires arg param\"),\n\t\t},\n\t\t{\n\t\t\tname: \"two\",\n\t\t\targs: []string{\"param1\", \"param2\"},\n\t\t\terr:  errors.New(\"accepts 1 arg(s), received 2\"),\n\t\t},\n\t}\n\n\tfor _, tc := range tcs {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tcmd := newCmd(mocks.NewMockFactory(t))\n\t\t\tcmd.SetArgs(tc.args)\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif assert.Error(t, err) {\n\t\t\t\tassert.Equal(t, err.Error(), tc.err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetCmdRun(t *testing.T) {\n\ttcs := []struct {\n\t\tname   string\n\t\targs   []string\n\t\tconfig func(*testing.T) cmdutil.Config\n\t\toutput string\n\t\terr    error\n\t}{\n\t\t{\n\t\t\tname: \"token with default format\",\n\t\t\targs: []string{\"token\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"Get\", \"token\").Once().Return(\"<token-value>\")\n\t\t\t\treturn c\n\t\t\t},\n\t\t\toutput: \"<token-value>\\n\",\n\t\t},\n\t\t{\n\t\t\tname: \"token with json format\",\n\t\t\targs: []string{\"token\", \"--format=json\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"Get\", \"token\").Once().Return(\"token-value\")\n\t\t\t\treturn c\n\t\t\t},\n\t\t\toutput: `\"token-value\"`,\n\t\t},\n\t\t{\n\t\t\tname: \"workdays default format\",\n\t\t\targs: []string{\"workdays\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"Get\", \"workdays\").Once().Return([]string{\n\t\t\t\t\t\"monday\",\n\t\t\t\t\t\"tuesday\",\n\t\t\t\t\t\"sunday\",\n\t\t\t\t})\n\t\t\t\treturn c\n\t\t\t},\n\t\t\toutput: heredoc.Doc(`\n\t\t\t- monday\n\t\t\t- tuesday\n\t\t\t- sunday\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"workdays json format\",\n\t\t\targs: []string{\"workdays\", \"--format\", \"json\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"Get\", \"workdays\").Once().Return([]string{\n\t\t\t\t\t\"monday\",\n\t\t\t\t\t\"tuesday\",\n\t\t\t\t\t\"sunday\",\n\t\t\t\t})\n\t\t\t\treturn c\n\t\t\t},\n\t\t\toutput: `[\"monday\",\"tuesday\",\"sunday\"]`,\n\t\t},\n\t\t{\n\t\t\tname: \"user.id default format\",\n\t\t\targs: []string{\"user.id\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"Get\", \"user.id\").Once().Return(\"someuserid\")\n\t\t\t\treturn c\n\t\t\t},\n\t\t\toutput: \"someuserid\\n\",\n\t\t},\n\t\t{\n\t\t\tname: \"user default format\",\n\t\t\targs: []string{\"user\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"Get\", \"user\").Once().Return(map[string]string{\n\t\t\t\t\t\"id\": \"someuserid\",\n\t\t\t\t})\n\t\t\t\treturn c\n\t\t\t},\n\t\t\toutput: \"id: someuserid\\n\",\n\t\t},\n\t\t{\n\t\t\tname: \"user json format\",\n\t\t\targs: []string{\"user\", \"-f=JSON\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"Get\", \"user\").Once().Return(map[string]string{\n\t\t\t\t\t\"id\": \"someuserid\",\n\t\t\t\t})\n\t\t\t\treturn c\n\t\t\t},\n\t\t\toutput: `{\"id\":\"someuserid\"}`,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid format\",\n\t\t\targs: []string{\"user\", \"--format\", \"tmol\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"Get\", \"user\").Return(map[string]string{\n\t\t\t\t\t\"id\": \"someuserid\",\n\t\t\t\t})\n\t\t\t\treturn c\n\t\t\t},\n\t\t\toutput: ``,\n\t\t\terr:    errors.New(\"invalid format\"),\n\t\t},\n\t}\n\n\tfor _, tc := range tcs {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tf.On(\"Config\").Once().Return(tc.config(t))\n\n\t\t\tcmd := newCmd(f)\n\t\t\tcmd.SetArgs(tc.args)\n\t\t\tout := cmd.OutOrStdout().(*bytes.Buffer)\n\t\t\t_, err := cmd.ExecuteC()\n\n\t\t\tassert.Equal(t, tc.output, out.String())\n\t\t\tif tc.err == nil {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !assert.Error(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, err.Error(), tc.err.Error())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/config/init/init.go",
    "content": "package init\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/ui\"\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/text/language\"\n)\n\nfunc queue(\n\ttasks ...func() error,\n) error {\n\tfor _, t := range tasks {\n\t\tif err := t(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// NewCmdInit executes and initialization of the config\nfunc NewCmdInit(f cmdutil.Factory) *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"init\",\n\t\tShort: \"Setups the CLI parameters and behavior\",\n\t\tLong: \"Setups the CLI parameters with tokens, default workspace, \" +\n\t\t\t\"user and behaviors\",\n\t\tArgs: cobra.ExactArgs(0),\n\t\tRunE: func(_ *cobra.Command, _ []string) error {\n\t\t\ti := f.UI()\n\t\t\tconfig := f.Config()\n\n\t\t\tapiURL := config.GetString(cmdutil.CONF_API_URL)\n\t\t\tif apiURL == \"\" {\n\t\t\t\tapiURL = api.BASE_URL\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tif apiURL, err = i.AskForText(\"Clockify API URL:\",\n\t\t\t\tui.WithDefault(apiURL),\n\t\t\t\tui.WithHelp(\"If you need a specific URL, you can find it at \"+\n\t\t\t\t\t\"https://clockify.me/help/getting-started/data-regions#data-residency-and-api, \"+\n\t\t\t\t\t\"or leave it empty for the default.\"),\n\t\t\t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif apiURL == api.BASE_URL {\n\t\t\t\tapiURL = \"\"\n\t\t\t}\n\t\t\tconfig.SetString(cmdutil.CONF_API_URL, apiURL)\n\n\t\t\ttoken := \"\"\n\t\t\tif token, err = i.AskForText(\"User Generated Token:\",\n\t\t\t\tui.WithDefault(config.GetString(cmdutil.CONF_TOKEN)),\n\t\t\t\tui.WithHelp(\"Can be generated at \"+\n\t\t\t\t\t\"https://app.clockify.me/manage-api-keys\"),\n\t\t\t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tconfig.SetString(cmdutil.CONF_TOKEN, token)\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := queue(\n\t\t\t\tfunc() error { return setWorkspace(c, config, i) },\n\t\t\t\tfunc() error { return setUser(c, config, i) },\n\t\t\t\tupdateFlag(\n\t\t\t\t\ti, config, cmdutil.CONF_ALLOW_NAME_FOR_ID,\n\t\t\t\t\t\"Should try to find projects/clients/users/tasks/tags by their names?\",\n\t\t\t\t),\n\t\t\t\tfunc() error {\n\t\t\t\t\tif !config.IsAllowNameForID() {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\treturn updateFlag(i, config,\n\t\t\t\t\t\tcmdutil.CONF_SEARCH_PROJECTS_WITH_CLIENT_NAME,\n\t\t\t\t\t\t`Should search projects looking into their `+\n\t\t\t\t\t\t\t`client's name too?`,\n\t\t\t\t\t)()\n\t\t\t\t},\n\t\t\t\tupdateFlag(i, config, cmdutil.CONF_INTERACTIVE,\n\t\t\t\t\t`Should use \"Interactive Mode\" by default?`,\n\t\t\t\t),\n\t\t\t\tupdateInt(i, config, cmdutil.CONF_INTERACTIVE_PAGE_SIZE,\n\t\t\t\t\t\"How many items should be shown when asking for \"+\n\t\t\t\t\t\t\"projects, tasks or tags?\"),\n\t\t\t\tfunc() error { return setWeekdays(config, i) },\n\t\t\t\tupdateFlag(i, config, cmdutil.CONF_ALLOW_INCOMPLETE,\n\t\t\t\t\t`Should allow starting time entries with incomplete data?`,\n\t\t\t\t),\n\t\t\t\tupdateFlag(i, config, cmdutil.CONF_SHOW_TASKS,\n\t\t\t\t\t`Should show task on time entries as a separated column?`,\n\t\t\t\t),\n\t\t\t\tupdateFlag(i, config, cmdutil.CONF_SHOW_CUSTOM_FIELDS,\n\t\t\t\t\t`Should show custom fields on time entries as a separated column?`,\n\t\t\t\t),\n\t\t\t\tupdateFlag(i, config, cmdutil.CONF_SHOW_CLIENT,\n\t\t\t\t\t`Should show client on time entries as a separated column?`,\n\t\t\t\t),\n\t\t\t\tupdateFlag(i, config, cmdutil.CONF_SHOW_TOTAL_DURATION,\n\t\t\t\t\t`Should show a line with the sum of `+\n\t\t\t\t\t\t`the time entries duration?`,\n\t\t\t\t),\n\t\t\t\tupdateFlag(i, config, cmdutil.CONF_DESCR_AUTOCOMP,\n\t\t\t\t\t`Allow description suggestions using `+\n\t\t\t\t\t\t`recent time entries' descriptions?`,\n\t\t\t\t),\n\t\t\t\tfunc() error {\n\t\t\t\t\tif !config.GetBool(cmdutil.CONF_DESCR_AUTOCOMP) {\n\t\t\t\t\t\tconfig.SetInt(cmdutil.CONF_DESCR_AUTOCOMP_DAYS, 0)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t\treturn updateInt(\n\t\t\t\t\t\ti, config, cmdutil.CONF_DESCR_AUTOCOMP_DAYS,\n\t\t\t\t\t\t`How many days should be used for a time entry to be `+\n\t\t\t\t\t\t\t`\"recent\"?`,\n\t\t\t\t\t)()\n\t\t\t\t},\n\t\t\t\tupdateFlag(i, config, cmdutil.CONF_ALLOW_ARCHIVED_TAGS,\n\t\t\t\t\t\"Should suggest and allow creating time entries \"+\n\t\t\t\t\t\t\"with archived tags?\",\n\t\t\t\t),\n\t\t\t\tsetLanguage(i, config),\n\t\t\t\tsetTimezone(i, config),\n\t\t\t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn config.Save()\n\t\t},\n\t}\n\n\treturn cmd\n}\n\nfunc setTimezone(i ui.UI, config cmdutil.Config) func() error {\n\treturn func() error {\n\t\ttzname, err := i.AskForValidText(\"What is your preferred timezone:\",\n\t\t\tfunc(s string) error {\n\t\t\t\t_, err := time.LoadLocation(s)\n\t\t\t\treturn err\n\t\t\t},\n\t\t\tui.WithHelp(\"Should be 'Local' to use the systems timezone, UTC \"+\n\t\t\t\t\"or valid TZ identifier from the IANA TZ database \"+\n\t\t\t\t\"https://en.wikipedia.org/wiki/List_of_tz_database_time_zones\"),\n\t\t\tui.WithDefault(config.TimeZone().String()),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ttz, _ := time.LoadLocation(tzname)\n\n\t\tconfig.SetTimeZone(tz)\n\n\t\treturn nil\n\t}\n}\n\nfunc setLanguage(i ui.UI, config cmdutil.Config) func() error {\n\treturn func() error {\n\t\tsuggestLanguages := []string{\n\t\t\tlanguage.English.String(),\n\t\t\tlanguage.German.String(),\n\t\t\tlanguage.Afrikaans.String(),\n\t\t\tlanguage.Chinese.String(),\n\t\t\tlanguage.Portuguese.String(),\n\t\t}\n\n\t\tlang, err := i.AskForValidText(\"What is your preferred language:\",\n\t\t\tfunc(s string) error {\n\t\t\t\t_, err := language.Parse(s)\n\t\t\t\treturn err\n\t\t\t},\n\t\t\tui.WithHelp(\"Accepts any IETF language tag \"+\n\t\t\t\t\"https://en.wikipedia.org/wiki/IETF_language_tag\"),\n\t\t\tui.WithSuggestion(func(toComplete string) []string {\n\t\t\t\treturn strhlp.Filter(\n\t\t\t\t\tstrhlp.IsSimilar(toComplete),\n\t\t\t\t\tsuggestLanguages,\n\t\t\t\t)\n\t\t\t}),\n\t\t\tui.WithDefault(config.Language().String()),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tconfig.SetLanguage(language.MustParse(lang))\n\t\treturn nil\n\t}\n}\n\nfunc setWeekdays(config cmdutil.Config, i ui.UI) (err error) {\n\tworkweekDays := config.GetStringSlice(cmdutil.CONF_WORKWEEK_DAYS)\n\tif workweekDays, err = i.AskManyFromOptions(\n\t\t\"Which days of the week do you work?\",\n\t\tcmdutil.GetWeekdays(),\n\t\tworkweekDays,\n\t\tnil,\n\t); err != nil {\n\t\treturn err\n\t}\n\tconfig.SetStringSlice(cmdutil.CONF_WORKWEEK_DAYS, workweekDays)\n\treturn nil\n}\n\nfunc setUser(c api.Client, config cmdutil.Config, i ui.UI) error {\n\tusers, err := c.WorkspaceUsers(api.WorkspaceUsersParam{\n\t\tWorkspace:       config.GetString(cmdutil.CONF_WORKSPACE),\n\t\tPaginationParam: api.AllPages(),\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuserID := config.GetString(cmdutil.CONF_USER_ID)\n\tdUser := \"\"\n\tusersString := make([]string, len(users))\n\tfor i := range users {\n\t\tusersString[i] = fmt.Sprintf(\"%s - %s\", users[i].ID, users[i].Name)\n\n\t\tif users[i].ID == userID {\n\t\t\tdUser = usersString[i]\n\t\t}\n\t}\n\n\tif userID, err = i.AskFromOptions(\n\t\t\"Choose your user:\", usersString, dUser); err != nil {\n\t\treturn err\n\t}\n\n\tconfig.SetString(cmdutil.CONF_USER_ID,\n\t\tstrings.TrimSpace(userID[0:strings.Index(userID, \" - \")]))\n\treturn nil\n}\n\nfunc setWorkspace(c api.Client, config cmdutil.Config, i ui.UI) error {\n\tws, err := c.GetWorkspaces(api.GetWorkspaces{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdWorkspace := \"\"\n\twsString := make([]string, len(ws))\n\tfor i := range ws {\n\t\twsString[i] = fmt.Sprintf(\"%s - %s\", ws[i].ID, ws[i].Name)\n\n\t\tif ws[i].ID == config.GetString(cmdutil.CONF_WORKSPACE) {\n\t\t\tdWorkspace = wsString[i]\n\t\t}\n\t}\n\n\tw := \"\"\n\tif w, err = i.AskFromOptions(\"Choose default Workspace:\",\n\t\twsString, dWorkspace); err != nil {\n\t\treturn err\n\t}\n\tconfig.SetString(cmdutil.CONF_WORKSPACE,\n\t\tstrings.TrimSpace(w[0:strings.Index(w, \" - \")]))\n\treturn err\n}\n\nfunc updateInt(ui ui.UI, config cmdutil.Config, param, desc string,\n) func() error {\n\treturn func() error {\n\t\tvalue := config.GetInt(param)\n\t\tvalue, err := ui.AskForInt(desc, value)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tconfig.SetInt(param, value)\n\t\treturn nil\n\t}\n}\n\nfunc updateFlag(\n\tui ui.UI, config cmdutil.Config, param, description string,\n) func() error {\n\treturn func() (err error) {\n\n\t\tb := config.GetBool(param)\n\t\tif b, err = ui.Confirm(description, b); err != nil {\n\t\t\treturn\n\t\t}\n\t\tconfig.SetBool(param, b)\n\t\treturn\n\t}\n\n}\n"
  },
  {
    "path": "pkg/cmd/config/init/init_test.go",
    "content": "package init_test\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/AlecAivazis/survey/v2/terminal\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/consoletest\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\tini \"github.com/lucassabreu/clockify-cli/pkg/cmd/config/init\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/ui\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"golang.org/x/text/language\"\n)\n\nfunc setStringFn(config *mocks.MockConfig, name, value string) *mock.Call {\n\tr := \"\"\n\tconfig.On(\"GetString\", name).\n\t\tReturn(func(string) string {\n\t\t\tv := r\n\t\t\tr = value\n\t\t\treturn v\n\t\t})\n\treturn config.On(\"SetString\", name, value)\n}\n\nfunc setBoolFn(config *mocks.MockConfig, name string, first, value bool) *mock.Call {\n\tr := first\n\tconfig.On(\"GetBool\", name).\n\t\tReturn(func(string) bool {\n\t\t\tv := r\n\t\t\tr = value\n\t\t\treturn v\n\t\t})\n\treturn config.On(\"SetBool\", name, value)\n}\n\nfunc TestInitCmd(t *testing.T) {\n\tconsoletest.RunTestConsole(t,\n\t\tfunc(out consoletest.FileWriter, in consoletest.FileReader) error {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tconfig := mocks.NewMockConfig(t)\n\t\t\tclient := mocks.NewMockClient(t)\n\n\t\t\tf.EXPECT().Config().Return(config)\n\t\t\tconfig.EXPECT().GetString(cmdutil.CONF_API_URL).Return(\"\")\n\t\t\tconfig.EXPECT().SetString(cmdutil.CONF_API_URL, \"\").Once()\n\t\t\tconfig.EXPECT().GetString(cmdutil.CONF_TOKEN).Return(\"\")\n\t\t\tconfig.EXPECT().SetString(cmdutil.CONF_TOKEN, \"new token\")\n\n\t\t\tf.EXPECT().Client().Return(client, nil)\n\n\t\t\tclient.EXPECT().GetWorkspaces(api.GetWorkspaces{}).\n\t\t\t\tReturn([]dto.Workspace{\n\t\t\t\t\t{ID: \"1\", Name: \"First\"},\n\t\t\t\t\t{ID: \"2\", Name: \"Second\"},\n\t\t\t\t}, nil)\n\n\t\t\tcall := setStringFn(config, cmdutil.CONF_WORKSPACE, \"2\")\n\n\t\t\tclient.EXPECT().WorkspaceUsers(api.WorkspaceUsersParam{\n\t\t\t\tWorkspace:       \"2\",\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t}).\n\t\t\t\tNotBefore(call).\n\t\t\t\tReturn([]dto.User{\n\t\t\t\t\t{ID: \"user-1\", Name: \"John Due\"},\n\t\t\t\t\t{ID: \"user-2\", Name: \"Joana D'ark\"},\n\t\t\t\t}, nil)\n\n\t\t\tsetStringFn(config, cmdutil.CONF_USER_ID, \"user-1\")\n\n\t\t\tsetBoolFn(config, cmdutil.CONF_ALLOW_NAME_FOR_ID, false, true).\n\t\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\t\tconfig.EXPECT().IsAllowNameForID().Return(true)\n\t\t\t\t})\n\t\t\tsetBoolFn(config, cmdutil.CONF_SEARCH_PROJECTS_WITH_CLIENT_NAME,\n\t\t\t\tfalse, true)\n\t\t\tsetBoolFn(config, cmdutil.CONF_INTERACTIVE, false, false)\n\n\t\t\tconfig.EXPECT().GetInt(cmdutil.CONF_INTERACTIVE_PAGE_SIZE).\n\t\t\t\tReturn(7)\n\t\t\tconfig.EXPECT().\n\t\t\t\tSetInt(cmdutil.CONF_INTERACTIVE_PAGE_SIZE, 10)\n\n\t\t\tconfig.EXPECT().GetStringSlice(cmdutil.CONF_WORKWEEK_DAYS).\n\t\t\t\tReturn([]string{})\n\t\t\tconfig.EXPECT().SetStringSlice(cmdutil.CONF_WORKWEEK_DAYS, []string{\n\t\t\t\tstrings.ToLower(time.Sunday.String()),\n\t\t\t\tstrings.ToLower(time.Tuesday.String()),\n\t\t\t\tstrings.ToLower(time.Thursday.String()),\n\t\t\t\tstrings.ToLower(time.Friday.String()),\n\t\t\t\tstrings.ToLower(time.Saturday.String()),\n\t\t\t})\n\n\t\t\tsetBoolFn(config, cmdutil.CONF_ALLOW_INCOMPLETE, false, false)\n\t\t\tsetBoolFn(config, cmdutil.CONF_SHOW_TASKS, true, true)\n\t\t\tsetBoolFn(config, cmdutil.CONF_SHOW_CUSTOM_FIELDS, true, true)\n\t\t\tsetBoolFn(config, cmdutil.CONF_SHOW_CLIENT, true, true)\n\t\t\tsetBoolFn(config, cmdutil.CONF_SHOW_TOTAL_DURATION, true, true)\n\t\t\tsetBoolFn(config, cmdutil.CONF_DESCR_AUTOCOMP, false, true)\n\n\t\t\tconfig.EXPECT().GetInt(cmdutil.CONF_DESCR_AUTOCOMP_DAYS).Return(0)\n\t\t\tconfig.EXPECT().SetInt(cmdutil.CONF_DESCR_AUTOCOMP_DAYS, 10)\n\n\t\t\tsetBoolFn(config, cmdutil.CONF_ALLOW_ARCHIVED_TAGS, true, false)\n\n\t\t\tconfig.EXPECT().Language().Return(language.English)\n\t\t\tconfig.EXPECT().SetLanguage(language.German)\n\n\t\t\tconfig.EXPECT().TimeZone().Return(time.Local)\n\t\t\tconfig.EXPECT().SetTimeZone(mock.Anything).\n\t\t\t\tRun(func(tz *time.Location) {\n\t\t\t\t\tassert.Equal(t, tz.String(), \"America/Bahia\")\n\t\t\t\t})\n\n\t\t\tconfig.EXPECT().Save().Once().Return(nil)\n\n\t\t\tf.EXPECT().UI().Return(ui.NewUI(in, out, out))\n\n\t\t\t_, err := ini.NewCmdInit(f).ExecuteC()\n\t\t\treturn err\n\t\t},\n\t\tfunc(c consoletest.ExpectConsole) {\n\t\t\tc.ExpectString(\"Clockify API URL:\")\n\t\t\tc.SendLine(\"\")\n\t\t\tc.ExpectString(\"https://api.clockify.me/api\")\n\n\t\t\tc.ExpectString(\"Token:\")\n\t\t\tc.SendLine(\"new token\")\n\t\t\tc.ExpectString(\"new token\")\n\n\t\t\tc.ExpectString(\"Choose default Workspace:\")\n\t\t\tc.ExpectString(\"First\")\n\t\t\tc.ExpectString(\"Second\")\n\t\t\tc.SendLine(\"sec\")\n\t\t\tc.ExpectString(\"Second\")\n\n\t\t\tc.ExpectString(\"Choose your user:\")\n\t\t\tc.ExpectString(\"John Due\")\n\t\t\tc.ExpectString(\"Joana\")\n\t\t\tc.SendLine(\"due\")\n\t\t\tc.ExpectString(\"John Due\")\n\n\t\t\tc.ExpectString(\"Should try to find\")\n\t\t\tc.ExpectString(\"by their names?\")\n\t\t\tc.SendLine(\"y\")\n\t\t\tc.ExpectString(\"Yes\")\n\n\t\t\tc.ExpectString(\"search projects looking into their client's name\")\n\t\t\tc.SendLine(\"y\")\n\t\t\tc.ExpectString(\"Yes\")\n\n\t\t\tc.ExpectString(\"Interactive Mode\\\" by default?\")\n\t\t\tc.SendLine(\"n\")\n\t\t\tc.ExpectString(\"No\")\n\n\t\t\tc.ExpectString(\"How many items should be shown when asking for \" +\n\t\t\t\t\"projects, tasks or tags?\")\n\t\t\tc.ExpectString(\"7\")\n\t\t\tc.SendLine(\"10\")\n\n\t\t\tc.ExpectString(\"Which days of the week do you work?\")\n\t\t\tc.ExpectString(\"sunday\")\n\t\t\tc.ExpectString(\"monday\")\n\t\t\tc.ExpectString(\"tuesday\")\n\t\t\tc.ExpectString(\"wednesday\")\n\t\t\tc.ExpectString(\"thursday\")\n\t\t\tc.ExpectString(\"friday\")\n\t\t\tc.ExpectString(\"saturday\")\n\n\t\t\tc.Send(string(terminal.KeySpace))\n\t\t\tc.Send(string(terminal.KeyArrowDown))\n\t\t\tc.Send(string(terminal.KeyArrowDown))\n\t\t\tc.Send(string(terminal.KeySpace))\n\t\t\tc.Send(string(terminal.KeyArrowDown))\n\t\t\tc.Send(string(terminal.KeyArrowDown))\n\t\t\tc.Send(string(terminal.KeyArrowDown))\n\t\t\tc.Send(string(terminal.KeySpace))\n\t\t\tc.Send(string(terminal.KeyArrowUp))\n\t\t\tc.Send(string(terminal.KeySpace))\n\t\t\tc.Send(\"sat\")\n\t\t\tc.Send(string(terminal.KeySpace))\n\t\t\tc.SendLine(\"\")\n\t\t\tc.ExpectString(\"sunday, tuesday, thursday, friday, saturday\")\n\n\t\t\tc.ExpectString(\"incomplete data?\")\n\t\t\tc.SendLine(\"\")\n\t\t\tc.ExpectString(\"No\")\n\n\t\t\tc.ExpectString(\"show task on time entries\")\n\t\t\tc.SendLine(\"\")\n\t\t\tc.ExpectString(\"Yes\")\n\n\t\t\tc.ExpectString(\"show custom fields\")\n\t\t\tc.SendLine(\"\")\n\t\t\tc.ExpectString(\"Yes\")\n\n\t\t\tc.ExpectString(\"show client on time entries\")\n\t\t\tc.SendLine(\"\")\n\t\t\tc.ExpectString(\"Yes\")\n\n\t\t\tc.ExpectString(\"sum of the time entries duration?\")\n\t\t\tc.SendLine(\"yes\")\n\t\t\tc.ExpectString(\"Yes\")\n\n\t\t\tc.ExpectString(\"descriptions?\")\n\t\t\tc.SendLine(\"YES\")\n\t\t\tc.ExpectString(\"Yes\")\n\n\t\t\tc.ExpectString(\"How many days\")\n\t\t\tc.SendLine(\"10\")\n\n\t\t\tc.ExpectString(\"archived tags?\")\n\t\t\tc.SendLine(\"n\")\n\t\t\tc.ExpectString(\"No\")\n\n\t\t\tc.ExpectString(\"preferred language\")\n\t\t\tc.Send(\"e\")\n\t\t\tc.Send(string(terminal.KeyTab))\n\t\t\tc.SendLine(string(terminal.KeyTab))\n\n\t\t\tc.ExpectString(\"preferred timezone:\")\n\t\t\tc.SendLine(\"America/Bahia\")\n\t\t\tc.ExpectString(\"America/Bahia\")\n\n\t\t\tc.ExpectEOF()\n\t\t})\n}\n\nfunc TestInitCmdCtrlC(t *testing.T) {\n\tconsoletest.RunTestConsole(t,\n\t\tfunc(out consoletest.FileWriter, in consoletest.FileReader) error {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tconfig := mocks.NewMockConfig(t)\n\n\t\t\tf.EXPECT().Config().Return(config)\n\t\t\tconfig.EXPECT().GetString(cmdutil.CONF_API_URL).Return(\"\")\n\n\t\t\tf.EXPECT().UI().Return(ui.NewUI(in, out, out))\n\n\t\t\t_, err := ini.NewCmdInit(f).ExecuteC()\n\t\t\tif !assert.Error(t, err) {\n\t\t\t\treturn errors.New(\"should have failed\")\n\t\t\t}\n\n\t\t\tassert.ErrorIs(t, err, terminal.InterruptErr)\n\t\t\treturn nil\n\t\t},\n\t\tfunc(c consoletest.ExpectConsole) {\n\t\t\tc.ExpectString(\"Clockify API URL:\")\n\t\t\tc.Send(string(terminal.KeyInterrupt))\n\n\t\t\tc.ExpectEOF()\n\t\t})\n}\n\nfunc TestInitCmdCtrlCAtToken(t *testing.T) {\n\tconsoletest.RunTestConsole(t,\n\t\tfunc(out consoletest.FileWriter, in consoletest.FileReader) error {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tconfig := mocks.NewMockConfig(t)\n\n\t\t\tf.EXPECT().Config().Return(config)\n\t\t\tconfig.EXPECT().GetString(cmdutil.CONF_API_URL).Return(\"\")\n\t\t\tconfig.EXPECT().SetString(cmdutil.CONF_API_URL, \"\").Once()\n\t\t\tconfig.EXPECT().GetString(cmdutil.CONF_TOKEN).Return(\"\")\n\n\t\t\tf.EXPECT().UI().Return(ui.NewUI(in, out, out))\n\n\t\t\t_, err := ini.NewCmdInit(f).ExecuteC()\n\t\t\tif !assert.Error(t, err) {\n\t\t\t\treturn errors.New(\"should have failed\")\n\t\t\t}\n\n\t\t\tassert.ErrorIs(t, err, terminal.InterruptErr)\n\t\t\treturn nil\n\t\t},\n\t\tfunc(c consoletest.ExpectConsole) {\n\t\t\tc.ExpectString(\"Clockify API URL:\")\n\t\t\tc.SendLine(\"\")\n\t\t\tc.ExpectString(\"https://api.clockify.me/api\")\n\n\t\t\tc.ExpectString(\"Token:\")\n\t\t\tc.Send(string(terminal.KeyInterrupt))\n\n\t\t\tc.ExpectEOF()\n\t\t})\n}\n"
  },
  {
    "path": "pkg/cmd/config/list/list.go",
    "content": "package list\n\nimport (\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/config/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdList creates the config list command\nfunc NewCmdList(f cmdutil.Factory) *cobra.Command {\n\tvar format string\n\tcmd := &cobra.Command{\n\t\tUse:     \"list\",\n\t\tAliases: []string{\"ls\"},\n\t\tShort:   \"List all parameters set by the user\",\n\t\tExample: heredoc.Doc(`\n\t\t\t$ clockify-cli config list\n\t\t\tallow-incomplete: false\n\t\t\tallow-name-for-id: true\n\t\t\tallow-project-name: true\n\t\t\tdebug: false\n\t\t\tdescription-autocomplete: true\n\t\t\tdescription-autocomplete-days: 15\n\t\t\tinteractive: true\n\t\t\tno-closing: false\n\t\t\tshow-task: false\n\t\t\tshow-custom-fields: false\n\t\t\tshow-total-duration: true\n\t\t\ttoken: Yamdas569\n\t\t\tuser:\n\t\t\t  id: ffffffffffffffffffffffff\n\t\t\tworkspace: eeeeeeeeeeeeeeeeeeeeeeee\n\t\t\tworkweek-days:\n\t\t\t- monday\n\t\t\t- tuesday\n\t\t\t- wednesday\n\t\t\t- thursday\n\t\t\t- friday\n\t\t`),\n\t\tArgs: cobra.NoArgs,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn util.Report(cmd.OutOrStdout(), format, f.Config().All())\n\t\t},\n\t}\n\n\t_ = util.AddReportFlags(cmd, &format)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/config/list/list_test.go",
    "content": "package list_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/config/list\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestListCmd(t *testing.T) {\n\ttts := []struct {\n\t\tname           string\n\t\targs           []string\n\t\tconfig         func(t *testing.T) cmdutil.Config\n\t\texpectedOutput string\n\t\terr            error\n\t}{\n\t\t{\n\t\t\tname:   \"no args\",\n\t\t\targs:   []string{\"param\"},\n\t\t\terr:    errors.New(`unknown command \"param\" for \"list\"`),\n\t\t\tconfig: func(t *testing.T) cmdutil.Config { return nil },\n\t\t},\n\t\t{\n\t\t\tname: \"default format\",\n\t\t\targs: []string{},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"All\").Once().Return(map[string]interface{}{\n\t\t\t\t\t\"token\": \"value\",\n\t\t\t\t\t\"user\":  map[string]string{\"id\": \"user.id\"},\n\t\t\t\t})\n\t\t\t\treturn c\n\t\t\t},\n\t\t\texpectedOutput: heredoc.Doc(`\n\t\t\ttoken: value\n\t\t\tuser:\n\t\t\t    id: user.id\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"json format\",\n\t\t\targs: []string{\"--format=json\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"All\").Once().Return(map[string]interface{}{\n\t\t\t\t\t\"token\": \"value\",\n\t\t\t\t\t\"user\":  map[string]string{\"id\": \"user.id\"},\n\t\t\t\t})\n\t\t\t\treturn c\n\t\t\t},\n\t\t\texpectedOutput: `{\"token\":\"value\",\"user\":{\"id\":\"user.id\"}}`,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid format\",\n\t\t\targs: []string{\"--format=tmol\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"All\").Once().Return(map[string]interface{}{})\n\t\t\t\treturn c\n\t\t\t},\n\t\t\terr: errors.New(\"invalid format\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tif c := tt.config(t); c != nil {\n\t\t\t\tf.On(\"Config\").Return(c)\n\t\t\t}\n\n\t\t\tcmd := list.NewCmdList(f)\n\t\t\tcmd.SilenceErrors = true\n\t\t\tcmd.SilenceUsage = true\n\n\t\t\tb := bytes.NewBufferString(\"\")\n\t\t\tcmd.SetOut(b)\n\t\t\tcmd.SetErr(b)\n\n\t\t\tcmd.SetArgs(tt.args)\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err != nil && assert.Error(t, err) {\n\t\t\t\tassert.EqualError(t, err, tt.err.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expectedOutput, b.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/config/set/set.go",
    "content": "package set\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/text/language\"\n)\n\n// NewCmdSet will update the value of one parameter\nfunc NewCmdSet(\n\tf cmdutil.Factory,\n\tvalidParameters cmdcompl.ValidArgsMap,\n) *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse: \"set <param> <value>\",\n\t\tArgs: cobra.MatchAll(\n\t\t\tcmdutil.RequiredNamedArgs(\"param\", \"value\"),\n\t\t\tcobra.ExactArgs(2),\n\t\t),\n\t\tValidArgs: validParameters.IntoValidArgs(),\n\t\tShort:     \"Changes the value of one parameter\",\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s token \"Yamdas569\"\n\t\t\t$ %[1]s workweek-days monday,tuesday,wednesday,thursday,friday\n\t\t\t$ %[1]s show-task true\n\t\t\t$ %[1]s show-custom-fields true\n\t\t\t$ %[1]s user.id 4564d5a6s4d54a5s4dasd5\n\t\t`, \"clockify-cli config set\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tparam := args[0]\n\t\t\tvalue := args[1]\n\t\t\tconfig := f.Config()\n\n\t\t\tswitch param {\n\t\t\tcase cmdutil.CONF_WORKWEEK_DAYS:\n\t\t\t\tws := strings.Split(strings.ToLower(value), \",\")\n\t\t\t\tws = strhlp.Filter(\n\t\t\t\t\tfunc(s string) bool {\n\t\t\t\t\t\treturn strhlp.Search(s, cmdutil.GetWeekdays()) != -1\n\t\t\t\t\t},\n\t\t\t\t\tws,\n\t\t\t\t)\n\t\t\t\tconfig.SetStringSlice(param, ws)\n\t\t\tcase cmdutil.CONF_LANGUAGE:\n\t\t\t\tlang, err := language.Parse(value)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\t\"%s is not a valid language: %w\", value, err)\n\t\t\t\t}\n\n\t\t\t\tconfig.SetLanguage(lang)\n\t\t\tcase cmdutil.CONF_TIMEZONE:\n\t\t\t\ttz, err := time.LoadLocation(value)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\t\"%s is not a valid timezone: %w\", value, err)\n\t\t\t\t}\n\n\t\t\t\tconfig.SetTimeZone(tz)\n\t\t\tdefault:\n\t\t\t\tconfig.SetString(param, value)\n\t\t\t}\n\n\t\t\treturn config.Save()\n\t\t},\n\t}\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/config/set/set_test.go",
    "content": "package set_test\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/config/set\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/text/language\"\n)\n\nfunc TestSetCmdArgs(t *testing.T) {\n\ttt := map[string][]string{\n\t\t\"zero\":  []string{},\n\t\t\"one\":   []string{\"param\"},\n\t\t\"three\": []string{\"param\", \"value\", \"other value\"},\n\t}\n\n\tfor name := range tt {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tcmd := set.NewCmdSet(\n\t\t\t\tmocks.NewMockFactory(t),\n\t\t\t\tcmdcompl.ValidArgsMap{},\n\t\t\t)\n\t\t\tb := bytes.NewBufferString(\"\")\n\t\t\tcmd.SetArgs(tt[name])\n\t\t\tcmd.SetErr(b)\n\t\t\tcmd.SetOut(b)\n\t\t\t_, err := cmd.ExecuteC()\n\n\t\t\tassert.Error(t, err)\n\t\t})\n\t}\n}\n\nfunc TestSetCmdRun(t *testing.T) {\n\tts := []struct {\n\t\tname   string\n\t\targs   []string\n\t\tconfig func(t *testing.T) cmdutil.Config\n\t}{\n\t\t{\n\t\t\tname: \"set token\",\n\t\t\targs: []string{\"token\", \"some value\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"SetString\", \"token\", \"some value\").Return(nil).Once()\n\t\t\t\tc.On(\"Save\").Once().Return(nil)\n\t\t\t\treturn c\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set weekdays\",\n\t\t\targs: []string{cmdutil.CONF_WORKWEEK_DAYS, \"SUNDAY,SATURDAY\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"SetStringSlice\", cmdutil.CONF_WORKWEEK_DAYS,\n\t\t\t\t\t[]string{\"sunday\", \"saturday\"}).\n\t\t\t\t\tReturn(nil).Once()\n\t\t\t\tc.On(\"Save\").Once().Return(nil)\n\t\t\t\treturn c\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set wrong weekdays\",\n\t\t\targs: []string{cmdutil.CONF_WORKWEEK_DAYS, \"monday,sunday,june\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"SetStringSlice\", cmdutil.CONF_WORKWEEK_DAYS,\n\t\t\t\t\t[]string{\"monday\", \"sunday\"}).\n\t\t\t\t\tReturn(nil).Once()\n\t\t\t\tc.On(\"Save\").Once().Return(nil)\n\t\t\t\treturn c\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set show client\",\n\t\t\targs: []string{cmdutil.CONF_SHOW_CLIENT, \"true\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.On(\"SetString\", cmdutil.CONF_SHOW_CLIENT,\n\t\t\t\t\t\"true\").\n\t\t\t\t\tReturn(nil).Once()\n\t\t\t\tc.On(\"Save\").Once().Return(nil)\n\t\t\t\treturn c\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set language\",\n\t\t\targs: []string{cmdutil.CONF_LANGUAGE, \"pt-br\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.EXPECT().SetLanguage(language.BrazilianPortuguese).Once()\n\t\t\t\tc.EXPECT().Save().Once().Return(nil)\n\t\t\t\treturn c\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set language (iso 639)\",\n\t\t\targs: []string{cmdutil.CONF_LANGUAGE, \"pt\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\tc.EXPECT().SetLanguage(language.Portuguese).Once()\n\t\t\t\tc.EXPECT().Save().Once().Return(nil)\n\t\t\t\treturn c\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set timezone\",\n\t\t\targs: []string{cmdutil.CONF_TIMEZONE, \"America/Sao_Paulo\"},\n\t\t\tconfig: func(t *testing.T) cmdutil.Config {\n\t\t\t\tc := mocks.NewMockConfig(t)\n\t\t\t\ttz, _ := time.LoadLocation(\"America/Sao_Paulo\")\n\t\t\t\tc.EXPECT().SetTimeZone(tz).Once()\n\t\t\t\tc.EXPECT().Save().Once().Return(nil)\n\t\t\t\treturn c\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range ts {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := tc.config(t)\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tf.On(\"Config\").Return(c)\n\t\t\tcmd := set.NewCmdSet(\n\t\t\t\tf,\n\t\t\t\tcmdcompl.ValidArgsMap{},\n\t\t\t)\n\t\t\tb := bytes.NewBufferString(\"\")\n\t\t\tcmd.SetArgs(tc.args)\n\t\t\tcmd.SetErr(b)\n\t\t\tcmd.SetOut(b)\n\t\t\t_, err := cmd.ExecuteC()\n\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestSetCmdShouldFail(t *testing.T) {\n\tts := []struct {\n\t\tname string\n\t\targs []string\n\t\terr  string\n\t}{\n\t\t{\n\t\t\tname: \"set language\",\n\t\t\targs: []string{cmdutil.CONF_LANGUAGE, \"klingon\"},\n\t\t\terr:  \"klingon is not a valid language.*\",\n\t\t},\n\t\t{\n\t\t\tname: \"set timezone\",\n\t\t\targs: []string{cmdutil.CONF_TIMEZONE, \"Murica\"},\n\t\t\terr:  \"Murica is not a valid timezone.*\",\n\t\t},\n\t\t{\n\t\t\tname: \"set timezone no caps\",\n\t\t\targs: []string{cmdutil.CONF_TIMEZONE, \"america/sao_paulo\"},\n\t\t\terr:  `america/sao_paulo is not a valid timezone`,\n\t\t},\n\t}\n\tfor _, tc := range ts {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tf.EXPECT().Config().Return(mocks.NewMockConfig(t))\n\t\t\tcmd := set.NewCmdSet(f, cmdcompl.ValidArgsMap{})\n\n\t\t\tb := bytes.NewBufferString(\"\")\n\t\t\tcmd.SetArgs(tc.args)\n\t\t\tcmd.SetErr(b)\n\t\t\tcmd.SetOut(b)\n\t\t\t_, err := cmd.ExecuteC()\n\n\t\t\tif !assert.Error(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Regexp(t, tc.err, err.Error())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/config/util/util.go",
    "content": "package util\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/spf13/cobra\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nconst FormatYAML = \"yaml\"\nconst FormatJSON = \"json\"\n\n// AddReportFlags adds the format flag\nfunc AddReportFlags(cmd *cobra.Command, format *string) error {\n\tcmd.Flags().StringVarP(format, \"format\", \"f\", FormatYAML, \"output format\")\n\treturn cmdcompl.AddFixedSuggestionsToFlag(cmd, \"format\",\n\t\tcmdcompl.ValidArgsSlide{FormatYAML, FormatJSON})\n}\n\n// Report prints the value as YAML or JSON\nfunc Report(out io.Writer, format string, v interface{}) error {\n\tformat = strings.ToLower(format)\n\tvar b []byte\n\tswitch format {\n\tcase FormatJSON:\n\t\tb, _ = json.Marshal(v)\n\tcase FormatYAML:\n\t\tb, _ = yaml.Marshal(v)\n\tdefault:\n\t\treturn errors.New(\"invalid format\")\n\t}\n\n\t_, err := out.Write(b)\n\treturn err\n}\n"
  },
  {
    "path": "pkg/cmd/project/add/add.go",
    "content": "package add\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"io\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/search\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdAdd represents the add command\nfunc NewCmdAdd(\n\tf cmdutil.Factory,\n\treport func(io.Writer, *util.OutputFlags, dto.Project) error,\n) *cobra.Command {\n\tof := util.OutputFlags{}\n\tp := api.AddProjectParam{}\n\trandomColor := false\n\tcmd := &cobra.Command{\n\t\tUse:     \"add\",\n\t\tAliases: []string{\"new\", \"create\"},\n\t\tShort:   \"Adds a project to the Clockify workspace\",\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s --name \"New One\"\n\t\t\t+--------------------------+---------+--------+\n\t\t\t|            ID            |  NAME   | CLIENT |\n\t\t\t+--------------------------+---------+--------+\n\t\t\t| 62a8b52d67f40258719037f2 | New One |        |\n\t\t\t+--------------------------+---------+--------+\n\n\t\t\t$ %[1]s --name=Other -q\n\t\t\t62a8b59067f40258719038fc\n\n\t\t\t$ %[1]s --name \"Other\" --client=\"Uber\" --csv --color=#fff\n\t\t\tid,name,client.id,client.name\n\t\t\t62a8b607027fe4592ef1520b,Other,62964b36bb48532a70730dbe,Uber Special\n\n\t\t\t$ %[1]s --name Other --random-color\n\t\t\tadd project: Other project for client Uber Special already exists. (code: 501)\n\n\t\t\t$ %[1]s --name \"Something\" --client=\"Uber\" --color=#fff\n\t\t\tthe following flags can't be used together: color and random-color\n\n\t\t\t$ %[1]s --name \"Something\" --client=\"Uber\"\n\t\t\tthe following flags can't be used together: color and random-color\n\n\t\t`, \"clockify-cli project add\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := cmdutil.XorFlag(map[string]bool{\n\t\t\t\t\"color\":        p.Color != \"\",\n\t\t\t\t\"random-color\": randomColor,\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tp.Workspace, err = f.GetWorkspaceID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif p.ClientId != \"\" && f.Config().IsAllowNameForID() {\n\t\t\t\tcs, err := search.GetClientsByName(\n\t\t\t\t\tc, p.Workspace, []string{p.ClientId})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tp.ClientId = cs[0]\n\t\t\t}\n\n\t\t\tif randomColor {\n\t\t\t\tbytes := make([]byte, 3)\n\t\t\t\tif _, err := rand.Read(bytes); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tp.Color = \"#\" + hex.EncodeToString(bytes)\n\t\t\t}\n\n\t\t\tproject, err := c.AddProject(p)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tout := cmd.OutOrStdout()\n\t\t\tif report != nil {\n\t\t\t\treturn report(out, &of, project)\n\t\t\t}\n\n\t\t\treturn util.ReportOne(project, out, of)\n\t\t},\n\t}\n\n\tcmd.Flags().StringVarP(&p.Name, \"name\", \"n\", \"\", \"name of the new project\")\n\t_ = cmd.MarkFlagRequired(\"name\")\n\n\tcmd.Flags().StringVarP(&p.Color, \"color\", \"c\", \"\",\n\t\t\"color of the new project\")\n\tcmd.Flags().BoolVar(&randomColor, \"random-color\", false,\n\t\t\"use a random color for the project\")\n\tcmd.Flags().StringVarP(&p.Note, \"note\", \"N\", \"\",\n\t\t\"note for the new project\")\n\tcmd.Flags().StringVar(&p.ClientId, \"client\", \"\",\n\t\t\"the id/name of the client the new project will go under\")\n\tcmd.Flags().BoolVarP(&p.Public, \"public\", \"p\", false,\n\t\t\"make the new project public\")\n\tcmd.Flags().BoolVarP(&p.Billable, \"billable\", \"b\", false,\n\t\t\"make the new project as billable\")\n\n\tutil.AddReportFlags(cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/project/add/add_test.go",
    "content": "package add_test\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/add\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n)\n\nfunc TestCmdAdd(t *testing.T) {\n\ttts := []struct {\n\t\tname    string\n\t\targs    []string\n\t\tfactory func(*testing.T) cmdutil.Factory\n\t\treport  func(*testing.T) func(\n\t\t\tio.Writer, *util.OutputFlags, dto.Project) error\n\t\terr string\n\t}{\n\t\t{\n\t\t\tname: \"only one format\",\n\t\t\targs: []string{\"--format={}\", \"-q\", \"-j\", \"-n=OK\"},\n\t\t\terr:  \"flags can't be used together.*format.*json.*quiet\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\treturn mocks.NewMockFactory(t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"random-color or color\",\n\t\t\targs: []string{\"--color=f00\", \"--random-color\", \"-n=OK\"},\n\t\t\terr:  \"flags can't be used together.*color.*random-color\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\treturn mocks.NewMockFactory(t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"name required\",\n\t\t\terr:  `\"name\" not set`,\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\treturn mocks.NewMockFactory(t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"client error\",\n\t\t\terr:  \"client error\",\n\t\t\targs: []string{\"-n=a\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(nil, errors.New(\"client error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workspace error\",\n\t\t\terr:  \"workspace error\",\n\t\t\targs: []string{\"-n=a\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"\", errors.New(\"workspace error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup client\",\n\t\t\terr:  \"no client\",\n\t\t\targs: []string{\"-n=error\", \"--client=rockr\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\n\t\t\t\tc.On(\"GetClients\", api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Client{}, errors.New(\"no client\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"http error\",\n\t\t\terr:  \"http error\",\n\t\t\targs: []string{\"-n=error\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tc.On(\"AddProject\", api.AddProjectParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tName:      \"error\",\n\t\t\t\t}).\n\t\t\t\t\tReturn(dto.Project{}, errors.New(\"http error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"add project\",\n\t\t\targs: []string{\n\t\t\t\t\"--name=Clockify\",\n\t\t\t\t\"--client=self\",\n\t\t\t\t\"--color=f00\",\n\t\t\t\t\"--note\", \"This one\",\n\t\t\t\t\"--public\",\n\t\t\t\t\"--billable\",\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\n\t\t\t\tc.On(\"GetClients\", api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Client{{ID: \"c-1\", Name: \"Myself\"}}, nil)\n\n\t\t\t\tc.On(\"AddProject\", api.AddProjectParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tName:      \"Clockify\",\n\t\t\t\t\tClientId:  \"c-1\",\n\t\t\t\t\tNote:      \"This one\",\n\t\t\t\t\tPublic:    true,\n\t\t\t\t\tBillable:  true,\n\t\t\t\t\tColor:     \"f00\",\n\t\t\t\t}).\n\t\t\t\t\tReturn(dto.Project{ID: \"project-id\"}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: func(t *testing.T) func(\n\t\t\t\tio.Writer, *util.OutputFlags, dto.Project) error {\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called) })\n\t\t\t\treturn func(\n\t\t\t\t\tw io.Writer, of *util.OutputFlags, p dto.Project) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, \"project-id\", p.ID)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"add with random color\",\n\t\t\targs: []string{\n\t\t\t\t\"--name=Clockify\",\n\t\t\t\t\"--client=c-id\",\n\t\t\t\t\"--random-color\",\n\t\t\t\t\"--note\", \"This one\",\n\t\t\t\t\"--public\",\n\t\t\t\t\"--billable\",\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(false)\n\n\t\t\t\tc.On(\"AddProject\", mock.AnythingOfType(\"api.AddProjectParam\")).\n\t\t\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\t\t\tp := args.Get(0).(api.AddProjectParam)\n\n\t\t\t\t\t\tassert.Equal(t, p.Workspace, \"w\")\n\t\t\t\t\t\tassert.Equal(t, p.Name, \"Clockify\")\n\t\t\t\t\t\tassert.Equal(t, p.ClientId, \"c-id\")\n\t\t\t\t\t\tassert.Equal(t, p.Note, \"This one\")\n\t\t\t\t\t\tassert.Equal(t, p.Public, true)\n\t\t\t\t\t\tassert.Equal(t, p.Billable, true)\n\t\t\t\t\t\tassert.Regexp(t,\n\t\t\t\t\t\t\tregexp.MustCompile(\"#[0-9a-f]{6}\"), p.Color)\n\t\t\t\t\t}).\n\t\t\t\t\tReturn(dto.Project{ID: \"project-id\"}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: func(t *testing.T) func(\n\t\t\t\tio.Writer, *util.OutputFlags, dto.Project) error {\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called) })\n\t\t\t\treturn func(\n\t\t\t\t\tw io.Writer, of *util.OutputFlags, p dto.Project) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, \"project-id\", p.ID)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := func(io.Writer, *util.OutputFlags, dto.Project) error {\n\t\t\t\tassert.Fail(t, \"failed\")\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif tt.report != nil {\n\t\t\t\tr = tt.report(t)\n\t\t\t}\n\n\t\t\tcmd := add.NewCmdAdd(tt.factory(t), r)\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n}\n\nfunc TestCmdAddReport(t *testing.T) {\n\tpr := dto.Project{Name: \"Coderockr\"}\n\ttts := []struct {\n\t\tname   string\n\t\targs   []string\n\t\tassert func(*testing.T, *util.OutputFlags, dto.Project)\n\t}{\n\t\t{\n\t\t\tname: \"report quiet\",\n\t\t\targs: []string{\"-q\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Project) {\n\t\t\t\tassert.True(t, of.Quiet)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report json\",\n\t\t\targs: []string{\"--json\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Project) {\n\t\t\t\tassert.True(t, of.JSON)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report format\",\n\t\t\targs: []string{\"--format={{.ID}}\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Project) {\n\t\t\t\tassert.Equal(t, \"{{.ID}}\", of.Format)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report csv\",\n\t\t\targs: []string{\"--csv\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Project) {\n\t\t\t\tassert.True(t, of.CSV)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report default\",\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Project) {\n\t\t\t\tassert.False(t, of.CSV)\n\t\t\t\tassert.False(t, of.JSON)\n\t\t\t\tassert.False(t, of.Quiet)\n\t\t\t\tassert.True(t, of.Format == \"\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\tReturn(\"w\", nil)\n\n\t\t\tc.On(\"AddProject\", api.AddProjectParam{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tName:      \"rockr\",\n\t\t\t}).\n\t\t\t\tReturn(pr, nil)\n\n\t\t\tcalled := false\n\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\tcmd := add.NewCmdAdd(f, func(\n\t\t\t\t_ io.Writer, of *util.OutputFlags, u dto.Project) error {\n\t\t\t\tcalled = true\n\t\t\t\tassert.Equal(t, pr, u)\n\t\t\t\ttt.assert(t, of, u)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(append(tt.args, \"-n=rockr\"))\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/project/edit/edit.go",
    "content": "package edit\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/search\"\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// NewCmdEdit updates a project\nfunc NewCmdEdit(\n\tf cmdutil.Factory,\n\treport func(io.Writer, *util.OutputFlags, []dto.Project) error,\n) *cobra.Command {\n\tof := util.OutputFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:     \"edit <project>...\",\n\t\tAliases: []string{\"update\"},\n\t\tShort:   \"Edit a project\",\n\t\tExample: heredoc.Docf(`\n\t\t\t# set a client form the project\n\t\t\t$ clockify-cli project edit cli --client Myself\n\t\t\t+--------------------------+--------------+--------------------------------+\n\t\t\t|            ID            |     NAME     |             CLIENT             |\n\t\t\t+--------------------------+--------------+--------------------------------+\n\t\t\t| 621948458cb9606d934ebb1c | Clockify Cli | Myself                         |\n\t\t\t|                          |              | (6202634a28782767054eec26)     |\n\t\t\t+--------------------------+--------------+--------------------------------+\n\n\t\t\t# remove client from a project\n\t\t\t$ clockify-cli project edit cli --no-client\n\t\t\t+--------------------------+--------------+--------+\n\t\t\t|            ID            |     NAME     | CLIENT |\n\t\t\t+--------------------------+--------------+--------+\n\t\t\t| 621948458cb9606d934ebb1c | Clockify Cli |        |\n\t\t\t+--------------------------+--------------+--------+\n\n\t\t\t# change name, color and make public\n\t\t\t$ clockify-cli project 62f19c254a912b05acc6d6cf \\\n\t\t\t\t--name First --public --color #0f0 \\\n\t\t\t\t--format \"{{.Name}} | {{.Public}} | {{.Color}}\"\n\t\t\tFirst | true | #00ff00\n\n\t\t\t# change to not billable, archived and leave a note\n\t\t\t$ clockify-cli project second --not-billable --archived \\\n\t\t\t\t--note \"$(cat notes.txt)\" \\\n\t\t\t\t--format 'n: {{.Name}}\\nb: {{.Billable}}\\na: {{.Archived}}\\nn:\\n{{ .Note }}'\n\t\t\tn: Noted\n\t\t\tb: false\n\t\t\ta: false\n\t\t\tn: one line\n\t\t\ttwo lines\n\t\t\tthree lines\n\n\t\t\t# archive multiple projects\n\t\t\t$ clockify-cli project first second \\\n\t\t\t\t--archived \\\n\t\t\t\t--format \"{{.Name}} | {{.Archived}}\"\n\t\t\tFirst | true\n\t\t\tSecond | true\n\t\t`),\n\t\tArgs: cmdutil.RequiredNamedArgs(\"project\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := cmdutil.XorFlagSet(\n\t\t\t\tcmd.Flags(), \"billable\", \"not-billable\"); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := cmdutil.XorFlagSet(\n\t\t\t\tcmd.Flags(), \"private\", \"public\"); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := cmdutil.XorFlagSet(\n\t\t\t\tcmd.Flags(), \"no-client\", \"client\"); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := cmdutil.XorFlagSet(\n\t\t\t\tcmd.Flags(), \"archived\", \"active\"); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif len(args) > 1 && cmd.Flags().Changed(\"name\") {\n\t\t\t\treturn errors.New(\n\t\t\t\t\t\"`--name` can't be changed for multiple projects\")\n\t\t\t}\n\n\t\t\tw, err := f.GetWorkspaceID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tids := strhlp.Unique(strhlp.Map(strings.TrimSpace, args))\n\t\t\tvar client *string\n\n\t\t\tif cmd.Flags().Changed(\"client\") {\n\t\t\t\tid, _ := cmd.Flags().GetString(\"client\")\n\t\t\t\tclient = &id\n\t\t\t} else if cmd.Flags().Changed(\"no-client\") {\n\t\t\t\tid := \"\"\n\t\t\t\tclient = &id\n\t\t\t}\n\n\t\t\tif f.Config().IsAllowNameForID() {\n\t\t\t\tif ids, err = search.GetProjectsByName(\n\t\t\t\t\tc, f.Config(), w, \"\", ids); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif client != nil && *client != \"\" {\n\t\t\t\t\tif *client, err = search.GetClientByName(\n\t\t\t\t\t\tc, w, *client); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tp := api.UpdateProjectParam{\n\t\t\t\tWorkspace: w,\n\t\t\t\tClientId:  client,\n\t\t\t}\n\n\t\t\tp.Name, _ = cmd.Flags().GetString(\"name\")\n\t\t\tp.Color, _ = cmd.Flags().GetString(\"color\")\n\n\t\t\tif cmd.Flags().Changed(\"note\") {\n\t\t\t\tn, _ := cmd.Flags().GetString(\"note\")\n\t\t\t\tp.Note = &n\n\t\t\t}\n\n\t\t\tif cmd.Flags().Changed(\"billable\") ||\n\t\t\t\tcmd.Flags().Changed(\"not-billable\") {\n\t\t\t\tb, _ := cmd.Flags().GetBool(\"billable\")\n\t\t\t\tp.Billable = &b\n\t\t\t}\n\n\t\t\tif cmd.Flags().Changed(\"archived\") ||\n\t\t\t\tcmd.Flags().Changed(\"active\") {\n\t\t\t\tb, _ := cmd.Flags().GetBool(\"archived\")\n\t\t\t\tp.Archived = &b\n\t\t\t}\n\n\t\t\tif cmd.Flags().Changed(\"public\") ||\n\t\t\t\tcmd.Flags().Changed(\"private\") {\n\t\t\t\tb, _ := cmd.Flags().GetBool(\"public\")\n\t\t\t\tp.Public = &b\n\t\t\t}\n\n\t\t\tvar g errgroup.Group\n\t\t\tprojects := make([]dto.Project, len(ids))\n\t\t\tfor i := 0; i < len(ids); i++ {\n\t\t\t\tj := i\n\t\t\t\tg.Go(func() error {\n\t\t\t\t\tcp := p\n\t\t\t\t\tcp.ProjectID = ids[j]\n\t\t\t\t\tprojects[j], err = c.UpdateProject(cp)\n\t\t\t\t\treturn err\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif err := g.Wait(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif report == nil {\n\t\t\t\treturn util.Report(projects, cmd.OutOrStdout(), of)\n\t\t\t}\n\n\t\t\treturn report(cmd.OutOrStdout(), &of, projects)\n\t\t},\n\t}\n\tcmd.Flags().StringP(\"name\", \"n\", \"\", \"name of the project\")\n\n\tcmd.Flags().StringP(\"color\", \"c\", \"\",\n\t\t\"color of the projects\")\n\tcmd.Flags().StringP(\"note\", \"N\", \"\",\n\t\t\"note for the projects\")\n\n\tcmd.Flags().String(\"client\", \"\",\n\t\t\"the id/name of the client the projects will go under\")\n\tcmd.Flags().Bool(\"no-client\", false,\n\t\t\"set projects as not having clients\")\n\n\tcmd.Flags().BoolP(\"public\", \"p\", false,\n\t\t\"set projects as public\")\n\tcmd.Flags().BoolP(\"private\", \"P\", false,\n\t\t\"set the projects as private\")\n\n\tcmd.Flags().BoolP(\"billable\", \"b\", false,\n\t\t\"set the projects as billable\")\n\tcmd.Flags().BoolP(\"not-billable\", \"B\", false,\n\t\t\"set the projects as not billable\")\n\n\tcmd.Flags().BoolP(\"archived\", \"A\", false,\n\t\t\"set projects as archived\")\n\tcmd.Flags().BoolP(\"active\", \"a\", false,\n\t\t\"set the projects as active\")\n\n\tutil.AddReportFlags(cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/project/edit/edit_test.go",
    "content": "package edit_test\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/edit\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype report func(io.Writer, *util.OutputFlags, []dto.Project) error\n\nfunc TestEditCmd(t *testing.T) {\n\ttts := []struct {\n\t\tname   string\n\t\targs   []string\n\t\terr    string\n\t\tparams func(*testing.T) (cmdutil.Factory, report)\n\t}{\n\t\t{\n\t\t\tname: \"project is required\",\n\t\t\terr:  \"requires arg project\",\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\treturn mocks.NewMockFactory(t), nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"can only change a project name\",\n\t\t\terr:  \"`--name` can't be changed for multiple projects\",\n\t\t\targs: []string{\"cli\", \"edit\", \"-n=wrong\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\treturn mocks.NewMockFactory(t), nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"only one format\",\n\t\t\targs: []string{\"--format={}\", \"-q\", \"-j\", \"cli\"},\n\t\t\terr:  \"flags can't be used together.*format.*json.*quiet\",\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\treturn mocks.NewMockFactory(t), nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"billable or not\",\n\t\t\targs: []string{\"--billable\", \"--not-billable\", \"cli\"},\n\t\t\terr:  \"flags can't be used together.*billable.*not-billable\",\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\treturn mocks.NewMockFactory(t), nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"active or archived\",\n\t\t\targs: []string{\"--active\", \"--archived\", \"cli\"},\n\t\t\terr:  \"flags can't be used together.*active.*archived\",\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\treturn mocks.NewMockFactory(t), nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"client and no client\",\n\t\t\targs: []string{\"--client=myself\", \"--no-client\", \"cli\"},\n\t\t\terr:  \"flags can't be used together.*client.*no-client\",\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\treturn mocks.NewMockFactory(t), nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"public or private\",\n\t\t\targs: []string{\"--private\", \"--public\", \"cli\"},\n\t\t\terr:  \"flags can't be used together.*private.*public\",\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\treturn mocks.NewMockFactory(t), nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workspace error\",\n\t\t\terr:  \"error\",\n\t\t\targs: []string{\"cli\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"\", errors.New(\"error\"))\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"client error\",\n\t\t\terr:  \"error\",\n\t\t\targs: []string{\"cli\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(nil, errors.New(\"error\"))\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup project error\",\n\t\t\terr:  \"No project with id or name\",\n\t\t\targs: []string{\"cli\", \"second\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := &mocks.SimpleConfig{AllowNameForID: true}\n\t\t\t\tf.On(\"Config\").Return(cf)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{}, nil)\n\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail to update second\",\n\t\t\targs: []string{\"cli\", \"second\",\n\t\t\t\t\"--public\",\n\t\t\t\t\"--billable\",\n\t\t\t\t\"--archived\",\n\t\t\t\t\"--no-client\"},\n\t\t\terr: \"error\",\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := &mocks.SimpleConfig{}\n\t\t\t\tf.On(\"Config\").Return(cf)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tb := true\n\t\t\t\tclient := \"\"\n\t\t\t\tc.On(\"UpdateProject\", api.UpdateProjectParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"cli\",\n\t\t\t\t\tClientId:  &client,\n\t\t\t\t\tPublic:    &b,\n\t\t\t\t\tBillable:  &b,\n\t\t\t\t\tArchived:  &b,\n\t\t\t\t}).Return(dto.Project{}, nil)\n\n\t\t\t\tc.On(\"UpdateProject\", api.UpdateProjectParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"second\",\n\t\t\t\t\tClientId:  &client,\n\t\t\t\t\tPublic:    &b,\n\t\t\t\t\tBillable:  &b,\n\t\t\t\t\tArchived:  &b,\n\t\t\t\t}).Return(dto.Project{}, errors.New(\"error\"))\n\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"update projects\",\n\t\t\targs: []string{\"cli\", \"second\",\n\t\t\t\t\"--private\",\n\t\t\t\t\"--not-billable\",\n\t\t\t\t\"--active\",\n\t\t\t\t\"--note=active, but not billable\",\n\t\t\t\t\"--client=myself\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := &mocks.SimpleConfig{AllowNameForID: true}\n\t\t\t\tf.On(\"Config\").Return(cf)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{\n\t\t\t\t\t{ID: \"p-1\", Name: \"Clockify CLI\"},\n\t\t\t\t\t{ID: \"p-2\", Name: \"Second\"},\n\t\t\t\t}, nil)\n\n\t\t\t\tc.On(\"GetClients\", api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Client{\n\t\t\t\t\t{ID: \"c-1\", Name: \"Myself\"},\n\t\t\t\t}, nil)\n\n\t\t\t\tb := false\n\t\t\t\tclient := \"c-1\"\n\t\t\t\tn := \"active, but not billable\"\n\t\t\t\tc.On(\"UpdateProject\", api.UpdateProjectParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"p-1\",\n\t\t\t\t\tClientId:  &client,\n\t\t\t\t\tPublic:    &b,\n\t\t\t\t\tBillable:  &b,\n\t\t\t\t\tArchived:  &b,\n\t\t\t\t\tNote:      &n,\n\t\t\t\t}).Return(dto.Project{ID: \"cli\"}, nil)\n\n\t\t\t\tc.On(\"UpdateProject\", api.UpdateProjectParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"p-2\",\n\t\t\t\t\tClientId:  &client,\n\t\t\t\t\tPublic:    &b,\n\t\t\t\t\tBillable:  &b,\n\t\t\t\t\tArchived:  &b,\n\t\t\t\t\tNote:      &n,\n\t\t\t\t}).Return(dto.Project{ID: \"edit\"}, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\t\treturn f, func(\n\t\t\t\t\tw io.Writer, of *util.OutputFlags, p []dto.Project) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Len(t, p, 2)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"change name and color\",\n\t\t\targs: []string{\"first\",\n\t\t\t\t\"--name=First Project\",\n\t\t\t\t\"--client=myself\",\n\t\t\t\t\"--color=0f0\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := &mocks.SimpleConfig{AllowNameForID: true}\n\t\t\t\tf.On(\"Config\").Return(cf)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{\n\t\t\t\t\t{ID: \"p-1\", Name: \"First\"},\n\t\t\t\t}, nil)\n\n\t\t\t\tc.On(\"GetClients\", api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Client{\n\t\t\t\t\t{ID: \"c-1\", Name: \"Myself\"},\n\t\t\t\t}, nil)\n\n\t\t\t\tclient := \"c-1\"\n\t\t\t\tc.On(\"UpdateProject\", api.UpdateProjectParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"p-1\",\n\t\t\t\t\tClientId:  &client,\n\t\t\t\t\tName:      \"First Project\",\n\t\t\t\t\tColor:     \"0f0\",\n\t\t\t\t}).Return(dto.Project{ID: \"first\"}, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\t\treturn f, func(\n\t\t\t\t\tw io.Writer, of *util.OutputFlags, p []dto.Project) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Len(t, p, 1)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf, r := tt.params(t)\n\t\t\tif r == nil {\n\t\t\t\tr = func(io.Writer, *util.OutputFlags, []dto.Project) error {\n\t\t\t\t\tt.Error(\"should not be called\")\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcmd := edit.NewCmdEdit(f, r)\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !assert.Error(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\n\t}\n}\n\nfunc TestEditCmdReport(t *testing.T) {\n\tpr := dto.Project{Name: \"Coderockr\"}\n\ttts := []struct {\n\t\tname   string\n\t\targs   []string\n\t\tassert func(*testing.T, *util.OutputFlags, []dto.Project)\n\t}{\n\t\t{\n\t\t\tname: \"report quiet\",\n\t\t\targs: []string{\"-q\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) {\n\t\t\t\tassert.True(t, of.Quiet)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report json\",\n\t\t\targs: []string{\"--json\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) {\n\t\t\t\tassert.True(t, of.JSON)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report format\",\n\t\t\targs: []string{\"--format={{.ID}}\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) {\n\t\t\t\tassert.Equal(t, \"{{.ID}}\", of.Format)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report csv\",\n\t\t\targs: []string{\"--csv\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) {\n\t\t\t\tassert.True(t, of.CSV)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report default\",\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) {\n\t\t\t\tassert.False(t, of.CSV)\n\t\t\t\tassert.False(t, of.JSON)\n\t\t\t\tassert.False(t, of.Quiet)\n\t\t\t\tassert.True(t, of.Format == \"\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\tReturn(\"w\", nil)\n\n\t\t\tcf := &mocks.SimpleConfig{AllowNameForID: false}\n\t\t\tf.On(\"Config\").Return(cf)\n\n\t\t\tc.On(\"UpdateProject\", api.UpdateProjectParam{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tProjectID: \"p-1\",\n\t\t\t\tName:      \"Myself\",\n\t\t\t}).\n\t\t\t\tReturn(pr, nil)\n\n\t\t\tcalled := false\n\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\tcmd := edit.NewCmdEdit(f, func(\n\t\t\t\t_ io.Writer, of *util.OutputFlags, u []dto.Project) error {\n\t\t\t\tcalled = true\n\t\t\t\tassert.Contains(t, u, pr)\n\t\t\t\ttt.assert(t, of, u)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(append(tt.args, \"-n=Myself\", \"p-1\"))\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/project/get/get.go",
    "content": "package get\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/search\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdGet looks for a project with the informed ID\nfunc NewCmdGet(\n\tf cmdutil.Factory,\n\treport func(io.Writer, *util.OutputFlags, dto.Project) error,\n) *cobra.Command {\n\tof := util.OutputFlags{}\n\tp := api.GetProjectParam{}\n\tcmd := &cobra.Command{\n\t\tUse:  \"get\",\n\t\tArgs: cmdutil.RequiredNamedArgs(\"project\"),\n\t\tValidArgsFunction: cmdcompl.CombineSuggestionsToArgs(\n\t\t\tcmdcomplutil.NewProjectAutoComplete(f, f.Config())),\n\t\tShort: \"Get a project on a Clockify workspace\",\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s 621948458cb9606d934ebb1c\n\t\t\t+--------------------------+-------------------+-----------------------------------------+\n\t\t\t|            ID            |       NAME        |                 CLIENT                  |\n\t\t\t+--------------------------+-------------------+-----------------------------------------+\n\t\t\t| 621948458cb9606d934ebb1c | Clockify Cli      | Special (6202634a28782767054eec26)      |\n\t\t\t+--------------------------+-------------------+-----------------------------------------+\n\n\t\t\t$ %[1]s cli -q\n\t\t\t621948458cb9606d934ebb1c\n\n\t\t\t$ %[1]s other --format '{{.Name}} - {{ .Color }} | {{ .ClientID }}'\n\t\t\tOther - #03A9F4 | 6202634a28782767054eec26\n\t\t`, \"clockify-cli project get\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) (err error) {\n\t\t\tif p.ProjectID = strings.TrimSpace(args[0]); p.ProjectID == \"\" {\n\t\t\t\treturn errors.New(\"project id should not be empty\")\n\t\t\t}\n\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif p.Workspace, err = f.GetWorkspaceID(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif f.Config().IsAllowNameForID() {\n\t\t\t\tif p.ProjectID, err = search.GetProjectByName(\n\t\t\t\t\tc, f.Config(), p.Workspace, p.ProjectID, \"\"); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tproject, err := c.GetProject(p)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif project == nil {\n\t\t\t\treturn api.EntityNotFound{\n\t\t\t\t\tEntityName: \"project\",\n\t\t\t\t\tID:         args[0],\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif report != nil {\n\t\t\t\treturn report(cmd.OutOrStdout(), &of, *project)\n\t\t\t}\n\n\t\t\treturn util.ReportOne(*project, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tcmd.Flags().BoolVarP(\n\t\t&p.Hydrate, \"hydrated\", \"H\", false,\n\t\t\"projects will have custom fields, tasks and memberships \"+\n\t\t\t\"filled for json and format outputs\")\n\n\tutil.AddReportFlags(cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/project/get/get_test.go",
    "content": "package get_test\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/get\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCmdGet(t *testing.T) {\n\tshouldCall := func(t *testing.T) func(\n\t\tio.Writer, *util.OutputFlags, dto.Project) error {\n\t\tcalled := false\n\t\tt.Cleanup(func() { assert.True(t, called) })\n\t\treturn func(w io.Writer, of *util.OutputFlags, p dto.Project) error {\n\t\t\tcalled = true\n\t\t\treturn nil\n\t\t}\n\t}\n\ttts := []struct {\n\t\tname    string\n\t\targs    []string\n\t\tfactory func(*testing.T) cmdutil.Factory\n\t\treport  func(*testing.T) func(\n\t\t\tio.Writer, *util.OutputFlags, dto.Project) error\n\t\terr string\n\t}{\n\t\t{\n\t\t\tname: \"only one format\",\n\t\t\targs: []string{\"--format={}\", \"-q\", \"-j\", \"p1\"},\n\t\t\terr:  \"flags can't be used together.*format.*json.*quiet\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workspace error\",\n\t\t\terr:  \"workspace error\",\n\t\t\targs: []string{\"p1\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"\", errors.New(\"workspace error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"client error\",\n\t\t\terr:  \"client error\",\n\t\t\targs: []string{\"p1\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.EXPECT().Client().Return(nil, errors.New(\"client error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"http error\",\n\t\t\terr:  \"http error\",\n\t\t\targs: []string{\"p1\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.EXPECT().Config().Return(cf)\n\t\t\t\tcf.EXPECT().IsAllowNameForID().Return(false)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tf.EXPECT().GetWorkspaceID().Return(\"w\", nil)\n\n\t\t\t\tc.EXPECT().GetProject(api.GetProjectParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"p1\",\n\t\t\t\t}).\n\t\t\t\t\tReturn(nil, errors.New(\"http error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"by id\",\n\t\t\targs: []string{\"p1\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.EXPECT().Config().Return(cf)\n\t\t\t\tcf.EXPECT().IsAllowNameForID().Return(false)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tc.EXPECT().GetProject(api.GetProjectParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"p1\",\n\t\t\t\t}).\n\t\t\t\t\tReturn(&dto.Project{}, nil)\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: shouldCall,\n\t\t},\n\t\t{\n\t\t\tname: \"by name\",\n\t\t\targs: []string{\n\t\t\t\t\"project\",\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.EXPECT().Config().Return(cf)\n\t\t\t\tcf.EXPECT().IsAllowNameForID().Return(true)\n\t\t\t\tcf.EXPECT().IsSearchProjectWithClientsName().Return(true)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{{Name: \"project\", ID: \"p1\"}}, nil)\n\n\t\t\t\tc.EXPECT().GetProject(api.GetProjectParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"p1\",\n\t\t\t\t}).\n\t\t\t\t\tReturn(&dto.Project{Name: \"project\", ID: \"p1\"}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: shouldCall,\n\t\t},\n\t\t{\n\t\t\tname: \"hydrated\",\n\t\t\targs: []string{\"-H\", \"project\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.EXPECT().Config().Return(cf)\n\t\t\t\tcf.EXPECT().IsAllowNameForID().Return(true)\n\t\t\t\tcf.EXPECT().IsSearchProjectWithClientsName().Return(true)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{{Name: \"project\", ID: \"p1\"}}, nil)\n\n\t\t\t\tc.EXPECT().GetProject(api.GetProjectParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"p1\",\n\t\t\t\t\tHydrate:   true,\n\t\t\t\t}).\n\t\t\t\t\tReturn(&dto.Project{\n\t\t\t\t\t\tName: \"project\", ID: \"p1\", Hydrated: true},\n\t\t\t\t\t\tnil)\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: shouldCall,\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := func(io.Writer, *util.OutputFlags, dto.Project) error {\n\t\t\t\tassert.Fail(t, \"failed\")\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif tt.report != nil {\n\t\t\t\tr = tt.report(t)\n\t\t\t}\n\n\t\t\tcmd := get.NewCmdGet(tt.factory(t), r)\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n}\n\nfunc TestCmdGetReport(t *testing.T) {\n\tpr := dto.Project{Name: \"Coderockr\"}\n\ttts := []struct {\n\t\tname   string\n\t\targs   []string\n\t\tassert func(*testing.T, *util.OutputFlags, dto.Project)\n\t}{\n\t\t{\n\t\t\tname: \"report quiet\",\n\t\t\targs: []string{\"-q\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Project) {\n\t\t\t\tassert.True(t, of.Quiet)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report json\",\n\t\t\targs: []string{\"--json\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Project) {\n\t\t\t\tassert.True(t, of.JSON)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report format\",\n\t\t\targs: []string{\"--format={{.ID}}\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Project) {\n\t\t\t\tassert.Equal(t, \"{{.ID}}\", of.Format)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report csv\",\n\t\t\targs: []string{\"--csv\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Project) {\n\t\t\t\tassert.True(t, of.CSV)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report default\",\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Project) {\n\t\t\t\tassert.False(t, of.CSV)\n\t\t\t\tassert.False(t, of.JSON)\n\t\t\t\tassert.False(t, of.Quiet)\n\t\t\t\tassert.True(t, of.Format == \"\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.EXPECT().Client().Return(c, nil)\n\t\t\tf.EXPECT().GetWorkspaceID().Return(\"w\", nil)\n\n\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\tf.EXPECT().Config().Return(cf)\n\t\t\tcf.EXPECT().IsAllowNameForID().Return(false)\n\n\t\t\tc.EXPECT().GetProject(api.GetProjectParam{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tProjectID: \"p1\",\n\t\t\t}).\n\t\t\t\tReturn(&pr, nil)\n\n\t\t\tcalled := false\n\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\tcmd := get.NewCmdGet(f, func(\n\t\t\t\t_ io.Writer, of *util.OutputFlags, u dto.Project) error {\n\t\t\t\tcalled = true\n\t\t\t\tassert.Equal(t, pr, u)\n\t\t\t\ttt.assert(t, of, u)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(append(tt.args, \"p1\"))\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/project/list/list.go",
    "content": "package list\n\nimport (\n\t\"io\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/search\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdList builds command to list projects\nfunc NewCmdList(\n\tf cmdutil.Factory,\n\treport func(io.Writer, *util.OutputFlags, []dto.Project) error,\n) *cobra.Command {\n\tof := util.OutputFlags{}\n\tp := api.GetProjectsParam{\n\t\tPaginationParam: api.AllPages(),\n\t}\n\tvar archived, notArchived bool\n\tcmd := &cobra.Command{\n\t\tUse:     \"list\",\n\t\tAliases: []string{\"ls\"},\n\t\tShort:   \"List projects on a Clockify workspace\",\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s\n\t\t\t+--------------------------+-------------------+-----------------------------------------+\n\t\t\t|            ID            |       NAME        |                 CLIENT                  |\n\t\t\t+--------------------------+-------------------+-----------------------------------------+\n\t\t\t| 621948458cb9606d934ebb1c | Clockify Cli      | Special (6202634a28782767054eec26)      |\n\t\t\t| 62a8b52d67f40258719037f2 | New One           |                                         |\n\t\t\t| 62a8b59067f40258719038fc | Other             |                                         |\n\t\t\t| 62a8b607027fe4592ef1520b | Other             | Uber Special (62964b36bb48532a70730dbe) |\n\t\t\t| 62894c3ed2df9d2867dc750b | Something Newer   | Special (6202634a28782767054eec26)      |\n\t\t\t+--------------------------+-------------------+-----------------------------------------+\n\n\t\t\t$ %[1]s --clients=uber\n\t\t\t+--------------------------+-------------------+-----------------------------------------+\n\t\t\t|            ID            |       NAME        |                 CLIENT                  |\n\t\t\t+--------------------------+-------------------+-----------------------------------------+\n\t\t\t| 62a8b607027fe4592ef1520b | Other             | Uber Special (62964b36bb48532a70730dbe) |\n\t\t\t+--------------------------+-------------------+-----------------------------------------+\n\n\t\t\t$ %[1]s --clients=uber --clients=special -q\n\t\t\t621948458cb9606d934ebb1c\n\t\t\t62a8b607027fe4592ef1520b\n\n\t\t\t$ %[1]s --name=other --format '{{.Name}} - {{ .Color }} | {{ .ClientID }}'\n\t\t\tOther - #607D8B | \n\t\t\tOther - #03A9F4 | 6202634a28782767054eec26\n\n\t\t\t$ %[1]s --archived\n\t\t\t+--------------------------+-------------------+-----------------------------------------+\n\t\t\t|            ID            |       NAME        |                 CLIENT                  |\n\t\t\t+--------------------------+-------------------+-----------------------------------------+\n\t\t\t| 62894c3ed2df9d2867dc750b | Something Newer   | Special (6202634a28782767054eec26)      |\n\t\t\t+--------------------------+-------------------+-----------------------------------------+\n\t\t`, \"clockify-cli project list\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) (err error) {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := cmdutil.XorFlag(map[string]bool{\n\t\t\t\t\"archived\":     archived,\n\t\t\t\t\"not-archived\": notArchived,\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif p.Workspace, err = f.GetWorkspaceID(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif len(p.Clients) > 0 && f.Config().IsAllowNameForID() {\n\t\t\t\tif p.Clients, err = search.GetClientsByName(\n\t\t\t\t\tc, p.Workspace, p.Clients); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif archived || notArchived {\n\t\t\t\tp.Archived = &archived\n\t\t\t}\n\n\t\t\tprojects, err := c.GetProjects(p)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif report != nil {\n\t\t\t\treturn report(cmd.OutOrStdout(), &of, projects)\n\t\t\t}\n\n\t\t\treturn util.Report(projects, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tcmd.Flags().StringVarP(&p.Name, \"name\", \"n\", \"\",\n\t\t\"will be used to filter the project by name\")\n\tcmd.Flags().StringSliceVarP(&p.Clients, \"clients\", \"c\", []string{},\n\t\t\"will be used to filter the project by client id/name\")\n\t_ = cmdcompl.AddSuggestionsToFlag(cmd, \"clients\",\n\t\tcmdcomplutil.NewClientAutoComplete(f))\n\n\tcmd.Flags().BoolVarP(\n\t\t&notArchived, \"not-archived\", \"\", false, \"list only active projects\")\n\tcmd.Flags().BoolVarP(\n\t\t&archived, \"archived\", \"\", false, \"list only archived projects\")\n\tcmd.Flags().BoolVarP(\n\t\t&p.Hydrate, \"hydrated\", \"H\", false,\n\t\t\"projects will have custom fields, tasks and memberships \"+\n\t\t\t\"filled for json and format outputs\")\n\n\tutil.AddReportFlags(cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/project/list/list_test.go",
    "content": "package list_test\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/list\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCmdList(t *testing.T) {\n\tshouldCall := func(t *testing.T) func(\n\t\tio.Writer, *util.OutputFlags, []dto.Project) error {\n\t\tcalled := false\n\t\tt.Cleanup(func() { assert.True(t, called) })\n\t\treturn func(w io.Writer, of *util.OutputFlags, p []dto.Project) error {\n\t\t\tcalled = true\n\t\t\treturn nil\n\t\t}\n\t}\n\ttts := []struct {\n\t\tname    string\n\t\targs    []string\n\t\tfactory func(*testing.T) cmdutil.Factory\n\t\treport  func(*testing.T) func(\n\t\t\tio.Writer, *util.OutputFlags, []dto.Project) error\n\t\terr string\n\t}{\n\t\t{\n\t\t\tname: \"only one format\",\n\t\t\targs: []string{\"--format={}\", \"-q\", \"-j\"},\n\t\t\terr:  \"flags can't be used together.*format.*json.*quiet\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\treturn mocks.NewMockFactory(t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"archived or not-archived\",\n\t\t\targs: []string{\"--archived\", \"--not-archived\"},\n\t\t\terr:  \"flags can't be used together.*archived.*not-archived\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\treturn mocks.NewMockFactory(t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workspace error\",\n\t\t\terr:  \"workspace error\",\n\t\t\targs: []string{\"-n=a\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"\", errors.New(\"workspace error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"client error\",\n\t\t\terr:  \"client error\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.EXPECT().Client().Return(nil, errors.New(\"client error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup client\",\n\t\t\terr:  \"no client\",\n\t\t\targs: []string{\"--clients=rockr\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\t\t\t\tf.EXPECT()\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.EXPECT().Config().Return(cf)\n\t\t\t\tcf.EXPECT().IsAllowNameForID().Return(true)\n\n\t\t\t\tc.EXPECT().GetClients(api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Client{}, errors.New(\"no client\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"client not found\",\n\t\t\terr:  \"No client with id or name containing 'other' was found\",\n\t\t\targs: []string{\"--clients=rockr\", \"--clients=other\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.EXPECT().Config().Return(cf)\n\t\t\t\tcf.EXPECT().IsAllowNameForID().Return(true)\n\n\t\t\t\tc.EXPECT().GetClients(api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Client{{ID: \"c1\", Name: \"Coderockr\"}}, nil)\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"http error\",\n\t\t\terr:  \"http error\",\n\t\t\targs: []string{\"-n=error\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tName:            \"error\",\n\t\t\t\t\tClients:         []string{},\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{}, errors.New(\"http error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"archived\",\n\t\t\targs: []string{\n\t\t\t\t\"--name=cli\",\n\t\t\t\t\"--clients=rockr\", \"--clients\", \"other\",\n\t\t\t\t\"--archived\",\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.EXPECT().Config().Return(cf)\n\t\t\t\tcf.EXPECT().IsAllowNameForID().Return(true)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\t\t\t\tc.EXPECT().GetClients(api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Client{\n\t\t\t\t\t\t{ID: \"c1\", Name: \"Coderockr\"},\n\t\t\t\t\t\t{ID: \"c2\", Name: \"Other\"},\n\t\t\t\t\t}, nil)\n\n\t\t\t\tb := true\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tName:            \"cli\",\n\t\t\t\t\tClients:         []string{\"c1\", \"c2\"},\n\t\t\t\t\tArchived:        &b,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{}, nil)\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: shouldCall,\n\t\t},\n\t\t{\n\t\t\tname: \"not archived\",\n\t\t\targs: []string{\n\t\t\t\t\"--name=cli\",\n\t\t\t\t\"--not-archived\",\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tb := false\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tName:            \"cli\",\n\t\t\t\t\tClients:         []string{},\n\t\t\t\t\tArchived:        &b,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{}, nil)\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: shouldCall,\n\t\t},\n\t\t{\n\t\t\tname: \"hydrated\",\n\t\t\targs: []string{\n\t\t\t\t\"--hydrated\",\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tClients:         []string{},\n\t\t\t\t\tHydrate:         true,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{}, nil)\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: shouldCall,\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := func(io.Writer, *util.OutputFlags, []dto.Project) error {\n\t\t\t\tassert.Fail(t, \"failed\")\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif tt.report != nil {\n\t\t\t\tr = tt.report(t)\n\t\t\t}\n\n\t\t\tcmd := list.NewCmdList(tt.factory(t), r)\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n}\n\nfunc TestCmdListReport(t *testing.T) {\n\tpr := []dto.Project{{Name: \"Coderockr\"}}\n\ttts := []struct {\n\t\tname   string\n\t\targs   []string\n\t\tassert func(*testing.T, *util.OutputFlags, []dto.Project)\n\t}{\n\t\t{\n\t\t\tname: \"report quiet\",\n\t\t\targs: []string{\"-q\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) {\n\t\t\t\tassert.True(t, of.Quiet)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report json\",\n\t\t\targs: []string{\"--json\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) {\n\t\t\t\tassert.True(t, of.JSON)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report format\",\n\t\t\targs: []string{\"--format={{.ID}}\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) {\n\t\t\t\tassert.Equal(t, \"{{.ID}}\", of.Format)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report csv\",\n\t\t\targs: []string{\"--csv\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) {\n\t\t\t\tassert.True(t, of.CSV)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report default\",\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Project) {\n\t\t\t\tassert.False(t, of.CSV)\n\t\t\t\tassert.False(t, of.JSON)\n\t\t\t\tassert.False(t, of.Quiet)\n\t\t\t\tassert.True(t, of.Format == \"\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.EXPECT().Client().Return(c, nil)\n\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\tReturn(\"w\", nil)\n\n\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\tWorkspace:       \"w\",\n\t\t\t\tClients:         []string{},\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t}).\n\t\t\t\tReturn(pr, nil)\n\n\t\t\tcalled := false\n\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\tcmd := list.NewCmdList(f, func(\n\t\t\t\t_ io.Writer, of *util.OutputFlags, u []dto.Project) error {\n\t\t\t\tcalled = true\n\t\t\t\tassert.Equal(t, pr, u)\n\t\t\t\ttt.assert(t, of, u)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/project/project.go",
    "content": "package project\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/add\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/edit\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/get\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project/list\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdProject represents the project command\nfunc NewCmdProject(f cmdutil.Factory) *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:     \"project\",\n\t\tAliases: []string{\"projects\"},\n\t\tShort:   \"Work with Clockify projects\",\n\t}\n\n\tcmd.AddCommand(list.NewCmdList(f, nil))\n\tcmd.AddCommand(get.NewCmdGet(f, nil))\n\tcmd.AddCommand(add.NewCmdAdd(f, nil))\n\tcmd.AddCommand(edit.NewCmdEdit(f, nil))\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/project/util/util.go",
    "content": "package util\n\nimport (\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/output/project\"\n\t\"github.com/spf13/cobra\"\n)\n\n// OutputFlags defines how to print the project\ntype OutputFlags struct {\n\tJSON   bool\n\tCSV    bool\n\tQuiet  bool\n\tFormat string\n}\n\nfunc (of OutputFlags) Check() error {\n\treturn cmdutil.XorFlag(map[string]bool{\n\t\t\"format\": of.Format != \"\",\n\t\t\"json\":   of.JSON,\n\t\t\"csv\":    of.CSV,\n\t\t\"quiet\":  of.Quiet,\n\t})\n}\n\n// AddReportFlags adds the common flags to print projects\nfunc AddReportFlags(cmd *cobra.Command, of *OutputFlags) {\n\tcmd.Flags().StringVarP(&of.Format, \"format\", \"f\", \"\",\n\t\t\"golang text/template format to be applied on each Project\")\n\tcmd.Flags().BoolVarP(&of.JSON, \"json\", \"j\", false, \"print as JSON\")\n\tcmd.Flags().BoolVarP(&of.CSV, \"csv\", \"v\", false, \"print as CSV\")\n\tcmd.Flags().BoolVarP(&of.Quiet, \"quiet\", \"q\", false, \"only display ids\")\n}\n\n// Report will print the projects as set by the flags\nfunc Report(list []dto.Project, out io.Writer, f OutputFlags) error {\n\tswitch {\n\tcase f.JSON:\n\t\treturn project.ProjectsJSONPrint(list, out)\n\tcase f.CSV:\n\t\treturn project.ProjectsCSVPrint(list, out)\n\tcase f.Quiet:\n\t\treturn project.ProjectPrintQuietly(list, out)\n\tcase f.Format != \"\":\n\t\treturn project.ProjectPrintWithTemplate(f.Format)(list, out)\n\tdefault:\n\t\treturn project.ProjectPrint(list, os.Stdout)\n\t}\n}\n\n// ReportOne will print a project as set by the flags\nfunc ReportOne(p dto.Project, out io.Writer, f OutputFlags) error {\n\tswitch {\n\tcase f.JSON:\n\t\treturn project.ProjectJSONPrint(p, out)\n\tdefault:\n\t\treturn Report([]dto.Project{p}, os.Stdout, f)\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/root.go",
    "content": "package cmd\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/client\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/completion\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/config\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/project\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/tag\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task\"\n\ttimeentry \"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/user\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/user/me\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/version\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/workspace\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdRoot creates the base command when called without any subcommands\nfunc NewCmdRoot(f cmdutil.Factory) *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:           \"clockify-cli\",\n\t\tShort:         \"Allow to integrate with Clockify through terminal\",\n\t\tSilenceErrors: true,\n\t\tSilenceUsage:  true,\n\t}\n\n\tcmd.PersistentFlags().StringP(\"token\", \"t\", \"\",\n\t\t\"clockify's token\\nCan be generated here: \"+\n\t\t\t\"https://clockify.me/user/settings#generateApiKeyBtn\")\n\n\tcmd.PersistentFlags().StringP(\"workspace\", \"w\", \"\", \"workspace to be used\")\n\t_ = cmdcompl.AddSuggestionsToFlag(cmd, \"workspace\",\n\t\tcmdcomplutil.NewWorspaceAutoComplete(f))\n\n\tcmd.PersistentFlags().StringP(\"user-id\", \"u\", \"\", \"user id from the token\")\n\t_ = cmdcompl.AddSuggestionsToFlag(cmd, \"user-id\",\n\t\tcmdcomplutil.NewUserAutoComplete(f))\n\n\tcmd.PersistentFlags().BoolP(\"interactive\", \"i\", false,\n\t\t\"will prompt you to confirm/complement commands input before \"+\n\t\t\t\"executing the action \")\n\n\tcmd.PersistentFlags().IntP(\"interactive-page-size\", \"L\", 7,\n\t\t\"will set how many items will be shown on interactive mode\")\n\n\tcmd.PersistentFlags().BoolP(\"allow-name-for-id\", \"\", false,\n\t\t\"allow use of project/client/tag's name when id is asked\")\n\n\tcmd.PersistentFlags().String(\n\t\t\"log-level\", cmdutil.LOG_LEVEL_NONE, \"set log level\")\n\t_ = cmdcompl.AddFixedSuggestionsToFlag(cmd, \"log-level\",\n\t\tcmdcompl.ValidArgsSlide{\n\t\t\tcmdutil.LOG_LEVEL_NONE,\n\t\t\tcmdutil.LOG_LEVEL_DEBUG,\n\t\t\tcmdutil.LOG_LEVEL_INFO,\n\t\t})\n\n\t_ = cmd.MarkFlagRequired(\"token\")\n\n\tcmd.AddCommand(version.NewCmdVersion(f))\n\n\tcmd.AddCommand(config.NewCmdConfig(f))\n\n\tcmd.AddCommand(workspace.NewCmdWorkspace(f))\n\n\tcmd.AddCommand(user.NewCmdUser(f, nil))\n\tcmd.AddCommand(me.NewCmdMe(f, nil))\n\n\tcmd.AddCommand(client.NewCmdClient(f))\n\tcmd.AddCommand(project.NewCmdProject(f))\n\tcmd.AddCommand(task.NewCmdTask(f))\n\n\tcmd.AddCommand(tag.NewCmdTag(f))\n\n\tcmd.AddCommand(timeentry.NewCmdTimeEntry(f)...)\n\n\tcmd.AddCommand(completion.NewCmdCompletion())\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/tag/tag.go",
    "content": "package tag\n\nimport (\n\t\"os\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\toutput \"github.com/lucassabreu/clockify-cli/pkg/output/tag\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdTag represents the tags command\nfunc NewCmdTag(f cmdutil.Factory) *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:     \"tag\",\n\t\tAliases: []string{\"tags\"},\n\t\tShort:   \"List tags on Clockify\",\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s\n\t\t\t+--------------------------+------------------+\n\t\t\t|            ID            |       NAME       |\n\t\t\t+--------------------------+------------------+\n\t\t\t| 62194867edaba27d0a45b464 | Code Review      |\n\t\t\t| 6219485e8cb9606d934ebb5f | Meeting          |\n\t\t\t| 621948708cb9606d934ebba7 | Pair Programming |\n\t\t\t| 6143b768195e5c503960a775 | Special Tag      |\n\t\t\t+--------------------------+------------------+\n\n\t\t\t$ %[1]s --name code -q\n\t\t\t62194867edaba27d0a45b464\n\n\t\t\t$ %[1]s --format \"{{.Name}}\" -archived\n\t\t\tArchived Tag\n\t\t`, \"clockify-cli tag\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tformat, _ := cmd.Flags().GetString(\"format\")\n\t\t\tquiet, _ := cmd.Flags().GetBool(\"quiet\")\n\t\t\tif err := cmdutil.XorFlag(map[string]bool{\n\t\t\t\t\"format\": format != \"\",\n\t\t\t\t\"quiet\":  quiet,\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tarchived, _ := cmd.Flags().GetBool(\"archived\")\n\t\t\tname, _ := cmd.Flags().GetString(\"name\")\n\n\t\t\ttags, err := getTags(f, name, archived)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tout := cmd.OutOrStdout()\n\t\t\tif format != \"\" {\n\t\t\t\treturn output.TagPrintWithTemplate(format)(tags, out)\n\t\t\t}\n\n\t\t\tif quiet {\n\t\t\t\treturn output.TagPrintQuietly(tags, out)\n\t\t\t}\n\n\t\t\treturn output.TagPrint(tags, os.Stdout)\n\t\t},\n\t}\n\n\tcmd.Flags().StringP(\"name\", \"n\", \"\",\n\t\t\"will be used to filter the tag by name\")\n\tcmd.Flags().StringP(\"format\", \"f\", \"\",\n\t\t\"golang text/template format to be applied on each Tag\")\n\tcmd.Flags().BoolP(\"quiet\", \"q\", false, \"only display ids\")\n\tcmd.Flags().BoolP(\"archived\", \"\", false, \"only display archived tags\")\n\n\treturn cmd\n}\n\nfunc getTags(f cmdutil.Factory, name string, archived bool) ([]dto.Tag, error) {\n\tc, err := f.Client()\n\tif err != nil {\n\t\treturn []dto.Tag{}, err\n\t}\n\n\tw, err := f.GetWorkspaceID()\n\tif err != nil {\n\t\treturn []dto.Tag{}, err\n\t}\n\n\treturn c.GetTags(api.GetTagsParam{\n\t\tWorkspace:       w,\n\t\tName:            name,\n\t\tArchived:        &archived,\n\t\tPaginationParam: api.AllPages(),\n\t})\n}\n"
  },
  {
    "path": "pkg/cmd/task/add/add.go",
    "content": "package add\n\nimport (\n\t\"io\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdAdd represents the add command\nfunc NewCmdAdd(\n\tf cmdutil.Factory,\n\treport func(io.Writer, *util.OutputFlags, dto.Task) error,\n) *cobra.Command {\n\tof := util.OutputFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:   \"add\",\n\t\tShort: \"Adds a new task to a project on Clockify\",\n\t\tLong: heredoc.Doc(`\n\t\t\tAdds a new active task to a project on Clockify, also allows to assign users to it at the same time\n\n\t\t\tTasks will be created as billable or not depending on the project settings.\n\t\t\tIf you set a estimate for the task, but the project is set as manual estimation, then it will have no effect on Clockify.\n\t\t`),\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s -p special --name=\"Very Important\"\n\t\t\t+--------------------------+----------------+--------+\n\t\t\t|            ID            |      NAME      | STATUS |\n\t\t\t+--------------------------+----------------+--------+\n\t\t\t| 62aa5d7049445270d7b979d6 | Very Important | ACTIVE |\n\t\t\t+--------------------------+----------------+--------+\n\n\t\t\t$ %[1]s -p special --name=\"Very Cool\" --assign john@example.com | \\\n\t\t\t  jq '.[] |.assigneeIds' --compact-output\n\t\t\t[\"dddddddddddddddddddddddd\"]\n\n\t\t\t$ %[1]s -p special --name Billable --billable --quiet\n\t\t\t62ab129e4ebb4f143c8e8622\n\n\t\t\t$ %[1]s -p special --name \"Not Billable\" --not-billable --csv\n\t\t\tid,name,status\n\t\t\t62ab145ec22de9759e6f6e36,Not Billable,ACTIVE\n\n\t\t\t$ %[1]s -p special --name 'With 1H to Make' --estimate 1\n\t\t\t+--------------------------+-----------------+--------+\n\t\t\t|            ID            |       NAME      | STATUS |\n\t\t\t+--------------------------+-----------------+--------+\n\t\t\t| 62aa5d7049445270d7b979d6 | With 1H to Make | ACTIVE |\n\t\t\t+--------------------------+-----------------+--------+\n\t\t`, \"clockify-cli task add\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfl, err := util.TaskReadFlags(cmd, f)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\ttask, err := c.AddTask(api.AddTaskParam{\n\t\t\t\tWorkspace:   fl.Workspace,\n\t\t\t\tProjectID:   fl.ProjectID,\n\t\t\t\tName:        fl.Name,\n\t\t\t\tEstimate:    fl.Estimate,\n\t\t\t\tAssigneeIDs: fl.AssigneeIDs,\n\t\t\t\tBillable:    fl.Billable,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif report != nil {\n\t\t\t\treturn report(cmd.OutOrStdout(), &of, task)\n\t\t\t}\n\n\t\t\treturn util.TaskReport(cmd, of, task)\n\t\t},\n\t}\n\n\tutil.TaskAddReportFlags(cmd, &of)\n\n\tutil.TaskAddPropFlags(cmd, f)\n\t_ = cmd.MarkFlagRequired(\"name\")\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/task/add/add_test.go",
    "content": "package add_test\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/add\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCmdAdd(t *testing.T) {\n\tshouldCall := func(t *testing.T) func(\n\t\tio.Writer, *util.OutputFlags, dto.Task) error {\n\t\tcalled := false\n\t\tt.Cleanup(func() { assert.True(t, called) })\n\t\treturn func(\n\t\t\tw io.Writer, of *util.OutputFlags, tk dto.Task) error {\n\t\t\tcalled = true\n\t\t\tassert.Equal(t, \"t-id\", tk.ID)\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tdFactory := func(t *testing.T) cmdutil.Factory {\n\t\tf := mocks.NewMockFactory(t)\n\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\treturn f\n\t}\n\n\ttts := []struct {\n\t\tname    string\n\t\targs    []string\n\t\tfactory func(*testing.T) cmdutil.Factory\n\t\treport  func(*testing.T) func(\n\t\t\tio.Writer, *util.OutputFlags, dto.Task) error\n\t\terr string\n\t}{\n\t\t{\n\t\t\tname:    \"only one format\",\n\t\t\targs:    []string{\"--format={}\", \"-q\", \"-j\", \"-n=OK\", \"-p=OK\"},\n\t\t\terr:     \"flags can't be used together.*format.*json.*quiet\",\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"billable or not\",\n\t\t\targs:    []string{\"--billable\", \"--not-billable\", \"-n=OK\", \"-p=OK\"},\n\t\t\terr:     \"flags can't be used together.*billable.*not-billable\",\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"assignee or no assignee\",\n\t\t\targs:    []string{\"--assignee=l\", \"--no-assignee\", \"-n=OK\", \"-p=OK\"},\n\t\t\terr:     \"flags can't be used together.*assignee.*no-assignee\",\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"name required\",\n\t\t\targs:    []string{\"-p=OK\"},\n\t\t\terr:     `\"name\" not set`,\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"project required\",\n\t\t\targs:    []string{\"-n=OK\"},\n\t\t\terr:     `\"project\" not set`,\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname: \"client error\",\n\t\t\terr:  \"client error\",\n\t\t\targs: []string{\"-n=a\", \"-p=b\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\n\t\t\t\tf.On(\"Client\").Return(nil, errors.New(\"client error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workspace error\",\n\t\t\terr:  \"workspace error\",\n\t\t\targs: []string{\"-n=a\", \"-p=b\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"\", errors.New(\"workspace error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup project\",\n\t\t\terr:  \"no project\",\n\t\t\targs: []string{\"-n=error\", \"-p=cli\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID: true,\n\t\t\t\t})\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{}, errors.New(\"no project\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup user\",\n\t\t\terr:  \"no user\",\n\t\t\targs: []string{\"-n=error\", \"-p=cli\", \"-A=who\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID: true,\n\t\t\t\t})\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{{Name: \"Cli\"}}, nil)\n\n\t\t\t\tc.On(\"WorkspaceUsers\", api.WorkspaceUsersParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.User{}, errors.New(\"no user\"))\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"http error\",\n\t\t\terr:  \"http error\",\n\t\t\targs: []string{\"-n=error\", \"-p=ok\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\n\t\t\t\tc.On(\"AddTask\", api.AddTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"ok\",\n\t\t\t\t\tName:      \"error\",\n\t\t\t\t}).\n\t\t\t\t\tReturn(dto.Task{}, errors.New(\"http error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"add billable task\",\n\t\t\targs: []string{\n\t\t\t\t\"--name=Add\",\n\t\t\t\t\"--project=cli\",\n\t\t\t\t\"--billable\",\n\t\t\t\t\"--estimate\", \"32\",\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID: true,\n\t\t\t\t})\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return(\n\t\t\t\t\t[]dto.Project{{ID: \"p-1\", Name: \"Clockify CLI\"}}, nil)\n\n\t\t\t\tb := true\n\t\t\t\te := time.Hour * 32\n\t\t\t\tc.On(\"AddTask\", api.AddTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tName:      \"Add\",\n\t\t\t\t\tProjectID: \"p-1\",\n\t\t\t\t\tBillable:  &b,\n\t\t\t\t\tEstimate:  &e,\n\t\t\t\t}).\n\t\t\t\t\tReturn(dto.Task{ID: \"t-id\"}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: shouldCall,\n\t\t},\n\t\t{\n\t\t\tname: \"add non-billable task\",\n\t\t\targs: []string{\n\t\t\t\t\"-n\", \"Add Task\",\n\t\t\t\t\"--project=p-1\",\n\t\t\t\t\"--assignee\", \"lucas\",\n\t\t\t\t\"--assignee=john\",\n\t\t\t\t\"--not-billable\",\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID: true,\n\t\t\t\t})\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return(\n\t\t\t\t\t[]dto.Project{{ID: \"p-1\", Name: \"Clockify CLI\"}}, nil)\n\n\t\t\t\tc.On(\"WorkspaceUsers\", api.WorkspaceUsersParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return(\n\t\t\t\t\t[]dto.User{\n\t\t\t\t\t\t{ID: \"u-1\", Name: \"Lucas Abreu\"},\n\t\t\t\t\t\t{ID: \"u-2\", Name: \"John Due\"},\n\t\t\t\t\t}, nil)\n\n\t\t\t\tb := false\n\t\t\t\tas := []string{\"u-1\", \"u-2\"}\n\t\t\t\tc.On(\"AddTask\", api.AddTaskParam{\n\t\t\t\t\tWorkspace:   \"w\",\n\t\t\t\t\tName:        \"Add Task\",\n\t\t\t\t\tProjectID:   \"p-1\",\n\t\t\t\t\tAssigneeIDs: &as,\n\t\t\t\t\tBillable:    &b,\n\t\t\t\t}).\n\t\t\t\t\tReturn(dto.Task{ID: \"t-id\"}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: shouldCall,\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := func(io.Writer, *util.OutputFlags, dto.Task) error {\n\t\t\t\tassert.Fail(t, \"failed\")\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif tt.report != nil {\n\t\t\t\tr = tt.report(t)\n\t\t\t}\n\n\t\t\tcmd := add.NewCmdAdd(tt.factory(t), r)\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n}\n\nfunc TestCmdAddReport(t *testing.T) {\n\tpr := dto.Task{Name: \"Coderockr\"}\n\ttts := []struct {\n\t\tname   string\n\t\targs   []string\n\t\tassert func(*testing.T, *util.OutputFlags, dto.Task)\n\t}{\n\t\t{\n\t\t\tname: \"report quiet\",\n\t\t\targs: []string{\"-q\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Task) {\n\t\t\t\tassert.True(t, of.Quiet)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report json\",\n\t\t\targs: []string{\"--json\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Task) {\n\t\t\t\tassert.True(t, of.JSON)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report format\",\n\t\t\targs: []string{\"--format={{.ID}}\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Task) {\n\t\t\t\tassert.Equal(t, \"{{.ID}}\", of.Format)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report csv\",\n\t\t\targs: []string{\"--csv\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) {\n\t\t\t\tassert.True(t, of.CSV)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report default\",\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) {\n\t\t\t\tassert.False(t, of.CSV)\n\t\t\t\tassert.False(t, of.JSON)\n\t\t\t\tassert.False(t, of.Quiet)\n\t\t\t\tassert.True(t, of.Format == \"\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\tReturn(\"w\", nil)\n\n\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\tf.On(\"Config\").Return(cf)\n\n\t\t\tcf.On(\"IsAllowNameForID\").Return(false)\n\n\t\t\tc.On(\"AddTask\", api.AddTaskParam{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tProjectID: \"p-1\",\n\t\t\t\tName:      \"Task Add\",\n\t\t\t}).\n\t\t\t\tReturn(pr, nil)\n\n\t\t\tcalled := false\n\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\tcmd := add.NewCmdAdd(f, func(\n\t\t\t\t_ io.Writer, of *util.OutputFlags, u dto.Task) error {\n\t\t\t\tcalled = true\n\t\t\t\tassert.Equal(t, pr, u)\n\t\t\t\ttt.assert(t, of, u)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(append(tt.args, \"-n\", \"Task Add\", \"-p=p-1\"))\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/task/delete/delete.go",
    "content": "package del\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/search\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdDelete represents the close command\nfunc NewCmdDelete(\n\tf cmdutil.Factory,\n\treport func(io.Writer, *util.OutputFlags, dto.Task) error,\n) *cobra.Command {\n\tof := util.OutputFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:     \"delete <task>\",\n\t\tAliases: []string{\"remove\", \"rm\", \"del\"},\n\t\tArgs:    cmdutil.RequiredNamedArgs(\"task\"),\n\t\tValidArgsFunction: cmdcompl.CombineSuggestionsToArgs(\n\t\t\tcmdcomplutil.NewTaskAutoComplete(f, false)),\n\t\tShort: \"Deletes a task from a project on Clockify\",\n\t\tLong: heredoc.Doc(`\n\t\t\tDeletes a task from a project on Clockify\n\t\t\tThis action can't be reverted, and all time entries using this task will revert to not having one\n\t\t`),\n\t\tExample: heredoc.Doc(`\n\t\t\t$ clockify-cli task delete -p \"special\" very\n\t\t\t+--------------------------+----------------+--------+\n\t\t\t|            ID            |      NAME      | STATUS |\n\t\t\t+--------------------------+----------------+--------+\n\t\t\t| 62aa5d7049445270d7b979d6 | Very Important | ACTIVE |\n\t\t\t+--------------------------+----------------+--------+\n\n\t\t\t$ clockify-cli task delete -p \"special\" 62aa4eed49445270d7b9666c\n\t\t\t+--------------------------+----------+--------+\n\t\t\t|            ID            |   NAME   | STATUS |\n\t\t\t+--------------------------+----------+--------+\n\t\t\t| 62aa4eed49445270d7b9666c | Inactive | DONE   |\n\t\t\t+--------------------------+----------+--------+\n\n\t\t\t$ clockify-cli task delete -p \"special\" 62aa4eed49445270d7b9666c\n\t\t\tNo task with id or name containing '62aa4eed49445270d7b9666c' was found\n\t\t`),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tproject, _ := cmd.Flags().GetString(\"project\")\n\t\t\ttask := strings.TrimSpace(args[0])\n\t\t\tif project == \"\" || task == \"\" {\n\t\t\t\treturn errors.New(\"project and task id should not be empty\")\n\t\t\t}\n\n\t\t\tw, err := f.GetWorkspaceID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif f.Config().IsAllowNameForID() {\n\t\t\t\tif project, err = search.GetProjectByName(\n\t\t\t\t\tc, f.Config(), w, project, \"\"); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif task, err = search.GetTaskByName(\n\t\t\t\t\tc,\n\t\t\t\t\tapi.GetTasksParam{Workspace: w, ProjectID: project},\n\t\t\t\t\ttask,\n\t\t\t\t); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tt, err := c.DeleteTask(api.DeleteTaskParam{\n\t\t\t\tWorkspace: w,\n\t\t\t\tProjectID: project,\n\t\t\t\tTaskID:    task,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif report == nil {\n\t\t\t\treturn util.TaskReport(cmd, of, t)\n\t\t\t}\n\n\t\t\treturn report(cmd.OutOrStdout(), &of, t)\n\t\t},\n\t}\n\n\tcmdutil.AddProjectFlags(cmd, f)\n\tutil.TaskAddReportFlags(cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/task/delete/delete_test.go",
    "content": "package del_test\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\tdel \"github.com/lucassabreu/clockify-cli/pkg/cmd/task/delete\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype report func(io.Writer, *util.OutputFlags, dto.Task) error\n\nfunc TestCmdDelete(t *testing.T) {\n\ttts := []struct {\n\t\tname   string\n\t\terr    string\n\t\targs   []string\n\t\tparams func(*testing.T) (\n\t\t\tcmdutil.Factory,\n\t\t\treport,\n\t\t)\n\t}{\n\t\t{\n\t\t\tname: \"task is required\",\n\t\t\targs: []string{},\n\t\t\terr:  \"requires arg task\",\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"project is required\",\n\t\t\terr:  \"flag.*project.*not set\",\n\t\t\targs: []string{\"task-id\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workspace error\",\n\t\t\terr:  \"w error\",\n\t\t\targs: []string{\"task-id\", \"-p\", \"p-1\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"\", errors.New(\"w error\"))\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"client error\",\n\t\t\terr:  \"c error\",\n\t\t\targs: []string{\"task-id\", \"-p\", \"p-1\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(nil, errors.New(\"c error\"))\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"project lookup error\",\n\t\t\terr:  \"No project with id or name containing.*p-1\",\n\t\t\targs: []string{\"task-id\", \"-p\", \"p-1\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\t\t\t\tcf.On(\"IsSearchProjectWithClientsName\").Return(true)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{}, nil)\n\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"task lookup error\",\n\t\t\terr:  \"No task with id or name containing.*task-id\",\n\t\t\targs: []string{\"task-id\", \"-p\", \"p-1\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\t\t\t\tcf.On(\"IsSearchProjectWithClientsName\").Return(true)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{{ID: \"p-1\"}}, nil)\n\n\t\t\t\tc.On(\"GetTasks\", api.GetTasksParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tProjectID:       \"p-1\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Task{}, nil)\n\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"task delete error\",\n\t\t\terr:  \"http error\",\n\t\t\targs: []string{\"delete\", \"-p\", \"p-1\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\t\t\t\tcf.On(\"IsSearchProjectWithClientsName\").Return(true)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{{ID: \"p-1\"}}, nil)\n\n\t\t\t\tte := dto.Task{\n\t\t\t\t\tID:        \"task-id\",\n\t\t\t\t\tName:      \"Delete Task\",\n\t\t\t\t\tProjectID: \"p-1\",\n\t\t\t\t}\n\t\t\t\tc.On(\"GetTasks\", api.GetTasksParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tProjectID:       \"p-1\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Task{te}, nil)\n\n\t\t\t\tc.On(\"DeleteTask\", api.DeleteTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"p-1\",\n\t\t\t\t\tTaskID:    \"task-id\",\n\t\t\t\t}).Return(te, errors.New(\"http error\"))\n\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"task delete \",\n\t\t\targs: []string{\"delete\", \"-p\", \"p-1\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\t\t\t\tcf.On(\"IsSearchProjectWithClientsName\").Return(true)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{{ID: \"p-1\"}}, nil)\n\n\t\t\t\tte := dto.Task{\n\t\t\t\t\tID:        \"task-id\",\n\t\t\t\t\tName:      \"Delete Task\",\n\t\t\t\t\tProjectID: \"p-1\",\n\t\t\t\t}\n\t\t\t\tc.On(\"GetTasks\", api.GetTasksParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tProjectID:       \"p-1\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Task{te}, nil)\n\n\t\t\t\tc.On(\"DeleteTask\", api.DeleteTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"p-1\",\n\t\t\t\t\tTaskID:    \"task-id\",\n\t\t\t\t}).Return(te, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called) })\n\t\t\t\treturn f, func(\n\t\t\t\t\t_ io.Writer, _ *util.OutputFlags, tr dto.Task) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, te, tr)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf, r := tt.params(t)\n\t\t\tif r == nil {\n\t\t\t\tr = func(w io.Writer, of *util.OutputFlags, _ dto.Task) error {\n\t\t\t\t\tt.Error(\"should not be called\")\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t\tcmd := del.NewCmdDelete(f, r)\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n}\n\nfunc TestCmdDeleteReport(t *testing.T) {\n\tpr := dto.Task{Name: \"Coderockr\"}\n\ttts := []struct {\n\t\tname   string\n\t\targs   []string\n\t\tassert func(*testing.T, *util.OutputFlags, dto.Task)\n\t}{\n\t\t{\n\t\t\tname: \"report quiet\",\n\t\t\targs: []string{\"-q\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Task) {\n\t\t\t\tassert.True(t, of.Quiet)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report json\",\n\t\t\targs: []string{\"--json\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Task) {\n\t\t\t\tassert.True(t, of.JSON)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report format\",\n\t\t\targs: []string{\"--format={{.ID}}\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Task) {\n\t\t\t\tassert.Equal(t, \"{{.ID}}\", of.Format)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report csv\",\n\t\t\targs: []string{\"--csv\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) {\n\t\t\t\tassert.True(t, of.CSV)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report default\",\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) {\n\t\t\t\tassert.False(t, of.CSV)\n\t\t\t\tassert.False(t, of.JSON)\n\t\t\t\tassert.False(t, of.Quiet)\n\t\t\t\tassert.True(t, of.Format == \"\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\tReturn(\"w\", nil)\n\n\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\tf.On(\"Config\").Return(cf)\n\n\t\t\tcf.On(\"IsAllowNameForID\").Return(false)\n\n\t\t\tc.On(\"DeleteTask\", api.DeleteTaskParam{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tProjectID: \"p-1\",\n\t\t\t\tTaskID:    \"t-1\",\n\t\t\t}).\n\t\t\t\tReturn(pr, nil)\n\n\t\t\tcalled := false\n\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\tcmd := del.NewCmdDelete(f, func(\n\t\t\t\t_ io.Writer, of *util.OutputFlags, u dto.Task) error {\n\t\t\t\tcalled = true\n\t\t\t\tassert.Equal(t, pr, u)\n\t\t\t\ttt.assert(t, of, u)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(append(tt.args, \"t-1\", \"-p=p-1\"))\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/task/done/done.go",
    "content": "package done\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/search\"\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// NewCmdDone represents the close command\nfunc NewCmdDone(\n\tf cmdutil.Factory,\n\treport func(io.Writer, *util.OutputFlags, []dto.Task) error,\n) *cobra.Command {\n\tof := util.OutputFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:     \"done <task>...\",\n\t\tAliases: []string{\"mark-as-done\", \"end\"},\n\t\tArgs:    cmdutil.RequiredNamedArgs(\"task\"),\n\t\tValidArgsFunction: cmdcompl.CombineSuggestionsToArgs(\n\t\t\tcmdcomplutil.NewTaskAutoComplete(f, true)),\n\t\tShort: \"Edits a task  to done\",\n\t\tLong:  \"Edits a task to done, similar to doing `task edit <task> --done`\",\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s ls\n\t\t\t+--------------------------+--------+--------+\n\t\t\t|            ID            |  NAME  | STATUS |\n\t\t\t+--------------------------+--------+--------+\n\t\t\t| 62adfcc8c22de9759e739d66 | Five   | ACTIVE |\n\t\t\t| 62adfcc4c22de9759e739d64 | Four   | ACTIVE |\n\t\t\t| 62adfcb649445270d7becfca | Three  | ACTIVE |\n\t\t\t| 62adfcb149445270d7becfc8 | Second | ACTIVE |\n\t\t\t| 62adfcaa4ebb4f143c92bf8b | First  | ACTIVE |\n\t\t\t+--------------------------+--------+--------+\n\n\t\t\t$ %[1]s done first second 62adfcb649445270d7becfca\n\t\t\t+--------------------------+--------+--------+\n\t\t\t|            ID            |  NAME  | STATUS |\n\t\t\t+--------------------------+--------+--------+\n\t\t\t| 62adfcaa4ebb4f143c92bf8b | First  | DONE   |\n\t\t\t| 62adfcb149445270d7becfc8 | Second | DONE   |\n\t\t\t| 62adfcb649445270d7becfca | Three  | DONE   |\n\t\t\t+--------------------------+--------+--------+\n\n\t\t\t$ %[1]s done four\n\t\t\tid,name,status\n\t\t\t62adfcc4c22de9759e739d64,Four,DONE\n\n\t\t\t$ %[1]s done five\n\t\t\tNo active task with id or name containing 'five' was found\n\t\t`, \"clockify-cli task -p cli\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tproject, _ := cmd.Flags().GetString(\"project\")\n\t\t\tproject = strings.TrimSpace(project)\n\t\t\tif project == \"\" {\n\t\t\t\treturn errors.New(\"project should not be empty\")\n\t\t\t}\n\n\t\t\tids := strhlp.Map(strings.TrimSpace, args)\n\t\t\tif strhlp.Search(\"\", ids) != -1 {\n\t\t\t\treturn errors.New(\"task id/name should not be empty\")\n\t\t\t}\n\n\t\t\tworkspace, err := f.GetWorkspaceID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif f.Config().IsAllowNameForID() {\n\t\t\t\tif project, err = search.GetProjectByName(\n\t\t\t\t\tc, f.Config(), workspace, project, \"\"); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif ids, err = search.GetTasksByName(\n\t\t\t\t\tc,\n\t\t\t\t\tapi.GetTasksParam{\n\t\t\t\t\t\tWorkspace: workspace,\n\t\t\t\t\t\tProjectID: project,\n\t\t\t\t\t\tActive:    true,\n\t\t\t\t\t},\n\t\t\t\t\tids,\n\t\t\t\t); err != nil {\n\t\t\t\t\tvar errNF search.ErrNotFound\n\t\t\t\t\tif errors.As(err, &errNF) {\n\t\t\t\t\t\treturn errors.New(\n\t\t\t\t\t\t\t\"No active task with id or name containing '\" +\n\t\t\t\t\t\t\t\terrNF.Reference + \"' was found\")\n\t\t\t\t\t}\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttasks := make([]dto.Task, len(ids))\n\t\t\tvar g errgroup.Group\n\t\t\tfor i := 0; i < len(ids); i++ {\n\t\t\t\tj := i\n\t\t\t\tg.Go(func() error {\n\t\t\t\t\tt, err := c.GetTask(api.GetTaskParam{\n\t\t\t\t\t\tWorkspace: workspace,\n\t\t\t\t\t\tProjectID: project,\n\t\t\t\t\t\tTaskID:    ids[j],\n\t\t\t\t\t})\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\ttasks[j], err = c.UpdateTask(api.UpdateTaskParam{\n\t\t\t\t\t\tWorkspace: workspace,\n\t\t\t\t\t\tProjectID: t.ProjectID,\n\t\t\t\t\t\tTaskID:    t.ID,\n\t\t\t\t\t\tName:      t.Name,\n\t\t\t\t\t\tStatus:    api.TaskStatusDone,\n\t\t\t\t\t})\n\n\t\t\t\t\treturn err\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif err := g.Wait(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif report == nil {\n\t\t\t\treturn util.TaskReport(cmd, of, tasks...)\n\t\t\t}\n\n\t\t\treturn report(cmd.OutOrStdout(), &of, tasks)\n\t\t},\n\t}\n\n\tcmdutil.AddProjectFlags(cmd, f)\n\tutil.TaskAddReportFlags(cmd, &of)\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/task/done/done_test.go",
    "content": "package done_test\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/done\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCmdDone(t *testing.T) {\n\tte := dto.Task{ID: \"task-id\", Name: \"Task with ID\", ProjectID: \"p-1\"}\n\tshouldCall := func(t *testing.T) func(\n\t\tio.Writer, *util.OutputFlags, []dto.Task) error {\n\t\tcalled := false\n\t\tt.Cleanup(func() { assert.True(t, called) })\n\t\treturn func(\n\t\t\tw io.Writer, of *util.OutputFlags, tr []dto.Task) error {\n\t\t\tcalled = true\n\t\t\tassert.Len(t, tr, 1)\n\t\t\tassert.Equal(t, te, tr[0])\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tdFactory := func(t *testing.T) cmdutil.Factory {\n\t\tf := mocks.NewMockFactory(t)\n\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\treturn f\n\t}\n\n\ttts := []struct {\n\t\tname    string\n\t\targs    []string\n\t\tfactory func(*testing.T) cmdutil.Factory\n\t\treport  func(*testing.T) func(\n\t\t\tio.Writer, *util.OutputFlags, []dto.Task) error\n\t\terr string\n\t}{\n\t\t{\n\t\t\tname:    \"task id required\",\n\t\t\targs:    []string{\"-p=cli\"},\n\t\t\terr:     `requires arg task`,\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"project required\",\n\t\t\targs:    []string{\"task\"},\n\t\t\terr:     `\"project\" not set`,\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"project not empty\",\n\t\t\targs:    []string{\"task\", \"-p=          \"},\n\t\t\terr:     `project should not be empty`,\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"task id not empty\",\n\t\t\targs:    []string{\"   \", \"-p=cli\"},\n\t\t\terr:     `task id/name should not be empty`,\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"task id not empty (nice try)\",\n\t\t\targs:    []string{\"not-empty\", \"  \", \"-p=cli\"},\n\t\t\terr:     `task id/name should not be empty`,\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"only one format\",\n\t\t\targs:    []string{\"--format={}\", \"-q\", \"-j\", \"-p=OK\", \"done\"},\n\t\t\terr:     \"flags can't be used together.*format.*json.*quiet\",\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname: \"client error\",\n\t\t\terr:  \"client error\",\n\t\t\targs: []string{\"done\", \"-p=b\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tf.On(\"Client\").Return(nil, errors.New(\"client error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workspace error\",\n\t\t\terr:  \"workspace error\",\n\t\t\targs: []string{\"done\", \"-p=b\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"\", errors.New(\"workspace error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup project\",\n\t\t\terr:  \"no project\",\n\t\t\targs: []string{\"done\", \"-p=cli\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{}, errors.New(\"no project\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"cant find task\",\n\t\t\terr:  \"No active task with id or name.*done\",\n\t\t\targs: []string{\"done\", \"-p=p-1\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\t\t\t\tcf.On(\"IsSearchProjectWithClientsName\").Return(true)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{{ID: \"p-1\"}}, nil)\n\n\t\t\t\tc.On(\"GetTasks\", api.GetTasksParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tProjectID:       \"p-1\",\n\t\t\t\t\tActive:          true,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Task{}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail to find\",\n\t\t\terr:  \"something went wrong\",\n\t\t\targs: []string{\"task 1\", \"task 2\", \"-p=cli\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\t\t\t\tcf.On(\"IsSearchProjectWithClientsName\").Return(true)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{{ID: \"p-1\", Name: \"Cli\"}}, nil)\n\n\t\t\t\tts := []dto.Task{\n\t\t\t\t\t{ID: \"t-1\", ProjectID: \"p-1\", Name: \"Task 1\"},\n\t\t\t\t\t{ID: \"t-2\", ProjectID: \"p-1\", Name: \"Task 2\"},\n\t\t\t\t}\n\t\t\t\tc.On(\"GetTasks\", api.GetTasksParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tProjectID:       \"p-1\",\n\t\t\t\t\tActive:          true,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(ts, nil)\n\n\t\t\t\tc.On(\"GetTask\", api.GetTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: ts[0].ProjectID,\n\t\t\t\t\tTaskID:    ts[0].ID,\n\t\t\t\t}).\n\t\t\t\t\tReturn(ts[0], nil)\n\n\t\t\t\tc.On(\"UpdateTask\", api.UpdateTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: ts[0].ProjectID,\n\t\t\t\t\tTaskID:    ts[0].ID,\n\t\t\t\t\tName:      ts[0].Name,\n\t\t\t\t\tStatus:    api.TaskStatusDone,\n\t\t\t\t}).\n\t\t\t\t\tReturn(ts[0], nil)\n\n\t\t\t\tc.On(\"GetTask\", api.GetTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: ts[1].ProjectID,\n\t\t\t\t\tTaskID:    ts[1].ID,\n\t\t\t\t}).\n\t\t\t\t\tReturn(ts[1], errors.New(\"something went wrong\"))\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail second update\",\n\t\t\terr:  \"something went wrong\",\n\t\t\targs: []string{\"task 1\", \"task 2\", \"-p=cli\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\t\t\t\tcf.On(\"IsSearchProjectWithClientsName\").Return(true)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{{ID: \"p-1\", Name: \"Cli\"}}, nil)\n\n\t\t\t\tts := []dto.Task{\n\t\t\t\t\t{ID: \"t-1\", ProjectID: \"p-1\", Name: \"Task 1\"},\n\t\t\t\t\t{ID: \"t-2\", ProjectID: \"p-1\", Name: \"Task 2\"},\n\t\t\t\t}\n\t\t\t\tc.On(\"GetTasks\", api.GetTasksParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tProjectID:       \"p-1\",\n\t\t\t\t\tActive:          true,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(ts, nil)\n\n\t\t\t\tc.On(\"GetTask\", api.GetTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: ts[0].ProjectID,\n\t\t\t\t\tTaskID:    ts[0].ID,\n\t\t\t\t}).\n\t\t\t\t\tReturn(ts[0], nil)\n\n\t\t\t\tc.On(\"UpdateTask\", api.UpdateTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: ts[0].ProjectID,\n\t\t\t\t\tTaskID:    ts[0].ID,\n\t\t\t\t\tName:      ts[0].Name,\n\t\t\t\t\tStatus:    api.TaskStatusDone,\n\t\t\t\t}).\n\t\t\t\t\tReturn(ts[0], nil)\n\n\t\t\t\tc.On(\"GetTask\", api.GetTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: ts[1].ProjectID,\n\t\t\t\t\tTaskID:    ts[1].ID,\n\t\t\t\t}).\n\t\t\t\t\tReturn(ts[1], nil)\n\n\t\t\t\tc.On(\"UpdateTask\", api.UpdateTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: ts[1].ProjectID,\n\t\t\t\t\tTaskID:    ts[1].ID,\n\t\t\t\t\tName:      ts[1].Name,\n\t\t\t\t\tStatus:    api.TaskStatusDone,\n\t\t\t\t}).\n\t\t\t\t\tReturn(ts[0], errors.New(\"something went wrong\"))\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"done\",\n\t\t\targs: []string{te.ID, \"-p=p-1\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(false)\n\n\t\t\t\tc.On(\"GetTask\", api.GetTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: te.ProjectID,\n\t\t\t\t\tTaskID:    te.ID,\n\t\t\t\t}).\n\t\t\t\t\tReturn(te, nil)\n\n\t\t\t\tc.On(\"UpdateTask\", api.UpdateTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: te.ProjectID,\n\t\t\t\t\tTaskID:    te.ID,\n\t\t\t\t\tName:      te.Name,\n\t\t\t\t\tStatus:    api.TaskStatusDone,\n\t\t\t\t}).\n\t\t\t\t\tReturn(te, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: shouldCall,\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := func(io.Writer, *util.OutputFlags, []dto.Task) error {\n\t\t\t\tassert.Fail(t, \"failed\")\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif tt.report != nil {\n\t\t\t\tr = tt.report(t)\n\t\t\t}\n\n\t\t\tcmd := done.NewCmdDone(tt.factory(t), r)\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n}\n\nfunc TestCmdDoneReport(t *testing.T) {\n\ttasks := []dto.Task{\n\t\t{ID: \"t-1\", ProjectID: \"p-1\", Name: \"Done Report\"},\n\t\t{ID: \"t-2\", ProjectID: \"p-1\", Name: \"Done Cmd\"},\n\t}\n\ttts := []struct {\n\t\tname   string\n\t\targs   []string\n\t\tassert func(*testing.T, *util.OutputFlags, []dto.Task)\n\t}{\n\t\t{\n\t\t\tname: \"report quiet\",\n\t\t\targs: []string{\"-q\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) {\n\t\t\t\tassert.True(t, of.Quiet)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report json\",\n\t\t\targs: []string{\"--json\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) {\n\t\t\t\tassert.True(t, of.JSON)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report format\",\n\t\t\targs: []string{\"--format={{.ID}}\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) {\n\t\t\t\tassert.Equal(t, \"{{.ID}}\", of.Format)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report csv\",\n\t\t\targs: []string{\"--csv\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) {\n\t\t\t\tassert.True(t, of.CSV)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report default\",\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) {\n\t\t\t\tassert.False(t, of.CSV)\n\t\t\t\tassert.False(t, of.JSON)\n\t\t\t\tassert.False(t, of.Quiet)\n\t\t\t\tassert.True(t, of.Format == \"\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\tReturn(\"w\", nil)\n\n\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\tf.On(\"Config\").Return(cf)\n\n\t\t\tcf.On(\"IsAllowNameForID\").Return(false)\n\n\t\t\tfn := func(t dto.Task) {\n\t\t\t\tc.On(\"GetTask\", api.GetTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: t.ProjectID,\n\t\t\t\t\tTaskID:    t.ID,\n\t\t\t\t}).\n\t\t\t\t\tReturn(t, nil)\n\n\t\t\t\tc.On(\"UpdateTask\", api.UpdateTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: t.ProjectID,\n\t\t\t\t\tTaskID:    t.ID,\n\t\t\t\t\tName:      t.Name,\n\t\t\t\t\tStatus:    api.TaskStatusDone,\n\t\t\t\t}).\n\t\t\t\t\tReturn(t, nil)\n\n\t\t\t}\n\n\t\t\tfn(tasks[0])\n\t\t\tfn(tasks[1])\n\n\t\t\tcalled := false\n\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\tcmd := done.NewCmdDone(f, func(\n\t\t\t\t_ io.Writer, of *util.OutputFlags, l []dto.Task) error {\n\t\t\t\tcalled = true\n\t\t\t\tassert.Contains(t, l, tasks[0])\n\t\t\t\tassert.Contains(t, l, tasks[1])\n\t\t\t\ttt.assert(t, of, l)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(append(tt.args, \"t-1\", \"-p=p-1\", \"t-2\"))\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/task/edit/edit.go",
    "content": "package edit\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/search\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdEdit represents the close command\nfunc NewCmdEdit(\n\tf cmdutil.Factory,\n\treport func(io.Writer, *util.OutputFlags, dto.Task) error,\n) *cobra.Command {\n\tof := util.OutputFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:     \"edit <task>\",\n\t\tAliases: []string{\"update\"},\n\t\tArgs:    cmdutil.RequiredNamedArgs(\"task\"),\n\t\tValidArgsFunction: cmdcompl.CombineSuggestionsToArgs(\n\t\t\tcmdcomplutil.NewTaskAutoComplete(f, false)),\n\t\tShort: \"Edit a task from a project on Clockify\",\n\t\tLong: heredoc.Doc(`\n\t\t\tEdits a task on a Clockify's project, allowing to change the name, estimated time, assignees, status and billable settings.\n\n\t\t\tIf you set a estimate for the task, but the project is set as manual estimation, then it will have no effect on Clockify.\n\t\t`),\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s -p special 62aa5d7049445270d7b979d6 --name=\"Very Important\"\n\t\t\t+--------------------------+----------------+--------+\n\t\t\t|            ID            |      NAME      | STATUS |\n\t\t\t+--------------------------+----------------+--------+\n\t\t\t| 62aa5d7049445270d7b979d6 | Very Important | ACTIVE |\n\t\t\t+--------------------------+----------------+--------+\n\n\t\t\t$ %[1]s -p special 'important' --assign john@example.com | \\\n\t\t\t  jq '.[] |.assigneeIds' --compact-output\n\t\t\t[\"dddddddddddddddddddddddd\"]\n\n\t\t\t$ %[1]s -p special important --billable --quiet\n\t\t\t62aa5d7049445270d7b979d6\n\n\t\t\t$ %[1]s -p special important --not-billable --csv\n\t\t\tid,name,status\n\t\t\t62aa5d7049445270d7b979d6,Very Important,ACTIVE\n\n\t\t\t$ %[1]s -p special very --estimate 1 --done\n\t\t\t+--------------------------+----------------+--------+\n\t\t\t|            ID            |      NAME      | STATUS |\n\t\t\t+--------------------------+----------------+--------+\n\t\t\t| 62aa5d7049445270d7b979d6 | Very Important | DONE   |\n\t\t\t+--------------------------+----------------+--------+\n\n\t\t\t$ %[1]s -p special 'very i' --active --format --no-assignee \\\n\t\t\t  --format '{{.Name}} | {{.Status}} | {{ .AssigneeIDs }}'\n\t\t\tVery Important | ACTIVE | []\n\t\t`, \"clockify-cli task edit\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\ttask := strings.TrimSpace(args[0])\n\t\t\tif task == \"\" {\n\t\t\t\treturn errors.New(\"task id should not be empty\")\n\t\t\t}\n\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfl, err := util.TaskReadFlags(cmd, f)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif f.Config().IsAllowNameForID() {\n\t\t\t\tif task, err = search.GetTaskByName(\n\t\t\t\t\tc,\n\t\t\t\t\tapi.GetTasksParam{\n\t\t\t\t\t\tWorkspace: fl.Workspace, ProjectID: fl.ProjectID},\n\t\t\t\t\ttask); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tp := api.UpdateTaskParam{\n\t\t\t\tWorkspace:   fl.Workspace,\n\t\t\t\tProjectID:   fl.ProjectID,\n\t\t\t\tTaskID:      task,\n\t\t\t\tName:        fl.Name,\n\t\t\t\tEstimate:    fl.Estimate,\n\t\t\t\tAssigneeIDs: fl.AssigneeIDs,\n\t\t\t\tBillable:    fl.Billable,\n\t\t\t}\n\n\t\t\tif !cmd.Flags().Changed(\"name\") {\n\t\t\t\tt, err := c.GetTask(api.GetTaskParam{\n\t\t\t\t\tWorkspace: fl.Workspace,\n\t\t\t\t\tProjectID: fl.ProjectID,\n\t\t\t\t\tTaskID:    task,\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tp.Name = t.Name\n\t\t\t}\n\n\t\t\tswitch {\n\t\t\tcase cmd.Flags().Changed(\"active\"):\n\t\t\t\tp.Status = api.TaskStatusActive\n\t\t\tcase cmd.Flags().Changed(\"done\"):\n\t\t\t\tp.Status = api.TaskStatusDone\n\t\t\tdefault:\n\t\t\t\tp.Status = api.TaskStatusDefault\n\t\t\t}\n\n\t\t\tt, err := c.UpdateTask(p)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif report == nil {\n\t\t\t\treturn util.TaskReport(cmd, of, t)\n\t\t\t}\n\n\t\t\treturn report(cmd.OutOrStdout(), &of, t)\n\t\t},\n\t}\n\n\tutil.TaskAddPropFlags(cmd, f)\n\n\tcmd.Flags().Bool(\"done\", false, \"sets the task as done\")\n\tcmd.Flags().Bool(\"active\", false, \"sets the task as active\")\n\n\tutil.TaskAddReportFlags(cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/task/edit/edit_test.go",
    "content": "package edit_test\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/edit\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCmdEdit(t *testing.T) {\n\tte := dto.Task{ID: \"task-id\"}\n\tshouldCall := func(t *testing.T) func(\n\t\tio.Writer, *util.OutputFlags, dto.Task) error {\n\t\tcalled := false\n\t\tt.Cleanup(func() { assert.True(t, called) })\n\t\treturn func(\n\t\t\tw io.Writer, of *util.OutputFlags, tr dto.Task) error {\n\t\t\tcalled = true\n\t\t\tassert.Equal(t, te, tr)\n\t\t\treturn nil\n\t\t}\n\t}\n\tdFactory := func(t *testing.T) cmdutil.Factory {\n\t\tf := mocks.NewMockFactory(t)\n\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\treturn f\n\t}\n\n\ttts := []struct {\n\t\tname    string\n\t\targs    []string\n\t\tfactory func(*testing.T) cmdutil.Factory\n\t\treport  func(*testing.T) func(\n\t\t\tio.Writer, *util.OutputFlags, dto.Task) error\n\t\terr string\n\t}{\n\t\t{\n\t\t\tname:    \"task id required\",\n\t\t\targs:    []string{\"-p=cli\"},\n\t\t\terr:     `requires arg task`,\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"project required\",\n\t\t\targs:    []string{\"task\"},\n\t\t\terr:     `\"project\" not set`,\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"task id not empty\",\n\t\t\targs:    []string{\"   \", \"-p=cli\"},\n\t\t\terr:     `task id should not be empty`,\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"only one format\",\n\t\t\targs:    []string{\"--format={}\", \"-q\", \"-j\", \"-p=OK\", \"edit\"},\n\t\t\terr:     \"flags can't be used together.*format.*json.*quiet\",\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"billable or not\",\n\t\t\targs:    []string{\"--billable\", \"--not-billable\", \"edit\", \"-p=OK\"},\n\t\t\terr:     \"flags can't be used together.*billable.*not-billable\",\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"assignee or no assignee\",\n\t\t\targs:    []string{\"--assignee=l\", \"--no-assignee\", \"edit\", \"-p=OK\"},\n\t\t\terr:     \"flags can't be used together.*assignee.*no-assignee\",\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname: \"client error\",\n\t\t\terr:  \"client error\",\n\t\t\targs: []string{\"edit\", \"-p=b\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\n\t\t\t\tf.On(\"Client\").Return(nil, errors.New(\"client error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workspace error\",\n\t\t\terr:  \"workspace error\",\n\t\t\targs: []string{\"edit\", \"-p=b\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"\", errors.New(\"workspace error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup project\",\n\t\t\terr:  \"no project\",\n\t\t\targs: []string{\"edit\", \"-p=cli\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{}, errors.New(\"no project\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup user\",\n\t\t\terr:  \"no user\",\n\t\t\targs: []string{\"edit\", \"-p=cli\", \"-A=who\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID: true,\n\t\t\t\t})\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{{Name: \"Cli\"}}, nil)\n\n\t\t\t\tc.On(\"WorkspaceUsers\", api.WorkspaceUsersParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.User{}, errors.New(\"no user\"))\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup task\",\n\t\t\terr:  \"No task with id or name.*edit\",\n\t\t\targs: []string{\"edit\", \"-p=cli\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID: true,\n\t\t\t\t})\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{{ID: \"p-1\", Name: \"Cli\"}}, nil)\n\n\t\t\t\tc.On(\"GetTasks\", api.GetTasksParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tProjectID:       \"p-1\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Task{{Name: \"other one\"}}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"http error\",\n\t\t\terr:  \"http error\",\n\t\t\targs: []string{\"task-id\", \"-p=ok\", \"-n=new\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(false)\n\n\t\t\t\tc.On(\"UpdateTask\", api.UpdateTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tTaskID:    \"task-id\",\n\t\t\t\t\tProjectID: \"ok\",\n\t\t\t\t\tName:      \"new\",\n\t\t\t\t}).\n\t\t\t\t\tReturn(dto.Task{}, errors.New(\"http error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"edit billable task\",\n\t\t\targs: []string{\n\t\t\t\t\"edit\",\n\t\t\t\t\"--name=Edit\",\n\t\t\t\t\"--project=cli\",\n\t\t\t\t\"--billable\",\n\t\t\t\t\"--no-assignee\",\n\t\t\t\t\"--estimate\", \"32\",\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID: true,\n\t\t\t\t})\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return(\n\t\t\t\t\t[]dto.Project{{ID: \"p-1\", Name: \"Clockify CLI\"}}, nil)\n\n\t\t\t\tc.On(\"GetTasks\", api.GetTasksParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tProjectID:       \"p-1\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return(\n\t\t\t\t\t[]dto.Task{{ID: \"t-1\", Name: \"Edit Command\"}}, nil)\n\n\t\t\t\tb := true\n\t\t\t\te := time.Hour * 32\n\t\t\t\tvar us []string\n\n\t\t\t\tc.On(\"UpdateTask\", api.UpdateTaskParam{\n\t\t\t\t\tWorkspace:   \"w\",\n\t\t\t\t\tTaskID:      \"t-1\",\n\t\t\t\t\tName:        \"Edit\",\n\t\t\t\t\tProjectID:   \"p-1\",\n\t\t\t\t\tBillable:    &b,\n\t\t\t\t\tEstimate:    &e,\n\t\t\t\t\tAssigneeIDs: &us,\n\t\t\t\t}).\n\t\t\t\t\tReturn(te, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: shouldCall,\n\t\t},\n\t\t{\n\t\t\tname: \"edit non-billable task\",\n\t\t\targs: []string{\n\t\t\t\t\"edit\",\n\t\t\t\t\"--project=p-1\",\n\t\t\t\t\"--assignee\", \"lucas\",\n\t\t\t\t\"--assignee=john\",\n\t\t\t\t\"--not-billable\",\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID: true,\n\t\t\t\t})\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return(\n\t\t\t\t\t[]dto.Project{{ID: \"p-1\", Name: \"Clockify CLI\"}}, nil)\n\n\t\t\t\tts := dto.Task{ID: \"t-1\", Name: \"Edit Command\"}\n\t\t\t\tc.On(\"GetTasks\", api.GetTasksParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tProjectID:       \"p-1\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return(\n\t\t\t\t\t[]dto.Task{ts}, nil)\n\n\t\t\t\tc.On(\"WorkspaceUsers\", api.WorkspaceUsersParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.User{\n\t\t\t\t\t\t{ID: \"u-1\", Name: \"Lucas Abreu\"},\n\t\t\t\t\t\t{ID: \"u-2\", Name: \"John Due\"},\n\t\t\t\t\t}, nil)\n\n\t\t\t\tc.On(\"GetTask\", api.GetTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"p-1\",\n\t\t\t\t\tTaskID:    \"t-1\",\n\t\t\t\t}).Return(ts, nil)\n\n\t\t\t\tb := false\n\t\t\t\tas := []string{\"u-1\", \"u-2\"}\n\t\t\t\tc.On(\"UpdateTask\", api.UpdateTaskParam{\n\t\t\t\t\tWorkspace:   \"w\",\n\t\t\t\t\tTaskID:      \"t-1\",\n\t\t\t\t\tName:        \"Edit Command\",\n\t\t\t\t\tProjectID:   \"p-1\",\n\t\t\t\t\tAssigneeIDs: &as,\n\t\t\t\t\tBillable:    &b,\n\t\t\t\t}).\n\t\t\t\t\tReturn(te, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: shouldCall,\n\t\t},\n\t\t{\n\t\t\tname: \"edit no allow name for id\",\n\t\t\targs: []string{\n\t\t\t\t\"t-1\",\n\t\t\t\t\"--project=p-1\",\n\t\t\t\t\"--assignee\", \"u-1\",\n\t\t\t\t\"--assignee=u-2\",\n\t\t\t\t\"--not-billable\",\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(false)\n\n\t\t\t\tts := dto.Task{ID: \"t-1\", Name: \"Edit Command\"}\n\t\t\t\tc.On(\"GetTask\", api.GetTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"p-1\",\n\t\t\t\t\tTaskID:    \"t-1\",\n\t\t\t\t}).Return(ts, nil)\n\n\t\t\t\tb := false\n\t\t\t\tas := []string{\"u-1\", \"u-2\"}\n\t\t\t\tc.On(\"UpdateTask\", api.UpdateTaskParam{\n\t\t\t\t\tWorkspace:   \"w\",\n\t\t\t\t\tTaskID:      \"t-1\",\n\t\t\t\t\tName:        \"Edit Command\",\n\t\t\t\t\tProjectID:   \"p-1\",\n\t\t\t\t\tAssigneeIDs: &as,\n\t\t\t\t\tBillable:    &b,\n\t\t\t\t}).\n\t\t\t\t\tReturn(te, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: shouldCall,\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := func(io.Writer, *util.OutputFlags, dto.Task) error {\n\t\t\t\tassert.Fail(t, \"failed\")\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif tt.report != nil {\n\t\t\t\tr = tt.report(t)\n\t\t\t}\n\n\t\t\tcmd := edit.NewCmdEdit(tt.factory(t), r)\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n}\n\nfunc TestCmdEditReport(t *testing.T) {\n\tpr := dto.Task{Name: \"Coderockr\"}\n\ttts := []struct {\n\t\tname   string\n\t\targs   []string\n\t\tassert func(*testing.T, *util.OutputFlags, dto.Task)\n\t}{\n\t\t{\n\t\t\tname: \"report quiet\",\n\t\t\targs: []string{\"-q\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Task) {\n\t\t\t\tassert.True(t, of.Quiet)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report json\",\n\t\t\targs: []string{\"--json\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Task) {\n\t\t\t\tassert.True(t, of.JSON)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report format\",\n\t\t\targs: []string{\"--format={{.ID}}\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Task) {\n\t\t\t\tassert.Equal(t, \"{{.ID}}\", of.Format)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report csv\",\n\t\t\targs: []string{\"--csv\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) {\n\t\t\t\tassert.True(t, of.CSV)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report default\",\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) {\n\t\t\t\tassert.False(t, of.CSV)\n\t\t\t\tassert.False(t, of.JSON)\n\t\t\t\tassert.False(t, of.Quiet)\n\t\t\t\tassert.True(t, of.Format == \"\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\tReturn(\"w\", nil)\n\n\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\tf.On(\"Config\").Return(cf)\n\n\t\t\tcf.On(\"IsAllowNameForID\").Return(false)\n\n\t\t\tc.On(\"UpdateTask\", api.UpdateTaskParam{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tProjectID: \"p-1\",\n\t\t\t\tTaskID:    \"t-1\",\n\t\t\t\tName:      \"Task Edit\",\n\t\t\t}).\n\t\t\t\tReturn(pr, nil)\n\n\t\t\tcalled := false\n\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\tcmd := edit.NewCmdEdit(f, func(\n\t\t\t\t_ io.Writer, of *util.OutputFlags, u dto.Task) error {\n\t\t\t\tcalled = true\n\t\t\t\tassert.Equal(t, pr, u)\n\t\t\t\ttt.assert(t, of, u)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(append(tt.args, \"t-1\", \"-n\", \"Task Edit\", \"-p=p-1\"))\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/task/list/list.go",
    "content": "package list\n\nimport (\n\t\"io\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/search\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdList represents the list command\nfunc NewCmdList(\n\tf cmdutil.Factory,\n\treport func(io.Writer, *util.OutputFlags, []dto.Task) error,\n) *cobra.Command {\n\tof := util.OutputFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:   \"list\",\n\t\tShort: \"List tasks in a Clockify project\",\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s --project special\n\t\t\t+--------------------------+----------+--------+\n\t\t\t|            ID            |   NAME   | STATUS |\n\t\t\t+--------------------------+----------+--------+\n\t\t\t| 62aa4eed49445270d7b9666c | Inactive | DONE   |\n\t\t\t| 62aa4ee64ebb4f143c8d5225 | Second   | ACTIVE |\n\t\t\t| 62aa4ea2c22de9759e6e3a0e | First    | ACTIVE |\n\t\t\t+--------------------------+----------+--------+\n\n\t\t\t$ %[1]s --project special --active --quiet\n\t\t\t62aa4ee64ebb4f143c8d5225\n\t\t\t62aa4ea2c22de9759e6e3a0e\n\n\t\t\t$ %[1]s --project special --name inact --csv\n\t\t\tid,name,status\n\t\t\t62aa4eed49445270d7b9666c,Inactive,DONE\n\t\t`, \"clockify-cli task list\"),\n\t\tAliases: []string{\"ls\"},\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tworkspace, err := f.GetWorkspaceID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tp := api.GetTasksParam{\n\t\t\t\tWorkspace:       workspace,\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t}\n\n\t\t\tp.Active, _ = cmd.Flags().GetBool(\"active\")\n\t\t\tp.Name, _ = cmd.Flags().GetString(\"name\")\n\t\t\tp.ProjectID, _ = cmd.Flags().GetString(\"project\")\n\n\t\t\tif f.Config().IsAllowNameForID() &&\n\t\t\t\tp.ProjectID != \"\" {\n\t\t\t\tif p.ProjectID, err = search.GetProjectByName(\n\t\t\t\t\tc, f.Config(), workspace, p.ProjectID, \"\"); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttasks, err := c.GetTasks(p)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif report == nil {\n\t\t\t\treturn util.TaskReport(cmd, of, tasks...)\n\t\t\t}\n\n\t\t\treturn report(cmd.OutOrStdout(), &of, tasks)\n\t\t},\n\t}\n\n\tcmd.Flags().StringP(\"name\", \"n\", \"\",\n\t\t\"will be used to filter the tag by name\")\n\tcmd.Flags().BoolP(\"active\", \"a\", false, \"display only active tasks\")\n\n\tutil.TaskAddReportFlags(cmd, &of)\n\tcmdutil.AddProjectFlags(cmd, f)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/task/list/list_test.go",
    "content": "package list_test\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/list\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype report func(io.Writer, *util.OutputFlags, []dto.Task) error\n\nfunc TestCmdList(t *testing.T) {\n\ttts := []struct {\n\t\tname   string\n\t\terr    string\n\t\targs   []string\n\t\tparams func(*testing.T) (cmdutil.Factory, report)\n\t}{\n\t\t{\n\t\t\tname: \"missing project\",\n\t\t\terr:  \"required flag.*project\",\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workspace error\",\n\t\t\terr:  \"error\",\n\t\t\targs: []string{\"-p=p-1\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"\", errors.New(\"error\"))\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"client error\",\n\t\t\terr:  \"error\",\n\t\t\targs: []string{\"-p=p-1\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(nil, errors.New(\"error\"))\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"project lookup error\",\n\t\t\terr:  \"No project with id or name containing.*p-1\",\n\t\t\targs: []string{\"-p=p-1\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\t\t\t\tcf.On(\"IsSearchProjectWithClientsName\").Return(false)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{}, nil)\n\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"list error\",\n\t\t\terr:  \"error\",\n\t\t\targs: []string{\"-p=p-1\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(false)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetTasks\", api.GetTasksParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tProjectID:       \"p-1\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Task{}, errors.New(\"error\"))\n\n\t\t\t\treturn f, nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"list active with name\",\n\t\t\targs: []string{\"-p=p-1\", \"--active\", \"-n=list\"},\n\t\t\tparams: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\t\t\t\tcf.On(\"IsSearchProjectWithClientsName\").Return(false)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{\n\t\t\t\t\t{ID: \"p-1\", Name: \"Cli\"},\n\t\t\t\t}, nil)\n\n\t\t\t\tl := []dto.Task{\n\t\t\t\t\t{ID: \"1\", Name: \"List\"},\n\t\t\t\t\t{ID: \"2\", Name: \"List Tasks\"},\n\t\t\t\t}\n\t\t\t\tc.On(\"GetTasks\", api.GetTasksParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tProjectID:       \"p-1\",\n\t\t\t\t\tActive:          true,\n\t\t\t\t\tName:            \"list\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return(l, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\t\treturn f, func(\n\t\t\t\t\tw io.Writer, of *util.OutputFlags, r []dto.Task) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, l, r)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf, r := tt.params(t)\n\t\t\tif r == nil {\n\t\t\t\tr = func(\n\t\t\t\t\t_ io.Writer, of *util.OutputFlags, l []dto.Task) error {\n\t\t\t\t\tt.Error(\"should not be called\")\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t\tcmd := list.NewCmdList(f, r)\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n\n}\n\nfunc TestCmdListReport(t *testing.T) {\n\ttasks := []dto.Task{\n\t\t{ID: \"t-1\", ProjectID: \"p-1\", Name: \"List Report\"},\n\t\t{ID: \"t-2\", ProjectID: \"p-1\", Name: \"List Cmd\"},\n\t}\n\ttts := []struct {\n\t\tname   string\n\t\targs   []string\n\t\tassert func(*testing.T, *util.OutputFlags, []dto.Task)\n\t}{\n\t\t{\n\t\t\tname: \"report quiet\",\n\t\t\targs: []string{\"-q\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) {\n\t\t\t\tassert.True(t, of.Quiet)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report json\",\n\t\t\targs: []string{\"--json\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) {\n\t\t\t\tassert.True(t, of.JSON)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report format\",\n\t\t\targs: []string{\"--format={{.ID}}\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) {\n\t\t\t\tassert.Equal(t, \"{{.ID}}\", of.Format)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report csv\",\n\t\t\targs: []string{\"--csv\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) {\n\t\t\t\tassert.True(t, of.CSV)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report default\",\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ []dto.Task) {\n\t\t\t\tassert.False(t, of.CSV)\n\t\t\t\tassert.False(t, of.JSON)\n\t\t\t\tassert.False(t, of.Quiet)\n\t\t\t\tassert.True(t, of.Format == \"\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\tReturn(\"w\", nil)\n\n\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\tf.On(\"Config\").Return(cf)\n\n\t\t\tcf.On(\"IsAllowNameForID\").Return(false)\n\n\t\t\tc.On(\"GetTasks\", api.GetTasksParam{\n\t\t\t\tWorkspace:       \"w\",\n\t\t\t\tProjectID:       \"p-1\",\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t}).\n\t\t\t\tReturn(tasks, nil)\n\n\t\t\tcalled := false\n\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\tcmd := list.NewCmdList(f, func(\n\t\t\t\t_ io.Writer, of *util.OutputFlags, l []dto.Task) error {\n\t\t\t\tcalled = true\n\t\t\t\tassert.Equal(t, l, tasks)\n\t\t\t\ttt.assert(t, of, l)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(append(tt.args, \"t-1\", \"-p=p-1\", \"t-2\"))\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/task/quick-add/quick-add.go",
    "content": "package quickadd\n\nimport (\n\t\"io\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/search\"\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// NewCmdQuickAdd will add multiple tasks to a project, but only setting its\n// name\nfunc NewCmdQuickAdd(\n\tf cmdutil.Factory,\n\treport func(io.Writer, *util.OutputFlags, []dto.Task) error,\n) *cobra.Command {\n\tof := util.OutputFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:     \"quick-add <name>...\",\n\t\tAliases: []string{\"quick\"},\n\t\tShort:   \"Adds tasks to a project on Clockify\",\n\t\tArgs:    cmdutil.RequiredNamedArgs(\"name\"),\n\t\tLong: \"Adds a new active tasks to a project on Clockify, \" +\n\t\t\t\"but only allow setting their names.\",\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s -p special \"Very Important\"\n\t\t\t+--------------------------+----------------+--------+\n\t\t\t|            ID            |      NAME      | STATUS |\n\t\t\t+--------------------------+----------------+--------+\n\t\t\t| 62aa5d7049445270d7b979d6 | Very Important | ACTIVE |\n\t\t\t+--------------------------+----------------+--------+\n\n\t\t\t$ %[1]s -p special \"Very Cool\" -q\n\t\t\tdddddddddddddddddddddddd\n\n\t\t\t$ %[1]s -p special Billable \"Not Billable\" --csv\n\t\t\tid,name,status\n\t\t\t62ab145ec22de9759e6f6e35,Billable,ACTIVE\n\t\t\t62ab145ec22de9759e6f6e36,Not Billable,ACTIVE\n\t\t`, \"clockify-cli task quick-add\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tw, err := f.GetWorkspaceID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tp, _ := cmd.Flags().GetString(\"project\")\n\t\t\tif f.Config().IsAllowNameForID() {\n\t\t\t\tif p, err = search.GetProjectByName(\n\t\t\t\t\tc, f.Config(), w, p, \"\"); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tnames := strhlp.Unique(args)\n\t\t\ttasks := make([]dto.Task, len(names))\n\t\t\tg := errgroup.Group{}\n\t\t\tfor j := range names {\n\t\t\t\ti := j\n\t\t\t\tg.Go(func() error {\n\t\t\t\t\ttasks[i], err = c.AddTask(api.AddTaskParam{\n\t\t\t\t\t\tWorkspace: w,\n\t\t\t\t\t\tProjectID: p,\n\t\t\t\t\t\tName:      names[i],\n\t\t\t\t\t})\n\t\t\t\t\treturn err\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif err := g.Wait(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif report != nil {\n\t\t\t\treturn report(cmd.OutOrStdout(), &of, tasks)\n\t\t\t}\n\n\t\t\treturn util.TaskReport(cmd, of, tasks...)\n\t\t},\n\t}\n\n\tcmdutil.AddProjectFlags(cmd, f)\n\tutil.TaskAddReportFlags(cmd, &of)\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/task/quick-add/quick-add_test.go",
    "content": "package quickadd_test\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\tquickadd \"github.com/lucassabreu/clockify-cli/pkg/cmd/task/quick-add\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCmdQuickAdd(t *testing.T) {\n\tshouldCall := func(t *testing.T) func(\n\t\tio.Writer, *util.OutputFlags, []dto.Task) error {\n\t\tcalled := false\n\t\tt.Cleanup(func() { assert.True(t, called) })\n\t\treturn func(\n\t\t\t_ io.Writer, _ *util.OutputFlags, ts []dto.Task) error {\n\t\t\tcalled = true\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tdFactory := func(t *testing.T) cmdutil.Factory {\n\t\tf := mocks.NewMockFactory(t)\n\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\treturn f\n\t}\n\n\ttts := []struct {\n\t\tname    string\n\t\targs    []string\n\t\tfactory func(*testing.T) cmdutil.Factory\n\t\treport  func(*testing.T) func(\n\t\t\tio.Writer, *util.OutputFlags, []dto.Task) error\n\t\terr string\n\t}{\n\t\t{\n\t\t\tname:    \"only one format\",\n\t\t\targs:    []string{\"--format={}\", \"-q\", \"-j\", \"OK\", \"-p=OK\"},\n\t\t\terr:     \"flags can't be used together.*format.*json.*quiet\",\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"name required\",\n\t\t\targs:    []string{\"-p=OK\"},\n\t\t\terr:     `requires arg name`,\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname:    \"project required\",\n\t\t\targs:    []string{\"OK\"},\n\t\t\terr:     `\"project\" not set`,\n\t\t\tfactory: dFactory,\n\t\t},\n\t\t{\n\t\t\tname: \"client error\",\n\t\t\terr:  \"client error\",\n\t\t\targs: []string{\"a\", \"-p=b\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.EXPECT().Client().Return(nil, errors.New(\"client error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workspace error\",\n\t\t\terr:  \"workspace error\",\n\t\t\targs: []string{\"a\", \"-p=b\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.EXPECT().Client().Return(mocks.NewMockClient(t), nil)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"\", errors.New(\"workspace error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup project\",\n\t\t\terr:  \"no project\",\n\t\t\targs: []string{\"error\", \"-p=cli\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.EXPECT().Config().Return(cf)\n\t\t\t\tcf.EXPECT().IsAllowNameForID().Return(true)\n\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{}, errors.New(\"no project\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"http error\",\n\t\t\terr:  \"http error\",\n\t\t\targs: []string{\"error\", \"-p=ok\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.EXPECT().Config().Return(cf)\n\t\t\t\tcf.EXPECT().IsAllowNameForID().Return(false)\n\n\t\t\t\tc.EXPECT().AddTask(api.AddTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tProjectID: \"ok\",\n\t\t\t\t\tName:      \"error\",\n\t\t\t\t}).\n\t\t\t\t\tReturn(dto.Task{}, errors.New(\"http error\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"add one task\",\n\t\t\targs: []string{\n\t\t\t\t\"--project=cli\",\n\t\t\t\t\"Add\",\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID: true,\n\t\t\t\t})\n\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return(\n\t\t\t\t\t[]dto.Project{{ID: \"p-1\", Name: \"Clockify CLI\"}}, nil)\n\n\t\t\t\tc.EXPECT().AddTask(api.AddTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tName:      \"Add\",\n\t\t\t\t\tProjectID: \"p-1\",\n\t\t\t\t}).\n\t\t\t\t\tReturn(dto.Task{ID: \"t-id\"}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: shouldCall,\n\t\t},\n\t\t{\n\t\t\tname: \"add multiple tasks\",\n\t\t\targs: []string{\n\t\t\t\t\"--project=cli\",\n\t\t\t\t\"Task 00\",\n\t\t\t\t\"Task 01\",\n\t\t\t\t\"Task 02\",\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID: true,\n\t\t\t\t})\n\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return(\n\t\t\t\t\t[]dto.Project{{ID: \"p-1\", Name: \"Clockify CLI\"}}, nil)\n\n\t\t\t\tc.EXPECT().AddTask(api.AddTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tName:      \"Task 00\",\n\t\t\t\t\tProjectID: \"p-1\",\n\t\t\t\t}).\n\t\t\t\t\tReturn(dto.Task{ID: \"00\"}, nil)\n\n\t\t\t\tc.EXPECT().AddTask(api.AddTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tName:      \"Task 01\",\n\t\t\t\t\tProjectID: \"p-1\",\n\t\t\t\t}).\n\t\t\t\t\tReturn(dto.Task{ID: \"01\"}, nil)\n\n\t\t\t\tc.EXPECT().AddTask(api.AddTaskParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tName:      \"Task 02\",\n\t\t\t\t\tProjectID: \"p-1\",\n\t\t\t\t}).\n\t\t\t\t\tReturn(dto.Task{ID: \"02\"}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\treport: shouldCall,\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := func(io.Writer, *util.OutputFlags, []dto.Task) error {\n\t\t\t\tassert.Fail(t, \"failed\")\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif tt.report != nil {\n\t\t\t\tr = tt.report(t)\n\t\t\t}\n\n\t\t\tcmd := quickadd.NewCmdQuickAdd(tt.factory(t), r)\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n}\n\nfunc TestCmdQuickAddReport(t *testing.T) {\n\tpr := dto.Task{Name: \"Coderockr\"}\n\ttts := []struct {\n\t\tname   string\n\t\targs   []string\n\t\tassert func(*testing.T, *util.OutputFlags, dto.Task)\n\t}{\n\t\t{\n\t\t\tname: \"report quiet\",\n\t\t\targs: []string{\"-q\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Task) {\n\t\t\t\tassert.True(t, of.Quiet)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report json\",\n\t\t\targs: []string{\"--json\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Task) {\n\t\t\t\tassert.True(t, of.JSON)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report format\",\n\t\t\targs: []string{\"--format={{.ID}}\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, c dto.Task) {\n\t\t\t\tassert.Equal(t, \"{{.ID}}\", of.Format)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report csv\",\n\t\t\targs: []string{\"--csv\"},\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) {\n\t\t\t\tassert.True(t, of.CSV)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report default\",\n\t\t\tassert: func(t *testing.T, of *util.OutputFlags, _ dto.Task) {\n\t\t\t\tassert.False(t, of.CSV)\n\t\t\t\tassert.False(t, of.JSON)\n\t\t\t\tassert.False(t, of.Quiet)\n\t\t\t\tassert.True(t, of.Format == \"\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.EXPECT().Client().Return(c, nil)\n\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\tReturn(\"w\", nil)\n\n\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\tf.EXPECT().Config().Return(cf)\n\n\t\t\tcf.EXPECT().IsAllowNameForID().Return(false)\n\n\t\t\tc.EXPECT().AddTask(api.AddTaskParam{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tProjectID: \"p-1\",\n\t\t\t\tName:      \"Task Add\",\n\t\t\t}).\n\t\t\t\tReturn(pr, nil)\n\n\t\t\tcalled := false\n\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\tcmd := quickadd.NewCmdQuickAdd(f, func(\n\t\t\t\t_ io.Writer, of *util.OutputFlags, ts []dto.Task) error {\n\t\t\t\tu := ts[0]\n\t\t\t\tcalled = true\n\t\t\t\tassert.Equal(t, pr, u)\n\t\t\t\ttt.assert(t, of, u)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(append(tt.args, \"Task Add\", \"-p=p-1\"))\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/task/task.go",
    "content": "package task\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/add\"\n\tdel \"github.com/lucassabreu/clockify-cli/pkg/cmd/task/delete\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/done\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/edit\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/task/list\"\n\tquickadd \"github.com/lucassabreu/clockify-cli/pkg/cmd/task/quick-add\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdTask represents the client command\nfunc NewCmdTask(f cmdutil.Factory) *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:     \"task\",\n\t\tAliases: []string{\"tasks\"},\n\t\tShort:   \"Work with Clockify tasks\",\n\t}\n\n\tcmd.AddCommand(list.NewCmdList(f, nil))\n\tcmd.AddCommand(add.NewCmdAdd(f, nil))\n\tcmd.AddCommand(quickadd.NewCmdQuickAdd(f, nil))\n\tcmd.AddCommand(edit.NewCmdEdit(f, nil))\n\tcmd.AddCommand(del.NewCmdDelete(f, nil))\n\tcmd.AddCommand(done.NewCmdDone(f, nil))\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/task/util/read-flags.go",
    "content": "package util\n\nimport (\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/search\"\n\t\"github.com/spf13/cobra\"\n)\n\n// TaskAddPropFlags add common flags expected on editing a task\nfunc TaskAddPropFlags(cmd *cobra.Command, f cmdutil.Factory) {\n\tcmd.Flags().StringP(\"name\", \"n\", \"\", \"new name of the task\")\n\tcmd.Flags().Int32P(\"estimate\", \"E\", 0, \"estimation on hours\")\n\tcmd.Flags().Bool(\"billable\", false, \"sets the task as billable\")\n\tcmd.Flags().Bool(\"not-billable\", false, \"sets the task as not billable\")\n\n\tcmd.Flags().StringSliceP(\"assignee\", \"A\", []string{},\n\t\t\"list of users that are assigned to this task\")\n\t_ = cmdcompl.AddSuggestionsToFlag(cmd, \"assignee\",\n\t\tcmdcomplutil.NewUserAutoComplete(f))\n\n\tcmd.Flags().Bool(\"no-assignee\", false,\n\t\t\"cleans the assignee list\")\n\n\tcmdutil.AddProjectFlags(cmd, f)\n}\n\n// FlagsDTO holds data about editing or creating a Task\ntype FlagsDTO struct {\n\tWorkspace   string\n\tProjectID   string\n\tName        string\n\tEstimate    *time.Duration\n\tAssigneeIDs *[]string\n\tBillable    *bool\n}\n\n// TaskReadFlags read the common flags expected when editing a task\nfunc TaskReadFlags(cmd *cobra.Command, f cmdutil.Factory) (p FlagsDTO, err error) {\n\tif err := cmdutil.XorFlag(map[string]bool{\n\t\t\"assignee\":    cmd.Flags().Changed(\"assignee\"),\n\t\t\"no-assignee\": cmd.Flags().Changed(\"no-assignee\"),\n\t}); err != nil {\n\t\treturn p, err\n\t}\n\n\tif err := cmdutil.XorFlag(map[string]bool{\n\t\t\"billable\":     cmd.Flags().Changed(\"billable\"),\n\t\t\"not-billable\": cmd.Flags().Changed(\"not-billable\"),\n\t}); err != nil {\n\t\treturn p, err\n\t}\n\n\tif p.Workspace, err = f.GetWorkspaceID(); err != nil {\n\t\treturn\n\t}\n\n\tp.ProjectID, _ = cmd.Flags().GetString(\"project\")\n\tp.Name, _ = cmd.Flags().GetString(\"name\")\n\n\tif cmd.Flags().Changed(\"estimate\") {\n\t\te, _ := cmd.Flags().GetInt32(\"estimate\")\n\t\td := time.Duration(e) * time.Hour\n\t\tp.Estimate = &d\n\t}\n\n\tif cmd.Flags().Changed(\"assignee\") {\n\t\tassignees, _ := cmd.Flags().GetStringSlice(\"assignee\")\n\t\tp.AssigneeIDs = &assignees\n\t}\n\n\tif f.Config().IsAllowNameForID() {\n\t\tc, err := f.Client()\n\t\tif err != nil {\n\t\t\treturn p, err\n\t\t}\n\n\t\tif p.ProjectID, err = search.GetProjectByName(\n\t\t\tc, f.Config(), p.Workspace, p.ProjectID, \"\"); err != nil {\n\t\t\treturn p, err\n\t\t}\n\n\t\tif p.AssigneeIDs != nil {\n\t\t\tas := *p.AssigneeIDs\n\t\t\tif as, err = search.GetUsersByName(\n\t\t\t\tc, p.Workspace, as); err != nil {\n\t\t\t\treturn p, err\n\t\t\t}\n\t\t\tp.AssigneeIDs = &as\n\t\t}\n\t}\n\n\tif cmd.Flags().Changed(\"no-assignee\") {\n\t\tvar a []string\n\n\t\tp.AssigneeIDs = &a\n\t}\n\n\tswitch {\n\tcase cmd.Flags().Changed(\"billable\"):\n\t\tb := true\n\t\tp.Billable = &b\n\tcase cmd.Flags().Changed(\"not-billable\"):\n\t\tb := false\n\t\tp.Billable = &b\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/cmd/task/util/report.go",
    "content": "package util\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/output/task\"\n\t\"github.com/spf13/cobra\"\n)\n\n// OutputFlags sets how the tasks will be printed\ntype OutputFlags struct {\n\tFormat string\n\tJSON   bool\n\tCSV    bool\n\tQuiet  bool\n}\n\n// Check guaranties that only one type of output is chosen\nfunc (of OutputFlags) Check() error {\n\treturn cmdutil.XorFlag(map[string]bool{\n\t\t\"format\": of.Format != \"\",\n\t\t\"json\":   of.JSON,\n\t\t\"csv\":    of.CSV,\n\t\t\"quiet\":  of.Quiet,\n\t})\n}\n\n// TaskAddReportFlags will add common format flags used for tasks\nfunc TaskAddReportFlags(cmd *cobra.Command, of *OutputFlags) {\n\tcmd.Flags().StringVarP(&of.Format, \"format\", \"f\", \"\",\n\t\t\"golang text/template format to be applied on each Client\")\n\tcmd.Flags().BoolVarP(&of.JSON, \"json\", \"j\", false, \"print as JSON\")\n\tcmd.Flags().BoolVarP(&of.CSV, \"csv\", \"v\", false, \"print as CSV\")\n\tcmd.Flags().BoolVarP(&of.Quiet, \"quiet\", \"q\", false, \"only display ids\")\n}\n\n// TaskReport will output the task as set by the flags\nfunc TaskReport(cmd *cobra.Command, of OutputFlags, tasks ...dto.Task) error {\n\tout := cmd.OutOrStdout()\n\n\tswitch {\n\tcase of.JSON:\n\t\treturn task.TasksJSONPrint(tasks, out)\n\tcase of.CSV:\n\t\treturn task.TasksCSVPrint(tasks, out)\n\tcase of.Quiet:\n\t\treturn task.TaskPrintQuietly(tasks, out)\n\tcase of.Format != \"\":\n\t\treturn task.TaskPrintWithTemplate(of.Format)(tasks, out)\n\tdefault:\n\t\treturn task.TaskPrint(tasks, out)\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/clone/clone.go",
    "content": "package clone\n\nimport (\n\t\"strings\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\ttimeentry \"github.com/lucassabreu/clockify-cli/pkg/output/time-entry\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timeentryhlp\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdClone represents the clone command\nfunc NewCmdClone(f cmdutil.Factory) *cobra.Command {\n\tof := util.OutputFlags{TimeFormat: timeentry.TimeFormatSimple}\n\tcmd := &cobra.Command{\n\t\tUse: \"clone \" +\n\t\t\t\"{ <time-entry-id> | \" + timeentryhlp.AliasLast + \"| ^<n> }\",\n\t\tShort: \"Copy a time entry and starts it \",\n\t\tLong: heredoc.Docf(`\n\t\t\tCopy a time entry and starts it.\n\n\t\t\tRunning 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.\n\n\t\t\tIf you want to clone the last (running) time entry you can use \"%[2]s\" instead of its ID.\n\t\t\tAlso if you want to clone the one previous to it, you can use \"^2\", for the before that \"^3\" and so on.\n\n\t\t\tThe rules defined in the workspace and project will be checked before creating it.\n\t\t`, \"`\", timeentryhlp.AliasLast) + \"\\n\" +\n\t\t\tutil.HelpTimeEntryNowIfNotSet +\n\t\t\t\"The same applies to end time (`--when-to-close`).\\n\\n\" +\n\t\t\tutil.HelpInteractiveByDefault + \"\\n\" +\n\t\t\tutil.HelpTimeInputOnTimeEntry + \"\\n\" +\n\t\t\tutil.HelpNamesForIds + \"\\n\" +\n\t\t\tutil.HelpMoreInfoAboutStarting + \"\\n\" +\n\t\t\tutil.HelpMoreInfoAboutPrinting,\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s in --project cli --tag dev -d \"Adding docs to clone\" --task \"clone\" --md\n\t\t\tID: %[2]s62ae4b304ebb4f143c931d50%[2]s  \n\t\t\tBillable: %[2]syes%[2]s  \n\t\t\tLocked: %[2]sno%[2]s  \n\t\t\tProject: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s)  \n\t\t\tTask: Clone Command (%[2]s62ae4af04ebb4f143c931d2e%[2]s)  \n\t\t\tInterval: %[2]s2022-06-18 22:01:16%[2]s until %[2]snow%[2]s  \n\t\t\tDescription:\n\t\t\t> Adding docs to clone\n\n\t\t\tTags:\n\t\t\t * Development (%[2]s62ae28b72518aa18da2acb49%[2]s)\n\n\t\t\t$ %[1]s clone last -d \"Adding examples to clone\" --md\n\t\t\tID: %[2]s62ae4b304ebb4f143c931d50%[2]s  \n\t\t\tBillable: %[2]syes%[2]s  \n\t\t\tLocked: %[2]sno%[2]s  \n\t\t\tProject: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s)  \n\t\t\tTask: Clone Command (%[2]s62ae4af04ebb4f143c931d2e%[2]s)  \n\t\t\tInterval: %[2]s2022-06-18 22:11:16%[2]s until %[2]snow%[2]s  \n\t\t\tDescription:\n\t\t\t> Adding examples to clone\n\n\t\t\tTags:\n\t\t\t * Development (%[2]s62ae28b72518aa18da2acb49%[2]s)\n\n\t\t\t$ %[1]s clone last -d \"Adding examples to in\" -T pair --task \"in command\" --md\n\t\t\tID: %[2]s62ae4dfe4ebb4f143c932106%[2]s\n\t\t\tBillable: %[2]syes%[2]s\n\t\t\tLocked: %[2]sno%[2]s\n\t\t\tProject: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s)\n\t\t\tTask: In Command (%[2]s62ae29e62518aa18da2acd14%[2]s)\n\t\t\tInterval: %[2]s2022-06-18 22:13:14%[2]s until %[2]snow%[2]s\n\t\t\tDescription:\n\t\t\t> Adding examples to in\n\n\t\t\tTags:\n\t\t\t * Pair Programming (%[2]s621948708cb9606d934ebba7%[2]s)\n\t\t`, \"clockify-cli\", \"`\"),\n\t\tArgs:      cmdutil.RequiredNamedArgs(\"time entry id\"),\n\t\tValidArgs: []string{timeentryhlp.AliasLast},\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tvar err error\n\t\t\tvar w, u string\n\n\t\t\tif w, err = f.GetWorkspaceID(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif u, err = f.GetUserID(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tid := strings.ToLower(strings.TrimSpace(args[0]))\n\t\t\tif id == timeentryhlp.AliasLast {\n\t\t\t\tid = timeentryhlp.AliasLatest\n\t\t\t}\n\n\t\t\ttec, err := timeentryhlp.GetTimeEntry(c, w, u, id)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\ttec.UserID = u\n\t\t\ttec.TimeInterval = dto.NewTimeInterval(timehlp.Now(), nil)\n\n\t\t\tnoClosing, _ := cmd.Flags().GetBool(\"no-closing\")\n\n\t\t\tdc := util.NewDescriptionCompleter(f)\n\n\t\t\tte := util.TimeEntryImplToDTO(tec)\n\t\t\tif te, err = util.Do(\n\t\t\t\tte,\n\t\t\t\tutil.FillTimeEntryWithFlags(cmd.Flags()),\n\t\t\t\tfunc(tec util.TimeEntryDTO) (util.TimeEntryDTO, error) {\n\t\t\t\t\tif noClosing {\n\t\t\t\t\t\treturn tec, nil\n\t\t\t\t\t}\n\n\t\t\t\t\treturn util.ValidateClosingTimeEntry(f)(tec)\n\t\t\t\t},\n\t\t\t\tutil.GetAllowNameForIDsFn(f.Config(), c),\n\t\t\t\tutil.GetPropsInteractiveFn(dc, f),\n\t\t\t\tutil.GetDatesInteractiveFn(f),\n\t\t\t\tutil.GetValidateTimeEntryFn(f),\n\t\t\t\tfunc(tec util.TimeEntryDTO) (util.TimeEntryDTO, error) {\n\t\t\t\t\tif noClosing {\n\t\t\t\t\t\treturn tec, nil\n\t\t\t\t\t}\n\n\t\t\t\t\treturn util.OutInProgressFn(c)(tec)\n\t\t\t\t},\n\t\t\t\tutil.CreateTimeEntryFn(c),\n\t\t\t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn util.PrintTimeEntryImpl(\n\t\t\t\tutil.TimeEntryDTOToImpl(te), f, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tutil.AddTimeEntryFlags(cmd, f, &of)\n\tutil.AddTimeEntryDateFlags(cmd)\n\tcmd.Flags().BoolP(\"no-closing\", \"\", false,\n\t\t\"don't close any active time entry\")\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/delete/delete.go",
    "content": "package del\n\nimport (\n\t\"errors\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timeentryhlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdDelete represents the delete command\nfunc NewCmdDelete(f cmdutil.Factory) *cobra.Command {\n\tva := cmdcompl.ValidArgsSlide{timeentryhlp.AliasCurrent, timeentryhlp.AliasLast}\n\tcmd := &cobra.Command{\n\t\tUse: \"delete { <time-entry-id> | \" +\n\t\t\tva.IntoUseOptions() + \" }...\",\n\t\tAliases:   []string{\"del\", \"rm\", \"remove\"},\n\t\tArgs:      cmdutil.RequiredNamedArgs(\"time entry id\"),\n\t\tValidArgs: va.IntoValidArgs(),\n\t\tShort: `Delete time entry(ies), use id \"` +\n\t\t\ttimeentryhlp.AliasCurrent + `\" to apply to time entry in progress`,\n\t\tLong: heredoc.Docf(`\n\t\t\tDelete time entries\n\n\t\t\tIf you want to delete the current (running) time entry you can use \"%s\" instead of its ID.\n\n\t\t\t**Important**: this action can't be reverted, once the time entry is deleted its ID is lost.\n\t\t`,\n\t\t\ttimeentryhlp.AliasCurrent,\n\t\t),\n\t\tExample: heredoc.Docf(`\n\t\t\t# trying to delete a time entry that does not exist, or from other workspace\n\t\t\t$ %[1]s 62af70d849445270d7c09fbc\n\t\t\tdelete time entry \"62af70d849445270d7c09fbc\": TIMEENTRY with id 62af70d849445270d7c09fbc doesn't belong to WORKSPACE with id cccccccccccccccccccccccc (code: 501)\n\n\t\t\t# deleting the running time entry\n\t\t\t$ %[1]s current\n\t\t\t# no output\n\n\t\t\t# deleting the last time entry\n\t\t\t$ %[1]s last\n\t\t\t# no output\n\n\t\t\t# deleting multiple time entries\n\t\t\t$ %[1]s 62b5b51085815e619d7ae18d 62b5d55185815e619d7af928\n\t\t\t# no output\n\t\t`, \"clockify-cli delete\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tvar err error\n\t\t\tvar w, u string\n\n\t\t\tif w, err = f.GetWorkspaceID(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif u, err = f.GetUserID(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfor i := range args {\n\t\t\t\tp := api.DeleteTimeEntryParam{\n\t\t\t\t\tWorkspace:   w,\n\t\t\t\t\tTimeEntryID: args[i],\n\t\t\t\t}\n\n\t\t\t\tif p.TimeEntryID == timeentryhlp.AliasCurrent {\n\t\t\t\t\tte, err := c.GetTimeEntryInProgress(\n\t\t\t\t\t\tapi.GetTimeEntryInProgressParam{\n\t\t\t\t\t\t\tWorkspace: p.Workspace,\n\t\t\t\t\t\t\tUserID:    u,\n\t\t\t\t\t\t})\n\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tif te == nil {\n\t\t\t\t\t\treturn errors.New(\"there is no time entry in progress\")\n\t\t\t\t\t}\n\n\t\t\t\t\tp.TimeEntryID = te.ID\n\t\t\t\t}\n\n\t\t\t\tif p.TimeEntryID == timeentryhlp.AliasLast {\n\t\t\t\t\tte, err := timeentryhlp.GetLatestEntryEntry(c, p.Workspace, u)\n\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tp.TimeEntryID = te.ID\n\t\t\t\t}\n\n\t\t\t\tif err := c.DeleteTimeEntry(p); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t}\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/edit/edit.go",
    "content": "package edit\n\nimport (\n\t\"io\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\toutput \"github.com/lucassabreu/clockify-cli/pkg/output/time-entry\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timeentryhlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdEdit represents the edit command\nfunc NewCmdEdit(\n\tf cmdutil.Factory,\n\treport func(dto.TimeEntryImpl, io.Writer, util.OutputFlags) error,\n) *cobra.Command {\n\tof := util.OutputFlags{TimeFormat: output.TimeFormatSimple}\n\tva := cmdcompl.ValidArgsSlide{\n\t\ttimeentryhlp.AliasCurrent, timeentryhlp.AliasLast}\n\tcmd := &cobra.Command{\n\t\tUse: \"edit { <time-entry-id> | \" + va.IntoUseOptions() +\n\t\t\t\" | ^n }\",\n\t\tAliases: []string{\"update\"},\n\t\tArgs: cobra.MatchAll(\n\t\t\tcmdutil.RequiredNamedArgs(\"time entry id\"),\n\t\t\tcobra.ExactArgs(1),\n\t\t),\n\t\tValidArgs: va.IntoValidArgs(),\n\t\tShort:     `Edit a time entry`,\n\t\tLong: heredoc.Docf(`\n\t\t\tEdit a time entry.\n\t\t\tOnly the inputs sent thought flags will be changed, any other properties will remain the same.\n\n\t\t\t%s\n\t\t\t%s\n\t\t\t%s\n\t\t\t%s\n\t\t\t%s\n\t\t`,\n\t\t\tutil.HelpTimeEntriesAliasForEdit,\n\t\t\tutil.HelpInteractiveByDefault,\n\t\t\tutil.HelpDateTimeFormats,\n\t\t\tutil.HelpNamesForIds,\n\t\t\tutil.HelpMoreInfoAboutPrinting,\n\t\t),\n\t\tExample: heredoc.Docf(`\n\t\t\t# starting a time entry\n\t\t\t$ %[1]s in --project cli --tag dev -d \"Adding docs to edit\" --task \"edit\" --md\n\t\t\tID: %[2]s62ae4b304ebb4f143c931d50%[2]s  \n\t\t\tBillable: %[2]syes%[2]s  \n\t\t\tLocked: %[2]sno%[2]s  \n\t\t\tProject: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s)  \n\t\t\tTask: Edit Command (%[2]s62ae4af04ebb4f143c931d2e%[2]s)  \n\t\t\tInterval: %[2]s2022-06-18 22:01:16%[2]s until %[2]snow%[2]s  \n\t\t\tDescription:\n\t\t\t> Adding docs to edit\n\n\t\t\tTags:\n\t\t\t * Development (%[2]s62ae28b72518aa18da2acb49%[2]s)\n\n\t\t\t# changing the description on the running time entry\n\t\t\t$ %[1]s edit current -d \"Adding examples to edit\" --md\n\t\t\tID: %[2]s62ae4b304ebb4f143c931d50%[2]s  \n\t\t\tBillable: %[2]syes%[2]s  \n\t\t\tLocked: %[2]sno%[2]s  \n\t\t\tProject: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s)  \n\t\t\tTask: Edit Command (%[2]s62ae4af04ebb4f143c931d2e%[2]s)  \n\t\t\tInterval: %[2]s2022-06-18 22:01:16%[2]s until %[2]snow%[2]s  \n\t\t\tDescription:\n\t\t\t> Adding examples to edit\n\n\t\t\tTags:\n\t\t\t * Development (%[2]s62ae28b72518aa18da2acb49%[2]s)\n\n\t\t\t# change the description, task, and tags\n\t\t\t$ %[1]s edit -d \"Adding examples to edit\" -T pair --task \"in command\" --md\n\t\t\tID: %[2]s62ae4b304ebb4f143c931d50%[2]s  \n\t\t\tBillable: %[2]syes%[2]s\n\t\t\tLocked: %[2]sno%[2]s\n\t\t\tProject: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s)\n\t\t\tTask: In Command (%[2]s62ae29e62518aa18da2acd14%[2]s)\n\t\t\tInterval: %[2]s2022-06-18 22:13:14%[2]s until %[2]snow%[2]s\n\t\t\tDescription:\n\t\t\t> Adding examples to edit\n\n\t\t\tTags:\n\t\t\t * Pair Programming (%[2]s621948708cb9606d934ebba7%[2]s)\n\t\t`, \"clockify-cli\", \"`\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tuserID, err := f.GetUserID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tw, err := f.GetWorkspaceID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\ttei, err := timeentryhlp.GetTimeEntry(\n\t\t\t\tc,\n\t\t\t\tw,\n\t\t\t\tuserID,\n\t\t\t\targs[0],\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tte := util.TimeEntryImplToDTO(tei)\n\t\t\tdc := util.NewDescriptionCompleter(f)\n\n\t\t\tif te, err = util.Do(\n\t\t\t\tte,\n\t\t\t\tutil.FillTimeEntryWithFlags(cmd.Flags()),\n\t\t\t\tutil.GetAllowNameForIDsFn(f.Config(), c),\n\t\t\t\tutil.GetPropsInteractiveFn(dc, f),\n\t\t\t\tutil.GetDatesInteractiveFn(f),\n\t\t\t\tutil.GetValidateTimeEntryFn(f),\n\t\t\t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif tei, err = c.UpdateTimeEntry(api.UpdateTimeEntryParam{\n\t\t\t\tWorkspace:   te.Workspace,\n\t\t\t\tTimeEntryID: te.ID,\n\t\t\t\tDescription: te.Description,\n\t\t\t\tStart:       te.Start,\n\t\t\t\tEnd:         te.End,\n\t\t\t\tBillable:    *te.Billable,\n\t\t\t\tProjectID:   te.ProjectID,\n\t\t\t\tTaskID:      te.TaskID,\n\t\t\t\tTagIDs:      te.TagIDs,\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn report(tei, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tutil.AddTimeEntryFlags(cmd, f, &of)\n\n\tcmd.Flags().StringP(\"when\", \"s\", \"\",\n\t\t\"when the entry should be started\")\n\tcmd.Flags().StringP(\"when-to-close\", \"e\", \"\",\n\t\t\"when the entry should be closed (same formats as `when`)\")\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/edit/edit_test.go",
    "content": "package edit_test\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/edit\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewCmdEditWhenChangingProjectOrTask(t *testing.T) {\n\tw := dto.Workspace{ID: \"w\"}\n\tte := dto.TimeEntryImpl{\n\t\tWorkspaceID: w.ID,\n\t\tID:          \"timeentryid\",\n\t\tDescription: \"Something\",\n\t\tProjectID:   \"oldproj\",\n\t\tTaskID:      \"oldtask\",\n\t\tTimeInterval: dto.TimeInterval{\n\t\t\tStart: time.Now(),\n\t\t},\n\t}\n\n\ttts := []struct {\n\t\tname        string\n\t\targs        []string\n\t\tproject     *dto.Project\n\t\tupdateParam api.UpdateTimeEntryParam\n\t}{\n\t\t{\n\t\t\tname:    \"should remove task, when changing project\",\n\t\t\targs:    []string{\"-p\", \"newproj\"},\n\t\t\tproject: &dto.Project{ID: \"newproj\", Name: \"newproj\"},\n\t\t\tupdateParam: api.UpdateTimeEntryParam{\n\t\t\t\tWorkspace:   te.WorkspaceID,\n\t\t\t\tTimeEntryID: te.ID,\n\t\t\t\tStart:       te.TimeInterval.Start,\n\t\t\t\tEnd:         te.TimeInterval.End,\n\t\t\t\tBillable:    te.Billable,\n\t\t\t\tDescription: te.Description,\n\t\t\t\tProjectID:   \"newproj\",\n\t\t\t\tTaskID:      \"\",\n\t\t\t\tTagIDs:      te.TagIDs,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"should remove task, when removing project\",\n\t\t\targs: []string{\"-p\", \"\"},\n\t\t\tupdateParam: api.UpdateTimeEntryParam{\n\t\t\t\tWorkspace:   te.WorkspaceID,\n\t\t\t\tTimeEntryID: te.ID,\n\t\t\t\tStart:       te.TimeInterval.Start,\n\t\t\t\tEnd:         te.TimeInterval.End,\n\t\t\t\tBillable:    te.Billable,\n\t\t\t\tDescription: te.Description,\n\t\t\t\tProjectID:   \"\",\n\t\t\t\tTaskID:      \"\",\n\t\t\t\tTagIDs:      te.TagIDs,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"should change project and task\",\n\t\t\targs:    []string{\"--task\", \"newtask\", \"-p=newproj\"},\n\t\t\tproject: &dto.Project{ID: \"newproj\", Name: \"newproj\"},\n\t\t\tupdateParam: api.UpdateTimeEntryParam{\n\t\t\t\tWorkspace:   te.WorkspaceID,\n\t\t\t\tTimeEntryID: te.ID,\n\t\t\t\tStart:       te.TimeInterval.Start,\n\t\t\t\tEnd:         te.TimeInterval.End,\n\t\t\t\tBillable:    te.Billable,\n\t\t\t\tDescription: te.Description,\n\t\t\t\tProjectID:   \"newproj\",\n\t\t\t\tTaskID:      \"newtask\",\n\t\t\t\tTagIDs:      te.TagIDs,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\ttt := &tts[i]\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\n\t\t\tf.EXPECT().GetUserID().Return(\"u\", nil)\n\t\t\tf.EXPECT().GetWorkspace().Return(w, nil)\n\t\t\tf.EXPECT().GetWorkspaceID().Return(w.ID, nil)\n\n\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\tAllowNameForID: true,\n\t\t\t})\n\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\tc.EXPECT().GetTimeEntryInProgress(api.GetTimeEntryInProgressParam{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t\tUserID:    \"u\",\n\t\t\t}).\n\t\t\t\tReturn(&te, nil)\n\n\t\t\tp := tt.project\n\t\t\tif p != nil {\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       w.ID,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{*p}, nil)\n\n\t\t\t\tc.EXPECT().GetProject(api.GetProjectParam{\n\t\t\t\t\tWorkspace: w.ID,\n\t\t\t\t\tProjectID: p.ID,\n\t\t\t\t}).\n\t\t\t\t\tReturn(p, nil)\n\t\t\t}\n\n\t\t\tif tt.updateParam.TaskID != \"\" {\n\t\t\t\tc.EXPECT().GetTasks(api.GetTasksParam{\n\t\t\t\t\tWorkspace:       w.ID,\n\t\t\t\t\tProjectID:       tt.updateParam.ProjectID,\n\t\t\t\t\tActive:          true,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Task{{ID: tt.updateParam.TaskID}}, nil)\n\t\t\t}\n\n\t\t\tc.EXPECT().UpdateTimeEntry(tt.updateParam).\n\t\t\t\tReturn(te, nil)\n\n\t\t\tcalled := false\n\t\t\tcmd := edit.NewCmdEdit(f, func(\n\t\t\t\t_ dto.TimeEntryImpl, _ io.Writer, _ util.OutputFlags) error {\n\t\t\t\tcalled = true\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SilenceErrors = true\n\n\t\t\tout := bytes.NewBufferString(\"\")\n\t\t\tcmd.SetOut(out)\n\t\t\tcmd.SetErr(out)\n\n\t\t\tcmd.SetArgs(append(tt.args, \"current\", \"-q\"))\n\t\t\t_, err := cmd.ExecuteC()\n\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tt.Cleanup(func() {\n\t\t\t\t\tassert.True(t, called)\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tt.Fatalf(\"err: %s\", err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/edit-multipple/edit-multiple.go",
    "content": "package editmultiple\n\nimport (\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\toutput \"github.com/lucassabreu/clockify-cli/pkg/output/time-entry\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timeentryhlp\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdEditMultiple represents the editMultiple command\nfunc NewCmdEditMultiple(f cmdutil.Factory) *cobra.Command {\n\tof := util.OutputFlags{TimeFormat: output.TimeFormatSimple}\n\tcmd := &cobra.Command{\n\t\tUse: \"edit-multiple { <time-entry-id> | \" +\n\t\t\ttimeentryhlp.AliasCurrent + \" | \" + timeentryhlp.AliasLast +\n\t\t\t\" }...\",\n\t\tAliases: []string{\n\t\t\t\"update-multiple\", \"multi-edit\",\n\t\t\t\"multi-update\", \"mult-edit\", \"mult-update\",\n\t\t},\n\t\tArgs: cobra.MatchAll(\n\t\t\tcmdutil.RequiredNamedArgs(\"time entry id\"),\n\t\t\tcobra.MinimumNArgs(2),\n\t\t),\n\t\tValidArgs: []string{timeentryhlp.AliasLast, timeentryhlp.AliasCurrent},\n\t\tShort:     `Edit multiple time entries at once`,\n\t\tLong: heredoc.Docf(`\n\t\t\tEdit multiple time entries at once.\n\n\t\t\tThis command does not allow to edit when the time entries start or ended, because different time entries will have different start and end times.\n\n\t\t\tExcept on interactive mode where the values informed, even if not changed will be applied to all entries (except for Start and End time).\n\t\t\tIf you wanna edit only some properties, than use the flags without interactive mode, only the input sent thought the flags will be changed.\n\n\t\t\t%s\n\t\t\t%s\n\t\t\t%s\n\t\t\t%s\n\t\t`,\n\t\t\tutil.HelpTimeEntriesAliasForEdit,\n\t\t\tutil.HelpInteractiveByDefault,\n\t\t\tutil.HelpNamesForIds,\n\t\t\tutil.HelpMoreInfoAboutPrinting,\n\t\t),\n\t\tExample: heredoc.Docf(`\n\t\t\t# just to help show the data\n\t\t\t$ export F=\"{{.ID}} :: {{ .Description }}\n\t\t\t  When: {{ fdt .TimeInterval.Start }} util {{ ft (.TimeInterval.End | now) }}\n\t\t\t  Task: {{ .Task.Name }} ({{ .Project.Name}})\n\t\t\t  Tags: {{ .Tags }}\n\t\t\t\"\n\n\t\t\t$ %[1]s report --format \"$F\"\n\t\t\t62af667c4ebb4f143c9482bb :: Edit multiple entries\n\t\t\t  When: 2022-06-19 18:10:01 util 18:10:15\n\t\t\t  Task: Edit Command (Clockify Cli)\n\t\t\t  Tags: [Development (62ae28b72518aa18da2acb49)]\n\n\t\t\t62af668b49445270d7c092e4 :: Adding examples\n\t\t\t  When: 2022-06-19 18:10:15 util 18:29:32\n\t\t\t  Task: Edit Command (Clockify Cli)\n\t\t\t  Tags: [Development (62ae28b72518aa18da2acb49)]\n\n\t\t\t62af6b0f4ebb4f143c94880e :: More examples\n\t\t\t  When: 2022-06-19 18:29:32 util 18:38:12\n\t\t\t  Task: Edit Command (Clockify Cli)\n\t\t\t  Tags: [Development (62ae28b72518aa18da2acb49)]\n\n\t\t\t# change all to use other task\n\t\t\t$ %[1]s edit-multiple -i=0 -f \"$F\" current last ^2 --task multiple\n\t\t\t62af6b0f4ebb4f143c94880e :: More examples\n\t\t\t  When: 2022-06-19 18:29:32 util 18:43:04\n\t\t\t  Task: Edit Multiple Command (Clockify Cli)\n\t\t\t  Tags: [Development (62ae28b72518aa18da2acb49)]\n\t\t\t62af668b49445270d7c092e4 :: Adding examples\n\t\t\t  When: 2022-06-19 18:10:15 util 18:29:32\n\t\t\t  Task: Edit Multiple Command (Clockify Cli)\n\t\t\t  Tags: [Development (62ae28b72518aa18da2acb49)]\n\t\t\t62af668b49445270d7c092e4 :: Adding examples\n\t\t\t  When: 2022-06-19 18:10:15 util 18:29:32\n\t\t\t  Task: Edit Multiple Command (Clockify Cli)\n\t\t\t  Tags: [Development (62ae28b72518aa18da2acb49)]\n\t\t`, \"clockify-cli\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tvar err error\n\t\t\tvar w, u string\n\n\t\t\tif w, err = f.GetWorkspaceID(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif u, err = f.GetUserID(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tteis := make([]util.TimeEntryDTO, len(args))\n\t\t\tfor i := range args {\n\t\t\t\tt, err := timeentryhlp.GetTimeEntry(c, w, u, args[i])\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tteis[i] = util.TimeEntryImplToDTO(t)\n\t\t\t}\n\n\t\t\ttei := teis[0]\n\t\t\teditFn := func(tei util.TimeEntryDTO) (util.TimeEntryDTO, error) {\n\t\t\t\tt, err := c.UpdateTimeEntry(api.UpdateTimeEntryParam{\n\t\t\t\t\tWorkspace:   tei.Workspace,\n\t\t\t\t\tTimeEntryID: tei.ID,\n\t\t\t\t\tDescription: tei.Description,\n\t\t\t\t\tStart:       tei.Start,\n\t\t\t\t\tEnd:         tei.End,\n\t\t\t\t\tBillable:    *tei.Billable,\n\t\t\t\t\tProjectID:   tei.ProjectID,\n\t\t\t\t\tTaskID:      tei.TaskID,\n\t\t\t\t\tTagIDs:      tei.TagIDs,\n\t\t\t\t})\n\n\t\t\t\treturn util.TimeEntryImplToDTO(t), err\n\t\t\t}\n\n\t\t\tfn := func(input util.TimeEntryDTO) (util.TimeEntryDTO, error) {\n\t\t\t\tvar err error\n\t\t\t\tfor i, tei := range teis {\n\t\t\t\t\tinput.Start = tei.Start\n\t\t\t\t\tinput.End = tei.End\n\t\t\t\t\tinput.ID = tei.ID\n\n\t\t\t\t\tif tei, err = editFn(input); err != nil {\n\t\t\t\t\t\treturn input, err\n\t\t\t\t\t}\n\n\t\t\t\t\tteis[i] = tei\n\t\t\t\t}\n\n\t\t\t\treturn input, err\n\t\t\t}\n\n\t\t\tif !f.Config().IsInteractive() {\n\t\t\t\tfn = func(input util.TimeEntryDTO) (util.TimeEntryDTO, error) {\n\t\t\t\t\tc := cmd.Flags().Changed\n\t\t\t\t\tfor i, tei := range teis {\n\t\t\t\t\t\tif c(\"project\") {\n\t\t\t\t\t\t\ttei.ProjectID = input.ProjectID\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif c(\"description\") {\n\t\t\t\t\t\t\ttei.Description = input.Description\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif c(\"task\") {\n\t\t\t\t\t\t\ttei.TaskID = input.TaskID\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif c(\"tag\") || c(\"tags\") {\n\t\t\t\t\t\t\ttei.TagIDs = input.TagIDs\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif c(\"not-billable\") {\n\t\t\t\t\t\t\ttei.Billable = input.Billable\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tteis[i] = tei\n\t\t\t\t\t\tif _, err = editFn(tei); err != nil {\n\t\t\t\t\t\t\treturn tei, err\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn input, nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdc := util.NewDescriptionCompleter(f)\n\n\t\t\tif _, err = util.Do(\n\t\t\t\ttei,\n\t\t\t\tutil.FillTimeEntryWithFlags(cmd.Flags()),\n\t\t\t\tutil.GetAllowNameForIDsFn(f.Config(), c),\n\t\t\t\tutil.GetPropsInteractiveFn(dc, f),\n\t\t\t\tutil.GetValidateTimeEntryFn(f),\n\t\t\t\tfn,\n\t\t\t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\ttes := make([]dto.TimeEntry, len(teis))\n\t\t\tvar t *dto.TimeEntry\n\t\t\tfor i, tei := range teis {\n\t\t\t\tt, err = c.GetHydratedTimeEntry(api.GetTimeEntryParam{\n\t\t\t\t\tTimeEntryID: tei.ID,\n\t\t\t\t\tWorkspace:   tei.Workspace,\n\t\t\t\t})\n\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\ttes[i] = *t\n\t\t\t}\n\n\t\t\treturn util.PrintTimeEntries(tes,\n\t\t\t\tcmd.OutOrStdout(), f.Config(), of)\n\t\t},\n\t}\n\n\tutil.AddTimeEntryFlags(cmd, f, &of)\n\tutil.AddPrintMultipleTimeEntriesFlags(cmd)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/in/in.go",
    "content": "package in\n\nimport (\n\t\"io\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\toutput \"github.com/lucassabreu/clockify-cli/pkg/output/time-entry\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdIn represents the in command\nfunc NewCmdIn(\n\tf cmdutil.Factory,\n\treport func(dto.TimeEntryImpl, io.Writer, util.OutputFlags) error,\n) *cobra.Command {\n\tof := util.OutputFlags{TimeFormat: output.TimeFormatSimple}\n\tcmd := &cobra.Command{\n\t\tUse:   \"in [<project-id>] [<description>]\",\n\t\tShort: \"Create a new Clockify time entry \",\n\t\tLong: heredoc.Doc(`\n\t\t\tCreate a new Clockify time entry\n\n\t\t\tRunning time entry will be stopped using the start time of this new entry.\n\t\t`) + \"\\n\" +\n\t\t\tutil.HelpTimeEntryNowIfNotSet + \"\\n\" +\n\t\t\tutil.HelpInteractiveByDefault + \"\\n\" +\n\t\t\tutil.HelpTimeInputOnTimeEntry + \"\\n\" +\n\t\t\tutil.HelpNamesForIds + \"\\n\" +\n\t\t\tutil.HelpValidateIncomplete + \"\\n\" +\n\t\t\tutil.HelpMoreInfoAboutPrinting,\n\t\tArgs: cobra.MaximumNArgs(2),\n\t\tValidArgsFunction: cmdcompl.CombineSuggestionsToArgs(\n\t\t\tcmdcomplutil.NewProjectAutoComplete(f, f.Config())),\n\t\tAliases: []string{\"start\"},\n\t\tExample: heredoc.Docf(`\n\t\t\t# start a timer with project and description, starting now\n\t\t\t$ %[1]s -i=0 \"Clockify CLI\" \"Documenting in command\"\n\t\t\t+--------------------------+----------+----------+---------+--------------+------------------------+------+\n\t\t\t|            ID            |  START   |   END    |   DUR   |   PROJECT    |      DESCRIPTION       | TAGS |\n\t\t\t+--------------------------+----------+----------+---------+--------------+------------------------+------+\n\t\t\t| 62ae2744c22de9759e73d038 | 13:28:01 | 13:28:04 | 0:00:03 | Clockify Cli | Documenting in command |      |\n\t\t\t+--------------------------+----------+----------+---------+--------------+------------------------+------+\n\n\t\t\t# start a timer with description, starting at 14:00\n\t\t\t$ %[1]s -i=0 -d \"Documenting in command\" -s \"14:00\"\n\t\t\t+--------------------------+----------+----------+---------+---------+------------------------+------+\n\t\t\t|            ID            |  START   |   END    |   DUR   | PROJECT |      DESCRIPTION       | TAGS |\n\t\t\t+--------------------------+----------+----------+---------+---------+------------------------+------+\n\t\t\t| 62ae27cd49445270d7bf0333 | 14:00:00 | 14:30:21 | 0:30:21 |         | Documenting in command |      |\n\t\t\t+--------------------------+----------+----------+---------+---------+------------------------+------+\n\n\t\t\t# start a timer with description, project and tags, starting 10 min ago\n\t\t\t$ %[1]s -i=0 -p 621948458cb9606d934ebb1c -d \"Documenting in command\" -s -10m --tag dev\n\t\t\t+--------------------------+----------+----------+---------+--------------+------------------------+--------------------------------+\n\t\t\t|            ID            |  START   |   END    |   DUR   |   PROJECT    |      DESCRIPTION       |              TAGS              |\n\t\t\t+--------------------------+----------+----------+---------+--------------+------------------------+--------------------------------+\n\t\t\t| 62ae29104ebb4f143c92f458 | 14:25:41 | 14:35:44 | 0:10:03 | Clockify Cli | Documenting in command | Development                    |\n\t\t\t|                          |          |          |         |              |                        | (62ae28b72518aa18da2acb49)     |\n\t\t\t+--------------------------+----------+----------+---------+--------------+------------------------+--------------------------------+\n\n\t\t\t# start a timer with description, project and task, starting at 10 min, but only showing its ID\n\t\t\t$ %[1]s -i=0 -p 621948458cb9606d934ebb1c -d \"Documenting in command\" -s -10m --task \"in command\"\n\t\t\t62ae29fdc22de9759e73d343\n\n\t\t\t# start a timer without description, with task and project\n\t\t\t$ %[1]s -i=0 -p 621948458cb9606d934ebb1c -s -10m --task \"in command\"\n\t\t\t62ae29fdc22de9759e73d343\n\n\t\t\t# start a timer interactively\n\t\t\t$ %[1]s -i\n\t\t\t? Choose your project: 621948458cb9606d934ebb1c - Clockify Cli      | Client: Myself (6202634a28782767054eec26)\n\t\t\t? Choose your task: 62ae29e62518aa18da2acd14 - In Command\n\t\t\t? Description: Adding more examples\n\t\t\t? Choose your tags: 62ae28b72518aa18da2acb49 - Development, 621948708cb9606d934ebba7 - Pair Programming\n\t\t\t? Start: now\n\t\t\t? End (leave it blank for empty):\n\t\t\t+--------------------------+----------+----------+---------+--------------+----------------------+-----------------------------------------+\n\t\t\t|            ID            |  START   |   END    |   DUR   |   PROJECT    |     DESCRIPTION      |                  TAGS                   |\n\t\t\t+--------------------------+----------+----------+---------+--------------+----------------------+-----------------------------------------+\n\t\t\t| 62ae37b84ebb4f143c930523 | 17:38:14 | 17:38:17 | 0:00:03 | Clockify Cli | Adding more examples | Pair Programming                        |\n\t\t\t|                          |          |          |         |              |                      | (621948708cb9606d934ebba7) Development  |\n\t\t\t|                          |          |          |         |              |                      | (62ae28b72518aa18da2acb49)              |\n\t\t\t+--------------------------+----------+----------+---------+--------------+----------------------+-----------------------------------------+\n\t\t`, \"clockify-cli in\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\ttei := util.TimeEntryDTO{\n\t\t\t\tStart: timehlp.Now(),\n\t\t\t}\n\n\t\t\tif tei.Workspace, err = f.GetWorkspaceID(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif tei.UserID, err = f.GetUserID(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif len(args) > 0 {\n\t\t\t\ttei.ProjectID = args[0]\n\t\t\t}\n\n\t\t\tif len(args) > 1 {\n\t\t\t\ttei.Description = args[1]\n\t\t\t}\n\n\t\t\tdc := util.NewDescriptionCompleter(f)\n\n\t\t\tif tei, err = util.Do(\n\t\t\t\ttei,\n\t\t\t\tutil.FillTimeEntryWithFlags(cmd.Flags()),\n\t\t\t\tutil.ValidateClosingTimeEntry(f),\n\t\t\t\tutil.GetAllowNameForIDsFn(f.Config(), c),\n\t\t\t\tutil.GetPropsInteractiveFn(dc, f),\n\t\t\t\tutil.GetDatesInteractiveFn(f),\n\t\t\t\tutil.FillMissingBillableFn(c),\n\t\t\t\tutil.GetValidateTimeEntryFn(f),\n\t\t\t\tutil.OutInProgressFn(c),\n\t\t\t\tutil.CreateTimeEntryFn(c),\n\t\t\t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn report(\n\t\t\t\tutil.TimeEntryDTOToImpl(tei), cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tutil.AddTimeEntryFlags(cmd, f, &of)\n\tutil.AddTimeEntryDateFlags(cmd)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/in/in_test.go",
    "content": "package in_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/in\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar w = dto.Workspace{ID: \"w\"}\n\nfunc TestNewCmdIn_ShouldBeBothBillableAndNotBillable(t *testing.T) {\n\tf := mocks.NewMockFactory(t)\n\n\tf.EXPECT().GetUserID().Return(\"u\", nil)\n\tf.EXPECT().GetWorkspaceID().Return(w.ID, nil)\n\n\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\n\tc := mocks.NewMockClient(t)\n\tf.EXPECT().Client().Return(c, nil)\n\n\tcalled := false\n\tcmd := in.NewCmdIn(f, func(\n\t\t_ dto.TimeEntryImpl, _ io.Writer, _ util.OutputFlags) error {\n\t\tcalled = true\n\t\treturn nil\n\t})\n\n\tcmd.SilenceUsage = true\n\tcmd.SilenceErrors = true\n\n\tout := bytes.NewBufferString(\"\")\n\tcmd.SetOut(out)\n\tcmd.SetErr(out)\n\n\tcmd.SetArgs([]string{\"--billable\", \"--not-billable\"})\n\t_, err := cmd.ExecuteC()\n\n\tif assert.Error(t, err) {\n\t\tassert.False(t, called)\n\t\tflagErr := &cmdutil.FlagError{}\n\t\tassert.ErrorAs(t, err, &flagErr)\n\t\treturn\n\t}\n\n\tt.Fatal(\"should've failed\")\n}\n\nfunc TestNewCmdIn_ShouldNotSetBillable_WhenNotAsked(t *testing.T) {\n\tbTrue := true\n\tbFalse := false\n\n\ttts := []struct {\n\t\tname  string\n\t\targs  []string\n\t\tparam api.CreateTimeEntryParam\n\t}{\n\t\t{\n\t\t\tname: \"should be nil\",\n\t\t\targs: []string{\"-s=08:00\"},\n\t\t\tparam: api.CreateTimeEntryParam{\n\t\t\t\tWorkspace: w.ID,\n\t\t\t\tStart:     timehlp.Today().Add(8 * time.Hour),\n\t\t\t\tBillable:  nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"should be billable\",\n\t\t\targs: []string{\"-s=08:00\", \"--billable\"},\n\t\t\tparam: api.CreateTimeEntryParam{\n\t\t\t\tWorkspace: w.ID,\n\t\t\t\tStart:     timehlp.Today().Add(8 * time.Hour),\n\t\t\t\tBillable:  &bTrue,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"should not be billable\",\n\t\t\targs: []string{\"-s=08:00\", \"--not-billable\"},\n\t\t\tparam: api.CreateTimeEntryParam{\n\t\t\t\tWorkspace: w.ID,\n\t\t\t\tStart:     timehlp.Today().Add(8 * time.Hour),\n\t\t\t\tBillable:  &bFalse,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\ttt := &tts[i]\n\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\n\t\t\tf.EXPECT().GetUserID().Return(\"u\", nil)\n\t\t\tf.EXPECT().GetWorkspace().Return(w, nil)\n\t\t\tf.EXPECT().GetWorkspaceID().Return(w.ID, nil)\n\n\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\tAllowNameForID: true,\n\t\t\t})\n\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\tc.EXPECT().GetTimeEntryInProgress(api.GetTimeEntryInProgressParam{\n\t\t\t\tWorkspace: w.ID,\n\t\t\t\tUserID:    \"u\",\n\t\t\t}).\n\t\t\t\tReturn(nil, nil)\n\n\t\t\tc.EXPECT().Out(api.OutParam{\n\t\t\t\tWorkspace: w.ID,\n\t\t\t\tUserID:    \"u\",\n\t\t\t\tEnd:       tt.param.Start,\n\t\t\t}).Return(api.ErrorNotFound)\n\n\t\t\tc.EXPECT().CreateTimeEntry(tt.param).\n\t\t\t\tReturn(dto.TimeEntryImpl{ID: \"te\"}, nil)\n\n\t\t\tcalled := false\n\t\t\tcmd := in.NewCmdIn(f, func(\n\t\t\t\t_ dto.TimeEntryImpl, _ io.Writer, _ util.OutputFlags) error {\n\t\t\t\tcalled = true\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SilenceErrors = true\n\n\t\t\tout := bytes.NewBufferString(\"\")\n\t\t\tcmd.SetOut(out)\n\t\t\tcmd.SetErr(out)\n\n\t\t\tcmd.SetArgs(append(tt.args, \"-q\"))\n\t\t\t_, err := cmd.ExecuteC()\n\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tt.Cleanup(func() {\n\t\t\t\t\tassert.True(t, called)\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tt.Fatalf(\"err: %s\", err)\n\t\t})\n\t}\n}\n\nfunc TestNewCmdIn_ShouldLookupProject_WithAndWithoutClient(t *testing.T) {\n\tdefaultStart := timehlp.Today().Add(8 * time.Hour)\n\tbFalse := false\n\n\tprojects := []dto.Project{\n\t\t{ID: \"p1\", Name: \"first\", ClientID: \"c1\", ClientName: \"other\"},\n\t\t{ID: \"p2\", Name: \"second\", ClientID: \"c2\", ClientName: \"me\"},\n\t\t{ID: \"p3\", Name: \"second\", ClientID: \"c3\", ClientName: \"clockify\"},\n\t\t{ID: \"p4\", Name: \"third\"},\n\t\t{ID: \"p5\", Name: \"notonclient\", ClientID: \"c3\", ClientName: \"clockify\"},\n\t}\n\n\ttts := []struct {\n\t\tname  string\n\t\targs  []string\n\t\tparam api.CreateTimeEntryParam\n\t\terr   error\n\t}{\n\t\t{\n\t\t\tname: \"only project\",\n\t\t\targs: []string{\"-s=08:00\", \"-p=first\"},\n\t\t\tparam: api.CreateTimeEntryParam{\n\t\t\t\tWorkspace: w.ID,\n\t\t\t\tStart:     defaultStart,\n\t\t\t\tProjectID: projects[0].ID,\n\t\t\t\tBillable:  &bFalse,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"project and client\",\n\t\t\targs: []string{\"-s=08:00\", \"-p=second\", \"-c=me\"},\n\t\t\tparam: api.CreateTimeEntryParam{\n\t\t\t\tWorkspace: w.ID,\n\t\t\t\tStart:     defaultStart,\n\t\t\t\tProjectID: projects[1].ID,\n\t\t\t\tBillable:  &bFalse,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"project and other client\",\n\t\t\targs: []string{\"-s=08:00\", \"-p=second\", \"-c=clockify\"},\n\t\t\tparam: api.CreateTimeEntryParam{\n\t\t\t\tWorkspace: w.ID,\n\t\t\t\tStart:     defaultStart,\n\t\t\t\tProjectID: projects[2].ID,\n\t\t\t\tBillable:  &bFalse,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"project without client\",\n\t\t\targs: []string{\"-s=08:00\", \"-p=third\"},\n\t\t\tparam: api.CreateTimeEntryParam{\n\t\t\t\tWorkspace: w.ID,\n\t\t\t\tStart:     defaultStart,\n\t\t\t\tProjectID: projects[3].ID,\n\t\t\t\tBillable:  &bFalse,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"project does not exist\",\n\t\t\targs: []string{\"-s=08:00\", \"-p=notfound\"},\n\t\t\terr: errors.New(\n\t\t\t\t\"No project with id or name containing 'notfound' \" +\n\t\t\t\t\t\"was found\"),\n\t\t},\n\t\t{\n\t\t\tname: \"project does not exist in this client\",\n\t\t\targs: []string{\"-s=08:00\", \"-p=notonclient\", \"-c=me\"},\n\t\t\terr: errors.New(\n\t\t\t\t\"No project with id or name containing 'notonclient' \" +\n\t\t\t\t\t\"was found for client 'me'\"),\n\t\t},\n\t\t{\n\t\t\tname: \"project with client name does not exist\",\n\t\t\targs: []string{\"-s=08:00\", \"-p\", \"notonclient me\"},\n\t\t\terr: errors.New(\n\t\t\t\t\"No project with id or name containing 'notonclient me' \" +\n\t\t\t\t\t\"was found\"),\n\t\t},\n\t\t{\n\t\t\tname: \"project and client's name\",\n\t\t\targs: []string{\"-s=08:00\", \"-p\", \"sec me\"},\n\t\t\tparam: api.CreateTimeEntryParam{\n\t\t\t\tWorkspace: w.ID,\n\t\t\t\tStart:     defaultStart,\n\t\t\t\tProjectID: projects[1].ID,\n\t\t\t\tBillable:  &bFalse,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"project and client's name (other)\",\n\t\t\targs: []string{\"-s=08:00\", \"-p\", \"sec cloc\"},\n\t\t\tparam: api.CreateTimeEntryParam{\n\t\t\t\tWorkspace: w.ID,\n\t\t\t\tStart:     defaultStart,\n\t\t\t\tProjectID: projects[2].ID,\n\t\t\t\tBillable:  &bFalse,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\ttt := &tts[i]\n\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\n\t\t\tf.EXPECT().GetUserID().Return(\"u\", nil)\n\t\t\tf.EXPECT().GetWorkspaceID().Return(w.ID, nil)\n\n\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\tAllowNameForID:               true,\n\t\t\t\tSearchProjectWithClientsName: true,\n\t\t\t})\n\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\tWorkspace:       w.ID,\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t}).\n\t\t\t\tReturn(projects, nil)\n\n\t\t\tc.EXPECT().GetTimeEntryInProgress(api.GetTimeEntryInProgressParam{\n\t\t\t\tWorkspace: w.ID,\n\t\t\t\tUserID:    \"u\",\n\t\t\t}).\n\t\t\t\tReturn(nil, nil)\n\n\t\t\tif tt.err == nil {\n\t\t\t\tc.EXPECT().GetProject(api.GetProjectParam{\n\t\t\t\t\tWorkspace: w.ID,\n\t\t\t\t\tProjectID: tt.param.ProjectID,\n\t\t\t\t}).\n\t\t\t\t\tReturn(&dto.Project{ID: tt.param.ProjectID}, nil)\n\n\t\t\t\tf.EXPECT().GetWorkspace().Return(w, nil)\n\n\t\t\t\tc.EXPECT().Out(api.OutParam{\n\t\t\t\t\tWorkspace: w.ID,\n\t\t\t\t\tUserID:    \"u\",\n\t\t\t\t\tEnd:       tt.param.Start,\n\t\t\t\t}).Return(api.ErrorNotFound)\n\n\t\t\t\tc.EXPECT().CreateTimeEntry(tt.param).\n\t\t\t\t\tReturn(dto.TimeEntryImpl{ID: \"te\"}, nil)\n\t\t\t}\n\n\t\t\tcalled := false\n\t\t\tcmd := in.NewCmdIn(f, func(\n\t\t\t\t_ dto.TimeEntryImpl, _ io.Writer, _ util.OutputFlags) error {\n\t\t\t\tcalled = true\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SilenceErrors = true\n\n\t\t\tout := bytes.NewBufferString(\"\")\n\t\t\tcmd.SetOut(out)\n\t\t\tcmd.SetErr(out)\n\n\t\t\tcmd.SetArgs(append(tt.args, \"-q\"))\n\t\t\t_, err := cmd.ExecuteC()\n\n\t\t\tif tt.err != nil {\n\t\t\t\tassert.EqualError(t, err, tt.err.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tt.Cleanup(func() {\n\t\t\t\tassert.True(t, called)\n\t\t\t})\n\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tt.Fatalf(\"err: %s\", err)\n\t\t})\n\t}\n\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/invoiced/invoiced.go",
    "content": "package invoiced\n\nimport (\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\toutput \"github.com/lucassabreu/clockify-cli/pkg/output/time-entry\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timeentryhlp\"\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdInvoiced represents invoiced command\nfunc NewCmdInvoiced(f cmdutil.Factory) []*cobra.Command {\n\tof := util.OutputFlags{TimeFormat: output.TimeFormatSimple}\n\taddCmd := func(cmd *cobra.Command) *cobra.Command {\n\t\tutil.AddPrintTimeEntriesFlags(cmd, &of)\n\t\tutil.AddPrintMultipleTimeEntriesFlags(cmd)\n\n\t\treturn cmd\n\t}\n\n\tva := cmdcompl.ValidArgsSlide{\n\t\ttimeentryhlp.AliasLast, timeentryhlp.AliasCurrent}\n\tuse := \"{ <time-entry-id> | \" + va.IntoUseOptions() + \" }...\"\n\targs := cmdutil.RequiredNamedArgs(\"time entry id\")\n\n\treturn []*cobra.Command{\n\t\taddCmd(&cobra.Command{\n\t\t\tUse:   \"mark-invoiced \" + use,\n\t\t\tShort: \"Marks times entries as invoiced\",\n\t\t\tLong: \"Marks times entries as invoiced\\n\\n\" +\n\t\t\t\tutil.HelpMoreInfoAboutPrinting,\n\t\t\tExample: heredoc.Docf(`\n\t\t\t\t# when the workspace does not allow invoicing\n\t\t\t\t$ %[1]s 62b49641f4b27f4ed7d20e75\n\t\t\t\tForbidden (code: 403)\n\n\t\t\t\t# set the running time entry as invoiced\n\t\t\t\t$ %[1]s current --quiet\n\t\t\t\t62b49641f4b27f4ed7d20e75\n\n\t\t\t\t# setting multiple time entries as invoiced\n\t\t\t\t$ %[1]s 62b5b51085815e619d7ae18d 62b5d55185815e619d7af928 --quiet\n\t\t\t\t62b5b51085815e619d7ae18d\n\t\t\t\t62b5d55185815e619d7af928\n\t\t\t`, \"clockify-cli mark-invoiced\"),\n\t\t\tArgs:      args,\n\t\t\tValidArgs: va,\n\t\t\tRunE:      changeInvoiced(f, &of, true),\n\t\t}),\n\t\taddCmd(&cobra.Command{\n\t\t\tUse:   \"mark-not-invoiced \" + use,\n\t\t\tShort: \"Mark times entries as not invoiced\",\n\t\t\tLong: \"Mark times entries as not invoiced\\n\\n\" +\n\t\t\t\tutil.HelpMoreInfoAboutPrinting,\n\t\t\tExample: heredoc.Docf(`\n\t\t\t\t# when the workspace does not allow invoicing\n\t\t\t\t$ %[1]s 62b49641f4b27f4ed7d20e75\n\t\t\t\tForbidden (code: 403)\n\n\t\t\t\t# set the running time entry as not invoiced\n\t\t\t\t$ %[1]s current --quiet\n\t\t\t\t62b49641f4b27f4ed7d20e75\n\n\t\t\t\t# setting multiple time entries as not invoiced\n\t\t\t\t$ %[1]s 62b5b51085815e619d7ae18d 62b5d55185815e619d7af928 --quiet\n\t\t\t\t62b5b51085815e619d7ae18d\n\t\t\t\t62b5d55185815e619d7af928\n\t\t\t`, \"clockify-cli mark-not-invoiced\"),\n\t\t\tArgs:      args,\n\t\t\tValidArgs: va,\n\t\t\tRunE:      changeInvoiced(f, &of, false),\n\t\t}),\n\t}\n}\n\nfunc changeInvoiced(\n\tf cmdutil.Factory, of *util.OutputFlags, invoiced bool,\n) func(cmd *cobra.Command, args []string) error {\n\treturn func(cmd *cobra.Command, args []string) error {\n\t\tif err := of.Check(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar err error\n\t\tvar w, u string\n\n\t\tif w, err = f.GetWorkspaceID(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif u, err = f.GetUserID(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc, err := f.Client()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\targs = strhlp.Unique(args)\n\t\ttes := make([]dto.TimeEntry, len(args))\n\t\tfor i, id := range args {\n\t\t\tif id == timeentryhlp.AliasCurrent ||\n\t\t\t\tid == timeentryhlp.AliasLast {\n\t\t\t\ttei, err := timeentryhlp.GetTimeEntry(c, w, u, id)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tid = tei.ID\n\t\t\t\targs[i] = id\n\t\t\t}\n\n\t\t\tte, err := c.GetHydratedTimeEntry(api.GetTimeEntryParam{\n\t\t\t\tWorkspace:   w,\n\t\t\t\tTimeEntryID: id,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\ttes[i] = *te\n\t\t}\n\n\t\tif err := c.ChangeInvoiced(api.ChangeInvoicedParam{\n\t\t\tWorkspace:    w,\n\t\t\tTimeEntryIDs: args,\n\t\t\tInvoiced:     invoiced,\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn util.PrintTimeEntries(tes, cmd.OutOrStdout(), f.Config(), *of)\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/manual/manual.go",
    "content": "package manual\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\toutput \"github.com/lucassabreu/clockify-cli/pkg/output/time-entry\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdManual represents the manual command\nfunc NewCmdManual(f cmdutil.Factory) *cobra.Command {\n\tof := util.OutputFlags{TimeFormat: output.TimeFormatSimple}\n\tcmd := &cobra.Command{\n\t\tUse:   \"manual [<project-id>] [<start>] [<end>] [<description>]\",\n\t\tShort: \"Create a new complete time entry\",\n\t\tLong: heredoc.Doc(`\n\t\t\tCreate a new complete time entry with start and end.\n\n\t\t\tThis command will not stop running time entries.\n\n\t\t\tThe rules defined in the workspace and project will be checked before creating it.\n\t\t`) + \"\\n\" +\n\t\t\tutil.HelpTimeEntryNowIfNotSet +\n\t\t\t\"The same applies to end time (`--when-to-close`).\\n\\n\" +\n\t\t\tutil.HelpInteractiveByDefault + \"\\n\" +\n\t\t\tutil.HelpTimeInputOnTimeEntry + \"\\n\" +\n\t\t\tutil.HelpNamesForIds + \"\\n\" +\n\t\t\tutil.HelpMoreInfoAboutStarting + \"\\n\" +\n\t\t\tutil.HelpMoreInfoAboutPrinting,\n\t\tArgs: cobra.MaximumNArgs(4),\n\t\tValidArgsFunction: cmdcompl.CombineSuggestionsToArgs(\n\t\t\tcmdcomplutil.NewProjectAutoComplete(f, f.Config())),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tvar whenToCloseDate time.Time\n\t\t\tvar err error\n\t\t\ttei := util.TimeEntryDTO{\n\t\t\t\tStart: timehlp.Now(),\n\t\t\t}\n\n\t\t\tif tei.Workspace, err = f.GetWorkspaceID(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif tei.UserID, err = f.GetUserID(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif len(args) > 0 {\n\t\t\t\ttei.ProjectID = args[0]\n\t\t\t}\n\n\t\t\tif len(args) > 1 {\n\t\t\t\ttei.Start, err = timehlp.ConvertToTime(args[1])\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\t\"fail to convert when to start: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(args) > 2 {\n\t\t\t\twhenToCloseDate, err = timehlp.ConvertToTime(args[2])\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\t\"fail to convert when to end: %w\", err)\n\t\t\t\t}\n\t\t\t\ttei.End = &whenToCloseDate\n\t\t\t}\n\n\t\t\tif len(args) > 3 {\n\t\t\t\ttei.Description = args[3]\n\t\t\t}\n\n\t\t\tdc := util.NewDescriptionCompleter(f)\n\n\t\t\tif tei, err = util.Do(\n\t\t\t\ttei,\n\t\t\t\tutil.FillTimeEntryWithFlags(cmd.Flags()),\n\t\t\t\tfunc(tei util.TimeEntryDTO) (util.TimeEntryDTO, error) {\n\t\t\t\t\tif tei.End != nil {\n\t\t\t\t\t\treturn tei, nil\n\t\t\t\t\t}\n\n\t\t\t\t\tnow, _ := timehlp.ConvertToTime(timehlp.NowTimeFormat)\n\t\t\t\t\ttei.End = &now\n\t\t\t\t\treturn tei, nil\n\t\t\t\t},\n\t\t\t\tutil.GetAllowNameForIDsFn(f.Config(), c),\n\t\t\t\tutil.GetPropsInteractiveFn(dc, f),\n\t\t\t\tutil.GetDatesInteractiveFn(f),\n\t\t\t\tutil.ValidateClosingTimeEntry(f),\n\t\t\t\tutil.CreateTimeEntryFn(c),\n\t\t\t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn util.PrintTimeEntryImpl(\n\t\t\t\tutil.TimeEntryDTOToImpl(tei), f, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tutil.AddTimeEntryFlags(cmd, f, &of)\n\tutil.AddTimeEntryDateFlags(cmd)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/out/out.go",
    "content": "package out\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\toutput \"github.com/lucassabreu/clockify-cli/pkg/output/time-entry\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdOut represents the out command\nfunc NewCmdOut(f cmdutil.Factory) *cobra.Command {\n\tof := util.OutputFlags{TimeFormat: output.TimeFormatSimple}\n\tcmd := &cobra.Command{\n\t\tUse:   \"out\",\n\t\tShort: \"Stops the running time entry\",\n\t\tLong: heredoc.Docf(`\n\t\t\tStops the running time entry.\n\n\t\t\tIf no value is set on %[1]s--when%[1]s, then current time will be used.\n\n\t\t\tWhen setting the end time you can use any of the following formats to set it:\n\t\t\t%[2]s\n\n\t\t\tUse %[1]sclockify-cli edit current%[1]s to edit any properties before ending it.\n\t\t\t%[3]s\n\t\t`, \"`\",\n\t\t\tutil.HelpDateTimeFormats,\n\t\t\tutil.HelpMoreInfoAboutPrinting,\n\t\t),\n\t\tExample: heredoc.Docf(`\n\t\t\t# stop running time entry with current time\n\t\t\t$ %[1]s out --md\n\t\t\tID: %[2]s62af6b0f4ebb4f143c94880e%[2]s  \n\t\t\tBillable: %[2]syes%[2]s  \n\t\t\tLocked: %[2]sno%[2]s  \n\t\t\tProject: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s)  \n\t\t\tTask: Out Command (%[2]s62af66454ebb4f143c948263%[2]s)  \n\t\t\tInterval: %[2]s2022-06-19 18:29:32%[2]s until %[2]s2022-06-19 18:52:13%[2]s  \n\t\t\tDescription:\n\t\t\t> Adding examples\n\n\t\t\tTags:\n\t\t\t * Development (%[2]s62ae28b72518aa18da2acb49%[2]s)\n\n\t\t\t# clone last and stopping it in 10 minutes\n\t\t\t$ %[1]s clone last -i=0 -d 'More examples' -q\n\t\t\t62af70d849445270d7c09fbd\n\n\t\t\t$ %[1]s out --when +10m --md\n\t\t\tID: %[2]s62af70d849445270d7c09fbd%[2]s  \n\t\t\tBillable: %[2]syes%[2]s  \n\t\t\tLocked: %[2]sno%[2]s  \n\t\t\tProject: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s)  \n\t\t\tTask: Out Command (%[2]s62af666349445270d7c09285%[2]s)  \n\t\t\tInterval: %[2]s2022-06-19 18:54:12%[2]s until %[2]s2022-06-19 19:08:26%[2]s  \n\t\t\tDescription:\n\t\t\t> More examples\n\n\t\t\tTags:\n\t\t\t * Development (%[2]s62ae28b72518aa18da2acb49%[2]s)\n\t\t`, \"clockify-cli\", \"`\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tvar whenDate time.Time\n\t\t\tvar err error\n\n\t\t\twhenString, _ := cmd.Flags().GetString(\"when\")\n\t\t\tif whenDate, err = timehlp.ConvertToTime(whenString); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tuserID, err := f.GetUserID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tw, err := f.GetWorkspaceID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tte, err := c.GetHydratedTimeEntryInProgress(\n\t\t\t\tapi.GetTimeEntryInProgressParam{\n\t\t\t\t\tWorkspace: w,\n\t\t\t\t\tUserID:    userID,\n\t\t\t\t})\n\n\t\t\tif te == nil && err == nil {\n\t\t\t\treturn errors.New(\"no time entry in progress\")\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err = c.Out(api.OutParam{\n\t\t\t\tWorkspace: w,\n\t\t\t\tUserID:    userID,\n\t\t\t\tEnd:       whenDate,\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tte.TimeInterval.End = &whenDate\n\n\t\t\treturn util.PrintTimeEntry(te, cmd.OutOrStdout(), f.Config(), of)\n\t\t},\n\t}\n\n\tutil.AddPrintTimeEntriesFlags(cmd, &of)\n\n\tcmd.Flags().String(\"when\", time.Now().Format(timehlp.FullTimeFormat),\n\t\t\"when the entry should be closed, \"+\n\t\t\t\"if not informed will use current time\")\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/report/last-day/last-day.go",
    "content": "package lastday\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timeentryhlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdLastDay represents the report last-day command\nfunc NewCmdLastDay(f cmdutil.Factory) *cobra.Command {\n\tof := util.NewReportFlags()\n\tcmd := &cobra.Command{\n\t\tUse:   \"last-day\",\n\t\tShort: \"List time entries from last day were a time entry was created\",\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tw, err := f.GetWorkspaceID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tu, err := f.GetUserID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tte, err := timeentryhlp.GetLatestEntryEntry(c, w, u)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn util.ReportWithRange(\n\t\t\t\tf, te.TimeInterval.Start, te.TimeInterval.Start,\n\t\t\t\tcmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tcmd.Long = cmd.Short + \"\\n\\n\" +\n\t\tutil.HelpNamesForIds + \"\\n\" +\n\t\tutil.HelpMoreInfoAboutPrinting\n\n\tutil.AddReportFlags(f, cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/report/last-month/last-month.go",
    "content": "package lastmonth\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdLastMonth represents the report last-month command\nfunc NewCmdLastMonth(f cmdutil.Factory) *cobra.Command {\n\tof := util.NewReportFlags()\n\tcmd := &cobra.Command{\n\t\tUse:   \"last-month\",\n\t\tShort: \"List all time entries in last month\",\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfirst, last := timehlp.GetMonthRange(\n\t\t\t\ttimehlp.Today().AddDate(0, -1, 0))\n\t\t\treturn util.ReportWithRange(f, first, last, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tcmd.Long = cmd.Short + \"\\n\\n\" +\n\t\tutil.HelpNamesForIds + \"\\n\" +\n\t\tutil.HelpMoreInfoAboutPrinting\n\n\tutil.AddReportFlags(f, cmd, &of)\n\n\treturn cmd\n\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/report/last-week/last-week.go",
    "content": "package lastweek\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdLastWeek represents the report last-week command\nfunc NewCmdLastWeek(f cmdutil.Factory) *cobra.Command {\n\tof := util.NewReportFlags()\n\tcmd := &cobra.Command{\n\t\tUse:   \"last-week\",\n\t\tShort: \"List all time entries in last week\",\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfirst, last := timehlp.GetWeekRange(\n\t\t\t\ttimehlp.TruncateDate(timehlp.Today()).AddDate(0, 0, -7))\n\t\t\treturn util.ReportWithRange(f, first, last, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tcmd.Long = cmd.Short + \"\\n\\n\" +\n\t\tutil.HelpNamesForIds + \"\\n\" +\n\t\tutil.HelpMoreInfoAboutPrinting\n\n\tutil.AddReportFlags(f, cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/report/last-week-day/last-week-day.go",
    "content": "package lastweekday\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdLastWeekDay represents the report last working week day command\nfunc NewCmdLastWeekDay(f cmdutil.Factory) *cobra.Command {\n\tof := util.NewReportFlags()\n\tcmd := &cobra.Command{\n\t\tUse:   \"last-week-day\",\n\t\tShort: \"List time entries from last week day\",\n\t\tLong: heredoc.Docf(`\n\t\t\tList time entries from last week day\n\n\t\t\tFor the CLI to know which days of the week you are expected to work, you will need to set them.\n\t\t\tThis can be done using:\n\t\t\t$ clockify-cli config init\n\n\t\t\tOr more directly by running the set command as follows:\n\t\t\t$ clockify-cli config set workweek-days monday,tuesday,wednesday,thursday,friday\n\n\t\t\t%s\n\t\t\t%s\n\t\t`,\n\t\t\tutil.HelpNamesForIds,\n\t\t\tutil.HelpMoreInfoAboutPrinting,\n\t\t),\n\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tworkweek := f.Config().GetWorkWeekdays()\n\t\t\tif len(workweek) == 0 {\n\t\t\t\treturn errors.New(\"no workweek days were set\")\n\t\t\t}\n\n\t\t\tday := timehlp.Today().Add(-1)\n\t\t\tif strhlp.Search(\n\t\t\t\tstrings.ToLower(day.Weekday().String()), workweek) != -1 {\n\t\t\t\treturn util.ReportWithRange(f, day, day, cmd.OutOrStdout(), of)\n\t\t\t}\n\n\t\t\tdayWeekday := int(day.Weekday())\n\t\t\tif dayWeekday == int(time.Sunday) {\n\t\t\t\tdayWeekday = int(time.Saturday + 1)\n\t\t\t}\n\n\t\t\tlastWeekDay := int(time.Sunday)\n\t\t\tfor _, w := range workweek {\n\t\t\t\ti := strhlp.Search(w, cmdutil.GetWeekdays())\n\t\t\t\tif i > lastWeekDay && i < dayWeekday {\n\t\t\t\t\tlastWeekDay = i\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tday = day.Add(\n\t\t\t\ttime.Duration(-24*(dayWeekday-lastWeekDay)) * time.Hour)\n\t\t\treturn util.ReportWithRange(f, day, day, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tutil.AddReportFlags(f, cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/report/report.go",
    "content": "package report\n\nimport (\n\t\"time\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\tlastday \"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/last-day\"\n\tlastmonth \"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/last-month\"\n\tlastweek \"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/last-week\"\n\tlastweekday \"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/last-week-day\"\n\tthismonth \"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/this-month\"\n\tthisweek \"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/this-week\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/today\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/yesterday\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdReport represents the reports command\nfunc NewCmdReport(f cmdutil.Factory) *cobra.Command {\n\tof := util.NewReportFlags()\n\tcmd := &cobra.Command{\n\t\tUse:   \"report [<start>] [<end>]\",\n\t\tShort: \"List all time entries for a given date range\",\n\t\tLong: heredoc.Docf(`\n\t\t\tList all time entries for a given date range\n\n\t\t\tIf no parameter is set, shows today's time entries\n\t\t\tAliases today/now can be used for <end> argument to represent current date\n\t\t\tAlias yesterday can be used for <end> argument to represent previous date\n\n\t\t\tTo choose a specific date to start or end use the format \"2006-01-02\"\n\n\t\t\t%s\n\t\t\tAll the subcommands have the same flags to filter and format the time entries, but will act as aliases to relative date ranges.\n\t\t`, util.HelpNamesForIds),\n\t\tExample: heredoc.Docf(`\n\t\t\t# reporting all time entries from today\n\t\t\t$ %[1]s\n\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+\n\t\t\t|            ID            |        START        |         END         |   DUR   |   PROJECT    |          DESCRIPTION           |              TAGS              |\n\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+\n\t\t\t| 62b87a9785815e619d7ce02e | 2022-06-26 12:25:56 | 2022-06-26 12:26:47 | 0:00:51 | Clockify Cli | Example for today              | Development                    |\n\t\t\t|                          |                     |                     |         |              |                                | (62ae28b72518aa18da2acb49)     |\n\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+\n\t\t\t| 62b87abb85815e619d7ce034 | 2022-06-26 12:26:47 | 2022-06-26 13:00:00 | 0:33:13 | Clockify Cli | Example for today (second one) | Development                    |\n\t\t\t|                          |                     |                     |         |              |                                | (62ae28b72518aa18da2acb49)     |\n\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+\n\t\t\t| TOTAL                    |                     |                     | 0:34:04 |              |                                |                                |\n\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+\n\n\t\t\t# reporting all time entries from 2022-06-24 to today\n\t\t\t$ %[1]s 2022-06-24 today\n\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+\n\t\t\t|            ID            |        START        |         END         |   DUR   |   PROJECT    |          DESCRIPTION           |              TAGS              |\n\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+\n\t\t\t| 62b8ce7185815e619d7d0a82 | 2022-06-24 08:00:00 | 2022-06-24 09:00:00 | 1:00:00 | Clockify Cli | Example for before yesterday   | Development                    |\n\t\t\t|                          |                     |                     |         |              |                                | (62ae28b72518aa18da2acb49)     |\n\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+\n\t\t\t| 62b8ce1edba0da0f21e7e688 | 2022-06-25 08:00:00 | 2022-06-25 09:00:00 | 1:00:00 | Clockify Cli | Example for yesterday          | Development                    |\n\t\t\t|                          |                     |                     |         |              |                                | (62ae28b72518aa18da2acb49)     |\n\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+\n\t\t\t| 62b87a9785815e619d7ce02e | 2022-06-26 12:25:56 | 2022-06-26 12:26:47 | 0:00:51 | Clockify Cli | Example for today              | Development                    |\n\t\t\t|                          |                     |                     |         |              |                                | (62ae28b72518aa18da2acb49)     |\n\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+\n\t\t\t| 62b87abb85815e619d7ce034 | 2022-06-26 12:26:47 | 2022-06-26 13:00:00 | 0:33:13 | Clockify Cli | Example for today (second one) | Development                    |\n\t\t\t|                          |                     |                     |         |              |                                | (62ae28b72518aa18da2acb49)     |\n\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+\n\t\t\t| TOTAL                    |                     |                     | 2:34:04 |              |                                |                                |\n\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+--------------------------------+--------------------------------+\n\n\t\t\t# when there are no entries for the range\n\t\t\t$ %[1]s 1999-01-01\n\t\t\t+-------+-------+-----+---------+---------+-------------+------+\n\t\t\t|  ID   | START | END |   DUR   | PROJECT | DESCRIPTION | TAGS |\n\t\t\t+-------+-------+-----+---------+---------+-------------+------+\n\t\t\t| TOTAL |       |     | 0:00:00 |         |             |      |\n\t\t\t+-------+-------+-----+---------+---------+-------------+------+\n\n\t\t\t# format output with golang template\n\t\t\t$ %[1]s 2022-06-23 --format \"{{.ID}} - {{ .TimeInterval.Duration }} - {{ pad .Project.Name 12 }} - {{ .Description }}\"\n\t\t\t62b8d162984dba2c06699e3f - PT1H - Clockify Cli - First example for report\n\t\t\t62b8d195dba0da0f21e7e85d - PT1H - Special      - Lunch break\n\t\t\t62b8d207dba0da0f21e7e868 - PT1H - Clockify Cli - After lunch\n\n\t\t\t# the default functions from the text/template package from Go are available, and the following functions are also allowed:\n\t\t\t#\n\t\t\t#  - formatDateTime(time.Time)         => format date/times with %[3]s\n\t\t\t#  - formatTime(time.Time)             => format date/times with %[4]s\n\t\t\t#  - json(interface{})                 => encodes a value to json\n\t\t\t#  - now(time.Time)                    => returns the argument or now if nil\n\t\t\t#  - pad(s string, size int)           => adds spaces to the end of a string until its length meets the size\n\t\t\t#  - since(s time.Time, [e time.Time]) => returns the time difference between the first and second time (or now if not set)\n\t\t\t#  - until(e time.Time, [s time.Time]) => returns the time difference between the second and first time (or now if not set)\n\t\t\t#  - yaml(interface{})                 => encodes a value to yaml\n\n\t\t\t# show time spent on the project \"Clockify CLI\" as float\n\t\t\t$ %[1]s 2022-06-23 --duration-float -p \"clockify cli\"\n\t\t\t2.000000\n\n\t\t\t# show time spent on the project \"Clockify CLI\" with \"lunch\" on description\n\t\t\t$ %[1]s 2022-06-23 --duration-formatted -p \"clockify cli\" -d lunch\n\t\t\t1:00:00\n\n\t\t\t# show ids from time entries from project \"clockify cli\"\n\t\t\t$ %[1]s 2022-06-23 -p \"clockify cli\" --quiet\n\t\t\t62b8d162984dba2c06699e3f\n\t\t\t62b8d207dba0da0f21e7e868\n\n\t\t\t# show time entries from project \"special\" as markdown\n\t\t\t$ %[1]s 2022-06-23 -p \"clockify cli\" --quiet\n\t\t\tID: %[2]s63b8d195dba0da0f21e7e85d%[2]s  \n\t\t\tBillable: %[2]sno%[2]s  \n\t\t\tLocked: %[2]sno%[2]s  \n\t\t\tProject: Special (%[2]s6202680228782767055ef004%[2]s)  \n\t\t\tInterval: %[2]s2023-06-23 15:00:00%[2]s until %[2]s2022-06-23 16:00:00%[2]s  \n\t\t\tDescription:\n\t\t\t> Lunch break\n\n\t\t\tTags:\n\t\t\t * Meeting (%[2]s6219486e8cb9606d934ebb5f%[2]s)\n\n\t\t\t# csv format output\n\t\t\t$ %[1]s --csv\n\t\t\tid,description,project.id,project.name,task.id,task.name,start,end,duration,user.id,user.email,user.name,tags...,customFields...\n\t\t\t62b87a9785815e619d7ce02e,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\n\t\t\t62b87abb85815e619d7ce034,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\n\t\t`, \"clockify-cli report\", \"`\",\n\t\t\ttimehlp.FullTimeFormat,\n\t\t\ttimehlp.OnlyTimeFormat,\n\t\t),\n\t\tArgs:    cobra.MaximumNArgs(2),\n\t\tAliases: []string{\"log\"},\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tvar err error\n\n\t\t\tstart := timehlp.Today()\n\t\t\tif len(args) > 0 {\n\t\t\t\tstart, err = time.Parse(\"2006-01-02\", args[0])\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tend := start\n\t\t\tif len(args) > 1 {\n\t\t\t\tif args[1] == \"now\" || args[1] == \"today\" {\n\t\t\t\t\tend = timehlp.Today()\n\t\t\t\t} else if args[1] == \"yesterday\" {\n\t\t\t\t\tend = timehlp.Today().Add(-1)\n\t\t\t\t} else if end, err = time.Parse(\n\t\t\t\t\t\"2006-01-02\", args[1]); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn util.ReportWithRange(\n\t\t\t\tf, start, end, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tcmd.AddCommand(thismonth.NewCmdThisMonth(f))\n\tcmd.AddCommand(lastmonth.NewCmdLastMonth(f))\n\tcmd.AddCommand(thisweek.NewCmdThisWeek(f))\n\tcmd.AddCommand(lastweek.NewCmdLastWeek(f))\n\tcmd.AddCommand(lastday.NewCmdLastDay(f))\n\tcmd.AddCommand(lastweekday.NewCmdLastWeekDay(f))\n\tcmd.AddCommand(today.NewCmdToday(f))\n\tcmd.AddCommand(yesterday.NewCmdYesterday(f))\n\n\tutil.AddReportFlags(f, cmd, &of)\n\t_ = cmd.MarkFlagRequired(\"workspace\")\n\t_ = cmd.MarkFlagRequired(\"user-id\")\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/report/this-month/this-month.go",
    "content": "package thismonth\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdThisMonth represents the reports this-month command\nfunc NewCmdThisMonth(f cmdutil.Factory) *cobra.Command {\n\tof := util.NewReportFlags()\n\tcmd := &cobra.Command{\n\t\tUse:   \"this-month\",\n\t\tShort: \"List all time entries in this month\",\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfirst, last := timehlp.GetMonthRange(timehlp.Today())\n\t\t\treturn util.ReportWithRange(f, first, last, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tcmd.Long = cmd.Short + \"\\n\\n\" +\n\t\tutil.HelpNamesForIds + \"\\n\" +\n\t\tutil.HelpMoreInfoAboutPrinting\n\n\tutil.AddReportFlags(f, cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/report/this-week/this-week.go",
    "content": "package thisweek\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdThisWeek represents the report this-week command\nfunc NewCmdThisWeek(f cmdutil.Factory) *cobra.Command {\n\tof := util.NewReportFlags()\n\tcmd := &cobra.Command{\n\t\tUse:   \"this-week\",\n\t\tShort: \"List all time entries in this week\",\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfirst, last := timehlp.GetWeekRange(timehlp.Today())\n\t\t\treturn util.ReportWithRange(f, first, last, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tcmd.Long = cmd.Short + \"\\n\\n\" +\n\t\tutil.HelpNamesForIds + \"\\n\" +\n\t\tutil.HelpMoreInfoAboutPrinting\n\n\tutil.AddReportFlags(f, cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/report/today/today.go",
    "content": "package today\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdToday represents report today command\nfunc NewCmdToday(f cmdutil.Factory) *cobra.Command {\n\tof := util.NewReportFlags()\n\tcmd := &cobra.Command{\n\t\tUse:   \"today\",\n\t\tShort: \"List all time entries created today\",\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\ttoday := timehlp.Today()\n\t\t\treturn util.ReportWithRange(f, today, today, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tcmd.Long = cmd.Short + \"\\n\\n\" +\n\t\tutil.HelpNamesForIds + \"\\n\" +\n\t\tutil.HelpMoreInfoAboutPrinting\n\n\tutil.AddReportFlags(f, cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/report/today/today_test.go",
    "content": "package today_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/today\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCmdToday(t *testing.T) {\n\tfirst := time.Now()\n\tfirst = time.Date(\n\t\tfirst.Year(),\n\t\tfirst.Month(),\n\t\tfirst.Day(),\n\t\t0,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\ttime.UTC,\n\t)\n\tlast := first.AddDate(0, 0, 1)\n\n\ttts := []struct {\n\t\tname     string\n\t\targs     string\n\t\terr      error\n\t\texpected string\n\t\tfactory  func(*testing.T) cmdutil.Factory\n\t}{\n\t\t{\n\t\t\tname: \"error on multi format\",\n\t\t\targs: \"--format {} --json --csv -q --md \" +\n\t\t\t\t\"--duration-float --duration-formatted\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\treturn f\n\t\t\t},\n\t\t\terr: errors.New(\n\t\t\t\t\"the following flags can't be used together: \" +\n\t\t\t\t\t\"`csv`, `duration-float`, `duration-formatted`, \" +\n\t\t\t\t\t\"`format`, `json`, `md` and `quiet`\",\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"all of them, but only ids\",\n\t\t\targs: \"-q\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.On(\"GetUserID\").Return(\"user-id\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w-id\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tc.On(\"LogRange\", api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w-id\",\n\t\t\t\t\tUserID:          \"user-id\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tTagIDs:          []string{},\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(\n\t\t\t\t\t\t[]dto.TimeEntry{{ID: \"time-entry-id\"}},\n\t\t\t\t\t\tnil,\n\t\t\t\t\t)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\texpected: \"time-entry-id\\n\",\n\t\t},\n\t\t{\n\t\t\tname: \"all of them, but fails\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.On(\"GetUserID\").Return(\"user-id\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w-id\", nil)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tc.On(\"LogRange\", api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w-id\",\n\t\t\t\t\tUserID:          \"user-id\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tTagIDs:          []string{},\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(\n\t\t\t\t\t\t[]dto.TimeEntry{},\n\t\t\t\t\t\terrors.New(\"failed\"),\n\t\t\t\t\t)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\terr: errors.New(\"failed\"),\n\t\t},\n\t\t{\n\t\t\tname: \"only project x, no results\",\n\t\t\targs: \"--project x\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"user-id\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w-id\", nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID: true,\n\t\t\t\t})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w-id\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(\n\t\t\t\t\t\t[]dto.Project{{\n\t\t\t\t\t\t\tID:   \"project-id\",\n\t\t\t\t\t\t\tName: \"xpecial\",\n\t\t\t\t\t\t}},\n\t\t\t\t\t\tnil,\n\t\t\t\t\t)\n\n\t\t\t\tc.On(\"LogRange\", api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w-id\",\n\t\t\t\t\tUserID:          \"user-id\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tProjectID:       \"project-id\",\n\t\t\t\t\tTagIDs:          []string{},\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(\n\t\t\t\t\t\t[]dto.TimeEntry{},\n\t\t\t\t\t\terrors.New(\"failed\"),\n\t\t\t\t\t)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\terr: errors.New(\"failed\"),\n\t\t},\n\t\t{\n\t\t\tname: \"only with desc on description\",\n\t\t\targs: \"--description desc -q\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"user-id\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w-id\", nil)\n\n\t\t\t\tf.On(\"Config\").Return(&mocks.SimpleConfig{})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"LogRange\", api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w-id\",\n\t\t\t\t\tUserID:          \"user-id\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tDescription:     \"desc\",\n\t\t\t\t\tTagIDs:          []string{},\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(\n\t\t\t\t\t\t[]dto.TimeEntry{\n\t\t\t\t\t\t\t{ID: \"time-entry-1\"},\n\t\t\t\t\t\t\t{ID: \"time-entry-2\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tnil,\n\t\t\t\t\t)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\ttime-entry-1\n\t\t\t\ttime-entry-2\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"report only the first time entry\",\n\t\t\targs: \"--limit 2 --page 10 -q\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"user-id\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w-id\", nil)\n\n\t\t\t\tf.On(\"Config\").Return(&mocks.SimpleConfig{})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"LogRange\", api.LogRangeParam{\n\t\t\t\t\tWorkspace: \"w-id\",\n\t\t\t\t\tUserID:    \"user-id\",\n\t\t\t\t\tFirstDate: first,\n\t\t\t\t\tLastDate:  last,\n\t\t\t\t\tTagIDs:    []string{},\n\t\t\t\t\tPaginationParam: api.PaginationParam{\n\t\t\t\t\t\tPage:     10,\n\t\t\t\t\t\tPageSize: 2,\n\t\t\t\t\t},\n\t\t\t\t}).\n\t\t\t\t\tReturn(\n\t\t\t\t\t\t[]dto.TimeEntry{\n\t\t\t\t\t\t\t{ID: \"time-entry-1\"},\n\t\t\t\t\t\t\t{ID: \"time-entry-2\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tnil,\n\t\t\t\t\t)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\ttime-entry-1\n\t\t\t\ttime-entry-2\n\t\t\t`),\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\ttt := tts[i]\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcmd := today.NewCmdToday(tt.factory(t))\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SilenceErrors = true\n\n\t\t\tcmd.SetArgs(strings.Split(tt.args, \" \"))\n\n\t\t\tout := bytes.NewBufferString(\"\")\n\n\t\t\tcmd.SetOut(out)\n\t\t\tcmd.SetErr(out)\n\n\t\t\t_, err := cmd.ExecuteC()\n\n\t\t\tif tt.err != nil {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.EqualError(t, err, tt.err.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expected, out.String())\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/report/util/report.go",
    "content": "package util\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/search\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nconst (\n\tHelpNamesForIds           = util.HelpNamesForIds\n\tHelpMoreInfoAboutPrinting = util.HelpMoreInfoAboutPrinting\n)\n\n// ReportFlags reads the \"shared\" flags for report commands\ntype ReportFlags struct {\n\tutil.OutputFlags\n\n\tFillMissingDates bool\n\tLimit            int\n\tPage             int\n\n\tBillable    bool\n\tNotBillable bool\n\n\tDescription string\n\tClient      string\n\tProjects    []string\n\tTagIDs      []string\n}\n\n// Check will assure that there is no conflicting flag values\nfunc (rf ReportFlags) Check() error {\n\tif err := rf.OutputFlags.Check(); err != nil {\n\t\treturn err\n\t}\n\n\tif rf.Page > 0 && rf.Limit <= 0 {\n\t\treturn cmdutil.FlagErrorWrap(\n\t\t\terrors.New(\"page can't be used without limit\"))\n\t}\n\n\tif err := cmdutil.XorFlag(map[string]bool{\n\t\t\"limit\":              rf.Limit > 0,\n\t\t\"fill-missing-dates\": rf.FillMissingDates,\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\treturn cmdutil.XorFlag(map[string]bool{\n\t\t\"billable\":     rf.Billable,\n\t\t\"not-billable\": rf.NotBillable,\n\t})\n}\n\n// NewReportFlags helps creating a util.ReportFlags for report commands\nfunc NewReportFlags() ReportFlags {\n\treturn ReportFlags{\n\t\tLimit: 0,\n\t\tOutputFlags: util.OutputFlags{\n\t\t\tTimeFormat: timehlp.FullTimeFormat,\n\t\t},\n\t}\n}\n\n// AddReportFlags add flags for print out the time entries\nfunc AddReportFlags(\n\tf cmdutil.Factory, cmd *cobra.Command, rf *ReportFlags,\n) {\n\tutil.AddPrintTimeEntriesFlags(cmd, &rf.OutputFlags)\n\tutil.AddPrintMultipleTimeEntriesFlags(cmd)\n\n\tcmd.Flags().IntVarP(&rf.Page, \"page\", \"P\", 0,\n\t\t\"set which page to return\")\n\tcmd.Flags().IntVarP(&rf.Limit, \"limit\", \"l\", 0,\n\t\t\"Only look for this quantity of time entries\")\n\tcmd.Flags().BoolVarP(&rf.FillMissingDates, \"fill-missing-dates\", \"e\", false,\n\t\t\"Add empty lines for dates without time entries\")\n\tcmd.Flags().StringVarP(&rf.Description, \"description\", \"d\", \"\",\n\t\t\"will filter time entries that contains this on the description field\")\n\tcmd.Flags().StringSliceVarP(&rf.Projects, \"project\", \"p\", []string{},\n\t\t\"Will filter time entries using this project\")\n\t_ = cmdcompl.AddSuggestionsToFlag(cmd, \"project\",\n\t\tcmdcomplutil.NewProjectAutoComplete(f, f.Config()))\n\tcmd.Flags().StringVarP(&rf.Client, \"client\", \"c\", \"\",\n\t\t\"Will filter projects from this client\")\n\t_ = cmdcompl.AddSuggestionsToFlag(cmd, \"project\",\n\t\tcmdcomplutil.NewProjectAutoComplete(f, f.Config()))\n\tcmd.Flags().StringSliceVarP(&rf.TagIDs, \"tag\", \"T\", []string{},\n\t\t\"Will filter time entries using these tags\")\n\t_ = cmdcompl.AddSuggestionsToFlag(cmd, \"tag\",\n\t\tcmdcomplutil.NewTagAutoComplete(f))\n\n\tcmd.Flags().BoolVar(&rf.Billable, \"billable\", false,\n\t\t\"Will filter time entries that are billable\")\n\tcmd.Flags().BoolVar(&rf.NotBillable, \"not-billable\", false,\n\t\t\"Will filter time entries that are not billable\")\n}\n\n// ReportWithRange fetches and prints out time entries\nfunc ReportWithRange(\n\tf cmdutil.Factory, start, end time.Time,\n\tout io.Writer, rf ReportFlags,\n) error {\n\tuserId, err := f.GetUserID()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tworkspace, err := f.GetWorkspaceID()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc, err := f.Client()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcnf := f.Config()\n\tif len(rf.Projects) != 0 {\n\t\tif f.Config().IsAllowNameForID() {\n\t\t\tif rf.Projects, err = search.GetProjectsByName(\n\t\t\t\tc, cnf, workspace, rf.Client, rf.Projects); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t} else if rf.Client != \"\" {\n\t\tif f.Config().IsAllowNameForID() {\n\t\t\tif rf.Client, err = search.GetClientByName(\n\t\t\t\tc, workspace, rf.Client); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tps, err := c.GetProjects(api.GetProjectsParam{\n\t\t\tWorkspace:       workspace,\n\t\t\tClients:         []string{rf.Client},\n\t\t\tHydrate:         false,\n\t\t\tPaginationParam: api.AllPages(),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\trf.Projects = make([]string, len(ps))\n\t\tfor i := range ps {\n\t\t\trf.Projects[i] = ps[i].ID\n\t\t}\n\t}\n\n\tif len(rf.TagIDs) > 0 && f.Config().IsAllowNameForID() {\n\t\tif rf.TagIDs, err = search.GetTagsByName(\n\t\t\tc, workspace, rf.TagIDs); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif len(rf.Projects) == 0 {\n\t\trf.Projects = []string{\"\"}\n\t}\n\n\tstart = timehlp.TruncateDate(start)\n\tend = timehlp.TruncateDate(end).Add(time.Hour * 24)\n\n\twg := errgroup.Group{}\n\tlogs := make([][]dto.TimeEntry, len(rf.Projects))\n\n\tpages := api.AllPages()\n\tif rf.Limit > 0 {\n\t\tpages = api.PaginationParam{\n\t\t\tPage:     1,\n\t\t\tPageSize: rf.Limit,\n\t\t}\n\n\t\tif rf.Page > 0 {\n\t\t\tpages.Page = rf.Page\n\t\t}\n\t}\n\n\tfor i := range rf.Projects {\n\t\ti := i\n\t\twg.Go(func() error {\n\t\t\tvar err error\n\t\t\tlogs[i], err = c.LogRange(api.LogRangeParam{\n\t\t\t\tWorkspace:       workspace,\n\t\t\t\tUserID:          userId,\n\t\t\t\tFirstDate:       start,\n\t\t\t\tLastDate:        end,\n\t\t\t\tDescription:     rf.Description,\n\t\t\t\tProjectID:       rf.Projects[i],\n\t\t\t\tTagIDs:          rf.TagIDs,\n\t\t\t\tPaginationParam: pages,\n\t\t\t})\n\n\t\t\treturn err\n\t\t})\n\t}\n\n\tif err = wg.Wait(); err != nil {\n\t\treturn err\n\t}\n\n\tlog := make([]dto.TimeEntry, 0)\n\tfor i := range logs {\n\t\tlog = append(log, logs[i]...)\n\t}\n\n\tif rf.Billable || rf.NotBillable {\n\t\tlog = filterBilling(log, rf.Billable)\n\t}\n\n\tsort.Slice(log, func(i, j int) bool {\n\t\treturn log[j].TimeInterval.Start.After(\n\t\t\tlog[i].TimeInterval.Start,\n\t\t)\n\t})\n\n\tif rf.Limit > 0 && len(log) > rf.Limit {\n\t\tlog = log[len(log)-rf.Limit:]\n\t}\n\n\tif rf.FillMissingDates && len(log) > 0 {\n\t\tl := log\n\t\tlog = make([]dto.TimeEntry, 0, len(l))\n\t\tlog = append(log, fillMissing(start, l[0].TimeInterval.Start)...)\n\n\t\tnextDay := start\n\t\tfor i := range l {\n\t\t\tlog = append(log,\n\t\t\t\tfillMissing(nextDay, l[i].TimeInterval.Start)...)\n\t\t\tlog = append(log, l[i])\n\t\t\tnextDay = l[i].TimeInterval.Start.Add(\n\t\t\t\ttime.Duration(24-l[i].TimeInterval.Start.Hour()) * time.Hour)\n\t\t}\n\n\t\tlog = append(log, fillMissing(nextDay, end)...)\n\t}\n\n\treturn util.PrintTimeEntries(\n\t\tlog, out, cnf, rf.OutputFlags)\n}\n\nfunc filterBilling(l []dto.TimeEntry, billable bool) []dto.TimeEntry {\n\tr := make([]dto.TimeEntry, 0, len(l))\n\tfor i := 0; i < len(l); i++ {\n\t\tif l[i].Billable == billable {\n\t\t\tr = append(r, l[i])\n\t\t}\n\t}\n\n\treturn r\n}\n\nfunc fillMissing(first, last time.Time) []dto.TimeEntry {\n\tfirst = timehlp.TruncateDate(first)\n\tlast = timehlp.TruncateDate(last)\n\n\td := int(last.Sub(first).Hours() / 24)\n\tif d <= 0 {\n\t\treturn []dto.TimeEntry{}\n\t}\n\n\tmissing := make([]dto.TimeEntry, d)\n\tfor i := 0; i < d; i++ {\n\t\tt := missing[i]\n\t\tti := first.AddDate(0, 0, i)\n\t\tt.TimeInterval.Start = ti\n\t\tt.TimeInterval.End = &ti\n\t\tmissing[i] = t\n\t}\n\n\treturn missing\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/report/util/report_flag_test.go",
    "content": "package util_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestReportFlags_Check(t *testing.T) {\n\ttts := map[string]struct {\n\t\trf  util.ReportFlags\n\t\terr string\n\t}{\n\t\t\"just billable\": {\n\t\t\trf: util.ReportFlags{\n\t\t\t\tBillable:    true,\n\t\t\t\tNotBillable: false,\n\t\t\t},\n\t\t},\n\t\t\"just not-billable\": {\n\t\t\trf: util.ReportFlags{\n\t\t\t\tBillable:    false,\n\t\t\t\tNotBillable: true,\n\t\t\t},\n\t\t},\n\t\t\"only one billable\": {\n\t\t\trf: util.ReportFlags{\n\t\t\t\tBillable:    true,\n\t\t\t\tNotBillable: true,\n\t\t\t},\n\t\t\terr: \"can't be used together.*billable.*not-billable\",\n\t\t},\n\t\t\"just client\": {\n\t\t\trf: util.ReportFlags{\n\t\t\t\tClient: \"me\",\n\t\t\t},\n\t\t},\n\t\t\"just projects\": {\n\t\t\trf: util.ReportFlags{\n\t\t\t\tProjects: []string{\"mine\"},\n\t\t\t},\n\t\t},\n\t\t\"client and project\": {\n\t\t\trf: util.ReportFlags{\n\t\t\t\tClient:   \"me\",\n\t\t\t\tProjects: []string{\"mine\"},\n\t\t\t},\n\t\t},\n\t\t\"fill missing dates\": {\n\t\t\trf: util.ReportFlags{\n\t\t\t\tFillMissingDates: true,\n\t\t\t},\n\t\t},\n\t\t\"limit\": {\n\t\t\trf: util.ReportFlags{\n\t\t\t\tLimit: 10,\n\t\t\t},\n\t\t},\n\t\t\"only limit or fill missing\": {\n\t\t\trf: util.ReportFlags{\n\t\t\t\tLimit:            10,\n\t\t\t\tFillMissingDates: true,\n\t\t\t},\n\t\t\terr: \"can't be used together.*fill-missing-dates.*limit\",\n\t\t},\n\t\t\"limit and page\": {\n\t\t\trf: util.ReportFlags{\n\t\t\t\tLimit: 10,\n\t\t\t\tPage:  10,\n\t\t\t},\n\t\t},\n\t\t\"page needs limit\": {\n\t\t\trf: util.ReportFlags{\n\t\t\t\tPage: 10,\n\t\t\t},\n\t\t\terr: \"page can't be used without limit\",\n\t\t},\n\t}\n\n\tfor name, tt := range tts {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\terr := tt.rf.Check()\n\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !assert.Error(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/report/util/reportwithrange_test.go",
    "content": "package util_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc newDate(s string) time.Time {\n\tdate, _ := time.ParseInLocation(\"2006-01-02\", s, time.UTC)\n\treturn date\n}\n\nfunc TestReportWithRange(t *testing.T) {\n\tdate := newDate(\"2006-01-02\")\n\tfirst := time.Date(\n\t\tdate.Year(),\n\t\tdate.Month(),\n\t\tdate.Day(),\n\t\t0,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\ttime.UTC,\n\t)\n\tlast := first.AddDate(0, 0, 3)\n\ttts := []struct {\n\t\tname     string\n\t\tfactory  func(*testing.T) cmdutil.Factory\n\t\tflags    func(*testing.T) util.ReportFlags\n\t\texpected string\n\t\terr      string\n\t}{\n\t\t{\n\t\t\tname: \"no user\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"\", errors.New(\"no user\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\treturn util.NewReportFlags()\n\t\t\t},\n\t\t\terr: \"no user\",\n\t\t},\n\t\t{\n\t\t\tname: \"no workspace\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"\", errors.New(\"no workspace\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\treturn util.NewReportFlags()\n\t\t\t},\n\t\t\terr: \"no workspace\",\n\t\t},\n\t\t{\n\t\t\tname: \"no client\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\t\t\t\tf.On(\"Client\").Return(nil, errors.New(\"no client\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\treturn util.NewReportFlags()\n\t\t\t},\n\t\t\terr: \"no client\",\n\t\t},\n\t\t{\n\t\t\tname: \"http error project\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{}, errors.New(\"http error\"))\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.Projects = []string{\"p\"}\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\terr: \"http error\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid project\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\t\t\t\tcf.On(\"IsSearchProjectWithClientsName\").Return(false)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{{Name: \"right\"}}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.Projects = []string{\"wrong\"}\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\terr: \"No project.*wrong' was found\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid client\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{{Name: \"right\"}}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.Client = \"right\"\n\t\t\t\trf.Projects = []string{\"wrong\"}\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\terr: \"No client.*right' was found\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid project for client\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\t\t\t\tcf.On(\"IsSearchProjectWithClientsName\").Return(false)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(\n\t\t\t\t\t\t[]dto.Project{{\n\t\t\t\t\t\t\tName:       \"right\",\n\t\t\t\t\t\t\tClientName: \"right\",\n\t\t\t\t\t\t\tClientID:   \"r1\",\n\t\t\t\t\t\t}},\n\t\t\t\t\t\tnil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.Client = \"right\"\n\t\t\t\trf.Projects = []string{\"wrong\"}\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\terr: \"No project.*wrong' was found for client 'right'\",\n\t\t},\n\t\t{\n\t\t\tname: \"range http error\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tcf := mocks.NewMockConfig(t)\n\t\t\t\tf.On(\"Config\").Return(cf)\n\t\t\t\tcf.On(\"IsAllowNameForID\").Return(true)\n\t\t\t\tcf.On(\"IsSearchProjectWithClientsName\").Return(false)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetProjects\", api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:         \"p\",\n\t\t\t\t\t\t\tName:       \"right\",\n\t\t\t\t\t\t\tClientName: \"right\",\n\t\t\t\t\t\t\tClientID:   \"c1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:         \"p\",\n\t\t\t\t\t\t\tName:       \"right\",\n\t\t\t\t\t\t\tClientName: \"wrong\",\n\t\t\t\t\t\t\tClientID:   \"c2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t}, nil)\n\n\t\t\t\tc.On(\"LogRange\", api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tProjectID:       \"p\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.TimeEntry{}, errors.New(\"http error\"))\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.Projects = []string{\"right\"}\n\t\t\t\trf.Client = \"right\"\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\terr: \"http error\",\n\t\t},\n\t\t{\n\t\t\tname: \"project and description\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID: false,\n\t\t\t\t})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"LogRange\", api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tProjectID:       \"p\",\n\t\t\t\t\tDescription:     \"desc\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"time-entry-1\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{Start: last}},\n\t\t\t\t\t{ID: \"time-entry-2\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{Start: first}},\n\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.Projects = []string{\"p\"}\n\t\t\t\trf.Description = \"desc\"\n\t\t\t\trf.Quiet = true\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\ttime-entry-2\n\t\t\t\ttime-entry-1\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"fill missing dates\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tf.On(\"Config\").Return(&mocks.SimpleConfig{})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"LogRange\", api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"time-entry-1\", TimeInterval: dto.TimeInterval{\n\t\t\t\t\t\tStart: newDate(\"2006-01-04\")}},\n\t\t\t\t\t{ID: \"time-entry-2\", TimeInterval: dto.TimeInterval{\n\t\t\t\t\t\tStart: newDate(\"2006-01-01\")}},\n\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.FillMissingDates = true\n\t\t\t\trf.Format = \"{{.ID}};{{ .TimeInterval.Start.Format \" +\n\t\t\t\t\t`\"2006-01-02\"` +\n\t\t\t\t\t\" }}\"\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\ttime-entry-2;2006-01-01\n\t\t\t\t;2006-01-02\n\t\t\t\t;2006-01-03\n\t\t\t\ttime-entry-1;2006-01-04\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"billable only\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"LogRange\", api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"time-entry-1\", Billable: true},\n\t\t\t\t\t{ID: \"time-entry-2\", Billable: false},\n\t\t\t\t\t{ID: \"time-entry-3\", Billable: true},\n\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.Billable = true\n\t\t\t\trf.Quiet = true\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\ttime-entry-1\n\t\t\t\ttime-entry-3\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"not billable only\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"LogRange\", api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"time-entry-1\", Billable: true},\n\t\t\t\t\t{ID: \"time-entry-2\", Billable: false},\n\t\t\t\t\t{ID: \"time-entry-3\", Billable: true},\n\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.NotBillable = true\n\t\t\t\trf.Quiet = true\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\ttime-entry-2\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"not billable & tag cli only\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID: true,\n\t\t\t\t})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\ttag := dto.Tag{ID: \"t1\", Name: \"Client\"}\n\t\t\t\tc.On(\"GetTags\", api.GetTagsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Tag{tag}, nil)\n\n\t\t\t\tc.On(\"LogRange\", api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tTagIDs:          []string{tag.ID},\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"te-1\", Tags: []dto.Tag{tag}, Billable: true},\n\t\t\t\t\t{ID: \"te-2\", Tags: []dto.Tag{tag}, Billable: false},\n\t\t\t\t\t{ID: \"te-3\", Tags: []dto.Tag{tag}, Billable: true},\n\t\t\t\t\t{ID: \"te-4\", Tags: []dto.Tag{tag}, Billable: false},\n\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.NotBillable = true\n\t\t\t\trf.Quiet = true\n\t\t\t\trf.TagIDs = []string{\"cli\"}\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\tte-2\n\t\t\t\tte-4\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"multiple projects\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tf.EXPECT().Config().Return(\n\t\t\t\t\t&mocks.SimpleConfig{AllowNameForID: true})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{\n\t\t\t\t\t{ID: \"p1\", Name: \"p1\"},\n\t\t\t\t\t{ID: \"p2\", Name: \"p2\"},\n\t\t\t\t\t{ID: \"p3\", Name: \"p3\"},\n\t\t\t\t}, nil)\n\n\t\t\t\tc.EXPECT().LogRange(api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tProjectID:       \"p1\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"te-1\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{ID: \"te-3\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first.Add(time.Duration(2)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, nil)\n\n\t\t\t\tc.EXPECT().LogRange(api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tProjectID:       \"p2\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"te-2\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first.Add(time.Duration(1)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{ID: \"te-4\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first.Add(time.Duration(3)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.Quiet = true\n\t\t\t\trf.Projects = []string{\"p1\", \"p2\"}\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\tte-1\n\t\t\t\tte-2\n\t\t\t\tte-3\n\t\t\t\tte-4\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"projects form a client\",\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.Quiet = true\n\t\t\t\trf.Client = \"me\"\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tf.EXPECT().Config().Return(\n\t\t\t\t\t&mocks.SimpleConfig{AllowNameForID: true})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.EXPECT().GetClients(api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Client{\n\t\t\t\t\t\t{ID: \"c1\", Name: \"me\"},\n\t\t\t\t\t\t{ID: \"c2\", Name: \"you\"},\n\t\t\t\t\t}, nil)\n\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tClients:         []string{\"c1\"},\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{\n\t\t\t\t\t{ID: \"p1\", Name: \"p1\", ClientID: \"c1\", ClientName: \"me\"},\n\t\t\t\t\t{ID: \"p3\", Name: \"p3\", ClientID: \"c1\", ClientName: \"me\"},\n\t\t\t\t}, nil)\n\n\t\t\t\tc.EXPECT().LogRange(api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tProjectID:       \"p1\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"te-1\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{ID: \"te-3\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first.Add(time.Duration(2)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, nil)\n\n\t\t\t\tc.EXPECT().LogRange(api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tProjectID:       \"p3\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"te-2\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first.Add(time.Duration(1)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\tte-1\n\t\t\t\tte-2\n\t\t\t\tte-3\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"change timezone\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\ttz, _ := time.LoadLocation(\"America/Sao_Paulo\")\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tTimeZoneLoc:    tz,\n\t\t\t\t\tAllowNameForID: false,\n\t\t\t\t})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"LogRange\", api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tProjectID:       \"p\",\n\t\t\t\t\tDescription:     \"desc\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"time-entry-1\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{Start: last}},\n\t\t\t\t\t{ID: \"time-entry-2\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{Start: first}},\n\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.Projects = []string{\"p\"}\n\t\t\t\trf.Description = \"desc\"\n\t\t\t\trf.Quiet = true\n\t\t\t\trf.Format = `{{ .TimeInterval.Start.Format \"` +\n\t\t\t\t\ttimehlp.FullTimeFormat +\n\t\t\t\t\t`\" }}`\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\t2006-01-01 22:00:00\n\t\t\t\t2006-01-04 22:00:00\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"limit number of time entries\",\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.Limit = 2\n\t\t\t\trf.Quiet = true\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tf.EXPECT().Config().Return(\n\t\t\t\t\t&mocks.SimpleConfig{AllowNameForID: true})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.EXPECT().LogRange(api.LogRangeParam{\n\t\t\t\t\tWorkspace: \"w\",\n\t\t\t\t\tUserID:    \"u\",\n\t\t\t\t\tFirstDate: first,\n\t\t\t\t\tLastDate:  last,\n\t\t\t\t\tPaginationParam: api.PaginationParam{\n\t\t\t\t\t\tPage:     1,\n\t\t\t\t\t\tPageSize: 2,\n\t\t\t\t\t},\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"te-1\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{ID: \"te-3\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first.Add(time.Duration(2)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\tte-1\n\t\t\t\tte-3\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"limit number of time entries with client filter\",\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.Limit = 2\n\t\t\t\trf.Client = \"me\"\n\t\t\t\trf.Quiet = true\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tf.EXPECT().Config().Return(\n\t\t\t\t\t&mocks.SimpleConfig{AllowNameForID: true})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.EXPECT().GetClients(api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Client{\n\t\t\t\t\t\t{ID: \"c1\", Name: \"me\"},\n\t\t\t\t\t\t{ID: \"c2\", Name: \"you\"},\n\t\t\t\t\t}, nil)\n\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tClients:         []string{\"c1\"},\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{\n\t\t\t\t\t{ID: \"p1\", Name: \"p1\", ClientID: \"c1\", ClientName: \"me\"},\n\t\t\t\t\t{ID: \"p3\", Name: \"p3\", ClientID: \"c1\", ClientName: \"me\"},\n\t\t\t\t}, nil)\n\n\t\t\t\tp := api.PaginationParam{Page: 1, PageSize: 2}\n\t\t\t\tc.EXPECT().LogRange(api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tProjectID:       \"p1\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tPaginationParam: p,\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"te-1\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{ID: \"te-3\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first.Add(time.Duration(2)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, nil)\n\n\t\t\t\tc.EXPECT().LogRange(api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tProjectID:       \"p3\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tPaginationParam: p,\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"te-2\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first.Add(time.Duration(1)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\tte-2\n\t\t\t\tte-3\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"only a limited page\",\n\t\t\tflags: func(t *testing.T) util.ReportFlags {\n\t\t\t\trf := util.NewReportFlags()\n\t\t\t\trf.Limit = 4\n\t\t\t\trf.Page = 12\n\t\t\t\trf.Client = \"me\"\n\t\t\t\trf.Quiet = true\n\t\t\t\treturn rf\n\t\t\t},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"GetUserID\").Return(\"u\", nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"w\", nil)\n\n\t\t\t\tf.EXPECT().Config().Return(\n\t\t\t\t\t&mocks.SimpleConfig{AllowNameForID: true})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.EXPECT().GetClients(api.GetClientsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Client{\n\t\t\t\t\t\t{ID: \"c1\", Name: \"me\"},\n\t\t\t\t\t\t{ID: \"c2\", Name: \"you\"},\n\t\t\t\t\t}, nil)\n\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tClients:         []string{\"c1\"},\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).Return([]dto.Project{\n\t\t\t\t\t{ID: \"p1\", Name: \"p1\", ClientID: \"c1\", ClientName: \"me\"},\n\t\t\t\t\t{ID: \"p3\", Name: \"p3\", ClientID: \"c1\", ClientName: \"me\"},\n\t\t\t\t}, nil)\n\n\t\t\t\tp := api.PaginationParam{Page: 12, PageSize: 4}\n\t\t\t\tc.EXPECT().LogRange(api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tProjectID:       \"p1\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tPaginationParam: p,\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"te-1\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{ID: \"te-3\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first.Add(time.Duration(2)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, nil)\n\n\t\t\t\tc.EXPECT().LogRange(api.LogRangeParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tUserID:          \"u\",\n\t\t\t\t\tProjectID:       \"p3\",\n\t\t\t\t\tFirstDate:       first,\n\t\t\t\t\tLastDate:        last,\n\t\t\t\t\tPaginationParam: p,\n\t\t\t\t}).Return([]dto.TimeEntry{\n\t\t\t\t\t{ID: \"te-2\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first.Add(time.Duration(1)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{ID: \"te-4\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first.Add(time.Duration(3)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{ID: \"te-5\",\n\t\t\t\t\t\tTimeInterval: dto.TimeInterval{\n\t\t\t\t\t\t\tStart: first.Add(time.Duration(4)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\tte-2\n\t\t\t\tte-3\n\t\t\t\tte-4\n\t\t\t\tte-5\n\t\t\t`),\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tb := bytes.NewBufferString(\"\")\n\t\t\terr := util.ReportWithRange(\n\t\t\t\ttt.factory(t),\n\t\t\t\tdate,\n\t\t\t\tdate.AddDate(0, 0, 2),\n\t\t\t\tb,\n\t\t\t\ttt.flags(t),\n\t\t\t)\n\n\t\t\tif tt.err != \"\" {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, b.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/report/yesterday/yesterday.go",
    "content": "package yesterday\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdYesterday represents report today command\nfunc NewCmdYesterday(f cmdutil.Factory) *cobra.Command {\n\tof := util.NewReportFlags()\n\tcmd := &cobra.Command{\n\t\tUse:   \"yesterday\",\n\t\tShort: \"List all time entries created yesterday\",\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tday := timehlp.Today().Add(-1)\n\t\t\treturn util.ReportWithRange(f, day, day, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tcmd.Long = cmd.Short + \"\\n\\n\" +\n\t\tutil.HelpNamesForIds + \"\\n\" +\n\t\tutil.HelpMoreInfoAboutPrinting\n\n\tutil.AddReportFlags(f, cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/show/show.go",
    "content": "package show\n\nimport (\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timeentryhlp\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdShow represents the show command\nfunc NewCmdShow(f cmdutil.Factory) *cobra.Command {\n\tof := util.OutputFlags{TimeFormat: timehlp.FullTimeFormat}\n\tva := cmdcompl.ValidArgsSlide{\n\t\ttimeentryhlp.AliasCurrent, timeentryhlp.AliasLast}\n\tcmd := &cobra.Command{\n\t\tUse: \"show [ <time-entry-id> | \" + va.IntoUseOptions() +\n\t\t\t\" | ^n ]\",\n\t\tValidArgs: va.IntoValidArgs(),\n\t\tArgs:      cobra.MaximumNArgs(1),\n\t\tShort:     \"Show information about one time entry.\",\n\t\tLong: heredoc.Docf(`\n\t\t\tShow information about one time entry.\n\n\t\t\tIf no time entry ID is informed it shows the running it exists.\n\n\t\t\tTo 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.\n\n\t\t\t%s\n\t\t`,\n\t\t\ttimeentryhlp.AliasLast,\n\t\t\tutil.HelpMoreInfoAboutPrinting,\n\t\t),\n\t\tExample: heredoc.Docf(`\n\t\t\t# trying to show running time entry, when there is none\n\t\t\t$ %[1]s\n\t\t\tlooking for running time entry: time entry was not found\n\n\t\t\t# show the last time entry (ended)\n\t\t\t$ %[1]s last -q\n\t\t\t62af70d849445270d7c09fbd\n\n\t\t\t# show the time entry before the last one\n\t\t\t$ %[1]s ^2 -q\n\t\t\t62af668b49445270d7c092e4\n\t\t`, \"clockify-cli show\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tuserID, err := f.GetUserID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tw, err := f.GetWorkspaceID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tid := timeentryhlp.AliasCurrent\n\t\t\tif len(args) > 0 {\n\t\t\t\tid = args[0]\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\ttei, err := timeentryhlp.GetTimeEntry(c, w, userID, id)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn util.PrintTimeEntryImpl(tei, f, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tutil.AddPrintTimeEntriesFlags(cmd, &of)\n\t_ = cmd.MarkFlagRequired(\"workspace\")\n\t_ = cmd.MarkFlagRequired(\"user-id\")\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/split/split.go",
    "content": "package split\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timeentryhlp\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nfunc NewCmdSplit(\n\tf cmdutil.Factory,\n\treport func([]dto.TimeEntry, io.Writer, util.OutputFlags) error,\n) *cobra.Command {\n\tof := util.OutputFlags{TimeFormat: timehlp.OnlyTimeFormat}\n\tva := cmdcompl.ValidArgsSlide{\n\t\ttimeentryhlp.AliasCurrent,\n\t\ttimeentryhlp.AliasLast,\n\t\ttimeentryhlp.AliasLatest,\n\t}\n\n\tcmd := &cobra.Command{\n\t\tUse: \"split { <time-entry-id> | \" + va.IntoUseOptions() + \" | ^n } \" +\n\t\t\t\" <time>...\",\n\t\tArgs: cobra.MatchAll(\n\t\t\tcmdutil.RequiredNamedArgs(\"time entry id\"),\n\t\t\tcobra.MinimumNArgs(2),\n\t\t),\n\t\tValidArgs: va.IntoValidArgs(),\n\t\tShort:     `Splits a time entry into multiple time entries`,\n\t\tLong: heredoc.Docf(`\n\t\t\tSplit a time entry.\n\t\t\tThe time arguments can be more than one, but must be increasing.\n\n\t\t\t%s\n\t\t\t%s\n\t\t\t%s\n\t\t\t%s\n\t\t\t%s\n\t\t`,\n\t\t\tutil.HelpTimeEntriesAliasForEdit,\n\t\t\tutil.HelpInteractiveByDefault,\n\t\t\tutil.HelpDateTimeFormats,\n\t\t\tutil.HelpNamesForIds,\n\t\t\tutil.HelpMoreInfoAboutPrinting,\n\t\t),\n\t\tExample: heredoc.Docf(`\n\t\t\t# starting a time entry\n\t\t\t$ %[1]s in --project cli --tag dev -d \"Doing work before lunch\" --task \"edit\" --md\n\t\t\tID: %[2]s62ae4b304ebb4f143c931d50%[2]s  \n\t\t\tBillable: %[2]syes%[2]s  \n\t\t\tLocked: %[2]sno%[2]s  \n\t\t\tProject: Clockify Cli (%[2]s621948458cb9606d934ebb1c%[2]s)  \n\t\t\tTask: Edit Command (%[2]s62ae4af04ebb4f143c931d2e%[2]s)  \n\t\t\tInterval: %[2]s2022-06-18 11:01:16%[2]s until %[2]snow%[2]s  \n\t\t\tDescription:\n\t\t\t> Adding docs to edit\n\n\t\t\tTags:\n\t\t\t * Development (%[2]s62ae28b72518aa18da2acb49%[2]s)\n\n\t\t\t# splits the time entry at lunch and now\n\t\t\t$ %[1]s split 12:00 13:30 --format '{{.ID}},{{.TimeInterval.Start|ft}}'\n\t\t\t62ae4b304ebb4f143c931d50,11:01\n\t\t\t3c931d502ae4b3064ebb4f14,12:00\n\t\t\tebb4f143c962ae4b30431d50,13:30\n\t\t`, \"clockify-cli\", \"`\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tsplits := make([]time.Time, len(args)-1)\n\t\t\tfor i := range splits {\n\t\t\t\tt, err := timehlp.ConvertToTime(args[1+i])\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\t\"argument %d could not be converted to time: %w\",\n\t\t\t\t\t\ti+2, err)\n\t\t\t\t}\n\n\t\t\t\tif i > 0 && t.Before(splits[i-1]) {\n\t\t\t\t\treturn errors.New(\"splits must be in increasing order\")\n\t\t\t\t}\n\n\t\t\t\tsplits[i] = t\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tuserID, err := f.GetUserID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tw, err := f.GetWorkspaceID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tte, err := timeentryhlp.GetTimeEntry(\n\t\t\t\tc,\n\t\t\t\tw,\n\t\t\t\tuserID,\n\t\t\t\targs[0],\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif te.TimeInterval.Start.After(splits[0]) {\n\t\t\t\treturn errors.New(\"time splits must be after \" +\n\t\t\t\t\tte.TimeInterval.Start.Format(timehlp.FullTimeFormat))\n\t\t\t}\n\n\t\t\tif te.TimeInterval.End != nil && te.TimeInterval.End.Before(\n\t\t\t\tsplits[len(splits)-1]) {\n\t\t\t\treturn errors.New(\"time splits must be before \" +\n\t\t\t\t\tte.TimeInterval.End.Format(timehlp.FullTimeFormat))\n\t\t\t}\n\n\t\t\tif _, err = c.UpdateTimeEntry(api.UpdateTimeEntryParam{\n\t\t\t\tWorkspace:   te.WorkspaceID,\n\t\t\t\tTimeEntryID: te.ID,\n\t\t\t\tDescription: te.Description,\n\t\t\t\tStart:       te.TimeInterval.Start,\n\t\t\t\tEnd:         &splits[0],\n\t\t\t\tBillable:    te.Billable,\n\t\t\t\tProjectID:   te.ProjectID,\n\t\t\t\tTaskID:      te.TaskID,\n\t\t\t\tTagIDs:      te.TagIDs,\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\ttes := make([]dto.TimeEntry, len(splits)+1)\n\t\t\tgetHydrated := func(i int, id string) error {\n\t\t\t\tt, err := c.GetHydratedTimeEntry(api.GetTimeEntryParam{\n\t\t\t\t\tTimeEntryID: id,\n\t\t\t\t\tWorkspace:   w,\n\t\t\t\t})\n\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\ttes[i] = *t\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\teg := errgroup.Group{}\n\t\t\teg.Go(func() error { return getHydrated(0, te.ID) })\n\n\t\t\tfor i := range splits {\n\t\t\t\ti := i\n\t\t\t\teg.Go(func() error {\n\t\t\t\t\tend := te.TimeInterval.End\n\t\t\t\t\tif i < len(splits)-1 {\n\t\t\t\t\t\tend = &splits[i+1]\n\t\t\t\t\t}\n\n\t\t\t\t\tte, err := c.CreateTimeEntry(api.CreateTimeEntryParam{\n\t\t\t\t\t\tWorkspace:   te.WorkspaceID,\n\t\t\t\t\t\tBillable:    &te.Billable,\n\t\t\t\t\t\tStart:       splits[i],\n\t\t\t\t\t\tEnd:         end,\n\t\t\t\t\t\tProjectID:   te.ProjectID,\n\t\t\t\t\t\tDescription: te.Description,\n\t\t\t\t\t\tTagIDs:      te.TagIDs,\n\t\t\t\t\t\tTaskID:      te.TaskID,\n\t\t\t\t\t})\n\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\treturn getHydrated(i+1, te.ID)\n\t\t\t\t})\n\n\t\t\t}\n\n\t\t\tif err := eg.Wait(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn report(tes, cmd.OutOrStdout(), of)\n\t\t},\n\t}\n\n\tutil.AddPrintTimeEntriesFlags(cmd, &of)\n\tutil.AddPrintMultipleTimeEntriesFlags(cmd)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/split/split_test.go",
    "content": "package split_test\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/split\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewCmdSplitShouldFail(t *testing.T) {\n\tw := dto.Workspace{ID: \"w\"}\n\tstart, _ := timehlp.ConvertToTime(\"08:15\")\n\tend, _ := timehlp.ConvertToTime(\"12:15\")\n\tte := dto.TimeEntryImpl{\n\t\tWorkspaceID: w.ID,\n\t\tID:          \"timeentryid\",\n\t\tDescription: \"Something\",\n\t\tProjectID:   \"oldproj\",\n\t\tTaskID:      \"oldtask\",\n\t\tTimeInterval: dto.TimeInterval{\n\t\t\tStart: start,\n\t\t\tEnd:   &end,\n\t\t},\n\t}\n\n\tfindTT := func(t *testing.T) cmdutil.Factory {\n\t\tf := mocks.NewMockFactory(t)\n\n\t\tf.EXPECT().GetWorkspaceID().Return(w.ID, nil)\n\t\tf.EXPECT().GetUserID().Return(w.ID, nil)\n\n\t\tc := mocks.NewMockClient(t)\n\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\tc.EXPECT().GetTimeEntry(api.GetTimeEntryParam{\n\t\t\tWorkspace:   te.WorkspaceID,\n\t\t\tTimeEntryID: te.ID,\n\t\t}).\n\t\t\tReturn(&te, nil)\n\n\t\treturn f\n\t}\n\n\ttts := []struct {\n\t\tname string\n\t\targs []string\n\t\tf    func(*testing.T) cmdutil.Factory\n\t\terr  string\n\t}{\n\t\t{\n\t\t\tname: \"time string is not valid\",\n\t\t\tf: func(t *testing.T) cmdutil.Factory {\n\t\t\t\treturn mocks.NewMockFactory(t)\n\t\t\t},\n\t\t\targs: []string{te.ID, \"ff\"},\n\t\t\terr:  \"argument 2 could not be converted\",\n\t\t},\n\t\t{\n\t\t\tname: \"third time string is not valid\",\n\t\t\tf: func(t *testing.T) cmdutil.Factory {\n\t\t\t\treturn mocks.NewMockFactory(t)\n\t\t\t},\n\t\t\targs: []string{te.ID, \"11:00\", \"ff\"},\n\t\t\terr:  \"argument 3 could not be converted\",\n\t\t},\n\t\t{\n\t\t\tname: \"split must be in order\",\n\t\t\tf: func(t *testing.T) cmdutil.Factory {\n\t\t\t\treturn mocks.NewMockFactory(t)\n\t\t\t},\n\t\t\targs: []string{te.ID, \"8:30\", \"08:20\"},\n\t\t\terr:  \"splits must be in increasing order\",\n\t\t},\n\t\t{\n\t\t\tname: \"time entry not found\",\n\t\t\tf: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\n\t\t\t\tf.EXPECT().GetWorkspaceID().Return(w.ID, nil)\n\t\t\t\tf.EXPECT().GetUserID().Return(w.ID, nil)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tc.EXPECT().GetTimeEntry(api.GetTimeEntryParam{\n\t\t\t\t\tWorkspace:   te.WorkspaceID,\n\t\t\t\t\tTimeEntryID: te.ID,\n\t\t\t\t}).\n\t\t\t\t\tReturn(nil, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\targs: []string{te.ID, \"11:00\"},\n\t\t\terr:  \"not found\",\n\t\t},\n\t\t{\n\t\t\tname: \"split must be after the time entry start\",\n\t\t\tf:    findTT,\n\t\t\targs: []string{te.ID, \"7:00\"},\n\t\t\terr:  \"time splits must be after .* 08:15\",\n\t\t},\n\t\t{\n\t\t\tname: \"split must be before the time entry ends\",\n\t\t\tf:    findTT,\n\t\t\targs: []string{te.ID, \"18:00\"},\n\t\t\terr:  \"time splits must be before .* 12:15\",\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\ttt := &tts[i]\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcalled := false\n\t\t\tcmd := split.NewCmdSplit(tt.f(t), func(\n\t\t\t\t_ []dto.TimeEntry, _ io.Writer, _ util.OutputFlags) error {\n\t\t\t\tcalled = true\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SilenceErrors = true\n\n\t\t\tout := bytes.NewBufferString(\"\")\n\t\t\tcmd.SetOut(out)\n\t\t\tcmd.SetErr(out)\n\n\t\t\tcmd.SetArgs(tt.args)\n\t\t\t_, err := cmd.ExecuteC()\n\n\t\t\tassert.False(t, called)\n\t\t\tassert.Error(t, err)\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n}\n\nfunc TestNewCmdSplitShouldCreateNewEntries(t *testing.T) {\n\tw := dto.Workspace{ID: \"w\"}\n\tstart, _ := timehlp.ConvertToTime(\"08:00\")\n\tte := dto.TimeEntryImpl{\n\t\tWorkspaceID: w.ID,\n\t\tID:          \"timeentryid\",\n\t\tDescription: \"Something\",\n\t\tProjectID:   \"oldproj\",\n\t\tTaskID:      \"oldtask\",\n\t\tTimeInterval: dto.TimeInterval{\n\t\t\tStart: start,\n\t\t},\n\t}\n\n\tgetH := func(c *mocks.MockClient, id string) {\n\n\t\tc.EXPECT().GetHydratedTimeEntry(api.GetTimeEntryParam{\n\t\t\tWorkspace:   w.ID,\n\t\t\tTimeEntryID: id,\n\t\t}).\n\t\t\tReturn(&dto.TimeEntry{\n\t\t\t\tWorkspaceID: te.WorkspaceID,\n\t\t\t\tID:          id,\n\t\t\t}, nil)\n\t}\n\n\tgetAndUpdate := func(\n\t\tt *testing.T, te dto.TimeEntryImpl, end time.Time,\n\t) (cmdutil.Factory, *mocks.MockClient) {\n\t\tf := mocks.NewMockFactory(t)\n\n\t\tf.EXPECT().GetWorkspaceID().Return(w.ID, nil)\n\t\tf.EXPECT().GetUserID().Return(w.ID, nil)\n\n\t\tc := mocks.NewMockClient(t)\n\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\tc.EXPECT().GetTimeEntry(api.GetTimeEntryParam{\n\t\t\tWorkspace:   te.WorkspaceID,\n\t\t\tTimeEntryID: te.ID,\n\t\t}).\n\t\t\tReturn(&te, nil)\n\n\t\tc.EXPECT().UpdateTimeEntry(api.UpdateTimeEntryParam{\n\t\t\tWorkspace:   te.WorkspaceID,\n\t\t\tTimeEntryID: te.ID,\n\t\t\tStart:       te.TimeInterval.Start,\n\t\t\tEnd:         &end,\n\t\t\tBillable:    false,\n\t\t\tDescription: te.Description,\n\t\t\tProjectID:   te.ProjectID,\n\t\t\tTaskID:      te.TaskID,\n\t\t\tTagIDs:      te.TagIDs,\n\t\t}).\n\t\t\tReturn(te, nil)\n\n\t\tgetH(c, te.ID)\n\n\t\treturn f, c\n\t}\n\n\tcreate := func(c *mocks.MockClient, id string, ted util.TimeEntryDTO) {\n\t\tc.EXPECT().CreateTimeEntry(api.CreateTimeEntryParam{\n\t\t\tWorkspace:   te.WorkspaceID,\n\t\t\tStart:       ted.Start,\n\t\t\tEnd:         ted.End,\n\t\t\tDescription: te.Description,\n\t\t\tProjectID:   te.ProjectID,\n\t\t\tTaskID:      te.TaskID,\n\t\t\tTagIDs:      te.TagIDs,\n\t\t\tBillable:    &te.Billable,\n\t\t}).\n\t\t\tReturn(dto.TimeEntryImpl{ID: id}, nil)\n\n\t\tgetH(c, id)\n\t}\n\n\ttts := []struct {\n\t\tname string\n\t\targs []string\n\t\tf    func(*testing.T) cmdutil.Factory\n\t}{\n\t\t{\n\t\t\tname: \"split in two\",\n\t\t\targs: []string{te.ID, \"8:30\"},\n\t\t\tf: func(t *testing.T) cmdutil.Factory {\n\t\t\t\ts, _ := timehlp.ConvertToTime(\"08:30\")\n\t\t\t\tf, c := getAndUpdate(t, te, s)\n\n\t\t\t\tcreate(c, \"123\", util.TimeEntryDTO{\n\t\t\t\t\tWorkspace:   te.ID,\n\t\t\t\t\tUserID:      te.UserID,\n\t\t\t\t\tProjectID:   te.ProjectID,\n\t\t\t\t\tTaskID:      te.TaskID,\n\t\t\t\t\tDescription: te.Description,\n\t\t\t\t\tStart:       s,\n\t\t\t\t\tEnd:         nil,\n\t\t\t\t})\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"split in three\",\n\t\t\targs: []string{te.ID, \"08:20\", \"8:30\"},\n\t\t\tf: func(t *testing.T) cmdutil.Factory {\n\t\t\t\ts, _ := timehlp.ConvertToTime(\"08:20\")\n\t\t\t\tf, c := getAndUpdate(t, te, s)\n\n\t\t\t\te, _ := timehlp.ConvertToTime(\"08:30\")\n\t\t\t\tcreate(c, \"123\", util.TimeEntryDTO{\n\t\t\t\t\tWorkspace:   te.ID,\n\t\t\t\t\tUserID:      te.UserID,\n\t\t\t\t\tProjectID:   te.ProjectID,\n\t\t\t\t\tTaskID:      te.TaskID,\n\t\t\t\t\tDescription: te.Description,\n\t\t\t\t\tStart:       s,\n\t\t\t\t\tEnd:         &e,\n\t\t\t\t})\n\n\t\t\t\tcreate(c, \"456\", util.TimeEntryDTO{\n\t\t\t\t\tWorkspace:   te.ID,\n\t\t\t\t\tUserID:      te.UserID,\n\t\t\t\t\tProjectID:   te.ProjectID,\n\t\t\t\t\tTaskID:      te.TaskID,\n\t\t\t\t\tDescription: te.Description,\n\t\t\t\t\tStart:       e,\n\t\t\t\t\tEnd:         nil,\n\t\t\t\t})\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"split in three with end\",\n\t\t\targs: []string{te.ID, \"08:30\", \"9:00\"},\n\t\t\tf: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tend, _ := timehlp.ConvertToTime(\"10:00\")\n\t\t\t\tte := dto.TimeEntryImpl{\n\t\t\t\t\tWorkspaceID: w.ID,\n\t\t\t\t\tID:          \"timeentryid\",\n\t\t\t\t\tDescription: \"Something\",\n\t\t\t\t\tProjectID:   \"oldproj\",\n\t\t\t\t\tTaskID:      \"oldtask\",\n\t\t\t\t\tTimeInterval: dto.NewTimeInterval(\n\t\t\t\t\t\tte.TimeInterval.Start,\n\t\t\t\t\t\t&end,\n\t\t\t\t\t),\n\t\t\t\t}\n\n\t\t\t\ts, _ := timehlp.ConvertToTime(\"08:30\")\n\t\t\t\tf, c := getAndUpdate(t, te, s)\n\n\t\t\t\te, _ := timehlp.ConvertToTime(\"09:00\")\n\t\t\t\tcreate(c, \"123\", util.TimeEntryDTO{\n\t\t\t\t\tWorkspace:   te.ID,\n\t\t\t\t\tUserID:      te.UserID,\n\t\t\t\t\tProjectID:   te.ProjectID,\n\t\t\t\t\tTaskID:      te.TaskID,\n\t\t\t\t\tDescription: te.Description,\n\t\t\t\t\tStart:       s,\n\t\t\t\t\tEnd:         &e,\n\t\t\t\t})\n\n\t\t\t\tcreate(c, \"456\", util.TimeEntryDTO{\n\t\t\t\t\tWorkspace:   te.ID,\n\t\t\t\t\tUserID:      te.UserID,\n\t\t\t\t\tProjectID:   te.ProjectID,\n\t\t\t\t\tTaskID:      te.TaskID,\n\t\t\t\t\tDescription: te.Description,\n\t\t\t\t\tStart:       e,\n\t\t\t\t\tEnd:         &end,\n\t\t\t\t})\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\ttt := &tts[i]\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcalled := false\n\t\t\tcmd := split.NewCmdSplit(tt.f(t), func(\n\t\t\t\t_ []dto.TimeEntry, _ io.Writer, _ util.OutputFlags) error {\n\t\t\t\tcalled = true\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SilenceErrors = true\n\n\t\t\tout := bytes.NewBufferString(\"\")\n\t\t\tcmd.SetOut(out)\n\t\t\tcmd.SetErr(out)\n\n\t\t\tcmd.SetArgs(tt.args)\n\t\t\t_, err := cmd.ExecuteC()\n\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif assert.True(t, called) {\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/timeentry.go",
    "content": "package timeentry\n\nimport (\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/clone\"\n\tdel \"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/delete\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/edit\"\n\tem \"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/edit-multipple\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/in\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/invoiced\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/manual\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/out\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/report\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/show\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/split\"\n\tteutil \"github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc NewCmdTimeEntry(f cmdutil.Factory) (cmds []*cobra.Command) {\n\trmFn := func(\n\t\ttes []dto.TimeEntry, out io.Writer, of teutil.OutputFlags) error {\n\t\treturn teutil.PrintTimeEntries(tes, out, f.Config(), of)\n\t}\n\n\trFn := func(\n\t\ttei dto.TimeEntryImpl, out io.Writer, of teutil.OutputFlags,\n\t) error {\n\t\treturn teutil.PrintTimeEntryImpl(tei, f, out, of)\n\t}\n\n\tcmds = append(\n\t\tcmds,\n\n\t\tin.NewCmdIn(f, rFn),\n\t\tmanual.NewCmdManual(f),\n\t\tclone.NewCmdClone(f),\n\n\t\tedit.NewCmdEdit(f, rFn),\n\t\tem.NewCmdEditMultiple(f),\n\n\t\tsplit.NewCmdSplit(f, rmFn),\n\n\t\tout.NewCmdOut(f),\n\n\t\tdel.NewCmdDelete(f),\n\n\t\tshow.NewCmdShow(f),\n\t\treport.NewCmdReport(f),\n\t)\n\n\tcmds = append(cmds, invoiced.NewCmdInvoiced(f)...)\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/util/create.go",
    "content": "package util\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/api\"\n)\n\n// FillMissingBillableFn returns a step that derives the billable flag when\n// the user did not explicitly set it, checking the task first, then the project.\nfunc FillMissingBillableFn(c api.Client) Step {\n\treturn func(dto TimeEntryDTO) (TimeEntryDTO, error) {\n\t\tif dto.Billable != nil || dto.ProjectID == \"\" {\n\t\t\treturn dto, nil\n\t\t}\n\n\t\tif dto.TaskID != \"\" {\n\t\t\tt, err := c.GetTask(api.GetTaskParam{\n\t\t\t\tWorkspace: dto.Workspace,\n\t\t\t\tProjectID: dto.ProjectID,\n\t\t\t\tTaskID:    dto.TaskID,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn dto, err\n\t\t\t}\n\t\t\tb := t.Billable\n\t\t\tdto.Billable = &b\n\t\t\treturn dto, nil\n\t\t}\n\n\t\tp, err := c.GetProject(api.GetProjectParam{\n\t\t\tWorkspace: dto.Workspace,\n\t\t\tProjectID: dto.ProjectID,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn dto, err\n\t\t}\n\t\tb := p.Billable\n\t\tdto.Billable = &b\n\t\treturn dto, nil\n\t}\n}\n\n// CreateTimeEntryFn will create a time entry\nfunc CreateTimeEntryFn(c api.Client) Step {\n\treturn func(dto TimeEntryDTO) (TimeEntryDTO, error) {\n\t\tte, err := c.CreateTimeEntry(api.CreateTimeEntryParam{\n\t\t\tWorkspace:   dto.Workspace,\n\t\t\tBillable:    dto.Billable,\n\t\t\tStart:       dto.Start,\n\t\t\tEnd:         dto.End,\n\t\t\tProjectID:   dto.ProjectID,\n\t\t\tDescription: dto.Description,\n\t\t\tTagIDs:      dto.TagIDs,\n\t\t\tTaskID:      dto.TaskID,\n\t\t})\n\n\t\tif err != nil {\n\t\t\treturn dto, err\n\t\t}\n\n\t\treturn TimeEntryImplToDTO(te), nil\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/util/description-completer.go",
    "content": "package util\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// DescriptionSuggestFn provides suggestions to user when setting the description of a\n// time entry\ntype DescriptionSuggestFn func(string) []string\n\n// descriptionCompleter looks for similar descriptions for auto-compliance\ntype descriptionCompleter struct {\n\tclient       api.Client\n\tloaded       bool\n\tparam        api.GetUserTimeEntriesParam\n\tdescriptions []string\n}\n\n// NewDescriptionCompleter create or not a descriptionCompleter based on params\nfunc NewDescriptionCompleter(f cmdutil.Factory) DescriptionSuggestFn {\n\tif !f.Config().GetBool(cmdutil.CONF_DESCR_AUTOCOMP) {\n\t\treturn func(s string) []string { return []string{} }\n\t}\n\n\tworkspaceID, err := f.GetWorkspaceID()\n\tif err != nil {\n\t\treturn func(s string) []string { return []string{} }\n\t}\n\n\tuserID, err := f.GetUserID()\n\tif err != nil {\n\t\treturn func(s string) []string { return []string{} }\n\t}\n\n\tc, err := f.Client()\n\tif err != nil {\n\t\treturn func(s string) []string { return []string{} }\n\t}\n\n\tend := time.Now().UTC()\n\tstart := end.Add(time.Hour *\n\t\ttime.Duration(-24*f.Config().GetInt(cmdutil.CONF_DESCR_AUTOCOMP_DAYS)))\n\n\td := &descriptionCompleter{\n\t\tclient: c,\n\t\tparam: api.GetUserTimeEntriesParam{\n\t\t\tWorkspace: workspaceID,\n\t\t\tUserID:    userID,\n\t\t\tEnd:       &end,\n\t\t\tStart:     &start,\n\t\t},\n\t}\n\n\treturn d.suggestFn\n}\n\n// getDescriptions load descriptions from recent time entries and list than\n// unique ones\nfunc (dc *descriptionCompleter) getDescriptions() []string {\n\tif dc.loaded {\n\t\treturn dc.descriptions\n\t}\n\n\ttes, err := dc.client.GetUserTimeEntries(dc.param)\n\n\tdc.loaded = true\n\tif err != nil {\n\t\treturn dc.descriptions\n\t}\n\n\tvar ss []string\n\n\tfor _, t := range tes {\n\t\tss = append(ss, t.Description)\n\t}\n\n\tdc.descriptions = strhlp.Unique(ss)\n\treturn dc.descriptions\n}\n\n// suggestFn returns a list of suggested descriptions based on a input string\nfunc (dc *descriptionCompleter) suggestFn(toComplete string) []string {\n\ttoComplete = strings.TrimSpace(toComplete)\n\tif toComplete == \"\" {\n\t\treturn dc.getDescriptions()\n\t}\n\n\ttoComplete = strhlp.Normalize(toComplete)\n\treturn strhlp.Filter(\n\t\tfunc(s string) bool {\n\t\t\treturn strings.Contains(strhlp.Normalize(s), toComplete)\n\t\t},\n\t\tdc.getDescriptions(),\n\t)\n}\n\nfunc newDescriptionAutoComplete(f cmdutil.Factory) cmdcompl.SuggestFn {\n\treturn func(\n\t\t_ *cobra.Command, _ []string, toComplete string,\n\t) (cmdcompl.ValidArgs, error) {\n\t\tif !f.Config().GetBool(cmdutil.CONF_DESCR_AUTOCOMP) {\n\t\t\treturn cmdcompl.EmptyValidArgs(), nil\n\t\t}\n\n\t\tdc := NewDescriptionCompleter(f)\n\t\treturn cmdcompl.ValidArgsSlide(dc(toComplete)), nil\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/util/fill-with-flags.go",
    "content": "package util\n\nimport (\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n)\n\ntype flagSet interface {\n\tChanged(string) bool\n\tGetString(string) (string, error)\n\tGetStringSlice(string) ([]string, error)\n}\n\n// FillTimeEntryWithFlags will read the flags and fill the time entry with they\nfunc FillTimeEntryWithFlags(flags flagSet) Step {\n\treturn func(dto TimeEntryDTO) (TimeEntryDTO, error) {\n\t\tif err := cmdutil.XorFlag(map[string]bool{\n\t\t\t\"billable\":     flags.Changed(\"billable\"),\n\t\t\t\"not-billable\": flags.Changed(\"not-billable\"),\n\t\t}); err != nil {\n\t\t\treturn dto, err\n\t\t}\n\n\t\tif flags.Changed(\"project\") {\n\t\t\tp, _ := flags.GetString(\"project\")\n\t\t\tif p != dto.ProjectID {\n\t\t\t\tdto.TaskID = \"\"\n\t\t\t}\n\t\t\tdto.ProjectID = p\n\n\t\t\tif flags.Changed(\"client\") {\n\t\t\t\tc, _ := flags.GetString(\"client\")\n\t\t\t\tif c != dto.Client {\n\t\t\t\t\tdto.TaskID = \"\"\n\t\t\t\t}\n\t\t\t\tdto.Client = c\n\t\t\t}\n\t\t}\n\n\t\tif flags.Changed(\"description\") {\n\t\t\tdto.Description, _ = flags.GetString(\"description\")\n\t\t}\n\n\t\tif flags.Changed(\"task\") {\n\t\t\tdto.TaskID, _ = flags.GetString(\"task\")\n\t\t}\n\n\t\tif flags.Changed(\"tag\") {\n\t\t\tdto.TagIDs, _ = flags.GetStringSlice(\"tag\")\n\t\t}\n\n\t\tif flags.Changed(\"tags\") {\n\t\t\tdto.TagIDs, _ = flags.GetStringSlice(\"tags\")\n\t\t}\n\n\t\tif flags.Changed(\"billable\") {\n\t\t\tb := true\n\t\t\tdto.Billable = &b\n\t\t}\n\n\t\tif flags.Changed(\"not-billable\") {\n\t\t\tb := false\n\t\t\tdto.Billable = &b\n\t\t}\n\n\t\tvar err error\n\t\tif flags.Changed(\"when\") {\n\t\t\twhenString, _ := flags.GetString(\"when\")\n\t\t\tvar v time.Time\n\t\t\tif v, err = timehlp.ConvertToTime(whenString); err != nil {\n\t\t\t\treturn dto, err\n\t\t\t}\n\t\t\tdto.Start = v\n\t\t}\n\n\t\tif flags.Changed(\"when-to-close\") {\n\t\t\twhenString, _ := flags.GetString(\"when-to-close\")\n\t\t\tvar v time.Time\n\t\t\tif v, err = timehlp.ConvertToTime(whenString); err != nil {\n\t\t\t\treturn dto, err\n\t\t\t}\n\t\t\tdto.End = &v\n\t\t}\n\n\t\treturn dto, nil\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/util/flags.go",
    "content": "package util\n\nimport (\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// AddTimeEntryFlags will add the common flags needed to add/edit a time entry\nfunc AddTimeEntryFlags(\n\tcmd *cobra.Command, f cmdutil.Factory, of *OutputFlags,\n) {\n\tcmd.Flags().BoolP(\"billable\", \"b\", false,\n\t\t\"this time entry is billable\")\n\tcmd.Flags().BoolP(\"not-billable\", \"n\", false,\n\t\t\"this time entry is not billable\")\n\tcmd.Flags().String(\"task\", \"\", \"add a task to the entry\")\n\t_ = cmdcompl.AddSuggestionsToFlag(cmd, \"task\",\n\t\tcmdcomplutil.NewTaskAutoComplete(f, true))\n\n\tcmd.Flags().StringSliceP(\"tag\", \"T\", []string{}, \"add tags to the entry (can be used multiple times)\")\n\t_ = cmdcompl.AddSuggestionsToFlag(cmd, \"tag\",\n\t\tcmdcomplutil.NewTagAutoComplete(f))\n\n\tcmd.Flags().BoolP(\"allow-incomplete\", \"A\", false,\n\t\t\"allow creation of incomplete time entries to be edited later\")\n\n\tcmd.Flags().StringP(\"client\", \"c\", \"\", \"client of the project to use for time entry\")\n\t_ = cmdcompl.AddSuggestionsToFlag(cmd, \"client\",\n\t\tcmdcomplutil.NewClientAutoComplete(f))\n\n\tcmd.Flags().StringP(\"project\", \"p\", \"\", \"project to use for time entry\")\n\t_ = cmdcompl.AddSuggestionsToFlag(cmd, \"project\",\n\t\tcmdcomplutil.NewProjectAutoComplete(f, f.Config()))\n\n\tcmd.Flags().StringP(\"description\", \"d\", \"\", \"time entry description\")\n\t_ = cmdcompl.AddSuggestionsToFlag(cmd, \"description\",\n\t\tnewDescriptionAutoComplete(f),\n\t)\n\n\tAddPrintTimeEntriesFlags(cmd, of)\n\n\t// deprecations\n\tcmd.Flags().StringSlice(\"tags\", []string{}, \"add tags to the entry\")\n\t_ = cmdcompl.AddSuggestionsToFlag(cmd, \"tags\",\n\t\tcmdcomplutil.NewTagAutoComplete(f))\n\t_ = cmd.Flags().MarkDeprecated(\"tags\", \"use tag instead\")\n}\n\n// AddTimeEntryDateFlags adds the default start and end flags\nfunc AddTimeEntryDateFlags(cmd *cobra.Command) {\n\tcmd.Flags().StringP(\"when\", \"s\", time.Now().Format(timehlp.FullTimeFormat),\n\t\t\"when the entry should be started, \"+\n\t\t\t\"if not informed will use current time\")\n\tcmd.Flags().StringP(\"when-to-close\", \"e\", \"\",\n\t\t\"when the entry should be closed, if not informed will let it open \"+\n\t\t\t\"(same formats as when)\")\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/util/help.go",
    "content": "package util\n\nimport \"github.com/lucassabreu/clockify-cli/pkg/timeentryhlp\"\n\nconst (\n\tHelpTimeEntryNowIfNotSet = \"If no start time (`--when`) is set then the \" +\n\t\t\"current time will be used.\\n\"\n\n\tHelpInteractiveByDefault = \"By default, the CLI will ask the \" +\n\t\t\"information interactively; use `--interactive=0` to disable it.\\n\" +\n\t\t\"\\n\" +\n\t\t\"If you prefer that it never don't do that by default, \" +\n\t\t\"run the bellow command, and use `--interactive` when you want \" +\n\t\t\"to be asked:\\n\" +\n\t\t\"```\\n\" +\n\t\t\"$ clockify-cli config set interactive false\\n\" +\n\t\t\"```\\n\"\n\n\tHelpDateTimeFormats = \"\" +\n\t\t` - Full Date and Time:                \"2016-02-01 15:04:05\"` + \"\\n\" +\n\t\t` - Date and Time (assumes 0 seconds): \"2016-02-01 15:04\"` + \"\\n\" +\n\t\t` - Yesterday with Time:               \"yesterday 15:04:05\"` + \"\\n\" +\n\t\t` - Yesterday with Time (0 seconds):   \"yesterday 15:04\"` + \"\\n\" +\n\t\t` - Today at Time:                     \"15:04:05\"` + \"\\n\" +\n\t\t` - Today at Time (assumes 0 seconds): \"15:04\"` + \"\\n\" +\n\t\t` - 10mins in the future:              +10m` + \"\\n\" +\n\t\t` - 1min and 30s ago:                  -90s` + \"\\n\" +\n\t\t` - 1hour and 10min ago:               -1:10s` + \"\\n\" +\n\t\t` - 1day, 10min and 30s ago:           -1d10m30s` + \"\\n\"\n\n\tHelpTimeInputOnTimeEntry = \"When setting a date/time input \" +\n\t\t\"(`--when` and `--when-to-close`) you can use any of the following \" +\n\t\t\"formats to set then:\\n\" +\n\t\tHelpDateTimeFormats\n\n\tHelpNamesForIds = \"To be able to use names of resources instead of its \" +\n\t\t\"IDs you must enable the feature 'allow-name-for-id', to do that \" +\n\t\t\"run the command (the commands may take longer to look for the \" +\n\t\t\"resource id):\\n\" +\n\t\t\"```\\n\" +\n\t\t\"$ clockify-cli config set allow-name-for-id true\\n\" +\n\t\t\"```\\n\\n\"\n\n\tHelpValidateIncomplete = \"By default, the CLI (and Clockify API) only \" +\n\t\t\"validates if the workspace and project rules are respected when a \" +\n\t\t\"time entry is stopped, if you prefer to validate when \" +\n\t\t\"starting/inserting it run the following command:\\n\" +\n\t\t\"```\\n\" +\n\t\t\"$ clockify-cli config set allow-incomplete false\\n\" +\n\t\t\"```\\n\\n\"\n\n\tHelpMoreInfoAboutStarting = \"Use `clockify-cli in --help` for more \" +\n\t\t\"information about creating new time entries.\"\n\n\tHelpMoreInfoAboutPrinting = \"Use `clockify-cli report --help` for more \" +\n\t\t\"information about printing time entries.\"\n\n\tHelpTimeEntriesAliasForEdit = \"\" +\n\t\t`If you want to edit the current (running) time entry you can ` +\n\t\t`use \"` + timeentryhlp.AliasCurrent + `\" instead of its ID.` + \"\\n\" +\n\t\t`To edit the last ended time entry you can use \"` +\n\t\ttimeentryhlp.AliasLast + `\" for it, for the one before that you ` +\n\t\t`can use \"^2\", for the previous \"^3\" and so on.` + \"\\n\"\n)\n"
  },
  {
    "path": "pkg/cmd/time-entry/util/interactive.go",
    "content": "package util\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode/utf8\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/ui\"\n)\n\n// GetDatesInteractiveFn will ask the user the start and end times of the entry\nfunc GetDatesInteractiveFn(f cmdutil.Factory) Step {\n\tif !f.Config().IsInteractive() {\n\t\treturn skip\n\t}\n\n\treturn func(t TimeEntryDTO) (TimeEntryDTO, error) {\n\t\treturn askTimeEntryDatesInteractive(f.UI(), t)\n\t}\n}\n\nfunc askTimeEntryDatesInteractive(\n\tui ui.UI,\n\tdto TimeEntryDTO,\n) (TimeEntryDTO, error) {\n\tvar err error\n\tdateString := dto.Start.In(time.Local).\n\t\tFormat(timehlp.FullTimeFormat)\n\tif dto.Start, err = ui.AskForDateTime(\n\t\t\"Start\", dateString, timehlp.ConvertToTime); err != nil {\n\t\treturn dto, err\n\t}\n\n\tdateString = \"\"\n\tif dto.End != nil {\n\t\tdateString = dto.End.In(time.Local).\n\t\t\tFormat(timehlp.FullTimeFormat)\n\t}\n\n\tif dto.End, err = ui.AskForDateTimeOrNil(\n\t\t\"End\", dateString, timehlp.ConvertToTime); err != nil {\n\t\treturn dto, err\n\t}\n\n\treturn dto, nil\n}\n\n// GetPropsInteractiveFn will return a callback that asks the user\n// interactively about the properties of the time entry, only if the parameter\n// cmdutil.CONF_INTERACTIVE is active\nfunc GetPropsInteractiveFn(\n\tdc DescriptionSuggestFn,\n\tf cmdutil.Factory,\n) Step {\n\tif !f.Config().IsInteractive() {\n\t\treturn skip\n\t}\n\n\treturn func(tei TimeEntryDTO) (TimeEntryDTO, error) {\n\t\tc, err := f.Client()\n\t\tif err != nil {\n\t\t\treturn tei, err\n\t\t}\n\n\t\treturn askTimeEntryPropsInteractive(\n\t\t\ttei,\n\t\t\tc,\n\t\t\tf.UI(),\n\t\t\tdc,\n\t\t\tf.Config().GetBool(cmdutil.CONF_ALLOW_ARCHIVED_TAGS),\n\t\t)\n\t}\n}\n\nfunc askTimeEntryPropsInteractive(\n\tte TimeEntryDTO,\n\tc api.Client,\n\tui ui.UI,\n\tdc DescriptionSuggestFn,\n\tallowArchived bool,\n) (TimeEntryDTO, error) {\n\tvar err error\n\tw, err := c.GetWorkspace(api.GetWorkspace{ID: te.Workspace})\n\tif err != nil {\n\t\treturn te, err\n\t}\n\n\tte.ProjectID, err = getProjectID(te.ProjectID, w, c, ui)\n\tif err != nil {\n\t\treturn te, err\n\t}\n\n\tif te.ProjectID != \"\" {\n\t\tte.TaskID, err = getTaskID(te.TaskID, te.ProjectID, w, c, ui)\n\t\tif err != nil {\n\t\t\treturn te, err\n\t\t}\n\t}\n\n\tte.Description = getDescription(te.Description, dc, ui,\n\t\tw.Settings.ForceDescription)\n\n\tte.TagIDs, err = getTagIDs(te.TagIDs, w, c, allowArchived, ui)\n\n\treturn te, err\n}\n\nconst noProject = \"No Project\"\n\nfunc getProjectID(\n\tprojectID string, w dto.Workspace, c api.Client, ui ui.UI,\n) (string, error) {\n\tb := false\n\tprojects, err := c.GetProjects(api.GetProjectsParam{\n\t\tWorkspace:       w.ID,\n\t\tArchived:        &b,\n\t\tPaginationParam: api.AllPages(),\n\t})\n\n\tif err != nil || len(projects) == 0 {\n\t\treturn \"\", err\n\t}\n\n\tprojectsString := make([]string, len(projects))\n\tfound := -1\n\tprojectNameSize := 0\n\n\tfor i := range projects {\n\t\tprojectsString[i] = projects[i].ID + \" - \" + projects[i].Name\n\t\tif c := utf8.RuneCountInString(projectsString[i]); projectNameSize < c {\n\t\t\tprojectNameSize = c\n\t\t}\n\n\t\tif found == -1 && projects[i].ID == projectID {\n\t\t\tprojectID = projectsString[i]\n\t\t\tfound = i\n\t\t}\n\t}\n\n\tformat := fmt.Sprintf(\"%%-%ds| %%s\", projectNameSize+1)\n\n\tfor i := range projects {\n\t\tclient := \"Without Client\"\n\t\tif projects[i].ClientID != \"\" {\n\t\t\tclient = \"Client: \" + projects[i].ClientName +\n\t\t\t\t\" (\" + projects[i].ClientID + \")\"\n\t\t}\n\n\t\tprojectsString[i] = fmt.Sprintf(\n\t\t\tformat,\n\t\t\tprojectsString[i],\n\t\t\tclient,\n\t\t)\n\t}\n\n\tif found == -1 {\n\t\tif projectID != \"\" {\n\t\t\tfmt.Printf(\"Project '%s' informed was not found.\\n\", projectID)\n\t\t\tprojectID = \"\"\n\t\t}\n\t} else {\n\t\tprojectID = projectsString[found]\n\t}\n\n\tif !w.Settings.ForceProjects {\n\t\tprojectsString = append([]string{noProject}, projectsString...)\n\t}\n\n\tprojectID, err = ui.AskFromOptions(\"Choose your project:\",\n\t\tprojectsString, projectID)\n\tif err != nil || projectID == noProject || projectID == \"\" {\n\t\treturn \"\", err\n\t}\n\n\treturn strings.TrimSpace(projectID[0:strings.Index(projectID, \" - \")]), nil\n}\n\nconst noTask = \"No Task\"\n\nfunc getTaskID(\n\ttaskID, projectID string, w dto.Workspace, c api.Client, ui ui.UI,\n) (string, error) {\n\ttasks, err := c.GetTasks(api.GetTasksParam{\n\t\tWorkspace:       w.ID,\n\t\tProjectID:       projectID,\n\t\tPaginationParam: api.AllPages(),\n\t\tActive:          true,\n\t})\n\n\t// todo: this is a workaround for the cli, the api needs to be fixed\n\tvar httpErr dto.Error\n\tif errors.As(err, &httpErr) && httpErr.Code == 501 && strings.Contains(\n\t\thttpErr.Message,\n\t\t\"doesn't belong to PROJECT with id \"+projectID,\n\t) {\n\t\treturn \"\", nil\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(tasks) == 0 {\n\t\treturn \"\", nil\n\t}\n\n\ttasksString := make([]string, len(tasks))\n\tfound := -1\n\n\tfor i := range tasks {\n\t\ttasksString[i] = tasks[i].ID + \" - \" + tasks[i].Name\n\n\t\tif found == -1 && tasks[i].ID == taskID {\n\t\t\ttaskID = tasksString[i]\n\t\t\tfound = i\n\t\t}\n\t}\n\n\tif found == -1 {\n\t\tif taskID != \"\" {\n\t\t\tfmt.Printf(\"Task '%s' informed was not found.\\n\", taskID)\n\t\t\ttaskID = \"\"\n\t\t}\n\t} else {\n\t\ttaskID = tasksString[found]\n\t}\n\n\tif !w.Settings.ForceTasks {\n\t\ttasksString = append([]string{noTask}, tasksString...)\n\t}\n\n\ttaskID, err = ui.AskFromOptions(\"Choose your task:\", tasksString, taskID)\n\tif err != nil || taskID == noTask || taskID == \"\" {\n\t\treturn \"\", err\n\t}\n\n\treturn strings.TrimSpace(taskID[0:strings.Index(taskID, \" - \")]), nil\n}\n\nfunc getDescription(\n\tdescription string,\n\tdc DescriptionSuggestFn,\n\ti ui.UI,\n\tforce bool,\n) string {\n\tvar v func(string) error\n\tif force {\n\t\tv = func(s string) error {\n\t\t\tif s == \"\" {\n\t\t\t\treturn errors.New(\"description should be informed\")\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tdescription, _ = i.AskForValidText(\n\t\t\"Description:\",\n\t\tv,\n\t\tui.WithDefault(description),\n\t\tui.WithSuggestion(dc),\n\t)\n\treturn description\n}\n\nfunc getTagIDs(\n\ttagIDs []string, w dto.Workspace, c api.Client, allowArchived bool,\n\tui ui.UI,\n) ([]string, error) {\n\tvar archived *bool\n\tif !allowArchived {\n\t\tf := false\n\t\tarchived = &f\n\t}\n\ttags, err := c.GetTags(api.GetTagsParam{\n\t\tWorkspace: w.ID,\n\t\tArchived:  archived,\n\t})\n\n\tif err != nil || len(tags) == 0 {\n\t\treturn nil, err\n\t}\n\n\ttagsString := make([]string, len(tags))\n\tfor i, u := range tags {\n\t\ttagsString[i] = fmt.Sprintf(\"%s - %s\", u.ID, u.Name)\n\t}\n\n\tcurrent := make([]string, len(tagIDs))\n\tfor i, t := range tagIDs {\n\t\tfor _, s := range tagsString {\n\t\t\tif strings.HasPrefix(s, t) {\n\t\t\t\tcurrent[i] = s\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tvar newTags []string\n\tif newTags, err = ui.AskManyFromOptions(\"Choose your tags:\",\n\t\ttagsString, current, func(s []string) error {\n\t\t\tif w.Settings.ForceTags && len(s) == 0 {\n\t\t\t\treturn errors.New(\"at least one tag should be selected\")\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}); err != nil {\n\t\treturn nil, nil\n\t}\n\n\tfor i, t := range newTags {\n\t\tnewTags[i] = strings.TrimSpace(t[0:strings.Index(t, \" - \")])\n\t}\n\n\treturn newTags, nil\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/util/interactive_test.go",
    "content": "package util\n\nimport (\n\t\"testing\"\n\n\t\"github.com/AlecAivazis/survey/v2/terminal\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/consoletest\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/ui\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n)\n\nfunc TestGetPropsInteractive_ShouldSkip_WhenDisabled(t *testing.T) {\n\tf := mocks.NewMockFactory(t)\n\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\tInteractive: false,\n\t})\n\ts := GetPropsInteractiveFn(nil, f)\n\n\tte := TimeEntryDTO{}\n\tte2, err := s(te)\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, te, te2)\n}\n\nfunc TestGetPropsInteractive_ShouldAskValues(t *testing.T) {\n\tconsoletest.RunTestConsole(t,\n\t\tfunc(out consoletest.FileWriter, in consoletest.FileReader) error {\n\t\t\tc := mocks.NewMockClient(t)\n\n\t\t\tc.EXPECT().GetWorkspace(mock.Anything).\n\t\t\t\tReturn(dto.Workspace{ID: \"w\"}, nil)\n\n\t\t\tc.EXPECT().GetProjects(mock.Anything).\n\t\t\t\tReturn(\n\t\t\t\t\t[]dto.Project{\n\t\t\t\t\t\t{ID: \"1\", Name: \"First\"},\n\t\t\t\t\t\t{ID: \"2\", Name: \"Second\",\n\t\t\t\t\t\t\tClientID: \"1\", ClientName: \"Client One\"},\n\t\t\t\t\t\t{ID: \"3\", Name: \"Third\",\n\t\t\t\t\t\t\tClientID: \"2\", ClientName: \"Client Two\"},\n\t\t\t\t\t\t{ID: \"4\", Name: \"Fourth\"},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\tc.EXPECT().GetTasks(mock.Anything).\n\t\t\t\tReturn(\n\t\t\t\t\t[]dto.Task{\n\t\t\t\t\t\t{ID: \"t1\", Name: \"First\"},\n\t\t\t\t\t\t{ID: \"t2\", Name: \"Second\"},\n\t\t\t\t\t\t{ID: \"t3\", Name: \"Third\"},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\tc.EXPECT().GetTags(mock.Anything).\n\t\t\t\tReturn(\n\t\t\t\t\t[]dto.Tag{\n\t\t\t\t\t\t{ID: \"tag1\", Name: \"meeting\"},\n\t\t\t\t\t\t{ID: \"tag2\", Name: \"backend\"},\n\t\t\t\t\t\t{ID: \"tag3\", Name: \"frontend\"},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tf.EXPECT().UI().Return(ui.NewUI(in, out, out))\n\t\t\tf.EXPECT().Client().Return(c, nil)\n\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{Interactive: true})\n\n\t\t\tte, err := GetPropsInteractiveFn(\n\t\t\t\tfunc(string) []string { return []string{} },\n\t\t\t\tf,\n\t\t\t)(TimeEntryDTO{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t})\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t,\n\t\t\t\tTimeEntryDTO{\n\t\t\t\t\tWorkspace:   \"w\",\n\t\t\t\t\tProjectID:   \"3\",\n\t\t\t\t\tTaskID:      \"t2\",\n\t\t\t\t\tDescription: \"a unique description\",\n\t\t\t\t\tTagIDs:      []string{\"tag2\", \"tag3\"},\n\t\t\t\t},\n\t\t\t\tte,\n\t\t\t)\n\n\t\t\treturn err\n\t\t}, func(c consoletest.ExpectConsole) {\n\t\t\tc.ExpectString(\"Choose your project:\")\n\t\t\tc.ExpectString(noProject)\n\t\t\tc.ExpectString(\"1 - First  | Without Client\")\n\t\t\tc.ExpectString(\"2 - Second | Client: Client One (1)\")\n\t\t\tc.ExpectString(\"3 - Third  | Client: Client Two (2)\")\n\t\t\tc.ExpectString(\"4 - Fourth | Without Client\")\n\n\t\t\tc.Send(\"ir\")\n\t\t\tc.ExpectString(\"1 - First  | Without Client\")\n\t\t\tc.ExpectString(\"3 - Third  | Client: Client Two (2)\")\n\n\t\t\tc.Send(string(terminal.KeyArrowDown))\n\t\t\tc.SendLine(\"\")\n\n\t\t\tc.ExpectString(\"Choose your task:\")\n\t\t\tc.ExpectString(noTask)\n\t\t\tc.ExpectString(\"t1 - First\")\n\t\t\tc.ExpectString(\"t2 - Second\")\n\t\t\tc.ExpectString(\"t3 - Third\")\n\n\t\t\tc.SendLine(\"2\")\n\n\t\t\tc.ExpectString(\"Description:\")\n\t\t\tc.SendLine(\"a unique description\")\n\n\t\t\tc.ExpectString(\"Choose your tags:\")\n\n\t\t\tc.ExpectString(\"tag1 - meeting\")\n\t\t\tc.ExpectString(\"tag2 - backend\")\n\t\t\tc.ExpectString(\"tag3 - frontend\")\n\n\t\t\tc.Send(\"end\")\n\t\t\tc.Send(string(terminal.KeyArrowRight))\n\t\t\tc.SendLine(\"\")\n\n\t\t\tc.ExpectEOF()\n\t\t})\n}\n\nfunc TestGetPropsInteractive_ShouldAllowEmptyValues(t *testing.T) {\n\tconsoletest.RunTestConsole(t,\n\t\tfunc(out consoletest.FileWriter, in consoletest.FileReader) error {\n\t\t\tc := mocks.NewMockClient(t)\n\n\t\t\tc.EXPECT().GetWorkspace(mock.Anything).\n\t\t\t\tReturn(dto.Workspace{ID: \"w\"}, nil)\n\n\t\t\tc.EXPECT().GetProjects(mock.Anything).\n\t\t\t\tReturn(\n\t\t\t\t\t[]dto.Project{\n\t\t\t\t\t\t{ID: \"1\", Name: \"First\"},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\tc.EXPECT().GetTags(mock.Anything).\n\t\t\t\tReturn(\n\t\t\t\t\t[]dto.Tag{\n\t\t\t\t\t\t{ID: \"tag1\", Name: \"meeting\"},\n\t\t\t\t\t\t{ID: \"tag2\", Name: \"backend\"},\n\t\t\t\t\t\t{ID: \"tag3\", Name: \"frontend\"},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tf.EXPECT().UI().Return(ui.NewUI(in, out, out))\n\t\t\tf.EXPECT().Client().Return(c, nil)\n\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{Interactive: true})\n\n\t\t\tte, err := GetPropsInteractiveFn(\n\t\t\t\tfunc(string) []string { return []string{} },\n\t\t\t\tf,\n\t\t\t)(TimeEntryDTO{\n\t\t\t\tWorkspace: \"w\",\n\t\t\t})\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t,\n\t\t\t\tTimeEntryDTO{\n\t\t\t\t\tWorkspace:   \"w\",\n\t\t\t\t\tProjectID:   \"\",\n\t\t\t\t\tTaskID:      \"\",\n\t\t\t\t\tDescription: \"\",\n\t\t\t\t\tTagIDs:      nil,\n\t\t\t\t},\n\t\t\t\tte,\n\t\t\t)\n\n\t\t\treturn err\n\t\t}, func(c consoletest.ExpectConsole) {\n\t\t\tc.ExpectString(\"Choose your project:\")\n\t\t\tc.ExpectString(noProject)\n\t\t\tc.ExpectString(\"1 - First | Without Client\")\n\n\t\t\tc.SendLine(\"\")\n\n\t\t\tc.ExpectString(\"Description:\")\n\t\t\tc.SendLine(\"\")\n\n\t\t\tc.ExpectString(\"Choose your tags:\")\n\n\t\t\tc.ExpectString(\"tag1 - meeting\")\n\t\t\tc.ExpectString(\"tag2 - backend\")\n\t\t\tc.ExpectString(\"tag3 - frontend\")\n\n\t\t\tc.Send(\"end\")\n\t\t\tc.SendLine(\"\")\n\n\t\t\tc.ExpectEOF()\n\t\t})\n}\n\nfunc TestGetPropsInteractive_ShouldUseInputAsSelected(t *testing.T) {\n\tconsoletest.RunTestConsole(t,\n\t\tfunc(out consoletest.FileWriter, in consoletest.FileReader) error {\n\t\t\tc := mocks.NewMockClient(t)\n\n\t\t\tc.EXPECT().GetWorkspace(mock.Anything).\n\t\t\t\tReturn(dto.Workspace{ID: \"w\"}, nil)\n\n\t\t\tc.EXPECT().GetProjects(mock.Anything).\n\t\t\t\tReturn(\n\t\t\t\t\t[]dto.Project{\n\t\t\t\t\t\t{ID: \"1\", Name: \"First\"},\n\t\t\t\t\t\t{ID: \"2\", Name: \"Second\",\n\t\t\t\t\t\t\tClientID: \"1\", ClientName: \"Client One\"},\n\t\t\t\t\t\t{ID: \"3\", Name: \"Third\",\n\t\t\t\t\t\t\tClientID: \"2\", ClientName: \"Client Two\"},\n\t\t\t\t\t\t{ID: \"4\", Name: \"Fourth\"},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\tc.EXPECT().GetTasks(mock.Anything).\n\t\t\t\tReturn(\n\t\t\t\t\t[]dto.Task{\n\t\t\t\t\t\t{ID: \"t1\", Name: \"First\"},\n\t\t\t\t\t\t{ID: \"t2\", Name: \"Second\"},\n\t\t\t\t\t\t{ID: \"t3\", Name: \"Third\"},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\tc.EXPECT().GetTags(mock.Anything).\n\t\t\t\tReturn(\n\t\t\t\t\t[]dto.Tag{\n\t\t\t\t\t\t{ID: \"tag1\", Name: \"meeting\"},\n\t\t\t\t\t\t{ID: \"tag2\", Name: \"backend\"},\n\t\t\t\t\t\t{ID: \"tag3\", Name: \"frontend\"},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\tinput := TimeEntryDTO{\n\t\t\t\tWorkspace:   \"w\",\n\t\t\t\tProjectID:   \"3\",\n\t\t\t\tTaskID:      \"t2\",\n\t\t\t\tDescription: \"a unique description\",\n\t\t\t\tTagIDs:      []string{\"tag2\", \"tag3\"},\n\t\t\t}\n\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tf.EXPECT().UI().Return(ui.NewUI(in, out, out))\n\t\t\tf.EXPECT().Client().Return(c, nil)\n\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{Interactive: true})\n\n\t\t\toutput, err := GetPropsInteractiveFn(\n\t\t\t\tfunc(string) []string { return []string{} },\n\t\t\t\tf,\n\t\t\t)(input)\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, input, output)\n\n\t\t\treturn err\n\t\t}, func(c consoletest.ExpectConsole) {\n\t\t\tc.ExpectString(\"Choose your project:\")\n\t\t\tc.ExpectString(\"Third\")\n\t\t\tc.SendLine(\"\")\n\n\t\t\tc.ExpectString(\"Choose your task:\")\n\t\t\tc.SendLine(\"\")\n\n\t\t\tc.ExpectString(\"Description:\")\n\t\t\tc.ExpectString(\"a unique description\")\n\t\t\tc.SendLine(\"\")\n\n\t\t\tc.ExpectString(\"Choose your tags:\")\n\t\t\tc.SendLine(\"\")\n\n\t\t\tc.ExpectEOF()\n\t\t})\n}\n\nfunc TestGetPropsInteractive_ShouldForceAnswer_WhenWorkspaceForces(\n\tt *testing.T) {\n\tconsoletest.RunTestConsole(t,\n\t\tfunc(out consoletest.FileWriter, in consoletest.FileReader) error {\n\t\t\tc := mocks.NewMockClient(t)\n\n\t\t\tc.EXPECT().GetWorkspace(mock.Anything).\n\t\t\t\tReturn(\n\t\t\t\t\tdto.Workspace{\n\t\t\t\t\t\tID: \"w\",\n\t\t\t\t\t\tSettings: dto.WorkspaceSettings{\n\t\t\t\t\t\t\tForceProjects:    true,\n\t\t\t\t\t\t\tForceTasks:       true,\n\t\t\t\t\t\t\tForceDescription: true,\n\t\t\t\t\t\t\tForceTags:        true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\tc.EXPECT().GetProjects(mock.Anything).\n\t\t\t\tReturn(\n\t\t\t\t\t[]dto.Project{\n\t\t\t\t\t\t{ID: \"1\", Name: \"First\"},\n\t\t\t\t\t\t{ID: \"2\", Name: \"Second\",\n\t\t\t\t\t\t\tClientID: \"1\", ClientName: \"Client One\"},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\tc.EXPECT().GetTasks(mock.Anything).\n\t\t\t\tReturn(\n\t\t\t\t\t[]dto.Task{\n\t\t\t\t\t\t{ID: \"t1\", Name: \"First\"},\n\t\t\t\t\t\t{ID: \"t2\", Name: \"Second\"},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\tc.EXPECT().GetTags(mock.Anything).\n\t\t\t\tReturn(\n\t\t\t\t\t[]dto.Tag{\n\t\t\t\t\t\t{ID: \"tag1\", Name: \"meeting\"},\n\t\t\t\t\t\t{ID: \"tag2\", Name: \"backend\"},\n\t\t\t\t\t\t{ID: \"tag3\", Name: \"frontend\"},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tf.EXPECT().UI().Return(ui.NewUI(in, out, out))\n\t\t\tf.EXPECT().Client().Return(c, nil)\n\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{Interactive: true})\n\n\t\t\toutput, err := GetPropsInteractiveFn(\n\t\t\t\tfunc(string) []string { return []string{} },\n\t\t\t\tf,\n\t\t\t)(TimeEntryDTO{Workspace: \"w\"})\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t,\n\t\t\t\tTimeEntryDTO{\n\t\t\t\t\tWorkspace:   \"w\",\n\t\t\t\t\tProjectID:   \"1\",\n\t\t\t\t\tTaskID:      \"t1\",\n\t\t\t\t\tDescription: \"something\",\n\t\t\t\t\tTagIDs:      []string{\"tag1\"},\n\t\t\t\t},\n\t\t\t\toutput,\n\t\t\t)\n\n\t\t\treturn err\n\t\t}, func(c consoletest.ExpectConsole) {\n\t\t\tc.ExpectString(\"Choose your project:\")\n\t\t\tc.SendLine(\"\")\n\n\t\t\tc.ExpectString(\"Choose your task:\")\n\t\t\tc.SendLine(\"\")\n\n\t\t\tc.ExpectString(\"Description:\")\n\t\t\tc.SendLine(\"\")\n\t\t\tc.ExpectString(\"description should be informed\")\n\t\t\tc.SendLine(\"something\")\n\n\t\t\tc.ExpectString(\"Choose your tags:\")\n\t\t\tc.SendLine(\"\")\n\t\t\tc.ExpectString(\"at least one tag should be selected\")\n\t\t\tc.SendLine(\" \")\n\n\t\t\tc.ExpectEOF()\n\t\t})\n}\n\nfunc TestGetPropsInteractive_ShouldNotAsk_WhenThereAreNoOptions(\n\tt *testing.T) {\n\tconsoletest.RunTestConsole(t,\n\t\tfunc(out consoletest.FileWriter, in consoletest.FileReader) error {\n\t\t\tc := mocks.NewMockClient(t)\n\n\t\t\tc.EXPECT().GetWorkspace(mock.Anything).\n\t\t\t\tReturn(dto.Workspace{ID: \"w\"}, nil)\n\n\t\t\tc.EXPECT().GetProjects(mock.Anything).\n\t\t\t\tReturn([]dto.Project{}, nil)\n\n\t\t\tc.EXPECT().GetTags(mock.Anything).\n\t\t\t\tReturn([]dto.Tag{}, nil)\n\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tf.EXPECT().UI().Return(ui.NewUI(in, out, out))\n\t\t\tf.EXPECT().Client().Return(c, nil)\n\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{Interactive: true})\n\n\t\t\toutput, err := GetPropsInteractiveFn(\n\t\t\t\tfunc(string) []string { return []string{} },\n\t\t\t\tf,\n\t\t\t)(TimeEntryDTO{Workspace: \"w\"})\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t,\n\t\t\t\tTimeEntryDTO{\n\t\t\t\t\tWorkspace:   \"w\",\n\t\t\t\t\tProjectID:   \"\",\n\t\t\t\t\tTaskID:      \"\",\n\t\t\t\t\tDescription: \"something\",\n\t\t\t\t\tTagIDs:      nil,\n\t\t\t\t},\n\t\t\t\toutput,\n\t\t\t)\n\n\t\t\treturn err\n\t\t}, func(c consoletest.ExpectConsole) {\n\t\t\tc.ExpectString(\"Description:\")\n\t\t\tc.SendLine(\"something\")\n\n\t\t\tc.ExpectEOF()\n\t\t})\n}\n\nfunc TestGetDatesInteractive_ShouldSkip_WhenDisabled(t *testing.T) {\n\tf := mocks.NewMockFactory(t)\n\tf.EXPECT().Config().Return(&mocks.SimpleConfig{Interactive: false})\n\n\ts := GetDatesInteractiveFn(f)\n\n\tte := TimeEntryDTO{}\n\tte2, err := s(te)\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, te, te2)\n}\n\nfunc TestGetDatesInteractive_ShouldValidateString_WhenWrongFormat(\n\tt *testing.T) {\n\tconsoletest.RunTestConsole(t,\n\t\tfunc(out consoletest.FileWriter, in consoletest.FileReader) error {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tf.EXPECT().UI().Return(ui.NewUI(in, out, out))\n\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{Interactive: true})\n\n\t\t\ts := GetDatesInteractiveFn(f)\n\n\t\t\tte, err := s(TimeEntryDTO{\n\t\t\t\tStart: timehlp.Now(),\n\t\t\t})\n\n\t\t\tassert.NoError(t, err)\n\t\t\tstart, _ := timehlp.ConvertToTime(timehlp.FullTimeFormat)\n\t\t\tassert.Equal(t,\n\t\t\t\tTimeEntryDTO{\n\t\t\t\t\tStart: start,\n\t\t\t\t\tEnd:   nil,\n\t\t\t\t},\n\t\t\t\tte)\n\n\t\t\treturn nil\n\t\t},\n\t\tfunc(c consoletest.ExpectConsole) {\n\t\t\twrongInputs := func() {\n\t\t\t\tfor _, v := range []string{\n\t\t\t\t\t\"wrong\",\n\t\t\t\t\t\"99:99\",\n\t\t\t\t\t\"99:99:99\",\n\t\t\t\t} {\n\t\t\t\t\tc.SendLine(v)\n\t\t\t\t\tc.ExpectString(\"Sorry, your reply was invalid\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tc.ExpectString(\"Start:\")\n\t\t\twrongInputs()\n\t\t\tc.SendLine(timehlp.FullTimeFormat)\n\n\t\t\tc.ExpectString(\"End\")\n\t\t\twrongInputs()\n\t\t\tc.SendLine(\"\")\n\n\t\t\tc.ExpectEOF()\n\t\t},\n\t)\n}\n\nfunc TestGetDatesInteractive_ShouldAccept_ValideTimeFormats(t *testing.T) {\n\t// toTimeRef := func(t time.Time) *time.Time { return &t }\n\tfromTimeString := func(s string) TimeEntryDTO {\n\t\tt, _ := timehlp.ConvertToTime(s)\n\t\treturn TimeEntryDTO{\n\t\t\tStart: t,\n\t\t\tEnd:   &t,\n\t\t}\n\t}\n\n\ttts := []struct {\n\t\ttimeString string\n\t\tinput      TimeEntryDTO\n\t\toutput     TimeEntryDTO\n\t}{\n\t\t{\n\t\t\ttimeString: timehlp.FullTimeFormat,\n\t\t\toutput:     fromTimeString(timehlp.FullTimeFormat),\n\t\t},\n\t\t{\n\t\t\ttimeString: timehlp.SimplerTimeFormat,\n\t\t\toutput:     fromTimeString(timehlp.SimplerTimeFormat),\n\t\t},\n\t\t{\n\t\t\ttimeString: timehlp.OnlyTimeFormat,\n\t\t\toutput:     fromTimeString(timehlp.OnlyTimeFormat),\n\t\t},\n\t\t{\n\t\t\ttimeString: timehlp.SimplerOnlyTimeFormat,\n\t\t\toutput:     fromTimeString(timehlp.SimplerOnlyTimeFormat),\n\t\t},\n\t\t{\n\t\t\ttimeString: timehlp.NowTimeFormat,\n\t\t\toutput:     fromTimeString(timehlp.NowTimeFormat),\n\t\t},\n\t\t{\n\t\t\ttimeString: \"\",\n\t\t\tinput:      fromTimeString(timehlp.FullTimeFormat),\n\t\t\toutput:     fromTimeString(timehlp.FullTimeFormat),\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\ttt := &tts[i]\n\n\t\tt.Run(tt.timeString, func(t *testing.T) {\n\t\t\tconsoletest.RunTestConsole(t,\n\t\t\t\tfunc(out consoletest.FileWriter, in consoletest.FileReader) error {\n\t\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\t\tf.EXPECT().UI().Return(ui.NewUI(in, out, out))\n\t\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{Interactive: true})\n\n\t\t\t\t\ts := GetDatesInteractiveFn(f)\n\n\t\t\t\t\tte, err := s(tt.input)\n\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, tt.output, te)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tfunc(c consoletest.ExpectConsole) {\n\t\t\t\t\tc.ExpectString(\"Start:\")\n\t\t\t\t\tc.SendLine(tt.timeString)\n\n\t\t\t\t\tc.ExpectString(\"End\")\n\t\t\t\t\tc.SendLine(tt.timeString)\n\n\t\t\t\t\tc.ExpectEOF()\n\t\t\t\t},\n\t\t\t)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/util/name-for-id.go",
    "content": "package util\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/search\"\n)\n\n// GetAllowNameForIDsFn will try to find project/task/tags by their names if\n// the value provided was not a ID\nfunc GetAllowNameForIDsFn(config cmdutil.Config, c api.Client) Step {\n\tif !config.GetBool(cmdutil.CONF_ALLOW_NAME_FOR_ID) {\n\t\treturn skip\n\t}\n\n\tcbs := []Step{\n\t\tlookupProject(c, config),\n\t\tlookupTask(c),\n\t\tlookupTags(c),\n\t}\n\n\tif config.IsInteractive() {\n\t\tcbs = disableErrorReporting(cbs)\n\t}\n\n\treturn compose(cbs...)\n}\n\nfunc lookupProject(c api.Client, cnf cmdutil.Config) Step {\n\treturn func(te TimeEntryDTO) (TimeEntryDTO, error) {\n\t\tif te.ProjectID == \"\" {\n\t\t\treturn te, nil\n\t\t}\n\n\t\tvar err error\n\t\tte.ProjectID, err = search.GetProjectByName(\n\t\t\tc, cnf, te.Workspace, te.ProjectID, te.Client)\n\t\treturn te, err\n\t}\n\n}\n\nfunc lookupTask(c api.Client) Step {\n\treturn func(te TimeEntryDTO) (TimeEntryDTO, error) {\n\t\tif te.TaskID == \"\" {\n\t\t\treturn te, nil\n\t\t}\n\n\t\tvar err error\n\t\tte.TaskID, err = search.GetTaskByName(\n\t\t\tc,\n\t\t\tapi.GetTasksParam{\n\t\t\t\tWorkspace: te.Workspace,\n\t\t\t\tProjectID: te.ProjectID,\n\t\t\t\tActive:    true,\n\t\t\t},\n\t\t\tte.TaskID)\n\t\treturn te, err\n\t}\n}\n\nfunc lookupTags(c api.Client) Step {\n\treturn func(te TimeEntryDTO) (TimeEntryDTO, error) {\n\t\tif len(te.TagIDs) == 0 {\n\t\t\treturn te, nil\n\t\t}\n\n\t\tvar err error\n\t\tte.TagIDs, err = search.GetTagsByName(c, te.Workspace, te.TagIDs)\n\t\treturn te, err\n\t}\n\n}\n\nfunc disableErrorReporting(cbs []Step) []Step {\n\tfor i := range cbs {\n\t\tcb := cbs[i]\n\t\tcbs[i] = func(tei TimeEntryDTO) (TimeEntryDTO, error) {\n\t\t\ttei, _ = cb(tei)\n\t\t\treturn tei, nil\n\t\t}\n\t}\n\treturn cbs\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/util/out-in-progress.go",
    "content": "package util\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// OutInProgressFn will stop the in progress time entry, if it exists\nfunc OutInProgressFn(c api.Client) Step {\n\treturn func(tei TimeEntryDTO) (TimeEntryDTO, error) {\n\t\treturn tei, out(c, tei.Workspace, tei.UserID, tei.Start)\n\t}\n}\n\nfunc out(c api.Client, w, u string, end time.Time) error {\n\tif err := c.Out(api.OutParam{\n\t\tWorkspace: w,\n\t\tUserID:    u,\n\t\tEnd:       end,\n\t}); getErrorCode(err) != 404 {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc getErrorCode(err error) int {\n\tvar e dto.Error\n\tif errors.As(err, &e) {\n\t\treturn e.Code\n\t}\n\n\treturn 0\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/util/report.go",
    "content": "package util\n\nimport (\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\toutput \"github.com/lucassabreu/clockify-cli/pkg/output/time-entry\"\n\t\"github.com/spf13/cobra\"\n)\n\n// OutputFlags sets how to print out a list of time entries\ntype OutputFlags struct {\n\tFormat            string\n\tCSV               bool\n\tJSON              bool\n\tQuiet             bool\n\tMarkdown          bool\n\tDurationFormatted bool\n\tDurationFloat     bool\n\tTimeFormat        string\n}\n\nfunc (of OutputFlags) Check() error {\n\treturn cmdutil.XorFlag(map[string]bool{\n\t\t\"format\":             of.Format != \"\",\n\t\t\"json\":               of.JSON,\n\t\t\"csv\":                of.CSV,\n\t\t\"quiet\":              of.Quiet,\n\t\t\"md\":                 of.Markdown,\n\t\t\"duration-float\":     of.DurationFloat,\n\t\t\"duration-formatted\": of.DurationFormatted,\n\t})\n}\n\n// AddPrintMultipleTimeEntriesFlags add flags to print multiple time entries\nfunc AddPrintMultipleTimeEntriesFlags(cmd *cobra.Command) {\n\tcmd.Flags().BoolP(\"with-totals\", \"S\", false,\n\t\t\"add a totals line at the end\")\n}\n\n// AddPrintTimeEntriesFlags add flags common to time entry print\nfunc AddPrintTimeEntriesFlags(cmd *cobra.Command, of *OutputFlags) {\n\tcmd.Flags().StringVarP(&of.Format, \"format\", \"f\", \"\",\n\t\t\"golang text/template format to be applied on each time entry\")\n\tcmd.Flags().String(\"tz\", \"Local\",\n\t\t\"time zone to be used on the time entries can be \"+\n\t\t\t\"'Local' to use the systems timezone, UTC \"+\n\t\t\t\"or valid TZ identifier from the IANA TZ database \"+\n\t\t\t\"https://en.wikipedia.org/wiki/List_of_tz_database_time_zones\")\n\tcmd.Flags().BoolVarP(&of.JSON, \"json\", \"j\", false, \"print as JSON\")\n\tcmd.Flags().BoolVarP(&of.CSV, \"csv\", \"v\", false, \"print as CSV\")\n\tcmd.Flags().BoolVarP(&of.Quiet, \"quiet\", \"q\", false, \"print only ID\")\n\tcmd.Flags().BoolVarP(&of.Markdown, \"md\", \"m\", false, \"print as Markdown\")\n\tcmd.Flags().BoolVarP(&of.DurationFormatted, \"duration-formatted\", \"D\", false,\n\t\t\"prints only the sum of duration formatted\")\n\tcmd.Flags().BoolVarP(&of.DurationFloat, \"duration-float\", \"F\", false,\n\t\t`prints only the sum of duration as a \"float hour\"`)\n}\n\n// PrintTimeEntryImpl will print out a time entries using parameters and flags\nfunc PrintTimeEntryImpl(\n\ttei dto.TimeEntryImpl,\n\tf cmdutil.Factory,\n\tout io.Writer,\n\tof OutputFlags,\n) error {\n\tc, err := f.Client()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfte, err := c.GetHydratedTimeEntry(api.GetTimeEntryParam{\n\t\tWorkspace:   tei.WorkspaceID,\n\t\tTimeEntryID: tei.ID,\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn PrintTimeEntry(fte, out, f.Config(), of)\n}\n\n// PrintTimeEntry will print out a time entries using parameters and flags\nfunc PrintTimeEntry(\n\tte *dto.TimeEntry, out io.Writer, config cmdutil.Config, of OutputFlags,\n) error {\n\tts := make([]dto.TimeEntry, 0)\n\tif te != nil {\n\t\tts = append(ts, *te)\n\t}\n\n\tb := config.GetBool(cmdutil.CONF_SHOW_TOTAL_DURATION)\n\tconfig.SetBool(cmdutil.CONF_SHOW_TOTAL_DURATION, false)\n\n\terr := PrintTimeEntries(ts, out, config, of)\n\n\tconfig.SetBool(cmdutil.CONF_SHOW_TOTAL_DURATION, b)\n\n\treturn err\n}\n\nfunc updateTimeZone(tes []dto.TimeEntry, config cmdutil.Config) []dto.TimeEntry {\n\tloc := config.TimeZone()\n\tif loc == time.Local {\n\t\treturn tes\n\t}\n\n\tfor i := range tes {\n\t\ttes[i].TimeInterval.Start = tes[i].TimeInterval.Start.In(loc)\n\t\tif tes[i].TimeInterval.End != nil {\n\t\t\tend := tes[i].TimeInterval.End.In(loc)\n\t\t\ttes[i].TimeInterval.End = &end\n\t\t}\n\t}\n\treturn tes\n}\n\n// PrintTimeEntries will print out a list of time entries using parameters and\n// flags\nfunc PrintTimeEntries(\n\ttes []dto.TimeEntry, out io.Writer, config cmdutil.Config, of OutputFlags,\n) error {\n\ttes = updateTimeZone(tes, config)\n\tswitch {\n\tcase of.Markdown:\n\t\treturn output.TimeEntriesMarkdownPrint(tes, out)\n\tcase of.JSON:\n\t\treturn output.TimeEntriesJSONPrint(tes, out)\n\tcase of.CSV:\n\t\treturn output.TimeEntriesCSVPrint(tes, out)\n\tcase of.Format != \"\":\n\t\treturn output.TimeEntriesPrintWithTemplate(of.Format)(tes, out)\n\tcase of.Quiet:\n\t\treturn output.TimeEntriesPrintQuietly(tes, out)\n\tcase of.DurationFloat:\n\t\treturn output.TimeEntriesTotalDurationOnlyAsFloat(\n\t\t\ttes, out, config.Language())\n\tcase of.DurationFormatted:\n\t\treturn output.TimeEntriesTotalDurationOnlyFormatted(tes, out)\n\tdefault:\n\t\topts := output.NewTimeEntryOutputOptions().\n\t\t\tWithTimeFormat(of.TimeFormat)\n\n\t\tif config.GetBool(cmdutil.CONF_SHOW_TASKS) {\n\t\t\topts = opts.WithShowTasks()\n\t\t}\n\n\t\tif config.GetBool(cmdutil.CONF_SHOW_CUSTOM_FIELDS) {\n\t\t\topts = opts.WithShowCustomFields()\n\t\t}\n\n\t\tif config.GetBool(cmdutil.CONF_SHOW_CLIENT) {\n\t\t\topts = opts.WithShowClients()\n\t\t}\n\n\t\tif config.GetBool(cmdutil.CONF_SHOW_TOTAL_DURATION) {\n\t\t\topts = opts.WithTotalDuration()\n\t\t}\n\n\t\treturn output.TimeEntriesPrint(opts)(tes, out)\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/util/util.go",
    "content": "// util package provides reusable functionality to the commands under\n// pkg/cmd/time-entry, be it editing, creating, or rendering time entries\npackage util\n\nimport (\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// TimeEntryDTO is used to keep and update the data of a time entry before\n// changing it, taking into account optional values (nil)\ntype TimeEntryDTO struct {\n\tID          string\n\tWorkspace   string\n\tUserID      string\n\tProjectID   string\n\tClient      string\n\tTaskID      string\n\tDescription string\n\tStart       time.Time\n\tEnd         *time.Time\n\tTagIDs      []string\n\tBillable    *bool\n\tLocked      *bool\n}\n\n// Step is used to stack multiple actions to be executed over a TimeEntryDTO\ntype Step func(TimeEntryDTO) (TimeEntryDTO, error)\n\nfunc skip(te TimeEntryDTO) (TimeEntryDTO, error) {\n\treturn te, nil\n}\n\n// Do will runs all callback functions over the time entry, keeping\n// the changes and returning it after\nfunc Do(te TimeEntryDTO, cbs ...Step) (TimeEntryDTO, error) {\n\treturn compose(cbs...)(te)\n}\n\nfunc compose(cbs ...Step) Step {\n\treturn func(dto TimeEntryDTO) (TimeEntryDTO, error) {\n\t\tvar err error\n\t\tfor _, cb := range cbs {\n\t\t\tif dto, err = cb(dto); err != nil {\n\t\t\t\treturn dto, err\n\t\t\t}\n\t\t}\n\n\t\treturn dto, err\n\t}\n}\n\n// TimeEntryImplToDTO returns a TimeEntryDTO using the information from a\n// TimeEntryImpl\nfunc TimeEntryImplToDTO(t dto.TimeEntryImpl) TimeEntryDTO {\n\treturn TimeEntryDTO{\n\t\tWorkspace:   t.WorkspaceID,\n\t\tUserID:      t.UserID,\n\t\tID:          t.ID,\n\t\tProjectID:   t.ProjectID,\n\t\tTaskID:      t.TaskID,\n\t\tDescription: t.Description,\n\t\tStart:       t.TimeInterval.Start,\n\t\tEnd:         t.TimeInterval.End,\n\t\tTagIDs:      t.TagIDs,\n\t\tBillable:    &t.Billable,\n\t\tLocked:      &t.IsLocked,\n\t}\n}\n\n// TimeEntryDTOToImpl returns a TimeEntryImpl using the information from a\n// TimeEntryDTO\nfunc TimeEntryDTOToImpl(t TimeEntryDTO) dto.TimeEntryImpl {\n\tif t.Billable == nil {\n\t\tb := false\n\t\tt.Billable = &b\n\t}\n\n\tif t.Locked == nil {\n\t\tb := false\n\t\tt.Locked = &b\n\t}\n\n\treturn dto.TimeEntryImpl{\n\t\tWorkspaceID:  t.Workspace,\n\t\tUserID:       t.UserID,\n\t\tDescription:  t.Description,\n\t\tID:           t.ID,\n\t\tProjectID:    t.ProjectID,\n\t\tTagIDs:       t.TagIDs,\n\t\tTaskID:       t.TaskID,\n\t\tTimeInterval: dto.NewTimeInterval(t.Start, t.End),\n\t\tBillable:     *t.Billable,\n\t\tIsLocked:     *t.Locked,\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/util/util_test.go",
    "content": "package util\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t. \"github.com/lucassabreu/clockify-cli/internal/testhlp\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n)\n\nvar bTrue = true\nvar bFalse = false\n\nfunc TestDo_ShouldApplySteps_InOrder(t *testing.T) {\n\tfirst := func(TimeEntryDTO) (TimeEntryDTO, error) {\n\t\treturn TimeEntryDTO{ID: \"first\"}, nil\n\t}\n\tsecond := func(TimeEntryDTO) (TimeEntryDTO, error) {\n\t\treturn TimeEntryDTO{ID: \"second\"}, nil\n\t}\n\n\ttts := []struct {\n\t\tname   string\n\t\tsteps  []Step\n\t\tresult TimeEntryDTO\n\t}{\n\t\t{\n\t\t\tname: \"first\",\n\t\t\tsteps: []Step{\n\t\t\t\tsecond,\n\t\t\t\tfirst,\n\t\t\t\tskip,\n\t\t\t\tskip,\n\t\t\t},\n\t\t\tresult: TimeEntryDTO{ID: \"first\"},\n\t\t},\n\t\t{\n\t\t\tname: \"second\",\n\t\t\tsteps: []Step{\n\t\t\t\tskip,\n\t\t\t\tfirst,\n\t\t\t\tskip,\n\t\t\t\tsecond,\n\t\t\t},\n\t\t\tresult: TimeEntryDTO{ID: \"second\"},\n\t\t},\n\t\t{\n\t\t\tname: \"only skips\",\n\t\t\tsteps: []Step{\n\t\t\t\tskip,\n\t\t\t\tskip,\n\t\t\t\tskip,\n\t\t\t},\n\t\t\tresult: TimeEntryDTO{},\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\ttt := &tts[i]\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr, err := Do(TimeEntryDTO{}, tt.steps...)\n\t\t\tif !assert.NoError(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.result, r)\n\t\t})\n\t}\n}\n\nfunc TestImplToDTOAndBack(t *testing.T) {\n\tend := MustParseTime(timehlp.SimplerTimeFormat, \"2022-11-07 11:00\")\n\timpl := dto.TimeEntryImpl{\n\t\tBillable:    true,\n\t\tDescription: \"unique\",\n\t\tID:          \"te\",\n\t\tIsLocked:    false,\n\t\tProjectID:   \"p\",\n\t\tTagIDs:      []string{\"tag\"},\n\t\tTaskID:      \"t\",\n\t\tTimeInterval: dto.NewTimeInterval(\n\t\t\tMustParseTime(timehlp.SimplerTimeFormat, \"2022-11-07 10:00\"),\n\t\t\t&end,\n\t\t),\n\t\tUserID:      \"u\",\n\t\tWorkspaceID: \"w\",\n\t}\n\n\tdto := TimeEntryImplToDTO(impl)\n\tnimpl := TimeEntryDTOToImpl(dto)\n\n\tassert.Equal(t, impl, nimpl)\n\tassert.Equal(t, dto, TimeEntryImplToDTO(nimpl))\n}\n\nfunc TestTimeEntryDTOToImpl_ShouldFillMissingProperties(t *testing.T) {\n\ttm := MustParseTime(timehlp.SimplerTimeFormat, \"2022-11-07 10:00\")\n\tassert.Equal(t,\n\t\tdto.TimeEntryImpl{TimeInterval: dto.NewTimeInterval(tm, &tm)},\n\t\tTimeEntryDTOToImpl(TimeEntryDTO{\n\t\t\tStart: tm,\n\t\t\tEnd:   &tm,\n\t\t}),\n\t)\n}\n\ntype flagSetMock struct {\n\tflags map[string]interface{}\n}\n\nfunc (f *flagSetMock) Changed(k string) bool {\n\t_, ok := f.flags[k]\n\treturn ok\n}\n\nfunc (f *flagSetMock) GetString(k string) (string, error) {\n\tif f.Changed(k) {\n\t\treturn f.flags[k].(string), nil\n\t}\n\n\treturn \"\", nil\n}\n\nfunc (f *flagSetMock) GetStringSlice(k string) ([]string, error) {\n\tif f.Changed(k) {\n\t\treturn f.flags[k].([]string), nil\n\t}\n\n\treturn []string{}, nil\n}\n\nfunc TestFillTimeEntryWithFlags_ShouldNotSetProperties_WhenNotChanged(\n\tt *testing.T) {\n\ttm := MustParseTime(timehlp.SimplerTimeFormat, \"2022-11-07 11:00\").Local()\n\n\tforEnd := func(t time.Time) *time.Time { return &t }\n\n\ttts := []struct {\n\t\tname   string\n\t\tflags  flagSet\n\t\tinput  TimeEntryDTO\n\t\toutput TimeEntryDTO\n\t\terr    string\n\t}{\n\t\t{\n\t\t\tname: \"only description\",\n\t\t\tflags: &flagSetMock{flags: map[string]interface{}{\n\t\t\t\t\"description\": \"diff\",\n\t\t\t}},\n\t\t\tinput:  TimeEntryDTO{Description: \"other\"},\n\t\t\toutput: TimeEntryDTO{Description: \"diff\"},\n\t\t},\n\t\t{\n\t\t\tname: \"only dates\",\n\t\t\tflags: &flagSetMock{flags: map[string]interface{}{\n\t\t\t\t\"when\":          tm.Format(timehlp.SimplerTimeFormat),\n\t\t\t\t\"when-to-close\": tm.Format(timehlp.SimplerTimeFormat),\n\t\t\t}},\n\t\t\toutput: TimeEntryDTO{\n\t\t\t\tStart: tm,\n\t\t\t\tEnd:   &tm,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"should accept descriptive relative dates\",\n\t\t\tflags: &flagSetMock{flags: map[string]interface{}{\n\t\t\t\t\"when\":          \"+1h15m15s\",\n\t\t\t\t\"when-to-close\": \"+2h16m16s\",\n\t\t\t}},\n\t\t\toutput: TimeEntryDTO{\n\t\t\t\tStart: timehlp.Now().\n\t\t\t\t\tAdd(time.Hour + time.Minute*15 + time.Second*15),\n\t\t\t\tEnd: forEnd(\n\t\t\t\t\ttimehlp.Now().\n\t\t\t\t\t\tAdd(time.Hour*2 + time.Minute*16 + time.Second*16),\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"should accept relative dates\",\n\t\t\tflags: &flagSetMock{flags: map[string]interface{}{\n\t\t\t\t\"when\":          \"+15:15\",\n\t\t\t\t\"when-to-close\": \"+16:16\",\n\t\t\t}},\n\t\t\toutput: TimeEntryDTO{\n\t\t\t\tStart: timehlp.Now().\n\t\t\t\t\tAdd(time.Minute*15 + time.Second*15),\n\t\t\t\tEnd: forEnd(\n\t\t\t\t\ttimehlp.Now().Add(time.Minute*16 + time.Second*16),\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"should accept fixed time\",\n\t\t\tflags: &flagSetMock{flags: map[string]interface{}{\n\t\t\t\t\"when\":          \"15:15\",\n\t\t\t\t\"when-to-close\": \"16:16\",\n\t\t\t}},\n\t\t\toutput: TimeEntryDTO{\n\t\t\t\tStart: timehlp.Today().\n\t\t\t\t\tAdd(time.Hour*15 + time.Minute*15),\n\t\t\t\tEnd: forEnd(\n\t\t\t\t\ttimehlp.Today().Add(time.Hour*16 + time.Minute*16),\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"should validate time-strings\",\n\t\t\tflags: &flagSetMock{flags: map[string]interface{}{\n\t\t\t\t\"when\": \"wrong\",\n\t\t\t}},\n\t\t\terr: \"parsing time.*\",\n\t\t},\n\t\t{\n\t\t\tname: \"should validate time-strings (close time)\",\n\t\t\tflags: &flagSetMock{flags: map[string]interface{}{\n\t\t\t\t\"when-to-close\": \"wrong\",\n\t\t\t}},\n\t\t\terr: \"parsing time.*\",\n\t\t},\n\t\t{\n\t\t\tname: \"should validate time-strings with right format, but wrong\",\n\t\t\tflags: &flagSetMock{flags: map[string]interface{}{\n\t\t\t\t\"when\": \"99:99:99\",\n\t\t\t}},\n\t\t\terr: \"parsing time.*\",\n\t\t},\n\t\t{\n\t\t\tname: \"should validate time-strings with right format, end time\",\n\t\t\tflags: &flagSetMock{flags: map[string]interface{}{\n\t\t\t\t\"when-to-close\": \"99:99:99\",\n\t\t\t}},\n\t\t\terr: \"parsing time.*\",\n\t\t},\n\t\t{\n\t\t\tname: \"all but dates and not-billable\",\n\t\t\tflags: &flagSetMock{flags: map[string]interface{}{\n\t\t\t\t\"description\": \"d\",\n\t\t\t\t\"project\":     \"p\",\n\t\t\t\t\"task\":        \"t\",\n\t\t\t\t\"tag\":         []string{\"t1\", \"t2\"},\n\t\t\t\t\"billable\":    true,\n\t\t\t}},\n\t\t\tinput: TimeEntryDTO{\n\t\t\t\tStart: tm,\n\t\t\t\tEnd:   &tm,\n\t\t\t},\n\t\t\toutput: TimeEntryDTO{\n\t\t\t\tStart:       tm,\n\t\t\t\tEnd:         &tm,\n\t\t\t\tDescription: \"d\",\n\t\t\t\tProjectID:   \"p\",\n\t\t\t\tTaskID:      \"t\",\n\t\t\t\tTagIDs:      []string{\"t1\", \"t2\"},\n\t\t\t\tBillable:    &bTrue,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"all but dates and billable\",\n\t\t\tflags: &flagSetMock{flags: map[string]interface{}{\n\t\t\t\t\"description\":  \"d\",\n\t\t\t\t\"project\":      \"p\",\n\t\t\t\t\"task\":         \"t\",\n\t\t\t\t\"tags\":         []string{\"t1\", \"t2\"},\n\t\t\t\t\"not-billable\": true,\n\t\t\t}},\n\t\t\tinput: TimeEntryDTO{\n\t\t\t\tStart:    tm,\n\t\t\t\tEnd:      &tm,\n\t\t\t\tBillable: &bTrue,\n\t\t\t},\n\t\t\toutput: TimeEntryDTO{\n\t\t\t\tStart:       tm,\n\t\t\t\tEnd:         &tm,\n\t\t\t\tDescription: \"d\",\n\t\t\t\tProjectID:   \"p\",\n\t\t\t\tTaskID:      \"t\",\n\t\t\t\tTagIDs:      []string{\"t1\", \"t2\"},\n\t\t\t\tBillable:    &bFalse,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"should reset task when project changes\",\n\t\t\tflags: &flagSetMock{flags: map[string]interface{}{\n\t\t\t\t\"description\":  \"d\",\n\t\t\t\t\"project\":      \"p2\",\n\t\t\t\t\"tags\":         []string{\"t1\", \"t2\"},\n\t\t\t\t\"not-billable\": true,\n\t\t\t}},\n\t\t\tinput: TimeEntryDTO{\n\t\t\t\tProjectID: \"p1\",\n\t\t\t\tTaskID:    \"t\",\n\t\t\t\tStart:     tm,\n\t\t\t\tEnd:       &tm,\n\t\t\t\tBillable:  &bTrue,\n\t\t\t},\n\t\t\toutput: TimeEntryDTO{\n\t\t\t\tStart:       tm,\n\t\t\t\tEnd:         &tm,\n\t\t\t\tDescription: \"d\",\n\t\t\t\tProjectID:   \"p2\",\n\t\t\t\tTaskID:      \"\",\n\t\t\t\tTagIDs:      []string{\"t1\", \"t2\"},\n\t\t\t\tBillable:    &bFalse,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"should not be billable and not billable\",\n\t\t\tflags: &flagSetMock{flags: map[string]interface{}{\n\t\t\t\t\"billable\":     true,\n\t\t\t\t\"not-billable\": true,\n\t\t\t}},\n\t\t\terr: \"flags can't be used together\",\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\ttt := &tts[i]\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\td, err := FillTimeEntryWithFlags(tt.flags)(tt.input)\n\t\t\tif tt.err != \"\" {\n\t\t\t\tif !assert.Error(t, err) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tassert.Regexp(t, tt.err, err.Error())\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.output, d)\n\t\t})\n\t}\n}\n\nfunc TestGetValidateTimeEntry_ShouldValidate_UsingSettingsAndConfigs(\n\tt *testing.T) {\n\tcnf := &mocks.SimpleConfig{\n\t\tAllowIncomplete: false,\n\t}\n\n\twSettingsFn := func(\n\t\tws dto.WorkspaceSettings) func(t *testing.T) cmdutil.Factory {\n\t\treturn func(t *testing.T) cmdutil.Factory {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tf.EXPECT().Config().Return(cnf)\n\n\t\t\tf.EXPECT().GetWorkspace().Return(dto.Workspace{Settings: ws}, nil)\n\n\t\t\treturn f\n\t\t}\n\t}\n\n\twSettingsAndProjectFn := func(\n\t\tw dto.WorkspaceSettings,\n\t\tp *dto.Project,\n\t\terr error,\n\t) func(t *testing.T) cmdutil.Factory {\n\t\treturn func(t *testing.T) cmdutil.Factory {\n\t\t\tf := wSettingsFn(w)(t).(*mocks.MockFactory)\n\n\t\t\tc := mocks.NewMockClient(t)\n\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\tc.EXPECT().GetProject(mock.Anything).Return(p, err)\n\n\t\t\treturn f\n\t\t}\n\t}\n\n\ttts := []struct {\n\t\tname    string\n\t\tinput   TimeEntryDTO\n\t\terr     string\n\t\tfactory func(*testing.T) cmdutil.Factory\n\t}{\n\t\t{\n\t\t\tname: \"do nothing\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowIncomplete: true,\n\t\t\t\t})\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail to find workspace\",\n\t\t\terr:  \"get workspace error\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(cnf)\n\n\t\t\t\tf.EXPECT().GetWorkspace().\n\t\t\t\t\tReturn(dto.Workspace{}, errors.New(\"get workspace error\"))\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"fail to init client\",\n\t\t\terr:   \"client error\",\n\t\t\tinput: TimeEntryDTO{ProjectID: \"p\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(cnf)\n\n\t\t\t\tf.EXPECT().GetWorkspace().Return(dto.Workspace{}, nil)\n\n\t\t\t\tf.EXPECT().Client().\n\t\t\t\t\tReturn(mocks.NewMockClient(t), errors.New(\"client error\"))\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"force project\",\n\t\t\tinput: TimeEntryDTO{},\n\t\t\terr:   \"workspace requires project\",\n\t\t\tfactory: wSettingsFn(dto.WorkspaceSettings{\n\t\t\t\tForceProjects: true,\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname:  \"force task\",\n\t\t\tinput: TimeEntryDTO{},\n\t\t\terr:   \"workspace requires task\",\n\t\t\tfactory: wSettingsFn(dto.WorkspaceSettings{\n\t\t\t\tForceTasks: true,\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname:  \"force description\",\n\t\t\tinput: TimeEntryDTO{},\n\t\t\terr:   \"workspace requires description\",\n\t\t\tfactory: wSettingsFn(dto.WorkspaceSettings{\n\t\t\t\tForceDescription: true,\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname:  \"force tags\",\n\t\t\tinput: TimeEntryDTO{},\n\t\t\terr:   \"workspace requires at least one tag\",\n\t\t\tfactory: wSettingsFn(dto.WorkspaceSettings{\n\t\t\t\tForceTags: true,\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"project not found\",\n\t\t\tinput: TimeEntryDTO{\n\t\t\t\tWorkspace:   \"w\",\n\t\t\t\tDescription: \"description\",\n\t\t\t\tProjectID:   \"project\",\n\t\t\t\tTaskID:      \"task\",\n\t\t\t\tTagIDs:      []string{\"tag\"},\n\t\t\t},\n\t\t\terr: \"not found\",\n\t\t\tfactory: wSettingsAndProjectFn(\n\t\t\t\tdto.WorkspaceSettings{\n\t\t\t\t\tForceDescription: true,\n\t\t\t\t\tForceProjects:    true,\n\t\t\t\t\tForceTasks:       true,\n\t\t\t\t\tForceTags:        true,\n\t\t\t\t},\n\t\t\t\tnil,\n\t\t\t\tapi.EntityNotFound{\n\t\t\t\t\tEntityName: \"project\",\n\t\t\t\t\tID:         \"project\",\n\t\t\t\t},\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"project is archived, all is required\",\n\t\t\tinput: TimeEntryDTO{\n\t\t\t\tWorkspace:   \"w\",\n\t\t\t\tDescription: \"description\",\n\t\t\t\tProjectID:   \"project\",\n\t\t\t\tTaskID:      \"task\",\n\t\t\t\tTagIDs:      []string{\"tag\"},\n\t\t\t},\n\t\t\terr: \"project \\\\w+ - \\\\w+ is archived\",\n\t\t\tfactory: wSettingsAndProjectFn(\n\t\t\t\tdto.WorkspaceSettings{\n\t\t\t\t\tForceDescription: true,\n\t\t\t\t\tForceProjects:    true,\n\t\t\t\t\tForceTasks:       true,\n\t\t\t\t\tForceTags:        true,\n\t\t\t\t},\n\t\t\t\t&dto.Project{\n\t\t\t\t\tID:       \"project\",\n\t\t\t\t\tName:     \"name\",\n\t\t\t\t\tArchived: true,\n\t\t\t\t}, nil,\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"project is archived, nothing is required\",\n\t\t\tinput: TimeEntryDTO{\n\t\t\t\tProjectID: \"project\",\n\t\t\t},\n\t\t\terr: \"project \\\\w+ - \\\\w+ is archived\",\n\t\t\tfactory: wSettingsAndProjectFn(\n\t\t\t\tdto.WorkspaceSettings{},\n\t\t\t\t&dto.Project{\n\t\t\t\t\tID:       \"project\",\n\t\t\t\t\tName:     \"name\",\n\t\t\t\t\tArchived: true,\n\t\t\t\t}, nil,\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"nothing is required, without project\",\n\t\t\tinput: TimeEntryDTO{\n\t\t\t\tWorkspace:   \"w\",\n\t\t\t\tDescription: \"description\",\n\t\t\t\tTaskID:      \"task\",\n\t\t\t\tTagIDs:      []string{\"tag\"},\n\t\t\t},\n\t\t\tfactory: wSettingsFn(dto.WorkspaceSettings{}),\n\t\t},\n\t\t{\n\t\t\tname: \"everything is right\",\n\t\t\tinput: TimeEntryDTO{\n\t\t\t\tWorkspace:   \"w\",\n\t\t\t\tDescription: \"description\",\n\t\t\t\tProjectID:   \"project\",\n\t\t\t\tTaskID:      \"task\",\n\t\t\t\tTagIDs:      []string{\"tag\"},\n\t\t\t},\n\t\t\tfactory: wSettingsAndProjectFn(\n\t\t\t\tdto.WorkspaceSettings{\n\t\t\t\t\tForceDescription: true,\n\t\t\t\t\tForceProjects:    true,\n\t\t\t\t\tForceTasks:       true,\n\t\t\t\t\tForceTags:        true,\n\t\t\t\t},\n\t\t\t\t&dto.Project{\n\t\t\t\t\tID:   \"project\",\n\t\t\t\t\tName: \"name\",\n\t\t\t\t},\n\t\t\t\tnil,\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname:    \"nothing is required\",\n\t\t\tinput:   TimeEntryDTO{},\n\t\t\tfactory: wSettingsFn(dto.WorkspaceSettings{}),\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\ttt := &tts[i]\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t_, err := GetValidateTimeEntryFn(tt.factory(t))(tt.input)\n\t\t\tif tt.err != \"\" {\n\t\t\t\tif !assert.Error(t, err) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestGetAllowNameForIDsFn_ShouldLookupEntityIDs_WhenFilled(t *testing.T) {\n\tte := TimeEntryDTO{\n\t\tWorkspace: \"w\",\n\t\tProjectID: \"p\",\n\t\tTaskID:    \"t\",\n\t\tTagIDs:    []string{\"t1\", \"t2\"},\n\t}\n\n\tcf := &mocks.SimpleConfig{AllowNameForID: true}\n\tc := mocks.NewMockClient(t)\n\n\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\tWorkspace:       te.Workspace,\n\t\tPaginationParam: api.AllPages(),\n\t}).\n\t\tReturn([]dto.Project{{ID: \"pj_id\", Name: \"project\"}}, nil)\n\n\tc.EXPECT().GetTasks(api.GetTasksParam{\n\t\tWorkspace:       te.Workspace,\n\t\tProjectID:       \"pj_id\",\n\t\tActive:          true,\n\t\tPaginationParam: api.AllPages(),\n\t}).\n\t\tReturn([]dto.Task{{ID: \"tk_id\", Name: \"task\"}}, nil)\n\n\tc.EXPECT().GetTags(api.GetTagsParam{\n\t\tWorkspace:       te.Workspace,\n\t\tPaginationParam: api.AllPages(),\n\t}).\n\t\tReturn([]dto.Tag{\n\t\t\t{ID: \"tg_id_1\", Name: \"t1\"},\n\t\t\t{ID: \"tg_id_2\", Name: \"t2\"},\n\t\t}, nil)\n\n\tte, err := GetAllowNameForIDsFn(cf, c)(te)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\n\tassert.Equal(t, \"pj_id\", te.ProjectID)\n\tassert.Equal(t, \"tk_id\", te.TaskID)\n\tassert.Equal(t, []string{\"tg_id_1\", \"tg_id_2\"}, te.TagIDs)\n}\n\nfunc TestGetAllowNameForIDsFn_ShouldNotLookupEntityIDs_WhenEmpty(\n\tt *testing.T) {\n\tte := TimeEntryDTO{\n\t\tWorkspace: \"\",\n\t\tProjectID: \"\",\n\t\tTaskID:    \"\",\n\t\tTagIDs:    []string{},\n\t}\n\n\tte2, err := GetAllowNameForIDsFn(\n\t\t&mocks.SimpleConfig{AllowNameForID: true}, mocks.NewMockClient(t))(te)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\n\tassert.Equal(t, te, te2)\n}\n\nfunc TestGetAllowNameForIDsFn_ShouldNotLookup_WhenDisabled(t *testing.T) {\n\tte := TimeEntryDTO{\n\t\tWorkspace: \"w\",\n\t\tProjectID: \"p\",\n\t\tTaskID:    \"t\",\n\t\tTagIDs:    []string{\"t1\", \"t2\"},\n\t}\n\n\tte2, err := GetAllowNameForIDsFn(\n\t\t&mocks.SimpleConfig{AllowNameForID: false}, mocks.NewMockClient(t))(te)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\n\tassert.Equal(t, te, te2)\n}\n\nfunc TestGetAllowNameForIDsFn_ShouldFail_WhenEntitiesNotFound(t *testing.T) {\n\tte := TimeEntryDTO{\n\t\tWorkspace: \"w\",\n\t\tProjectID: \"p\",\n\t\tTaskID:    \"t\",\n\t\tTagIDs:    []string{\"t1\"},\n\t}\n\n\tcf := &mocks.SimpleConfig{AllowNameForID: true}\n\tc := mocks.NewMockClient(t)\n\n\tc.EXPECT().GetProjects(mock.Anything).Return([]dto.Project{}, nil).Once()\n\tc.EXPECT().GetTasks(mock.Anything).Return([]dto.Task{}, nil).Once()\n\tc.EXPECT().GetTags(mock.Anything).Return([]dto.Tag{}, nil).Once()\n\n\t_, err := GetAllowNameForIDsFn(cf, c)(te)\n\tif !assert.Error(t, err) {\n\t\treturn\n\t}\n\tassert.Regexp(t, \"No project with id or name .*\", err.Error())\n\n\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\tWorkspace:       te.Workspace,\n\t\tPaginationParam: api.AllPages(),\n\t}).\n\t\tReturn([]dto.Project{{ID: \"pj_id\", Name: \"project\"}}, nil)\n\n\t_, err = GetAllowNameForIDsFn(cf, c)(te)\n\tif !assert.Error(t, err) {\n\t\treturn\n\t}\n\tassert.Regexp(t, \"No task with id or name .*\", err.Error())\n\n\tc.EXPECT().GetTasks(api.GetTasksParam{\n\t\tWorkspace:       te.Workspace,\n\t\tProjectID:       \"pj_id\",\n\t\tActive:          true,\n\t\tPaginationParam: api.AllPages(),\n\t}).\n\t\tReturn([]dto.Task{{ID: \"tk_id\", Name: \"task\"}}, nil)\n\n\t_, err = GetAllowNameForIDsFn(cf, c)(te)\n\tif !assert.Error(t, err) {\n\t\treturn\n\t}\n\tassert.Regexp(t, \"No tag with id or name .*\", err.Error())\n\n\tc.EXPECT().GetTags(api.GetTagsParam{\n\t\tWorkspace:       te.Workspace,\n\t\tPaginationParam: api.AllPages(),\n\t}).\n\t\tReturn([]dto.Tag{{ID: \"tg_id_1\", Name: \"t1\"}}, nil)\n\n\t_, err = GetAllowNameForIDsFn(cf, c)(te)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\tassert.NoError(t, err)\n}\n\nfunc TestGetAllowNameForIDsFn_ShouldBeQuiet_WhenInteractive(t *testing.T) {\n\tte := TimeEntryDTO{\n\t\tWorkspace: \"w\",\n\t\tProjectID: \"p\",\n\t\tTagIDs:    []string{\"t1\"},\n\t}\n\n\tcf := &mocks.SimpleConfig{\n\t\tAllowNameForID: true,\n\t\tInteractive:    true,\n\t}\n\tc := mocks.NewMockClient(t)\n\n\tc.EXPECT().GetProjects(mock.Anything).Return([]dto.Project{}, nil).Once()\n\tc.EXPECT().GetTags(mock.Anything).Return([]dto.Tag{}, nil).Once()\n\n\t_, err := GetAllowNameForIDsFn(cf, c)(te)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\n\tte.TaskID = \"t\"\n\n\tc.EXPECT().GetProjects(mock.Anything).\n\t\tReturn([]dto.Project{{ID: \"pj_id\", Name: \"project\"}}, nil).Once()\n\n\tc.EXPECT().GetTasks(mock.Anything).Return([]dto.Task{}, nil).Once()\n\tc.EXPECT().GetTags(mock.Anything).Return([]dto.Tag{}, nil).Once()\n\n\t_, err = GetAllowNameForIDsFn(cf, c)(te)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n}\n\nfunc TestFillMissingBillableFn(t *testing.T) {\n\ttts := []struct {\n\t\tname    string\n\t\tinput   TimeEntryDTO\n\t\tproject *dto.Project\n\t\ttask    *dto.Task\n\t\twant    *bool\n\t}{\n\t\t{\n\t\t\tname:  \"billable already set, no api call\",\n\t\t\tinput: TimeEntryDTO{Billable: &bTrue, ProjectID: \"p\"},\n\t\t\twant:  &bTrue,\n\t\t},\n\t\t{\n\t\t\tname:  \"not-billable already set, no api call\",\n\t\t\tinput: TimeEntryDTO{Billable: &bFalse, ProjectID: \"p\"},\n\t\t\twant:  &bFalse,\n\t\t},\n\t\t{\n\t\t\tname:  \"no project, no api call\",\n\t\t\tinput: TimeEntryDTO{},\n\t\t\twant:  nil,\n\t\t},\n\t\t{\n\t\t\tname:    \"derives true from project when no task\",\n\t\t\tinput:   TimeEntryDTO{Workspace: \"w\", ProjectID: \"p\"},\n\t\t\tproject: &dto.Project{ID: \"p\", Billable: true},\n\t\t\twant:    &bTrue,\n\t\t},\n\t\t{\n\t\t\tname:    \"derives false from project when no task\",\n\t\t\tinput:   TimeEntryDTO{Workspace: \"w\", ProjectID: \"p\"},\n\t\t\tproject: &dto.Project{ID: \"p\", Billable: false},\n\t\t\twant:    &bFalse,\n\t\t},\n\t\t{\n\t\t\tname:  \"derives true from task, ignores project\",\n\t\t\tinput: TimeEntryDTO{Workspace: \"w\", ProjectID: \"p\", TaskID: \"t\"},\n\t\t\ttask:  &dto.Task{ID: \"t\", Billable: true},\n\t\t\twant:  &bTrue,\n\t\t},\n\t\t{\n\t\t\tname:  \"derives false from task, ignores project\",\n\t\t\tinput: TimeEntryDTO{Workspace: \"w\", ProjectID: \"p\", TaskID: \"t\"},\n\t\t\ttask:  &dto.Task{ID: \"t\", Billable: false},\n\t\t\twant:  &bFalse,\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\ttt := &tts[i]\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := mocks.NewMockClient(t)\n\n\t\t\tif tt.project != nil {\n\t\t\t\tc.EXPECT().GetProject(api.GetProjectParam{\n\t\t\t\t\tWorkspace: tt.input.Workspace,\n\t\t\t\t\tProjectID: tt.input.ProjectID,\n\t\t\t\t}).Return(tt.project, nil)\n\t\t\t}\n\n\t\t\tif tt.task != nil {\n\t\t\t\tc.EXPECT().GetTask(api.GetTaskParam{\n\t\t\t\t\tWorkspace: tt.input.Workspace,\n\t\t\t\t\tProjectID: tt.input.ProjectID,\n\t\t\t\t\tTaskID:    tt.input.TaskID,\n\t\t\t\t}).Return(*tt.task, nil)\n\t\t\t}\n\n\t\t\tresult, err := FillMissingBillableFn(c)(tt.input)\n\t\t\tif !assert.NoError(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.want, result.Billable)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/util/validate-closing.go",
    "content": "package util\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n)\n\n// ValidateClosingTimeEntry checks if the current time entry will fail to be\n// stopped\nfunc ValidateClosingTimeEntry(f cmdutil.Factory) Step {\n\treturn func(dto TimeEntryDTO) (TimeEntryDTO, error) {\n\t\tc, err := f.Client()\n\t\tif err != nil {\n\t\t\treturn dto, err\n\t\t}\n\n\t\tte, err := c.GetTimeEntryInProgress(api.GetTimeEntryInProgressParam{\n\t\t\tWorkspace: dto.Workspace,\n\t\t\tUserID:    dto.UserID,\n\t\t})\n\n\t\tif te == nil || err != nil {\n\t\t\treturn dto, err\n\t\t}\n\n\t\tif err = validateTimeEntry(TimeEntryImplToDTO(*te), f); err != nil {\n\t\t\treturn dto, fmt.Errorf(\n\t\t\t\t\"running time entry can't be ended: %w\", err)\n\t\t}\n\n\t\treturn dto, nil\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/time-entry/util/validate.go",
    "content": "package util\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n)\n\n// GetValidateTimeEntryFn will check if the time entry is valid given the\n// workspace parameters\nfunc GetValidateTimeEntryFn(f cmdutil.Factory) Step {\n\tif f.Config().GetBool(cmdutil.CONF_ALLOW_INCOMPLETE) {\n\t\treturn skip\n\t}\n\n\treturn func(tei TimeEntryDTO) (TimeEntryDTO, error) {\n\t\treturn tei, validateTimeEntry(tei, f)\n\t}\n}\n\nfunc validateTimeEntry(te TimeEntryDTO, f cmdutil.Factory) error {\n\tw, err := f.GetWorkspace()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif w.Settings.ForceProjects && te.ProjectID == \"\" {\n\t\treturn errors.New(\"workspace requires project\")\n\t}\n\n\tif w.Settings.ForceTasks && te.TaskID == \"\" {\n\t\treturn errors.New(\"workspace requires task\")\n\t}\n\n\tif w.Settings.ForceDescription && strings.TrimSpace(te.Description) == \"\" {\n\t\treturn errors.New(\"workspace requires description\")\n\t}\n\n\tif w.Settings.ForceTags && len(te.TagIDs) == 0 {\n\t\treturn errors.New(\"workspace requires at least one tag\")\n\t}\n\n\tif te.ProjectID == \"\" {\n\t\treturn nil\n\t}\n\n\tc, err := f.Client()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp, err := c.GetProject(api.GetProjectParam{\n\t\tWorkspace: te.Workspace,\n\t\tProjectID: te.ProjectID,\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif p.Archived {\n\t\treturn fmt.Errorf(\"project %s - %s is archived\", p.ID, p.Name)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/cmd/user/me/me.go",
    "content": "package me\n\nimport (\n\t\"io\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/user/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/output/user\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdMe represents the me command\nfunc NewCmdMe(\n\tf cmdutil.Factory,\n\treport func(io.Writer, *util.OutputFlags, dto.User) error,\n) *cobra.Command {\n\tof := util.OutputFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:   \"me\",\n\t\tShort: \"Show details about the user who owns the token\",\n\t\tLong: heredoc.Doc(`\n\t\t\tShows details about the user who owns the token used by the CLI.\n\n\t\t\tThis user may be different from the one set at \"user.id\", but if the parameter is not set the CLI will defaults to this one.\n\t\t`),\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s\n\t\t\t+--------------------------+-------------+--------------+--------+-------------------+\n\t\t\t|            ID            |    NAME     |     EMAIL    | STATUS |     TIMEZONE      |\n\t\t\t+--------------------------+-------------+--------------+--------+-------------------+\n\t\t\t| ffffffffffffffffffffffff | John JD Due | due@john.net | ACTIVE | America/Sao_Paulo |\n\t\t\t+--------------------------+-------------+--------------+--------+-------------------+\n\n\t\t\t$ %[1]s --quiet\n\t\t\tffffffffffffffffffffffff\n\n\t\t\t$ %[1]s --format \"{{ .Name }} ({{ .Email }})\"\n\t\t\tJohn JD Due (due@john.net)\n\t\t`, \"clockify-cli user me\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tu, err := c.GetMe()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tout := cmd.OutOrStdout()\n\t\t\tif report != nil {\n\t\t\t\treturn report(out, &of, u)\n\t\t\t}\n\n\t\t\tif of.JSON {\n\t\t\t\treturn user.UserJSONPrint(u, out)\n\t\t\t}\n\n\t\t\treturn util.Report([]dto.User{u}, out, of)\n\t\t},\n\t}\n\n\tutil.AddReportFlags(cmd, &of)\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/user/me/me_test.go",
    "content": "package me_test\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/user/me\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/user/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype report func(io.Writer, *util.OutputFlags, dto.User) error\n\nfunc TestCmdMe(t *testing.T) {\n\tdefReport := func(t *testing.T) report {\n\t\treturn func(io.Writer, *util.OutputFlags, dto.User) error {\n\t\t\tt.Error(\"should not report users\")\n\t\t\treturn nil\n\t\t}\n\t}\n\ttts := []struct {\n\t\tname    string\n\t\targs    []string\n\t\tfactory func(*testing.T) (cmdutil.Factory, report)\n\t\terr     string\n\t}{\n\t\t{\n\t\t\tname: \"only one format\",\n\t\t\targs: []string{\"--format={}\", \"-q\", \"-j\"},\n\t\t\terr:  \"flags can't be used together.*format.*json.*quiet\",\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\treturn mocks.NewMockFactory(t), defReport(t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"client error\",\n\t\t\terr:  \"client error\",\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"Client\").Return(nil, errors.New(\"client error\"))\n\t\t\t\treturn f, defReport(t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"http error\",\n\t\t\terr:  \"http error\",\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tc.On(\"GetMe\").\n\t\t\t\t\tReturn(dto.User{}, errors.New(\"http error\"))\n\t\t\t\treturn f, defReport(t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report json\",\n\t\t\targs: []string{\"--json\"},\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tr := dto.User{Email: \"john@due.com\"}\n\t\t\t\tc.On(\"GetMe\").Return(r, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\t\treturn f, func(\n\t\t\t\t\t_ io.Writer, of *util.OutputFlags, u dto.User) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, r, u)\n\t\t\t\t\tassert.True(t, of.JSON)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report quiet\",\n\t\t\targs: []string{\"-q\"},\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tr := dto.User{Email: \"john@due.com\"}\n\t\t\t\tc.On(\"GetMe\").Return(r, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\t\treturn f, func(\n\t\t\t\t\t_ io.Writer, of *util.OutputFlags, u dto.User) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, r, u)\n\t\t\t\t\tassert.True(t, of.Quiet)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report format\",\n\t\t\targs: []string{\"--format={{.Email}}\"},\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tr := dto.User{Email: \"john@due.com\"}\n\t\t\t\tc.On(\"GetMe\").Return(r, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\t\treturn f, func(\n\t\t\t\t\t_ io.Writer, of *util.OutputFlags, u dto.User) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, r, u)\n\t\t\t\t\tassert.Equal(t, of.Format, \"{{.Email}}\")\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcmd := me.NewCmdMe(tt.factory(t))\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/user/user.go",
    "content": "package user\n\nimport (\n\t\"io\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/user/me\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/user/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdUser represents the users command\nfunc NewCmdUser(\n\tf cmdutil.Factory,\n\treport func(io.Writer, *util.OutputFlags, []dto.User) error,\n) *cobra.Command {\n\tof := util.OutputFlags{}\n\tcmd := &cobra.Command{\n\t\tUse:     \"user\",\n\t\tAliases: []string{\"users\"},\n\t\tShort:   \"List users of a workspace\",\n\t\tExample: heredoc.Docf(`\n\t\t\t$ %[1]s\n\t\t\t+--------------------------+-------------+--------------+--------+-------------------+\n\t\t\t|            ID            |    NAME     |     EMAIL    | STATUS |     TIMEZONE      |\n\t\t\t+--------------------------+-------------+--------------+--------+-------------------+\n\t\t\t| eeeeeeeeeeeeeeeeeeeeeeee | John Due    | john@due.net | ACTIVE | America/Sao_Paulo |\n\t\t\t| ffffffffffffffffffffffff | John JD Due | due@john.net | ACTIVE | America/Sao_Paulo |\n\t\t\t+--------------------------+-------------+--------------+--------+-------------------+\n\n\t\t\t$ %[1]s --quiet\n\t\t\teeeeeeeeeeeeeeeeeeeeeeee\n\t\t\tffffffffffffffffffffffff\n\n\t\t\t$ %[1]s --email due@john.net\n\t\t\t+--------------------------+-------------+--------------+--------+-------------------+\n\t\t\t|            ID            |    NAME     |     EMAIL    | STATUS |     TIMEZONE      |\n\t\t\t+--------------------------+-------------+--------------+--------+-------------------+\n\t\t\t| ffffffffffffffffffffffff | John JD Due | due@john.net | ACTIVE | America/Sao_Paulo |\n\t\t\t+--------------------------+-------------+--------------+--------+-------------------+\n\n\t\t\t$ %[1]s me --format \"{{ .Name }} ({{ .Email }})\" --email due@john.net\n\t\t\tJohn JD Due (due@john.net)\n\t\t`, \"clockify-cli user\"),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\temail, _ := cmd.Flags().GetString(\"email\")\n\t\t\tif err := of.Check(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tw, err := f.GetWorkspaceID()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tusers, err := c.WorkspaceUsers(api.WorkspaceUsersParam{\n\t\t\t\tWorkspace:       w,\n\t\t\t\tEmail:           email,\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif report != nil {\n\t\t\t\treturn report(cmd.OutOrStderr(), &of, users)\n\t\t\t}\n\n\t\t\treturn util.Report(users, cmd.OutOrStderr(), of)\n\t\t},\n\t}\n\n\tcmd.Flags().StringP(\"email\", \"e\", \"\",\n\t\t\"will be used to filter the workspaces by email\")\n\n\tutil.AddReportFlags(cmd, &of)\n\n\t_ = cmd.MarkFlagRequired(\"workspace\")\n\n\tcmd.AddCommand(me.NewCmdMe(f, nil))\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/user/user_test.go",
    "content": "package user_test\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/user\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/user/util\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype report func(io.Writer, *util.OutputFlags, []dto.User) error\n\nfunc TestCmdUser(t *testing.T) {\n\tdefReport := func(t *testing.T) report {\n\t\treturn func(io.Writer, *util.OutputFlags, []dto.User) error {\n\t\t\tt.Error(\"should not report users\")\n\t\t\treturn nil\n\t\t}\n\t}\n\ttts := []struct {\n\t\tname    string\n\t\targs    []string\n\t\tfactory func(*testing.T) (cmdutil.Factory, report)\n\t\terr     string\n\t}{\n\t\t{\n\t\t\tname: \"only one format\",\n\t\t\targs: []string{\"--format={}\", \"-q\", \"-j\"},\n\t\t\terr:  \"flags can't be used together.*format.*json.*quiet\",\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\treturn mocks.NewMockFactory(t), defReport(t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"client error\",\n\t\t\terr:  \"client error\",\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"Client\").Return(nil, errors.New(\"client error\"))\n\t\t\t\treturn f, defReport(t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workspace error\",\n\t\t\terr:  \"workspace error\",\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"Client\").Return(mocks.NewMockClient(t), nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"\", errors.New(\"workspace error\"))\n\t\t\t\treturn f, defReport(t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"http error\",\n\t\t\terr:  \"http error\",\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\t\t\t\tc.On(\"WorkspaceUsers\", api.WorkspaceUsersParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.User{}, errors.New(\"http error\"))\n\t\t\t\treturn f, defReport(t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report quiet\",\n\t\t\targs: []string{\"--email=john@due.com\", \"-q\"},\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tlist := []dto.User{{Email: \"john@due.com\"}}\n\t\t\t\tc.On(\"WorkspaceUsers\", api.WorkspaceUsersParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tEmail:           \"john@due.com\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(list, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\t\treturn f, func(\n\t\t\t\t\t_ io.Writer, of *util.OutputFlags, u []dto.User) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, list, u)\n\t\t\t\t\tassert.True(t, of.Quiet)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report json\",\n\t\t\targs: []string{\"--json\"},\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tlist := []dto.User{{Email: \"john@due.com\"}}\n\t\t\t\tc.On(\"WorkspaceUsers\", api.WorkspaceUsersParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(list, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\t\treturn f, func(\n\t\t\t\t\t_ io.Writer, of *util.OutputFlags, u []dto.User) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, list, u)\n\t\t\t\t\tassert.True(t, of.JSON)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"report format\",\n\t\t\targs: []string{\"--format={{.Name}}\"},\n\t\t\tfactory: func(t *testing.T) (cmdutil.Factory, report) {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\t\t\t\tf.On(\"GetWorkspaceID\").\n\t\t\t\t\tReturn(\"w\", nil)\n\n\t\t\t\tlist := []dto.User{{Email: \"john@due.com\"}}\n\t\t\t\tc.On(\"WorkspaceUsers\", api.WorkspaceUsersParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn(list, nil)\n\n\t\t\t\tcalled := false\n\t\t\t\tt.Cleanup(func() { assert.True(t, called, \"was not called\") })\n\t\t\t\treturn f, func(\n\t\t\t\t\t_ io.Writer, of *util.OutputFlags, u []dto.User) error {\n\t\t\t\t\tcalled = true\n\t\t\t\t\tassert.Equal(t, list, u)\n\t\t\t\t\tassert.Equal(t, \"{{.Name}}\", of.Format)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcmd := user.NewCmdUser(tt.factory(t))\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tassert.Regexp(t, tt.err, err.Error())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/user/util/util.go",
    "content": "package util\n\nimport (\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/output/user\"\n\t\"github.com/spf13/cobra\"\n)\n\n// OutputFlags sets how to print out a list of users\ntype OutputFlags struct {\n\tFormat string\n\tJSON   bool\n\tQuiet  bool\n}\n\nfunc (of OutputFlags) Check() error {\n\treturn cmdutil.XorFlag(map[string]bool{\n\t\t\"format\": of.Format != \"\",\n\t\t\"json\":   of.JSON,\n\t\t\"quiet\":  of.Quiet,\n\t})\n}\n\n// AddReportFlags adds the default output flags for users\nfunc AddReportFlags(cmd *cobra.Command, of *OutputFlags) {\n\tcmd.Flags().StringVarP(&of.Format, \"format\", \"f\", \"\",\n\t\t\"golang text/template format to be applied on each workspace\")\n\tcmd.Flags().BoolVarP(&of.Quiet, \"quiet\", \"q\", false, \"only display ids\")\n\tcmd.Flags().BoolVarP(&of.JSON, \"json\", \"j\", false, \"print as json\")\n}\n\n// Report prints out the users\nfunc Report(u []dto.User, out io.Writer, of OutputFlags) error {\n\tswitch {\n\tcase of.Format != \"\":\n\t\treturn user.UserPrintWithTemplate(of.Format)(u, out)\n\tcase of.Quiet:\n\t\treturn user.UserPrintQuietly(u, out)\n\tdefault:\n\t\treturn user.UserPrint(u, out)\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/version/version.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdVersion represents the version command\nfunc NewCmdVersion(f cmdutil.Factory) *cobra.Command {\n\treturn &cobra.Command{\n\t\tUse:   \"version\",\n\t\tShort: \"Shows the CLI version\",\n\t\tRunE: func(cmd *cobra.Command, _ []string) error {\n\t\t\tv := f.Version()\n\t\t\t_, err := fmt.Fprintln(cmd.OutOrStdout(),\n\t\t\t\t\"Version: \"+v.Tag+\", Commit: \"+v.Commit+\", Build At: \"+v.Date,\n\t\t\t)\n\n\t\t\treturn err\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/version/version_test.go",
    "content": "package version_test\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/version\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestVersion(t *testing.T) {\n\ttype c struct {\n\t\tname     string\n\t\tversion  cmdutil.Version\n\t\texpected string\n\t}\n\tcases := []c{\n\t\tc{\n\t\t\tname:     \"when no version\",\n\t\t\tversion:  cmdutil.Version{},\n\t\t\texpected: \"Version: , Commit: , Build At:\",\n\t\t},\n\t\tc{\n\t\t\tname: \"when default version\",\n\t\t\tversion: cmdutil.Version{\n\t\t\t\tTag: \"dev\", Commit: \"none\", Date: \"unknown\"},\n\t\t\texpected: \"Version: dev, Commit: none, Build At: unknown\",\n\t\t},\n\t\tc{\n\t\t\tname: \"with valid version\",\n\t\t\tversion: cmdutil.Version{\n\t\t\t\tTag:    \"1.0.0\",\n\t\t\t\tCommit: \"63595df3d0dd6e4eef2e973631013ca9e06928ef\",\n\t\t\t\tDate:   \"2022-07-01T04:10:47Z\"},\n\t\t\texpected: \"Version: 1.0.0, \" +\n\t\t\t\t\"Commit: 63595df3d0dd6e4eef2e973631013ca9e06928ef, \" +\n\t\t\t\t\"Build At: 2022-07-01T04:10:47Z\",\n\t\t},\n\t}\n\n\tfor _, tt := range cases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := mocks.NewMockFactory(t)\n\t\t\tf.On(\"Version\").Return(tt.version)\n\n\t\t\tcmd := version.NewCmdVersion(f)\n\t\t\tcmd.SetArgs([]string{})\n\n\t\t\tb := bytes.NewBufferString(\"\")\n\t\t\tcmd.SetOut(b)\n\t\t\tcmd.SetErr(b)\n\n\t\t\t_, err := cmd.ExecuteC()\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, strings.TrimSpace(b.String()))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmd/workspace/workspace.go",
    "content": "package workspace\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\toutput \"github.com/lucassabreu/clockify-cli/pkg/output/workspace\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewCmdWorkspace represents the workspaces command\nfunc NewCmdWorkspace(f cmdutil.Factory) *cobra.Command {\n\tfl := struct {\n\t\tname   string\n\t\tformat string\n\t\tquiet  bool\n\t}{}\n\tcmd := &cobra.Command{\n\t\tUse:     \"workspace\",\n\t\tAliases: []string{\"workspaces\"},\n\t\tShort:   \"List your available workspaces\",\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tif err := cmdutil.XorFlag(map[string]bool{\n\t\t\t\t\"format\": fl.format != \"\",\n\t\t\t\t\"quiet\":  fl.quiet,\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc, err := f.Client()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tlist, err := c.GetWorkspaces(api.GetWorkspaces{\n\t\t\t\tName: fl.name,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif fl.quiet {\n\t\t\t\treturn output.WorkspacePrintQuietly(list, cmd.OutOrStdout())\n\t\t\t}\n\n\t\t\tif fl.format != \"\" {\n\t\t\t\treturn output.WorkspacePrintWithTemplate(fl.format)(\n\t\t\t\t\tlist, cmd.OutOrStdout())\n\t\t\t}\n\n\t\t\tw, _ := f.GetWorkspaceID()\n\t\t\treturn output.WorkspacePrint(w)(list, cmd.OutOrStdout())\n\t\t},\n\t}\n\n\tcmd.Flags().StringVarP(&fl.name, \"name\", \"n\", \"\",\n\t\t\"will be used to filter the workspaces by name\")\n\tcmd.Flags().StringVarP(&fl.format, \"format\", \"f\", \"\",\n\t\t\"golang text/template format to be applied on each workspace\")\n\tcmd.Flags().BoolVarP(&fl.quiet, \"quiet\", \"q\", false, \"only display ids\")\n\n\treturn cmd\n}\n"
  },
  {
    "path": "pkg/cmd/workspace/workspace_test.go",
    "content": "package workspace_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmd/workspace\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCmdWorkspaces(t *testing.T) {\n\ttts := []struct {\n\t\tname     string\n\t\targs     []string\n\t\tfactory  func(*testing.T) cmdutil.Factory\n\t\terr      string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"only_format_or_quiet\",\n\t\t\targs: []string{\"-q\", \"--format\", \"{}\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\treturn mocks.NewMockFactory(t)\n\t\t\t},\n\t\t\terr: \"the following flags can't be used together: \" +\n\t\t\t\t\"`format` and `quiet`\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid_client\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.On(\"Client\").Return(nil, errors.New(\"no client\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t\terr: \"no client\",\n\t\t},\n\t\t{\n\t\t\tname: \"quiet\",\n\t\t\targs: []string{\"-q\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetWorkspaces\", api.GetWorkspaces{}).Return(\n\t\t\t\t\t[]dto.Workspace{\n\t\t\t\t\t\t{ID: \"w1\"},\n\t\t\t\t\t\t{ID: \"w2\"},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\tw1\n\t\t\t\tw2\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"failed to query\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetWorkspaces\", api.GetWorkspaces{}).\n\t\t\t\t\tReturn(nil, errors.New(\"failed querying\"))\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\terr: \"failed querying\",\n\t\t},\n\t\t{\n\t\t\tname: \"format\",\n\t\t\targs: []string{\"--format\", \"ID: {{.ID}} | Name: {{ .Name }}\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetWorkspaces\", api.GetWorkspaces{}).Return(\n\t\t\t\t\t[]dto.Workspace{\n\t\t\t\t\t\t{ID: \"w1\", Name: \"first\"},\n\t\t\t\t\t\t{ID: \"w2\", Name: \"last\"},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\texpected: heredoc.Doc(`\n\t\t\t\tID: w1 | Name: first\n\t\t\t\tID: w2 | Name: last\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"default\",\n\t\t\targs: []string{\"--name\", \"second\"},\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.On(\"Client\").Return(c, nil)\n\n\t\t\t\tc.On(\"GetWorkspaces\", api.GetWorkspaces{\n\t\t\t\t\tName: \"second\",\n\t\t\t\t}).Return(\n\t\t\t\t\t[]dto.Workspace{\n\t\t\t\t\t\t{ID: \"w1\", Name: \"first\"},\n\t\t\t\t\t\t{ID: \"w2\", Name: \"last\"},\n\t\t\t\t\t},\n\t\t\t\t\tnil,\n\t\t\t\t)\n\n\t\t\t\tf.On(\"GetWorkspaceID\").Return(\"first\", nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\texpected: \".*first.*\\n.*last.*\",\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcmd := workspace.NewCmdWorkspace(tt.factory(t))\n\t\t\tb := bytes.NewBufferString(\"\")\n\n\t\t\tcmd.SetOut(b)\n\n\t\t\tcmd.SilenceUsage = true\n\t\t\tcmd.SilenceErrors = true\n\n\t\t\tcmd.SetArgs(tt.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\t\t\tif tt.err != \"\" {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.EqualError(t, err, tt.err)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Regexp(t, tt.expected, b.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmdcompl/flags.go",
    "content": "package cmdcompl\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\n// AddFixedSuggestionsToFlag add fixed suggestions to a flag\nfunc AddFixedSuggestionsToFlag(cmd *cobra.Command, flagName string, va ValidArgs) error {\n\tf := cmd.Flags().Lookup(flagName)\n\tif f == nil {\n\t\tf = cmd.PersistentFlags().Lookup(flagName)\n\t}\n\n\tf.Usage = va.IntoUse() + \" \" + f.Usage\n\treturn cmd.RegisterFlagCompletionFunc(\n\t\tf.Name,\n\t\tfunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\t\treturn va.IntoValidArgs(), cobra.ShellCompDirectiveDefault\n\t\t},\n\t)\n}\n\n// SuggestFn is a function to search valid suggestions for a flag/argument\ntype SuggestFn func(cmd *cobra.Command, args []string, toComplete string) (ValidArgs, error)\n\nfunc process(va ValidArgs, err error) ([]string, cobra.ShellCompDirective) {\n\tif err != nil {\n\t\tcobra.CompError(err.Error())\n\t\treturn []string{}, cobra.ShellCompDirectiveError\n\t}\n\n\treturn va.IntoValidArgs(), cobra.ShellCompDirectiveDefault\n}\n\n// AddSuggestionsToFlag add fixed suggestions to a flag\nfunc AddSuggestionsToFlag(cmd *cobra.Command, flagName string, suggestFn SuggestFn) error {\n\treturn cmd.RegisterFlagCompletionFunc(\n\t\tflagName,\n\t\tfunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\t\treturn process(suggestFn(cmd, args, toComplete))\n\t\t},\n\t)\n}\n\nfunc EmptySuggestionFuncion(_ *cobra.Command, _ []string, _ string) (ValidArgs, error) {\n\treturn EmptyValidArgs(), nil\n}\n\n// CombineSuggestionsToArgs combine one or more suggestion resolver functions and call then accordingly with arg count\nfunc CombineSuggestionsToArgs(fns ...SuggestFn) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\treturn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif len(args) > len(fns) {\n\t\t\treturn []string{}, cobra.ShellCompDirectiveDefault\n\t\t}\n\n\t\treturn process(fns[len(args)](cmd, args, toComplete))\n\t}\n}\n"
  },
  {
    "path": "pkg/cmdcompl/valid-args.go",
    "content": "package cmdcompl\n\nimport (\n\t\"sort\"\n\t\"strings\"\n)\n\ntype ValidArgs interface {\n\t// IntoUse will return a string with a complete arg use\n\t// Example: \"{ arg1 | arg2 | arg3 }\"\n\tIntoUse() string\n\t// IntoUse will return a string with the joined options\n\t// Example: \"arg1 | arg2 | arg3\"\n\tIntoUseOptions() string\n\t// OnlyArgs will return a []string to be used on cobra.Command.ValidArgs\n\tOnlyArgs() []string\n\t// OnlyArgs will return a []string to be used as result for auto-complete\n\tIntoValidArgs() []string\n}\n\n// EmptyValidArgs returns a ValidArgs with no options\nfunc EmptyValidArgs() ValidArgs {\n\treturn new(ValidArgsSlide)\n}\n\ntype ValidArgsMap map[string]string\n\nfunc (va ValidArgsMap) Set(k, v string) ValidArgsMap {\n\tva[k] = v\n\treturn va\n}\n\nfunc (va ValidArgsMap) OnlyArgs() []string {\n\tkeys := make([]string, len(va))\n\ti := 0\n\tfor k := range va {\n\t\tkeys[i] = k\n\t\ti++\n\t}\n\n\tsort.Strings(keys)\n\treturn keys\n}\n\nfunc (va ValidArgsMap) IntoUseOptions() string {\n\treturn strings.Join(va.OnlyArgs(), \" | \")\n}\n\nfunc (va ValidArgsMap) IntoUse() string {\n\treturn \"{ \" + va.IntoUseOptions() + \" }\"\n}\n\nfunc (va ValidArgsMap) IntoValidArgs() []string {\n\tvar args []string\n\tfor k, v := range va {\n\t\targs = append(args, k+\"\\t\"+v)\n\t}\n\treturn args\n}\n\nfunc (va ValidArgsMap) Long() string {\n\tstr := \"\"\n\tfor _, k := range va.OnlyArgs() {\n\t\tstr = str + \" - \" + k + \": \" + va[k] + \"\\n\"\n\t}\n\n\treturn str\n}\n\ntype ValidArgsSlide []string\n\nfunc (va ValidArgsSlide) IntoUseOptions() string {\n\treturn strings.Join(va, \" | \")\n}\n\nfunc (va ValidArgsSlide) IntoUse() string {\n\treturn \"{ \" + va.IntoUseOptions() + \" }\"\n}\n\nfunc (va ValidArgsSlide) IntoValidArgs() []string {\n\treturn va\n}\n\nfunc (va ValidArgsSlide) OnlyArgs() []string {\n\treturn va\n}\n"
  },
  {
    "path": "pkg/cmdcomplutil/client.go",
    "content": "package cmdcomplutil\n\nimport (\n\t\"strings\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewClientAutoComplete will provide auto-completion to flags or args\nfunc NewClientAutoComplete(f factory) cmdcompl.SuggestFn {\n\treturn func(\n\t\tcmd *cobra.Command, args []string, toComplete string,\n\t) (cmdcompl.ValidArgs, error) {\n\t\tw, err := f.GetWorkspaceID()\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tc, err := f.Client()\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tb := false\n\t\tclients, err := c.GetClients(api.GetClientsParam{\n\t\t\tWorkspace:       w,\n\t\t\tArchived:        &b,\n\t\t\tPaginationParam: api.AllPages(),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tva := make(cmdcompl.ValidArgsMap)\n\t\ttoComplete = strings.ToLower(toComplete)\n\t\tfor _, client := range clients {\n\t\t\tif toComplete != \"\" && !strings.Contains(client.ID, toComplete) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tva.Set(client.ID, client.Name)\n\t\t}\n\n\t\treturn va, nil\n\t}\n}\n"
  },
  {
    "path": "pkg/cmdcomplutil/factory.go",
    "content": "package cmdcomplutil\n\nimport \"github.com/lucassabreu/clockify-cli/api\"\n\ntype config interface {\n\tIsAllowNameForID() bool\n\tIsSearchProjectWithClientsName() bool\n}\n\ntype factory interface {\n\tClient() (api.Client, error)\n\tGetWorkspaceID() (string, error)\n}\n"
  },
  {
    "path": "pkg/cmdcomplutil/project.go",
    "content": "package cmdcomplutil\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewProjectAutoComplete will provide auto-completion to flags or args\nfunc NewProjectAutoComplete(f factory, config config) cmdcompl.SuggestFn {\n\treturn func(\n\t\tcmd *cobra.Command, args []string, toComplete string,\n\t) (cmdcompl.ValidArgs, error) {\n\t\tw, err := f.GetWorkspaceID()\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tc, err := f.Client()\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tb := false\n\t\tps, err := c.GetProjects(api.GetProjectsParam{\n\t\t\tWorkspace:       w,\n\t\t\tArchived:        &b,\n\t\t\tPaginationParam: api.AllPages(),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tfilter := makeFilter(toComplete, config)\n\n\t\tpsf := make([]dto.Project, 0)\n\t\tpadding := 0\n\t\tfor i := range ps {\n\t\t\tif !filter(ps[i]) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif padding < len(ps[i].Name) {\n\t\t\t\tpadding = len(ps[i].Name)\n\t\t\t}\n\n\t\t\tpsf = append(psf, ps[i])\n\t\t}\n\n\t\tformat := func(p dto.Project) string { return p.Name }\n\t\tif config.IsSearchProjectWithClientsName() {\n\t\t\tf := fmt.Sprintf(\"%%-%ds\", padding)\n\t\t\tformat = func(p dto.Project) string {\n\t\t\t\tclient := \"Without Client\"\n\t\t\t\tif p.ClientID != \"\" {\n\t\t\t\t\tclient = p.ClientID + \" -- \" + p.ClientName\n\t\t\t\t}\n\t\t\t\treturn fmt.Sprintf(f, p.Name) + \" | \" + client\n\t\t\t}\n\t\t}\n\n\t\tva := make(cmdcompl.ValidArgsMap)\n\t\tfor i := range psf {\n\t\t\tva.Set(psf[i].ID, format(psf[i]))\n\t\t}\n\n\t\treturn va, nil\n\t}\n}\n\nfunc makeFilter(toComplete string, config config) func(dto.Project) bool {\n\tif toComplete == \"\" {\n\t\treturn func(_ dto.Project) bool { return true }\n\t}\n\n\tif config.IsAllowNameForID() &&\n\t\tconfig.IsSearchProjectWithClientsName() {\n\t\ts := strhlp.IsSimilar(toComplete)\n\t\treturn func(p dto.Project) bool {\n\t\t\treturn strings.Contains(p.ID, toComplete) || s(p.Name) ||\n\t\t\t\tstrings.Contains(p.ClientID, toComplete) || s(p.ClientName)\n\t\t}\n\t}\n\n\tif config.IsAllowNameForID() {\n\t\ts := strhlp.IsSimilar(toComplete)\n\t\treturn func(p dto.Project) bool {\n\t\t\treturn strings.Contains(p.ID, toComplete) || s(p.Name)\n\t\t}\n\t}\n\n\treturn func(p dto.Project) bool {\n\t\treturn strings.Contains(p.ID, toComplete)\n\t}\n}\n"
  },
  {
    "path": "pkg/cmdcomplutil/project_test.go",
    "content": "package cmdcomplutil_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/internal/mocks\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n)\n\nfunc TestNewProjectAutoComplete(t *testing.T) {\n\tbFalse := false\n\ttts := []struct {\n\t\tname       string\n\t\ttoComplete string\n\t\tfactory    func(t *testing.T) cmdutil.Factory\n\t\terr        string\n\t\targs       cmdcompl.ValidArgs\n\t}{\n\t\t{\n\t\t\tname: \"no workspace, nothing\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.EXPECT().GetWorkspaceID().\n\t\t\t\t\tReturn(\"\", errors.New(\"no workspace\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t\terr:  \"no workspace\",\n\t\t\targs: cmdcompl.EmptyValidArgs(),\n\t\t},\n\t\t{\n\t\t\tname: \"no client, nothing\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.EXPECT().GetWorkspaceID().Return(\"w\", nil)\n\n\t\t\t\tf.EXPECT().Client().Return(nil, errors.New(\"no client\"))\n\t\t\t\treturn f\n\t\t\t},\n\t\t\terr:  \"no client\",\n\t\t\targs: cmdcompl.EmptyValidArgs(),\n\t\t},\n\t\t{\n\t\t\tname: \"fail get projects, nothing\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.EXPECT().GetWorkspaceID().Return(\"w\", nil)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tc.EXPECT().GetProjects(mock.Anything).\n\t\t\t\t\tReturn(nil, errors.New(\"fail to request\"))\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\terr:  \"fail to request\",\n\t\t\targs: cmdcompl.EmptyValidArgs(),\n\t\t},\n\t\t{\n\t\t\tname: \"all projects\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.EXPECT().GetWorkspaceID().Return(\"w\", nil)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tArchived:        &bFalse,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{\n\t\t\t\t\t\t{ID: \"p1\", Name: \"Project 1\"},\n\t\t\t\t\t\t{ID: \"p2\", Name: \"Project 2\"},\n\t\t\t\t\t\t{ID: \"p3\", Name: \"Project 3\"},\n\t\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\targs: cmdcompl.ValidArgsMap{\n\t\t\t\t\"p1\": \"Project 1\",\n\t\t\t\t\"p2\": \"Project 2\",\n\t\t\t\t\"p3\": \"Project 3\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"only projects with id with cat\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{})\n\t\t\t\tf.EXPECT().GetWorkspaceID().Return(\"w\", nil)\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tArchived:        &bFalse,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{\n\t\t\t\t\t\t{ID: \"p0dog\", Name: \"Project 0\"},\n\t\t\t\t\t\t{ID: \"pcat\", Name: \"Project 1\"},\n\t\t\t\t\t\t{ID: \"catp\", Name: \"Project 2\"},\n\t\t\t\t\t\t{ID: \"pcatp\", Name: \"Project 3\"},\n\t\t\t\t\t\t{ID: \"p4\", Name: \"Project 4\"},\n\t\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\ttoComplete: \"cat\",\n\t\t\targs: cmdcompl.ValidArgsMap{\n\t\t\t\t\"pcat\":  \"Project 1\",\n\t\t\t\t\"catp\":  \"Project 2\",\n\t\t\t\t\"pcatp\": \"Project 3\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"only projects with id or name with cat\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().Return(\"w\", nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID: true,\n\t\t\t\t})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tArchived:        &bFalse,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{\n\t\t\t\t\t\t{ID: \"p0dog\", Name: \"Project 0\"},\n\t\t\t\t\t\t{ID: \"pcat\", Name: \"Project 1\"},\n\t\t\t\t\t\t{ID: \"catp\", Name: \"Project 2\"},\n\t\t\t\t\t\t{ID: \"pcatp\", Name: \"Project 3\"},\n\t\t\t\t\t\t{ID: \"p4\", Name: \"Project 4\"},\n\t\t\t\t\t\t{ID: \"p5\", Name: \"Project Cat 5\"},\n\t\t\t\t\t\t{ID: \"p6\", Name: \"CAT Project 6\"},\n\t\t\t\t\t\t{ID: \"p7\", Name: \"Project 7 cats\"},\n\t\t\t\t\t\t{ID: \"p8\", Name: \"Catalog\"},\n\t\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\ttoComplete: \"cat\",\n\t\t\targs: cmdcompl.ValidArgsMap{\n\t\t\t\t\"pcat\":  \"Project 1\",\n\t\t\t\t\"catp\":  \"Project 2\",\n\t\t\t\t\"pcatp\": \"Project 3\",\n\t\t\t\t\"p5\":    \"Project Cat 5\",\n\t\t\t\t\"p6\":    \"CAT Project 6\",\n\t\t\t\t\"p7\":    \"Project 7 cats\",\n\t\t\t\t\"p8\":    \"Catalog\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"only projects where the client has id or name with cat\",\n\t\t\tfactory: func(t *testing.T) cmdutil.Factory {\n\t\t\t\tf := mocks.NewMockFactory(t)\n\t\t\t\tf.EXPECT().GetWorkspaceID().Return(\"w\", nil)\n\n\t\t\t\tf.EXPECT().Config().Return(&mocks.SimpleConfig{\n\t\t\t\t\tAllowNameForID:               true,\n\t\t\t\t\tSearchProjectWithClientsName: true,\n\t\t\t\t})\n\n\t\t\t\tc := mocks.NewMockClient(t)\n\t\t\t\tf.EXPECT().Client().Return(c, nil)\n\n\t\t\t\tc.EXPECT().GetProjects(api.GetProjectsParam{\n\t\t\t\t\tWorkspace:       \"w\",\n\t\t\t\t\tArchived:        &bFalse,\n\t\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t\t}).\n\t\t\t\t\tReturn([]dto.Project{\n\t\t\t\t\t\t{ID: \"p1\", Name: \"Project 1\"},\n\t\t\t\t\t\t{ID: \"p2\", Name: \"CAT Project\"},\n\t\t\t\t\t\t{ID: \"p3\", Name: \"Catalog\"},\n\t\t\t\t\t\t{ID: \"p4\", Name: \"Project\",\n\t\t\t\t\t\t\tClientID: \"c10\", ClientName: \"Cats\"},\n\t\t\t\t\t\t{ID: \"p5\", Name: \"Project\",\n\t\t\t\t\t\t\tClientID: \"cat\", ClientName: \"Client\"},\n\t\t\t\t\t\t{ID: \"p6\", Name: \"Project\",\n\t\t\t\t\t\t\tClientID: \"c30\", ClientName: \"Client\"},\n\t\t\t\t\t}, nil)\n\n\t\t\t\treturn f\n\t\t\t},\n\t\t\ttoComplete: \"cat\",\n\t\t\targs: cmdcompl.ValidArgsMap{\n\t\t\t\t\"p2\": \"CAT Project | Without Client\",\n\t\t\t\t\"p3\": \"Catalog     | Without Client\",\n\t\t\t\t\"p4\": \"Project     | c10 -- Cats\",\n\t\t\t\t\"p5\": \"Project     | cat -- Client\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\ttt := tts[i]\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tf := tt.factory(t)\n\t\t\tautoComplete := cmdcomplutil.NewProjectAutoComplete(\n\t\t\t\tf, f.Config())\n\n\t\t\targs, err := autoComplete(\n\t\t\t\t&cobra.Command{}, []string{}, tt.toComplete)\n\n\t\t\tif tt.err == \"\" && !assert.NoError(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.err != \"\" && (!assert.Error(t, err) || !assert.Regexp(\n\t\t\t\tt, tt.err, err.Error())) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.args, args)\n\t\t})\n\t}\n\n}\n"
  },
  {
    "path": "pkg/cmdcomplutil/tag.go",
    "content": "package cmdcomplutil\n\nimport (\n\t\"strings\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewTagAutoComplete will provide auto-completion to flags or args\nfunc NewTagAutoComplete(f factory) cmdcompl.SuggestFn {\n\treturn func(\n\t\tcmd *cobra.Command, args []string, toComplete string,\n\t) (cmdcompl.ValidArgs, error) {\n\t\tw, err := f.GetWorkspaceID()\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tc, err := f.Client()\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tb := false\n\t\tprojects, err := c.GetTags(api.GetTagsParam{\n\t\t\tWorkspace:       w,\n\t\t\tArchived:        &b,\n\t\t\tPaginationParam: api.AllPages(),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tva := make(cmdcompl.ValidArgsMap)\n\t\ttoComplete = strings.ToLower(toComplete)\n\t\tfor _, e := range projects {\n\t\t\tif toComplete != \"\" && !strings.Contains(e.ID, toComplete) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tva.Set(e.ID, e.Name)\n\t\t}\n\n\t\treturn va, nil\n\t}\n}\n"
  },
  {
    "path": "pkg/cmdcomplutil/task.go",
    "content": "package cmdcomplutil\n\nimport (\n\t\"strings\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewTaskAutoComplete will provide auto-completion to flags or args\nfunc NewTaskAutoComplete(f factory, onlyActive bool) cmdcompl.SuggestFn {\n\treturn func(\n\t\tcmd *cobra.Command, args []string, toComplete string,\n\t) (cmdcompl.ValidArgs, error) {\n\t\tproject, err := cmd.Flags().GetString(\"project\")\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tw, err := f.GetWorkspaceID()\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tc, err := f.Client()\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tts, err := c.GetTasks(api.GetTasksParam{\n\t\t\tWorkspace: w,\n\t\t\tProjectID: project,\n\t\t\tActive:    onlyActive,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tva := make(cmdcompl.ValidArgsMap)\n\t\ttoComplete = strings.ToLower(toComplete)\n\t\tfor i := range ts {\n\t\t\tif toComplete != \"\" && !strings.Contains(ts[i].ID, toComplete) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tva.Set(ts[i].ID, ts[i].Name)\n\t\t}\n\n\t\treturn va, nil\n\t}\n}\n"
  },
  {
    "path": "pkg/cmdcomplutil/user.go",
    "content": "package cmdcomplutil\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewUserAutoComplete will provice auto-completion for flags or args\nfunc NewUserAutoComplete(f factory) cmdcompl.SuggestFn {\n\treturn func(\n\t\tcmd *cobra.Command, args []string, toComplete string,\n\t) (cmdcompl.ValidArgs, error) {\n\t\tw, err := f.GetWorkspaceID()\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tc, err := f.Client()\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tus, err := c.WorkspaceUsers(api.WorkspaceUsersParam{\n\t\t\tWorkspace:       w,\n\t\t\tPaginationParam: api.AllPages(),\n\t\t})\n\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tva := make(cmdcompl.ValidArgsMap)\n\t\ttoComplete = strings.ToLower(toComplete)\n\t\tfor i := range us {\n\t\t\tif toComplete != \"\" && !strings.Contains(us[i].ID, toComplete) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tva.Set(us[i].ID, fmt.Sprintf(\"%s (%s)\", us[i].Name, us[i].Email))\n\t\t}\n\n\t\treturn va, nil\n\t}\n}\n"
  },
  {
    "path": "pkg/cmdcomplutil/workspace.go",
    "content": "package cmdcomplutil\n\nimport (\n\t\"strings\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewWorspaceAutoComplete will provice auto-completion for flags or args\nfunc NewWorspaceAutoComplete(f factory) cmdcompl.SuggestFn {\n\treturn func(\n\t\tcmd *cobra.Command, args []string, toComplete string,\n\t) (cmdcompl.ValidArgs, error) {\n\t\tc, err := f.Client()\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tws, err := c.GetWorkspaces(api.GetWorkspaces{})\n\n\t\tif err != nil {\n\t\t\treturn cmdcompl.EmptyValidArgs(), err\n\t\t}\n\n\t\tva := make(cmdcompl.ValidArgsMap)\n\t\ttoComplete = strings.ToLower(toComplete)\n\t\tfor i := range ws {\n\t\t\tif toComplete != \"\" && !strings.Contains(ws[i].ID, toComplete) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tva.Set(ws[i].ID, ws[i].Name)\n\t\t}\n\n\t\treturn va, nil\n\t}\n}\n"
  },
  {
    "path": "pkg/cmdutil/args.go",
    "content": "package cmdutil\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/cobra\"\n)\n\n// RequiredNamedArgs will fail if the number of arguments received is less than\n// the length of the parameter `names`, and will show the help explaining\n// required named arguments\nfunc RequiredNamedArgs(names ...string) cobra.PositionalArgs {\n\treturn func(cmd *cobra.Command, args []string) error {\n\t\tif len(args) >= len(names) {\n\t\t\treturn nil\n\t\t}\n\n\t\tif len(names) == 1 {\n\t\t\treturn FlagErrorWrap(errors.New(\"requires arg \" + names[0]))\n\t\t}\n\n\t\treturn FlagErrorWrap(errors.Errorf(\n\t\t\t\"requires args %s; %d of those received\",\n\t\t\tstrhlp.ListForHumans(names), len(args),\n\t\t))\n\t}\n}\n"
  },
  {
    "path": "pkg/cmdutil/args_test.go",
    "content": "package cmdutil_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRequiredNamedArgs(t *testing.T) {\n\ttt := []struct {\n\t\tname    string\n\t\tposArgs cobra.PositionalArgs\n\t\targs    []string\n\t\terr     error\n\t}{\n\t\t{\n\t\t\tname:    \"req one and none sent\",\n\t\t\tposArgs: cmdutil.RequiredNamedArgs(\"param1\"),\n\t\t\targs:    []string{},\n\t\t\terr:     errors.New(\"requires arg param1\"),\n\t\t},\n\t\t{\n\t\t\tname:    \"req two and none sent\",\n\t\t\tposArgs: cmdutil.RequiredNamedArgs(\"param1\", \"param2\"),\n\t\t\targs:    []string{},\n\t\t\terr: errors.New(\n\t\t\t\t\"requires args param1 and param2; 0 of those received\"),\n\t\t},\n\t\t{\n\t\t\tname:    \"req three and one sent\",\n\t\t\tposArgs: cmdutil.RequiredNamedArgs(\"param1\", \"param2\", \"param3\"),\n\t\t\targs:    []string{\"param1\"},\n\t\t\terr: errors.New(\n\t\t\t\t\"requires args param1, param2 and param3; 1 of those received\",\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname:    \"req one and one sent\",\n\t\t\tposArgs: cmdutil.RequiredNamedArgs(\"param1\"),\n\t\t\targs:    []string{\"param1\"},\n\t\t\terr:     nil,\n\t\t},\n\t\t{\n\t\t\tname:    \"req two and two sent\",\n\t\t\tposArgs: cmdutil.RequiredNamedArgs(\"param1\", \"param2\"),\n\t\t\targs:    []string{\"param1\", \"param2\"},\n\t\t\terr:     nil,\n\t\t},\n\t\t{\n\t\t\tname:    \"req one and two sent\",\n\t\t\tposArgs: cmdutil.RequiredNamedArgs(\"param1\"),\n\t\t\targs:    []string{\"param1\", \"param2\"},\n\t\t\terr:     nil,\n\t\t},\n\t}\n\n\tfor _, tc := range tt {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tcmd := cobra.Command{\n\t\t\t\tArgs: tc.posArgs,\n\t\t\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\t\t\tif tc.err != nil {\n\t\t\t\t\t\tt.Fatal(\"should not get here\")\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tcmd.SetArgs(tc.args)\n\n\t\t\t_, err := cmd.ExecuteC()\n\n\t\t\tif tc.err == nil {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar flagErr cmdutil.FlagError\n\t\t\tif !assert.Error(t, err) &&\n\t\t\t\tassert.ErrorAs(t, err, &flagErr) {\n\t\t\t\tassert.Equal(t, flagErr.Error(), tc.err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmdutil/config.go",
    "content": "package cmdutil\n\nimport (\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/mitchellh/go-homedir\"\n\t\"github.com/spf13/viper\"\n\t\"golang.org/x/text/language\"\n)\n\nconst (\n\tCONF_WORKWEEK_DAYS                    = \"workweek-days\"\n\tCONF_INTERACTIVE                      = \"interactive\"\n\tCONF_ALLOW_NAME_FOR_ID                = \"allow-name-for-id\"\n\tCONF_SEARCH_PROJECTS_WITH_CLIENT_NAME = \"search-project-with-client\"\n\tCONF_USER_ID                          = \"user.id\"\n\tCONF_WORKSPACE                        = \"workspace\"\n\tCONF_TOKEN                            = \"token\"\n\tCONF_ALLOW_INCOMPLETE                 = \"allow-incomplete\"\n\tCONF_SHOW_TASKS                       = \"show-task\"\n\tCONF_SHOW_CUSTOM_FIELDS               = \"show-custom-fields\"\n\tCONF_SHOW_CLIENT                      = \"show-client\"\n\tCONF_DESCR_AUTOCOMP                   = \"description-autocomplete\"\n\tCONF_DESCR_AUTOCOMP_DAYS              = \"description-autocomplete-days\"\n\tCONF_SHOW_TOTAL_DURATION              = \"show-total-duration\"\n\tCONF_LOG_LEVEL                        = \"log-level\"\n\tCONF_ALLOW_ARCHIVED_TAGS              = \"allow-archived-tags\"\n\tCONF_INTERACTIVE_PAGE_SIZE            = \"interactive-page-size\"\n\tCONF_LANGUAGE                         = \"lang\"\n\tCONF_TIMEZONE                         = \"time-zone\"\n\tCONF_API_URL                          = \"api-url\"\n)\n\nconst (\n\tLOG_LEVEL_NONE  = \"none\"\n\tLOG_LEVEL_DEBUG = \"debug\"\n\tLOG_LEVEL_INFO  = \"info\"\n)\n\n// Config manages configs and parameters used locally by the CLI\ntype Config interface {\n\t// GetBool retrieves a config by its name as a bool\n\tGetBool(string) bool\n\t// SetBool changes a bool config by its name\n\tSetBool(string, bool)\n\n\t// GetInt retrieves a config by its name as a int\n\tGetInt(string) int\n\t// SetInt changes a int config by its name\n\tSetInt(string, int)\n\n\t// GetString retrieves a config by its name as a string\n\tGetString(string) string\n\t// SetString changes a string config by its name\n\tSetString(string, string)\n\n\t// SetStringSlice retrieves a config by its name as a []string\n\tGetStringSlice(string) []string\n\t// SetStringSlice changes a []string config by its name\n\tSetStringSlice(string, []string)\n\n\t// IsDebuging configures CLI to log most of the data being used\n\tIsDebuging() bool\n\t// IsAllowNameForID configures the CLI to lookup entities ids by their name\n\tIsAllowNameForID() bool\n\t// IsInteractive configures the CLI to prompt the user interactively\n\tIsInteractive() bool\n\t// GetWorkWeekdays set which days of the week the user is expected to work\n\tGetWorkWeekdays() []string\n\t// InteractivePageSize sets how many items are shown when prompting\n\t// projects\n\tInteractivePageSize() int\n\t// IsSearchProjectWithClientsName defines if the project name for ID should\n\t// include the client's name\n\tIsSearchProjectWithClientsName() bool\n\n\t// Language what is the language to used when printing numbers\n\tLanguage() language.Tag\n\n\t// SetLanguage changes the language used for printing numbers\n\tSetLanguage(language.Tag)\n\n\t// TimeZone which time zone to use for showing date & time\n\tTimeZone() *time.Location\n\n\t// SetTimeZone changes the timezone used for dates\n\tSetTimeZone(*time.Location)\n\n\t// Get retrieves a config by its name\n\tGet(string) interface{}\n\t// All retrieves all the configurations of the CLI as a map\n\tAll() map[string]interface{}\n\n\t// LogLevel sets how much should be logged during execution\n\tLogLevel() string\n\n\t// Save will persist the changes made to the configuration\n\tSave() error\n}\n\ntype config struct {\n\tlngOnce sync.Once\n\tlang    language.Tag\n\n\ttzOnce   sync.Once\n\ttimezone *time.Location\n}\n\n// IsSearchProjectWithClientsName defines if the project name for ID should\n// include the client's name\nfunc (c *config) IsSearchProjectWithClientsName() bool {\n\treturn c.GetBool(CONF_SEARCH_PROJECTS_WITH_CLIENT_NAME)\n}\n\nfunc (c *config) InteractivePageSize() int {\n\ti := c.GetInt(CONF_INTERACTIVE_PAGE_SIZE)\n\tif i <= 0 {\n\t\treturn 7\n\t}\n\treturn i\n}\n\nfunc (c *config) LogLevel() string {\n\tl := c.GetString(CONF_LOG_LEVEL)\n\tswitch l {\n\tcase LOG_LEVEL_INFO, LOG_LEVEL_DEBUG:\n\t\treturn l\n\tdefault:\n\t\treturn LOG_LEVEL_NONE\n\t}\n}\n\nfunc (*config) GetBool(param string) bool {\n\treturn viper.GetBool(param)\n}\n\nfunc (*config) SetBool(p string, b bool) {\n\tviper.Set(p, b)\n}\n\nfunc (*config) GetString(param string) string {\n\treturn viper.GetString(param)\n}\n\nfunc (*config) SetString(p, s string) {\n\tviper.Set(p, s)\n}\n\nfunc (*config) GetInt(param string) int {\n\treturn viper.GetInt(param)\n}\n\nfunc (*config) SetInt(p string, i int) {\n\tviper.Set(p, i)\n}\n\nfunc (*config) GetStringSlice(param string) []string {\n\treturn viper.GetStringSlice(param)\n}\n\nfunc (*config) SetStringSlice(p string, ss []string) {\n\tviper.Set(p, ss)\n}\n\nfunc (c *config) IsDebuging() bool {\n\treturn c.LogLevel() == LOG_LEVEL_DEBUG\n}\n\nfunc (c *config) GetWorkWeekdays() []string {\n\treturn strhlp.Map(strings.ToLower, c.GetStringSlice(CONF_WORKWEEK_DAYS))\n}\n\nfunc (c *config) IsAllowNameForID() bool {\n\treturn c.GetBool(CONF_ALLOW_NAME_FOR_ID)\n}\n\nfunc (c *config) IsInteractive() bool {\n\treturn c.GetBool(CONF_INTERACTIVE)\n}\n\nfunc (c *config) SetLanguage(l language.Tag) {\n\tc.lngOnce = sync.Once{}\n\tc.SetString(CONF_LANGUAGE, l.String())\n}\n\nfunc (c *config) Language() language.Tag {\n\tc.lngOnce.Do(func() {\n\t\tc.lang = language.English\n\n\t\tlang := c.GetString(CONF_LANGUAGE)\n\t\tif lang == \"\" {\n\t\t\treturn\n\t\t}\n\n\t\tif l, err := language.Parse(lang); err == nil {\n\t\t\tc.lang = l\n\t\t\treturn\n\t\t}\n\t})\n\n\treturn c.lang\n}\n\nfunc (*config) Get(p string) interface{} {\n\treturn viper.Get(p)\n}\n\nfunc (*config) All() map[string]interface{} {\n\treturn viper.AllSettings()\n}\n\nfunc (*config) Save() error {\n\tfilename := viper.ConfigFileUsed()\n\tif filename == \"\" {\n\t\thome, err := homedir.Dir()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdir := path.Join(home, \".config\", \"clockify-cli\")\n\t\t_ = os.MkdirAll(dir, os.ModePerm)\n\t\tfilename = path.Join(dir, \".clockify-cli.yaml\")\n\t}\n\n\treturn viper.WriteConfigAs(filename)\n}\n\nfunc configFunc() func() (c Config) {\n\treturn func() Config {\n\t\treturn &config{}\n\t}\n}\n\n// GetWeekdays with their names\nfunc GetWeekdays() []string {\n\treturn []string{\n\t\ttime.Sunday:    strings.ToLower(time.Sunday.String()),\n\t\ttime.Monday:    strings.ToLower(time.Monday.String()),\n\t\ttime.Tuesday:   strings.ToLower(time.Tuesday.String()),\n\t\ttime.Wednesday: strings.ToLower(time.Wednesday.String()),\n\t\ttime.Thursday:  strings.ToLower(time.Thursday.String()),\n\t\ttime.Friday:    strings.ToLower(time.Friday.String()),\n\t\ttime.Saturday:  strings.ToLower(time.Saturday.String()),\n\t}\n}\n\n// SetTimeZone changes the timezone used for dates\nfunc (c *config) SetTimeZone(tz *time.Location) {\n\tc.tzOnce = sync.Once{}\n\tc.SetString(CONF_TIMEZONE, tz.String())\n}\n\n// TimeZone which time zone to use for showing date & time\nfunc (c *config) TimeZone() *time.Location {\n\tc.tzOnce.Do(func() {\n\t\tc.timezone = time.Local\n\n\t\ttz := c.GetString(CONF_TIMEZONE)\n\t\tif tz == \"\" {\n\t\t\treturn\n\t\t}\n\n\t\tif tz, err := time.LoadLocation(tz); err == nil {\n\t\t\tc.timezone = tz\n\t\t\treturn\n\t\t}\n\t})\n\n\treturn c.timezone\n}\n"
  },
  {
    "path": "pkg/cmdutil/errors.go",
    "content": "package cmdutil\n\n// FlagError happens when a non-cobra validation fails\ntype FlagError struct {\n\terr error\n}\n\nfunc (fe *FlagError) Error() string {\n\treturn fe.err.Error()\n}\n\nfunc (fe *FlagError) Unwrap() error {\n\treturn fe.err\n}\n\nfunc FlagErrorWrap(err error) *FlagError {\n\treturn &FlagError{err: err}\n}\n"
  },
  {
    "path": "pkg/cmdutil/factory.go",
    "content": "package cmdutil\n\nimport (\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/ui\"\n)\n\n// Factory is a container/factory builder for the commands and its helpers\ntype Factory interface {\n\t// Version of the CLI\n\tVersion() Version\n\n\t// Config returns configurations set by the user\n\tConfig() Config\n\t// Client builds a client for Clockify's API\n\tClient() (api.Client, error)\n\t// UI builds a control to prompt information from the user\n\tUI() ui.UI\n\n\t// GetUserID returns the current user id\n\tGetUserID() (string, error)\n\t// GetWorkspaceID returns the current workspace id\n\tGetWorkspaceID() (string, error)\n\t// GetWorkspaceID returns the current workspace\n\tGetWorkspace() (dto.Workspace, error)\n}\n\ntype factory struct {\n\tversion func() Version\n\n\tconfig func() Config\n\tclient func() (api.Client, error)\n\tui     func() ui.UI\n\n\tgetUserID      func() (string, error)\n\tgetWorkspaceID func() (string, error)\n\tgetWorkspace   func() (dto.Workspace, error)\n}\n\nfunc (f *factory) Version() Version {\n\treturn f.version()\n}\n\nfunc (f *factory) Config() Config {\n\treturn f.config()\n}\n\nfunc (f *factory) Client() (api.Client, error) {\n\treturn f.client()\n}\n\nfunc (f *factory) UI() ui.UI {\n\treturn f.ui()\n}\n\nfunc (f *factory) GetUserID() (string, error) {\n\treturn f.getUserID()\n}\n\nfunc (f *factory) GetWorkspaceID() (string, error) {\n\treturn f.getWorkspaceID()\n}\n\nfunc (f *factory) GetWorkspace() (dto.Workspace, error) {\n\treturn f.getWorkspace()\n}\n\nfunc NewFactory(v Version) Factory {\n\tf := &factory{\n\t\tversion: func() Version { return v },\n\t\tconfig:  configFunc(),\n\t}\n\n\tf.ui = getUi(f)\n\n\tf.client = clientFunc(f)\n\n\tf.getUserID = getUserIDFunc(f)\n\n\tf.getWorkspace = getWorkspaceFunc(f)\n\tf.getWorkspaceID = getWorkspaceIDFunc(f)\n\n\treturn f\n}\n\nfunc getUserIDFunc(f Factory) func() (string, error) {\n\tvar userID string\n\tvar err error\n\treturn func() (string, error) {\n\t\tif userID != \"\" || err != nil {\n\t\t\treturn userID, err\n\t\t}\n\n\t\tuserID = f.Config().GetString(CONF_USER_ID)\n\t\tif userID != \"\" {\n\t\t\treturn userID, err\n\t\t}\n\n\t\tclient, err := f.Client()\n\t\tif err != nil {\n\t\t\treturn userID, err\n\t\t}\n\n\t\tu, err := client.GetMe()\n\t\tif err != nil {\n\t\t\treturn userID, err\n\t\t}\n\n\t\tuserID = u.ID\n\t\treturn userID, err\n\t}\n}\n\nfunc getWorkspaceFunc(f Factory) func() (dto.Workspace, error) {\n\tvar w *dto.Workspace\n\tvar err error\n\treturn func() (dto.Workspace, error) {\n\t\tif w != nil {\n\t\t\treturn *w, nil\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn dto.Workspace{}, err\n\t\t}\n\n\t\tid, err := f.GetWorkspaceID()\n\t\tif err != nil {\n\t\t\treturn dto.Workspace{}, err\n\t\t}\n\n\t\tclient, err := f.Client()\n\t\tif err != nil {\n\t\t\treturn dto.Workspace{}, err\n\t\t}\n\n\t\toW, err := client.GetWorkspace(api.GetWorkspace{ID: id})\n\t\tif err != nil {\n\t\t\treturn dto.Workspace{}, err\n\t\t}\n\n\t\tw = &oW\n\t\treturn *w, err\n\t}\n}\n\nfunc getWorkspaceIDFunc(f Factory) func() (string, error) {\n\tvar w string\n\tvar err error\n\treturn func() (string, error) {\n\t\tif w != \"\" || err != nil {\n\t\t\treturn w, err\n\t\t}\n\n\t\tw = f.Config().GetString(CONF_WORKSPACE)\n\t\tif w != \"\" {\n\t\t\treturn w, err\n\t\t}\n\n\t\tclient, err := f.Client()\n\t\tif err != nil {\n\t\t\treturn w, err\n\t\t}\n\n\t\tu, err := client.GetMe()\n\t\tw = u.DefaultWorkspace\n\n\t\treturn w, err\n\t}\n}\n\nfunc clientFunc(f Factory) func() (api.Client, error) {\n\tvar c api.Client\n\tvar err error\n\n\treturn func() (api.Client, error) {\n\t\tif c != nil || err != nil {\n\t\t\treturn c, err\n\t\t}\n\n\t\tapiUrl := f.Config().GetString(CONF_API_URL)\n\t\tif apiUrl != \"\" {\n\t\t\tc, err = api.NewClientFromUrlAndKey(\n\t\t\t\tf.Config().GetString(CONF_TOKEN),\n\t\t\t\tapiUrl,\n\t\t\t)\n\t\t} else {\n\t\t\tc, err = api.NewClient(f.Config().GetString(CONF_TOKEN))\n\t\t}\n\t\tif err != nil {\n\t\t\treturn c, err\n\t\t}\n\n\t\tll := f.Config().LogLevel()\n\t\tif ll == LOG_LEVEL_NONE {\n\t\t\treturn c, err\n\t\t}\n\n\t\tc.SetInfoLogger(\n\t\t\tlog.New(os.Stdout, \"INFO  \", log.LstdFlags),\n\t\t)\n\n\t\tif ll == LOG_LEVEL_INFO {\n\t\t\treturn c, err\n\t\t}\n\n\t\tc.SetDebugLogger(\n\t\t\tlog.New(os.Stdout, \"DEBUG \", log.LstdFlags),\n\t\t)\n\n\t\treturn c, err\n\t}\n}\n\nfunc getUi(f Factory) func() ui.UI {\n\tvar i ui.UI\n\treturn func() ui.UI {\n\t\tif i == nil {\n\t\t\ti = ui.NewUI(os.Stdin, os.Stdout, os.Stderr)\n\t\t\ti.SetPageSize(uint(f.Config().InteractivePageSize()))\n\t\t}\n\n\t\treturn i\n\t}\n\n}\n"
  },
  {
    "path": "pkg/cmdutil/flags.go",
    "content": "package cmdutil\n\nimport (\n\t\"sort\"\n\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/pflag\"\n)\n\n// XorFlag will fail if 2 or more entries are true\nfunc XorFlag(exclusiveFlags map[string]bool) error {\n\tfs := make([]string, 0)\n\tfor n := range exclusiveFlags {\n\t\tif exclusiveFlags[n] {\n\t\t\tfs = append(fs, n)\n\t\t}\n\t}\n\n\tif len(fs) < 2 {\n\t\treturn nil\n\t}\n\n\tsort.Strings(fs)\n\tfs = strhlp.Map(func(s string) string { return \"`\" + s + \"`\" }, fs)\n\treturn FlagErrorWrap(errors.New(\n\t\t\"the following flags can't be used together: \" +\n\t\t\tstrhlp.ListForHumans(fs),\n\t))\n}\n\n// XorFlagSet works like XorFlag, but will read if the flag was changed from\n// the pflag.FlagSet\nfunc XorFlagSet(f *pflag.FlagSet, exclusiveFlags ...string) error {\n\tfs := map[string]bool{}\n\tfor _, ef := range exclusiveFlags {\n\t\tfs[ef] = f.Changed(ef)\n\t}\n\n\treturn XorFlag(fs)\n}\n"
  },
  {
    "path": "pkg/cmdutil/flags_test.go",
    "content": "package cmdutil_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/spf13/pflag\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype testcase struct {\n\tname  string\n\tparam map[string]bool\n\terr   error\n}\n\nfunc testcases() []testcase {\n\treturn []testcase{\n\t\t{\n\t\t\tname: \"all false\",\n\t\t\tparam: map[string]bool{\n\t\t\t\t\"pos1\": false,\n\t\t\t\t\"pos2\": false,\n\t\t\t\t\"pos3\": false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"empty\",\n\t\t\tparam: map[string]bool{},\n\t\t},\n\t\t{\n\t\t\tname: \"pos1 and pos2 are true\",\n\t\t\tparam: map[string]bool{\n\t\t\t\t\"pos1\": true,\n\t\t\t\t\"pos2\": true,\n\t\t\t\t\"pos3\": false,\n\t\t\t},\n\t\t\terr: errors.New(\n\t\t\t\t\"the following flags can't be used together: \" +\n\t\t\t\t\t\"`pos1` and `pos2`\"),\n\t\t},\n\t\t{\n\t\t\tname: \"pos1, pos2 and pos3 are true\",\n\t\t\tparam: map[string]bool{\n\t\t\t\t\"pos1\": true,\n\t\t\t\t\"pos2\": true,\n\t\t\t\t\"pos4\": false,\n\t\t\t\t\"pos3\": true,\n\t\t\t},\n\t\t\terr: errors.New(\n\t\t\t\t\"the following flags can't be used together: \" +\n\t\t\t\t\t\"`pos1`, `pos2` and `pos3`\"),\n\t\t},\n\t\t{\n\t\t\tname: \"pos1 and pos4 are true\",\n\t\t\tparam: map[string]bool{\n\t\t\t\t\"pos1\": true,\n\t\t\t\t\"pos2\": false,\n\t\t\t\t\"pos3\": false,\n\t\t\t\t\"pos4\": true,\n\t\t\t},\n\t\t\terr: errors.New(\n\t\t\t\t\"the following flags can't be used together: \" +\n\t\t\t\t\t\"`pos1` and `pos4`\"),\n\t\t},\n\t}\n}\n\nfunc TestXorFlag(t *testing.T) {\n\tfor _, tc := range testcases() {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := cmdutil.XorFlag(tc.param)\n\t\t\tif tc.err == nil && assert.NoError(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tvar fErr *cmdutil.FlagError\n\t\t\tassert.ErrorAs(t, err, &fErr)\n\t\t\tassert.EqualError(t, tc.err, err.Error())\n\t\t})\n\t}\n}\n\nfunc TestXorFlagSet(t *testing.T) {\n\tfor _, tc := range testcases() {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tfs := pflag.NewFlagSet(\"test\", pflag.ContinueOnError)\n\t\t\tflags := make([]string, len(tc.param))\n\t\t\tvar args []string\n\n\t\t\tfor fl := range tc.param {\n\t\t\t\tflags = append(flags, fl)\n\t\t\t\tfs.Bool(fl, false, \"help\")\n\t\t\t\tif tc.param[fl] {\n\t\t\t\t\targs = append(args, \"--\"+fl)\n\t\t\t\t}\n\t\t\t}\n\t\t\t_ = fs.Parse(args)\n\n\t\t\terr := cmdutil.XorFlagSet(fs, flags...)\n\t\t\tif tc.err == nil && assert.NoError(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Error(t, err)\n\t\t\tvar fErr *cmdutil.FlagError\n\t\t\tassert.ErrorAs(t, err, &fErr)\n\t\t\tassert.EqualError(t, tc.err, err.Error())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/cmdutil/project.go",
    "content": "package cmdutil\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcompl\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil\"\n\t\"github.com/spf13/cobra\"\n)\n\n// AddProjectFlags creates a project flag with autocomplete configured\nfunc AddProjectFlags(cmd *cobra.Command, f Factory) {\n\tcmd.Flags().StringP(\"project\", \"p\", \"\",\n\t\t\"the name/id of the project to work on\")\n\t_ = cmd.MarkFlagRequired(\"project\")\n\n\t_ = cmdcompl.AddSuggestionsToFlag(cmd, \"project\",\n\t\tcmdcomplutil.NewProjectAutoComplete(f, f.Config()))\n}\n"
  },
  {
    "path": "pkg/cmdutil/version.go",
    "content": "package cmdutil\n\n// Version register which is the CLI tag, commit and build date\ntype Version struct {\n\tTag    string\n\tCommit string\n\tDate   string\n}\n"
  },
  {
    "path": "pkg/output/client/csv.go",
    "content": "package client\n\nimport (\n\t\"encoding/csv\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// ClientsCSVPrint will print as CSV\nfunc ClientsCSVPrint(clients []dto.Client, out io.Writer) error {\n\tw := csv.NewWriter(out)\n\n\tif err := w.Write([]string{\n\t\t\"id\",\n\t\t\"name\",\n\t\t\"archived\",\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tfor i := 0; i < len(clients); i++ {\n\t\tc := clients[i]\n\t\tif err := w.Write([]string{\n\t\t\tc.ID,\n\t\t\tc.Name,\n\t\t\tfmt.Sprintf(\"%v\", c.Archived),\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tw.Flush()\n\treturn w.Error()\n}\n"
  },
  {
    "path": "pkg/output/client/default.go",
    "content": "package client\n\nimport (\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"golang.org/x/term\"\n)\n\n// ClientPrint will print more details\nfunc ClientPrint(cs []dto.Client, w io.Writer) error {\n\ttw := tablewriter.NewWriter(w)\n\ttw.SetHeader([]string{\"ID\", \"Name\", \"Archived\"})\n\n\tyesNo := map[bool]string{\n\t\ttrue:  \"YES\",\n\t\tfalse: \"NO\",\n\t}\n\n\tlines := make([][]string, len(cs))\n\tfor i := 0; i < len(cs); i++ {\n\t\tc := cs[i]\n\t\tlines[i] = []string{\n\t\t\tc.ID,\n\t\t\tc.Name,\n\t\t\tyesNo[c.Archived],\n\t\t}\n\t}\n\n\tif width, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil {\n\t\ttw.SetColWidth(width / 3)\n\t}\n\ttw.AppendBulk(lines)\n\ttw.Render()\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/output/client/json.go",
    "content": "package client\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// ClientJSONPrint will print as JSON\nfunc ClientJSONPrint(t dto.Client, w io.Writer) error {\n\treturn json.NewEncoder(w).Encode(t)\n}\n\n// ClientsJSONPrint will print as JSON\nfunc ClientsJSONPrint(t []dto.Client, w io.Writer) error {\n\treturn json.NewEncoder(w).Encode(t)\n}\n"
  },
  {
    "path": "pkg/output/client/quiet.go",
    "content": "package client\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// ClientPrintQuietly will only print the IDs\nfunc ClientPrintQuietly(cs []dto.Client, w io.Writer) error {\n\tfor i := 0; i < len(cs); i++ {\n\t\tif _, err := fmt.Fprintln(w, cs[i].ID); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/output/client/template.go",
    "content": "package client\n\nimport (\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/output/util\"\n)\n\n// ClientPrintWithTemplate will print each client using the format string\nfunc ClientPrintWithTemplate(format string) func([]dto.Client, io.Writer) error {\n\treturn func(cs []dto.Client, w io.Writer) error {\n\t\tt, err := util.NewTemplate(format)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor i := 0; i < len(cs); i++ {\n\t\t\tif err := t.Execute(w, cs[i]); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "pkg/output/project/csv.go",
    "content": "package project\n\nimport (\n\t\"encoding/csv\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// ProjectsCSVPrint will print each time entry using the format string\nfunc ProjectsCSVPrint(ps []dto.Project, out io.Writer) error {\n\tw := csv.NewWriter(out)\n\n\tif err := w.Write([]string{\n\t\t\"id\",\n\t\t\"name\",\n\t\t\"client.id\",\n\t\t\"client.name\",\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tfor i := 0; i < len(ps); i++ {\n\t\tp := ps[i]\n\t\tif err := w.Write([]string{\n\t\t\tp.ID,\n\t\t\tp.Name,\n\t\t\tp.ClientID,\n\t\t\tp.ClientName,\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tw.Flush()\n\treturn w.Error()\n}\n"
  },
  {
    "path": "pkg/output/project/default.go",
    "content": "package project\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/output/util\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"golang.org/x/term\"\n)\n\n// ProjectPrint will print more details\nfunc ProjectPrint(ps []dto.Project, w io.Writer) error {\n\ttw := tablewriter.NewWriter(w)\n\ttw.SetHeader([]string{\"ID\", \"Name\", \"Client\"})\n\n\tif width, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil {\n\t\ttw.SetColWidth(width / 3)\n\t}\n\n\tcolors := make([]tablewriter.Colors, 3)\n\tfor i := 0; i < len(ps); i++ {\n\t\tw := ps[i]\n\t\tclient := \"\"\n\t\tif w.ClientID != \"\" {\n\t\t\tclient = fmt.Sprintf(\"%s (%s)\", w.ClientName, w.ClientID)\n\t\t}\n\t\tcolors[1] = []int{}\n\t\tif w.Color != \"\" {\n\t\t\tcolors[1] = util.ColorToTermColor(w.Color)\n\t\t}\n\n\t\ttw.Rich([]string{\n\t\t\tw.ID,\n\t\t\tw.Name,\n\t\t\tclient,\n\t\t}, colors)\n\t}\n\n\ttw.Render()\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/output/project/json.go",
    "content": "package project\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// ProjectsJSONPrint will print as JSON\nfunc ProjectsJSONPrint(t []dto.Project, w io.Writer) error {\n\treturn json.NewEncoder(w).Encode(t)\n}\n\n// ProjectJSONPrint will print as JSON\nfunc ProjectJSONPrint(t dto.Project, w io.Writer) error {\n\treturn json.NewEncoder(w).Encode(t)\n}\n"
  },
  {
    "path": "pkg/output/project/quiet.go",
    "content": "package project\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// ProjectPrintQuietly will only print the IDs\nfunc ProjectPrintQuietly(ps []dto.Project, w io.Writer) error {\n\tfor i := 0; i < len(ps); i++ {\n\t\tif _, err := fmt.Fprintln(w, ps[i].ID); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/output/project/template.go",
    "content": "package project\n\nimport (\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/output/util\"\n)\n\n// ProjectPrintWithTemplate will print each worspace using the format string\nfunc ProjectPrintWithTemplate(format string) func([]dto.Project, io.Writer) error {\n\treturn func(ps []dto.Project, w io.Writer) error {\n\t\tt, err := util.NewTemplate(format)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor i := 0; i < len(ps); i++ {\n\t\t\tif err := t.Execute(w, ps[i]); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "pkg/output/tag/default.go",
    "content": "package tag\n\nimport (\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/olekukonko/tablewriter\"\n)\n\n// TagPrint will print more details\nfunc TagPrint(ts []dto.Tag, w io.Writer) error {\n\ttw := tablewriter.NewWriter(w)\n\ttw.SetHeader([]string{\"ID\", \"Name\"})\n\n\tlines := make([][]string, len(ts))\n\tfor i := 0; i < len(ts); i++ {\n\t\tlines[i] = []string{\n\t\t\tts[i].ID,\n\t\t\tts[i].Name,\n\t\t}\n\t}\n\n\ttw.AppendBulk(lines)\n\ttw.Render()\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/output/tag/quiet.go",
    "content": "package tag\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// TagPrintQuietly will only print the IDs\nfunc TagPrintQuietly(ts []dto.Tag, w io.Writer) error {\n\tfor i := 0; i < len(ts); i++ {\n\t\tif _, err := fmt.Fprintln(w, ts[i].ID); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/output/tag/template.go",
    "content": "package tag\n\nimport (\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/output/util\"\n)\n\n// TagPrintWithTemplate will print each worspace using the format string\nfunc TagPrintWithTemplate(format string) func([]dto.Tag, io.Writer) error {\n\treturn func(ts []dto.Tag, w io.Writer) error {\n\t\tt, err := util.NewTemplate(format)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor i := 0; i < len(ts); i++ {\n\t\t\tif err := t.Execute(w, ts[i]); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "pkg/output/task/csv.go",
    "content": "package task\n\nimport (\n\t\"encoding/csv\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// TasksCSVPrint will print as CSV\nfunc TasksCSVPrint(ts []dto.Task, out io.Writer) error {\n\tw := csv.NewWriter(out)\n\n\tif err := w.Write([]string{\n\t\t\"id\",\n\t\t\"name\",\n\t\t\"status\",\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tfor i := 0; i < len(ts); i++ {\n\t\tif err := w.Write([]string{\n\t\t\tts[i].ID,\n\t\t\tts[i].Name,\n\t\t\tstring(ts[i].Status),\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tw.Flush()\n\treturn w.Error()\n}\n"
  },
  {
    "path": "pkg/output/task/default.go",
    "content": "package task\n\nimport (\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"golang.org/x/term\"\n)\n\n// TaskPrint will print more details\nfunc TaskPrint(ts []dto.Task, w io.Writer) error {\n\ttw := tablewriter.NewWriter(w)\n\ttw.SetHeader([]string{\"ID\", \"Name\", \"Status\"})\n\n\tlines := make([][]string, len(ts))\n\tfor i := 0; i < len(ts); i++ {\n\t\tlines[i] = []string{\n\t\t\tts[i].ID,\n\t\t\tts[i].Name,\n\t\t\tstring(ts[i].Status),\n\t\t}\n\t}\n\n\tif width, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil {\n\t\ttw.SetColWidth(width / 3)\n\t}\n\ttw.AppendBulk(lines)\n\ttw.Render()\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/output/task/json.go",
    "content": "package task\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// TasksJSONPrint will print as JSON\nfunc TasksJSONPrint(t []dto.Task, w io.Writer) error {\n\treturn json.NewEncoder(w).Encode(t)\n}\n"
  },
  {
    "path": "pkg/output/task/quiet.go",
    "content": "package task\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// TaskPrintQuietly will only print the IDs\nfunc TaskPrintQuietly(ts []dto.Task, w io.Writer) error {\n\tfor i := 0; i < len(ts); i++ {\n\t\tif _, err := fmt.Fprintln(w, ts[i].ID); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/output/task/template.go",
    "content": "package task\n\nimport (\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/output/util\"\n)\n\n// TaskPrintWithTemplate will print each client using the format string\nfunc TaskPrintWithTemplate(format string) func([]dto.Task, io.Writer) error {\n\treturn func(ts []dto.Task, w io.Writer) error {\n\t\tt, err := util.NewTemplate(format)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor i := 0; i < len(ts); i++ {\n\t\t\tif err := t.Execute(w, ts[i]); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "pkg/output/time-entry/csv.go",
    "content": "package timeentry\n\nimport (\n\t\"encoding/csv\"\n\t\"io\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// TimeEntriesCSVPrint will print each time entry using the format string\nfunc TimeEntriesCSVPrint(timeEntries []dto.TimeEntry, out io.Writer) error {\n\tw := csv.NewWriter(out)\n\n\tif err := w.Write([]string{\n\t\t\"id\",\n\t\t\"description\",\n\t\t\"project.id\",\n\t\t\"project.name\",\n\t\t\"task.id\",\n\t\t\"task.name\",\n\t\t\"start\",\n\t\t\"end\",\n\t\t\"duration\",\n\t\t\"user.id\",\n\t\t\"user.email\",\n\t\t\"user.name\",\n\t\t\"tags...\",\n\t\t\"customFields...\",\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tformat := func(t *time.Time) string {\n\t\tif t == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn t.In(time.Local).Format(TimeFormatFull)\n\t}\n\n\tfor i := 0; i < len(timeEntries); i++ {\n\t\tte := timeEntries[i]\n\t\tvar p dto.Project\n\t\tif te.Project != nil {\n\t\t\tp = *te.Project\n\t\t}\n\n\t\tend := time.Now()\n\t\tif te.TimeInterval.End != nil {\n\t\t\tend = *te.TimeInterval.End\n\t\t}\n\n\t\tif te.User == nil {\n\t\t\tu := dto.User{}\n\t\t\tte.User = &u\n\t\t}\n\n\t\tif te.Task == nil {\n\t\t\tt := dto.Task{}\n\t\t\tte.Task = &t\n\t\t}\n\n\t\tarr := []string{\n\t\t\tte.ID,\n\t\t\tte.Description,\n\t\t\tp.ID,\n\t\t\tp.Name,\n\t\t\tte.Task.ID,\n\t\t\tte.Task.Name,\n\t\t\tformat(&te.TimeInterval.Start),\n\t\t\tformat(te.TimeInterval.End),\n\t\t\tdurationToString(end.Sub(te.TimeInterval.Start)),\n\t\t\tte.User.ID,\n\t\t\tte.User.Email,\n\t\t\tte.User.Name,\n\t\t}\n\n\t\tarr = append(arr, strings.Join(tagsToStringSlice(te.Tags), \";\"))\n\t\tarr = append(arr, strings.Join(customFieldsToStringSlice(te.CustomFields), \";\"))\n\n\t\tif err := w.Write(arr); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tw.Flush()\n\treturn w.Error()\n}\n"
  },
  {
    "path": "pkg/output/time-entry/default.go",
    "content": "package timeentry\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/output/util\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"golang.org/x/term\"\n)\n\nfunc sumTimeEntriesDuration(ts []dto.TimeEntry) time.Duration {\n\ts := time.Duration(0)\n\tfor i := 0; i < len(ts); i++ {\n\t\tend := time.Now()\n\t\tif ts[i].TimeInterval.End != nil {\n\t\t\tend = *ts[i].TimeInterval.End\n\t\t}\n\n\t\td := end.Sub(ts[i].TimeInterval.Start)\n\t\ts = s + d\n\t}\n\treturn s\n}\n\nconst (\n\tTimeFormatFull   = \"2006-01-02 15:04:05\"\n\tTimeFormatSimple = \"15:04:05\"\n)\n\n// TimeEntryOutputOptions sets how the \"table\" format should print the time\n// entries\ntype TimeEntryOutputOptions struct {\n\tShowTasks         bool\n\tShowCustomFields  bool\n\tShowClients       bool\n\tShowTotalDuration bool\n\tTimeFormat        string\n}\n\n// NewTimeEntryOutputOptions creates a default TimeEntryOutputOptions\nfunc NewTimeEntryOutputOptions() TimeEntryOutputOptions {\n\treturn TimeEntryOutputOptions{\n\t\tTimeFormat:        TimeFormatSimple,\n\t\tShowTasks:         false,\n\t\tShowCustomFields:  false,\n\t\tShowClients:       false,\n\t\tShowTotalDuration: false,\n\t}\n}\n\n// WithTimeFormat sets the date-time output format\nfunc (teo TimeEntryOutputOptions) WithTimeFormat(\n\tformat string) TimeEntryOutputOptions {\n\tteo.TimeFormat = format\n\treturn teo\n\n}\n\n// WithShowTasks shows a new column with the task of the time entry\nfunc (teo TimeEntryOutputOptions) WithShowTasks() TimeEntryOutputOptions {\n\tteo.ShowTasks = true\n\treturn teo\n}\n\n// WithShowCustomFields shows a new column with the custom fields of the time entry\nfunc (teo TimeEntryOutputOptions) WithShowCustomFields() TimeEntryOutputOptions {\n\tteo.ShowCustomFields = true\n\treturn teo\n}\n\n// WithShowClients shows a new column with the client of the time entry\nfunc (teo TimeEntryOutputOptions) WithShowClients() TimeEntryOutputOptions {\n\tteo.ShowClients = true\n\treturn teo\n}\n\n// WithTotalDuration shows a footer with the sum of the durations of the time\n// entries\nfunc (teo TimeEntryOutputOptions) WithTotalDuration() TimeEntryOutputOptions {\n\tteo.ShowTotalDuration = true\n\treturn teo\n}\n\n// TimeEntriesPrint will print more details\nfunc TimeEntriesPrint(\n\toptions TimeEntryOutputOptions) func([]dto.TimeEntry, io.Writer) error {\n\treturn func(timeEntries []dto.TimeEntry, w io.Writer) error {\n\t\ttw := tablewriter.NewWriter(w)\n\t\tprojectColumn := 4\n\t\theader := []string{\"ID\", \"Start\", \"End\", \"Dur\", \"Project\"}\n\n\t\tif options.ShowClients {\n\t\t\theader = append(header, \"Client\")\n\t\t}\n\n\t\tif options.ShowTasks {\n\t\t\theader = append(header, \"Task\")\n\t\t}\n\n\t\theader = append(header, \"Description\", \"Tags\")\n\n\t\tif options.ShowCustomFields {\n\t\t\theader = append(header, \"Custom Fields\")\n\t\t}\n\n\t\ttw.SetHeader(header)\n\t\ttw.SetRowLine(true)\n\t\tif width, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil {\n\t\t\tif options.ShowClients || options.ShowTasks {\n\t\t\t\ttw.SetColWidth(width / 4)\n\t\t\t} else {\n\t\t\t\ttw.SetColWidth(width / 3)\n\t\t\t}\n\t\t}\n\n\t\tcolors := make([]tablewriter.Colors, len(header))\n\t\tfor i := 0; i < len(timeEntries); i++ {\n\t\t\tt := timeEntries[i]\n\t\t\tend := time.Now()\n\t\t\tif t.TimeInterval.End != nil {\n\t\t\t\tend = *t.TimeInterval.End\n\t\t\t}\n\n\t\t\tprojectName := \"\"\n\t\t\tcolors[projectColumn] = []int{}\n\t\t\tif t.Project != nil {\n\t\t\t\tcolors[projectColumn] = util.ColorToTermColor(t.Project.Color)\n\t\t\t\tprojectName = t.Project.Name\n\t\t\t}\n\n\t\t\tline := []string{\n\t\t\t\tt.ID,\n\t\t\t\tt.TimeInterval.Start.In(time.Local).Format(options.TimeFormat),\n\t\t\t\tend.In(time.Local).Format(options.TimeFormat),\n\t\t\t\tdurationToString(end.Sub(t.TimeInterval.Start)),\n\t\t\t\tprojectName,\n\t\t\t}\n\n\t\t\tif options.ShowClients {\n\t\t\t\tclient := \"\"\n\t\t\t\tif t.Project != nil && t.Project.ClientName != \"\" {\n\t\t\t\t\tcolors[len(line)] = colors[projectColumn]\n\t\t\t\t\tclient = t.Project.ClientName\n\t\t\t\t}\n\t\t\t\tline = append(line, client)\n\t\t\t}\n\n\t\t\tif options.ShowTasks {\n\t\t\t\ttask := \"\"\n\t\t\t\tif t.Task != nil {\n\t\t\t\t\ttask = fmt.Sprintf(\"%s (%s)\", t.Task.Name, t.Task.ID)\n\t\t\t\t}\n\t\t\t\tline = append(line, task)\n\t\t\t}\n\n\t\t\tline = append(\n\t\t\t\tline,\n\t\t\t\tt.Description,\n\t\t\t\tstrings.Join(tagsToStringSlice(t.Tags), \"\\n\"),\n\t\t\t)\n\n\t\t\tif options.ShowCustomFields {\n\t\t\t\tline = append(\n\t\t\t\t\tline,\n\t\t\t\t\tstrings.Join(customFieldsToStringSlice(t.CustomFields), \"\\n\"),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\ttw.Rich(line, colors)\n\t\t}\n\n\t\tif options.ShowTotalDuration {\n\t\t\tline := make([]string, len(header))\n\t\t\tline[0] = \"TOTAL\"\n\t\t\tline[3] = durationToString(sumTimeEntriesDuration(timeEntries))\n\t\t\ttw.Append(line)\n\t\t}\n\n\t\ttw.Render()\n\n\t\treturn nil\n\t}\n}\n\nfunc tagsToStringSlice(tags []dto.Tag) []string {\n\ts := make([]string, len(tags))\n\n\tfor i, t := range tags {\n\t\ts[i] = fmt.Sprintf(\"%s (%s)\", t.Name, t.ID)\n\t}\n\n\treturn s\n}\n\nfunc durationToString(d time.Duration) string {\n\treturn dto.Duration{Duration: d}.HumanString()\n}\n\nfunc customFieldsToStringSlice(customFields []dto.CustomField) []string {\n\ts := make([]string, len(customFields))\n\n\tfor i, cf := range customFields {\n\t\ts[i] = fmt.Sprintf(\"%s(%s)=%s\", cf.Name, cf.CustomFieldID, cf.ValueAsString())\n\t}\n\n\treturn s\n}\n"
  },
  {
    "path": "pkg/output/time-entry/default_test.go",
    "content": "package timeentry_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\ttimeentry \"github.com/lucassabreu/clockify-cli/pkg/output/time-entry\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTimeEntriesDefaultPrint(t *testing.T) {\n\tstart, _ := time.Parse(timehlp.FullTimeFormat, \"2024-06-15 10:00:01\")\n\tend := start.Add(2*time.Minute + 1*time.Second)\n\n\ttts := []struct {\n\t\tname   string\n\t\topts   timeentry.TimeEntryOutputOptions\n\t\ttes    []dto.TimeEntry\n\t\toutput string\n\t}{\n\t\t{\n\t\t\tname: \"show clients on its own column\",\n\t\t\topts: timeentry.TimeEntryOutputOptions{\n\t\t\t\tShowClients:       true,\n\t\t\t\tTimeFormat:        timehlp.FullTimeFormat,\n\t\t\t\tShowTotalDuration: true,\n\t\t\t},\n\t\t\ttes: []dto.TimeEntry{\n\t\t\t\t{\n\t\t\t\t\tWorkspaceID: \"w1\",\n\t\t\t\t\tID:          \"dasdasdasdaasdasdasdasda\",\n\t\t\t\t\tBillable:    true,\n\t\t\t\t\tDescription: \"With project\",\n\t\t\t\t\tProject: &dto.Project{\n\t\t\t\t\t\tName:       \"Project Name\",\n\t\t\t\t\t\tClientName: \"Client Name\",\n\t\t\t\t\t},\n\t\t\t\t\tTags: []dto.Tag{{\n\t\t\t\t\t\tID:   \"tag1\",\n\t\t\t\t\t\tName: \"Tag Name\",\n\t\t\t\t\t}},\n\t\t\t\t\tTimeInterval: dto.NewTimeInterval(\n\t\t\t\t\t\tstart,\n\t\t\t\t\t\t&end,\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tWorkspaceID: \"w1\",\n\t\t\t\t\tID:          \"dfsdfsdfsdffsdfsdfsdfsdf\",\n\t\t\t\t\tBillable:    true,\n\t\t\t\t\tDescription: \"Without project\",\n\t\t\t\t\tTags: []dto.Tag{{\n\t\t\t\t\t\tID:   \"tag1\",\n\t\t\t\t\t\tName: \"Tag Name\",\n\t\t\t\t\t}},\n\t\t\t\t\tTimeInterval: dto.NewTimeInterval(\n\t\t\t\t\t\tstart,\n\t\t\t\t\t\t&end,\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t\toutput: heredoc.Docf(`\n\t\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+-------------+-----------------+-----------------+\n\t\t\t\t|            ID            |        START        |         END         |   DUR   |   PROJECT    |   CLIENT    |   DESCRIPTION   |      TAGS       |\n\t\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+-------------+-----------------+-----------------+\n\t\t\t\t| dasdasdasdaasdasdasdasda | %s | %s | 0:02:01 | Project Name | Client Name | With project    | Tag Name (tag1) |\n\t\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+-------------+-----------------+-----------------+\n\t\t\t\t| dfsdfsdfsdffsdfsdfsdfsdf | %s | %s | 0:02:01 |              |             | Without project | Tag Name (tag1) |\n\t\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+-------------+-----------------+-----------------+\n\t\t\t\t| TOTAL                    |                     |                     | 0:04:02 |              |             |                 |                 |\n\t\t\t\t+--------------------------+---------------------+---------------------+---------+--------------+-------------+-----------------+-----------------+\n\t\t\t\t`,\n\t\t\t\tstart.In(time.Local).Format(timehlp.FullTimeFormat), end.In(time.Local).Format(timehlp.FullTimeFormat),\n\t\t\t\tstart.In(time.Local).Format(timehlp.FullTimeFormat), end.In(time.Local).Format(timehlp.FullTimeFormat),\n\t\t\t),\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuffer := &strings.Builder{}\n\t\t\terr := timeentry.TimeEntriesPrint(tt.opts)(tt.tes, buffer)\n\n\t\t\tif !assert.NoError(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.output, buffer.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/output/time-entry/duration.go",
    "content": "package timeentry\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"golang.org/x/text/language\"\n\t\"golang.org/x/text/message\"\n\t\"golang.org/x/text/number\"\n)\n\nfunc timeEntriesTotalDurationOnly(\n\tf func(time.Duration) string,\n\ttimeEntries []dto.TimeEntry,\n\tw io.Writer,\n) error {\n\t_, err := fmt.Fprintln(w, f(sumTimeEntriesDuration(timeEntries)))\n\treturn err\n}\n\n// TimeEntriesTotalDurationOnlyAsFloat will only print the total duration as\n// float\nfunc TimeEntriesTotalDurationOnlyAsFloat(\n\ttimeEntries []dto.TimeEntry, w io.Writer,\n\tl language.Tag) error {\n\tp := message.NewPrinter(l)\n\treturn timeEntriesTotalDurationOnly(\n\t\tfunc(d time.Duration) string {\n\t\t\treturn p.Sprintf(\"%f\", number.Decimal(d.Hours()))\n\t\t},\n\t\ttimeEntries,\n\t\tw,\n\t)\n}\n\n// TimeEntriesTotalDurationOnlyFormatted will only print the total duration as\n// float\nfunc TimeEntriesTotalDurationOnlyFormatted(\n\ttimeEntries []dto.TimeEntry, w io.Writer) error {\n\treturn timeEntriesTotalDurationOnly(\n\t\tdurationToString,\n\t\ttimeEntries,\n\t\tw,\n\t)\n}\n"
  },
  {
    "path": "pkg/output/time-entry/duration_test.go",
    "content": "package timeentry_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\ttimeentry \"github.com/lucassabreu/clockify-cli/pkg/output/time-entry\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/text/language\"\n)\n\nfunc TestTimeEntriesTotalDurationOnlyAsFloat_ShouldUseUserLanguage(\n\tt *testing.T) {\n\n\tstart, _ := timehlp.ConvertToTime(\"2024-01-01 00:00\")\n\tend := start.Add(time.Hour*1000 + (time.Minute * 31))\n\n\ttes := []dto.TimeEntry{\n\t\t{TimeInterval: dto.NewTimeInterval(start, &end)},\n\t}\n\n\ttts := []struct {\n\t\tname     string\n\t\tlanguage language.Tag\n\t\toutput   string\n\t}{\n\t\t{language: language.English, output: \"1,000.517\"},\n\t\t{language: language.German, output: \"1.000,517\"},\n\t\t{language: language.MustParse(\"pt-br\"), output: \"1.000,517\"},\n\t\t{language: language.Spanish, output: \"1.000,517\"},\n\t\t{language: language.Afrikaans, output: \"1\\u00a0000,517\"},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.language.String(), func(t *testing.T) {\n\t\t\tbuffer := strings.Builder{}\n\n\t\t\terr := timeentry.TimeEntriesTotalDurationOnlyAsFloat(\n\t\t\t\ttes, &buffer, tt.language)\n\n\t\t\tif !assert.NoError(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.output+\"\\n\", buffer.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/output/time-entry/json.go",
    "content": "package timeentry\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// TimeEntryJSONPrint will print as JSON\nfunc TimeEntryJSONPrint(t dto.TimeEntry, w io.Writer) error {\n\treturn json.NewEncoder(w).Encode(t)\n}\n\n// TimeEntriesJSONPrint will print as JSON\nfunc TimeEntriesJSONPrint(t []dto.TimeEntry, w io.Writer) error {\n\treturn json.NewEncoder(w).Encode(t)\n}\n"
  },
  {
    "path": "pkg/output/time-entry/markdown.go",
    "content": "package timeentry\n\nimport (\n\t_ \"embed\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n//go:embed markdown.gotmpl.md\nvar mdTemplate string\n\n// TimeEntriesMarkdownPrint will print time entries in \"markdown blocks\"\nfunc TimeEntriesMarkdownPrint(tes []dto.TimeEntry, w io.Writer) error {\n\treturn TimeEntriesPrintWithTemplate(mdTemplate)(tes, w)\n}\n"
  },
  {
    "path": "pkg/output/time-entry/markdown.gotmpl.md",
    "content": "{{- $project := \"\" -}}\n{{- if eq .Project nil }}\n  {{- $project = \"No Project\" -}}\n{{- else -}}\n  {{- $project = concat \"**\" .Project.Name \"**\" -}}\n  {{- if ne .Task nil -}}\n    {{- $project = concat $project \": \" .Task.Name  -}}\n  {{- else if ne .Project.ClientName \"\" -}}\n    {{- $project = concat $project \" - \" .Project.ClientName  -}}\n  {{- end -}}\n{{- end -}}\n\n{{- $bil := \"No\" -}}\n{{- if .Billable -}}{{ $bil = \"Yes\" }}{{- end -}}\n\n{{- $tags := \"\" -}}\n{{- with .Tags -}}\n  {{- range $index, $element := . -}}\n    {{- if ne $index 0 }}{{ $tags = concat $tags \", \" }}{{ end -}}\n    {{- $tags = concat $tags $element.Name -}}\n  {{- end -}}\n{{- else -}}\n  {{- $tags = \"No Tags\" -}}\n{{- end -}}\n\n{{- $customFields := \"\" -}}\n{{- $hasCustomFields := false -}}\n{{- with .CustomFields -}}\n  {{- range $index, $element := . -}}\n    {{- $value := $element.ValueAsString -}}\n    {{- if ne $value \"\" -}}\n      {{- if ne $index 0 }}{{ $customFields = concat $customFields \", \" }}{{ end -}}\n      {{- $customFields = concat $customFields $element.Name \": \" $value -}}\n      {{- $hasCustomFields = true -}}\n    {{- end -}}\n  {{- end -}}\n{{- end -}}\n\n{{- $pad := maxLength .Description $project $tags $customFields $bil -}}\n\n## _Time Entry_: {{ .ID }}\n\n_Time and date_  \n**{{ dsf .TimeInterval.Duration }}** | {{ if eq .TimeInterval.End nil -}}\nStart Time: _{{ formatTimeWS .TimeInterval.Start }}_ 🗓 Today\n{{- else -}}\n{{ formatTimeWS .TimeInterval.Start }} - {{ formatTimeWS .TimeInterval.End }} 🗓\n{{- .TimeInterval.Start.Format \" 01/02/2006\" }}\n{{- end }}\n\n|                 | {{ pad \"\" $pad }} |\n|-----------------|-{{ repeatString \"-\" $pad }}-|\n| _Description_   | {{ pad .Description $pad }} |\n| _Project_       | {{ pad $project $pad }} |\n| _Tags_          | {{ pad $tags $pad }} |\n| _Billable_      | {{ pad $bil $pad }} |\n{{- if $hasCustomFields }}\n| _Custom Fields_ | {{ pad $customFields $pad }} |\n{{- end }}\n"
  },
  {
    "path": "pkg/output/time-entry/markdown_test.go",
    "content": "package timeentry_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/MakeNowJust/heredoc\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\ttimeentry \"github.com/lucassabreu/clockify-cli/pkg/output/time-entry\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTimeEntriesMarkdownPrint(t *testing.T) {\n\tt65Min1SecAgo, _ := timehlp.ConvertToTime(\"-65m1s\")\n\tstart, _ := time.Parse(timehlp.FullTimeFormat, \"2024-06-15 10:00:01\")\n\tend := start.Add(2*time.Minute + 1*time.Second)\n\n\ttts := []struct {\n\t\tname   string\n\t\ttes    []dto.TimeEntry\n\t\toutput string\n\t}{\n\t\t{\n\t\t\tname: \"open without tags or project\",\n\t\t\ttes: []dto.TimeEntry{{\n\t\t\t\tWorkspaceID:  \"w1\",\n\t\t\t\tID:           \"te1\",\n\t\t\t\tBillable:     false,\n\t\t\t\tDescription:  \"Open and without project\",\n\t\t\t\tTimeInterval: dto.NewTimeInterval(t65Min1SecAgo, nil),\n\t\t\t}},\n\t\t\toutput: heredoc.Docf(`\n\t\t\t\t## _Time Entry_: te1\n\n\t\t\t\t_Time and date_  \n\t\t\t\t**1:05:01** | Start Time: _%s_ 🗓 Today\n\n\t\t\t\t|                 |                          |\n\t\t\t\t|-----------------|--------------------------|\n\t\t\t\t| _Description_   | Open and without project |\n\t\t\t\t| _Project_       | No Project               |\n\t\t\t\t| _Tags_          | No Tags                  |\n\t\t\t\t| _Billable_      | No                       |\n\t\t\t`, t65Min1SecAgo.UTC().Format(timehlp.SimplerOnlyTimeFormat)),\n\t\t},\n\t\t{\n\t\t\tname: \"closed without tags or project\",\n\t\t\ttes: []dto.TimeEntry{{\n\t\t\t\tWorkspaceID: \"w1\",\n\t\t\t\tID:          \"te1\",\n\t\t\t\tBillable:    false,\n\t\t\t\tDescription: \"Closed and without project\",\n\t\t\t\tTimeInterval: dto.NewTimeInterval(\n\t\t\t\t\tstart,\n\t\t\t\t\t&end,\n\t\t\t\t),\n\t\t\t}},\n\t\t\toutput: heredoc.Doc(`\n\t\t\t\t## _Time Entry_: te1\n\n\t\t\t\t_Time and date_  \n\t\t\t\t**0:02:01** | 10:00 - 10:02 🗓 06/15/2024\n\n\t\t\t\t|                 |                            |\n\t\t\t\t|-----------------|----------------------------|\n\t\t\t\t| _Description_   | Closed and without project |\n\t\t\t\t| _Project_       | No Project                 |\n\t\t\t\t| _Tags_          | No Tags                    |\n\t\t\t\t| _Billable_      | No                         |\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"Closed with project\",\n\t\t\ttes: []dto.TimeEntry{{\n\t\t\t\tWorkspaceID: \"w1\",\n\t\t\t\tID:          \"te1\",\n\t\t\t\tBillable:    false,\n\t\t\t\tDescription: \"With project\",\n\t\t\t\tProject: &dto.Project{\n\t\t\t\t\tName: \"Project Name\",\n\t\t\t\t},\n\t\t\t\tTimeInterval: dto.NewTimeInterval(\n\t\t\t\t\tstart,\n\t\t\t\t\t&end,\n\t\t\t\t),\n\t\t\t}},\n\t\t\toutput: heredoc.Doc(`\n\t\t\t\t## _Time Entry_: te1\n\n\t\t\t\t_Time and date_  \n\t\t\t\t**0:02:01** | 10:00 - 10:02 🗓 06/15/2024\n\n\t\t\t\t|                 |                  |\n\t\t\t\t|-----------------|------------------|\n\t\t\t\t| _Description_   | With project     |\n\t\t\t\t| _Project_       | **Project Name** |\n\t\t\t\t| _Tags_          | No Tags          |\n\t\t\t\t| _Billable_      | No               |\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"Closed with project with client\",\n\t\t\ttes: []dto.TimeEntry{{\n\t\t\t\tWorkspaceID: \"w1\",\n\t\t\t\tID:          \"te1\",\n\t\t\t\tBillable:    true,\n\t\t\t\tDescription: \"With project\",\n\t\t\t\tProject: &dto.Project{\n\t\t\t\t\tName:       \"Project Name\",\n\t\t\t\t\tClientName: \"Client Name\",\n\t\t\t\t},\n\t\t\t\tTimeInterval: dto.NewTimeInterval(\n\t\t\t\t\tstart,\n\t\t\t\t\t&end,\n\t\t\t\t),\n\t\t\t}},\n\t\t\toutput: heredoc.Doc(`\n\t\t\t\t## _Time Entry_: te1\n\n\t\t\t\t_Time and date_  \n\t\t\t\t**0:02:01** | 10:00 - 10:02 🗓 06/15/2024\n\n\t\t\t\t|                 |                                |\n\t\t\t\t|-----------------|--------------------------------|\n\t\t\t\t| _Description_   | With project                   |\n\t\t\t\t| _Project_       | **Project Name** - Client Name |\n\t\t\t\t| _Tags_          | No Tags                        |\n\t\t\t\t| _Billable_      | Yes                            |\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"Closed with project, client and task\",\n\t\t\ttes: []dto.TimeEntry{{\n\t\t\t\tWorkspaceID: \"w1\",\n\t\t\t\tID:          \"te1\",\n\t\t\t\tBillable:    true,\n\t\t\t\tDescription: \"With project\",\n\t\t\t\tProject: &dto.Project{\n\t\t\t\t\tName:       \"Project Name\",\n\t\t\t\t\tClientName: \"Client Name\",\n\t\t\t\t},\n\t\t\t\tTask: &dto.Task{\n\t\t\t\t\tName: \"Task Name\",\n\t\t\t\t},\n\t\t\t\tTimeInterval: dto.NewTimeInterval(\n\t\t\t\t\tstart,\n\t\t\t\t\t&end,\n\t\t\t\t),\n\t\t\t}},\n\t\t\toutput: heredoc.Doc(`\n\t\t\t\t## _Time Entry_: te1\n\n\t\t\t\t_Time and date_  \n\t\t\t\t**0:02:01** | 10:00 - 10:02 🗓 06/15/2024\n\n\t\t\t\t|                 |                             |\n\t\t\t\t|-----------------|-----------------------------|\n\t\t\t\t| _Description_   | With project                |\n\t\t\t\t| _Project_       | **Project Name**: Task Name |\n\t\t\t\t| _Tags_          | No Tags                     |\n\t\t\t\t| _Billable_      | Yes                         |\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"Closed with project, client, task and a tag\",\n\t\t\ttes: []dto.TimeEntry{{\n\t\t\t\tWorkspaceID: \"w1\",\n\t\t\t\tID:          \"te1\",\n\t\t\t\tBillable:    true,\n\t\t\t\tDescription: \"With project\",\n\t\t\t\tProject: &dto.Project{\n\t\t\t\t\tName:       \"Project Name\",\n\t\t\t\t\tClientName: \"Client Name\",\n\t\t\t\t},\n\t\t\t\tTask: &dto.Task{\n\t\t\t\t\tName: \"Task Name\",\n\t\t\t\t},\n\t\t\t\tTags: []dto.Tag{\n\t\t\t\t\t{Name: \"Stand-up Meeting\"},\n\t\t\t\t},\n\t\t\t\tTimeInterval: dto.NewTimeInterval(\n\t\t\t\t\tstart,\n\t\t\t\t\t&end,\n\t\t\t\t),\n\t\t\t}},\n\t\t\toutput: heredoc.Doc(`\n\t\t\t\t## _Time Entry_: te1\n\n\t\t\t\t_Time and date_  \n\t\t\t\t**0:02:01** | 10:00 - 10:02 🗓 06/15/2024\n\n\t\t\t\t|                 |                             |\n\t\t\t\t|-----------------|-----------------------------|\n\t\t\t\t| _Description_   | With project                |\n\t\t\t\t| _Project_       | **Project Name**: Task Name |\n\t\t\t\t| _Tags_          | Stand-up Meeting            |\n\t\t\t\t| _Billable_      | Yes                         |\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"Closed with project, client, task and tags\",\n\t\t\ttes: []dto.TimeEntry{{\n\t\t\t\tWorkspaceID: \"w1\",\n\t\t\t\tID:          \"te1\",\n\t\t\t\tBillable:    true,\n\t\t\t\tDescription: \"With project\",\n\t\t\t\tProject: &dto.Project{\n\t\t\t\t\tName:       \"Project Name\",\n\t\t\t\t\tClientName: \"Client Name\",\n\t\t\t\t},\n\t\t\t\tTask: &dto.Task{\n\t\t\t\t\tName: \"Task Name\",\n\t\t\t\t},\n\t\t\t\tTags: []dto.Tag{\n\t\t\t\t\t{Name: \"A Tag with long name\"},\n\t\t\t\t\t{Name: \"Normal tag\"},\n\t\t\t\t},\n\t\t\t\tTimeInterval: dto.NewTimeInterval(\n\t\t\t\t\tstart,\n\t\t\t\t\t&end,\n\t\t\t\t),\n\t\t\t}},\n\t\t\toutput: heredoc.Doc(`\n\t\t\t\t## _Time Entry_: te1\n\n\t\t\t\t_Time and date_  \n\t\t\t\t**0:02:01** | 10:00 - 10:02 🗓 06/15/2024\n\n\t\t\t\t|                 |                                  |\n\t\t\t\t|-----------------|----------------------------------|\n\t\t\t\t| _Description_   | With project                     |\n\t\t\t\t| _Project_       | **Project Name**: Task Name      |\n\t\t\t\t| _Tags_          | A Tag with long name, Normal tag |\n\t\t\t\t| _Billable_      | Yes                              |\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"Closed with project, client, task, tags and a custom field with non empty value\",\n\t\t\ttes: []dto.TimeEntry{{\n\t\t\t\tWorkspaceID: \"w1\",\n\t\t\t\tID:          \"te1\",\n\t\t\t\tBillable:    true,\n\t\t\t\tDescription: \"With project\",\n\t\t\t\tProject: &dto.Project{\n\t\t\t\t\tName:       \"Project Name\",\n\t\t\t\t\tClientName: \"Client Name\",\n\t\t\t\t},\n\t\t\t\tTask: &dto.Task{\n\t\t\t\t\tName: \"Task Name\",\n\t\t\t\t},\n\t\t\t\tCustomFields: []dto.CustomField{{\n\t\t\t\t\tCustomFieldID: \"abcdef123456\",\n\t\t\t\t\tName:          \"A custom field name\",\n\t\t\t\t\tTimeEntryId:   \"te1\",\n\t\t\t\t\tType:          \"DROPDOWN_SINGLE\",\n\t\t\t\t\tValue:         \"A custom field value\",\n\t\t\t\t}},\n\t\t\t\tTags: []dto.Tag{\n\t\t\t\t\t{Name: \"A Tag with long name\"},\n\t\t\t\t\t{Name: \"Normal tag\"},\n\t\t\t\t},\n\t\t\t\tTimeInterval: dto.NewTimeInterval(\n\t\t\t\t\tstart,\n\t\t\t\t\t&end,\n\t\t\t\t),\n\t\t\t}},\n\t\t\toutput: heredoc.Doc(`\n\t\t\t\t## _Time Entry_: te1\n\n\t\t\t\t_Time and date_  \n\t\t\t\t**0:02:01** | 10:00 - 10:02 🗓 06/15/2024\n\n\t\t\t\t|                 |                                           |\n\t\t\t\t|-----------------|-------------------------------------------|\n\t\t\t\t| _Description_   | With project                              |\n\t\t\t\t| _Project_       | **Project Name**: Task Name               |\n\t\t\t\t| _Tags_          | A Tag with long name, Normal tag          |\n\t\t\t\t| _Billable_      | Yes                                       |\n\t\t\t\t| _Custom Fields_ | A custom field name: A custom field value |\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"Closed with project, client, task, tags and a custom field with an empty value\",\n\t\t\ttes: []dto.TimeEntry{{\n\t\t\t\tWorkspaceID: \"w1\",\n\t\t\t\tID:          \"te1\",\n\t\t\t\tBillable:    true,\n\t\t\t\tDescription: \"With project\",\n\t\t\t\tProject: &dto.Project{\n\t\t\t\t\tName:       \"Project Name\",\n\t\t\t\t\tClientName: \"Client Name\",\n\t\t\t\t},\n\t\t\t\tTask: &dto.Task{\n\t\t\t\t\tName: \"Task Name\",\n\t\t\t\t},\n\t\t\t\tCustomFields: []dto.CustomField{{\n\t\t\t\t\tCustomFieldID: \"abcdef123456\",\n\t\t\t\t\tName:          \"A custom field name\",\n\t\t\t\t\tTimeEntryId:   \"te1\",\n\t\t\t\t\tType:          \"DROPDOWN_SINGLE\",\n\t\t\t\t\tValue:         \"\",\n\t\t\t\t}},\n\t\t\t\tTags: []dto.Tag{\n\t\t\t\t\t{Name: \"A Tag with long name\"},\n\t\t\t\t\t{Name: \"Normal tag\"},\n\t\t\t\t},\n\t\t\t\tTimeInterval: dto.NewTimeInterval(\n\t\t\t\t\tstart,\n\t\t\t\t\t&end,\n\t\t\t\t),\n\t\t\t}},\n\t\t\toutput: heredoc.Doc(`\n\t\t\t\t## _Time Entry_: te1\n\n\t\t\t\t_Time and date_  \n\t\t\t\t**0:02:01** | 10:00 - 10:02 🗓 06/15/2024\n\n\t\t\t\t|                 |                                  |\n\t\t\t\t|-----------------|----------------------------------|\n\t\t\t\t| _Description_   | With project                     |\n\t\t\t\t| _Project_       | **Project Name**: Task Name      |\n\t\t\t\t| _Tags_          | A Tag with long name, Normal tag |\n\t\t\t\t| _Billable_      | Yes                              |\n\t\t\t`),\n\t\t},\n\t\t{\n\t\t\tname: \"Closed with project, client, task, tags and a custom field non empty value and a custom field with multiple values\",\n\t\t\ttes: []dto.TimeEntry{{\n\t\t\t\tWorkspaceID: \"w1\",\n\t\t\t\tID:          \"te1\",\n\t\t\t\tBillable:    true,\n\t\t\t\tDescription: \"With project\",\n\t\t\t\tProject: &dto.Project{\n\t\t\t\t\tName:       \"Project Name\",\n\t\t\t\t\tClientName: \"Client Name\",\n\t\t\t\t},\n\t\t\t\tTask: &dto.Task{\n\t\t\t\t\tName: \"Task Name\",\n\t\t\t\t},\n\t\t\t\tCustomFields: []dto.CustomField{\n\t\t\t\t\t{\n\t\t\t\t\t\tCustomFieldID: \"abcdef123456\",\n\t\t\t\t\t\tName:          \"A custom field name\",\n\t\t\t\t\t\tTimeEntryId:   \"te1\",\n\t\t\t\t\t\tType:          \"DROPDOWN_SINGLE\",\n\t\t\t\t\t\tValue:         \"A custom field value\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tCustomFieldID: \"abcdef123457\",\n\t\t\t\t\t\tName:          \"Another custom field name\",\n\t\t\t\t\t\tTimeEntryId:   \"te1\",\n\t\t\t\t\t\tType:          \"DROPDOWN_MULTIPLE\",\n\t\t\t\t\t\tValue:         []string{\"Value 1\", \"Value 2\"},\n\t\t\t\t\t}},\n\t\t\t\tTags: []dto.Tag{\n\t\t\t\t\t{Name: \"A Tag with long name\"},\n\t\t\t\t\t{Name: \"Normal tag\"},\n\t\t\t\t},\n\t\t\t\tTimeInterval: dto.NewTimeInterval(\n\t\t\t\t\tstart,\n\t\t\t\t\t&end,\n\t\t\t\t),\n\t\t\t}},\n\t\t\toutput: heredoc.Doc(`\n\t\t\t\t## _Time Entry_: te1\n\n\t\t\t\t_Time and date_  \n\t\t\t\t**0:02:01** | 10:00 - 10:02 🗓 06/15/2024\n\n\t\t\t\t|                 |                                                                                       |\n\t\t\t\t|-----------------|---------------------------------------------------------------------------------------|\n\t\t\t\t| _Description_   | With project                                                                          |\n\t\t\t\t| _Project_       | **Project Name**: Task Name                                                           |\n\t\t\t\t| _Tags_          | A Tag with long name, Normal tag                                                      |\n\t\t\t\t| _Billable_      | Yes                                                                                   |\n\t\t\t\t| _Custom Fields_ | A custom field name: A custom field value, Another custom field name: Value 1|Value 2 |\n\t\t\t`),\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuffer := &strings.Builder{}\n\t\t\terr := timeentry.TimeEntriesMarkdownPrint(tt.tes, buffer)\n\n\t\t\tif !assert.NoError(t, err) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.output+\"\\n\", buffer.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/output/time-entry/quiet.go",
    "content": "package timeentry\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// TimeEntriesPrintQuietly will only print the IDs\nfunc TimeEntriesPrintQuietly(timeEntries []dto.TimeEntry, w io.Writer) error {\n\tfor i := 0; i < len(timeEntries); i++ {\n\t\tif _, err := fmt.Fprintln(w, timeEntries[i].ID); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/output/time-entry/template.go",
    "content": "package timeentry\n\nimport (\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/output/util\"\n)\n\n// TimeEntriesPrintWithTemplate will print each time entry using the format\n// string\nfunc TimeEntriesPrintWithTemplate(\n\tformat string,\n) func([]dto.TimeEntry, io.Writer) error {\n\treturn func(timeEntries []dto.TimeEntry, w io.Writer) error {\n\t\tt, err := util.NewTemplate(format)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tl := len(timeEntries)\n\t\tfor i := 0; i < l; i++ {\n\t\t\tif err := t.Execute(w, struct {\n\t\t\t\tdto.TimeEntry\n\t\t\t\tFirst bool\n\t\t\t\tLast  bool\n\t\t\t}{\n\t\t\t\tTimeEntry: timeEntries[i],\n\t\t\t\tFirst:     i == 0,\n\t\t\t\tLast:      i == (l - 1),\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "pkg/output/user/default.go",
    "content": "package user\n\nimport (\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/olekukonko/tablewriter\"\n)\n\n// UserPrint will print more details\nfunc UserPrint(users []dto.User, w io.Writer) error {\n\ttw := tablewriter.NewWriter(w)\n\ttw.SetHeader([]string{\"ID\", \"Name\", \"Email\", \"Status\", \"TimeZone\"})\n\n\tlines := make([][]string, len(users))\n\tfor i := 0; i < len(users); i++ {\n\t\tlines[i] = []string{\n\t\t\tusers[i].ID,\n\t\t\tusers[i].Name,\n\t\t\tusers[i].Email,\n\t\t\tstring(users[i].Status),\n\t\t\tusers[i].Settings.TimeZone,\n\t\t}\n\t}\n\n\ttw.AppendBulk(lines)\n\ttw.Render()\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/output/user/json.go",
    "content": "package user\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// UserJSONPrint will print the user as a JSON\nfunc UserJSONPrint(u dto.User, w io.Writer) error {\n\treturn json.NewEncoder(w).Encode(u)\n}\n"
  },
  {
    "path": "pkg/output/user/quiet.go",
    "content": "package user\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// UserPrintQuietly will only print the IDs\nfunc UserPrintQuietly(users []dto.User, w io.Writer) error {\n\tfor i := 0; i < len(users); i++ {\n\t\tif _, err := fmt.Fprintln(w, users[i].ID); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/output/user/template.go",
    "content": "package user\n\nimport (\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/output/util\"\n)\n\n// UserPrintWithTemplate will print each worspace using the format string\nfunc UserPrintWithTemplate(format string) func([]dto.User, io.Writer) error {\n\treturn func(users []dto.User, w io.Writer) error {\n\t\tt, err := util.NewTemplate(format)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor i := 0; i < len(users); i++ {\n\t\t\tif err := t.Execute(w, users[i]); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "pkg/output/util/color.go",
    "content": "package util\n\nimport (\n\t\"os\"\n\n\t\"github.com/lucassabreu/clockify-cli/pkg/ui\"\n)\n\n// ColorToTermColor coverts HEX color to term colors\nfunc ColorToTermColor(hex string) []int {\n\tif hex == \"\" {\n\t\treturn []int{}\n\t}\n\n\tfi, _ := os.Stdout.Stat()\n\tif fi.Mode()&os.ModeCharDevice == 0 {\n\t\treturn []int{}\n\t}\n\n\tif c, err := ui.HEX(hex[1:]); err == nil {\n\t\treturn append(\n\t\t\t[]int{38, 2},\n\t\t\tc.Values()...,\n\t\t)\n\t}\n\n\treturn []int{}\n}\n"
  },
  {
    "path": "pkg/output/util/template.go",
    "content": "package util\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc formatTime(f string) func(time.Time) string {\n\treturn func(t time.Time) string {\n\t\treturn t.Format(f)\n\t}\n}\n\nvar funcMap = template.FuncMap{\n\t\"formatDateTime\": formatTime(timehlp.FullTimeFormat),\n\t\"fdt\":            formatTime(timehlp.FullTimeFormat),\n\t\"formatTime\":     formatTime(timehlp.OnlyTimeFormat),\n\t\"formatTimeWS\":   formatTime(timehlp.SimplerOnlyTimeFormat),\n\t\"ft\":             formatTime(timehlp.OnlyTimeFormat),\n\t\"now\": func(t *time.Time) time.Time {\n\t\tif t == nil {\n\t\t\treturn timehlp.Now().UTC()\n\t\t}\n\n\t\treturn *t\n\t},\n\t\"json\": func(j interface{}) string {\n\t\tw := bytes.NewBufferString(\"\")\n\t\tif err := json.NewEncoder(w).Encode(j); err != nil {\n\t\t\treturn \"\"\n\t\t}\n\n\t\treturn w.String()\n\t},\n\t\"yaml\": func(j interface{}) string {\n\t\tw := bytes.NewBufferString(\"\")\n\t\tif err := yaml.NewEncoder(w).Encode(j); err != nil {\n\t\t\treturn \"\"\n\t\t}\n\n\t\treturn w.String()\n\t},\n\t\"pad\": strhlp.PadSpace,\n\t\"ident\": func(s, prefix string) string {\n\t\treturn prefix + strings.ReplaceAll(s, \"\\n\", \"\\n\"+prefix)\n\t},\n\t\"since\": func(s time.Time, e ...time.Time) dto.Duration {\n\t\treturn diff(s, firstOrNow(e))\n\t},\n\t\"until\": func(s time.Time, e ...time.Time) dto.Duration {\n\t\treturn diff(firstOrNow(e), s)\n\t},\n\t\"repeatString\": strings.Repeat,\n\t\"maxLength\": func(s ...string) int {\n\t\tlength := 0\n\t\tfor i := range s {\n\t\t\tl := len(s[i])\n\t\t\tif l > length {\n\t\t\t\tlength = l\n\t\t\t}\n\t\t}\n\n\t\treturn length\n\t},\n\t\"concat\": func(ss ...string) string {\n\t\tb := &strings.Builder{}\n\t\tfor _, s := range ss {\n\t\t\tb.WriteString(s)\n\t\t}\n\n\t\treturn b.String()\n\t},\n\t\"dsf\": func(ds string) string {\n\t\td, err := dto.StringToDuration(ds)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\treturn dto.Duration{Duration: d}.HumanString()\n\t},\n}\n\nfunc firstOrNow(ts []time.Time) time.Time {\n\tif len(ts) == 0 {\n\t\treturn timehlp.Now().UTC()\n\t}\n\treturn ts[0]\n}\n\nfunc diff(s, e time.Time) dto.Duration {\n\treturn dto.Duration{Duration: e.Sub(s)}\n}\n\nfunc NewTemplate(format string) (*template.Template, error) {\n\tformat = strings.ReplaceAll(format, \"\\\\n\", \"\\n\")\n\tformat = strings.ReplaceAll(format, \"\\\\t\", \"\\t\")\n\treturn template.New(\"tmpl\").Funcs(funcMap).Parse(format + \"\\n\")\n}\n"
  },
  {
    "path": "pkg/output/workspace/default.go",
    "content": "package workspace\n\nimport (\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/olekukonko/tablewriter\"\n)\n\n// WorkspacePrint will print more details\nfunc WorkspacePrint(\n\twDefault string) func(ws []dto.Workspace, w io.Writer) error {\n\treturn func(ws []dto.Workspace, w io.Writer) error {\n\t\ttw := tablewriter.NewWriter(w)\n\t\ttw.SetHeader([]string{\"ID\", \"Name\", \"Image\"})\n\n\t\tlines := make([][]string, len(ws))\n\t\tfor i := 0; i < len(ws); i++ {\n\t\t\tlines[i] = []string{\n\t\t\t\tws[i].ID,\n\t\t\t\tws[i].Name,\n\t\t\t\tws[i].ImageURL,\n\t\t\t}\n\t\t\tif wDefault == ws[i].ID {\n\t\t\t\tlines[i][1] = lines[i][1] + \" (default)\"\n\t\t\t}\n\t\t}\n\n\t\ttw.AppendBulk(lines)\n\t\ttw.Render()\n\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "pkg/output/workspace/quiet.go",
    "content": "package workspace\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n)\n\n// WorkspacePrintQuietly will only print the IDs\nfunc WorkspacePrintQuietly(ws []dto.Workspace, w io.Writer) error {\n\tfor i := 0; i < len(ws); i++ {\n\t\tif _, err := fmt.Fprintln(w, ws[i].ID); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/output/workspace/template.go",
    "content": "package workspace\n\nimport (\n\t\"io\"\n\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/output/util\"\n)\n\n// WorkspacePrintWithTemplate will print each worspace using the format string\nfunc WorkspacePrintWithTemplate(\n\tformat string) func([]dto.Workspace, io.Writer) error {\n\treturn func(ws []dto.Workspace, w io.Writer) error {\n\t\tt, err := util.NewTemplate(format)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor i := 0; i < len(ws); i++ {\n\t\t\tif err := t.Execute(w, ws[i]); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "pkg/search/client.go",
    "content": "package search\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// GetClientsByName receives a list of id or names of clients and returns their\n// ids\nfunc GetClientsByName(\n\tc api.Client,\n\tworkspace string,\n\tclients []string,\n) ([]string, error) {\n\tif len(clients) == 0 {\n\t\treturn clients, nil\n\t}\n\n\tcs, err := c.GetClients(api.GetClientsParam{\n\t\tWorkspace:       workspace,\n\t\tPaginationParam: api.AllPages(),\n\t})\n\tif err != nil {\n\t\treturn clients, err\n\t}\n\n\tns := make([]named, len(cs))\n\tfor i := 0; i < len(ns); i++ {\n\t\tns[i] = cs[i]\n\t}\n\n\tvar g errgroup.Group\n\tfor i := 0; i < len(clients); i++ {\n\t\tj := i\n\t\tg.Go(func() error {\n\t\t\tid, err := findByName(\n\t\t\t\tclients[j],\n\t\t\t\t\"client\", func() ([]named, error) { return ns, nil },\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tclients[j] = id\n\t\t\treturn nil\n\t\t})\n\t}\n\n\treturn clients, g.Wait()\n}\n\n// GetClientByName will look for a client that the id or name Contains the\n// string on client parameter\nfunc GetClientByName(\n\tc api.Client,\n\tworkspace string,\n\tclient string,\n) (string, error) {\n\treturn findByName(\n\t\tclient,\n\t\t\"client\", func() ([]named, error) {\n\n\t\t\tcs, err := c.GetClients(api.GetClientsParam{\n\t\t\t\tWorkspace:       workspace,\n\t\t\t\tPaginationParam: api.AllPages(),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn []named{}, err\n\t\t\t}\n\n\t\t\tns := make([]named, len(cs))\n\t\t\tfor i := 0; i < len(ns); i++ {\n\t\t\t\tns[i] = cs[i]\n\t\t\t}\n\t\t\treturn ns, nil\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "pkg/search/errors.go",
    "content": "package search\n\nimport (\n\t\"sort\"\n\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n)\n\n// ErrNotFound represents a fail to identify a entity by its name or id\ntype ErrNotFound struct {\n\tEntityName string\n\tReference  string\n\tFilters    map[string]string\n}\n\nfunc (e ErrNotFound) Error() string {\n\tsufix := \"\"\n\tif len(e.Filters) > 0 {\n\t\tsufix = \" for \"\n\t\tkeys := make([]string, len(e.Filters))\n\t\ti := 0\n\t\tfor k := range e.Filters {\n\t\t\tkeys[i] = k\n\t\t\ti++\n\t\t}\n\n\t\tsort.Strings(keys)\n\t\tfor i := range keys {\n\t\t\tkeys[i] = keys[i] + \" '\" + e.Filters[keys[i]] + \"'\"\n\t\t}\n\n\t\tsufix = sufix + strhlp.ListForHumans(keys)\n\t}\n\n\treturn \"No \" + e.EntityName + \" with id or name containing '\" +\n\t\te.Reference + \"' was found\" + sufix\n}\n"
  },
  {
    "path": "pkg/search/find.go",
    "content": "package search\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n)\n\ntype named interface {\n\tGetID() string\n\tGetName() string\n}\n\nvar ErrEmptyReference = errors.New(\"no reference informed\")\n\nfunc findByName(\n\tr, entityName string, fn func() ([]named, error)) (string, error) {\n\tname := strhlp.Normalize(strings.TrimSpace(r))\n\tif name == \"\" {\n\t\treturn r, ErrEmptyReference\n\t}\n\n\tl, err := fn()\n\tif err != nil {\n\t\treturn r, err\n\t}\n\n\tisSimilar := strhlp.IsSimilar(name)\n\tfor _, e := range l {\n\t\tif strings.ToLower(e.GetID()) == name {\n\t\t\treturn e.GetID(), nil\n\t\t}\n\n\t\tif isSimilar(e.GetName()) {\n\t\t\treturn e.GetID(), nil\n\t\t}\n\t}\n\n\treturn r, ErrNotFound{\n\t\tEntityName: entityName,\n\t\tReference:  r,\n\t}\n}\n"
  },
  {
    "path": "pkg/search/find_test.go",
    "content": "package search\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSearchOnList(t *testing.T) {\n\tentities := []named{\n\t\tnamedStruct{\n\t\t\tID:   \"1\",\n\t\t\tName: \"entity one\",\n\t\t},\n\t\tnamedStruct{\n\t\t\tID:   \"2\",\n\t\t\tName: \"entity two\",\n\t\t},\n\t\tnamedStruct{\n\t\t\tID:   \"3\",\n\t\t\tName: \"entity three\",\n\t\t},\n\t\tnamedStruct{\n\t\t\tID:   \"4\",\n\t\t\tName: \"more complex name\",\n\t\t},\n\t\tnamedStruct{\n\t\t\tID:   \"id\",\n\t\t\tName: \"by id\",\n\t\t},\n\t\tnamedStruct{\n\t\t\tID:   \"bra\",\n\t\t\tName: \"with [bracket]\",\n\t\t},\n\t}\n\n\ttts := []struct {\n\t\tname     string\n\t\tsearch   string\n\t\tentities []named\n\t\tresult   string\n\t}{\n\t\t{\n\t\t\tname:     \"one term\",\n\t\t\tsearch:   \"two\",\n\t\t\tentities: entities,\n\t\t\tresult:   \"2\",\n\t\t},\n\t\t{\n\t\t\tname:     \"two terms\",\n\t\t\tsearch:   \"complex name\",\n\t\t\tentities: entities,\n\t\t\tresult:   \"4\",\n\t\t},\n\t\t{\n\t\t\tname:     \"sections of the name\",\n\t\t\tsearch:   \"mo nam\",\n\t\t\tentities: entities,\n\t\t\tresult:   \"4\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with brackets\",\n\t\t\tsearch:   \"[bracket]\",\n\t\t\tentities: entities,\n\t\t\tresult:   \"bra\",\n\t\t},\n\t\t{\n\t\t\tname:     \"using id\",\n\t\t\tsearch:   \"by id\",\n\t\t\tentities: entities,\n\t\t\tresult:   \"id\",\n\t\t},\n\t}\n\n\tfor i := range tts {\n\t\ttt := tts[i]\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tid, err := findByName(tt.search, \"element\", func() ([]named, error) {\n\t\t\t\treturn tt.entities, nil\n\t\t\t})\n\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Equal(t, tt.result, id)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/search/project.go",
    "content": "package search\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/cmdutil\"\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nfunc GetProjectByName(\n\tc api.Client,\n\tcnf cmdutil.Config,\n\tworkspace string,\n\tproject string,\n\tclient string,\n) (string, error) {\n\tps, err := c.GetProjects(api.GetProjectsParam{\n\t\tWorkspace:       workspace,\n\t\tPaginationParam: api.AllPages(),\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif ps, err = filterClientProjects(ps, client); err != nil {\n\t\treturn \"\", err\n\t}\n\n\ttoNamed := func(p dto.Project) named { return p }\n\tif cnf.IsSearchProjectWithClientsName() {\n\t\ttoNamed = func(p dto.Project) named {\n\t\t\treturn namedStruct{\n\t\t\t\tID:   p.ID,\n\t\t\t\tName: p.Name + \" \" + p.ClientName,\n\t\t\t}\n\t\t}\n\t}\n\n\tid, err := findByName(project, \"project\", func() ([]named, error) {\n\t\tns := make([]named, len(ps))\n\t\tfor i := 0; i < len(ps); i++ {\n\t\t\tns[i] = toNamed(ps[i])\n\t\t}\n\n\t\treturn ns, nil\n\t})\n\n\tvar eNotFound ErrNotFound\n\tif errors.As(err, &eNotFound) {\n\t\tif client == \"\" {\n\t\t\treturn id, err\n\t\t}\n\n\t\treturn id, ErrNotFound{\n\t\t\tEntityName: eNotFound.EntityName,\n\t\t\tReference:  eNotFound.Reference,\n\t\t\tFilters: map[string]string{\n\t\t\t\t\"client\": client,\n\t\t\t},\n\t\t}\n\t}\n\n\treturn id, err\n}\n\ntype namedStruct struct {\n\tID   string\n\tName string\n}\n\nfunc (c namedStruct) GetID() string {\n\treturn c.ID\n}\n\nfunc (c namedStruct) GetName() string {\n\treturn c.Name\n}\n\nfunc filterClientProjects(\n\tps []dto.Project,\n\tclient string,\n) ([]dto.Project, error) {\n\tif client == \"\" {\n\t\treturn ps, nil\n\t}\n\n\tclients := make([]named, len(ps))\n\tfor i := 0; i < len(ps); i++ {\n\t\tclients[i] = namedStruct{\n\t\t\tID:   ps[i].ClientID,\n\t\t\tName: ps[i].ClientName,\n\t\t}\n\t}\n\n\tid, err := findByName(client, \"client\",\n\t\tfunc() ([]named, error) { return clients, nil })\n\n\tif err != nil {\n\t\treturn ps, err\n\t}\n\n\tfPs := make([]dto.Project, 0)\n\tfor i := 0; i < len(ps); i++ {\n\t\tif ps[i].ClientID != id {\n\t\t\tcontinue\n\t\t}\n\n\t\tfPs = append(fPs, ps[i])\n\t}\n\n\treturn fPs, nil\n}\n\n// GetProjectsByName will try to find projects containing the string on its\n// name or id that matches the value\nfunc GetProjectsByName(\n\tc api.Client,\n\tcnf cmdutil.Config,\n\tworkspace string,\n\tclient string,\n\tprojects []string,\n) ([]string, error) {\n\tif len(projects) == 0 {\n\t\treturn projects, nil\n\t}\n\n\tps, err := c.GetProjects(api.GetProjectsParam{\n\t\tWorkspace:       workspace,\n\t\tPaginationParam: api.AllPages(),\n\t})\n\tif err != nil {\n\t\treturn projects, err\n\t}\n\n\tif ps, err = filterClientProjects(ps, client); err != nil {\n\t\treturn projects, err\n\t}\n\n\ttoNamed := func(p dto.Project) named { return p }\n\tif cnf.IsSearchProjectWithClientsName() {\n\t\ttoNamed = func(p dto.Project) named {\n\t\t\treturn namedStruct{\n\t\t\t\tID:   p.ID,\n\t\t\t\tName: p.Name + \" \" + p.ClientName,\n\t\t\t}\n\t\t}\n\t}\n\n\tns := make([]named, len(ps))\n\tfor i := 0; i < len(ns); i++ {\n\t\tns[i] = toNamed(ps[i])\n\t}\n\n\tvar g errgroup.Group\n\tfor i := 0; i < len(projects); i++ {\n\t\tj := i\n\t\tg.Go(func() error {\n\t\t\tid, err := findByName(\n\t\t\t\tprojects[j],\n\t\t\t\t\"project\", func() ([]named, error) { return ns, nil },\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tprojects[j] = id\n\t\t\treturn nil\n\t\t})\n\t}\n\n\terr = g.Wait()\n\tvar eNotFound ErrNotFound\n\tif client != \"\" && errors.As(err, &eNotFound) {\n\t\terr = ErrNotFound{\n\t\t\tEntityName: eNotFound.EntityName,\n\t\t\tReference:  eNotFound.Reference,\n\t\t\tFilters: map[string]string{\n\t\t\t\t\"client\": client,\n\t\t\t},\n\t\t}\n\t}\n\n\treturn projects, err\n}\n"
  },
  {
    "path": "pkg/search/tag.go",
    "content": "package search\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// GetTagsByName receives a list of id or names of tags and returns their ids\nfunc GetTagsByName(\n\tc api.Client,\n\tworkspace string,\n\ttags []string,\n) ([]string, error) {\n\tif len(tags) == 0 {\n\t\treturn tags, nil\n\t}\n\n\tts, err := c.GetTags(api.GetTagsParam{\n\t\tWorkspace:       workspace,\n\t\tPaginationParam: api.AllPages(),\n\t})\n\tif err != nil {\n\t\treturn tags, err\n\t}\n\n\tns := make([]named, len(ts))\n\tfor i := 0; i < len(ns); i++ {\n\t\tns[i] = ts[i]\n\t}\n\n\tvar g errgroup.Group\n\tfor i := 0; i < len(tags); i++ {\n\t\tj := i\n\t\tg.Go(func() error {\n\t\t\tid, err := findByName(\n\t\t\t\ttags[j],\n\t\t\t\t\"tag\", func() ([]named, error) { return ns, nil },\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\ttags[j] = id\n\t\t\treturn nil\n\t\t})\n\t}\n\n\treturn tags, g.Wait()\n}\n"
  },
  {
    "path": "pkg/search/task.go",
    "content": "package search\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// GetTaskByName will try to find the first task containing the string on its\n// name or id that matches the value\nfunc GetTaskByName(\n\tc api.Client,\n\tf api.GetTasksParam,\n\ttask string,\n) (string, error) {\n\treturn findByName(task, \"task\", func() ([]named, error) {\n\t\tf.PaginationParam = api.AllPages()\n\t\tts, err := c.GetTasks(f)\n\t\tif err != nil {\n\t\t\treturn []named{}, err\n\t\t}\n\n\t\tns := make([]named, len(ts))\n\t\tfor i := 0; i < len(ns); i++ {\n\t\t\tns[i] = ts[i]\n\t\t}\n\n\t\treturn ns, nil\n\t})\n}\n\n// GetTasksByName will try to find tasks containing the string on its name or\n// id that matches the value\nfunc GetTasksByName(\n\tc api.Client,\n\tf api.GetTasksParam,\n\ttasks []string,\n) ([]string, error) {\n\tif len(tasks) == 0 {\n\t\treturn tasks, nil\n\t}\n\n\tf.PaginationParam = api.AllPages()\n\tts, err := c.GetTasks(f)\n\tif err != nil {\n\t\treturn tasks, err\n\t}\n\n\tns := make([]named, len(ts))\n\tfor i := 0; i < len(ns); i++ {\n\t\tns[i] = ts[i]\n\t}\n\n\tvar g errgroup.Group\n\tfor i := 0; i < len(tasks); i++ {\n\t\tj := i\n\t\tg.Go(func() error {\n\t\t\tid, err := findByName(\n\t\t\t\ttasks[j],\n\t\t\t\t\"task\", func() ([]named, error) { return ns, nil },\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\ttasks[j] = id\n\t\t\treturn nil\n\t\t})\n\t}\n\n\treturn tasks, g.Wait()\n}\n"
  },
  {
    "path": "pkg/search/user.go",
    "content": "package search\n\nimport (\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// GetUsersByName receives a list of id or names of clients and returns their\n// ids\nfunc GetUsersByName(\n\tc api.Client,\n\tworkspace string,\n\tusers []string,\n) ([]string, error) {\n\tif len(users) == 0 {\n\t\treturn users, nil\n\t}\n\n\tus, err := c.WorkspaceUsers(api.WorkspaceUsersParam{\n\t\tWorkspace:       workspace,\n\t\tPaginationParam: api.AllPages(),\n\t})\n\tif err != nil {\n\t\treturn users, err\n\t}\n\n\tns := make([]named, len(us))\n\tfor i := 0; i < len(ns); i++ {\n\t\tns[i] = us[i]\n\t}\n\n\tvar g errgroup.Group\n\tfor i := 0; i < len(users); i++ {\n\t\tj := i\n\t\tg.Go(func() error {\n\t\t\tid, err := findByName(\n\t\t\t\tusers[j], \"user\",\n\t\t\t\tfunc() ([]named, error) { return ns, nil },\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tusers[j] = id\n\t\t\treturn nil\n\t\t})\n\t}\n\n\treturn users, g.Wait()\n}\n"
  },
  {
    "path": "pkg/timeentryhlp/timeentry.go",
    "content": "package timeentryhlp\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/lucassabreu/clockify-cli/api\"\n\t\"github.com/lucassabreu/clockify-cli/api/dto\"\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/pkg/errors\"\n)\n\nconst (\n\tAliasCurrent = \"current\"\n\tAliasLast    = \"last\"\n\tAliasLatest  = \"latest\"\n)\n\n// GetLatestEntryEntry will return the last time entry of a user, if it exists\nfunc GetLatestEntryEntry(\n\tc api.Client, workspace, userID string) (dto.TimeEntryImpl, error) {\n\treturn GetTimeEntry(c, workspace, userID, AliasLatest)\n}\n\nvar ErrNoTimeEntry = errors.New(\"time entry was not found\")\n\nfunc mayNotFound(tei *dto.TimeEntryImpl, err error) (\n\tdto.TimeEntryImpl, error) {\n\tif err != nil {\n\t\treturn dto.TimeEntryImpl{}, err\n\t}\n\n\tif tei == nil {\n\t\treturn dto.TimeEntryImpl{}, ErrNoTimeEntry\n\t}\n\n\treturn *tei, nil\n}\n\n// GetTimeEntry will look for the time entry of a user for the id or alias\n// provided\nfunc GetTimeEntry(\n\tc api.Client,\n\tworkspace,\n\tuserID,\n\tid string,\n) (dto.TimeEntryImpl, error) {\n\tid = strings.TrimSpace(strings.ToLower(id))\n\n\tvar onlyInProgress *bool\n\tswitch id {\n\tcase \"^0\", AliasCurrent:\n\t\ttei, err := mayNotFound(c.GetTimeEntryInProgress(\n\t\t\tapi.GetTimeEntryInProgressParam{\n\t\t\t\tWorkspace: workspace,\n\t\t\t\tUserID:    userID,\n\t\t\t}))\n\t\tif err == ErrNoTimeEntry {\n\t\t\treturn tei, errors.Wrap(err, \"looking for running time entry\")\n\t\t}\n\n\t\treturn tei, err\n\tcase \"^1\", AliasLast:\n\t\tid = AliasLast\n\t\tb := false\n\t\tonlyInProgress = &b\n\tcase AliasLatest:\n\t\tid = AliasLatest\n\t\tonlyInProgress = nil\n\t}\n\n\tif id != AliasLast && id != AliasLatest && !strings.HasPrefix(id, \"^\") {\n\t\treturn mayNotFound(c.GetTimeEntry(api.GetTimeEntryParam{\n\t\t\tWorkspace:   workspace,\n\t\t\tTimeEntryID: id,\n\t\t}))\n\t}\n\n\tpage := 1\n\tif strings.HasPrefix(id, \"^\") {\n\t\tvar err error\n\t\tif page, err = strconv.Atoi(id[1:]); err != nil {\n\t\t\treturn dto.TimeEntryImpl{}, fmt.Errorf(\n\t\t\t\t`n on \"^n\" must be a unsigned integer, you sent: %s`,\n\t\t\t\tid[1:],\n\t\t\t)\n\t\t}\n\t}\n\n\tnow := timehlp.Now()\n\tlist, err := c.GetUserTimeEntries(api.GetUserTimeEntriesParam{\n\t\tWorkspace:      workspace,\n\t\tUserID:         userID,\n\t\tOnlyInProgress: onlyInProgress,\n\t\tEnd:            &now,\n\t\tPaginationParam: api.PaginationParam{\n\t\t\tPageSize: 1,\n\t\t\tPage:     page,\n\t\t},\n\t})\n\n\tif err != nil {\n\t\treturn dto.TimeEntryImpl{}, err\n\t}\n\n\tif len(list) == 0 {\n\t\treturn dto.TimeEntryImpl{}, ErrNoTimeEntry\n\t}\n\n\treturn list[0], err\n}\n"
  },
  {
    "path": "pkg/timehlp/range.go",
    "content": "package timehlp\n\nimport \"time\"\n\n// GetMonthRange given a time it returns the first and last date of a month\nfunc GetMonthRange(ref time.Time) (first, last time.Time) {\n\tfirst = ref.AddDate(0, 0, ref.Day()*-1+1)\n\tlast = first.AddDate(0, 1, -1)\n\n\treturn\n}\n\n// GetWeekRange given a time it returns the first and last date of a week\nfunc GetWeekRange(ref time.Time) (first, last time.Time) {\n\tfirst = ref.AddDate(0, 0, int(ref.Weekday())*-1)\n\tlast = first.AddDate(0, 0, 7)\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/timehlp/relative.go",
    "content": "package timehlp\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n)\n\nvar ErrInvalidReliveTime = errors.New(\n\t\"supported relative time formats are: \" +\n\t\t\"+15:04:05, +15:04 or unit descriptive +1d15h4m5s, \" +\n\t\t\"+15h5s, 120m\",\n)\n\nfunc relativeToTime(timeString string) (t time.Time, err error) {\n\tvar d time.Duration\n\ttimeString = strings.ReplaceAll(timeString, \" \", \"\")\n\n\tif c := strings.Count(timeString, \":\"); c > 0 {\n\t\td, err = relativeColonTimeToDuration(timeString[1:])\n\t} else {\n\t\td, err = relativeUnitDescriptiveTimeToDuration(timeString[1:])\n\t}\n\n\tif timeString[0] == '-' {\n\t\td = d * -1\n\t}\n\n\tt = Now().Add(d)\n\treturn\n}\n\nfunc relativeColonTimeToDuration(s string) (d time.Duration, err error) {\n\tparts := strings.Split(s, \":\")\n\tc := len(parts)\n\tif c > 2 || c == 0 {\n\t\treturn d, ErrInvalidReliveTime\n\t}\n\n\tu := time.Second\n\tfor i := c - 1; i >= 0; i-- {\n\t\tp := strings.TrimPrefix(parts[i], \"0\")\n\t\tv, err := strconv.Atoi(p)\n\t\tif err != nil && p != \"\" {\n\t\t\treturn d, ErrInvalidReliveTime\n\t\t}\n\t\td = d + time.Duration(v)*u\n\t\tu = u * 60\n\t}\n\n\treturn\n}\n\nfunc relativeUnitDescriptiveTimeToDuration(s string) (\n\td time.Duration, err error) {\n\tvar u time.Duration\n\tvar i, j int\n\tfor ; i < len(s); i++ {\n\t\tswitch s[i] {\n\t\tcase 'd':\n\t\t\tu = time.Hour * 24\n\t\tcase 'h':\n\t\t\tu = time.Hour\n\t\tcase 'm':\n\t\t\tu = time.Minute\n\t\tcase 's':\n\t\t\tu = time.Second\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\n\t\tv, err := strconv.Atoi(s[j:i])\n\t\tif err != nil {\n\t\t\treturn d, ErrInvalidReliveTime\n\t\t}\n\n\t\td = d + time.Duration(v)*u\n\t\tj = i + 1\n\t}\n\n\tif i != j {\n\t\treturn d, ErrInvalidReliveTime\n\t}\n\n\treturn d, nil\n}\n"
  },
  {
    "path": "pkg/timehlp/time.go",
    "content": "package timehlp\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n)\n\nconst (\n\tFullTimeFormat           = \"2006-01-02 15:04:05\"\n\tSimplerTimeFormat        = \"2006-01-02 15:04\"\n\tOnlyTimeFormat           = \"15:04:05\"\n\tSimplerOnlyTimeFormat    = \"15:04\"\n\tSimplerOnlyTimeFormatWL  = \"5:04\"\n\tNowTimeFormat            = \"now\"\n\tSimplestOnlyTimeFormat   = \"1504\"\n\tSimplestOnlyTimeFormatWL = \"504\"\n)\n\n// ConvertToTime will try to convert a string do time.Time looking for the\n// format that best fits it and assuming \"today\" when necessary.\n// If the string starts with `yesterday`, than it will be exchanged for a\n// date-string with the format: 2006-01-02\n// If the string starts with `+` or `-` than the string will be treated as\n// \"relative time expressions\", and will be calculated as the diff from now and\n// it.\n// If the string is \"now\" than `time.Now()` in the local timezone will be\n// returned.\nfunc ConvertToTime(timeString string) (t time.Time, err error) {\n\ttimeString = strings.ToLower(strings.TrimSpace(timeString))\n\n\tif NowTimeFormat == timeString {\n\t\treturn Now(), nil\n\t}\n\n\tif strings.HasPrefix(timeString, \"+\") ||\n\t\tstrings.HasPrefix(timeString, \"-\") {\n\t\treturn relativeToTime(timeString)\n\t}\n\n\tif strings.HasPrefix(timeString, \"yesterday \") {\n\t\ttimeString = Today().\n\t\t\tAdd(-1).Format(\"2006-01-02\") + \" \" + timeString[10:]\n\t}\n\n\tl := len(timeString)\n\tif len(FullTimeFormat) != l &&\n\t\tlen(SimplerTimeFormat) != l &&\n\t\tlen(OnlyTimeFormat) != l &&\n\t\tlen(SimplerOnlyTimeFormat) != l &&\n\t\tlen(SimplestOnlyTimeFormat) != l &&\n\t\tlen(SimplestOnlyTimeFormatWL) != l {\n\t\treturn t, fmt.Errorf(\n\t\t\t\"supported formats are: %s\",\n\t\t\tstrings.Join(\n\t\t\t\t[]string{\n\t\t\t\t\tFullTimeFormat, SimplerTimeFormat, OnlyTimeFormat,\n\t\t\t\t\tSimplerOnlyTimeFormat, SimplerOnlyTimeFormatWL, NowTimeFormat,\n\t\t\t\t\tSimplestOnlyTimeFormat, SimplestOnlyTimeFormatWL,\n\t\t\t\t},\n\t\t\t\t\", \",\n\t\t\t),\n\t\t)\n\t}\n\n\ttimeString = normalizeFormats(timeString)\n\tt, err = time.ParseInLocation(FullTimeFormat, timeString, time.Local)\n\tif err != nil {\n\t\treturn t, err\n\t}\n\n\treturn t.Truncate(time.Second), nil\n}\n\n// Adds data to the partial timeString to match a full\n// datetime with seconds precission.\n// Receives a time in any of the defined formats, and return\n// a date in the FullTimeFormat\nfunc normalizeFormats(timeString string) string {\n\tl := len(timeString)\n\n\t// change from 9:14 to 09:14\n\tif len(SimplerOnlyTimeFormatWL) == l && strings.Contains(timeString, \":\") {\n\t\ttimeString = \"0\" + timeString\n\t\tl = l + 1\n\t}\n\n\t// change from 914 to 0914\n\tif len(SimplestOnlyTimeFormatWL) == l && !strings.Contains(timeString, \":\") {\n\t\ttimeString = \"0\" + timeString\n\t\tl = l + 1\n\t}\n\n\t// change from 0914 to 09:14\n\tif len(SimplestOnlyTimeFormat) == l {\n\t\ttimeString = timeString[0:2] + \":\" + timeString[2:]\n\t\tl = l + 1\n\t}\n\n\t// change from 09:14 to 09:14:00\n\tif len(SimplerOnlyTimeFormat) == l || len(SimplerTimeFormat) == l {\n\t\ttimeString = timeString + \":00\"\n\t\tl = l + 3\n\t}\n\n\t// change from 09:14 to 2006-01-02 09:14:00\n\tif len(OnlyTimeFormat) == l {\n\t\ttimeString = Today().Format(\"2006-01-02\") + \" \" + timeString\n\t}\n\treturn timeString\n}\n"
  },
  {
    "path": "pkg/timehlp/time_test.go",
    "content": "package timehlp_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/pkg/timehlp\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParseTime(t *testing.T) {\n\tnow := timehlp.Today()\n\tnowStr := now.Format(\"2006-01-02\")\n\n\tt.Run(\"now\", func(t *testing.T) {\n\t\t// this case is special because it is not deterministic\n\t\tparsed, err := timehlp.ConvertToTime(\"now\")\n\n\t\tassert.Nil(t, err)\n\t\tassert.Equal(t, nowStr, parsed.Format(\"2006-01-02\"))\n\n\t})\n\n\ttts := []struct {\n\t\tname     string\n\t\texpected string\n\t\ttoParse  string\n\t}{\n\t\t{name: \"FullTimeFormat\", expected: \"09:59:01\", toParse: fmt.Sprintf(\"%s %s\", nowStr, \"09:59:01\")},\n\t\t{name: \"SimplerTimeFormat\", expected: \"09:59:00\", toParse: fmt.Sprintf(\"%s %s\", nowStr, \"09:59\")},\n\t\t{name: \"OnlyTimeFormat\", expected: \"16:03:02\", toParse: \"16:03:02\"},\n\t\t{name: \"SimplerOnlyTimeFormat\", expected: \"16:03:00\", toParse: \"16:03\"},\n\t\t{name: \"SimplerOnlyTimeFormat\", expected: \"06:03:00\", toParse: \"06:03\"},\n\t\t{name: \"SimplerOnlyTimeFormatWL\", expected: \"06:03:00\", toParse: \"6:03\"},\n\t\t{name: \"SimplestOnlyTimeFormat\", expected: \"16:03:00\", toParse: \"1603\"},\n\t\t{name: \"SimplestOnlyTimeFormatWL\", expected: \"06:03:00\", toParse: \"603\"},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\n\t\t\tparsed, err := timehlp.ConvertToTime(tt.toParse)\n\n\t\t\tassert.Nil(t, err)\n\t\t\tassert.Equal(t, fmt.Sprintf(\"%s %s\", nowStr, tt.expected), parsed.Format(\"2006-01-02 15:04:05\"))\n\t\t})\n\t}\n}\n\nfunc TestFailParseTime(t *testing.T) {\n\t_, err := timehlp.ConvertToTime(\"2024-05-25 25:61\")\n\tassert.Error(t, err,\n\t\t\"parsing time \\\"2024-05-25 25:61:00\\\": hour out of range\")\n}\n"
  },
  {
    "path": "pkg/timehlp/util.go",
    "content": "package timehlp\n\nimport \"time\"\n\n// TruncateDate clears the hours, minutes and seconds of a time.Time for UTC\nfunc TruncateDate(t time.Time) time.Time {\n\treturn TruncateDateWithTimezone(t, time.UTC)\n}\n\n// TruncateDateWithTimezone clears the hours, minutes and seconds of a\n// time.Time for a time.Location\nfunc TruncateDateWithTimezone(t time.Time, l *time.Location) time.Time {\n\treturn time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, l).\n\t\tTruncate(time.Second)\n}\n\n// Today will return a UTC time.Time for the same day as time.Now() in Local\n// time, but at 0:00:00.000\nfunc Today() time.Time {\n\tn := Now()\n\treturn TruncateDateWithTimezone(n, n.Location())\n}\n\n// Now returns a time.Time using the local timezone\nfunc Now() time.Time {\n\treturn time.Now().In(time.Local).Truncate(time.Second)\n}\n"
  },
  {
    "path": "pkg/ui/color.go",
    "content": "package ui\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n)\n\nfunc HEX(hex string) (RGB, error) {\n\tif len(hex) != 6 {\n\t\treturn RGB{}, errors.New(\"HEX must be 6 characters\")\n\t}\n\n\tvalues, err := strconv.ParseUint(string(hex), 16, 32)\n\n\tif err != nil {\n\t\treturn RGB{}, err\n\t}\n\n\treturn RGB{\n\t\tint(values >> 16),\n\t\tint((values >> 8) & 0xFF),\n\t\tint(values & 0xFF),\n\t}, nil\n}\n\ntype RGB [3]int\n\nfunc (c RGB) R() int {\n\treturn c[0]\n}\n\nfunc (c RGB) G() int {\n\treturn c[1]\n}\n\nfunc (c RGB) B() int {\n\treturn c[2]\n}\n\nfunc (c RGB) Values() []int {\n\treturn []int{c.R(), c.G(), c.B()}\n}\n"
  },
  {
    "path": "pkg/ui/ui.go",
    "content": "package ui\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/AlecAivazis/survey/v2\"\n\t\"github.com/AlecAivazis/survey/v2/terminal\"\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/pkg/errors\"\n)\n\n// FileReader represents the input of a terminal\ntype FileReader interface {\n\tio.Reader\n\tFd() uintptr\n}\n\n// FileWriter represents the output of a terminal\ntype FileWriter interface {\n\tio.Writer\n\tFd() uintptr\n}\n\n// NewUI creates a new UI instance\nfunc NewUI(in FileReader, out FileWriter, err io.Writer) UI {\n\treturn &ui{\n\t\toptions: []survey.AskOpt{\n\t\t\tsurvey.WithStdio(in, out, err),\n\t\t},\n\t}\n}\n\n// UI provides functions to prompt information from a terminal\ntype UI interface {\n\t// SetPageSize changes how many entries are shown on AskFromOptions and\n\t// AskManyFromOptions at a time\n\tSetPageSize(p uint) UI\n\t// AskForText interactively ask for one string from the user\n\tAskForText(m string, opts ...InputOption) (string, error)\n\t// AskForValidText for a string interactively from the user and validates\n\t// it\n\tAskForValidText(\n\t\tm string, validate func(string) error, opts ...InputOption,\n\t) (string, error)\n\t// AskForDateTime interactively ask for one date and time from the user\n\tAskForDateTime(m, d string, ct convertTime) (time.Time, error)\n\t// AskForDateTimeOrNil interactively ask for one date and time from the\n\t// user, but allows a empty response\n\tAskForDateTimeOrNil(m, d string, ct convertTime) (*time.Time, error)\n\t// AskForInt interactively ask for one int from the user\n\tAskForInt(m string, d int) (int, error)\n\t// AskFromOptions interactively ask the user to choose one option or none\n\tAskFromOptions(m string, o []string, d string) (string, error)\n\t// AskManyFromOptions interactively ask the user to choose none or many\n\t// option\n\tAskManyFromOptions(\n\t\tm string, o, d []string, validade func([]string) error,\n\t) ([]string, error)\n\t// Confirm interactively ask the user a yes/no question\n\tConfirm(m string, d bool) (bool, error)\n}\n\ntype ui struct {\n\toptions []survey.AskOpt\n}\n\nfunc (u *ui) SetPageSize(p uint) UI {\n\tif p == 0 {\n\t\tp = 7\n\t}\n\tu.options = append(u.options, survey.WithPageSize(int(p)))\n\treturn u\n}\n\nfunc selectFilter(filter, value string, _ int) bool {\n\treturn strhlp.IsSimilar(filter)(value)\n}\n\nfunc askString(p survey.Prompt, options ...survey.AskOpt) (string, error) {\n\tanswer := \"\"\n\treturn answer, errors.WithStack(survey.AskOne(p, &answer, options...))\n}\n\n// WithSuggestion applies the suggestion function to the input question\nfunc WithSuggestion(fn func(toComplete string) []string) InputOption {\n\treturn func(i *survey.Input) {\n\t\ti.Suggest = fn\n\t}\n}\n\n// WithHelp add help to input question\nfunc WithHelp(help string) InputOption {\n\treturn func(i *survey.Input) {\n\t\ti.Help = help\n\t}\n}\n\n// WithDefault will set a default answer to the question\nfunc WithDefault(d string) InputOption {\n\treturn func(i *survey.Input) {\n\t\ti.Default = d\n\t}\n}\n\n// InputOption represets a funcion the customizes a survey.Input object\ntype InputOption func(*survey.Input)\n\n// AskForValidText for a string interactively from the user and validates it\nfunc (u *ui) AskForValidText(\n\tmessage string,\n\tvalidateFn func(string) error,\n\topts ...InputOption,\n) (string, error) {\n\ti := &survey.Input{\n\t\tMessage: message,\n\t}\n\n\tfor _, o := range opts {\n\t\to(i)\n\t}\n\n\tos := u.options\n\tif validateFn != nil {\n\t\tos = append(os, survey.WithValidator(func(ans interface{}) error {\n\t\t\treturn validateFn(ans.(string))\n\t\t}))\n\t}\n\n\treturn askString(i, os...)\n}\n\n// AskForText interactively ask for one string from the user\nfunc (u *ui) AskForText(message string, opts ...InputOption) (string, error) {\n\ti := &survey.Input{\n\t\tMessage: message,\n\t}\n\n\tfor _, o := range opts {\n\t\to(i)\n\t}\n\n\treturn askString(i, u.options...)\n}\n\ntype timeAnswer struct {\n\t*time.Time\n\tconvert func(string) (time.Time, error)\n}\n\nfunc (ans *timeAnswer) validate(v interface{}) error {\n\ts, ok := v.(string)\n\tif !ok || s == \"\" {\n\t\treturn nil\n\t}\n\n\t_, err := ans.convert(s)\n\treturn err\n}\n\nfunc (ans *timeAnswer) WriteAnswer(_ string, v interface{}) error {\n\ts, ok := v.(string)\n\tif !ok || s == \"\" {\n\t\treturn nil\n\t}\n\n\tt, err := ans.convert(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tans.Time = &t\n\treturn nil\n}\n\ntype convertTime func(string) (time.Time, error)\n\n// AskForDateTime interactively ask for one date and time from the user\nfunc (u *ui) AskForDateTime(\n\tname,\n\tvalue string,\n\tconvert convertTime,\n) (time.Time, error) {\n\ti := &survey.Input{\n\t\tMessage: name + \":\",\n\t\tDefault: value,\n\t}\n\n\tt := timeAnswer{convert: convert}\n\topts := make([]survey.AskOpt, 0)\n\topts = append(opts, u.options...)\n\topts = append(opts,\n\t\tsurvey.WithValidator(survey.Required),\n\t\tsurvey.WithValidator(t.validate),\n\t)\n\n\tfor {\n\t\terr := survey.AskOne(i, &t, opts...)\n\t\tif err == terminal.InterruptErr {\n\t\t\treturn time.Time{}, err\n\t\t}\n\n\t\tif t.Time != nil {\n\t\t\treturn *t.Time, err\n\t\t}\n\t}\n}\n\nfunc (u *ui) AskForDateTimeOrNil(\n\tname,\n\tvalue string,\n\tconvert convertTime,\n) (*time.Time, error) {\n\tt := timeAnswer{convert: convert}\n\topts := []survey.AskOpt{survey.WithValidator(t.validate)}\n\topts = append(opts, u.options...)\n\treturn t.Time, survey.AskOne(\n\t\t&survey.Input{\n\t\t\tMessage: name + \" (leave it blank for empty):\",\n\t\t\tDefault: value,\n\t\t},\n\t\t&t,\n\t\topts...,\n\t)\n}\n\n// AskForInt interactively ask for one int from the user\nfunc (u *ui) AskForInt(message string, d int) (int, error) {\n\topts := []survey.AskOpt{survey.WithValidator(func(ans interface{}) error {\n\t\tv, ok := ans.(string)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"needs to be a string\")\n\t\t}\n\n\t\t_, err := strconv.Atoi(v)\n\t\treturn err\n\t})}\n\topts = append(opts, u.options...)\n\treturn d, survey.AskOne(\n\t\t&survey.Input{\n\t\t\tMessage: message,\n\t\t\tDefault: strconv.Itoa(d),\n\t\t},\n\t\t&d,\n\t\topts...,\n\t)\n}\n\n// AskFromOptions interactively ask the user to choose one option or none\nfunc (u *ui) AskFromOptions(message string, options []string, d string) (string, error) {\n\tp := &survey.Select{\n\t\tMessage: message,\n\t\tOptions: options,\n\t\tFilter:  selectFilter,\n\t}\n\n\tif d != \"\" && strhlp.Search(d, options) != -1 {\n\t\tp.Default = d\n\t}\n\n\treturn askString(p, u.options...)\n}\n\n// AskManyFromOptions interactively ask the user to choose none or many option\nfunc (u *ui) AskManyFromOptions(\n\tmessage string,\n\topts, d []string,\n\tvalidateFn func([]string) error,\n) ([]string, error) {\n\tvar choices []string\n\n\tos := u.options\n\tif validateFn != nil {\n\t\tos = append(os, survey.WithValidator(func(ans interface{}) error {\n\t\t\to := ans.([]survey.OptionAnswer)\n\t\t\ts := make([]string, len(o))\n\t\t\tfor i := range o {\n\t\t\t\ts[i] = o[i].Value\n\t\t\t}\n\t\t\treturn validateFn(s)\n\t\t}))\n\t}\n\n\treturn choices, survey.AskOne(\n\t\t&survey.MultiSelect{\n\t\t\tMessage: message,\n\t\t\tOptions: opts,\n\t\t\tDefault: d,\n\t\t\tFilter:  selectFilter,\n\t\t},\n\t\t&choices,\n\t\tos...,\n\t)\n}\n\n// Confirm interactively ask the user a yes/no question\nfunc (u *ui) Confirm(message string, d bool) (bool, error) {\n\tv := false\n\treturn v, survey.AskOne(\n\t\t&survey.Confirm{\n\t\t\tMessage: message,\n\t\t\tDefault: d,\n\t\t},\n\t\t&v,\n\t\tu.options...,\n\t)\n}\n"
  },
  {
    "path": "scripts/site-build",
    "content": "#!/bin/sh\n\nset -e\n\nrm -rf site/content/commands/* || true\n\necho \"generating documentation for commands...\"\ngo run ./cmd/gendocs/main.go site/content/commands\n\necho \"doing last touches...\"\n# fix root command to be \"chapter root\"\nmv site/content/commands/clockify-cli.md site/content/commands/_index.md\n\n# add license information\nmkdir -p site/content/license\necho '---\ntitle: License\nchapter: true\n---\n```txt' > site/content/license/_index.md\ncat LICENSE >> site/content/license/_index.md\necho '```' >> site/content/license/_index.md\n\n# add changelog\nmkdir -p site/content/changelog\necho '---\ntitle: CHANGELOG\nchapter: true\n---\n' > site/content/changelog/_index.md\ntail -n+2 CHANGELOG.md >> site/content/changelog/_index.md\n\necho \"building site :tada:\"\ncd site && hugo\n"
  },
  {
    "path": "site/.gitignore",
    "content": ".hugo_build.lock\n"
  },
  {
    "path": "site/archetypes/default.md",
    "content": "---\ntitle: \"{{ replace .Name \"-\" \" \" | title }}\"\ndate: {{ .Date }}\ndraft: true\n---\n\n"
  },
  {
    "path": "site/config.toml",
    "content": "baseURL = \"http://example.org/\"\nlanguageCode = \"en-us\"\ntitle = \"Clockify CLI\"\n\ntheme = \"hugo-theme-relearn\"\n\npygmentsCodeFences = true\npygmentsUseClasses = true\npygmentsOptions = \"hl_lines=3,startinline=1\"\n\nauthor = \"Lucas dos Santos Abreu\"\ndescription = \"Clockify CLI\"\n\nignoreLogs = ['warning-goldmark-raw-html']\n\n[outputs]\n    home = [\"HTML\", \"RSS\", \"JSON\"]\n\n[params]\n    showVisitedLinks = false\n    custom_css = [\"css/custom.css\"]\n    editURL = \"https://github.com/lucassabreu/clockify-cli/tree/main/site/\"\n    disableNextPrev = true\n    themeVariant = 'learn'\n"
  },
  {
    "path": "site/content/_index.md",
    "content": "# Getting Started\n\nClockify CLI is a command line tool to help manage time entries from [Clockify][clockify] and\nrelated resources.\n\n- [Available commands][commands]\n- [Usage examples][usage]\n- [Installation][install]\n\n## Configuration\n\n- Generate a API key by visiting your user [settings on Clockify.me][settings], in the \"API\"\n  section generate with you don't have one and copy your API key.\n- Run the command `clockify-cli config init`, it will ask for the API key you copied.\n  - The CLI will also ask for your preferences and default settings. You can change these\n    preferences any time, see [clockify-cli config][cli-config] about the options.\n- After this run the command `clockify-cli in` to start a time entry.\n- (optional) To add auto completion follow the instructions [here][auto-complete]\n\n## Usage\n\nAlmost every command has examples on its help, just run [`clockify-cli in --help`][cli-in-examples]\nto see some of them.\n\nFor a more step by step scenarios look at [Usage document][usage].\n\n## How to Contact\n\n- Questions about how to use the CLI?\n- Wanna provide feedback on some feature?\n- Report a bug or ask for a feature?\n\nAll these can be done opening a [issue on Github][issues].\n\n## Contributing\n\nWants to help improve the CLI or the project, check out our [contributing page][contributing]\n\n#### Disclaimer\n\nThe maintainers of this CLI are just users of Clockify and have no inside view from it, all actions\nperformed by it are possible using the [API][api] provided by Clockify.\n\n[clockify]: https://clockify.me/\n[api]: https://clockify.me/developers-api\n[install]: https://github.com/lucassabreu/clockify-cli#how-to-install-\n[usage]: /en/usage/\n[commands]: /en/commands/clockify-cli/\n[settings]: https://app.clockify.me/user/settings\n[auto-complete]: /en/commands/clockify-cli_completion/#synopsis\n[issues]: https://github.com/lucassabreu/clockify-cli/issues\n[contributing]: https://github.com/lucassabreu/clockify-cli/blob/main/CONTRIBUTING.md\n[cli-config]: /en/commands/clockify-cli_config/\n[cli-in-examples]: /en/commands/clockify-cli_in/#examples\n"
  },
  {
    "path": "site/content/usage/_index.md",
    "content": "---\nurl: /en/usage/\ntitle: Usage examples\nweight: 10\n---\n\nAfter you [install the CLI][install] the first thing to do is run the command `clockifycli config\ninit`, it will interactively ask you the information necessary to setup your environment.\n\n```sh\n$ clockify-cli config init\n? User Generated Token: <your-api-token>\n? Choose default Workspace: <workspace-id> - John Doe's workspace\n? Choose your user: <user-id> - John Doe\n? Should try to find projects/tasks/tags by their names? Yes\n? Should use \"Interactive Mode\" by default? Yes\n? Which days of the week do you work? monday, tuesday, wednesday, thursday, friday\n? Should allow starting time entries with incomplete data? No\n? Should show task on time entries as a separated column? Yes\n```\n\nThese answers will be saved at `$HOME/.clockify-cli.yaml` by default and can be copied from machine\nto machine if needed. You can see all the options [here][cli-config].\n\n> ❗ If you have installed the client using `snap` this file will not be accessible to\n> you, but the configurations will still be persisted.\n\nAfter that you can start a new entry using the `clockify-cli in` command.\n\n```sh\n$ clockify-cli in\n? Choose your project: 621948458cb9606d934ebb1c - Clockify Cli      | Client: Myself (6202634a28782767054eec26)\n? Choose your task: 62ae29e62518aa18da2acd14 - In Command\n? Description: Some description\n? Choose your tags: 62ae28b72518aa18da2acb49 - Development\n? Start: 2022-06-30 22:49:34\n? End (leave it blank for empty):\n+--------------------------+----------+----------+---------+--------------+------------------+----------------------------------------+\n|            ID            |  START   |   END    |   DUR   |   PROJECT    |   DESCRIPTION    |                  TAGS                  |\n+--------------------------+----------+----------+---------+--------------+------------------+----------------------------------------+\n| 62be52d2f2c0e80ba36fce0a | 22:49:34 | 22:50:10 | 0:00:36 | Clockify Cli | Some description | Development (62ae28b72518aa18da2acb49) |\n+--------------------------+----------+----------+---------+--------------+------------------+----------------------------------------+\n```\n\n> See more about [in here][cli-in]\n\nBy default it will prompt you about the details of the time entry, if you don't like that as the\ndefault behavior you can change it by running the command `clockify-cli config interactive false`,\nand if there is a situation were you want to be prompted, then run `clockify-cli in --interactive`.\n\nThis behavior is true for all interactive commands, except for `clockify-cli config init`.\n\nOnce you finish the activity or need to stop timer, run `clockify-cli out` to stop it.\n\n```sh\n$ clockify-cli out\n+--------------------------+----------+----------+---------+--------------+------------------+----------------------------------------+\n|            ID            |  START   |   END    |   DUR   |   PROJECT    |   DESCRIPTION    |                  TAGS                  |\n+--------------------------+----------+----------+---------+--------------+------------------+----------------------------------------+\n| 62be52d2f2c0e80ba36fce0a | 22:49:34 | 23:02:41 | 0:13:07 | Clockify Cli | Some description | Development (62ae28b72518aa18da2acb49) |\n+--------------------------+----------+----------+---------+--------------+------------------+----------------------------------------+\n```\n> See more about [out here][cli-out]\n\nTo start a new timer with the same information as the last one you did, you can run `clockify-cli\nclone last` and a new timer with the same properties as the last stopped one will be started.\n\n```sh\n$ clockify-cli clone last -i=0 # -i=0 will stop the CLI from prompting you about the timer\n+--------------------------+----------+----------+---------+--------------+------------------+----------------------------------------+\n|            ID            |  START   |   END    |   DUR   |   PROJECT    |   DESCRIPTION    |                  TAGS                  |\n+--------------------------+----------+----------+---------+--------------+------------------+----------------------------------------+\n| 62be5684f2c0e80ba36fcecd | 23:05:53 | 23:05:56 | 0:00:03 | Clockify Cli | Some description | Development (62ae28b72518aa18da2acb49) |\n+--------------------------+----------+----------+---------+--------------+------------------+----------------------------------------+\n```\n\nLets say that the current activity is for the same task, project and description, but you doing a\npairing with someone now. You can fix the timer using the `clockify-cli edit current` command and\nchange the tags of the timer.\n\n```sh\n$ clockify-cli edit current -T pair -T web\n+--------------------------+----------+----------+---------+--------------+------------------+---------------------------------------------+\n|            ID            |  START   |   END    |   DUR   |   PROJECT    |   DESCRIPTION    |                    TAGS                     |\n+--------------------------+----------+----------+---------+--------------+------------------+---------------------------------------------+\n| 62be5684f2c0e80ba36fcecd | 23:05:53 | 23:29:14 | 0:23:21 | Clockify Cli | Some description | Pair Programming (621948708cb9606d934ebba7) |\n|                          |          |          |         |              |                  | Development (62ae28b72518aa18da2acb49)      |\n+--------------------------+----------+----------+---------+--------------+------------------+---------------------------------------------+\n```\n> See more about [edit here][cli-edit]\n\nNow you remembered that yesterday you had a meeting at the end of the day that you forgot to\nregister, but you don't want to stop the running one.\n\nTo create a time entry that has a start and end without tempering with a running timer you can use\nthe command `clockify-cli manual`.\n\n```sh\n$ clockify-cli manual -s \"yesterday 17:50\" -e \"yesterday 18:00\" -T meet -d 'About the Calendar' \\\n    -p cli\n\n+--------------------------+----------+----------+---------+--------------+--------------------+------------------------------------+\n|            ID            |  START   |   END    |   DUR   |   PROJECT    |    DESCRIPTION     |                TAGS                |\n+--------------------------+----------+----------+---------+--------------+--------------------+------------------------------------+\n| 62be5d49f2c0e80ba36fd01e | 17:50:00 | 18:00:00 | 0:10:00 | Clockify Cli | About the Calendar | Meeting (6219485e8cb9606d934ebb5f) |\n+--------------------------+----------+----------+---------+--------------+--------------------+------------------------------------+\n```\n> See more about [manual here][cli-manual]\n\nIf you forgot to stop a timer running and wants to stop it with a specific instead of now, you can\nuse the flag `--when` to set the end time.\n\n```sh\n$ clockify-cli out --when 23:35\n+--------------------------+----------+----------+---------+--------------+------------------+---------------------------------------------+\n|            ID            |  START   |   END    |   DUR   |   PROJECT    |   DESCRIPTION    |                    TAGS                     |\n+--------------------------+----------+----------+---------+--------------+------------------+---------------------------------------------+\n| 62be5684f2c0e80ba36fcecd | 23:05:53 | 23:35:00 | 0:29:07 | Clockify Cli | Some description | Pair Programming (621948708cb9606d934ebba7) |\n|                          |          |          |         |              |                  | Development (62ae28b72518aa18da2acb49)      |\n+--------------------------+----------+----------+---------+--------------+------------------+---------------------------------------------+\n```\n\nTo see the entries for today you can use the command `clockify-cli report` to list them.\n\n```sh\n$ clockify-cli report\n+--------------------------+---------------------+---------------------+---------+--------------+-------------------+---------------------------------------------+\n|            ID            |        START        |         END         |   DUR   |   PROJECT    |    DESCRIPTION    |                    TAGS                     |\n+--------------------------+---------------------+---------------------+---------+--------------+-------------------+---------------------------------------------+\n| 62be52d2f2c0e80ba36fce0a | 2022-06-30 22:49:34 | 2022-06-30 23:02:41 | 0:13:07 | Clockify Cli | Some description  | Development (62ae28b72518aa18da2acb49)      |\n+--------------------------+---------------------+---------------------+---------+--------------+-------------------+---------------------------------------------+\n| 62be5684f2c0e80ba36fcecd | 2022-06-30 23:05:53 | 2022-06-30 23:35:00 | 0:29:07 | Clockify Cli | Some description  | Pair Programming (621948708cb9606d934ebba7) |\n|                          |                     |                     |         |              |                   | Development (62ae28b72518aa18da2acb49)      |\n+--------------------------+---------------------+---------------------+---------+--------------+-------------------+---------------------------------------------+\n| 62be5eb535710e76ef03c884 | 2022-06-30 23:40:42 | 2022-06-30 23:41:27 | 0:00:45 | Clockify Cli | Other description | Development (62ae28b72518aa18da2acb49)      |\n+--------------------------+---------------------+---------------------+---------+--------------+-------------------+---------------------------------------------+\n| TOTAL                    |                     |                     | 0:42:59 |              |                   |                                             |\n+--------------------------+---------------------+---------------------+---------+--------------+-------------------+---------------------------------------------+\n```\n> See more about [report here][cli-report]\n\nIf you need to quickly see how much time was spent this month in a project you can use\n`clockify-cli report this-month`, the flag `--project` to filter the timers and\n`--duration-formatted` to get only the sum of time.\n\n```sh\n$ clockify-cli report this-month -p cli --duration-formatted\n6:23:52\n```\n\n[install]: https://github.com/lucassabreu/clockify-cli#how-to-install-\n[cli-config]: /en/commands/clockify-cli_config/\n[cli-in]: /en/commands/clockify-cli_in/\n[cli-manual]: /en/commands/clockify-cli_manual/\n[cli-clone]: /en/commands/clockify-cli_clone/\n[cli-report]: /en/commands/clockify-cli_report/\n[cli-edit]: /en/commands/clockify-cli_edit/\n"
  },
  {
    "path": "site/layouts/partials/logo.html",
    "content": "<a id=\"logo\" href=\"/\">\n\n<svg\n   xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n   xmlns:cc=\"http://creativecommons.org/ns#\"\n   xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n   xmlns:svg=\"http://www.w3.org/2000/svg\"\n   xmlns=\"http://www.w3.org/2000/svg\"\n   xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\"\n   xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\"\n   width=\"42.020515mm\"\n   height=\"10.345892mm\"\n   viewBox=\"0 0 42.020515 10.345892\"\n   version=\"1.1\"\n   id=\"svg8\"\n   inkscape:version=\"1.0 (4035a4fb49, 2020-05-01)\"\n   sodipodi:docname=\"logo.svg\">\n  <defs\n     id=\"defs2\" />\n  <sodipodi:namedview\n     id=\"base\"\n     pagecolor=\"#ffffff\"\n     bordercolor=\"#666666\"\n     borderopacity=\"1.0\"\n     inkscape:pageopacity=\"0.0\"\n     inkscape:pageshadow=\"2\"\n     inkscape:zoom=\"1.4\"\n     inkscape:cx=\"179.81612\"\n     inkscape:cy=\"-42.534592\"\n     inkscape:document-units=\"mm\"\n     inkscape:current-layer=\"layer1\"\n     inkscape:document-rotation=\"0\"\n     showgrid=\"false\"\n     inkscape:window-width=\"1920\"\n     inkscape:window-height=\"1023\"\n     inkscape:window-x=\"0\"\n     inkscape:window-y=\"27\"\n     inkscape:window-maximized=\"1\" />\n  <metadata\n     id=\"metadata5\">\n    <rdf:RDF>\n      <cc:Work\n         rdf:about=\"\">\n        <dc:format>image/svg+xml</dc:format>\n        <dc:type\n           rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" />\n        <dc:title></dc:title>\n      </cc:Work>\n    </rdf:RDF>\n  </metadata>\n  <g\n     transform=\"translate(-73.054547,-80.239384)\"\n     inkscape:label=\"Layer 1\"\n     inkscape:groupmode=\"layer\"\n     id=\"layer1\">\n    <text\n       xml:space=\"preserve\"\n       style=\"font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:9.97595px;line-height:1.25;font-family:'Space Mono';-inkscape-font-specification:'Space Mono';letter-spacing:0px;word-spacing:0px;fill:#00acef;fill-opacity:1;stroke:none;stroke-width:0.249398\"\n       x=\"59.911442\"\n       y=\"104.45831\"\n       id=\"text12\"\n       transform=\"scale(1.2097069,0.82664654)\"><tspan\n         sodipodi:role=\"line\"\n         id=\"tspan10\"\n         x=\"59.911442\"\n         y=\"104.45831\"\n         style=\"font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Space Mono';-inkscape-font-specification:'Space Mono';fill:#00acef;stroke-width:0.249398\"><tspan\n   style=\"font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Nimbus Sans';-inkscape-font-specification:'Nimbus Sans';stroke-width:0.249398\"\n   id=\"tspan46\">Clockify</tspan> </tspan></text>\n    <text\n       xml:space=\"preserve\"\n       style=\"font-style:normal;font-weight:normal;font-size:5.69827px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#4e9906;fill-opacity:1;stroke:none;stroke-width:0.142457\"\n       x=\"100.20509\"\n       y=\"89.803307\"\n       id=\"text16\"\n       transform=\"scale(0.99212245,1.0079401)\"><tspan\n         sodipodi:role=\"line\"\n         id=\"tspan14\"\n         x=\"100.20509\"\n         y=\"89.803307\"\n         style=\"font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'Source Code Variable';-inkscape-font-specification:'Source Code Variable Bold';fill:#4e9906;stroke-width:0.142457\">CLI</tspan></text>\n  </g>\n</svg>\n\n</a>\n"
  },
  {
    "path": "site/layouts/partials/menu-footer.html",
    "content": "<hr style=\"border-color: white\" />\n\n<center>\n    <!-- Place this tag where you want the button to render. -->\n    <a class=\"github-button\" href=\"https://github.com/lucassabreu/clockify-cli/archive/main.zip\" data-icon=\"octicon-cloud-download\" aria-label=\"Download lucassabreu/clockify-cli on GitHub\">Download</a>\n\n    <!-- Place this tag where you want the button to render. -->\n    <a class=\"github-button\" href=\"https://github.com/lucassabreu/clockify-cli\" data-icon=\"octicon-star\" data-show-count=\"true\" aria-label=\"Star lucassabreu/clockify-cli on GitHub\">Star</a>\n\n    <!-- Place this tag where you want the button to render. -->\n    <a class=\"github-button\" href=\"https://github.com/lucassabreu/clockify-cli/fork\" data-icon=\"octicon-repo-forked\" data-show-count=\"true\" aria-label=\"Fork lucassabreu/clockify-cli on GitHub\">Fork</a>\n\n    <p>Built with <a href=\"https://github.com/matcornic/hugo-theme-learn\"><i class=\"fas fa-heart\"></i></a> from <a href=\"https://getgrav.org\">Grav</a> and <a href=\"https://gohugo.io/\">Hugo</a></p>\n</center>\n<!-- Place this tag in your head or just before your close body tag. -->\n<script async defer src=\"https://buttons.github.io/buttons.js\"></script>\n"
  },
  {
    "path": "site/static/css/custom.css",
    "content": "#body img.badges {\n  display: inline;\n  margin: 0;\n}\n\nh6,\nfooter.footline {\n  font-size: 0.7rem;\n  line-height: 110% !important;\n  margin-top: 0.5rem !important;\n}\n\n#logo {\n  display: block;\n  padding-top: 2rem;\n  padding-bottom: 1rem;\n}\n"
  },
  {
    "path": "strhlp/strhlp.go",
    "content": "package strhlp\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"golang.org/x/text/runes\"\n\t\"golang.org/x/text/transform\"\n\t\"golang.org/x/text/unicode/norm\"\n)\n\n// Normalize a string removes all non-english characters and lower\n// case the string to help compare it with other strings\nfunc Normalize(s string) string {\n\tif r, _, err := transform.String(\n\t\ttransform.Chain(\n\t\t\tnorm.NFD,\n\t\t\trunes.Remove(runes.In(unicode.Mn)),\n\t\t\tnorm.NFC,\n\t\t),\n\t\tstrings.ToLower(s),\n\t); err == nil {\n\t\ts = r\n\t}\n\n\treturn s\n}\n\n// InSlice will return true if the needle is one of the values in list\nfunc InSlice(needle string, list []string) bool {\n\treturn Search(needle, list) != -1\n}\n\n// Search will search for a exact match of the string on the slide\n// provided and if found will return its index position, or -1 if not\nfunc Search(s string, list []string) int {\n\tfor i, sl := range list {\n\t\tif sl == s {\n\t\t\treturn i\n\t\t}\n\t}\n\n\treturn -1\n}\n\n// Map will apply the map function provided on every entry of\n// the string slice and return a slice with the changes\nfunc Map(f func(string) string, s []string) []string {\n\tfor i, e := range s {\n\t\ts[i] = f(e)\n\t}\n\treturn s\n}\n\n// Filter will run the function for every entry of the slice\n// and will return a new slice with the entries that return true\n// when used on the function\nfunc Filter(f func(string) bool, s []string) []string {\n\tns := make([]string, 0)\n\tfor _, e := range s {\n\t\tif f(e) {\n\t\t\tns = append(ns, e)\n\t\t}\n\t}\n\treturn ns\n}\n\n// Unique will remove all duplicated strings from the slice\nfunc Unique(ss []string) []string {\n\tr := make([]string, 0)\n\tfor _, s := range ss {\n\t\tif Search(s, r) == -1 {\n\t\t\tr = append(r, s)\n\t\t}\n\t}\n\n\treturn r\n}\n\n// ListForHumans returns a string listing the strings from the parameter\n//\n// Example: ListForHumans([]string{\"one\", \"two\", \"three\"}) will output:\n// \"one, two and three\"\nfunc ListForHumans(s []string) string {\n\tif len(s) == 1 {\n\t\treturn s[0]\n\t}\n\n\treturn strings.Join(s[:len(s)-1], \", \") + \" and \" + s[len(s)-1]\n}\n\n// PadSpace will add spaces to the end of a string until it reaches the size\n// set at the second parameter\nfunc PadSpace(s string, size int) string {\n\tfor i := len(s); i < size; i++ {\n\t\ts = s + \" \"\n\t}\n\treturn s\n}\n\n// IsSimilar will convert the string into a regex and return a function the\n// checks if a second string is similar to it.\n//\n// Both strings will normalized before mathing and any space on the filter\n// string will be taken as .* on a regex\nfunc IsSimilar(filter string) func(string) bool {\n\t// skipcq: GO-C4007\n\tfilter = regexp.MustCompile(`[\\[\\]\\^\\\\\\,\\.\\(\\)\\-]+`).\n\t\tReplaceAllString(Normalize(filter), \" \")\n\tfilter = regexp.MustCompile(`\\s+`).ReplaceAllString(filter, \" \")\n\tfilter = strings.ReplaceAll(filter, \" \", \".*\")\n\tfilter = strings.ReplaceAll(filter, \"*\", \".*\")\n\n\tr := regexp.MustCompile(filter)\n\n\treturn func(s string) bool {\n\t\treturn r.MatchString(Normalize(s))\n\t}\n}\n"
  },
  {
    "path": "strhlp/strhlp_test.go",
    "content": "package strhlp_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/lucassabreu/clockify-cli/strhlp\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNormalize(t *testing.T) {\n\ttts := map[string]string{\n\t\t\"some long string\": \"Some Long STRING\",\n\t\t\"atencao\":          \"Atenção\",\n\t\t\"it should've this kind of keep \\\"stuff\\\"\": \"It should've this kind of keep \\\"STUFF\\\"\",\n\t}\n\n\tfor expected, argument := range tts {\n\t\tt.Run(expected, func(t *testing.T) {\n\t\t\tassert.Equal(t, expected, strhlp.Normalize(argument))\n\t\t})\n\t}\n}\n\nfunc TestInSlice(t *testing.T) {\n\ttts := []struct {\n\t\tname   string\n\t\tb      bool\n\t\tsearch string\n\t\tlist   []string\n\t}{\n\t\t{\n\t\t\tname:   \"unique\",\n\t\t\tb:      true,\n\t\t\tsearch: \"str 3\",\n\t\t\tlist:   []string{\"str 0\", \"str 1\", \"str 2\", \"str 3\", \"str 4\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"first\",\n\t\t\tb:      true,\n\t\t\tsearch: \"str 1\",\n\t\t\tlist:   []string{\"str 0\", \"str 1\", \"str 1\", \"str 1\", \"str 2\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"unordered\",\n\t\t\tb:      true,\n\t\t\tsearch: \"str 1\",\n\t\t\tlist:   []string{\"str 0\", \"str 3\", \"str 4\", \"str 2\", \"str 1\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"not found\",\n\t\t\tb:      false,\n\t\t\tsearch: \"str a\",\n\t\t\tlist:   []string{\"str 0\", \"str 3\", \"str 4\", \"str 2\", \"str 1\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.b, strhlp.InSlice(\n\t\t\t\ttt.search,\n\t\t\t\ttt.list,\n\t\t\t))\n\t\t})\n\t}\n}\n\nfunc TestSearch(t *testing.T) {\n\ttts := []struct {\n\t\tname   string\n\t\tpos    int\n\t\tsearch string\n\t\tlist   []string\n\t}{\n\t\t{\n\t\t\tname:   \"unique\",\n\t\t\tpos:    3,\n\t\t\tsearch: \"str 3\",\n\t\t\tlist:   []string{\"str 0\", \"str 1\", \"str 2\", \"str 3\", \"str 4\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"first\",\n\t\t\tpos:    1,\n\t\t\tsearch: \"str 1\",\n\t\t\tlist:   []string{\"str 0\", \"str 1\", \"str 1\", \"str 1\", \"str 2\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"unordered\",\n\t\t\tpos:    4,\n\t\t\tsearch: \"str 1\",\n\t\t\tlist:   []string{\"str 0\", \"str 3\", \"str 4\", \"str 2\", \"str 1\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"not found\",\n\t\t\tpos:    -1,\n\t\t\tsearch: \"str a\",\n\t\t\tlist:   []string{\"str 0\", \"str 3\", \"str 4\", \"str 2\", \"str 1\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.pos, strhlp.Search(\n\t\t\t\ttt.search,\n\t\t\t\ttt.list,\n\t\t\t))\n\t\t})\n\t}\n}\nfunc TestMap(t *testing.T) {\n\ttts := []struct {\n\t\tname     string\n\t\tfn       func(string) string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"upper\",\n\t\t\tfn:       strings.ToUpper,\n\t\t\texpected: []string{\"VALUE 1\", \"VALUE 2\", \"VALUE 3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"last digit\",\n\t\t\tfn:       func(s string) string { return s[len(s)-1:] },\n\t\t\texpected: []string{\"1\", \"2\", \"3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"same\",\n\t\t\tfn:       func(s string) string { return \"same\" },\n\t\t\texpected: []string{\"same\", \"same\", \"same\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, strhlp.Map(\n\t\t\t\ttt.fn,\n\t\t\t\t[]string{\"value 1\", \"value 2\", \"value 3\"},\n\t\t\t))\n\t\t})\n\t}\n}\n\nfunc TestFilter(t *testing.T) {\n\ttts := []struct {\n\t\tname     string\n\t\tfn       func(string) bool\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"keep all\",\n\t\t\tfn:       func(s string) bool { return true },\n\t\t\texpected: []string{\"first\", \"second\", \"third\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"keep none\",\n\t\t\tfn:       func(s string) bool { return false },\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"keep second\",\n\t\t\tfn:       func(s string) bool { return s == \"second\" },\n\t\t\texpected: []string{\"second\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"keep all but second\",\n\t\t\tfn:       func(s string) bool { return s != \"second\" },\n\t\t\texpected: []string{\"first\", \"third\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, strhlp.Filter(\n\t\t\t\ttt.fn,\n\t\t\t\t[]string{\"first\", \"second\", \"third\"},\n\t\t\t))\n\t\t})\n\t}\n}\n\nfunc TestUnique(t *testing.T) {\n\ttts := []struct {\n\t\tname     string\n\t\targs     []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"one\",\n\t\t\targs:     []string{\"one\", \"one\", \"one\"},\n\t\t\texpected: []string{\"one\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"ordered\",\n\t\t\targs:     []string{\"1\", \"1\", \"2\", \"2\", \"3\", \"3\"},\n\t\t\texpected: []string{\"1\", \"2\", \"3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"shuffled\",\n\t\t\targs:     []string{\"2\", \"3\", \"2\", \"1\", \"3\", \"2\"},\n\t\t\texpected: []string{\"2\", \"3\", \"1\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"no changes\",\n\t\t\targs:     []string{\"2\", \"3\", \"4\", \"1\", \"5\", \"6\"},\n\t\t\texpected: []string{\"2\", \"3\", \"4\", \"1\", \"5\", \"6\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, strhlp.Unique(tt.args))\n\t\t})\n\t}\n}\n\nfunc TestPadSpace(t *testing.T) {\n\ttts := []struct {\n\t\tname     string\n\t\tsize     int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"zero\",\n\t\t\tsize:     0,\n\t\t\texpected: \"some word\",\n\t\t},\n\t\t{\n\t\t\tname:     \"same size\",\n\t\t\tsize:     9,\n\t\t\texpected: \"some word\",\n\t\t},\n\t\t{\n\t\t\tname:     \"10\",\n\t\t\tsize:     10,\n\t\t\texpected: \"some word \",\n\t\t},\n\t\t{\n\t\t\tname:     \"30\",\n\t\t\tsize:     30,\n\t\t\texpected: \"some word                     \",\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, strhlp.PadSpace(\n\t\t\t\t\"some word\",\n\t\t\t\ttt.size,\n\t\t\t))\n\t\t})\n\t}\n}\n\nfunc TestListForHumans(t *testing.T) {\n\ttts := []struct {\n\t\targs     []string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\targs:     []string{\"uno\", \"dos\", \"tres\"},\n\t\t\texpected: \"uno, dos and tres\",\n\t\t},\n\t\t{\n\t\t\targs:     []string{\"first\", \"last\"},\n\t\t\texpected: \"first and last\",\n\t\t},\n\t\t{\n\t\t\targs:     []string{\"1\", \"2\", \"3\", \"go!\"},\n\t\t\texpected: \"1, 2, 3 and go!\",\n\t\t},\n\t\t{\n\t\t\targs:     []string{\"only one\"},\n\t\t\texpected: \"only one\",\n\t\t},\n\t}\n\n\tfor _, tt := range tts {\n\t\tt.Run(tt.expected, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, strhlp.ListForHumans(tt.args))\n\t\t})\n\t}\n}\n\nfunc TestIsSimilar(t *testing.T) {\n\ttts := [][]string{\n\t\t{\"similar\", \"similar\"},\n\t\t{\"some long string\", \"Some Long STRING\"},\n\t\t{\"atencao\", \"Atenção\"},\n\t\t{\"it should've this kind of keep \\\"stuff\\\"\",\n\t\t\t\"It should've this kind of keep \\\"STUFF\\\"\"},\n\t}\n\n\tfor i := range tts {\n\t\tt.Run(tts[i][0], func(t *testing.T) {\n\t\t\tassert.True(t, strhlp.IsSimilar(tts[i][0])(tts[i][1]),\n\t\t\t\t\"first should be similar to second\")\n\t\t\tassert.True(t, strhlp.IsSimilar(tts[i][1])(tts[i][0]),\n\t\t\t\t\"second should be similar to first\")\n\t\t})\n\t}\n}\n"
  }
]