From 533681e0f83ec4f69b3b8e9f1982ed9f089285b4 Mon Sep 17 00:00:00 2001 From: Clemens Fries Date: Tue, 1 Nov 2016 17:11:58 +0100 Subject: Initial commit --- cmd/check.go | 131 ++++++++++++++++++++++ cmd/create.go | 212 +++++++++++++++++++++++++++++++++++ cmd/create_test.go | 41 +++++++ cmd/debug.go | 73 +++++++++++++ cmd/next.go | 91 +++++++++++++++ cmd/run.go | 316 +++++++++++++++++++++++++++++++++++++++++++++++++++++ cmd/run_test.go | 39 +++++++ 7 files changed, 903 insertions(+) create mode 100644 cmd/check.go create mode 100644 cmd/create.go create mode 100644 cmd/create_test.go create mode 100644 cmd/debug.go create mode 100644 cmd/next.go create mode 100644 cmd/run.go create mode 100644 cmd/run_test.go (limited to 'cmd') diff --git a/cmd/check.go b/cmd/check.go new file mode 100644 index 0000000..2b7316e --- /dev/null +++ b/cmd/check.go @@ -0,0 +1,131 @@ +/* check.go: check a given message for problems + * + * Copyright (C) 2016 Clemens Fries + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package cmd + +import ( + "fmt" + "github.com/docopt/docopt.go" + . "github.com/githubert/clockrotz/common" + "os" + "path/filepath" + "sort" +) + +var usageCheck = +// tag::check[] +` +Usage: + clockrotz check [--silent] [FILE] + +Options: + --silent Suppress output, useful for silent checks. + FILE Check only the given file. + +If no FILE is provided, check will inspect all messages in the todo/ and the +drafts/ folder. The command exit with a code of 0, if there were no problems. +` // end::check[] + +func Check(argv []string, conf *Configuration) { + args, _ := docopt.Parse(usageCheck, argv, true, "", false) + + silent := args["--silent"].(bool) + + ok := true + + if args["FILE"] != nil { + message, err := NewMessageFromFile(args["FILE"].(string)) + + if err != nil { + fmt.Printf("Error while reading message: %s\n", err.Error()) + os.Exit(1) + } + + message.Conf.MergeWith(conf) + ok = checkMessage(message, silent) + } else { + draftOk := checkFolder(DIR_DRAFTS, conf, silent) + + if !silent { + // A bit of space between the drafts/ and todo/ listing + fmt.Println() + } + + todoOk := checkFolder(DIR_TODO, conf, silent) + + ok = draftOk && todoOk + } + + if ok { + os.Exit(0) + } else { + os.Exit(1) + } +} + +// Inspect all messages in a folder. +func checkFolder(folder string, conf *Configuration, silent bool) bool { + messages := NewMessagesFromDirectory(filepath.Join(conf.Get(CONF_WORKDIR), folder)) + sort.Sort(Messages(messages)) + + if !silent { + fmt.Printf("in %s:\n", folder) + } + + ok := true + count := 0 + + for _, message := range messages { + count++ + message.Conf.MergeWith(conf) + + if !checkMessage(message, silent) { + ok = false + } + } + + if ok && !silent { + fmt.Printf(" All (%d) messages are valid.\n", count) + } + + return ok +} + +// Check the given message. If `silent` is false, all problems will be printed +// to stdout. +func checkMessage(message Message, silent bool) bool { + ok := true + errs := message.Verify() + + if errs != nil { + ok = false + } + + if silent { + return ok + } + + if !ok { + fmt.Printf(" %s:\n", message.Name) + + for _, err := range errs { + fmt.Printf(" %s\n", err.Error()) + } + } + + return ok +} diff --git a/cmd/create.go b/cmd/create.go new file mode 100644 index 0000000..3d02bc7 --- /dev/null +++ b/cmd/create.go @@ -0,0 +1,212 @@ +/* create.go: help creating a new message + * + * Copyright (C) 2016 Clemens Fries + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package cmd + +import ( + "bufio" + "fmt" + "github.com/docopt/docopt.go" + . "github.com/githubert/clockrotz/common" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +var usageCreate = +// tag::create[] +` +Usage: + clockrotz create [--draft=FILE] [options] + +Options: + --help Show this help. + --to=ADDR Destination address. + --from=ADDR Sender address. + --subject=ADDR Short subject. + --cc=ADDR Set "Cc". + --bcc=ADDR Set "Bcc". + --reply-to=ADDR Set "Reply-To". + --draft=FILE Use FILE from the drafts/ folder as template. +` // end::create[] + +func Create(argv []string, conf *Configuration) { + args, _ := docopt.Parse(usageCreate, argv, true, "", false) + + tmpFile, err := ioutil.TempFile("", "clockrotz") + + if err != nil { + fmt.Printf("Error while creating temporary file: %s\n", err.Error()) + os.Exit(1) + } + + // We close the file right away, because we need only its soulless + // shell, mwhaha. + tmpFile.Close() + + draftsDir := filepath.Join(conf.Get(CONF_WORKDIR), DIR_DRAFTS) + todoDir := filepath.Join(conf.Get(CONF_WORKDIR), DIR_TODO) + + defer os.Remove(tmpFile.Name()) + + message := NewMessage() + + if args["--draft"] != nil { + draft := filepath.Join(draftsDir, args["--draft"].(string)) + m, err := NewMessageFromFile(draft) + + if err != nil { + fmt.Printf("Error while reading draft: %s\n", err.Error()) + os.Exit(1) + } + + message = &m + } + + message.Conf.MergeWithDocOptArgs(CMD_USAGE, &args) + + // MergeWithDocOptArgs will also copy --draft and --help over, but we do not want + // that. + delete(message.Conf.Data, "draft") + delete(message.Conf.Data, "help") + + if message.Get("date") == "" { + // Add tomorrow's date. + message.Conf.Set("date", time.Now().AddDate(0, 0, 1).Format(DATE_FORMAT)) + } + + if message.Get("subject") == "" { + message.Conf.Set("subject", "Type subject here") + } + + if len(message.Body) == 0 { + message.Body = append(message.Body, "Add message to the world of tomorrow here.") + } + + message.WriteToFile(tmpFile.Name()) + + editor := editor() + + fmt.Printf("Opening %s using %s.\n", tmpFile.Name(), editor) + + cmd := exec.Command(editor, tmpFile.Name()) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Run() + + if cmd.ProcessState.Success() { + fmt.Printf("\nSave message? ([(y)es], (d)raft, (n)o): ") + reader := bufio.NewReader(os.Stdin) + response, err := reader.ReadString('\n') + + if err != nil { + fmt.Printf("Error when reading response: %s\n", err.Error()) + os.Exit(1) + } + + var dst string + + saved := false + + response = strings.TrimSpace(response) + + switch response { + case "y", "yes", "": + dst = filepath.Join(todoDir, filepath.Base(tmpFile.Name())+".msg") + dst, err = copyFile(tmpFile.Name(), dst, false) + saved = true + case "d", "draft": + dst = filepath.Join(draftsDir, filepath.Base(tmpFile.Name())+".msg") + dst, err = copyFile(tmpFile.Name(), dst, false) + saved = true + } + + if err != nil { + fmt.Printf("Error when saving %s: %s\n", dst, err) + } else if saved { + fmt.Printf("Saved as: %s\n", dst) + } else { + fmt.Println("Discarding message.") + } + } +} + +// Copy file from `src` to `dst`. If `overwrite` is false, then an alternative +// file name will be used and returned as string. +// TODO: Portability issues (cp) / https://github.com/golang/go/issues/8868 +func copyFile(src, dst string, overwrite bool) (string, error) { + if !overwrite { + var err error + + dst, err = nextFreeFilename(dst) + + if err != nil { + return "", err + } + } + + err := exec.Command("cp", "-f", src, dst).Run() + + return dst, err +} + +func nextFreeFilename(dst string) (string, error) { + _, e := os.Stat(dst) + + // If the file does not exist, we can use the name + if os.IsNotExist(e) { + return dst, nil + } + + alt := "" + + for i := 0; i < 255; i++ { + name := fmt.Sprintf("%s.%d", dst, i) + _, e := os.Stat(name) + + if os.IsNotExist(e) { + alt = name + break + } + } + + if alt == "" { + return "", fmt.Errorf("No suitable file name could be found.") + } + + return alt, nil +} + +func editor() string { + editor := os.Getenv("VISUAL") + + if editor != "" { + return editor + } + + editor = os.Getenv("EDITOR") + + if editor != "" { + return editor + } + + return "vi" +} diff --git a/cmd/create_test.go b/cmd/create_test.go new file mode 100644 index 0000000..388ce36 --- /dev/null +++ b/cmd/create_test.go @@ -0,0 +1,41 @@ +/* create_test.go: unit tests for 'create' command + * + * Copyright (C) 2016 Clemens Fries + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package cmd + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestEditor(t *testing.T) { + os.Setenv("VISUAL", "") + os.Setenv("EDITOR", "") + + assert.Equal(t, "vi", editor()) + + os.Setenv("EDITOR", "editor") + assert.Equal(t, "editor", editor()) + + os.Setenv("VISUAL", "visual") + assert.Equal(t, "visual", editor()) +} \ No newline at end of file diff --git a/cmd/debug.go b/cmd/debug.go new file mode 100644 index 0000000..03f529b --- /dev/null +++ b/cmd/debug.go @@ -0,0 +1,73 @@ +/* debug.go: show debug information for a provided message + * + * Copyright (C) 2016 Clemens Fries + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package cmd + +import ( + "fmt" + "github.com/docopt/docopt.go" + . "github.com/githubert/clockrotz/common" + "os" +) + +var usageDebug = +// tag::debug[] +` +Usage: + clockrotz debug FILENAME + +Options: + --help Show this help. + FILENAME File name of the message to inspect. +` // end::debug[] + +func Debug(argv []string, conf *Configuration) { + args, _ := docopt.Parse(usageDebug, argv, true, "", false) + + message, err := NewMessageFromFile(args["FILENAME"].(string)) + + if err != nil { + fmt.Printf("Error while reading file: %s\n", err.Error()) + os.Exit(1) + } + + fmt.Println("Configuration\n-------------") + + message.Conf.MergeWith(conf) + + for _, line := range message.Conf.DumpConfig() { + fmt.Println(line) + } + + // TODO: Verify Message? + e, err := prepareEmail(&message) + + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + m, err := e.Bytes() + + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + fmt.Println("\nEmail message\n-------------") + fmt.Println(string(m)) +} diff --git a/cmd/next.go b/cmd/next.go new file mode 100644 index 0000000..f62aad2 --- /dev/null +++ b/cmd/next.go @@ -0,0 +1,91 @@ +/* next.go: show upcoming messages + * + * Copyright (C) 2016 Clemens Fries + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package cmd + +import ( + "fmt" + "github.com/docopt/docopt.go" + . "github.com/githubert/clockrotz/common" + "path/filepath" + "sort" + "strconv" + "time" +) + +var usageNext = +// tag::next[] +` +Usage: + clockrotz next [--days=DAYS | --all] + +Options: + --help Show this help. + --days=DAYS List messages for the next DAYS days. (default: 7) + --all List all pending messages. +` // end::next[] + +func Next(argv []string, conf *Configuration) { + args, _ := docopt.Parse(usageNext, argv, true, "", false) + + if args["--days"] != nil { + conf.Set("days", args["--days"].(string)) + } + + days, err := strconv.ParseInt(conf.Get(CONF_DAYS), 10, 0) + + if err != nil { + fmt.Println("Error while parsing value of --days") + return + } + + all := args["--all"].(bool) + + future := buildTime(time.Now().AddDate(0, 0, int(days)), 23, 59, false) + + messages := NewMessagesFromDirectory(filepath.Join(conf.Get(CONF_WORKDIR), DIR_TODO)) + sort.Sort(messages) + + if all { + fmt.Printf("Showing all messages.\n\n") + } else { + fmt.Printf("Showing messages before %s.\n\n", future.Format(DATETIME_FORMAT)) + } + + count := 0 + + for _, message := range messages { + message.Conf.MergeWith(conf) + + if errs := message.Verify(); errs != nil { + fmt.Printf("Error in message \"%s\". Please run 'clockrotz check'.\n", message.Name) + continue + } + + // Errors are caught already by Verify() + messageDate, _ := ParseTime(message.Get(CONF_DATE)) + + if all || messageDate.Before(future) { + count++ + fmt.Printf("%s %s (%s)\n", messageDate.Format(DATETIME_FORMAT), message.Get(CONF_SUBJECT), message.Name) + } + } + + if count == 0 { + fmt.Println("No messages.") + } +} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..4ba7b16 --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,316 @@ +/* run.go: send pending messages + * + * Copyright (C) 2016 Clemens Fries + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package cmd + +import ( + "crypto/tls" + "fmt" + "github.com/docopt/docopt.go" + . "github.com/githubert/clockrotz/common" + "github.com/jordan-wright/email" + "net/mail" + "net/textproto" + "os" + "path/filepath" + "strings" + "time" +) + +var usageRun = +// tag::run[] +` +Usage: + clockrotz run [options] + +Options: + --help Show this help. + --to=ADDR Destination address. + --from=ADDR Sender address. + --subject=ADDR Short subject. + --cc=ADDR Set "Cc". + --bcc=ADDR Set "Bcc". + --reply-to=ADDR Set "Reply-To". + --not-before=TIME Not before TIME. (default: 00:00) + --not-after=TIME Not after TIME. (default: 23:59) + --server=HOST SMTP hostname. (default: localhost) + --port=PORT SMTP port. (default: 587) + --verbose Report on successfully sent messages. + --dry-run Do not send the message. + --insecure Accept any TLS certificate. +` // end::run[] + +func Run(argv []string, conf *Configuration) { + args, _ := docopt.Parse(usageRun, argv, true, "", false) + conf.MergeWithDocOptArgs(CMD_RUN, &args) + + verbose := args["--verbose"].(bool) + dryRun := args["--dry-run"].(bool) + insecure := args["--insecure"].(bool) + + now := time.Now() + + notBeforeTime, err := time.Parse(TIME_FORMAT, conf.Get(CONF_NOT_BEFORE)) + + if err != nil { + fmt.Printf("Failed parsing not-before time: %s\n", err.Error()) + return + } + + notAfterTime, err := time.Parse(TIME_FORMAT, conf.Get(CONF_NOT_AFTER)) + + if err != nil { + fmt.Printf("Failed parsing not-after time: %s\n", err.Error()) + return + } + + notBefore := buildTime(now, notBeforeTime.Hour(), notBeforeTime.Minute(), true) + notAfter := buildTime(now, notAfterTime.Hour(), notAfterTime.Minute(), false) + + // Return if we are in some quiet period. + if now.After(notAfter) || now.Before(notBefore) { + return + } + + messages := NewMessagesFromDirectory(filepath.Join(conf.Get(CONF_WORKDIR), DIR_TODO)) + + verificationError := false + + for _, message := range messages { + message.Conf.MergeWith(conf) + err := processMessage(message, now, dryRun, insecure, verbose) + + if err != nil { + verificationError = true + continue + } + } + + if verificationError { + // FIXME: This suggests that other messages were not sent, but they were.... + fmt.Println("There were errors when verifying one or more messages.") + fmt.Println("Please run 'clockrotz check'") + os.Exit(1) + } +} + +func processMessage(message Message, now time.Time, dryRun, insecure, verbose bool) error { + if errs := message.Verify(); errs != nil { + return fmt.Errorf("Message %s failed verification.", message.Name) + } + + date, err := ParseTime(message.Get("date")) + + if err != nil { + return err + } + + if !now.After(date) { + return nil + } + + sendErr := sendMessage(message, dryRun, insecure) + + if sendErr != nil { + if !dryRun { + err := moveMessage(message, DIR_ERRORS) + + if err != nil { + fmt.Printf("Error when moving message %s: %s\n", message.Name, err.Error()) + } + + logMessage(message, DIR_ERRORS, sendErr.Error()) + } + + fmt.Printf("Error when sending message %s: %s\n", message.Name, sendErr.Error()) + } else { + if !dryRun { + err := moveMessage(message, DIR_DONE) + + if err != nil { + fmt.Printf("Error when moving message %s: %s\n", message.Name, err.Error()) + } + + logMessage(message, DIR_DONE, "Successfully delivered.") + } + + if verbose { + fmt.Printf("Message %s delivered.\n", message.Name) + } + } + + return nil // TODO: what about errors that are not verification errors? +} + +func logMessage(message Message, dir string, logMessage string) { + dstDir := filepath.Join(message.Get(CONF_WORKDIR), dir) + filename := filepath.Join(dstDir, message.Name[:len(message.Name)-len(".msg")]+".log") + + f, err := os.Create(filename) + + if err != nil { + fmt.Printf("Error when creating log file: %s\n", err.Error()) + return + } + + defer f.Close() + + f.WriteString("Log message:\n") + f.WriteString(fmt.Sprintf(" %s\n", logMessage)) + f.WriteString("\n") + + f.WriteString("\nConfiguration:\n") + for _, s := range message.Conf.DumpConfig() { + f.WriteString(fmt.Sprintf(" %s\n", s)) + } + + f.WriteString("\nBody:\n") + for _, s := range message.Body { + f.WriteString(fmt.Sprintf(" %s\n", s)) + } +} + +// Turn the given plain address list string into an array. +func getAddresses(addressList string) ([]string, error) { + addresses, err := mail.ParseAddressList(addressList) + + if err != nil { + return nil, err + } + + result := []string{} + + for _, address := range addresses { + result = append(result, address.String()) + } + + return result, nil +} + +// Prepare a ready-to-send Email message. +func prepareEmail(message *Message) (*email.Email, error) { + e := email.NewEmail() + e.From = message.Get(CONF_FROM) + e.Subject = message.Get(CONF_SUBJECT) + + // Build list of To addresses. + to, err := getAddresses(message.Get(CONF_TO)) + + if err != nil { + return nil, err + } + + e.To = to + + // Set optional Reply-To header. + if r := message.Get(CONF_REPLY_TO); r != "" { + replyTo, err := getAddresses(r) + + if err != nil { + return nil, err + } + + e.Headers = textproto.MIMEHeader{"Reply-To": replyTo} + } + + // Build list of Cc addresses. + if r := message.Get(CONF_CC); r != "" { + cc, err := getAddresses(r) + + if err != nil { + return nil, err + } + + e.Cc = cc + } + + // Build list of Bcc addresses. + if r := message.Get(CONF_BCC); r != "" { + bcc, err := getAddresses(r) + + if err != nil { + return nil, err + } + + e.Bcc = bcc + } + + e.Text = []byte(strings.Join(message.Body, "\n")) + + return e, nil +} + +// Send the given message, unless `dryRun` is true. Use `insecure` to work +// around things like self-signed certificates. +func sendMessage(message Message, dryRun bool, insecure bool) error { + e, err := prepareEmail(&message) + + if err != nil { + return err + } + + smtpServer := message.Get(CONF_SMTP_SERVER) + ":" + message.Get(CONF_SMTP_PORT) + + if dryRun { + fmt.Printf("Skip sending message %s through %s.\n", message.Name, smtpServer) + return nil + } + + if insecure { + return e.SendWithTLS(smtpServer, nil, &tls.Config{InsecureSkipVerify: true}) + } else { + return e.Send(smtpServer, nil) + } +} + +// Move the given message to a folder relative to the working directory. +func moveMessage(message Message, relative string) error { + todo := filepath.Join(message.Get(CONF_WORKDIR), DIR_TODO) + to := filepath.Join(message.Get(CONF_WORKDIR), relative) + + // FIXME: BUG: This will overwrite existing messages. Look at create.go:nextFreeFilename() + // for ideas on how to resolve this. We could try to use a similar approach. + // `foo.msg` to `foo.1.msg` and `foo.1.log`. + + return os.Rename(filepath.Join(todo, message.Name), filepath.Join(to, message.Name)) +} + +// Build a time.Time from some given base time. If floor is true, seconds will +// be set to 0, if false, 59. +// TODO: Maybe there is a better way?… +func buildTime(base time.Time, hour int, minute int, floor bool) time.Time { + seconds := 0 + + if !floor { + // We ignore the possibility of leap seconds here, this is + // just done so that we can get 23:59:59 instead of 23:59:00. + // 23:59:00 would make us miss a whole minute. It would be + // nice if there were a way to indicate to time.Date() to + // build a date with start of the day / end of the day... + seconds = 59 + } + + return time.Date( + base.Year(), + base.Month(), + base.Day(), + hour, + minute, + seconds, + 0, + base.Location()) +} diff --git a/cmd/run_test.go b/cmd/run_test.go new file mode 100644 index 0000000..2c405e9 --- /dev/null +++ b/cmd/run_test.go @@ -0,0 +1,39 @@ +/* run_test.go: unit tests for 'run' command + * + * Copyright (C) 2016 Clemens Fries + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package cmd + +import ( + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestBuildTime(t *testing.T) { + h := 8 + m := 30 + + t1 := buildTime(time.Now(), h, m, true) + + assert.Equal(t, t1.Hour(), 8) + assert.Equal(t, t1.Minute(), 30) + assert.Equal(t, t1.Second(), 0) + + t2 := buildTime(time.Now(), h, m, false) + + assert.Equal(t, t2.Second(), 59) +} -- cgit