diff --git a/mob.go b/mob.go index f07c2a0f..01b8523b 100644 --- a/mob.go +++ b/mob.go @@ -21,6 +21,7 @@ import ( "github.com/remotemobprogramming/mob/v5/help" "github.com/remotemobprogramming/mob/v5/open" "github.com/remotemobprogramming/mob/v5/say" + "github.com/remotemobprogramming/mob/v5/timer/localtimer" "github.com/remotemobprogramming/mob/v5/workdir" ) @@ -329,7 +330,7 @@ func execute(command string, parameter []string, configuration config.Configurat help.Help(configuration) } case "moo": - moo(configuration) + localtimer.Moo(configuration) case "sw", "squash-wip": if len(parameter) > 1 && parameter[0] == "--git-editor" { squashWipGitEditor(parameter[1], configuration) @@ -434,6 +435,24 @@ func determineBranches(currentBranch Branch, localBranches []string, configurati return } +func enrichConfigurationWithBranchQualifier(configuration config.Configuration) config.Configuration { + if !isGit() { + return configuration + } + + if configuration.WipBranchQualifier == "" { + currentBranch := gitCurrentBranch() + currentBaseBranch, _ := determineBranches(currentBranch, gitBranches(), configuration) + + if currentBranch.IsWipBranch(configuration) { + wipBranchWithoutWipPrefix := currentBranch.removeWipPrefix(configuration).Name + configuration.WipBranchQualifier = removePrefix(removePrefix(wipBranchWithoutWipPrefix, currentBaseBranch.Name), configuration.WipBranchQualifierSeparator) + } + } + + return configuration +} + func injectCommandWithMessage(command string, message string) string { placeHolders := strings.Count(command, "%s") if placeHolders > 1 { @@ -446,41 +465,10 @@ func injectCommandWithMessage(command string, message string) string { return fmt.Sprintf(command, message) } -func executeCommandsInBackgroundProcess(commands ...string) (err error) { - cmds := make([]string, 0) - for _, c := range commands { - if len(c) > 0 { - cmds = append(cmds, c) - } - } - say.Debug(fmt.Sprintf("Operating System %s", runtime.GOOS)) - switch runtime.GOOS { - case "windows": - _, err = startCommand("powershell", "-command", fmt.Sprintf("start-process powershell -NoNewWindow -ArgumentList '-command \"%s\"'", strings.Join(cmds, ";"))) - case "darwin", "linux": - _, err = startCommand("sh", "-c", fmt.Sprintf("(%s) &", strings.Join(cmds, ";"))) - default: - say.Warning(fmt.Sprintf("Cannot execute background commands on your os: %s", runtime.GOOS)) - } - return err -} - func currentTime() string { return time.Now().Format("15:04") } -func moo(configuration config.Configuration) { - voiceMessage := "moo" - err := executeCommandsInBackgroundProcess(getVoiceCommand(voiceMessage, configuration.VoiceCommand)) - - if err != nil { - say.Warning(fmt.Sprintf("can't run voice command on your system (%s)", runtime.GOOS)) - say.Warning(err.Error()) - return - } - - say.Info(voiceMessage) -} func reset(configuration config.Configuration) { if configuration.ResetDeleteRemoteWipBranch { diff --git a/test/test.go b/test/test.go index 761174f0..62359274 100644 --- a/test/test.go +++ b/test/test.go @@ -82,6 +82,14 @@ func AssertOutputNotContains(t *testing.T, output *string, notContains string) { } } +func AwaitFileCreated(t *testing.T, path string) { + t.Helper() + Await(t, func() bool { + _, err := os.Stat(path) + return err == nil + }, "file created: "+path) +} + func Await(t *testing.T, until func() bool, awaitedState string) { AwaitBlocking(t, AWAIT_DEFAULT_POLL_INTERVAL, AWAIT_DEFAULT_AT_MOST, until, awaitedState) } diff --git a/timer.go b/timer.go index 0896eec1..c35c1113 100644 --- a/timer.go +++ b/timer.go @@ -1,17 +1,9 @@ package main import ( - "encoding/json" - "errors" - "fmt" - "runtime" - "strconv" - "time" - config "github.com/remotemobprogramming/mob/v5/configuration" "github.com/remotemobprogramming/mob/v5/exit" - "github.com/remotemobprogramming/mob/v5/httpclient" - "github.com/remotemobprogramming/mob/v5/say" + "github.com/remotemobprogramming/mob/v5/timer" ) func StartTimer(timerInMinutes string, configuration config.Configuration) { @@ -21,71 +13,8 @@ func StartTimer(timerInMinutes string, configuration config.Configuration) { } func startTimer(timerInMinutes string, configuration config.Configuration) error { - err, timeoutInMinutes := toMinutes(timerInMinutes) - if err != nil { - return err - } - - timeoutInSeconds := timeoutInMinutes * 60 - timeOfTimeout := time.Now().Add(time.Minute * time.Duration(timeoutInMinutes)).Format("15:04") - say.Debug(fmt.Sprintf("Starting timer at %s for %d minutes = %d seconds (parsed from user input %s)", timeOfTimeout, timeoutInMinutes, timeoutInSeconds, timerInMinutes)) - - room := getMobTimerRoom(configuration) - startRemoteTimer := room != "" - startLocalTimer := configuration.TimerLocal - - if !startRemoteTimer && !startLocalTimer { - say.Error("No timer configured, not starting timer") - exit.Exit(1) - } - - if startRemoteTimer { - timerUser := getUserForMobTimer(configuration.TimerUser) - err := httpPutTimer(timeoutInMinutes, room, timerUser, configuration.TimerUrl, configuration.TimerInsecure) - if err != nil { - say.Error("remote timer couldn't be started") - say.Error(err.Error()) - exit.Exit(1) - } - } - - if startLocalTimer { - err := executeCommandsInBackgroundProcess(getSleepCommand(timeoutInSeconds), getVoiceCommand(configuration.VoiceMessage, configuration.VoiceCommand), getNotifyCommand(configuration.NotifyMessage, configuration.NotifyCommand), "echo \"mobTimer\"") - - if err != nil { - say.Error(fmt.Sprintf("timer couldn't be started on your system (%s)", runtime.GOOS)) - say.Error(err.Error()) - exit.Exit(1) - } - } - - say.Info("It's now " + currentTime() + ". " + fmt.Sprintf("%d min timer ends at approx. %s", timeoutInMinutes, timeOfTimeout) + ". Happy collaborating! :)") - return nil -} - -func getMobTimerRoom(configuration config.Configuration) string { - if !isGit() { - say.Debug("timer not in git repository, using MOB_TIMER_ROOM for room name") - return configuration.TimerRoom - } - - currentWipBranchQualifier := configuration.WipBranchQualifier - if currentWipBranchQualifier == "" { - currentBranch := gitCurrentBranch() - currentBaseBranch, _ := determineBranches(currentBranch, gitBranches(), configuration) - - if currentBranch.IsWipBranch(configuration) { - wipBranchWithoutWipPrefix := currentBranch.removeWipPrefix(configuration).Name - currentWipBranchQualifier = removePrefix(removePrefix(wipBranchWithoutWipPrefix, currentBaseBranch.Name), configuration.WipBranchQualifierSeparator) - } - } - - if configuration.TimerRoomUseWipBranchQualifier && currentWipBranchQualifier != "" { - say.Info("Using wip branch qualifier for room name") - return currentWipBranchQualifier - } - - return configuration.TimerRoom + configuration = enrichConfigurationWithBranchQualifier(configuration) + return timer.RunTimer(timerInMinutes, configuration) } func StartBreakTimer(timerInMinutes string, configuration config.Configuration) { @@ -95,99 +24,6 @@ func StartBreakTimer(timerInMinutes string, configuration config.Configuration) } func startBreakTimer(timerInMinutes string, configuration config.Configuration) error { - err, timeoutInMinutes := toMinutes(timerInMinutes) - if err != nil { - return err - } - - timeoutInSeconds := timeoutInMinutes * 60 - timeOfTimeout := time.Now().Add(time.Minute * time.Duration(timeoutInMinutes)).Format("15:04") - say.Debug(fmt.Sprintf("Starting break timer at %s for %d minutes = %d seconds (parsed from user input %s)", timeOfTimeout, timeoutInMinutes, timeoutInSeconds, timerInMinutes)) - - room := getMobTimerRoom(configuration) - startRemoteTimer := room != "" - startLocalTimer := configuration.TimerLocal - - if !startRemoteTimer && !startLocalTimer { - say.Error("No break timer configured, not starting break timer") - exit.Exit(1) - } - - if startRemoteTimer { - timerUser := getUserForMobTimer(configuration.TimerUser) - err := httpPutBreakTimer(timeoutInMinutes, room, timerUser, configuration.TimerUrl, configuration.TimerInsecure) - - if err != nil { - say.Error("remote break timer couldn't be started") - say.Error(err.Error()) - exit.Exit(1) - } - } - - if startLocalTimer { - err := executeCommandsInBackgroundProcess(getSleepCommand(timeoutInSeconds), getVoiceCommand("mob start", configuration.VoiceCommand), getNotifyCommand("mob start", configuration.NotifyCommand), "echo \"mobTimer\"") - - if err != nil { - say.Error(fmt.Sprintf("break timer couldn't be started on your system (%s)", runtime.GOOS)) - say.Error(err.Error()) - exit.Exit(1) - } - } - - say.Info("It's now " + currentTime() + ". " + fmt.Sprintf("%d min break timer ends at approx. %s", timeoutInMinutes, timeOfTimeout) + ". So take a break now! :)") - return nil -} - -func getUserForMobTimer(userOverride string) string { - if userOverride == "" { - return gitUserName() - } - return userOverride -} - -func toMinutes(timerInMinutes string) (error, int) { - timeoutInMinutes, err := strconv.Atoi(timerInMinutes) - if err != nil || timeoutInMinutes < 1 { - say.Error(fmt.Sprintf("The parameter must be an integer number greater then zero")) - return errors.New("The parameter must be an integer number greater then zero"), 0 - } - return nil, timeoutInMinutes -} - -func httpPutTimer(timeoutInMinutes int, room string, user string, timerService string, disableSSLVerification bool) error { - putBody, _ := json.Marshal(map[string]interface{}{ - "timer": timeoutInMinutes, - "user": user, - }) - client := httpclient.CreateHttpClient(disableSSLVerification) - _, err := client.SendRequest(putBody, "PUT", timerService+room) - return err -} - -func httpPutBreakTimer(timeoutInMinutes int, room string, user string, timerService string, disableSSLVerification bool) error { - putBody, _ := json.Marshal(map[string]interface{}{ - "breaktimer": timeoutInMinutes, - "user": user, - }) - client := httpclient.CreateHttpClient(disableSSLVerification) - _, err := client.SendRequest(putBody, "PUT", timerService+room) - return err -} - -func getSleepCommand(timeoutInSeconds int) string { - return fmt.Sprintf("sleep %d", timeoutInSeconds) -} - -func getVoiceCommand(message string, voiceCommand string) string { - if len(voiceCommand) == 0 { - return "" - } - return injectCommandWithMessage(voiceCommand, message) -} - -func getNotifyCommand(message string, notifyCommand string) string { - if len(notifyCommand) == 0 { - return "" - } - return injectCommandWithMessage(notifyCommand, message) + configuration = enrichConfigurationWithBranchQualifier(configuration) + return timer.RunBreakTimer(timerInMinutes, configuration) } diff --git a/timer/localtimer/localtimer.go b/timer/localtimer/localtimer.go new file mode 100644 index 00000000..e1793ab7 --- /dev/null +++ b/timer/localtimer/localtimer.go @@ -0,0 +1,123 @@ +package localtimer + +import ( + "fmt" + "os/exec" + "runtime" + "strings" + + config "github.com/remotemobprogramming/mob/v5/configuration" + "github.com/remotemobprogramming/mob/v5/exit" + "github.com/remotemobprogramming/mob/v5/say" + "github.com/remotemobprogramming/mob/v5/workdir" +) + +// ProcessLocalTimer is a Timer implementation that uses background OS processes. +type ProcessLocalTimer struct { + configuration config.Configuration +} + +func NewProcessLocalTimer(configuration config.Configuration) ProcessLocalTimer { + return ProcessLocalTimer{configuration: configuration} +} + +func (t ProcessLocalTimer) IsActive() bool { + return t.configuration.TimerLocal +} + +func (t ProcessLocalTimer) StartTimer(minutes int) error { + timeoutInSeconds := minutes * 60 + if err := executeCommandsInBackgroundProcess( + sleepCommand(timeoutInSeconds), + voiceCommand(t.configuration.VoiceMessage, t.configuration.VoiceCommand), + notifyCommand(t.configuration.NotifyMessage, t.configuration.NotifyCommand), + "echo \"mobTimer\"", + ); err != nil { + return fmt.Errorf("timer couldn't be started on your system (%s): %w", runtime.GOOS, err) + } + return nil +} + +func (t ProcessLocalTimer) StartBreakTimer(minutes int) error { + timeoutInSeconds := minutes * 60 + if err := executeCommandsInBackgroundProcess( + sleepCommand(timeoutInSeconds), + voiceCommand("mob start", t.configuration.VoiceCommand), + notifyCommand("mob start", t.configuration.NotifyCommand), + "echo \"mobTimer\"", + ); err != nil { + return fmt.Errorf("break timer couldn't be started on your system (%s): %w", runtime.GOOS, err) + } + return nil +} + +func Moo(configuration config.Configuration) { + voiceMessage := "moo" + err := executeCommandsInBackgroundProcess(voiceCommand(voiceMessage, configuration.VoiceCommand)) + if err != nil { + say.Warning(fmt.Sprintf("can't run voice command on your system (%s)", runtime.GOOS)) + say.Warning(err.Error()) + return + } + say.Info(voiceMessage) +} + +func sleepCommand(timeoutInSeconds int) string { + return fmt.Sprintf("sleep %d", timeoutInSeconds) +} + +func voiceCommand(message string, voiceCommand string) string { + if len(voiceCommand) == 0 { + return "" + } + return injectCommandWithMessage(voiceCommand, message) +} + +func notifyCommand(message string, notifyCommand string) string { + if len(notifyCommand) == 0 { + return "" + } + return injectCommandWithMessage(notifyCommand, message) +} + +func injectCommandWithMessage(command string, message string) string { + placeHolders := strings.Count(command, "%s") + if placeHolders > 1 { + say.Error(fmt.Sprintf("Too many placeholders (%d) in format command string: %s", placeHolders, command)) + exit.Exit(1) + } + if placeHolders == 0 { + return fmt.Sprintf("%s %s", command, message) + } + return fmt.Sprintf(command, message) +} + +func executeCommandsInBackgroundProcess(commands ...string) error { + cmds := make([]string, 0) + for _, c := range commands { + if len(c) > 0 { + cmds = append(cmds, c) + } + } + say.Debug(fmt.Sprintf("Operating System %s", runtime.GOOS)) + var err error + switch runtime.GOOS { + case "windows": + err = runInBackground("powershell", "-command", fmt.Sprintf("start-process powershell -NoNewWindow -ArgumentList '-command \"%s\"'", strings.Join(cmds, ";"))) + case "darwin", "linux": + err = runInBackground("sh", "-c", fmt.Sprintf("(%s) &", strings.Join(cmds, ";"))) + default: + say.Warning(fmt.Sprintf("Cannot execute background commands on your os: %s", runtime.GOOS)) + } + return err +} + +func runInBackground(name string, args ...string) error { + command := exec.Command(name, args...) + if len(workdir.Path) > 0 { + command.Dir = workdir.Path + } + commandString := strings.Join(command.Args, " ") + say.Debug("Starting command " + commandString) + return command.Start() +} diff --git a/timer/localtimer/localtimer_test.go b/timer/localtimer/localtimer_test.go new file mode 100644 index 00000000..85135d8f --- /dev/null +++ b/timer/localtimer/localtimer_test.go @@ -0,0 +1,92 @@ +package localtimer + +import ( + "path/filepath" + "testing" + + config "github.com/remotemobprogramming/mob/v5/configuration" + "github.com/remotemobprogramming/mob/v5/say" + "github.com/remotemobprogramming/mob/v5/test" +) + +func TestIsActiveWhenTimerLocalTrue(t *testing.T) { + cfg := config.GetDefaultConfiguration() + cfg.TimerLocal = true + + timer := NewProcessLocalTimer(cfg) + + test.Equals(t, true, timer.IsActive()) +} + +func TestIsInactiveWhenTimerLocalFalse(t *testing.T) { + cfg := config.GetDefaultConfiguration() + cfg.TimerLocal = false + + timer := NewProcessLocalTimer(cfg) + + test.Equals(t, false, timer.IsActive()) +} + +func TestVoiceCommandReturnsEmptyWhenCommandNotConfigured(t *testing.T) { + result := voiceCommand("mob next", "") + + test.Equals(t, "", result) +} + +func TestVoiceCommandInjectsMessageWithPlaceholder(t *testing.T) { + result := voiceCommand("mob next", "say %s") + + test.Equals(t, "say mob next", result) +} + +func TestVoiceCommandAppendsMessageWithoutPlaceholder(t *testing.T) { + result := voiceCommand("mob next", "say") + + test.Equals(t, "say mob next", result) +} + +func TestStartTimerExecutesBackgroundProcess(t *testing.T) { + say.TurnOnDebugging() + output := test.CaptureOutput(t) + voiceFile := filepath.Join(t.TempDir(), "timer_voice") + notifyFile := filepath.Join(t.TempDir(), "timer_notify") + cfg := config.GetDefaultConfiguration() + cfg.VoiceCommand = "touch " + voiceFile + "; true" + cfg.NotifyCommand = "touch " + notifyFile + "; true" + timer := NewProcessLocalTimer(cfg) + + err := timer.StartTimer(0) + + test.Equals(t, nil, err) + test.AssertOutputContains(t, output, "Starting command") + test.AwaitFileCreated(t, voiceFile) + test.AwaitFileCreated(t, notifyFile) +} + +func TestStartBreakTimerExecutesBackgroundProcess(t *testing.T) { + say.TurnOnDebugging() + output := test.CaptureOutput(t) + voiceFile := filepath.Join(t.TempDir(), "break_timer_voice") + notifyFile := filepath.Join(t.TempDir(), "break_timer_notify") + cfg := config.GetDefaultConfiguration() + cfg.VoiceCommand = "touch " + voiceFile + "; true" + cfg.NotifyCommand = "touch " + notifyFile + "; true" + timer := NewProcessLocalTimer(cfg) + + err := timer.StartBreakTimer(0) + + test.Equals(t, nil, err) + test.AssertOutputContains(t, output, "Starting command") + test.AwaitFileCreated(t, voiceFile) + test.AwaitFileCreated(t, notifyFile) +} + +func TestMooLogsInfoMessage(t *testing.T) { + output := test.CaptureOutput(t) + cfg := config.GetDefaultConfiguration() + cfg.VoiceCommand = "echo" + + Moo(cfg) + + test.AssertOutputContains(t, output, "moo") +} diff --git a/timer/timer.go b/timer/timer.go new file mode 100644 index 00000000..b08022c0 --- /dev/null +++ b/timer/timer.go @@ -0,0 +1,115 @@ +package timer + +import ( + "errors" + "fmt" + "strconv" + "time" + + config "github.com/remotemobprogramming/mob/v5/configuration" + "github.com/remotemobprogramming/mob/v5/exit" + "github.com/remotemobprogramming/mob/v5/say" + "github.com/remotemobprogramming/mob/v5/timer/localtimer" + "github.com/remotemobprogramming/mob/v5/timer/webtimer" +) + +// Timer abstracts timer functionality so different implementations can be used. +type Timer interface { + IsActive() bool + StartTimer(minutes int) error + StartBreakTimer(minutes int) error +} + +func buildTimers(configuration config.Configuration) []Timer { + return []Timer{ + webtimer.NewWebTimer(configuration), + localtimer.NewProcessLocalTimer(configuration), + } +} + +func getActiveTimer(timers []Timer) Timer { + var active []string + var first Timer + for _, t := range timers { + if t.IsActive() { + active = append(active, fmt.Sprintf("%T", t)) + if first == nil { + first = t + } + } + } + say.Debug(fmt.Sprintf("Active timers: %v", active)) + say.Debug(fmt.Sprintf("Using timer: %T", first)) + return first +} + +// RunTimer parses timerInMinutes and starts the first active timer. +func RunTimer(timerInMinutes string, configuration config.Configuration) error { + return runWith(buildTimers(configuration), timerInMinutes) +} + +func runWith(timers []Timer, timerInMinutes string) error { + err, minutes := toMinutes(timerInMinutes) + if err != nil { + return err + } + + timeOfTimeout := time.Now().Add(time.Minute * time.Duration(minutes)).Format("15:04") + say.Debug(fmt.Sprintf("Starting timer at %s for %d minutes (parsed from user input %s)", timeOfTimeout, minutes, timerInMinutes)) + + timer := getActiveTimer(timers) + if timer == nil { + say.Error("No timer configured, not starting timer") + exit.Exit(1) + } + + if err := timer.StartTimer(minutes); err != nil { + say.Error(err.Error()) + exit.Exit(1) + } + + say.Info(fmt.Sprintf("It's now %s. %d min timer ends at approx. %s. Happy collaborating! :)", currentTime(), minutes, timeOfTimeout)) + return nil +} + +// RunBreakTimer parses timerInMinutes and starts the first active break timer. +func RunBreakTimer(timerInMinutes string, configuration config.Configuration) error { + return runBreakWith(buildTimers(configuration), timerInMinutes) +} + +func runBreakWith(timers []Timer, timerInMinutes string) error { + err, minutes := toMinutes(timerInMinutes) + if err != nil { + return err + } + + timeOfTimeout := time.Now().Add(time.Minute * time.Duration(minutes)).Format("15:04") + say.Debug(fmt.Sprintf("Starting break timer at %s for %d minutes (parsed from user input %s)", timeOfTimeout, minutes, timerInMinutes)) + + timer := getActiveTimer(timers) + if timer == nil { + say.Error("No break timer configured, not starting break timer") + exit.Exit(1) + } + + if err := timer.StartBreakTimer(minutes); err != nil { + say.Error(err.Error()) + exit.Exit(1) + } + + say.Info(fmt.Sprintf("It's now %s. %d min break timer ends at approx. %s. So take a break now! :)", currentTime(), minutes, timeOfTimeout)) + return nil +} + +func toMinutes(timerInMinutes string) (error, int) { + timeoutInMinutes, err := strconv.Atoi(timerInMinutes) + if err != nil || timeoutInMinutes < 1 { + say.Error(fmt.Sprintf("The parameter must be an integer number greater then zero")) + return errors.New("The parameter must be an integer number greater then zero"), 0 + } + return nil, timeoutInMinutes +} + +func currentTime() string { + return time.Now().Format("15:04") +} diff --git a/timer/timer_test.go b/timer/timer_test.go new file mode 100644 index 00000000..adac20ad --- /dev/null +++ b/timer/timer_test.go @@ -0,0 +1,104 @@ +package timer + +import ( + "testing" + + config "github.com/remotemobprogramming/mob/v5/configuration" + "github.com/remotemobprogramming/mob/v5/test" +) + +type mockTimer struct { + active bool + startTimerMinutes int + startBreakTimerMinutes int +} + +func (m *mockTimer) IsActive() bool { return m.active } +func (m *mockTimer) StartTimer(minutes int) error { + m.startTimerMinutes = minutes + return nil +} +func (m *mockTimer) StartBreakTimer(minutes int) error { + m.startBreakTimerMinutes = minutes + return nil +} + +func TestGetActiveTimerReturnsFirstActiveTimer(t *testing.T) { + inactive := &mockTimer{active: false} + active := &mockTimer{active: true} + + result := getActiveTimer([]Timer{inactive, active}) + + test.Equals(t, active, result) +} + +func TestGetActiveTimerReturnsNilWhenNoneActive(t *testing.T) { + result := getActiveTimer([]Timer{&mockTimer{active: false}}) + + test.Equals(t, nil, result) +} + +func TestGetActiveTimerPrefersFirstOverSecond(t *testing.T) { + first := &mockTimer{active: true} + second := &mockTimer{active: true} + + result := getActiveTimer([]Timer{first, second}) + + test.Equals(t, first, result) +} + +func TestRunWithPassesMinutesToStartTimer(t *testing.T) { + output := test.CaptureOutput(t) + mock := &mockTimer{active: true} + + runWith([]Timer{mock}, "5") + + test.Equals(t, 5, mock.startTimerMinutes) + test.AssertOutputContains(t, output, "Happy collaborating!") +} + +func TestRunBreakWithPassesMinutesToStartBreakTimer(t *testing.T) { + output := test.CaptureOutput(t) + mock := &mockTimer{active: true} + + runBreakWith([]Timer{mock}, "10") + + test.Equals(t, 10, mock.startBreakTimerMinutes) + test.AssertOutputContains(t, output, "So take a break now!") +} + +func TestRunTimerReturnsErrorForZeroMinutes(t *testing.T) { + output := test.CaptureOutput(t) + + err := RunTimer("0", config.GetDefaultConfiguration()) + + test.NotEquals(t, nil, err) + test.AssertOutputContains(t, output, "The parameter must be an integer number greater then zero") +} + +func TestRunTimerReturnsErrorForNonNumericInput(t *testing.T) { + output := test.CaptureOutput(t) + + err := RunTimer("NotANumber", config.GetDefaultConfiguration()) + + test.NotEquals(t, nil, err) + test.AssertOutputContains(t, output, "The parameter must be an integer number greater then zero") +} + +func TestRunBreakTimerReturnsErrorForZeroMinutes(t *testing.T) { + output := test.CaptureOutput(t) + + err := RunBreakTimer("0", config.GetDefaultConfiguration()) + + test.NotEquals(t, nil, err) + test.AssertOutputContains(t, output, "The parameter must be an integer number greater then zero") +} + +func TestRunBreakTimerReturnsErrorForNonNumericInput(t *testing.T) { + output := test.CaptureOutput(t) + + err := RunBreakTimer("NotANumber", config.GetDefaultConfiguration()) + + test.NotEquals(t, nil, err) + test.AssertOutputContains(t, output, "The parameter must be an integer number greater then zero") +} diff --git a/timer/webtimer/webtimer.go b/timer/webtimer/webtimer.go new file mode 100644 index 00000000..6d06190e --- /dev/null +++ b/timer/webtimer/webtimer.go @@ -0,0 +1,77 @@ +package webtimer + +import ( + "encoding/json" + "fmt" + + config "github.com/remotemobprogramming/mob/v5/configuration" + "github.com/remotemobprogramming/mob/v5/git" + "github.com/remotemobprogramming/mob/v5/httpclient" +) + +// WebTimer is a Timer implementation that notifies a remote timer service via HTTP. +type WebTimer struct { + room string + timerUser string + timerUrl string + timerInsecure bool +} + +func NewWebTimer(configuration config.Configuration) WebTimer { + room := configuration.TimerRoom + if configuration.TimerRoomUseWipBranchQualifier && configuration.WipBranchQualifier != "" { + room = configuration.WipBranchQualifier + } + return WebTimer{ + room: room, + timerUser: getUserForMobTimer(configuration.TimerUser), + timerUrl: configuration.TimerUrl, + timerInsecure: configuration.TimerInsecure, + } +} + +func getUserForMobTimer(userOverride string) string { + if userOverride == "" { + gitClient := &git.Client{} + return gitClient.UserName() + } + return userOverride +} + +func (t WebTimer) IsActive() bool { + return t.room != "" +} + +func (t WebTimer) StartTimer(minutes int) error { + if err := httpPutTimer(minutes, t.room, t.timerUser, t.timerUrl, t.timerInsecure); err != nil { + return fmt.Errorf("remote timer couldn't be started: %w", err) + } + return nil +} + +func (t WebTimer) StartBreakTimer(minutes int) error { + if err := httpPutBreakTimer(minutes, t.room, t.timerUser, t.timerUrl, t.timerInsecure); err != nil { + return fmt.Errorf("remote break timer couldn't be started: %w", err) + } + return nil +} + +func httpPutTimer(timeoutInMinutes int, room string, user string, timerService string, disableSSLVerification bool) error { + putBody, _ := json.Marshal(map[string]interface{}{ + "timer": timeoutInMinutes, + "user": user, + }) + client := httpclient.CreateHttpClient(disableSSLVerification) + _, err := client.SendRequest(putBody, "PUT", timerService+room) + return err +} + +func httpPutBreakTimer(timeoutInMinutes int, room string, user string, timerService string, disableSSLVerification bool) error { + putBody, _ := json.Marshal(map[string]interface{}{ + "breaktimer": timeoutInMinutes, + "user": user, + }) + client := httpclient.CreateHttpClient(disableSSLVerification) + _, err := client.SendRequest(putBody, "PUT", timerService+room) + return err +} diff --git a/timer/webtimer/webtimer_test.go b/timer/webtimer/webtimer_test.go new file mode 100644 index 00000000..83aa32e0 --- /dev/null +++ b/timer/webtimer/webtimer_test.go @@ -0,0 +1,104 @@ +package webtimer_test + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + config "github.com/remotemobprogramming/mob/v5/configuration" + "github.com/remotemobprogramming/mob/v5/test" + "github.com/remotemobprogramming/mob/v5/timer/webtimer" +) + +func newCapturingServer(t *testing.T) (*httptest.Server, *string, *[]byte) { + t.Helper() + var capturedMethod string + var capturedBody []byte + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedMethod = r.Method + capturedBody, _ = io.ReadAll(r.Body) + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(server.Close) + return server, &capturedMethod, &capturedBody +} + +func TestIsActiveWhenRoomIsSet(t *testing.T) { + cfg := config.GetDefaultConfiguration() + cfg.TimerRoom = "testroom" + + timer := webtimer.NewWebTimer(cfg) + + test.Equals(t, true, timer.IsActive()) +} + +func TestIsInactiveWhenRoomIsEmpty(t *testing.T) { + cfg := config.GetDefaultConfiguration() + cfg.TimerRoom = "" + + timer := webtimer.NewWebTimer(cfg) + + test.Equals(t, false, timer.IsActive()) +} + +func TestUsesWipBranchQualifierAsRoom(t *testing.T) { + cfg := config.GetDefaultConfiguration() + cfg.TimerRoom = "" + cfg.TimerRoomUseWipBranchQualifier = true + cfg.WipBranchQualifier = "feature-x" + + timer := webtimer.NewWebTimer(cfg) + + test.Equals(t, true, timer.IsActive()) +} + +func TestUsesTimerRoomWhenWipBranchQualifierIsEmpty(t *testing.T) { + cfg := config.GetDefaultConfiguration() + cfg.TimerRoom = "myroom" + cfg.TimerRoomUseWipBranchQualifier = true + cfg.WipBranchQualifier = "" + + timer := webtimer.NewWebTimer(cfg) + + test.Equals(t, true, timer.IsActive()) +} + +func TestStartTimerSendsPutWithTimerAndUser(t *testing.T) { + server, capturedMethod, capturedBody := newCapturingServer(t) + + cfg := config.GetDefaultConfiguration() + cfg.TimerRoom = "testroom" + cfg.TimerUser = "testuser" + cfg.TimerUrl = server.URL + "/" + timer := webtimer.NewWebTimer(cfg) + + err := timer.StartTimer(10) + + var body map[string]interface{} + json.Unmarshal(*capturedBody, &body) + test.Equals(t, nil, err) + test.Equals(t, "PUT", *capturedMethod) + test.Equals(t, float64(10), body["timer"]) + test.Equals(t, "testuser", body["user"]) +} + +func TestStartBreakTimerSendsPutWithBreakTimerAndUser(t *testing.T) { + server, capturedMethod, capturedBody := newCapturingServer(t) + + cfg := config.GetDefaultConfiguration() + cfg.TimerRoom = "testroom" + cfg.TimerUser = "testuser" + cfg.TimerUrl = server.URL + "/" + timer := webtimer.NewWebTimer(cfg) + + err := timer.StartBreakTimer(5) + + var body map[string]interface{} + json.Unmarshal(*capturedBody, &body) + test.Equals(t, nil, err) + test.Equals(t, "PUT", *capturedMethod) + test.Equals(t, float64(5), body["breaktimer"]) + test.Equals(t, "testuser", body["user"]) +}