From a523504c40e9ed05233799466258fa5101543acc Mon Sep 17 00:00:00 2001 From: David Dworken Date: Sat, 8 Jan 2022 21:59:28 -0800 Subject: [PATCH] split into local client and remote client, and add tests --- README.md | 5 + client/client.go | 207 --------------------------------------- clients/local/client.go | 46 +++++++++ clients/remote/client.go | 98 ++++++++++++++++++ go.mod | 4 +- server/server.go | 42 ++------ server/server_test.go | 58 +++++++++++ shared/client.go | 133 +++++++++++++++++++++++++ shared/client_test.go | 66 +++++++++++++ shared/data.go | 40 +++++++- shared/data_test.go | 26 +++++ shared/testutils.go | 40 ++++++++ 12 files changed, 522 insertions(+), 243 deletions(-) create mode 100644 README.md delete mode 100644 client/client.go create mode 100644 clients/local/client.go create mode 100644 clients/remote/client.go create mode 100644 server/server_test.go create mode 100644 shared/client.go create mode 100644 shared/client_test.go create mode 100644 shared/data_test.go create mode 100644 shared/testutils.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b349c9 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +Installation: + +``` +export PROMPT_COMMAND='~/.hishtory-client upload $? "`history 1`"' +``` diff --git a/client/client.go b/client/client.go deleted file mode 100644 index ac75ddb..0000000 --- a/client/client.go +++ /dev/null @@ -1,207 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "os" - "os/user" - "path" - "strconv" - "strings" - "time" - - "github.com/fatih/color" - "github.com/google/uuid" - "github.com/rodaine/table" - - "github.com/ddworken/hishtory/shared" -) - -func main() { - switch os.Args[1] { - case "upload": - upload() - case "query": - query() - case "init": - setup() - } -} - -func setup() { - userSecret := uuid.Must(uuid.NewRandom()).String() - if len(os.Args) > 2 && os.Args[2] != "" { - userSecret = os.Args[2] - } - fmt.Println("Setting secret hishtory key to " + string(userSecret)) - - homedir, err := os.UserHomeDir() - if err != nil { - panic(err) - } - err = os.WriteFile(path.Join(homedir, ".hishtory.secret"), []byte(userSecret), 0400) - if err != nil { - panic(err) - } -} - -func getUserSecret() (string, error) { - homedir, err := os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("failed to read secret hishtory key: %v", err) - } - secret, err := os.ReadFile(path.Join(homedir, ".hishtory.secret")) - if err != nil { - return "", fmt.Errorf("failed to read secret hishtory key: %v", err) - } - return string(secret), nil -} - -func getServerHostname() string { - if server := os.Getenv("HISHTORY_SERVER"); server != "" { - return server - } - return "http://localhost:8080" -} - -func query() { - userSecret, err := getUserSecret() - if err != nil { - panic(err) - } - - req, err := http.NewRequest("GET", getServerHostname()+"/api/v1/search", nil) - if err != nil { - panic(err) - } - - q := req.URL.Query() - q.Add("query", strings.Join(os.Args[2:], " ")) - q.Add("user_secret", userSecret) - q.Add("limit", "25") - req.URL.RawQuery = q.Encode() - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - panic(err) - } - defer resp.Body.Close() - resp_body, err := ioutil.ReadAll(resp.Body) - if err != nil { - panic(err) - } - if resp.Status != "200 OK" { - panic("search API returned invalid result. status=" + resp.Status) - } - - var data []*shared.HistoryEntry - err = json.Unmarshal(resp_body, &data) - if err != nil { - panic(err) - } - display(data) -} - -func display(results []*shared.HistoryEntry) { - headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc() - tbl := table.New("Hostname", "CWD", "Timestamp", "Exit Code", "Command") - tbl.WithHeaderFormatter(headerFmt) - - for _, result := range results { - tbl.AddRow(result.Hostname, result.CurrentWorkingDirectory, result.EndTime.Format("Jan 2 2006 15:04:05 MST"), result.ExitCode, result.Command) - } - - tbl.Print() -} - -func upload() { - var entry shared.HistoryEntry - - // exitCode - exitCode, err := strconv.Atoi(os.Args[2]) - if err != nil { - panic(err) - } - entry.ExitCode = exitCode - - // user - user, err := user.Current() - if err != nil { - panic(err) - } - entry.LocalUsername = user.Username - - // cwd - cwd, err := getCwd() - if err != nil { - panic(err) - } - entry.CurrentWorkingDirectory = cwd - - // TODO(ddworken): start time - - // end time - entry.EndTime = time.Now() - - // command - cmd, err := getLastCommand(os.Args[3]) - if err != nil { - panic(err) - } - entry.Command = cmd - - // hostname - hostname, err := os.Hostname() - if err != nil { - panic(err) - } - entry.Hostname = hostname - - // user secret - userSecret, err := getUserSecret() - if err != nil { - panic(err) - } - entry.UserSecret = userSecret - - err = send(entry) - if err != nil { - panic(err) - } -} - -func getCwd() (string, error) { - cwd, err := os.Getwd() - if err != nil { - return "", fmt.Errorf("failed to get cwd for last command: %v", err) - } - homedir, err := os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("failed to get user's home directory: %v", err) - } - if strings.HasPrefix(cwd, homedir) { - return strings.Replace(cwd, homedir, "~", 1), nil - } - return cwd, nil -} - -func getLastCommand(history string) (string, error) { - return strings.TrimSpace(strings.SplitN(strings.TrimSpace(history), " ", 2)[1]), nil -} - -func send(entry shared.HistoryEntry) error { - jsonValue, err := json.Marshal(entry) - if err != nil { - return fmt.Errorf("failed to marshal HistoryEntry as json: %v", err) - } - - _, err = http.Post(getServerHostname()+"/api/v1/submit", "application/json", bytes.NewBuffer(jsonValue)) - if err != nil { - return fmt.Errorf("failed to send HistoryEntry to api: %v", err) - } - return nil -} diff --git a/clients/local/client.go b/clients/local/client.go new file mode 100644 index 0000000..b552de5 --- /dev/null +++ b/clients/local/client.go @@ -0,0 +1,46 @@ +package main + +import ( + "os" + + "github.com/ddworken/hishtory/shared" +) + +func main() { + switch os.Args[1] { + case "saveHistoryEntry": + saveHistoryEntry() + case "query": + query() + case "init": + err := shared.Setup(os.Args) + if err != nil { + panic(err) + } + } +} + +func getServerHostname() string { + if server := os.Getenv("HISHTORY_SERVER"); server != "" { + return server + } + return "http://localhost:8080" +} + +func query() { + // TODO(ddworken) + var data []*shared.HistoryEntry + shared.DisplayResults(data) +} + +func saveHistoryEntry() { + entry, err := shared.BuildHistoryEntry(os.Args) + if err != nil { + panic(err) + } + + err = shared.Persist(*entry) + if err != nil { + panic(err) + } +} diff --git a/clients/remote/client.go b/clients/remote/client.go new file mode 100644 index 0000000..6f17498 --- /dev/null +++ b/clients/remote/client.go @@ -0,0 +1,98 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "strings" + + "github.com/ddworken/hishtory/shared" +) + +func main() { + switch os.Args[1] { + case "saveHistoryEntry": + saveHistoryEntry() + case "query": + query() + case "init": + err := shared.Setup(os.Args) + if err != nil { + panic(err) + } + } +} + +func getServerHostname() string { + if server := os.Getenv("HISHTORY_SERVER"); server != "" { + return server + } + return "http://localhost:8080" +} + +func query() { + userSecret, err := shared.GetUserSecret() + if err != nil { + panic(err) + } + + req, err := http.NewRequest("GET", getServerHostname()+"/api/v1/search", nil) + if err != nil { + panic(err) + } + + q := req.URL.Query() + q.Add("query", strings.Join(os.Args[2:], " ")) + q.Add("user_secret", userSecret) + q.Add("limit", "25") + req.URL.RawQuery = q.Encode() + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + resp_body, err := ioutil.ReadAll(resp.Body) + if err != nil { + panic(err) + } + if resp.Status != "200 OK" { + panic("search API returned invalid result. status=" + resp.Status) + } + + var data []*shared.HistoryEntry + err = json.Unmarshal(resp_body, &data) + if err != nil { + panic(err) + } + shared.DisplayResults(data) +} + +func saveHistoryEntry() { + entry, err := shared.BuildHistoryEntry(os.Args) + if err != nil { + panic(err) + } + + err = send(*entry) + if err != nil { + panic(err) + } +} + +func send(entry shared.HistoryEntry) error { + jsonValue, err := json.Marshal(entry) + if err != nil { + return fmt.Errorf("failed to marshal HistoryEntry as json: %v", err) + } + + _, err = http.Post(getServerHostname()+"/api/v1/submit", "application/json", bytes.NewBuffer(jsonValue)) + if err != nil { + return fmt.Errorf("failed to send HistoryEntry to api: %v", err) + } + return nil +} diff --git a/go.mod b/go.mod index 7a08116..ad76c31 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,6 @@ require ( github.com/fatih/color v1.13.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/rodaine/table v1.0.1 // indirect - gorm.io/driver/sqlite v1.2.6 // indirect - gorm.io/gorm v1.22.4 // indirect + gorm.io/driver/sqlite v1.2.6 + gorm.io/gorm v1.22.4 ) diff --git a/server/server.go b/server/server.go index 361b3da..7065c72 100644 --- a/server/server.go +++ b/server/server.go @@ -5,46 +5,19 @@ import ( "fmt" "log" "net/http" - "os" - "path" "strconv" "strings" - "github.com/ddworken/hishtory/shared" - "gorm.io/driver/sqlite" "gorm.io/gorm" + + "github.com/ddworken/hishtory/shared" ) -func openDB() (*gorm.DB, error) { - homedir, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("failed to get user's home directory: %v", err) - } - db, err := gorm.Open(sqlite.Open(path.Join(homedir, ".hishtory.db")), &gorm.Config{}) - if err != nil { - panic("failed to connect database") - } - db.AutoMigrate(&shared.HistoryEntry{}) - return db, nil -} - -func persist(entry shared.HistoryEntry) error { - log.Printf("Saving %#v to the DB\n", entry) - db, err := openDB() - if err != nil { - return err - } - conn, err := db.DB() - defer conn.Close() - db.Create(&entry).Commit() - return nil -} - func search(db *gorm.DB, userSecret, query string, limit int) ([]*shared.HistoryEntry, error) { fmt.Println("Received search query: " + query) tokens, err := tokenize(query) if err != nil { - return nil, fmt.Errorf("failed to tokenize query: %v") + return nil, fmt.Errorf("failed to tokenize query: %v", err) } tx := db.Debug().Where("user_secret = ?", userSecret) for _, token := range tokens { @@ -72,6 +45,9 @@ func search(db *gorm.DB, userSecret, query string, limit int) ([]*shared.History } func tokenize(query string) ([]string, error) { + if query == "" { + return []string{}, nil + } return strings.Split(query, " "), nil } @@ -82,7 +58,7 @@ func apiSubmitHandler(w http.ResponseWriter, r *http.Request) { if err != nil { panic(err) } - err = persist(entry) + err = shared.Persist(entry) if err != nil { panic(err) } @@ -99,7 +75,7 @@ func apiSearchHandler(w http.ResponseWriter, r *http.Request) { if err != nil { limit = 0 } - db, err := openDB() + db, err := shared.OpenDB() if err != nil { panic(err) } @@ -118,7 +94,7 @@ func apiSearchHandler(w http.ResponseWriter, r *http.Request) { } func main() { - _, err := openDB() + _, err := shared.OpenDB() if err != nil { panic(err) } diff --git a/server/server_test.go b/server/server_test.go new file mode 100644 index 0000000..5cf462b --- /dev/null +++ b/server/server_test.go @@ -0,0 +1,58 @@ +package main + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/ddworken/hishtory/shared" +) + +func TestSubmitThenQuery(t *testing.T) { + // Set up + defer shared.BackupAndRestore(t) + shared.Check(t, shared.Setup([]string{})) + + // Submit an entry + entry, err := shared.BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / "}) + shared.Check(t, err) + reqBody, err := json.Marshal(entry) + shared.Check(t, err) + submitReq := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(reqBody)) + apiSubmitHandler(nil, submitReq) + // Also submit one for another user + otherEntry := *entry + otherEntry.UserSecret = "aaaaaaaaa" + otherEntry.Command = "other" + reqBody, err = json.Marshal(otherEntry) + shared.Check(t, err) + submitReq = httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(reqBody)) + apiSubmitHandler(nil, submitReq) + + // Retrieve the entry + secret, err := shared.GetUserSecret() + shared.Check(t, err) + w := httptest.NewRecorder() + searchReq := httptest.NewRequest(http.MethodGet, "/?user_secret="+secret, nil) + apiSearchHandler(w, searchReq) + res := w.Result() + defer res.Body.Close() + data, err := ioutil.ReadAll(res.Body) + shared.Check(t, err) + var retrievedEntries []*shared.HistoryEntry + shared.Check(t, json.Unmarshal(data, &retrievedEntries)) + dbEntry := retrievedEntries[0] + if len(retrievedEntries) != 1 { + t.Fatalf("Expected to retrieve 1 entry, found %d", len(retrievedEntries)) + } + if dbEntry.UserSecret != "" { + t.Fatalf("Response contains a user secret: %#v", *dbEntry) + } + entry.UserSecret = "" + if !shared.EntryEquals(*dbEntry, *entry) { + t.Fatalf("DB data is different than input! \ndb =%#v\ninput=%#v", *dbEntry, *entry) + } +} diff --git a/shared/client.go b/shared/client.go new file mode 100644 index 0000000..58bf93e --- /dev/null +++ b/shared/client.go @@ -0,0 +1,133 @@ +package shared + +import ( + "fmt" + "os" + "os/user" + "path" + "strconv" + "strings" + "time" + + "github.com/fatih/color" + "github.com/google/uuid" + "github.com/rodaine/table" +) + +const ( + SECRET_PATH = ".hishtory.secret" +) + +func getCwd() (string, error) { + cwd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get cwd for last command: %v", err) + } + homedir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get user's home directory: %v", err) + } + if strings.HasPrefix(cwd, homedir) { + return strings.Replace(cwd, homedir, "~", 1), nil + } + return cwd, nil +} + +func BuildHistoryEntry(args []string) (*HistoryEntry, error) { + var entry HistoryEntry + + // exitCode + exitCode, err := strconv.Atoi(args[2]) + if err != nil { + return nil, fmt.Errorf("failed to build history entry: %v", err) + } + entry.ExitCode = exitCode + + // user + user, err := user.Current() + if err != nil { + return nil, fmt.Errorf("failed to build history entry: %v", err) + } + entry.LocalUsername = user.Username + + // cwd + cwd, err := getCwd() + if err != nil { + return nil, fmt.Errorf("failed to build history entry: %v", err) + } + entry.CurrentWorkingDirectory = cwd + + // TODO(ddworken): start time + + // end time + entry.EndTime = time.Now() + + // command + cmd, err := getLastCommand(args[3]) + if err != nil { + return nil, fmt.Errorf("failed to build history entry: %v", err) + } + entry.Command = cmd + + // hostname + hostname, err := os.Hostname() + if err != nil { + return nil, fmt.Errorf("failed to build history entry: %v", err) + } + entry.Hostname = hostname + + // user secret + userSecret, err := GetUserSecret() + if err != nil { + return nil, fmt.Errorf("failed to build history entry: %v", err) + } + entry.UserSecret = userSecret + + return &entry, nil +} + +func getLastCommand(history string) (string, error) { + return strings.TrimSpace(strings.SplitN(strings.TrimSpace(history), " ", 2)[1]), nil +} + +func GetUserSecret() (string, error) { + homedir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to read secret hishtory key: %v", err) + } + secret, err := os.ReadFile(path.Join(homedir, SECRET_PATH)) + if err != nil { + return "", fmt.Errorf("failed to read secret hishtory key: %v", err) + } + return string(secret), nil +} + +func Setup(args []string) error { + userSecret := uuid.Must(uuid.NewRandom()).String() + if len(args) > 2 && args[2] != "" { + userSecret = args[2] + } + fmt.Println("Setting secret hishtory key to " + string(userSecret)) + + homedir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to retrieve homedir: %v", err) + } + err = os.WriteFile(path.Join(homedir, SECRET_PATH), []byte(userSecret), 0600) + if err != nil { + return fmt.Errorf("failed to write hishtory secret: %v", err) + } + return nil +} + +func DisplayResults(results []*HistoryEntry) { + headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc() + tbl := table.New("Hostname", "CWD", "Timestamp", "Exit Code", "Command") + tbl.WithHeaderFormatter(headerFmt) + + for _, result := range results { + tbl.AddRow(result.Hostname, result.CurrentWorkingDirectory, result.EndTime.Format("Jan 2 2006 15:04:05 MST"), result.ExitCode, result.Command) + } + + tbl.Print() +} diff --git a/shared/client_test.go b/shared/client_test.go new file mode 100644 index 0000000..2019084 --- /dev/null +++ b/shared/client_test.go @@ -0,0 +1,66 @@ +package shared + +import ( + "os" + "path" + "strings" + "testing" +) + +func TestSetup(t *testing.T) { + defer BackupAndRestore(t) + homedir, err := os.UserHomeDir() + Check(t, err) + if _, err := os.Stat(path.Join(homedir, SECRET_PATH)); err == nil { + t.Fatalf("hishtory secret file already exists!") + } + Check(t, Setup([]string{})) + if _, err := os.Stat(path.Join(homedir, SECRET_PATH)); err != nil { + t.Fatalf("hishtory secret file does not exist after Setup()!") + } + data, err := os.ReadFile(path.Join(homedir, SECRET_PATH)) + Check(t, err) + if len(data) < 10 { + t.Fatalf("hishtory secret has unexpected length: %d", len(data)) + } +} + +func TestBuildHistoryEntry(t *testing.T) { + defer BackupAndRestore(t) + Check(t, Setup([]string{})) + entry, err := BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / "}) + Check(t, err) + if entry.UserSecret == "" || len(entry.UserSecret) < 10 || strings.TrimSpace(entry.UserSecret) != entry.UserSecret { + t.Fatalf("history entry has unexpected user secret: %v", entry.UserSecret) + } + if entry.ExitCode != 120 { + t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode) + } + if entry.LocalUsername != "david" { + t.Fatalf("history entry has unexpected user name: %v", entry.LocalUsername) + } + if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") { + t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory) + } + if entry.Command != "ls /" { + t.Fatalf("history entry has unexpected command: %v", entry.Command) + } +} + +func TestGetUserSecret(t *testing.T) { + defer BackupAndRestore(t) + Check(t, Setup([]string{})) + secret1, err := GetUserSecret() + Check(t, err) + if len(secret1) < 10 || strings.Contains(secret1, " ") || strings.Contains(secret1, "\n") { + t.Fatalf("unexpected secret: %v", secret1) + } + + Check(t, Setup([]string{})) + secret2, err := GetUserSecret() + Check(t, err) + + if secret1 == secret2 { + t.Fatalf("GetUserSecret() returned the same values for different setups! val=%v", secret1) + } +} diff --git a/shared/data.go b/shared/data.go index 5a7080e..c6f7677 100644 --- a/shared/data.go +++ b/shared/data.go @@ -1,6 +1,15 @@ package shared -import "time" +import ( + "fmt" + "log" + "os" + "path" + "time" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) type HistoryEntry struct { UserSecret string `json:"user_secret"` @@ -12,3 +21,32 @@ type HistoryEntry struct { StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` } + +const ( + DB_PATH = ".hishtory.db" +) + +func Persist(entry HistoryEntry) error { + log.Printf("Saving %#v to the DB\n", entry) + db, err := OpenDB() + if err != nil { + return err + } + conn, err := db.DB() + defer conn.Close() + db.Create(&entry).Commit() + return nil +} + +func OpenDB() (*gorm.DB, error) { + homedir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get user's home directory: %v", err) + } + db, err := gorm.Open(sqlite.Open(path.Join(homedir, DB_PATH)), &gorm.Config{}) + if err != nil { + panic("failed to connect database") + } + db.AutoMigrate(&HistoryEntry{}) + return db, nil +} diff --git a/shared/data_test.go b/shared/data_test.go new file mode 100644 index 0000000..7e2d2b3 --- /dev/null +++ b/shared/data_test.go @@ -0,0 +1,26 @@ +package shared + +import ( + "testing" +) + +func TestPersist(t *testing.T) { + defer BackupAndRestore(t) + Check(t, Setup([]string{})) + entry, err := BuildHistoryEntry([]string{"unused", "saveHistoryEntry", "120", " 123 ls / "}) + Check(t, err) + Check(t, Persist(*entry)) + + db, err := OpenDB() + Check(t, err) + var historyEntries []*HistoryEntry + result := db.Find(&historyEntries) + Check(t, result.Error) + if len(historyEntries) != 1 { + t.Fatalf("DB has %d entries, expected 1!", len(historyEntries)) + } + dbEntry := historyEntries[0] + if !EntryEquals(*entry, *dbEntry) { + t.Fatalf("DB data is different than input! \ndb =%#v \ninput=%#v", *dbEntry, *entry) + } +} diff --git a/shared/testutils.go b/shared/testutils.go new file mode 100644 index 0000000..8f59ebb --- /dev/null +++ b/shared/testutils.go @@ -0,0 +1,40 @@ +package shared + +import ( + "os" + "path" + "testing" + "time" +) + +func Check(t *testing.T, err error) { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } +} + +func BackupAndRestore(t *testing.T) func() { + homedir, err := os.UserHomeDir() + if err != nil { + t.Fatalf("failed to retrieve homedir: %v", err) + } + + os.Rename(path.Join(homedir, DB_PATH), path.Join(homedir, DB_PATH+".bak")) + os.Rename(path.Join(homedir, SECRET_PATH), path.Join(homedir, SECRET_PATH+".bak")) + return func() { + Check(t, os.Rename(path.Join(homedir, DB_PATH+".bak"), path.Join(homedir, DB_PATH))) + Check(t, os.Rename(path.Join(homedir, SECRET_PATH+".bak"), path.Join(homedir, SECRET_PATH))) + } +} + +func EntryEquals(entry1, entry2 HistoryEntry) bool { + return entry1.UserSecret == entry2.UserSecret && + entry1.LocalUsername == entry2.LocalUsername && + entry1.Hostname == entry2.Hostname && + entry1.Command == entry2.Command && + entry1.CurrentWorkingDirectory == entry2.CurrentWorkingDirectory && + entry1.ExitCode == entry2.ExitCode && + entry1.StartTime.Format(time.RFC3339) == entry2.StartTime.Format(time.RFC3339) && + entry1.EndTime.Format(time.RFC3339) == entry2.EndTime.Format(time.RFC3339) + +}