diff --git a/pkg/cmd/time-entry/defaults/defaults.go b/pkg/cmd/time-entry/defaults/defaults.go new file mode 100644 index 00000000..33aa89e1 --- /dev/null +++ b/pkg/cmd/time-entry/defaults/defaults.go @@ -0,0 +1,33 @@ +package defaults + +import ( + "io" + + "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/defaults/set" + "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/defaults/show" + "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util/defaults" + "github.com/lucassabreu/clockify-cli/pkg/cmdutil" + outd "github.com/lucassabreu/clockify-cli/pkg/output/defaults" + "github.com/spf13/cobra" +) + +// NewCmdDefaults creates commands to manage default parameters when creating +// time entries +func NewCmdDefaults(f cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "defaults", + Aliases: []string{"def"}, + Short: "Manages the default parameters for time entries " + + "in the current folder", + Args: cobra.ExactArgs(0), + } + + cmd.AddCommand( + set.NewCmdSet(f, func(of outd.OutputFlags, w io.Writer, dte defaults.DefaultTimeEntry) error { + return nil + }), + show.NewCmdShow(f), + ) + + return cmd +} diff --git a/pkg/cmd/time-entry/defaults/set/set.go b/pkg/cmd/time-entry/defaults/set/set.go new file mode 100644 index 00000000..0a6167f8 --- /dev/null +++ b/pkg/cmd/time-entry/defaults/set/set.go @@ -0,0 +1,347 @@ +package set + +import ( + "io" + + "github.com/lucassabreu/clockify-cli/api" + "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util/defaults" + "github.com/lucassabreu/clockify-cli/pkg/cmdcompl" + "github.com/lucassabreu/clockify-cli/pkg/cmdcomplutil" + "github.com/lucassabreu/clockify-cli/pkg/cmdutil" + outd "github.com/lucassabreu/clockify-cli/pkg/output/defaults" + "github.com/lucassabreu/clockify-cli/pkg/search" + "github.com/lucassabreu/clockify-cli/pkg/ui" + "github.com/lucassabreu/clockify-cli/pkg/uiutil" + "github.com/lucassabreu/clockify-cli/strhlp" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// NewCmdSet sets the default parameters for time entries in the current folder +func NewCmdSet( + f cmdutil.Factory, + report func(outd.OutputFlags, io.Writer, defaults.DefaultTimeEntry) error, +) *cobra.Command { + if report == nil { + panic(errors.New("report parameter should not be nil")) + } + + short := "Sets the default parameters for the current folder" + of := outd.OutputFlags{} + cmd := &cobra.Command{ + Use: "set", + Short: short, + Long: short + "\n" + + "The parameters will be saved in the current working directory " + + "in the file " + defaults.DEFAULT_FILENAME + ".yaml", + Example: "", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.XorFlagSet( + cmd.Flags(), "billable", "not-billable"); err != nil { + return err + } + + d, err := f.TimeEntryDefaults().Read() + if err != nil && err != defaults.DefaultsFileNotFoundErr { + return err + } + + n, changed := readFlags(d, cmd.Flags()) + + if n.Workspace, err = f.GetWorkspaceID(); err != nil { + return err + } + + if changed || d.Workspace != n.Workspace { + if n.TaskID != "" && n.ProjectID == "" { + return errors.New("can't set task without project") + } + + c, err := f.Client() + if err != nil { + return err + } + + if f.Config().IsAllowNameForID() { + if n, err = updateIDsByNames( + c, n, f.Config()); err != nil { + return err + } + } + + if f.Config().IsInteractive() { + if n, err = ask(n, f.Config(), c, f.UI()); err != nil { + return err + } + } + + if !f.Config().IsAllowNameForID() { + if err = checkIDs(c, n); err != nil { + return err + } + } + } + + if err = f.TimeEntryDefaults().Write(n); err != nil { + return err + } + + return report(of, cmd.OutOrStdout(), n) + }, + } + + cmd.Flags().BoolP("billable", "b", false, + "time entry should be billable by default") + cmd.Flags().BoolP("not-billable", "n", false, + "time entry should not be billable by default") + cmd.Flags().String("task", "", "default task") + _ = cmdcompl.AddSuggestionsToFlag(cmd, "task", + cmdcomplutil.NewTaskAutoComplete(f, true)) + + cmd.Flags().StringSliceP("tag", "T", []string{}, + "add tags be used by default") + _ = cmdcompl.AddSuggestionsToFlag(cmd, "tag", + cmdcomplutil.NewTagAutoComplete(f)) + + cmd.Flags().StringP("project", "p", "", "project to used by default") + _ = cmdcompl.AddSuggestionsToFlag(cmd, "project", + cmdcomplutil.NewProjectAutoComplete(f)) + + return cmd +} + +func readFlags( + d defaults.DefaultTimeEntry, + f *pflag.FlagSet, +) (defaults.DefaultTimeEntry, bool) { + changed := false + if f.Changed("project") { + d.ProjectID, _ = f.GetString("project") + changed = true + } + + if f.Changed("task") { + d.TaskID, _ = f.GetString("task") + changed = true + } + + if f.Changed("tag") { + d.TagIDs, _ = f.GetStringSlice("tag") + d.TagIDs = strhlp.Unique(d.TagIDs) + changed = true + } + + if f.Changed("billable") { + b := true + d.Billable = &b + changed = true + } else if f.Changed("not-billable") { + b := false + d.Billable = &b + changed = true + } + + return d, changed +} + +func checkIDs(c api.Client, d defaults.DefaultTimeEntry) error { + if d.ProjectID != "" { + p, err := c.GetProject(api.GetProjectParam{ + Workspace: d.Workspace, + ProjectID: d.ProjectID, + Hydrate: d.TaskID != "", + }) + + if err != nil { + return err + } + + if d.TaskID != "" { + found := false + for i := range p.Tasks { + if p.Tasks[i].ID == d.TaskID { + found = true + break + } + } + + if !found { + return errors.New( + "can't find task with ID \"" + d.TaskID + + "\" on project \"" + d.ProjectID + "\"") + } + } + } else if d.TaskID != "" { + return errors.New("task can't be set without a project") + } + + tags, err := c.GetTags(api.GetTagsParam{ + Workspace: d.Workspace, + Archived: &archived, + PaginationParam: api.AllPages(), + }) + if err != nil { + return err + } + + ids := make([]string, len(tags)) + for i := range tags { + ids[i] = tags[i].ID + } + + for _, id := range d.TagIDs { + if !strhlp.InSlice(id, ids) { + return errors.Errorf("can't find tag with ID \"%s\"", id) + } + } + + return nil +} + +var archived = false + +func updateIDsByNames( + c api.Client, d defaults.DefaultTimeEntry, cnf cmdutil.Config) ( + defaults.DefaultTimeEntry, + error, +) { + var err error + if d.ProjectID != "" { + d.ProjectID, err = search.GetProjectByName(c, cnf, + d.Workspace, d.ProjectID, "") + if err != nil { + d.ProjectID = "" + d.TaskID = "" + if !cnf.IsInteractive() { + return d, err + } + } + } + + if d.TaskID != "" { + d.TaskID, err = search.GetTaskByName(c, api.GetTasksParam{ + Workspace: d.Workspace, + ProjectID: d.ProjectID, + Active: true, + }, d.TaskID) + if err != nil && !cnf.IsInteractive() { + return d, err + } + } + + if len(d.TagIDs) > 0 { + d.TagIDs, err = search.GetTagsByName( + c, d.Workspace, !cnf.IsAllowArchivedTags(), d.TagIDs) + if err != nil && !cnf.IsInteractive() { + return d, err + } + } + + return d, nil +} + +func ask( + d defaults.DefaultTimeEntry, + cnf cmdutil.Config, + c api.Client, + ui ui.UI, +) ( + defaults.DefaultTimeEntry, + error, +) { + ui.SetPageSize(uint(cnf.InteractivePageSize())) + + ps, err := c.GetProjects(api.GetProjectsParam{ + Workspace: d.Workspace, + Archived: &archived, + PaginationParam: api.AllPages(), + }) + if err != nil { + return d, err + } + + p, err := uiutil.AskProject(uiutil.AskProjectParam{ + UI: ui, + ProjectID: d.ProjectID, + Projects: ps, + }) + if err != nil { + return d, err + } + if p != nil { + d.ProjectID = p.ID + } else { + d.ProjectID = "" + } + + if d.ProjectID != "" { + ts, err := c.GetTasks(api.GetTasksParam{ + Workspace: d.Workspace, + ProjectID: d.ProjectID, + Active: true, + PaginationParam: api.AllPages(), + }) + if err != nil { + return d, err + } + + t, err := uiutil.AskTask(uiutil.AskTaskParam{ + UI: ui, + TaskID: d.TaskID, + Tasks: ts, + }) + if err != nil { + return d, err + } + if t != nil { + d.TaskID = t.ID + } else { + d.TaskID = "" + } + } else { + d.TaskID = "" + } + + var archived *bool + if !cnf.IsAllowArchivedTags() { + b := false + archived = &b + } + + tags, err := c.GetTags(api.GetTagsParam{ + Workspace: d.Workspace, + Archived: archived, + PaginationParam: api.AllPages(), + }) + if err != nil { + return d, err + } + + tags, err = uiutil.AskTags(uiutil.AskTagsParam{ + UI: ui, + TagIDs: d.TagIDs, + Tags: tags, + }) + if err != nil { + return d, err + } + d.TagIDs = make([]string, len(tags)) + for i := range tags { + d.TagIDs[i] = tags[i].ID + } + + b := false + if d.Billable != nil { + b = *d.Billable + } + + b, err = ui.Confirm("Should be billable?", b) + if err != nil { + return d, err + } + d.Billable = &b + + return d, err +} diff --git a/pkg/cmd/time-entry/defaults/set/set_test.go b/pkg/cmd/time-entry/defaults/set/set_test.go new file mode 100644 index 00000000..8f720963 --- /dev/null +++ b/pkg/cmd/time-entry/defaults/set/set_test.go @@ -0,0 +1,677 @@ +package set_test + +import ( + "errors" + "io" + "testing" + + "github.com/AlecAivazis/survey/v2/terminal" + "github.com/lucassabreu/clockify-cli/api" + "github.com/lucassabreu/clockify-cli/api/dto" + "github.com/lucassabreu/clockify-cli/internal/consoletest" + "github.com/lucassabreu/clockify-cli/internal/mocks" + "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/defaults/set" + "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/util/defaults" + "github.com/lucassabreu/clockify-cli/pkg/cmdutil" + . "github.com/lucassabreu/clockify-cli/pkg/output/defaults" + "github.com/lucassabreu/clockify-cli/pkg/ui" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +var bTrue = true +var bFalse = false + +func TestNewCmdSet_ShouldAskInfo_WhenInteractive(t *testing.T) { + consoletest.RunTestConsole(t, + func(out consoletest.FileWriter, in consoletest.FileReader) error { + f := mocks.NewMockFactory(t) + + f.EXPECT().UI().Return(ui.NewUI(in, out, out)) + + ted := mocks.NewMockTimeEntryDefaults(t) + ted.EXPECT().Read().Return(defaults.DefaultTimeEntry{}, nil) + ted.On("Write", mock.Anything).Return(nil) + f.EXPECT().TimeEntryDefaults().Return(ted) + + f.EXPECT().GetWorkspaceID().Return("w", nil) + + c := mocks.NewMockClient(t) + + c.EXPECT().GetProjects(api.GetProjectsParam{ + Workspace: "w", + PaginationParam: api.AllPages(), + }). + Return([]dto.Project{}, nil) + + c.EXPECT().GetProjects(api.GetProjectsParam{ + Workspace: "w", + Archived: &bFalse, + PaginationParam: api.AllPages(), + }). + Return([]dto.Project{ + {ID: "p1", Name: "first"}, + {ID: "p2", Name: "second", + ClientID: "c", ClientName: "Myself"}, + {ID: "p3", Name: "third"}, + }, nil) + + c.EXPECT().GetTasks(api.GetTasksParam{ + Workspace: "w", + ProjectID: "p3", + Active: true, + PaginationParam: api.AllPages(), + }). + Return([]dto.Task{ + {ID: "t", Name: "first"}, + {ID: "t2", Name: "second"}, + {ID: "t3", Name: "third"}, + }, nil) + + c.EXPECT().GetTags(api.GetTagsParam{ + Workspace: "w", + Archived: &bFalse, + PaginationParam: api.AllPages(), + }). + Return([]dto.Tag{ + {ID: "tg1", Name: "first"}, + {ID: "tg2", Name: "second"}, + {ID: "tg3", Name: "third"}, + }, nil) + + f.EXPECT().Client().Return(c, nil) + + f.EXPECT().Config().Return(&mocks.SimpleConfig{ + AllowNameForID: true, + Interactive: true, + InteractivePageSizeNumber: 7, + }) + + var d defaults.DefaultTimeEntry + cmd := set.NewCmdSet(f, func(_ OutputFlags, _ io.Writer, + dte defaults.DefaultTimeEntry) error { + d = dte + return nil + }) + + cmd.SilenceUsage = true + cmd.SilenceErrors = true + cmd.SetArgs([]string{ + "-p=not found", + "--task=nf", + "-T=nf", + "--not-billable", + }) + _, err := cmd.ExecuteC() + + if !assert.NoError(t, err) { + return err + } + + assert.Equal(t, "w", d.Workspace) + assert.Equal(t, "p3", d.ProjectID) + assert.Equal(t, "t2", d.TaskID) + assert.Equal(t, []string{"tg1", "tg2", "tg3"}, d.TagIDs) + assert.Equal(t, &bTrue, d.Billable) + + return err + }, + func(c consoletest.ExpectConsole) { + c.ExpectString("Choose your project:") + c.ExpectString("> No Project") + c.ExpectString("first | Without Client") + c.ExpectString("second | Client: Myself") + c.ExpectString("third | Without Client") + + c.Send("without") + c.SendLine(string(terminal.KeyArrowDown)) + + c.ExpectString("Choose your task:") + c.ExpectString("> No Task") + c.ExpectString("first") + c.ExpectString("second") + c.ExpectString("third") + c.SendLine("sec") + + c.ExpectString("Choose your tags:") + c.ExpectString("first") + c.ExpectString("second") + c.ExpectString("third") + c.SendLine(string(terminal.KeyArrowRight)) + + c.ExpectString("Should be billable?") + c.SendLine("y") + + c.ExpectEOF() + }, + ) +} + +func runCmd(f cmdutil.Factory, args []string) ( + d defaults.DefaultTimeEntry, reported bool, err error) { + + cmd := set.NewCmdSet(f, func(_ OutputFlags, _ io.Writer, + dte defaults.DefaultTimeEntry) error { + reported = true + d = dte + return nil + }) + + cmd.SilenceUsage = true + cmd.SilenceErrors = true + cmd.SetArgs(args) + _, err = cmd.ExecuteC() + + return d, reported, err +} + +func TestNewCmdSet_ShouldFail_WhenInvalidArgs(t *testing.T) { + tts := []struct { + name string + args []string + err string + factory func(t *testing.T) cmdutil.Factory + }{ + { + name: "can't be not billable and billable", + args: []string{"--billable", "--not-billable"}, + err: ".*flags can't be used together.*", + factory: func(*testing.T) cmdutil.Factory { + return mocks.NewMockFactory(t) + }, + }, + { + name: "can't read file", + err: "failed", + factory: func(*testing.T) cmdutil.Factory { + ted := mocks.NewMockTimeEntryDefaults(t) + ted.EXPECT().Read().Return( + defaults.DefaultTimeEntry{}, + errors.New("failed"), + ) + + f := mocks.NewMockFactory(t) + f.EXPECT().TimeEntryDefaults().Return(ted) + + return f + }, + }, + { + name: "failed to get client", + args: []string{"--project", "p1"}, + err: "failed", + factory: func(*testing.T) cmdutil.Factory { + ted := mocks.NewMockTimeEntryDefaults(t) + ted.EXPECT().Read().Return( + defaults.DefaultTimeEntry{}, + defaults.DefaultsFileNotFoundErr, + ) + + f := mocks.NewMockFactory(t) + f.EXPECT().TimeEntryDefaults().Return(ted) + f.EXPECT().GetWorkspaceID().Return("w", nil) + f.EXPECT().Client().Return( + mocks.NewMockClient(t), + errors.New("failed"), + ) + + return f + }, + }, + { + name: "can't get workspace", + err: "failed", + factory: func(*testing.T) cmdutil.Factory { + ted := mocks.NewMockTimeEntryDefaults(t) + ted.EXPECT().Read().Return( + defaults.DefaultTimeEntry{}, + defaults.DefaultsFileNotFoundErr, + ) + + f := mocks.NewMockFactory(t) + f.EXPECT().TimeEntryDefaults().Return(ted) + + f.EXPECT().GetWorkspaceID().Return("", errors.New("failed")) + + return f + }, + }, + { + name: "can't get project", + err: "failed", + args: []string{"--project", "p"}, + factory: func(*testing.T) cmdutil.Factory { + f := mocks.NewMockFactory(t) + + ted := mocks.NewMockTimeEntryDefaults(t) + ted.EXPECT().Read().Return( + defaults.DefaultTimeEntry{}, + defaults.DefaultsFileNotFoundErr, + ) + + f.EXPECT().TimeEntryDefaults().Return(ted) + f.EXPECT().GetWorkspaceID().Return("w", nil) + f.EXPECT().Config().Return(&mocks.SimpleConfig{ + AllowNameForID: false, + }) + + cl := mocks.NewMockClient(t) + cl.EXPECT().GetProject(api.GetProjectParam{ + Workspace: "w", + ProjectID: "p", + Hydrate: false, + }).Return(nil, errors.New("failed")) + + f.EXPECT().Client().Return(cl, nil) + + return f + }, + }, + { + name: "can't find task", + err: `can't find task with ID "tk" on project "p"`, + args: []string{ + "--project", "p", + "--task=tk", + }, + factory: func(*testing.T) cmdutil.Factory { + f := mocks.NewMockFactory(t) + + ted := mocks.NewMockTimeEntryDefaults(t) + ted.EXPECT().Read().Return( + defaults.DefaultTimeEntry{}, + defaults.DefaultsFileNotFoundErr, + ) + + f.EXPECT().TimeEntryDefaults().Return(ted) + + f.EXPECT().GetWorkspaceID().Return("w", nil) + + f.EXPECT().Config().Return(&mocks.SimpleConfig{ + AllowNameForID: false, + }) + + cl := mocks.NewMockClient(t) + cl.EXPECT().GetProject(api.GetProjectParam{ + Workspace: "w", + ProjectID: "p", + Hydrate: true, + }).Return(&dto.Project{ID: "p", Name: "project"}, nil) + + f.EXPECT().Client().Return(cl, nil) + + return f + }, + }, + { + name: "can't find tag", + err: "can't find tag with ID \"tg\"", + args: []string{ + "--project", "p", + "-T", "tg", + }, + factory: func(*testing.T) cmdutil.Factory { + f := mocks.NewMockFactory(t) + + ted := mocks.NewMockTimeEntryDefaults(t) + ted.EXPECT().Read().Return( + defaults.DefaultTimeEntry{}, + defaults.DefaultsFileNotFoundErr, + ) + + f.EXPECT().TimeEntryDefaults().Return(ted) + + f.EXPECT().GetWorkspaceID().Return("w", nil) + + f.EXPECT().Config().Return(&mocks.SimpleConfig{ + AllowNameForID: false, + }) + + cl := mocks.NewMockClient(t) + cl.EXPECT().GetProject(api.GetProjectParam{ + Workspace: "w", + ProjectID: "p", + Hydrate: false, + }).Return(&dto.Project{ID: "p", Name: "project"}, nil) + + cl.EXPECT().GetTags(api.GetTagsParam{ + Workspace: "w", + Archived: &bFalse, + PaginationParam: api.AllPages(), + }).Return([]dto.Tag{{ID: "not that"}}, nil) + + f.EXPECT().Client().Return(cl, nil) + + return f + }, + }, + { + name: "can't look for tag", + err: "failed", + args: []string{ + "-T", "tg", + }, + factory: func(*testing.T) cmdutil.Factory { + f := mocks.NewMockFactory(t) + + ted := mocks.NewMockTimeEntryDefaults(t) + ted.EXPECT().Read().Return( + defaults.DefaultTimeEntry{}, + defaults.DefaultsFileNotFoundErr, + ) + + f.EXPECT().TimeEntryDefaults().Return(ted) + + f.EXPECT().GetWorkspaceID().Return("w", nil) + + f.EXPECT().Config().Return(&mocks.SimpleConfig{ + AllowNameForID: false, + }) + + cl := mocks.NewMockClient(t) + + cl.EXPECT().GetTags(api.GetTagsParam{ + Workspace: "w", + Archived: &bFalse, + PaginationParam: api.AllPages(), + }).Return(nil, errors.New("failed")) + + f.EXPECT().Client().Return(cl, nil) + + return f + }, + }, + { + name: "can't find project by name", + err: "No project with id or name containing 'p' was found", + args: []string{"--project", "p"}, + factory: func(*testing.T) cmdutil.Factory { + f := mocks.NewMockFactory(t) + + ted := mocks.NewMockTimeEntryDefaults(t) + ted.EXPECT().Read().Return( + defaults.DefaultTimeEntry{}, + defaults.DefaultsFileNotFoundErr, + ) + + f.EXPECT().TimeEntryDefaults().Return(ted) + + f.EXPECT().GetWorkspaceID().Return("w", nil) + + f.EXPECT().Config().Return(&mocks.SimpleConfig{ + AllowNameForID: true, + }) + + cl := mocks.NewMockClient(t) + cl.EXPECT().GetProjects(mock.Anything). + Return([]dto.Project{}, nil) + + f.EXPECT().Client().Return(cl, nil) + + return f + }, + }, + { + name: "can't find task by name", + err: "No task with id or name containing 'task' was found", + args: []string{"--project", "project", "--task=task"}, + factory: func(*testing.T) cmdutil.Factory { + f := mocks.NewMockFactory(t) + + ted := mocks.NewMockTimeEntryDefaults(t) + ted.EXPECT().Read().Return( + defaults.DefaultTimeEntry{}, + defaults.DefaultsFileNotFoundErr, + ) + + f.EXPECT().TimeEntryDefaults().Return(ted) + + f.EXPECT().GetWorkspaceID().Return("w", nil) + + f.EXPECT().Config().Return(&mocks.SimpleConfig{ + AllowNameForID: true, + }) + + cl := mocks.NewMockClient(t) + cl.EXPECT().GetProjects(mock.Anything). + Return([]dto.Project{{ID: "p", Name: "project"}}, nil) + + cl.EXPECT().GetTasks(api.GetTasksParam{ + Workspace: "w", + ProjectID: "p", + Active: true, + PaginationParam: api.AllPages(), + }). + Return([]dto.Task{{ID: "tk", Name: "other"}}, nil) + + f.EXPECT().Client().Return(cl, nil) + + return f + }, + }, + { + name: "can't find tag by name", + err: "No tag with id or name containing 'tag' was found", + args: []string{ + "--project", "project", + "--task=task", + "-T=tag", + }, + factory: func(*testing.T) cmdutil.Factory { + f := mocks.NewMockFactory(t) + + ted := mocks.NewMockTimeEntryDefaults(t) + ted.EXPECT().Read().Return( + defaults.DefaultTimeEntry{}, + defaults.DefaultsFileNotFoundErr, + ) + + f.EXPECT().TimeEntryDefaults().Return(ted) + + f.EXPECT().GetWorkspaceID().Return("w", nil) + + f.EXPECT().Config().Return(&mocks.SimpleConfig{ + AllowNameForID: true, + }) + + cl := mocks.NewMockClient(t) + cl.EXPECT().GetProjects(mock.Anything). + Return([]dto.Project{{ID: "p", Name: "project"}}, nil) + + cl.EXPECT().GetTasks(api.GetTasksParam{ + Workspace: "w", + ProjectID: "p", + Active: true, + PaginationParam: api.AllPages(), + }). + Return([]dto.Task{{ID: "tk", Name: "task"}}, nil) + + cl.EXPECT().GetTags(api.GetTagsParam{ + Workspace: "w", + Archived: &bFalse, + PaginationParam: api.AllPages(), + }). + Return([]dto.Tag{{ID: "tg", Name: "other"}}, nil) + + f.EXPECT().Client().Return(cl, nil) + + return f + }, + }, + { + name: "can't set task without project", + err: "can't set task without project", + args: []string{"--task=task"}, + factory: func(*testing.T) cmdutil.Factory { + f := mocks.NewMockFactory(t) + + ted := mocks.NewMockTimeEntryDefaults(t) + ted.EXPECT().Read().Return( + defaults.DefaultTimeEntry{}, + defaults.DefaultsFileNotFoundErr, + ) + + f.EXPECT().TimeEntryDefaults().Return(ted) + + f.EXPECT().GetWorkspaceID().Return("w", nil) + + return f + }, + }, + { + name: "can't find tag by name (no project)", + err: "No tag with id or name containing 'tag2' was found", + args: []string{"-T=tag", "-T=tag2"}, + factory: func(*testing.T) cmdutil.Factory { + f := mocks.NewMockFactory(t) + + ted := mocks.NewMockTimeEntryDefaults(t) + ted.EXPECT().Read().Return( + defaults.DefaultTimeEntry{}, + defaults.DefaultsFileNotFoundErr, + ) + + f.EXPECT().TimeEntryDefaults().Return(ted) + + f.EXPECT().GetWorkspaceID().Return("w", nil) + + f.EXPECT().Config().Return(&mocks.SimpleConfig{ + AllowNameForID: true, + }) + + cl := mocks.NewMockClient(t) + + cl.EXPECT().GetTags(api.GetTagsParam{ + Workspace: "w", + Archived: &bFalse, + PaginationParam: api.AllPages(), + }). + Return([]dto.Tag{{ID: "tag", Name: "other"}}, nil) + + f.EXPECT().Client().Return(cl, nil) + + return f + }, + }, + } + + for i := range tts { + tt := &tts[i] + t.Run(tt.name, func(t *testing.T) { + _, called, err := runCmd(tt.factory(t), tt.args) + if !assert.Error(t, err, "should have failed") { + return + } + assert.False(t, called) + assert.Regexp(t, tt.err, err) + }) + } +} + +func TestNewCmdSet_ShouldUpdateDefaultsFile_OnlyByFlags(t *testing.T) { + tts := []struct { + name string + args []string + current defaults.DefaultTimeEntry + expected defaults.DefaultTimeEntry + }{ + { + name: "no arguments, no changes", + args: []string{}, + current: defaults.DefaultTimeEntry{ + Workspace: "w1", ProjectID: "p1"}, + expected: defaults.DefaultTimeEntry{ + Workspace: "w1", ProjectID: "p1"}, + }, + { + name: "all arguments", + args: []string{ + "-p=p2", + "--task=t2", + "-T=tg1", "-T=tg2", + "--billable", + }, + expected: defaults.DefaultTimeEntry{ + Workspace: "w2", + ProjectID: "p2", + TaskID: "t2", + Billable: &bTrue, + TagIDs: []string{"tg1", "tg2"}, + }, + }, + { + name: "not billable", + args: []string{"--not-billable"}, + current: defaults.DefaultTimeEntry{ + Workspace: "w2", + ProjectID: "p2", + TaskID: "t2", + Billable: &bTrue, + TagIDs: []string{"tg1", "tg2"}, + }, + expected: defaults.DefaultTimeEntry{ + Workspace: "w2", + ProjectID: "p2", + TaskID: "t2", + Billable: &bFalse, + TagIDs: []string{"tg1", "tg2"}, + }, + }, + } + + for i := range tts { + tt := &tts[i] + t.Run(tt.name, func(t *testing.T) { + f := mocks.NewMockFactory(t) + + if len(tt.args) != 0 { + f.EXPECT().Config().Return(&mocks.SimpleConfig{ + AllowNameForID: false, + Interactive: false, + }) + + c := mocks.NewMockClient(t) + + tasks := make([]dto.Task, 0) + + if tt.expected.TaskID != "" { + tasks = append(tasks, dto.Task{ + ID: tt.expected.TaskID, + }) + } + + if tt.expected.ProjectID != "" { + c.On("GetProject", mock.Anything).Return(&dto.Project{ + ID: tt.expected.ProjectID, + Tasks: tasks, + }, nil) + } + + if len(tt.expected.TagIDs) != 0 { + tags := make([]dto.Tag, len(tt.expected.TagIDs)) + + for i := range tt.expected.TagIDs { + tags[i] = dto.Tag{ID: tt.expected.TagIDs[i]} + } + + c.On("GetTags", mock.Anything).Return(tags, nil) + } + + f.EXPECT().Client().Return(c, nil) + } + + f.EXPECT().GetWorkspaceID().Return(tt.expected.Workspace, nil) + + ted := mocks.NewMockTimeEntryDefaults(t) + ted.EXPECT().Read().Return(tt.current, nil) + ted.EXPECT().Write(tt.expected).Return(nil) + f.EXPECT().TimeEntryDefaults().Return(ted) + + result, called, err := runCmd(f, tt.args) + + assert.NoError(t, err, "should not have failed") + assert.True(t, called) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/cmd/time-entry/defaults/show/show.go b/pkg/cmd/time-entry/defaults/show/show.go new file mode 100644 index 00000000..5fbb9e88 --- /dev/null +++ b/pkg/cmd/time-entry/defaults/show/show.go @@ -0,0 +1,12 @@ +package show + +import ( + "github.com/lucassabreu/clockify-cli/pkg/cmdutil" + "github.com/spf13/cobra" +) + +// NewCmdShow prints the default options for the current folder +func NewCmdShow(f cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{} + return cmd +} diff --git a/pkg/cmd/time-entry/timeentry.go b/pkg/cmd/time-entry/timeentry.go index ba5d7ef1..3aab90b2 100644 --- a/pkg/cmd/time-entry/timeentry.go +++ b/pkg/cmd/time-entry/timeentry.go @@ -5,6 +5,7 @@ import ( "github.com/lucassabreu/clockify-cli/api/dto" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/clone" + "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/defaults" del "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/delete" "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/edit" em "github.com/lucassabreu/clockify-cli/pkg/cmd/time-entry/edit-multipple" @@ -50,6 +51,8 @@ func NewCmdTimeEntry(f cmdutil.Factory) (cmds []*cobra.Command) { show.NewCmdShow(f), report.NewCmdReport(f), + + defaults.NewCmdDefaults(f), ) cmds = append(cmds, invoiced.NewCmdInvoiced(f)...) diff --git a/pkg/output/defaults/default.go b/pkg/output/defaults/default.go new file mode 100644 index 00000000..f97bb14d --- /dev/null +++ b/pkg/output/defaults/default.go @@ -0,0 +1,7 @@ +package defaults + +type OutputFlags struct { + Format string + CSV bool + JSON bool +}