2022-01-09 20:00:53 +01:00
package main
import (
"bytes"
2022-04-07 08:17:58 +02:00
"encoding/json"
2022-04-07 07:44:10 +02:00
"fmt"
2022-04-07 08:17:58 +02:00
"net/http"
2022-04-07 07:43:07 +02:00
"os"
2022-04-07 07:44:10 +02:00
"os/exec"
2022-04-16 09:56:08 +02:00
"path"
2022-01-09 23:34:59 +01:00
"regexp"
2022-04-12 07:36:52 +02:00
"runtime"
2022-09-17 20:49:31 +02:00
"strconv"
2022-01-09 23:34:59 +01:00
"strings"
2022-01-09 20:00:53 +01:00
"testing"
2022-04-15 05:18:49 +02:00
"time"
"github.com/google/go-cmp/cmp"
2022-09-17 20:49:31 +02:00
"github.com/google/uuid"
2022-01-09 20:00:53 +01:00
2022-04-08 05:59:40 +02:00
"github.com/ddworken/hishtory/client/data"
2022-09-21 07:28:40 +02:00
"github.com/ddworken/hishtory/client/hctx"
2022-05-02 04:37:26 +02:00
"github.com/ddworken/hishtory/client/lib"
2022-01-09 20:00:53 +01:00
"github.com/ddworken/hishtory/shared"
2022-10-28 06:53:47 +02:00
"github.com/ddworken/hishtory/shared/testutils"
2022-01-09 20:00:53 +01:00
)
2022-09-28 07:03:37 +02:00
func skipSlowTests ( ) bool {
return os . Getenv ( "FAST" ) != ""
}
2022-10-30 02:53:40 +02:00
var initialWd string
2022-04-15 20:20:23 +02:00
func TestMain ( m * testing . M ) {
2022-10-28 06:53:47 +02:00
defer testutils . RunTestServer ( ) ( )
2022-04-16 03:12:26 +02:00
cmd := exec . Command ( "go" , "build" , "-o" , "/tmp/client" )
2022-09-22 04:58:10 +02:00
cmd . Env = os . Environ ( )
cmd . Env = append ( cmd . Env , "CGO_ENABLED=0" )
2022-04-16 03:12:26 +02:00
err := cmd . Run ( )
if err != nil {
panic ( fmt . Sprintf ( "failed to build client: %v" , err ) )
}
2022-10-30 02:53:40 +02:00
cwd , err := os . Getwd ( )
if err != nil {
panic ( fmt . Sprintf ( "failed to os.Getwd(): %v" , err ) )
}
initialWd = cwd
2022-04-15 20:20:23 +02:00
m . Run ( )
}
2022-04-18 04:54:17 +02:00
type shellTester interface {
RunInteractiveShell ( t * testing . T , script string ) string
RunInteractiveShellRelaxed ( t * testing . T , script string ) ( string , error )
ShellName ( ) string
}
type bashTester struct {
shellTester
}
func ( b bashTester ) RunInteractiveShell ( t * testing . T , script string ) string {
out , err := b . RunInteractiveShellRelaxed ( t , "set -emo pipefail\n" + script )
2022-04-08 08:25:13 +02:00
if err != nil {
2022-04-12 07:36:52 +02:00
_ , filename , line , _ := runtime . Caller ( 1 )
t . Fatalf ( "error when running command at %s:%d: %v" , filename , line , err )
2022-04-08 08:25:13 +02:00
}
return out
}
2022-04-18 04:54:17 +02:00
func ( b bashTester ) RunInteractiveShellRelaxed ( t * testing . T , script string ) ( string , error ) {
2022-01-09 23:34:59 +01:00
cmd := exec . Command ( "bash" , "-i" )
cmd . Stdin = strings . NewReader ( script )
2022-04-08 08:25:13 +02:00
var stdout bytes . Buffer
cmd . Stdout = & stdout
var stderr bytes . Buffer
cmd . Stderr = & stderr
err := cmd . Run ( )
if err != nil {
return "" , fmt . Errorf ( "unexpected error when running commands, out=%#v, err=%#v: %v" , stdout . String ( ) , stderr . String ( ) , err )
}
outStr := stdout . String ( )
2022-04-15 05:18:49 +02:00
if strings . Contains ( outStr , "hishtory fatal error" ) {
2022-04-07 08:05:30 +02:00
t . Fatalf ( "Ran command, but hishtory had a fatal error! out=%#v" , outStr )
}
2022-04-08 08:25:13 +02:00
return outStr , nil
2022-01-09 23:34:59 +01:00
}
2022-04-18 04:54:17 +02:00
func ( b bashTester ) ShellName ( ) string {
return "bash"
}
type zshTester struct {
shellTester
}
func ( z zshTester ) RunInteractiveShell ( t * testing . T , script string ) string {
2022-06-05 08:11:26 +02:00
res , err := z . RunInteractiveShellRelaxed ( t , "set -eo pipefail\n" + script )
2022-04-18 04:54:17 +02:00
if err != nil {
t . Fatal ( err )
}
return res
}
func ( z zshTester ) RunInteractiveShellRelaxed ( t * testing . T , script string ) ( string , error ) {
cmd := exec . Command ( "zsh" , "-is" )
cmd . Stdin = strings . NewReader ( script )
var stdout bytes . Buffer
cmd . Stdout = & stdout
var stderr bytes . Buffer
cmd . Stderr = & stderr
err := cmd . Run ( )
if err != nil {
2022-09-18 06:56:39 +02:00
return stdout . String ( ) , fmt . Errorf ( "unexpected error when running command=%#v, out=%#v, err=%#v: %v" , script , stdout . String ( ) , stderr . String ( ) , err )
2022-04-18 04:54:17 +02:00
}
outStr := stdout . String ( )
if strings . Contains ( outStr , "hishtory fatal error" ) {
t . Fatalf ( "Ran command, but hishtory had a fatal error! out=%#v" , outStr )
}
return outStr , nil
}
func ( z zshTester ) ShellName ( ) string {
return "zsh"
}
var shellTesters [ ] shellTester = [ ] shellTester { bashTester { } , zshTester { } }
2022-11-03 21:16:45 +01:00
type OnlineStatus int64
const (
Online OnlineStatus = iota
Offline
)
2022-04-18 04:54:17 +02:00
func TestParameterized ( t * testing . T ) {
2022-09-28 07:03:37 +02:00
if skipSlowTests ( ) {
shellTesters = shellTesters [ : 1 ]
}
2022-04-18 04:54:17 +02:00
for _ , tester := range shellTesters {
t . Run ( "testRepeatedCommandThenQuery/" + tester . ShellName ( ) , func ( t * testing . T ) { testRepeatedCommandThenQuery ( t , tester ) } )
t . Run ( "testRepeatedCommandAndQuery/" + tester . ShellName ( ) , func ( t * testing . T ) { testRepeatedCommandAndQuery ( t , tester ) } )
t . Run ( "testRepeatedEnableDisable/" + tester . ShellName ( ) , func ( t * testing . T ) { testRepeatedEnableDisable ( t , tester ) } )
t . Run ( "testExcludeHiddenCommand/" + tester . ShellName ( ) , func ( t * testing . T ) { testExcludeHiddenCommand ( t , tester ) } )
t . Run ( "testUpdate/" + tester . ShellName ( ) , func ( t * testing . T ) { testUpdate ( t , tester ) } )
t . Run ( "testAdvancedQuery/" + tester . ShellName ( ) , func ( t * testing . T ) { testAdvancedQuery ( t , tester ) } )
2022-11-03 21:16:45 +01:00
t . Run ( "testIntegration/" + tester . ShellName ( ) , func ( t * testing . T ) { testIntegration ( t , tester , Online ) } )
t . Run ( "testIntegration/offline/" + tester . ShellName ( ) , func ( t * testing . T ) { testIntegration ( t , tester , Offline ) } )
2022-04-18 04:54:17 +02:00
t . Run ( "testIntegrationWithNewDevice/" + tester . ShellName ( ) , func ( t * testing . T ) { testIntegrationWithNewDevice ( t , tester ) } )
t . Run ( "testHishtoryBackgroundSaving/" + tester . ShellName ( ) , func ( t * testing . T ) { testHishtoryBackgroundSaving ( t , tester ) } )
t . Run ( "testDisplayTable/" + tester . ShellName ( ) , func ( t * testing . T ) { testDisplayTable ( t , tester ) } )
2022-04-19 08:07:39 +02:00
t . Run ( "testTableDisplayCwd/" + tester . ShellName ( ) , func ( t * testing . T ) { testTableDisplayCwd ( t , tester ) } )
2022-04-19 07:07:10 +02:00
t . Run ( "testTimestampsAreReasonablyCorrect/" + tester . ShellName ( ) , func ( t * testing . T ) { testTimestampsAreReasonablyCorrect ( t , tester ) } )
2022-05-02 04:37:26 +02:00
t . Run ( "testRequestAndReceiveDbDump/" + tester . ShellName ( ) , func ( t * testing . T ) { testRequestAndReceiveDbDump ( t , tester ) } )
2022-06-05 07:27:04 +02:00
t . Run ( "testInstallViaPythonScript/" + tester . ShellName ( ) , func ( t * testing . T ) { testInstallViaPythonScript ( t , tester ) } )
2022-06-06 03:05:06 +02:00
t . Run ( "testExportWithQuery/" + tester . ShellName ( ) , func ( t * testing . T ) { testExportWithQuery ( t , tester ) } )
2022-06-06 03:26:02 +02:00
t . Run ( "testHelpCommand/" + tester . ShellName ( ) , func ( t * testing . T ) { testHelpCommand ( t , tester ) } )
2022-06-13 06:28:19 +02:00
t . Run ( "testStripBashTimePrefix/" + tester . ShellName ( ) , func ( t * testing . T ) { testStripBashTimePrefix ( t , tester ) } )
2022-09-05 03:37:46 +02:00
t . Run ( "testReuploadHistoryEntries/" + tester . ShellName ( ) , func ( t * testing . T ) { testReuploadHistoryEntries ( t , tester ) } )
2022-09-22 05:09:49 +02:00
t . Run ( "testHishtoryOffline/" + tester . ShellName ( ) , func ( t * testing . T ) { testHishtoryOffline ( t , tester ) } )
2022-09-17 20:49:31 +02:00
t . Run ( "testInitialHistoryImport/" + tester . ShellName ( ) , func ( t * testing . T ) { testInitialHistoryImport ( t , tester ) } )
2022-11-03 21:16:45 +01:00
t . Run ( "testLocalRedaction/" + tester . ShellName ( ) , func ( t * testing . T ) { testLocalRedaction ( t , tester , Online ) } )
t . Run ( "testLocalRedaction/offline/" + tester . ShellName ( ) , func ( t * testing . T ) { testLocalRedaction ( t , tester , Offline ) } )
2022-09-20 07:49:48 +02:00
t . Run ( "testRemoteRedaction/" + tester . ShellName ( ) , func ( t * testing . T ) { testRemoteRedaction ( t , tester ) } )
2022-09-23 06:45:20 +02:00
t . Run ( "testMultipleUsers/" + tester . ShellName ( ) , func ( t * testing . T ) { testMultipleUsers ( t , tester ) } )
2022-10-16 21:43:16 +02:00
t . Run ( "testConfigGetSet/" + tester . ShellName ( ) , func ( t * testing . T ) { testConfigGetSet ( t , tester ) } )
2022-11-03 21:16:45 +01:00
t . Run ( "testControlR/" + tester . ShellName ( ) , func ( t * testing . T ) { testControlR ( t , tester , tester . ShellName ( ) , Online ) } )
t . Run ( "testControlR/offline/" + tester . ShellName ( ) , func ( t * testing . T ) { testControlR ( t , tester , tester . ShellName ( ) , Offline ) } )
2022-10-24 04:29:29 +02:00
t . Run ( "testHandleUpgradedFeatures/" + tester . ShellName ( ) , func ( t * testing . T ) { testHandleUpgradedFeatures ( t , tester ) } )
2022-10-31 01:55:48 +01:00
t . Run ( "testCustomColumns/" + tester . ShellName ( ) , func ( t * testing . T ) { testCustomColumns ( t , tester ) } )
2022-11-03 03:41:49 +01:00
t . Run ( "testUninstall/" + tester . ShellName ( ) , func ( t * testing . T ) { testUninstall ( t , tester ) } )
2022-04-18 04:54:17 +02:00
}
2022-11-03 21:16:45 +01:00
t . Run ( "testControlR/fish" , func ( t * testing . T ) { testControlR ( t , bashTester { } , "fish" , Online ) } )
2022-04-18 04:54:17 +02:00
}
2022-11-03 21:16:45 +01:00
func testIntegration ( t * testing . T , tester shellTester , onlineStatus OnlineStatus ) {
2022-01-09 20:00:53 +01:00
// Set up
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-03-30 06:56:28 +02:00
2022-04-07 03:18:46 +02:00
// Run the test
2022-11-03 21:16:45 +01:00
testBasicUserFlow ( t , tester , onlineStatus )
2022-04-06 08:31:24 +02:00
}
2022-01-09 20:00:53 +01:00
2022-04-18 04:54:17 +02:00
func testIntegrationWithNewDevice ( t * testing . T , tester shellTester ) {
2022-04-06 08:31:24 +02:00
// Set up
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-04-06 08:31:24 +02:00
2022-04-07 03:18:46 +02:00
// Run the test
2022-11-03 21:16:45 +01:00
userSecret := testBasicUserFlow ( t , tester , Online )
2022-04-06 08:31:24 +02:00
// Clear all local state
2022-10-28 06:53:47 +02:00
testutils . ResetLocalState ( t )
2022-04-06 08:31:24 +02:00
// Install it again
2022-04-18 04:54:17 +02:00
installHishtory ( t , tester , userSecret )
2022-01-09 23:34:59 +01:00
2022-04-07 03:18:46 +02:00
// Querying should show the history from the previous run
2022-11-03 21:16:45 +01:00
out := tester . RunInteractiveShell ( t , ` hishtory query ` )
2022-04-06 08:31:24 +02:00
expected := [ ] string { "echo thisisrecorded" , "hishtory enable" , "echo bar" , "echo foo" , "ls /foo" , "ls /bar" , "ls /a" }
for _ , item := range expected {
if ! strings . Contains ( out , item ) {
t . Fatalf ( "output is missing expected item %#v: %#v" , item , out )
}
if strings . Count ( out , item ) != 1 {
t . Fatalf ( "output has %#v in it multiple times! out=%#v" , item , out )
}
}
2022-04-18 04:54:17 +02:00
tester . RunInteractiveShell ( t , "echo mynewcommand" )
out = hishtoryQuery ( t , tester , "" )
2022-04-06 08:31:24 +02:00
if ! strings . Contains ( out , "echo mynewcommand" ) {
t . Fatalf ( "output is missing `echo mynewcommand`" )
}
if strings . Count ( out , "echo mynewcommand" ) != 1 {
t . Fatalf ( "output has `echo mynewcommand` the wrong number of times" )
}
2022-04-07 03:18:46 +02:00
// Clear local state again
2022-10-28 06:53:47 +02:00
testutils . ResetLocalState ( t )
2022-04-07 02:47:21 +02:00
// Install it a 3rd time
2022-04-18 04:54:17 +02:00
installHishtory ( t , tester , "adifferentsecret" )
2022-04-07 02:47:21 +02:00
2022-04-07 07:43:07 +02:00
// Run a command that shouldn't be in the hishtory later on
2022-04-18 04:54:17 +02:00
tester . RunInteractiveShell ( t , ` echo notinthehistory ` )
out = hishtoryQuery ( t , tester , "" )
2022-04-07 07:43:07 +02:00
if ! strings . Contains ( out , "echo notinthehistory" ) {
t . Fatalf ( "output is missing `echo notinthehistory`" )
}
2022-04-07 03:18:46 +02:00
// Set the secret key to the previous secret key
2022-10-01 18:50:06 +02:00
out , err := tester . RunInteractiveShellRelaxed ( t , ` yes | hishtory init ` + userSecret )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-04-07 03:18:46 +02:00
if ! strings . Contains ( out , "Setting secret hishtory key to " + userSecret ) {
2022-04-07 02:47:21 +02:00
t . Fatalf ( "Failed to re-init with the user secret: %v" , out )
}
2022-04-07 03:18:46 +02:00
// Querying should show the history from the previous run
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , "" )
2022-04-07 02:47:21 +02:00
expected = [ ] string { "echo thisisrecorded" , "echo mynewcommand" , "hishtory enable" , "echo bar" , "echo foo" , "ls /foo" , "ls /bar" , "ls /a" }
for _ , item := range expected {
if ! strings . Contains ( out , item ) {
t . Fatalf ( "output is missing expected item %#v: %#v" , item , out )
}
if strings . Count ( out , item ) != 1 {
t . Fatalf ( "output has %#v in it multiple times! out=%#v" , item , out )
}
}
2022-04-07 07:43:07 +02:00
// But not from the previous account
if strings . Contains ( out , "notinthehistory" ) {
t . Fatalf ( "output contains the unexpected item: notinthehistory" )
}
2022-04-07 02:47:21 +02:00
2022-04-18 04:54:17 +02:00
tester . RunInteractiveShell ( t , "echo mynewercommand" )
out = hishtoryQuery ( t , tester , "" )
2022-04-07 02:47:21 +02:00
if ! strings . Contains ( out , "echo mynewercommand" ) {
t . Fatalf ( "output is missing `echo mynewercommand`" )
}
if strings . Count ( out , "echo mynewercommand" ) != 1 {
t . Fatalf ( "output has `echo mynewercommand` the wrong number of times" )
}
2022-04-06 08:31:24 +02:00
2022-04-07 07:49:45 +02:00
// Manually submit an event that isn't in the local DB, and then we'll
2022-04-07 08:17:58 +02:00
// check if we see it when we do a query without ever having done an init
2022-10-28 06:53:47 +02:00
newEntry := testutils . MakeFakeHistoryEntry ( "othercomputer" )
2022-10-28 06:20:33 +02:00
newEntry . StartTime = time . Now ( )
newEntry . EndTime = time . Now ( )
2022-04-11 02:38:20 +02:00
manuallySubmitHistoryEntry ( t , userSecret , newEntry )
2022-04-07 08:17:58 +02:00
// Now check if that is in there when we do hishtory query
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , "" )
2022-04-07 07:49:45 +02:00
if ! strings . Contains ( out , "othercomputer" ) {
t . Fatalf ( "hishtory query doesn't contain cmd run on another machine! out=%#v" , out )
}
2022-04-07 08:05:30 +02:00
2022-10-09 21:13:05 +02:00
// Run a reupload just to test that flow
tester . RunInteractiveShell ( t , "hishtory reupload" )
2022-04-07 08:05:30 +02:00
// Finally, test the export command
2022-04-18 04:54:17 +02:00
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail | grep -v '/tmp/client install' ` )
2022-04-15 05:18:49 +02:00
if strings . Contains ( out , "thisisnotrecorded" ) {
t . Fatalf ( "hishtory export contains a command that should not have been recorded, out=%#v" , out )
}
2022-11-05 05:05:44 +01:00
expectedOutputWithoutKey := "hishtory status\nhishtory query\nls /a\nls /bar\nls /foo\necho foo\necho bar\nhishtory enable\necho thisisrecorded\nhishtory query\nhishtory query foo\necho hello | grep complex | sed s/h/i/g; echo baz && echo \"fo 'o\" # mycommand\nhishtory query complex\nhishtory query\necho mynewcommand\nhishtory query\nyes | hishtory init %s\nhishtory query\necho mynewercommand\nhishtory query\nothercomputer\nhishtory query\nhishtory reupload\n"
2022-04-15 05:18:49 +02:00
expectedOutput := fmt . Sprintf ( expectedOutputWithoutKey , userSecret )
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
2022-04-16 07:54:38 +02:00
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
2022-04-07 08:05:30 +02:00
}
2022-11-01 18:45:16 +01:00
// And test the export for each shell without anything filtered out
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v 'hishtory init ' ` )
compareGoldens ( t , out , "testIntegrationWithNewDevice-" + tester . ShellName ( ) )
// And test the table but with a subset of columns that is static
tester . RunInteractiveShell ( t , ` hishtory config-set displayed-columns Hostname 'Exit Code' Command ` )
out = tester . RunInteractiveShell ( t , ` hishtory query | grep -v 'hishtory init ' ` )
compareGoldens ( t , out , "testIntegrationWithNewDevice-table" + tester . ShellName ( ) )
2022-04-06 08:31:24 +02:00
}
2022-04-18 04:54:17 +02:00
func installHishtory ( t * testing . T , tester shellTester , userSecret string ) string {
out := tester . RunInteractiveShell ( t , ` /tmp/client install ` + userSecret )
2022-04-06 08:31:24 +02:00
r := regexp . MustCompile ( ` Setting secret hishtory key to (.*) ` )
matches := r . FindStringSubmatch ( out )
if len ( matches ) != 2 {
2022-09-21 08:30:57 +02:00
t . Fatalf ( "Failed to extract userSecret from output=%#v: matches=%#v" , out , matches )
2022-04-06 08:31:24 +02:00
}
2022-04-08 07:53:39 +02:00
return matches [ 1 ]
}
2022-11-03 21:16:45 +01:00
func initFromOnlineStatus ( t * testing . T , tester shellTester , onlineStatus OnlineStatus ) string {
if onlineStatus == Online {
return installHishtory ( t , tester , "" )
} else {
return installHishtory ( t , tester , "--offline" )
}
}
func assertOnlineStatus ( t * testing . T , onlineStatus OnlineStatus ) {
config := hctx . GetConf ( hctx . MakeContext ( ) )
if onlineStatus == Online && config . IsOffline == true {
t . Fatalf ( "We're supposed to be online, yet config.IsOffline=%#v (config=%#v)" , config . IsOffline , config )
}
if onlineStatus == Offline && config . IsOffline == false {
t . Fatalf ( "We're supposed to be offline, yet config.IsOffline=%#v (config=%#v)" , config . IsOffline , config )
}
}
func testBasicUserFlow ( t * testing . T , tester shellTester , onlineStatus OnlineStatus ) string {
2022-04-08 07:53:39 +02:00
// Test install
2022-11-03 21:16:45 +01:00
userSecret := initFromOnlineStatus ( t , tester , onlineStatus )
assertOnlineStatus ( t , onlineStatus )
2022-04-06 08:31:24 +02:00
2022-04-07 07:43:07 +02:00
// Test the status subcommand
2022-04-18 04:54:17 +02:00
out := tester . RunInteractiveShell ( t , ` hishtory status ` )
2022-09-27 07:55:59 +02:00
if out != fmt . Sprintf ( "hiSHtory: v0.Unknown\nEnabled: true\nSecret Key: %s\nCommit Hash: Unknown\n" , userSecret ) {
2022-04-07 02:47:21 +02:00
t . Fatalf ( "status command has unexpected output: %#v" , out )
}
2022-04-16 09:56:08 +02:00
// Assert that hishtory is correctly using the dev config.sh
homedir , err := os . UserHomeDir ( )
if err != nil {
t . Fatalf ( "failed to get homedir: %v" , err )
}
2022-10-28 06:53:47 +02:00
dat , err := os . ReadFile ( path . Join ( homedir , data . HISHTORY_PATH , "config.sh" ) )
2022-04-16 09:56:08 +02:00
if err != nil {
t . Fatalf ( "failed to read config.sh: %v" , err )
}
2022-10-24 02:35:02 +02:00
if strings . Contains ( string ( dat ) , "# Background Run" ) {
t . Fatalf ( "config.sh is the prod version when it shouldn't be, config.sh=%#v" , string ( dat ) )
2022-04-16 09:56:08 +02:00
}
2022-04-07 07:44:10 +02:00
// Test the banner
2022-11-03 21:16:45 +01:00
if onlineStatus == Online {
os . Setenv ( "FORCED_BANNER" , "HELLO_FROM_SERVER" )
defer os . Setenv ( "FORCED_BANNER" , "" )
out = hishtoryQuery ( t , tester , "" )
if ! strings . Contains ( out , "HELLO_FROM_SERVER\nHostname" ) {
t . Fatalf ( "hishtory query didn't show the banner message! out=%#v" , out )
}
os . Setenv ( "FORCED_BANNER" , "" )
2022-04-07 07:43:07 +02:00
}
2022-01-09 23:34:59 +01:00
// Test recording commands
2022-04-20 06:05:54 +02:00
out , err = tester . RunInteractiveShellRelaxed ( t , ` ls / a
2022-04-14 06:48:52 +02:00
ls / bar
ls / foo
echo foo
echo bar
hishtory disable
echo thisisnotrecorded
2022-04-15 05:18:49 +02:00
sleep 0.5
2022-04-14 06:48:52 +02:00
hishtory enable
echo thisisrecorded ` )
2022-04-08 08:25:13 +02:00
if err != nil {
t . Fatal ( err )
}
2022-01-09 23:34:59 +01:00
if out != "foo\nbar\nthisisnotrecorded\nthisisrecorded\n" {
t . Fatalf ( "unexpected output from running commands: %#v" , out )
}
// Test querying for all commands
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , "" )
2022-01-10 00:48:20 +01:00
expected := [ ] string { "echo thisisrecorded" , "hishtory enable" , "echo bar" , "echo foo" , "ls /foo" , "ls /bar" , "ls /a" }
for _ , item := range expected {
if ! strings . Contains ( out , item ) {
t . Fatalf ( "output is missing expected item %#v: %#v" , item , out )
}
}
2022-09-17 21:19:29 +02:00
// Test the actual table output
hostnameMatcher := ` \S+ `
tableDividerMatcher := ` \s+ `
2022-11-05 07:14:34 +01:00
pathMatcher := ` ~?/[a-zA-Z_0-9/-]+ `
2022-10-01 08:56:04 +02:00
datetimeMatcher := ` [a-zA-Z] { 3}\s\d { 1,2}\s\d { 4}\s[0-9:]+\s([A-Z] { 3}|[+-]\d { 4}) `
2022-09-17 21:37:02 +02:00
runtimeMatcher := ` [0-9.ms]+ `
2022-09-17 21:19:29 +02:00
exitCodeMatcher := ` 0 `
2022-09-17 21:49:27 +02:00
pipefailMatcher := ` set -em?o pipefail `
2022-09-21 08:46:17 +02:00
line1Matcher := ` Hostname ` + tableDividerMatcher + ` CWD ` + tableDividerMatcher + ` Timestamp ` + tableDividerMatcher + ` Runtime ` + tableDividerMatcher + ` Exit Code ` + tableDividerMatcher + ` Command\s*\n `
line2Matcher := hostnameMatcher + tableDividerMatcher + pathMatcher + tableDividerMatcher + datetimeMatcher + tableDividerMatcher + runtimeMatcher + tableDividerMatcher + exitCodeMatcher + tableDividerMatcher + pipefailMatcher + tableDividerMatcher + ` \n `
line3Matcher := hostnameMatcher + tableDividerMatcher + pathMatcher + tableDividerMatcher + datetimeMatcher + tableDividerMatcher + runtimeMatcher + tableDividerMatcher + exitCodeMatcher + tableDividerMatcher + ` echo thisisrecorded ` + tableDividerMatcher + ` \n `
2022-09-17 21:49:27 +02:00
match , err := regexp . MatchString ( line3Matcher , out )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-09-17 21:19:29 +02:00
if ! match {
t . Fatalf ( "output is missing the row for `echo thisisrecorded`: %v" , out )
}
2022-09-21 08:46:17 +02:00
match , err = regexp . MatchString ( line1Matcher , out )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-09-21 08:46:17 +02:00
if ! match {
t . Fatalf ( "output is missing the headings: %v" , out )
}
match , err = regexp . MatchString ( line2Matcher , out )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-09-21 08:46:17 +02:00
if ! match {
t . Fatalf ( "output is missing the pipefail: %v" , out )
}
2022-09-17 21:49:27 +02:00
match , err = regexp . MatchString ( line1Matcher + line2Matcher + line3Matcher , out )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-09-17 21:49:27 +02:00
if ! match {
t . Fatalf ( "output doesn't match the expected table: %v" , out )
}
2022-01-10 00:48:20 +01:00
// Test querying for a specific command
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , "foo" )
2022-01-10 00:48:20 +01:00
expected = [ ] string { "echo foo" , "ls /foo" }
unexpected := [ ] string { "echo thisisrecorded" , "hishtory enable" , "echo bar" , "ls /bar" , "ls /a" }
2022-01-09 23:34:59 +01:00
for _ , item := range expected {
if ! strings . Contains ( out , item ) {
t . Fatalf ( "output is missing expected item %#v: %#v" , item , out )
}
2022-04-06 08:31:24 +02:00
if strings . Count ( out , item ) != 1 {
t . Fatalf ( "output has %#v in it multiple times! out=%#v" , item , out )
}
2022-01-09 20:00:53 +01:00
}
2022-01-10 00:48:20 +01:00
for _ , item := range unexpected {
if strings . Contains ( out , item ) {
t . Fatalf ( "output is containing unexpected item %#v: %#v" , item , out )
}
}
2022-04-06 08:31:24 +02:00
2022-04-16 03:24:58 +02:00
// Add a complex command
2022-11-05 04:34:36 +01:00
complexCommand := "echo hello | grep complex | sed s/h/i/g; echo baz && echo \"fo 'o\" # mycommand"
2022-04-18 04:54:17 +02:00
_ , _ = tester . RunInteractiveShellRelaxed ( t , complexCommand )
2022-04-16 03:24:58 +02:00
// Query for it
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , "complex" )
2022-04-16 03:24:58 +02:00
if strings . Count ( out , "\n" ) != 2 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
if ! strings . Contains ( out , complexCommand ) {
t . Fatalf ( "hishtory query doesn't contain the expected complex command, out=%#v" , out )
}
2022-04-06 08:31:24 +02:00
return userSecret
2022-01-09 20:00:53 +01:00
}
2022-04-08 07:53:39 +02:00
2022-04-18 04:54:17 +02:00
func testAdvancedQuery ( t * testing . T , tester shellTester ) {
2022-04-08 07:53:39 +02:00
// Set up
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-04-08 07:53:39 +02:00
// Install hishtory
2022-04-18 04:54:17 +02:00
userSecret := installHishtory ( t , tester , "" )
2022-04-08 07:53:39 +02:00
// Run some commands we can query for
2022-04-18 08:31:08 +02:00
_ , err := tester . RunInteractiveShellRelaxed ( t , ` echo nevershouldappear
2022-04-15 05:18:49 +02:00
notacommand
cd / tmp /
echo querybydir
hishtory disable ` )
2022-04-12 08:48:51 +02:00
if err != nil {
t . Fatal ( err )
}
2022-04-08 07:53:39 +02:00
2022-04-13 06:24:25 +02:00
// A super basic query just to ensure the basics are working
2022-04-18 04:54:17 +02:00
out := hishtoryQuery ( t , tester , ` echo ` )
2022-04-13 06:24:25 +02:00
if ! strings . Contains ( out , "echo querybydir" ) {
2022-04-15 05:18:49 +02:00
t . Fatalf ( "hishtory query doesn't contain result matching echo querybydir, out=%#v" , out )
2022-04-13 06:24:25 +02:00
}
if strings . Count ( out , "\n" ) != 3 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
2022-04-08 07:53:39 +02:00
// Query based on cwd
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` cwd:/tmp ` )
2022-04-08 07:53:39 +02:00
if ! strings . Contains ( out , "echo querybydir" ) {
t . Fatalf ( "hishtory query doesn't contain result matching cwd:/tmp, out=%#v" , out )
}
if strings . Contains ( out , "nevershouldappear" ) {
t . Fatalf ( "hishtory query contains unexpected entry, out=%#v" , out )
}
if strings . Count ( out , "\n" ) != 3 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
2022-06-06 03:05:06 +02:00
// And again, but with a strailing slash
out = hishtoryQuery ( t , tester , ` cwd:/tmp/ ` )
if ! strings . Contains ( out , "echo querybydir" ) {
t . Fatalf ( "hishtory query doesn't contain result matching cwd:/tmp, out=%#v" , out )
}
if strings . Contains ( out , "nevershouldappear" ) {
t . Fatalf ( "hishtory query contains unexpected entry, out=%#v" , out )
}
if strings . Count ( out , "\n" ) != 3 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
2022-04-08 07:53:39 +02:00
2022-04-09 03:23:17 +02:00
// Query based on cwd without the slash
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` cwd:tmp ` )
2022-04-09 03:23:17 +02:00
if ! strings . Contains ( out , "echo querybydir" ) {
t . Fatalf ( "hishtory query doesn't contain result matching cwd:tmp, out=%#v" , out )
}
if strings . Count ( out , "\n" ) != 3 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
2022-04-08 07:53:39 +02:00
// Query based on cwd and another term
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` cwd:/tmp querybydir ` )
2022-04-08 07:53:39 +02:00
if ! strings . Contains ( out , "echo querybydir" ) {
t . Fatalf ( "hishtory query doesn't contain result matching cwd:/tmp, out=%#v" , out )
}
if strings . Contains ( out , "nevershouldappear" ) {
t . Fatalf ( "hishtory query contains unexpected entry, out=%#v" , out )
}
if strings . Count ( out , "\n" ) != 2 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
2022-04-08 08:25:13 +02:00
// Query based on exit_code
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` exit_code:127 ` )
2022-04-08 08:25:13 +02:00
if ! strings . Contains ( out , "notacommand" ) {
t . Fatalf ( "hishtory query doesn't contain expected result, out=%#v" , out )
}
if strings . Count ( out , "\n" ) != 2 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
2022-04-08 08:30:31 +02:00
// Query based on exit_code and something else that matches nothing
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` exit_code:127 foo ` )
2022-04-08 08:30:31 +02:00
if strings . Count ( out , "\n" ) != 1 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
2022-04-09 03:23:17 +02:00
// Query based on before: and cwd:
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` before:2125-07-02 cwd:/tmp ` )
2022-04-09 03:23:17 +02:00
if strings . Count ( out , "\n" ) != 3 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` before:2125-07-02 cwd:tmp ` )
2022-04-09 03:23:17 +02:00
if strings . Count ( out , "\n" ) != 3 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` before:2125-07-02 cwd:mp ` )
2022-04-09 03:23:17 +02:00
if strings . Count ( out , "\n" ) != 3 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
// Query based on after: and cwd:
2022-04-19 08:07:39 +02:00
out = hishtoryQuery ( t , tester , ` after:1980-07-02 cwd:/tmp ` )
if strings . Count ( out , "\n" ) != 3 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
2022-04-09 03:23:17 +02:00
}
// Query based on after: that returns no results
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` after:2120-07-02 cwd:/tmp ` )
2022-04-09 03:23:17 +02:00
if strings . Count ( out , "\n" ) != 1 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
2022-04-11 02:38:20 +02:00
// Manually submit an entry with a different hostname and username so we can test those atoms
2022-10-28 06:53:47 +02:00
entry := testutils . MakeFakeHistoryEntry ( "cmd_with_diff_hostname_and_username" )
2022-04-11 02:38:20 +02:00
entry . LocalUsername = "otheruser"
entry . Hostname = "otherhostname"
manuallySubmitHistoryEntry ( t , userSecret , entry )
// Query based on the username that exists
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` user:otheruser ` )
2022-04-11 02:38:20 +02:00
if ! strings . Contains ( out , "cmd_with_diff_hostname_and_username" ) {
t . Fatalf ( "hishtory query doesn't contain expected result, out=%#v" , out )
}
if strings . Count ( out , "\n" ) != 2 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
// Query based on the username that doesn't exist
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` user:noexist ` )
2022-04-11 02:38:20 +02:00
if strings . Count ( out , "\n" ) != 1 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
// Query based on the hostname
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` hostname:otherhostname ` )
2022-04-11 02:38:20 +02:00
if ! strings . Contains ( out , "cmd_with_diff_hostname_and_username" ) {
t . Fatalf ( "hishtory query doesn't contain expected result, out=%#v" , out )
}
if strings . Count ( out , "\n" ) != 2 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
2022-04-12 07:36:52 +02:00
// Test filtering out a search item
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , "" )
2022-04-12 07:36:52 +02:00
if ! strings . Contains ( out , "cmd_with_diff_hostname_and_username" ) {
t . Fatalf ( "hishtory query doesn't contain expected result, out=%#v" , out )
}
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` -cmd_with_diff_hostname_and_username ` )
2022-04-12 07:36:52 +02:00
if strings . Contains ( out , "cmd_with_diff_hostname_and_username" ) {
t . Fatalf ( "hishtory query contains unexpected result, out=%#v" , out )
}
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` -echo ` )
2022-04-12 07:36:52 +02:00
if strings . Contains ( out , "echo" ) {
t . Fatalf ( "hishtory query contains unexpected result, out=%#v" , out )
}
2022-04-18 08:31:08 +02:00
if strings . Count ( out , "\n" ) != 4 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
2022-04-12 07:36:52 +02:00
}
// Test filtering out with an atom
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` -hostname:otherhostname ` )
2022-04-12 07:36:52 +02:00
if strings . Contains ( out , "cmd_with_diff_hostname_and_username" ) {
t . Fatalf ( "hishtory query contains unexpected result, out=%#v" , out )
}
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` -user:otheruser ` )
2022-04-12 07:36:52 +02:00
if strings . Contains ( out , "cmd_with_diff_hostname_and_username" ) {
t . Fatalf ( "hishtory query contains unexpected result, out=%#v" , out )
}
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` -exit_code:0 ` )
2022-04-12 07:36:52 +02:00
if strings . Count ( out , "\n" ) != 3 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
// Test filtering out a search item that also looks like it could be a search for a flag
2022-10-28 06:53:47 +02:00
entry = testutils . MakeFakeHistoryEntry ( "foo -echo" )
2022-04-12 07:36:52 +02:00
manuallySubmitHistoryEntry ( t , userSecret , entry )
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` -echo -install ` )
2022-04-12 07:36:52 +02:00
if strings . Contains ( out , "echo" ) {
t . Fatalf ( "hishtory query contains unexpected result, out=%#v" , out )
}
2022-04-18 08:31:08 +02:00
if strings . Count ( out , "\n" ) != 4 {
2022-04-12 07:36:52 +02:00
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
2022-09-08 08:20:31 +02:00
// Search for a cwd based on the home directory
2022-10-28 06:53:47 +02:00
entry = testutils . MakeFakeHistoryEntry ( "foobar" )
2022-09-08 08:20:31 +02:00
entry . HomeDirectory = "/home/david/"
entry . CurrentWorkingDirectory = "~/dir/"
manuallySubmitHistoryEntry ( t , userSecret , entry )
out = tester . RunInteractiveShell ( t , ` hishtory export cwd:~/dir ` )
expectedOutput := "foobar\n"
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
// And search with the fully expanded path
out = tester . RunInteractiveShell ( t , ` hishtory export cwd:/home/david/dir ` )
expectedOutput = "foobar\n"
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
2022-04-08 07:53:39 +02:00
}
2022-04-09 08:47:13 +02:00
2022-04-18 04:54:17 +02:00
func testUpdate ( t * testing . T , tester shellTester ) {
2022-10-28 06:53:47 +02:00
if ! testutils . IsOnline ( ) {
2022-04-28 20:26:55 +02:00
t . Skip ( "skipping because we're currently offline" )
}
2022-04-21 05:56:01 +02:00
if runtime . GOOS == "linux" && runtime . GOARCH == "arm64" {
t . Skip ( "skipping on linux/arm64 which is unsupported" )
}
2022-09-28 07:03:37 +02:00
if skipSlowTests ( ) {
t . Skip ( "skipping slow tests" )
}
2022-04-09 21:50:01 +02:00
// Set up
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-04-18 04:54:17 +02:00
userSecret := installHishtory ( t , tester , "" )
2022-04-09 21:50:01 +02:00
2022-04-17 08:00:04 +02:00
// Record a command before the update
2022-04-18 04:54:17 +02:00
tester . RunInteractiveShell ( t , "echo hello" )
2022-04-17 08:00:04 +02:00
2022-04-09 21:50:01 +02:00
// Check the status command
2022-04-18 04:54:17 +02:00
out := tester . RunInteractiveShell ( t , ` hishtory status ` )
2022-09-27 07:55:59 +02:00
if out != fmt . Sprintf ( "hiSHtory: v0.Unknown\nEnabled: true\nSecret Key: %s\nCommit Hash: Unknown\n" , userSecret ) {
2022-04-09 21:50:01 +02:00
t . Fatalf ( "status command has unexpected output: %#v" , out )
}
// Update
2022-04-18 04:54:17 +02:00
out = tester . RunInteractiveShell ( t , ` hishtory update ` )
2022-06-05 06:42:40 +02:00
isExpected , err := regexp . MatchString ( ` Successfully updated hishtory from v0[.]Unknown to v0.\d+ ` , out )
2022-04-17 08:00:04 +02:00
if err != nil {
t . Fatalf ( "regex failure: %v" , err )
}
if ! isExpected {
t . Fatalf ( "hishtory update returned unexpected out=%#v" , out )
}
2022-11-01 19:03:49 +01:00
if strings . Contains ( out , "skipping SLSA validation" ) {
t . Fatalf ( "SLSA validation was skipped, out=%#v" , out )
}
2022-04-17 08:00:04 +02:00
// Update again and assert that it skipped the update
2022-04-18 04:54:17 +02:00
out = tester . RunInteractiveShell ( t , ` hishtory update ` )
2022-04-17 08:00:04 +02:00
if strings . Count ( out , "\n" ) != 1 || ! strings . Contains ( out , "is already installed" ) {
t . Fatalf ( "repeated hishtory update didn't skip the update, out=%#v" , out )
}
2022-04-09 21:50:01 +02:00
// Then check the status command again to confirm the update worked
2022-04-18 04:54:17 +02:00
out = tester . RunInteractiveShell ( t , ` hishtory status ` )
2022-04-09 23:48:17 +02:00
if ! strings . Contains ( out , fmt . Sprintf ( "\nEnabled: true\nSecret Key: %s\nCommit Hash: " , userSecret ) ) {
2022-04-09 21:50:01 +02:00
t . Fatalf ( "status command has unexpected output: %#v" , out )
}
if strings . Contains ( out , "\nCommit Hash: Unknown\n" ) {
t . Fatalf ( "status command has unexpected output: %#v" , out )
}
2022-04-17 08:00:04 +02:00
// Check that the history was preserved after the update
2022-04-19 08:07:39 +02:00
out = tester . RunInteractiveShell ( t , "hishtory export | grep -v pipefail | grep -v '/tmp/client install'" )
expectedOutput := "echo hello\nhishtory status\nhishtory update\nhishtory update\nhishtory status\n"
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
2022-04-17 08:00:04 +02:00
}
2022-11-01 19:03:49 +01:00
// TODO: write a test that updates from v.prev to latest rather than v.Unknown to latest
2022-04-09 21:50:01 +02:00
}
2022-04-18 04:54:17 +02:00
func testRepeatedCommandThenQuery ( t * testing . T , tester shellTester ) {
2022-04-15 05:18:49 +02:00
// Set up
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-04-18 04:54:17 +02:00
userSecret := installHishtory ( t , tester , "" )
2022-04-15 05:18:49 +02:00
// Check the status command
2022-04-18 04:54:17 +02:00
out := tester . RunInteractiveShell ( t , ` hishtory status ` )
2022-09-27 07:55:59 +02:00
if out != fmt . Sprintf ( "hiSHtory: v0.Unknown\nEnabled: true\nSecret Key: %s\nCommit Hash: Unknown\n" , userSecret ) {
2022-04-15 05:18:49 +02:00
t . Fatalf ( "status command has unexpected output: %#v" , out )
}
// Run a command many times
for i := 0 ; i < 25 ; i ++ {
2022-04-18 04:54:17 +02:00
tester . RunInteractiveShell ( t , fmt . Sprintf ( "echo mycommand-%d" , i ) )
2022-04-15 05:18:49 +02:00
}
// Check that it shows up correctly
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , ` mycommand ` )
2022-04-15 05:18:49 +02:00
if strings . Count ( out , "\n" ) != 26 {
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v" , strings . Count ( out , "\n" ) , out )
}
if strings . Count ( out , "echo mycommand" ) != 25 {
t . Fatalf ( "hishtory query has the wrong number of commands=%d, out=%#v" , strings . Count ( out , "echo mycommand" ) , out )
}
2022-10-24 00:40:30 +02:00
// Run a few more commands including some empty lines that don't get recorded
2022-04-18 04:54:17 +02:00
tester . RunInteractiveShell ( t , ` echo mycommand - 30
2022-10-24 00:40:30 +02:00
2022-04-15 05:18:49 +02:00
echo mycommand - 31
echo mycommand - 3 ` )
2022-04-18 04:54:17 +02:00
out = tester . RunInteractiveShell ( t , "hishtory export | grep -v pipefail | grep -v '/tmp/client install'" )
expectedOutput := "hishtory status\necho mycommand-0\necho mycommand-1\necho mycommand-2\necho mycommand-3\necho mycommand-4\necho mycommand-5\necho mycommand-6\necho mycommand-7\necho mycommand-8\necho mycommand-9\necho mycommand-10\necho mycommand-11\necho mycommand-12\necho mycommand-13\necho mycommand-14\necho mycommand-15\necho mycommand-16\necho mycommand-17\necho mycommand-18\necho mycommand-19\necho mycommand-20\necho mycommand-21\necho mycommand-22\necho mycommand-23\necho mycommand-24\nhishtory query mycommand\necho mycommand-30\necho mycommand-31\necho mycommand-3\n"
2022-04-15 05:18:49 +02:00
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
}
2022-04-18 04:54:17 +02:00
func testRepeatedCommandAndQuery ( t * testing . T , tester shellTester ) {
2022-04-15 05:18:49 +02:00
// Set up
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-04-18 04:54:17 +02:00
userSecret := installHishtory ( t , tester , "" )
2022-04-15 05:18:49 +02:00
// Check the status command
2022-04-18 04:54:17 +02:00
out := tester . RunInteractiveShell ( t , ` hishtory status ` )
2022-09-27 07:55:59 +02:00
if out != fmt . Sprintf ( "hiSHtory: v0.Unknown\nEnabled: true\nSecret Key: %s\nCommit Hash: Unknown\n" , userSecret ) {
2022-04-15 05:18:49 +02:00
t . Fatalf ( "status command has unexpected output: %#v" , out )
}
// Run a command many times
for i := 0 ; i < 25 ; i ++ {
2022-04-18 04:54:17 +02:00
tester . RunInteractiveShell ( t , fmt . Sprintf ( "echo mycommand-%d" , i ) )
out = hishtoryQuery ( t , tester , fmt . Sprintf ( "mycommand-%d" , i ) )
2022-04-15 05:18:49 +02:00
if strings . Count ( out , "\n" ) != 2 {
t . Fatalf ( "hishtory query #%d has the wrong number of lines=%d, out=%#v" , i , strings . Count ( out , "\n" ) , out )
}
if strings . Count ( out , "echo mycommand" ) != 1 {
t . Fatalf ( "hishtory query #%d has the wrong number of commands=%d, out=%#v" , i , strings . Count ( out , "echo mycommand" ) , out )
}
}
}
2022-04-18 04:54:17 +02:00
func testRepeatedEnableDisable ( t * testing . T , tester shellTester ) {
2022-04-15 05:18:49 +02:00
// Set up
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-04-18 04:54:17 +02:00
installHishtory ( t , tester , "" )
2022-04-15 05:18:49 +02:00
// Run a command many times
for i := 0 ; i < 25 ; i ++ {
2022-04-18 04:54:17 +02:00
tester . RunInteractiveShell ( t , fmt . Sprintf ( ` echo mycommand - % d
2022-04-15 05:18:49 +02:00
hishtory disable
echo shouldnotshowup
sleep 0.5
hishtory enable ` , i ) )
2022-04-18 04:54:17 +02:00
out := hishtoryQuery ( t , tester , fmt . Sprintf ( "mycommand-%d" , i ) )
2022-04-15 05:18:49 +02:00
if strings . Count ( out , "\n" ) != 2 {
t . Fatalf ( "hishtory query #%d has the wrong number of lines=%d, out=%#v" , i , strings . Count ( out , "\n" ) , out )
}
if strings . Count ( out , "echo mycommand" ) != 1 {
t . Fatalf ( "hishtory query #%d has the wrong number of commands=%d, out=%#v" , i , strings . Count ( out , "echo mycommand" ) , out )
}
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , "" )
2022-04-15 05:18:49 +02:00
if strings . Contains ( out , "shouldnotshowup" ) {
t . Fatalf ( "hishtory query contains a result that should not have been recorded, out=%#v" , out )
}
}
2022-04-18 04:54:17 +02:00
out := tester . RunInteractiveShell ( t , "hishtory export | grep -v pipefail | grep -v '/tmp/client install'" )
expectedOutput := "echo mycommand-0\nhishtory enable\nhishtory query mycommand-0\nhishtory query\necho mycommand-1\nhishtory enable\nhishtory query mycommand-1\nhishtory query\necho mycommand-2\nhishtory enable\nhishtory query mycommand-2\nhishtory query\necho mycommand-3\nhishtory enable\nhishtory query mycommand-3\nhishtory query\necho mycommand-4\nhishtory enable\nhishtory query mycommand-4\nhishtory query\necho mycommand-5\nhishtory enable\nhishtory query mycommand-5\nhishtory query\necho mycommand-6\nhishtory enable\nhishtory query mycommand-6\nhishtory query\necho mycommand-7\nhishtory enable\nhishtory query mycommand-7\nhishtory query\necho mycommand-8\nhishtory enable\nhishtory query mycommand-8\nhishtory query\necho mycommand-9\nhishtory enable\nhishtory query mycommand-9\nhishtory query\necho mycommand-10\nhishtory enable\nhishtory query mycommand-10\nhishtory query\necho mycommand-11\nhishtory enable\nhishtory query mycommand-11\nhishtory query\necho mycommand-12\nhishtory enable\nhishtory query mycommand-12\nhishtory query\necho mycommand-13\nhishtory enable\nhishtory query mycommand-13\nhishtory query\necho mycommand-14\nhishtory enable\nhishtory query mycommand-14\nhishtory query\necho mycommand-15\nhishtory enable\nhishtory query mycommand-15\nhishtory query\necho mycommand-16\nhishtory enable\nhishtory query mycommand-16\nhishtory query\necho mycommand-17\nhishtory enable\nhishtory query mycommand-17\nhishtory query\necho mycommand-18\nhishtory enable\nhishtory query mycommand-18\nhishtory query\necho mycommand-19\nhishtory enable\nhishtory query mycommand-19\nhishtory query\necho mycommand-20\nhishtory enable\nhishtory query mycommand-20\nhishtory query\necho mycommand-21\nhishtory enable\nhishtory query mycommand-21\nhishtory query\necho mycommand-22\nhishtory enable\nhishtory query mycommand-22\nhishtory query\necho mycommand-23\nhishtory enable\nhishtory query mycommand-23\nhishtory query\necho mycommand-24\nhishtory enable\nhishtory query mycommand-24\nhishtory query\n"
2022-04-16 09:44:47 +02:00
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
2022-04-15 05:18:49 +02:00
}
}
2022-04-18 04:54:17 +02:00
func testExcludeHiddenCommand ( t * testing . T , tester shellTester ) {
2022-04-15 09:04:49 +02:00
// Set up
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-04-18 04:54:17 +02:00
installHishtory ( t , tester , "" )
2022-04-15 09:04:49 +02:00
2022-04-18 04:54:17 +02:00
tester . RunInteractiveShell ( t , ` echo hello1
2022-04-15 09:04:49 +02:00
echo hidden
echo hello2
echo hidden ` )
2022-04-18 04:54:17 +02:00
tester . RunInteractiveShell ( t , " echo hidden" )
2022-04-18 06:04:44 +02:00
out := hishtoryQuery ( t , tester , "-pipefail" )
if strings . Count ( out , "\n" ) != 3 {
2022-04-19 06:28:41 +02:00
t . Fatalf ( "hishtory query has the wrong number of lines=%d, out=%#v, bash hishtory file=%#v" , strings . Count ( out , "\n" ) , out , tester . RunInteractiveShell ( t , "cat ~/.bash_history" ) )
2022-04-15 09:04:49 +02:00
}
if strings . Count ( out , "echo hello" ) != 2 {
t . Fatalf ( "hishtory query has the wrong number of commands=%d, out=%#v" , strings . Count ( out , "echo mycommand" ) , out )
}
if strings . Count ( out , "echo hello1" ) != 1 {
t . Fatalf ( "hishtory query has the wrong number of commands=%d, out=%#v" , strings . Count ( out , "echo mycommand" ) , out )
}
if strings . Count ( out , "echo hello2" ) != 1 {
t . Fatalf ( "hishtory query has the wrong number of commands=%d, out=%#v" , strings . Count ( out , "echo mycommand" ) , out )
}
if strings . Contains ( out , "hidden" ) {
t . Fatalf ( "hishtory query contains a result that should not have been recorded, out=%#v" , out )
}
2022-04-18 04:54:17 +02:00
out = tester . RunInteractiveShell ( t , "hishtory export | grep -v pipefail | grep -v '/tmp/client install'" )
2022-04-18 06:04:44 +02:00
expectedOutput := "echo hello1\necho hello2\n"
2022-04-15 09:04:49 +02:00
if out != expectedOutput {
t . Fatalf ( "hishtory export has unexpected output=%#v" , out )
}
}
2022-04-17 20:24:55 +02:00
func getPidofCommand ( ) string {
2022-11-05 07:14:34 +01:00
if runtime . GOOS == "darwin" {
2022-04-17 20:24:55 +02:00
// MacOS doesn't have pidof by default
return "pgrep"
}
return "pidof"
}
2022-04-15 05:18:49 +02:00
func waitForBackgroundSavesToComplete ( t * testing . T ) {
2022-10-23 09:20:47 +02:00
lastOut := ""
lastErr := ""
for i := 0 ; i < 20 ; i ++ {
2022-04-17 20:24:55 +02:00
cmd := exec . Command ( getPidofCommand ( ) , "hishtory" )
2022-04-15 05:18:49 +02:00
var stdout bytes . Buffer
var stderr bytes . Buffer
cmd . Stdout = & stdout
cmd . Stderr = & stderr
err := cmd . Run ( )
if err != nil && err . Error ( ) != "exit status 1" {
t . Fatalf ( "failed to check if hishtory was running: %v, stdout=%#v, stderr=%#v" , err , stdout . String ( ) , stderr . String ( ) )
}
if ! strings . Contains ( stdout . String ( ) , "\n" ) {
2022-04-21 05:56:01 +02:00
// pidof had no output, so hishtory isn't running and we're done waiting
time . Sleep ( 1000 * time . Millisecond )
2022-04-15 05:18:49 +02:00
return
}
2022-10-23 09:20:47 +02:00
lastOut = stdout . String ( )
2022-10-23 09:46:56 +02:00
lastErr = stderr . String ( )
2022-04-15 05:18:49 +02:00
time . Sleep ( 50 * time . Millisecond )
}
2022-10-23 09:20:47 +02:00
t . Fatalf ( "failed to wait until hishtory wasn't running (lastOut=%#v, lastErr=%#v)" , lastOut , lastErr )
2022-04-15 05:18:49 +02:00
}
2022-04-18 04:54:17 +02:00
func hishtoryQuery ( t * testing . T , tester shellTester , query string ) string {
return tester . RunInteractiveShell ( t , "hishtory query " + query )
2022-04-15 05:18:49 +02:00
}
2022-04-11 02:38:20 +02:00
func manuallySubmitHistoryEntry ( t * testing . T , userSecret string , entry data . HistoryEntry ) {
encEntry , err := data . EncryptHistoryEntry ( userSecret , entry )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-09-20 07:49:48 +02:00
if encEntry . Date != entry . EndTime {
t . Fatalf ( "encEntry.Date does not match the entry" )
}
2022-04-11 02:38:20 +02:00
jsonValue , err := json . Marshal ( [ ] shared . EncHistoryEntry { encEntry } )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-04-28 20:26:55 +02:00
resp , err := http . Post ( "http://localhost:8080/api/v1/submit" , "application/json" , bytes . NewBuffer ( jsonValue ) )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-04-11 02:38:20 +02:00
if resp . StatusCode != 200 {
t . Fatalf ( "failed to submit result to backend, status_code=%d" , resp . StatusCode )
2022-04-09 08:47:13 +02:00
}
}
2022-04-15 05:18:49 +02:00
2022-04-19 07:07:10 +02:00
func testTimestampsAreReasonablyCorrect ( t * testing . T , tester shellTester ) {
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-04-19 07:07:10 +02:00
installHishtory ( t , tester , "" )
// Record a command
out := tester . RunInteractiveShell ( t , "echo hello" )
if out != "hello\n" {
t . Fatalf ( "running echo hello had unexpected out=%#v" , out )
}
// Query for it and check that the timestamp that gets recorded looks reasonable
out = hishtoryQuery ( t , tester , "echo hello" )
if strings . Count ( out , "\n" ) != 2 {
t . Fatalf ( "hishtory query has unexpected number of lines: out=%#v" , out )
}
expectedDate := time . Now ( ) . Format ( "Jan 2 2006" )
if ! strings . Contains ( out , expectedDate ) {
t . Fatalf ( "hishtory query has an incorrect date: out=%#v" , out )
}
}
2022-04-19 07:45:07 +02:00
func testTableDisplayCwd ( t * testing . T , tester shellTester ) {
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-04-19 07:45:07 +02:00
installHishtory ( t , tester , "" )
// Record a command
out := tester . RunInteractiveShell ( t , ` cd ~ / . hishtory /
echo hello
cd / tmp /
echo other ` )
if out != "hello\nother\n" {
t . Fatalf ( "running echo hello had unexpected out=%#v" , out )
}
// Query for it and check that the directory gets recorded correctly
out = hishtoryQuery ( t , tester , "echo hello" )
if strings . Count ( out , "\n" ) != 2 {
t . Fatalf ( "hishtory query has unexpected number of lines: out=%#v" , out )
}
2022-04-20 04:21:39 +02:00
if ! strings . Contains ( out , "~/.hishtory" ) {
2022-04-19 07:45:07 +02:00
t . Fatalf ( "hishtory query has an incorrect CWD: out=%#v" , out )
}
out = hishtoryQuery ( t , tester , "echo other" )
if strings . Count ( out , "\n" ) != 2 {
t . Fatalf ( "hishtory query has unexpected number of lines: out=%#v" , out )
}
2022-04-20 04:21:39 +02:00
if ! strings . Contains ( out , "/tmp" ) {
2022-04-19 07:45:07 +02:00
t . Fatalf ( "hishtory query has an incorrect CWD: out=%#v" , out )
}
}
2022-04-18 04:54:17 +02:00
func testHishtoryBackgroundSaving ( t * testing . T , tester shellTester ) {
2022-09-23 03:09:51 +02:00
if runtime . GOOS == "darwin" {
t . Skip ( "skip testing background saving since it is flakey on MacOs" )
2022-04-28 20:26:55 +02:00
}
2022-04-15 05:18:49 +02:00
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-04-15 05:18:49 +02:00
2022-09-23 03:09:51 +02:00
// Check that we can find the go binary
_ , err := exec . LookPath ( "go" )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-09-22 06:49:08 +02:00
2022-04-15 05:18:49 +02:00
// Test install with an unset HISHTORY_TEST var so that we save in the background (this is likely to be flakey!)
2022-04-18 04:54:17 +02:00
out := tester . RunInteractiveShell ( t , ` unset HISHTORY_TEST
2022-09-22 05:30:50 +02:00
CGO_ENABLED = 0 go build - o / tmp / client
2022-04-15 05:18:49 +02:00
/ tmp / client install ` )
r := regexp . MustCompile ( ` Setting secret hishtory key to (.*) ` )
matches := r . FindStringSubmatch ( out )
if len ( matches ) != 2 {
2022-09-21 08:30:57 +02:00
t . Fatalf ( "Failed to extract userSecret from output=%#v: matches=%#v" , out , matches )
2022-04-15 05:18:49 +02:00
}
userSecret := matches [ 1 ]
// Assert that config.sh isn't the dev version
2022-04-16 09:56:08 +02:00
homedir , err := os . UserHomeDir ( )
if err != nil {
t . Fatalf ( "failed to get homedir: %v" , err )
}
2022-10-28 06:53:47 +02:00
dat , err := os . ReadFile ( path . Join ( homedir , data . HISHTORY_PATH , "config.sh" ) )
2022-04-16 09:56:08 +02:00
if err != nil {
t . Fatalf ( "failed to read config.sh: %v" , err )
}
if strings . Contains ( string ( dat ) , "except it doesn't run the save process in the background" ) {
t . Fatalf ( "config.sh is the testing version when it shouldn't be, config.sh=%#v" , dat )
}
2022-04-15 05:18:49 +02:00
// Test the status subcommand
2022-04-18 04:54:17 +02:00
out = tester . RunInteractiveShell ( t , ` hishtory status ` )
2022-09-27 07:55:59 +02:00
if out != fmt . Sprintf ( "hiSHtory: v0.Unknown\nEnabled: true\nSecret Key: %s\nCommit Hash: Unknown\n" , userSecret ) {
2022-04-15 05:18:49 +02:00
t . Fatalf ( "status command has unexpected output: %#v" , out )
}
// Test recording commands
2022-04-20 06:05:54 +02:00
out , err = tester . RunInteractiveShellRelaxed ( t , ` ls / a
2022-04-15 05:18:49 +02:00
echo foo ` )
if err != nil {
t . Fatal ( err )
}
if out != "foo\n" {
t . Fatalf ( "unexpected output from running commands: %#v" , out )
}
// Test querying for all commands
2022-04-15 20:20:23 +02:00
waitForBackgroundSavesToComplete ( t )
2022-04-22 07:25:24 +02:00
time . Sleep ( time . Second )
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , "" )
2022-04-15 05:18:49 +02:00
expected := [ ] string { "echo foo" , "ls /a" }
for _ , item := range expected {
if ! strings . Contains ( out , item ) {
t . Fatalf ( "output is missing expected item %#v: %#v" , item , out )
}
}
// Test querying for a specific command
2022-04-15 20:20:23 +02:00
waitForBackgroundSavesToComplete ( t )
2022-04-18 04:54:17 +02:00
out = hishtoryQuery ( t , tester , "foo" )
2022-04-15 05:18:49 +02:00
if ! strings . Contains ( out , "echo foo" ) {
t . Fatalf ( "output doesn't contain the expected item, out=%#v" , out )
}
if strings . Contains ( out , "ls /a" ) {
t . Fatalf ( "output contains unexpected item, out=%#v" , out )
}
}
2022-04-16 10:09:25 +02:00
2022-04-18 04:54:17 +02:00
func testDisplayTable ( t * testing . T , tester shellTester ) {
2022-04-16 10:09:25 +02:00
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-04-18 04:54:17 +02:00
userSecret := installHishtory ( t , tester , "" )
2022-04-16 10:09:25 +02:00
// Submit two fake entries
2022-04-16 19:46:57 +02:00
tmz , err := time . LoadLocation ( "America/Los_Angeles" )
if err != nil {
t . Fatalf ( "failed to load timezone: %v" , err )
}
2022-10-28 06:53:47 +02:00
entry1 := testutils . MakeFakeHistoryEntry ( "table_cmd1" )
2022-04-16 19:46:57 +02:00
entry1 . StartTime = time . Unix ( 1650096186 , 0 ) . In ( tmz )
entry1 . EndTime = time . Unix ( 1650096190 , 0 ) . In ( tmz )
2022-04-16 10:09:25 +02:00
manuallySubmitHistoryEntry ( t , userSecret , entry1 )
2022-10-28 06:53:47 +02:00
entry2 := testutils . MakeFakeHistoryEntry ( "table_cmd2" )
2022-04-16 19:46:57 +02:00
entry2 . StartTime = time . Unix ( 1650096196 , 0 ) . In ( tmz )
entry2 . EndTime = time . Unix ( 1650096220 , 0 ) . In ( tmz )
2022-04-16 10:09:25 +02:00
entry2 . CurrentWorkingDirectory = "~/foo/"
entry2 . ExitCode = 3
manuallySubmitHistoryEntry ( t , userSecret , entry2 )
// Query and check the table
2022-10-30 02:34:16 +01:00
tester . RunInteractiveShell ( t , ` hishtory disable ` )
2022-04-18 04:54:17 +02:00
out := hishtoryQuery ( t , tester , "table" )
2022-10-28 06:28:37 +02:00
compareGoldens ( t , out , "testDisplayTable-defaultColumns" )
// Adjust the columns that should be displayed
tester . RunInteractiveShell ( t , ` hishtory config-set displayed-columns Hostname Command ` )
// And check the table again
out = hishtoryQuery ( t , tester , "table" )
compareGoldens ( t , out , "testDisplayTable-customColumns" )
2022-10-28 06:57:09 +02:00
// And again
tester . RunInteractiveShell ( t , ` hishtory config-set displayed-columns Hostname 'Exit Code' Command ` )
out = hishtoryQuery ( t , tester , "table" )
compareGoldens ( t , out , "testDisplayTable-customColumns-2" )
2022-10-28 06:58:39 +02:00
// And again
tester . RunInteractiveShell ( t , ` hishtory config-add displayed-columns CWD ` )
out = hishtoryQuery ( t , tester , "table" )
compareGoldens ( t , out , "testDisplayTable-customColumns-3" )
2022-10-28 07:02:19 +02:00
// Test displaying a command with multiple lines
entry3 := testutils . MakeFakeHistoryEntry ( "while :\ndo\nls /table/\ndone" )
manuallySubmitHistoryEntry ( t , userSecret , entry3 )
out = hishtoryQuery ( t , tester , "table" )
compareGoldens ( t , out , "testDisplayTable-customColumns-multiLineCommand" )
2022-10-28 07:31:59 +02:00
// Add a custom column
tester . RunInteractiveShell ( t , ` hishtory config-add custom-columns foo "echo aaaaaaaaaaaaa" ` )
2022-10-30 02:53:40 +02:00
testutils . Check ( t , os . Chdir ( "/" ) )
2022-10-28 07:31:59 +02:00
tester . RunInteractiveShell ( t , ` hishtory enable ` )
tester . RunInteractiveShell ( t , ` echo table-1 ` )
tester . RunInteractiveShell ( t , ` echo table-2 ` )
tester . RunInteractiveShell ( t , ` echo bar ` )
tester . RunInteractiveShell ( t , ` hishtory disable ` )
tester . RunInteractiveShell ( t , ` hishtory config-add displayed-columns foo ` )
// And run a query and confirm it is displayed
out = hishtoryQuery ( t , tester , "table" )
compareGoldens ( t , out , "testDisplayTable-customColumns-trulyCustom" )
2022-04-16 10:09:25 +02:00
}
2022-05-02 04:37:26 +02:00
func testRequestAndReceiveDbDump ( t * testing . T , tester shellTester ) {
// Set up
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-05-02 04:37:26 +02:00
secretKey := installHishtory ( t , tester , "" )
2022-05-23 04:45:46 +02:00
// Confirm there are no pending dump requests
2022-09-21 07:28:40 +02:00
config := hctx . GetConf ( hctx . MakeContext ( ) )
2022-06-05 08:03:05 +02:00
deviceId1 := config . DeviceId
resp , err := lib . ApiGet ( "/api/v1/get-dump-requests?user_id=" + data . UserId ( secretKey ) + "&device_id=" + deviceId1 )
2022-05-23 04:45:46 +02:00
if err != nil {
t . Fatalf ( "failed to get pending dump requests: %v" , err )
}
if string ( resp ) != "[]" {
t . Fatalf ( "There are pending dump requests! user_id=%#v, resp=%#v" , data . UserId ( secretKey ) , string ( resp ) )
}
2022-05-02 04:37:26 +02:00
// Record two commands and then query for them
out := tester . RunInteractiveShell ( t , ` echo hello
2022-05-23 04:45:46 +02:00
echo other ` )
if out != "hello\nother\n" {
t . Fatalf ( "running echo had unexpected out=%#v" , out )
}
2022-05-02 04:37:26 +02:00
// Query for it and check that the directory gets recorded correctly
out = hishtoryQuery ( t , tester , "echo" )
if strings . Count ( out , "\n" ) != 3 {
t . Fatalf ( "hishtory query has unexpected number of lines: out=%#v" , out )
}
if ! strings . Contains ( out , "echo hello" ) {
t . Fatalf ( "hishtory query doesn't contain expected command, out=%#v" , out )
}
if ! strings . Contains ( out , "echo other" ) {
t . Fatalf ( "hishtory query doesn't contain expected command, out=%#v" , out )
}
// Back up this copy
2022-10-28 06:53:47 +02:00
restoreFirstInstallation := testutils . BackupAndRestoreWithId ( t , "-install1" )
2022-05-02 04:37:26 +02:00
// Wipe the DB to simulate entries getting deleted because they've already been read and expired
2022-05-23 04:45:46 +02:00
_ , err = lib . ApiGet ( "/api/v1/wipe-db" )
2022-05-02 04:37:26 +02:00
if err != nil {
t . Fatalf ( "failed to wipe the DB: %v" , err )
}
// Install a new one (with the same secret key but a diff device id)
installHishtory ( t , tester , secretKey )
2022-06-05 08:03:05 +02:00
// Confirm there is now a pending dump requests that the first device should respond to
resp , err = lib . ApiGet ( "/api/v1/get-dump-requests?user_id=" + data . UserId ( secretKey ) + "&device_id=" + deviceId1 )
2022-05-23 04:45:46 +02:00
if err != nil {
t . Fatalf ( "failed to get pending dump requests: %v" , err )
}
if string ( resp ) == "[]" {
t . Fatalf ( "There are no pending dump requests! user_id=%#v, resp=%#v" , data . UserId ( secretKey ) , string ( resp ) )
}
2022-05-02 04:37:26 +02:00
// Check that the new one doesn't have the commands yet
out = hishtoryQuery ( t , tester , "echo" )
if strings . Count ( out , "\n" ) != 1 {
t . Fatalf ( "hishtory query has unexpected number of lines, should contain no entries: out=%#v" , out )
}
if strings . Contains ( out , "echo hello" ) || strings . Contains ( "echo other" , out ) {
t . Fatalf ( "hishtory query contains unexpected command, out=%#v" , out )
}
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
if out != "hishtory query echo\n" {
t . Fatalf ( "hishtory export has unexpected out=%#v" , out )
}
// Restore the first copy
2022-10-28 06:53:47 +02:00
restoreSecondInstallation := testutils . BackupAndRestoreWithId ( t , "-install2" )
2022-05-02 04:37:26 +02:00
restoreFirstInstallation ( )
// Confirm it still has the correct entries via hishtory export (and this runs a command to trigger it to dump the DB)
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
2022-05-23 04:45:46 +02:00
expectedOutput := "echo hello\necho other\nhishtory query echo\nhishtory query echo\n"
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
2022-06-05 08:03:05 +02:00
// Confirm there are no pending dump requests for the first device
resp , err = lib . ApiGet ( "/api/v1/get-dump-requests?user_id=" + data . UserId ( secretKey ) + "&device_id=" + deviceId1 )
2022-05-23 04:45:46 +02:00
if err != nil {
t . Fatalf ( "failed to get pending dump requests: %v" , err )
}
if string ( resp ) != "[]" {
t . Fatalf ( "There are pending dump requests! user_id=%#v, resp=%#v" , data . UserId ( secretKey ) , string ( resp ) )
2022-05-02 04:37:26 +02:00
}
// Restore the second copy and confirm it has the commands
restoreSecondInstallation ( )
out = hishtoryQuery ( t , tester , "ech" )
if strings . Count ( out , "\n" ) != 5 {
t . Fatalf ( "hishtory query has unexpected number of lines=%d: out=%#v" , strings . Count ( out , "\n" ) , out )
}
expected := [ ] string { "echo hello" , "echo other" }
for _ , item := range expected {
if ! strings . Contains ( out , item ) {
t . Fatalf ( "output is missing expected item %#v: %#v" , item , out )
}
if strings . Count ( out , item ) != 1 {
t . Fatalf ( "output has %#v in it multiple times! out=%#v" , item , out )
}
}
// And check hishtory export too for good measure
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
2022-05-23 04:45:46 +02:00
expectedOutput = "echo hello\necho other\nhishtory query echo\nhishtory query echo\nhishtory query ech\n"
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
2022-05-02 04:37:26 +02:00
}
}
2022-06-05 07:27:04 +02:00
func testInstallViaPythonScript ( t * testing . T , tester shellTester ) {
// Set up
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
defer testutils . BackupAndRestoreEnv ( "HISHTORY_TEST" ) ( )
2022-06-05 07:27:04 +02:00
// Install via the python script
out := tester . RunInteractiveShell ( t , ` curl https://hishtory.dev/install.py | python3 - ` )
if ! strings . Contains ( out , "Succesfully installed hishtory" ) {
t . Fatalf ( "unexpected output when installing hishtory, out=%#v" , out )
}
r := regexp . MustCompile ( ` Setting secret hishtory key to (.*) ` )
matches := r . FindStringSubmatch ( out )
if len ( matches ) != 2 {
2022-09-21 08:30:57 +02:00
t . Fatalf ( "Failed to extract userSecret from output=%#v: matches=%#v" , out , matches )
2022-06-05 07:27:04 +02:00
}
userSecret := matches [ 1 ]
// Test the status subcommand
downloadData , err := lib . GetDownloadData ( )
if err != nil {
t . Fatal ( err )
}
out = tester . RunInteractiveShell ( t , ` hishtory status ` )
2022-09-27 07:55:59 +02:00
expectedOut := fmt . Sprintf ( "hiSHtory: %s\nEnabled: true\nSecret Key: %s\nCommit Hash: " , downloadData . Version , userSecret )
2022-06-05 07:27:04 +02:00
if ! strings . Contains ( out , expectedOut ) {
t . Fatalf ( "status command has unexpected output: actual=%#v, expected=%#v" , out , expectedOut )
}
// And test that it recorded that command
2022-10-16 18:56:14 +02:00
time . Sleep ( time . Second )
2022-06-05 07:27:04 +02:00
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
if out != "hishtory status\n" {
t . Fatalf ( "unexpected output from hishtory export=%#v" , out )
}
}
2022-06-06 03:05:06 +02:00
func testExportWithQuery ( t * testing . T , tester shellTester ) {
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-06-06 03:05:06 +02:00
installHishtory ( t , tester , "" )
// Test recording commands
out , err := tester . RunInteractiveShellRelaxed ( t , ` ls / a
ls / bar
ls / foo
echo foo
echo bar
hishtory disable
echo thisisnotrecorded
sleep 0.5
cd / tmp /
hishtory enable
2022-10-20 06:33:31 +02:00
echo thisisrecorded
echo bar &
sleep 1 ` )
2022-06-06 03:05:06 +02:00
if err != nil {
t . Fatal ( err )
}
2022-10-20 06:33:31 +02:00
if out != "foo\nbar\nthisisnotrecorded\nthisisrecorded\nbar\n" {
2022-06-06 03:05:06 +02:00
t . Fatalf ( "unexpected output from running commands: %#v" , out )
}
// Test querying for all commands
out = hishtoryQuery ( t , tester , "" )
2022-10-20 06:33:31 +02:00
expected := [ ] string { "echo thisisrecorded" , "hishtory enable" , "echo bar" , "echo foo" , "ls /foo" , "ls /bar" , "ls /a" , "echo bar &" , "sleep 1" }
2022-06-06 03:05:06 +02:00
for _ , item := range expected {
if ! strings . Contains ( out , item ) {
t . Fatalf ( "output is missing expected item %#v: %#v" , item , out )
}
}
// Test querying for a specific command
out = hishtoryQuery ( t , tester , "foo" )
expected = [ ] string { "echo foo" , "ls /foo" }
unexpected := [ ] string { "echo thisisrecorded" , "hishtory enable" , "echo bar" , "ls /bar" , "ls /a" }
for _ , item := range expected {
if ! strings . Contains ( out , item ) {
t . Fatalf ( "output is missing expected item %#v: %#v" , item , out )
}
if strings . Count ( out , item ) != 1 {
t . Fatalf ( "output has %#v in it multiple times! out=%#v" , item , out )
}
}
for _ , item := range unexpected {
if strings . Contains ( out , item ) {
t . Fatalf ( "output is containing unexpected item %#v: %#v" , item , out )
}
}
// Test using export with a query
out = tester . RunInteractiveShell ( t , ` hishtory export foo ` )
if out != "ls /foo\necho foo\nhishtory query foo\n" {
t . Fatalf ( "expected hishtory export to equal out=%#v" , out )
}
// Test a more complex query with export
out = tester . RunInteractiveShell ( t , ` hishtory export cwd:/tmp/ ` )
2022-10-20 06:33:31 +02:00
if out != "hishtory enable\necho thisisrecorded\necho bar &\nsleep 1\n" {
2022-06-06 03:05:06 +02:00
t . Fatalf ( "expected hishtory export to equal out=%#v" , out )
}
}
2022-06-06 03:26:02 +02:00
func testHelpCommand ( t * testing . T , tester shellTester ) {
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-06-06 03:26:02 +02:00
installHishtory ( t , tester , "" )
// Test the help command
out := tester . RunInteractiveShell ( t , ` hishtory help ` )
2022-09-25 01:18:24 +02:00
if ! strings . HasPrefix ( out , "hiSHtory: Better shell history\n\nSupported commands:\n" ) {
2022-06-06 03:26:02 +02:00
t . Fatalf ( "expected hishtory help to contain intro, actual=%#v" , out )
}
out2 := tester . RunInteractiveShell ( t , ` hishtory -h ` )
if out != out2 {
t . Fatalf ( "expected hishtory -h to equal help" )
}
}
2022-06-13 06:28:19 +02:00
func testStripBashTimePrefix ( t * testing . T , tester shellTester ) {
if tester . ShellName ( ) != "bash" {
t . Skip ( )
}
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-06-13 06:28:19 +02:00
installHishtory ( t , tester , "" )
// Add a HISTTIMEFORMAT to the bashrc
homedir , err := os . UserHomeDir ( )
if err != nil {
t . Fatal ( err )
}
f , err := os . OpenFile ( path . Join ( homedir , ".hishtory" , "config.sh" ) ,
os . O_APPEND | os . O_WRONLY , 0644 )
if err != nil {
t . Fatal ( err )
}
defer f . Close ( )
_ , err = f . WriteString ( "\nexport HISTTIMEFORMAT='%F %T '\n" )
if err != nil {
t . Fatal ( err )
}
// Record a command
tester . RunInteractiveShell ( t , ` ls -Slah ` )
// Check it shows up correctly
out := tester . RunInteractiveShell ( t , "hishtory export ls" )
if out != "ls -Slah\n" {
t . Fatalf ( "hishtory had unexpected output=%#v" , out )
}
// Update it to another complex one
homedir , err = os . UserHomeDir ( )
if err != nil {
t . Fatal ( err )
}
f , err = os . OpenFile ( path . Join ( homedir , ".hishtory" , "config.sh" ) ,
os . O_APPEND | os . O_WRONLY , 0644 )
if err != nil {
t . Fatal ( err )
}
defer f . Close ( )
_ , err = f . WriteString ( "\nexport HISTTIMEFORMAT='[%c] '\n" )
if err != nil {
t . Fatal ( err )
}
// Record a command
tester . RunInteractiveShell ( t , ` echo foo ` )
// Check it shows up correctly
out = tester . RunInteractiveShell ( t , "hishtory export echo" )
if out != "echo foo\n" {
t . Fatalf ( "hishtory had unexpected output=%#v" , out )
}
}
2022-09-05 03:37:46 +02:00
func testReuploadHistoryEntries ( t * testing . T , tester shellTester ) {
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-09-05 03:37:46 +02:00
// Init an initial device
userSecret := installHishtory ( t , tester , "" )
// Set up a second device
2022-10-28 06:53:47 +02:00
restoreFirstProfile := testutils . BackupAndRestoreWithId ( t , "-install1" )
2022-09-05 03:37:46 +02:00
installHishtory ( t , tester , userSecret )
// Device 2: Record a command
tester . RunInteractiveShell ( t , ` echo 1 ` )
// Device 2: Record a command with a simulated network error
tester . RunInteractiveShell ( t , ` echo 2; export HISHTORY_SIMULATE_NETWORK_ERROR=1; echo 3 ` )
// Device 1: Run an export and confirm that the network only contains the first command
2022-10-28 06:53:47 +02:00
restoreSecondProfile := testutils . BackupAndRestoreWithId ( t , "-install2" )
2022-09-05 03:37:46 +02:00
restoreFirstProfile ( )
out := tester . RunInteractiveShell ( t , "hishtory export | grep -v pipefail" )
expectedOutput := "echo 1\n"
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
// Device 2: Run another command but with the network re-enabled
2022-10-28 06:53:47 +02:00
restoreFirstProfile = testutils . BackupAndRestoreWithId ( t , "-install1" )
2022-09-05 03:37:46 +02:00
restoreSecondProfile ( )
tester . RunInteractiveShell ( t , ` unset HISHTORY_SIMULATE_NETWORK_ERROR; echo 4 ` )
// Device 2: Run export which contains all results (as it did all along since it is stored offline)
out = tester . RunInteractiveShell ( t , "hishtory export | grep -v pipefail" )
expectedOutput = "echo 1\necho 2; export HISHTORY_SIMULATE_NETWORK_ERROR=1; echo 3\nunset HISHTORY_SIMULATE_NETWORK_ERROR; echo 4\n"
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
// Device 1: Now it too sees all the results
restoreFirstProfile ( )
out = tester . RunInteractiveShell ( t , "hishtory export | grep -v pipefail" )
expectedOutput = "echo 1\necho 2; export HISHTORY_SIMULATE_NETWORK_ERROR=1; echo 3\nunset HISHTORY_SIMULATE_NETWORK_ERROR; echo 4\n"
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
}
2022-09-22 05:09:49 +02:00
func testHishtoryOffline ( t * testing . T , tester shellTester ) {
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-09-22 05:09:49 +02:00
// Init an initial device
userSecret := installHishtory ( t , tester , "" )
// Set up a second device
2022-10-28 06:53:47 +02:00
restoreFirstProfile := testutils . BackupAndRestoreWithId ( t , "-install1" )
2022-09-22 05:09:49 +02:00
installHishtory ( t , tester , userSecret )
// Device 2: Record a command
tester . RunInteractiveShell ( t , ` echo dev2 ` )
// Device 1: Run a command
2022-10-28 06:53:47 +02:00
restoreSecondProfile := testutils . BackupAndRestoreWithId ( t , "-install2" )
2022-09-22 05:09:49 +02:00
restoreFirstProfile ( )
tester . RunInteractiveShell ( t , ` echo dev1-a ` )
// Device 1: Query while offline
out := tester . RunInteractiveShell ( t , ` export HISHTORY_SIMULATE_NETWORK_ERROR=1; hishtory export | grep -v pipefail ` )
expectedOutput := "echo dev2\necho dev1-a\n"
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
// Device 2: Record another command
2022-10-28 06:53:47 +02:00
restoreFirstProfile = testutils . BackupAndRestoreWithId ( t , "-install1" )
2022-09-22 05:09:49 +02:00
restoreSecondProfile ( )
tester . RunInteractiveShell ( t , ` echo dev2-b ` )
// Device 1: Query while offline before ever retrieving the command
restoreFirstProfile ( )
out = tester . RunInteractiveShell ( t , ` export HISHTORY_SIMULATE_NETWORK_ERROR=1; hishtory export | grep -v pipefail ` )
expectedOutput = "echo dev2\necho dev1-a\n"
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
// Device 1: Query while online and get the command
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
expectedOutput = "echo dev2\necho dev1-a\necho dev2-b\n"
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
}
2022-09-17 20:49:31 +02:00
func testInitialHistoryImport ( t * testing . T , tester shellTester ) {
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-09-17 20:49:31 +02:00
// Record some commands before installing hishtory
2022-11-04 06:47:43 +01:00
captureTerminalOutputWithShellName ( t , tester , "fish" , [ ] string { "echo SPACE fishcommand ENTER" } )
2022-09-17 20:49:31 +02:00
randomCmdUuid := uuid . Must ( uuid . NewRandom ( ) ) . String ( )
randomCmd := fmt . Sprintf ( ` echo % v - foo
echo % v - bar ` , randomCmdUuid , randomCmdUuid )
tester . RunInteractiveShell ( t , randomCmd )
// Install hishtory
installHishtory ( t , tester , "" )
// Check that hishtory export doesn't have the commands yet
out := tester . RunInteractiveShell ( t , ` hishtory export ` + randomCmdUuid )
expectedOutput := ""
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
// Trigger an import
2022-10-01 08:50:25 +02:00
out = tester . RunInteractiveShell ( t , "echo stdincommand | hishtory import" )
2022-09-17 20:49:31 +02:00
r := regexp . MustCompile ( ` Imported (.+) history entries from your existing shell history ` )
matches := r . FindStringSubmatch ( out )
if len ( matches ) != 2 {
t . Fatalf ( "Failed to extract history entries count from output: matches=%#v, out=%#v" , matches , out )
}
num , err := strconv . Atoi ( matches [ 1 ] )
if err != nil {
t . Fatal ( err )
}
if num <= 2 {
t . Fatalf ( "hishtory didn't import enough entries, only found %v entries" , num )
}
// Check that the previously recorded commands are in hishtory
2022-09-23 03:09:51 +02:00
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
2022-11-04 06:47:43 +01:00
expectedOutput = fmt . Sprintf ( "hishtory export %s\necho %s-foo\necho %s-bar\n/tmp/client install \nhishtory export %s\necho fishcommand\nstdincommand\necho stdincommand | hishtory import\n" , randomCmdUuid , randomCmdUuid , randomCmdUuid , randomCmdUuid )
2022-09-23 03:09:51 +02:00
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
// And check a different way
2022-09-17 20:49:31 +02:00
out = tester . RunInteractiveShell ( t , ` hishtory export ` + randomCmdUuid )
expectedOutput = fmt . Sprintf ( "hishtory export %s\necho %s-foo\necho %s-bar\nhishtory export %s\n" , randomCmdUuid , randomCmdUuid , randomCmdUuid , randomCmdUuid )
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
}
2022-09-20 07:49:48 +02:00
2022-11-03 21:16:45 +01:00
func testLocalRedaction ( t * testing . T , tester shellTester , onlineStatus OnlineStatus ) {
2022-09-20 07:49:48 +02:00
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-11-03 21:16:45 +01:00
initFromOnlineStatus ( t , tester , onlineStatus )
assertOnlineStatus ( t , onlineStatus )
2022-09-20 07:49:48 +02:00
// Record some commands
randomCmdUuid := uuid . Must ( uuid . NewRandom ( ) ) . String ( )
randomCmd := fmt . Sprintf ( ` echo % v - foo
echo % v - bas
echo foo
ls / tmp ` , randomCmdUuid , randomCmdUuid )
tester . RunInteractiveShell ( t , randomCmd )
// Check that the previously recorded commands are in hishtory
out := tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
expectedOutput := fmt . Sprintf ( "echo %s-foo\necho %s-bas\necho foo\nls /tmp\n" , randomCmdUuid , randomCmdUuid )
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
// Redact foo
out = tester . RunInteractiveShell ( t , ` hishtory redact --force foo ` )
2022-09-22 04:58:10 +02:00
if out != "Permanently deleting 2 entries\n" {
2022-09-20 07:49:48 +02:00
t . Fatalf ( "hishtory redact gave unexpected output=%#v" , out )
}
// Check that the commands are redacted
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
expectedOutput = fmt . Sprintf ( "echo %s-bas\nls /tmp\nhishtory redact --force foo\n" , randomCmdUuid )
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
// Redact s
out = tester . RunInteractiveShell ( t , ` hishtory redact --force s ` )
2022-09-22 04:58:10 +02:00
if out != "Permanently deleting 10 entries\n" {
2022-09-20 07:49:48 +02:00
t . Fatalf ( "hishtory redact gave unexpected output=%#v" , out )
}
// Check that the commands are redacted
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
expectedOutput = "hishtory redact --force s\n"
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
2022-09-23 03:22:06 +02:00
// Record another command
tester . RunInteractiveShell ( t , ` echo hello ` )
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
expectedOutput = "hishtory redact --force s\necho hello\n"
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
// Redact it without --force
out , err := tester . RunInteractiveShellRelaxed ( t , ` yes | hishtory redact hello ` )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-09-23 03:22:06 +02:00
if out != "This will permanently delete 1 entries, are you sure? [y/N]" {
t . Fatalf ( "hishtory redact gave unexpected output=%#v" , out )
}
// And check it was redacted
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
expectedOutput = "hishtory redact --force s\nyes | hishtory redact hello\n"
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
2022-09-20 07:49:48 +02:00
}
func testRemoteRedaction ( t * testing . T , tester shellTester ) {
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-09-20 07:49:48 +02:00
// Install hishtory client 1
userSecret := installHishtory ( t , tester , "" )
// Record some commands
randomCmdUuid := uuid . Must ( uuid . NewRandom ( ) ) . String ( )
randomCmd := fmt . Sprintf ( ` echo % v - foo
2022-09-21 06:38:04 +02:00
echo % v - bas
echo foo
ls / tmp ` , randomCmdUuid , randomCmdUuid )
2022-09-20 07:49:48 +02:00
tester . RunInteractiveShell ( t , randomCmd )
// Check that the previously recorded commands are in hishtory
out := tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
expectedOutput := fmt . Sprintf ( "echo %s-foo\necho %s-bas\necho foo\nls /tmp\n" , randomCmdUuid , randomCmdUuid )
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
// Install hishtory client 2
2022-10-28 06:53:47 +02:00
restoreInstall1 := testutils . BackupAndRestoreWithId ( t , "-1" )
2022-09-20 07:49:48 +02:00
installHishtory ( t , tester , userSecret )
// And confirm that it has the commands too
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
// Restore the first client, and redact some commands
2022-10-28 06:53:47 +02:00
restoreInstall2 := testutils . BackupAndRestoreWithId ( t , "-2" )
2022-09-20 07:49:48 +02:00
restoreInstall1 ( )
out = tester . RunInteractiveShell ( t , ` hishtory redact --force ` + randomCmdUuid )
2022-09-22 04:58:10 +02:00
if out != "Permanently deleting 2 entries\n" {
2022-09-20 07:49:48 +02:00
t . Fatalf ( "hishtory redact gave unexpected output=%#v" , out )
}
// Confirm that client1 doesn't have the commands
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
expectedOutput = fmt . Sprintf ( "echo foo\nls /tmp\nhishtory redact --force %s\n" , randomCmdUuid )
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
// Swap back to the second client and then confirm it processed the deletion request
restoreInstall2 ( )
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
}
2022-09-22 05:30:50 +02:00
2022-10-16 21:43:16 +02:00
func testConfigGetSet ( t * testing . T , tester shellTester ) {
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-10-16 21:43:16 +02:00
installHishtory ( t , tester , "" )
2022-10-28 08:07:00 +02:00
// Config-get and set for enable-control-r
2022-10-16 21:43:16 +02:00
out := tester . RunInteractiveShell ( t , ` hishtory config-get enable-control-r ` )
2022-10-24 01:51:39 +02:00
if out != "true" {
t . Fatalf ( "unexpected config-get output: %#v" , out )
}
tester . RunInteractiveShell ( t , ` hishtory config-set enable-control-r false ` )
out = tester . RunInteractiveShell ( t , ` hishtory config-get enable-control-r ` )
2022-10-16 21:43:16 +02:00
if out != "false" {
t . Fatalf ( "unexpected config-get output: %#v" , out )
}
tester . RunInteractiveShell ( t , ` hishtory config-set enable-control-r true ` )
out = tester . RunInteractiveShell ( t , ` hishtory config-get enable-control-r ` )
if out != "true" {
t . Fatalf ( "unexpected config-get output: %#v" , out )
}
2022-10-28 08:07:00 +02:00
// config for displayed-columns
out = tester . RunInteractiveShell ( t , ` hishtory config-get displayed-columns ` )
if out != "Hostname CWD Timestamp Runtime \"Exit Code\" Command \n" {
t . Fatalf ( "unexpected config-get output: %#v" , out )
}
tester . RunInteractiveShell ( t , ` hishtory config-set displayed-columns Hostname Command 'Exit Code' ` )
out = tester . RunInteractiveShell ( t , ` hishtory config-get displayed-columns ` )
if out != "Hostname Command \"Exit Code\" \n" {
t . Fatalf ( "unexpected config-get output: %#v" , out )
}
tester . RunInteractiveShell ( t , ` hishtory config-add displayed-columns Timestamp ` )
out = tester . RunInteractiveShell ( t , ` hishtory config-get displayed-columns ` )
if out != "Hostname Command \"Exit Code\" Timestamp \n" {
t . Fatalf ( "unexpected config-get output: %#v" , out )
}
tester . RunInteractiveShell ( t , ` hishtory config-delete displayed-columns Hostname ` )
out = tester . RunInteractiveShell ( t , ` hishtory config-get displayed-columns ` )
if out != "Command \"Exit Code\" Timestamp \n" {
t . Fatalf ( "unexpected config-get output: %#v" , out )
}
tester . RunInteractiveShell ( t , ` hishtory config-add displayed-columns foobar ` )
out = tester . RunInteractiveShell ( t , ` hishtory config-get displayed-columns ` )
if out != "Command \"Exit Code\" Timestamp foobar \n" {
t . Fatalf ( "unexpected config-get output: %#v" , out )
}
2022-10-24 01:51:39 +02:00
}
2022-10-16 21:43:16 +02:00
2022-10-24 01:51:39 +02:00
func clearControlRSearchFromConfig ( t * testing . T ) {
configContents , err := hctx . GetConfigContents ( )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-10-24 01:51:39 +02:00
configContents = [ ] byte ( strings . ReplaceAll ( string ( configContents ) , "enable_control_r_search" , "something-else" ) )
homedir , err := os . UserHomeDir ( )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
err = os . WriteFile ( path . Join ( homedir , data . HISHTORY_PATH , data . CONFIG_PATH ) , configContents , 0 o644 )
testutils . Check ( t , err )
2022-10-24 01:51:39 +02:00
}
2022-10-24 04:29:29 +02:00
func testHandleUpgradedFeatures ( t * testing . T , tester shellTester ) {
2022-10-24 01:51:39 +02:00
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-10-24 01:51:39 +02:00
installHishtory ( t , tester , "" )
2022-10-24 04:29:29 +02:00
// Install, and there is no prompt since the config already mentions control-r
_ , err := tester . RunInteractiveShellRelaxed ( t , ` /tmp/client install ` )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-10-24 04:29:29 +02:00
_ , err = tester . RunInteractiveShellRelaxed ( t , ` hishtory disable ` )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-10-24 01:51:39 +02:00
// Ensure that the config doesn't mention control-r
clearControlRSearchFromConfig ( t )
// And check that hishtory says it is false by default
out := tester . RunInteractiveShell ( t , ` hishtory config-get enable-control-r ` )
if out != "false" {
t . Fatalf ( "unexpected config-get output: %#v" , out )
}
2022-10-24 04:29:29 +02:00
// And install again, this time it will get set to true by default
2022-10-24 01:51:39 +02:00
clearControlRSearchFromConfig ( t )
2022-10-24 04:29:29 +02:00
tester . RunInteractiveShell ( t , ` /tmp/client install ` )
2022-10-24 01:51:39 +02:00
// Now it should be enabled
out = tester . RunInteractiveShell ( t , ` hishtory config-get enable-control-r ` )
if out != "true" {
t . Fatalf ( "unexpected config-get output: %#v" , out )
}
2022-10-16 21:43:16 +02:00
}
2022-10-19 06:00:30 +02:00
func TestFish ( t * testing . T ) {
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-10-22 08:16:27 +02:00
tester := bashTester { }
2022-10-19 06:00:30 +02:00
installHishtory ( t , tester , "" )
// Test recording in fish
2022-10-31 02:40:25 +01:00
testutils . Check ( t , os . Chdir ( "/" ) )
2022-10-31 02:01:53 +01:00
out := captureTerminalOutputWithShellName ( t , tester , "fish" , [ ] string {
"echo SPACE foo ENTER" ,
"ENTER" ,
"SPACE echo SPACE baz ENTER" ,
"echo SPACE bar ENTER" ,
2022-11-01 18:37:01 +01:00
"echo SPACE '\"foo\"' ENTER" ,
2022-10-31 02:01:53 +01:00
"SPACE echo SPACE foobar ENTER" ,
2022-11-01 20:00:40 +01:00
"ls SPACE /tmp/ SPACE '&' ENTER" ,
2022-10-31 02:01:53 +01:00
} )
2022-11-01 20:00:40 +01:00
if ! strings . Contains ( out , "Welcome to fish, the friendly interactive shell" ) || ! strings . Contains ( out , "foo" ) || ! strings . Contains ( out , "bar" ) || ! strings . Contains ( out , "baz" ) {
t . Fatalf ( "fish output looks wrong" )
2022-11-01 18:37:01 +01:00
}
2022-10-19 06:00:30 +02:00
2022-10-31 02:40:25 +01:00
// Check export
2022-10-19 06:00:30 +02:00
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail | grep -v ps ` )
2022-11-01 20:00:40 +01:00
expectedOutput := "echo foo\necho bar\necho \"foo\"\nls /tmp/ &\n"
2022-10-19 06:00:30 +02:00
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
2022-10-31 02:40:25 +01:00
// Check a table to see some other metadata
tester . RunInteractiveShell ( t , ` hishtory config-set displayed-columns CWD Hostname 'Exit Code' Command ` )
out = hishtoryQuery ( t , tester , "-pipefail" )
compareGoldens ( t , out , "TestFish-table" )
2022-10-19 06:00:30 +02:00
}
2022-10-28 08:30:09 +02:00
func normalizeHostnames ( data string ) string {
hostnames := [ ] string { "Davids-MacBook-Air.local" , "ghaction-runner-hostname" }
for _ , hostname := range hostnames {
data = strings . ReplaceAll ( data , hostname , "ghaction-runner-hostname" )
}
return data
}
2022-10-28 06:20:33 +02:00
func compareGoldens ( t * testing . T , out , goldenName string ) {
2022-10-28 08:30:09 +02:00
out = normalizeHostnames ( out )
2022-10-30 02:53:40 +02:00
goldenPath := path . Join ( initialWd , "client/lib/goldens/" , goldenName )
2022-10-28 06:20:33 +02:00
expected , err := os . ReadFile ( goldenPath )
if err != nil {
if os . IsNotExist ( err ) {
expected = [ ] byte { }
} else {
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-10-28 06:20:33 +02:00
}
}
if diff := cmp . Diff ( string ( expected ) , out ) ; diff != "" {
if os . Getenv ( "HISHTORY_UPDATE_GOLDENS" ) == "" {
_ , filename , line , _ := runtime . Caller ( 1 )
2022-11-01 05:16:15 +01:00
t . Fatalf ( "hishtory golden mismatch for %s at %s:%d (-expected +got):\n%s\nactual=\n%s" , goldenName , filename , line , diff , out )
2022-10-28 06:20:33 +02:00
} else {
2022-10-28 06:53:47 +02:00
testutils . Check ( t , os . WriteFile ( goldenPath , [ ] byte ( out ) , 0644 ) )
2022-10-28 06:20:33 +02:00
}
}
}
2022-10-21 09:16:25 +02:00
func TestTui ( t * testing . T ) {
2022-10-18 06:58:56 +02:00
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-10-21 09:16:25 +02:00
tester := zshTester { }
2022-10-18 06:58:56 +02:00
installHishtory ( t , tester , "" )
// Disable recording so that all our testing commands don't get recorded
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` hishtory disable ` )
// Insert a couple hishtory entries
db := hctx . GetDb ( hctx . MakeContext ( ) )
2022-10-28 06:53:47 +02:00
db . Create ( testutils . MakeFakeHistoryEntry ( "ls ~/" ) )
db . Create ( testutils . MakeFakeHistoryEntry ( "echo 'aaaaaa bbbb'" ) )
2022-10-18 06:58:56 +02:00
2022-10-19 00:07:56 +02:00
// Check the initial output when there is no search
2022-10-22 09:17:21 +02:00
out := captureTerminalOutput ( t , tester , [ ] string { "hishtory SPACE tquery ENTER" } )
if len ( strings . Split ( out , "hishtory tquery" ) ) != 2 {
t . Fatalf ( "failed to split out=%#v" , out )
}
out = strings . TrimSpace ( strings . Split ( out , "hishtory tquery" ) [ 1 ] )
2022-10-28 06:20:33 +02:00
compareGoldens ( t , out , "TestTui-Initial" )
2022-10-19 00:07:56 +02:00
// Check the output when there is a search
2022-10-22 09:17:21 +02:00
out = captureTerminalOutput ( t , tester , [ ] string {
"hishtory SPACE tquery ENTER" ,
"ls" ,
} )
out = strings . TrimSpace ( strings . Split ( out , "hishtory tquery" ) [ 1 ] )
2022-10-28 06:20:33 +02:00
compareGoldens ( t , out , "TestTui-Search" )
2022-10-19 00:07:56 +02:00
// Check the output when there is a selected result
2022-10-22 09:17:21 +02:00
out = captureTerminalOutput ( t , tester , [ ] string {
"hishtory SPACE tquery ENTER" ,
"ls ENTER" ,
} )
out = strings . Split ( strings . TrimSpace ( strings . Split ( out , "hishtory tquery" ) [ 1 ] ) , "\n" ) [ 0 ]
2022-10-28 06:20:33 +02:00
expected := ` ls ~/ `
2022-10-18 06:58:56 +02:00
if diff := cmp . Diff ( expected , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s" , diff )
}
2022-11-03 02:50:27 +01:00
// Check the output when the size is adjusted
out = captureTerminalOutputWithShellNameAndDimensions ( t , tester , tester . ShellName ( ) , 100 , 20 , [ ] string {
"hishtory SPACE tquery ENTER" ,
} )
out = strings . TrimSpace ( strings . Split ( out , "hishtory tquery" ) [ 1 ] )
compareGoldens ( t , out , "TestTui-SmallTerminal" )
2022-10-22 09:17:21 +02:00
}
2022-10-18 06:58:56 +02:00
2022-10-22 09:17:21 +02:00
func captureTerminalOutput ( t * testing . T , tester shellTester , commands [ ] string ) string {
2022-10-22 20:42:37 +02:00
return captureTerminalOutputWithShellName ( t , tester , tester . ShellName ( ) , commands )
}
func captureTerminalOutputWithShellName ( t * testing . T , tester shellTester , overriddenShellName string , commands [ ] string ) string {
2022-11-03 02:50:27 +01:00
return captureTerminalOutputWithShellNameAndDimensions ( t , tester , overriddenShellName , 200 , 50 , commands )
}
func captureTerminalOutputWithShellNameAndDimensions ( t * testing . T , tester shellTester , overriddenShellName string , width , height int , commands [ ] string ) string {
2022-10-22 20:42:37 +02:00
sleepAmount := "0.1"
2022-10-23 09:58:08 +02:00
if runtime . GOOS == "linux" {
sleepAmount = "0.2"
}
2022-10-22 20:42:37 +02:00
if overriddenShellName == "fish" {
// Fish is considerably slower so this is sadly necessary
sleepAmount = "0.5"
}
2022-10-22 09:17:21 +02:00
fullCommand := ""
fullCommand += " tmux kill-session -t foo || true\n"
2022-11-03 02:50:27 +01:00
fullCommand += fmt . Sprintf ( " tmux -u new-session -d -x %d -y %d -s foo %s\n" , width , height , overriddenShellName )
2022-10-23 23:58:47 +02:00
fullCommand += " sleep 1\n"
2022-10-22 20:42:37 +02:00
if overriddenShellName == "bash" {
2022-10-22 09:17:21 +02:00
fullCommand += " tmux send -t foo source SPACE ~/.bashrc ENTER\n"
}
2022-10-22 20:42:37 +02:00
fullCommand += " sleep " + sleepAmount + "\n"
2022-10-22 09:17:21 +02:00
for _ , cmd := range commands {
2022-10-28 07:20:53 +02:00
fullCommand += " tmux send -t foo -- "
2022-10-22 09:17:21 +02:00
fullCommand += cmd
fullCommand += "\n"
2022-10-22 20:42:37 +02:00
fullCommand += " sleep " + sleepAmount + "\n"
2022-10-22 09:17:21 +02:00
}
2022-11-05 03:56:22 +01:00
fullCommand += " sleep 0.5\n"
2022-10-22 09:17:21 +02:00
fullCommand += " tmux capture-pane -p\n"
fullCommand += " tmux kill-session -t foo\n"
2022-10-22 20:42:37 +02:00
hctx . GetLogger ( ) . Printf ( "Running tmux command: " + fullCommand )
2022-10-22 09:17:21 +02:00
return strings . TrimSpace ( tester . RunInteractiveShell ( t , fullCommand ) )
2022-10-21 09:14:28 +02:00
}
2022-11-03 21:16:45 +01:00
func testControlR ( t * testing . T , tester shellTester , shellName string , onlineStatus OnlineStatus ) {
2022-11-04 05:27:01 +01:00
if os . Getenv ( "GITHUB_ACTION" ) != "" && runtime . GOOS == "darwin" {
t . Skip ( ) // See the mysterious failure here: https://github.com/ddworken/hishtory/actions/runs/3390515329/jobs/5634750567
}
2022-10-21 09:14:28 +02:00
// Setup
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-11-03 21:16:45 +01:00
initFromOnlineStatus ( t , tester , onlineStatus )
assertOnlineStatus ( t , onlineStatus )
2022-10-21 09:14:28 +02:00
// Disable recording so that all our testing commands don't get recorded
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` hishtory disable ` )
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` hishtory config-set enable-control-r true ` )
// Insert a few hishtory entries
db := hctx . GetDb ( hctx . MakeContext ( ) )
2022-10-28 06:53:47 +02:00
e1 := testutils . MakeFakeHistoryEntry ( "ls ~/" )
2022-10-21 09:14:28 +02:00
e1 . CurrentWorkingDirectory = "/etc/"
e1 . Hostname = "server"
e1 . ExitCode = 127
db . Create ( e1 )
2022-10-28 06:53:47 +02:00
db . Create ( testutils . MakeFakeHistoryEntry ( "ls ~/foo/" ) )
db . Create ( testutils . MakeFakeHistoryEntry ( "ls ~/bar/" ) )
db . Create ( testutils . MakeFakeHistoryEntry ( "echo 'aaaaaa bbbb'" ) )
db . Create ( testutils . MakeFakeHistoryEntry ( "echo 'bar' &" ) )
2022-10-21 09:14:28 +02:00
// And check that the control-r binding brings up the search
2022-10-22 20:42:37 +02:00
out := captureTerminalOutputWithShellName ( t , tester , shellName , [ ] string { "C-R" } )
2022-10-21 09:14:28 +02:00
if ! strings . Contains ( out , "\n\n\n" ) {
t . Fatalf ( "failed to find separator in %#v" , out )
}
2022-10-28 06:20:33 +02:00
out = strings . TrimSpace ( strings . Split ( out , "\n\n\n" ) [ 1 ] )
compareGoldens ( t , out , "testControlR-Initial" )
2022-10-22 09:27:53 +02:00
// And check that we can scroll down and select an option
2022-10-28 06:20:33 +02:00
out = captureTerminalOutputWithShellName ( t , tester , shellName , [ ] string { "C-R" , "Down Down" , "Enter" } )
2022-10-22 09:27:53 +02:00
if ! strings . HasSuffix ( out , " ls ~/bar/" ) {
t . Fatalf ( "hishtory tquery returned the wrong result, out=%#v" , out )
}
2022-10-28 06:20:33 +02:00
// And that the above works, but also with an ENTER to actually execute the selected command
2022-10-22 20:42:37 +02:00
out = captureTerminalOutputWithShellName ( t , tester , shellName , [ ] string { "C-R" , "Down" , "Enter" , "Enter" } )
2022-10-22 09:27:53 +02:00
if ! strings . Contains ( out , "echo 'aaaaaa bbbb'\naaaaaa bbbb\n" ) {
t . Fatalf ( "hishtory tquery executed the wrong result, out=%#v" , out )
2022-10-21 09:14:28 +02:00
}
2022-10-22 09:34:19 +02:00
// Search for something more specific and select it
2022-10-22 20:42:37 +02:00
out = captureTerminalOutputWithShellName ( t , tester , shellName , [ ] string { "C-R" , "foo" , "Enter" } )
2022-10-22 09:34:19 +02:00
if ! strings . HasSuffix ( out , " ls ~/foo/" ) {
t . Fatalf ( "hishtory tquery returned the wrong result, out=%#v" , out )
}
// Search for something more specific, and then unsearch, and then search for something else
2022-10-22 20:42:37 +02:00
out = captureTerminalOutputWithShellName ( t , tester , shellName , [ ] string { "C-R" , "fo" , "BSpace BSpace" , "bar" , "Down" , "Enter" } )
2022-10-22 09:34:19 +02:00
if ! strings . HasSuffix ( out , " ls ~/bar/" ) {
t . Fatalf ( "hishtory tquery returned the wrong result, out=%#v" , out )
}
2022-10-22 09:50:34 +02:00
// Search using an atom
2022-10-22 20:58:59 +02:00
out = captureTerminalOutputWithShellName ( t , tester , shellName , [ ] string { "C-R" , "fo" , "BSpace BSpace" , "exit_code:2" , "Enter" } )
if ! strings . HasSuffix ( out , " echo 'bar' &" ) {
t . Fatalf ( "hishtory tquery returned the wrong result, out=%#v" , out )
2022-10-22 09:50:34 +02:00
}
// Search and check that the table is updated
2022-10-22 20:42:37 +02:00
out = captureTerminalOutputWithShellName ( t , tester , shellName , [ ] string { "C-R" , "echo" } )
2022-10-28 07:28:43 +02:00
if strings . Contains ( out , "\n\n\n" ) {
2022-10-28 06:20:33 +02:00
out = strings . TrimSpace ( strings . Split ( out , "\n\n\n" ) [ 1 ] )
2022-10-22 09:50:34 +02:00
}
2022-10-28 06:20:33 +02:00
compareGoldens ( t , out , "testControlR-Search" )
2022-10-23 09:53:47 +02:00
2022-10-28 07:07:11 +02:00
// An advanced search and check that the table is updated
out = captureTerminalOutputWithShellName ( t , tester , shellName , [ ] string { "C-R" , "cwd:/tmp/ SPACE ls" } )
2022-10-28 07:28:43 +02:00
if strings . Contains ( out , "\n\n\n" ) {
2022-10-28 07:07:11 +02:00
out = strings . TrimSpace ( strings . Split ( out , "\n\n\n" ) [ 1 ] )
}
compareGoldens ( t , out , "testControlR-AdvancedSearch" )
2022-10-28 07:11:57 +02:00
// Set some different columns to be displayed and check that the table displays those
tester . RunInteractiveShell ( t , ` hishtory config-set displayed-columns Hostname 'Exit Code' Command ` )
out = captureTerminalOutputWithShellName ( t , tester , shellName , [ ] string { "C-R" } )
2022-10-28 07:28:43 +02:00
if strings . Contains ( out , "\n\n\n" ) {
2022-10-28 07:11:57 +02:00
out = strings . TrimSpace ( strings . Split ( out , "\n\n\n" ) [ 1 ] )
}
2022-10-28 07:20:53 +02:00
compareGoldens ( t , out , "testControlR-displayedColumns" )
// Add a custom column
tester . RunInteractiveShell ( t , ` hishtory config-add custom-columns foo "echo foo" ` )
tester . RunInteractiveShell ( t , ` hishtory enable ` )
tester . RunInteractiveShell ( t , ` ls / ` )
tester . RunInteractiveShell ( t , ` hishtory disable ` )
// And run a query and confirm it is displayed
tester . RunInteractiveShell ( t , ` hishtory config-add displayed-columns foo ` )
out = captureTerminalOutputWithShellName ( t , tester , shellName , [ ] string { "C-R" , "-pipefail" } )
out = strings . TrimSpace ( out )
if tester . ShellName ( ) == "bash" {
if ! strings . Contains ( out , "\n\n\n" ) {
t . Fatalf ( "failed to find separator in %#v" , out )
}
out = strings . TrimSpace ( strings . Split ( out , "\n\n\n" ) [ 1 ] )
}
compareGoldens ( t , out , "testControlR-customColumn" )
2022-10-28 07:11:57 +02:00
2022-10-23 09:53:47 +02:00
// Disable control-r
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` hishtory config-set enable-control-r false ` )
// And it shouldn't pop up
out = captureTerminalOutputWithShellName ( t , tester , shellName , [ ] string { "C-R" } )
if strings . Contains ( out , "Search Query" ) || strings . Contains ( out , "─────" ) || strings . Contains ( out , "Exit Code" ) {
t . Fatalf ( "hishtory overrode control-r even when this was disabled? out=%#v" , out )
}
2022-10-30 02:53:40 +02:00
if os . Getenv ( "GITHUB_ACTION" ) == "" {
// This bit is broken on actions since actions run as a different user
compareGoldens ( t , out , "testControlR-" + shellName + "-Disabled" )
}
2022-10-18 06:58:56 +02:00
}
2022-10-31 01:55:48 +01:00
func testCustomColumns ( t * testing . T , tester shellTester ) {
// Setup
defer testutils . BackupAndRestore ( t ) ( )
installHishtory ( t , tester , "" )
// Record a few commands with no custom columns
out := tester . RunInteractiveShell ( t , ` export FOOBAR = ' hello '
echo $ FOOBAR world
cd /
echo baz ` )
if out != "hello world\nbaz\n" {
t . Fatalf ( "unexpected command output=%#v" , out )
}
// Check that the hishtory is saved correctly
out = tester . RunInteractiveShell ( t , ` hishtory export | grep -v pipefail ` )
compareGoldens ( t , out , "testCustomColumns-initHistory" )
// Configure a custom column
tester . RunInteractiveShell ( t , ` hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || true' ` )
// Run a few commands, some of which will have a git_remote
out = tester . RunInteractiveShell ( t , ` echo foo
cd /
echo bar ` )
if out != "foo\nbar\n" {
t . Fatalf ( "unexpected command output=%#v" , out )
}
// And check that it is all recorded correctly
tester . RunInteractiveShell ( t , ` hishtory config-set displayed-columns 'Exit Code' git_remote Command ` )
out = tester . RunInteractiveShell ( t , ` hishtory query -pipefail ` )
2022-11-01 19:03:49 +01:00
compareGoldens ( t , out , fmt . Sprintf ( "testCustomColumns-query-isAction=%v" , ( os . Getenv ( "GITHUB_ACTION" ) != "" ) ) )
2022-10-31 01:55:48 +01:00
out = captureTerminalOutput ( t , tester , [ ] string { "hishtory SPACE tquery SPACE -pipefail ENTER" } )
2022-11-04 05:27:01 +01:00
testName := "testCustomColumns-tquery-" + tester . ShellName ( )
if os . Getenv ( "GITHUB_ACTION" ) != "" {
testName += "-isAction"
testName += "-" + runtime . GOOS
}
compareGoldens ( t , out , testName )
2022-10-31 01:55:48 +01:00
}
2022-11-03 03:41:49 +01:00
func testUninstall ( t * testing . T , tester shellTester ) {
// Setup
defer testutils . BackupAndRestore ( t ) ( )
installHishtory ( t , tester , "" )
// Record a few commands and check that they get recorded
tester . RunInteractiveShell ( t , ` echo foo
echo baz ` )
out := tester . RunInteractiveShell ( t , ` hishtory export -pipefail ` )
compareGoldens ( t , out , "testUninstall-recorded" )
// And then uninstall
out , err := tester . RunInteractiveShellRelaxed ( t , ` yes | hishtory uninstall ` )
testutils . Check ( t , err )
compareGoldens ( t , out , "testUninstall-uninstall" )
// And check that hishtory has been uninstalled
out , err = tester . RunInteractiveShellRelaxed ( t , ` echo foo
hishtory
echo bar ` )
testutils . Check ( t , err )
compareGoldens ( t , out , "testUninstall-post-uninstall" )
// And check again, but in a way that shows the full terminal output
if os . Getenv ( "GITHUB_ACTION" ) == "" {
out = captureTerminalOutput ( t , tester , [ ] string {
"echo SPACE foo ENTER" ,
"hishtory ENTER" ,
"echo SPACE bar ENTER" ,
} )
compareGoldens ( t , out , "testUninstall-post-uninstall-" + tester . ShellName ( ) )
}
}
2022-11-04 04:36:36 +01:00
func TestRemoveDuplicateRows ( t * testing . T ) {
// Setup
2022-11-04 05:16:53 +01:00
tester := zshTester { }
2022-11-04 04:36:36 +01:00
defer testutils . BackupAndRestore ( t ) ( )
installHishtory ( t , tester , "" )
// Record a few commands and check that they get recorded and all are displayed in a table
tester . RunInteractiveShell ( t , ` echo foo
echo foo
echo baz
echo baz
echo foo ` )
out := tester . RunInteractiveShell ( t , ` hishtory export -pipefail ` )
compareGoldens ( t , out , "testRemoveDuplicateRows-export" )
tester . RunInteractiveShell ( t , ` hishtory config-set displayed-columns 'Exit Code' Command ` )
out = tester . RunInteractiveShell ( t , ` hishtory query -pipefail ` )
compareGoldens ( t , out , "testRemoveDuplicateRows-query" )
out = captureTerminalOutput ( t , tester , [ ] string { "hishtory SPACE tquery SPACE -pipefail ENTER" } )
out = strings . TrimSpace ( strings . Split ( out , "hishtory tquery" ) [ 1 ] )
compareGoldens ( t , out , "testRemoveDuplicateRows-tquery" )
// And change the config to filter out duplicate rows
tester . RunInteractiveShell ( t , ` hishtory config-set filter-duplicate-commands true ` )
out = tester . RunInteractiveShell ( t , ` hishtory export -pipefail ` )
compareGoldens ( t , out , "testRemoveDuplicateRows-enabled-export" )
out = tester . RunInteractiveShell ( t , ` hishtory query -pipefail ` )
compareGoldens ( t , out , "testRemoveDuplicateRows-enabled-query" )
out = captureTerminalOutput ( t , tester , [ ] string { "hishtory SPACE tquery SPACE -pipefail ENTER" } )
out = strings . TrimSpace ( strings . Split ( out , "hishtory tquery" ) [ 1 ] )
compareGoldens ( t , out , "testRemoveDuplicateRows-enabled-tquery" )
}
2022-09-23 06:45:20 +02:00
type deviceSet struct {
deviceMap * map [ device ] deviceOp
currentDevice * device
}
type device struct {
key string
deviceId string
}
type deviceOp struct {
backup func ( )
restore func ( )
}
func createDevice ( t * testing . T , tester shellTester , devices * deviceSet , key , deviceId string ) {
hctx . GetLogger ( ) . Printf ( "devices=%#v\n" , devices )
d := device { key , deviceId }
_ , ok := ( * devices . deviceMap ) [ d ]
if ok {
t . Fatal ( fmt . Errorf ( "cannot create device twice for key=%s deviceId=%s" , key , deviceId ) )
}
installHishtory ( t , tester , key )
( * devices . deviceMap ) [ d ] = deviceOp {
2022-10-28 06:53:47 +02:00
backup : func ( ) { testutils . BackupAndRestoreWithId ( t , key + deviceId ) } ,
restore : testutils . BackupAndRestoreWithId ( t , key + deviceId ) ,
2022-09-23 06:45:20 +02:00
}
}
func switchToDevice ( devices * deviceSet , d device ) {
2022-09-27 07:55:59 +02:00
if devices . currentDevice != nil && d == * devices . currentDevice {
return
}
2022-09-23 06:45:20 +02:00
if devices . currentDevice != nil {
( * devices . deviceMap ) [ * devices . currentDevice ] . backup ( )
}
devices . currentDevice = & d
( * devices . deviceMap ) [ d ] . restore ( )
}
func testMultipleUsers ( t * testing . T , tester shellTester ) {
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-09-23 06:45:20 +02:00
// Create all our devices
var deviceMap map [ device ] deviceOp = make ( map [ device ] deviceOp )
var devices deviceSet = deviceSet { }
devices . deviceMap = & deviceMap
devices . currentDevice = nil
u1d1 := device { key : "user1" , deviceId : "1" }
createDevice ( t , tester , & devices , u1d1 . key , u1d1 . deviceId )
u1d2 := device { key : "user1" , deviceId : "2" }
createDevice ( t , tester , & devices , u1d2 . key , u1d2 . deviceId )
u2d1 := device { key : "user2" , deviceId : "1" }
createDevice ( t , tester , & devices , u2d1 . key , u2d1 . deviceId )
u2d2 := device { key : "user2" , deviceId : "2" }
createDevice ( t , tester , & devices , u2d2 . key , u2d2 . deviceId )
u2d3 := device { key : "user2" , deviceId : "3" }
createDevice ( t , tester , & devices , u2d3 . key , u2d3 . deviceId )
// Run commands on user1
switchToDevice ( & devices , u1d1 )
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` echo u1d1 ` )
switchToDevice ( & devices , u1d2 )
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` echo u1d2 ` )
// Run commands on user2
switchToDevice ( & devices , u2d1 )
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` echo u2d1 ` )
switchToDevice ( & devices , u2d2 )
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` echo u2d2 ` )
switchToDevice ( & devices , u2d3 )
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` echo u2d3 ` )
// Run more commands on user1
switchToDevice ( & devices , u1d1 )
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` echo u1d1-b ` )
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` echo u1d1-c ` )
switchToDevice ( & devices , u1d2 )
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` echo u1d2-b ` )
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` echo u1d2-c ` )
// Check that the right commands were recorded for user1
for i , d := range [ ] device { u1d1 , u1d2 } {
switchToDevice ( & devices , d )
out , err := tester . RunInteractiveShellRelaxed ( t , ` hishtory export ` )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-09-23 06:45:20 +02:00
expectedOutput := "echo u1d1\necho u1d2\necho u1d1-b\necho u1d1-c\necho u1d2-b\necho u1d2-c\n"
for j := 0 ; j < i ; j ++ {
expectedOutput += "hishtory export\n"
}
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
}
// Run more commands on user2
switchToDevice ( & devices , u2d1 )
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` echo u1d1-b ` )
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` echo u1d1-c ` )
switchToDevice ( & devices , u2d3 )
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` echo u2d3-b ` )
_ , _ = tester . RunInteractiveShellRelaxed ( t , ` echo u2d3-c ` )
// Check that the right commands were recorded for user2
for i , d := range [ ] device { u2d1 , u2d2 , u2d3 } {
switchToDevice ( & devices , d )
out , err := tester . RunInteractiveShellRelaxed ( t , ` hishtory export ` )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-09-23 06:45:20 +02:00
expectedOutput := "echo u2d1\necho u2d2\necho u2d3\necho u1d1-b\necho u1d1-c\necho u2d3-b\necho u2d3-c\n"
for j := 0 ; j < i ; j ++ {
expectedOutput += "hishtory export\n"
}
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch (-expected +got):\n%s\nout=%#v" , diff , out )
}
}
}
2022-09-23 08:06:28 +02:00
type operation struct {
2022-09-25 01:00:03 +02:00
device device
cmd string
redactQuery string
2022-09-23 08:06:28 +02:00
}
2022-09-23 08:12:41 +02:00
var tmp int = 0
var runCounter * int = & tmp
func fuzzTest ( t * testing . T , tester shellTester , input string ) {
* runCounter += 1
// Parse the input
if len ( input ) > 1_000 {
return
}
input = strings . TrimSpace ( input )
ops := make ( [ ] operation , 0 )
for _ , line := range strings . Split ( input , "\n" ) {
split1 := strings . SplitN ( line , "|" , 2 )
if len ( split1 ) != 2 {
2022-09-25 01:00:03 +02:00
panic ( "malformed: split1" )
2022-09-23 08:12:41 +02:00
}
split2 := strings . SplitN ( split1 [ 0 ] , ";" , 2 )
if len ( split2 ) != 2 {
2022-09-25 01:00:03 +02:00
panic ( "malformed: split2" )
}
thingToDo := split1 [ 1 ]
cmd := ""
redactQuery := ""
if strings . HasPrefix ( thingToDo , "!" ) {
redactQuery = thingToDo [ 1 : ]
} else {
cmd = "echo " + thingToDo
2022-09-23 08:12:41 +02:00
}
re := regexp . MustCompile ( ` [a-zA-Z]+ ` )
2022-09-25 01:00:03 +02:00
if ! re . MatchString ( cmd ) && cmd != "" {
panic ( "malformed: re" )
2022-09-23 08:12:41 +02:00
}
key := split2 [ 0 ]
if strings . Contains ( key , "-" ) {
2022-09-25 01:00:03 +02:00
panic ( "malformed: key-" )
2022-09-23 08:12:41 +02:00
}
2022-09-25 01:00:03 +02:00
op := operation { device : device { key : key + "-" + strconv . Itoa ( * runCounter ) , deviceId : split2 [ 1 ] } , cmd : cmd , redactQuery : redactQuery }
2022-09-23 08:12:41 +02:00
ops = append ( ops , op )
}
// Set up and create the devices
2022-10-28 06:53:47 +02:00
defer testutils . BackupAndRestore ( t ) ( )
2022-09-23 08:12:41 +02:00
var deviceMap map [ device ] deviceOp = make ( map [ device ] deviceOp )
var devices deviceSet = deviceSet { }
devices . deviceMap = & deviceMap
devices . currentDevice = nil
for _ , op := range ops {
_ , ok := ( * devices . deviceMap ) [ op . device ]
if ok {
continue
}
createDevice ( t , tester , & devices , op . device . key , op . device . deviceId )
}
// Persist our basic in-memory copy of expected shell commands
keyToCommands := make ( map [ string ] string )
// Run the commands
for _ , op := range ops {
// Run the command
switchToDevice ( & devices , op . device )
2022-09-25 01:00:03 +02:00
if op . cmd != "" {
_ , err := tester . RunInteractiveShellRelaxed ( t , op . cmd )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-09-25 01:00:03 +02:00
}
if op . redactQuery != "" {
_ , err := tester . RunInteractiveShellRelaxed ( t , ` hishtory redact --force ` + op . redactQuery )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-09-25 01:00:03 +02:00
}
2022-09-23 08:12:41 +02:00
// Calculate the expected output of hishtory export
val , ok := keyToCommands [ op . device . key ]
if ! ok {
val = ""
}
2022-09-25 01:00:03 +02:00
if op . cmd != "" {
val += op . cmd
val += "\n"
}
if op . redactQuery != "" {
lines := strings . Split ( val , "\n" )
filteredLines := make ( [ ] string , 0 )
for _ , line := range lines {
if strings . Contains ( line , op . redactQuery ) {
continue
}
filteredLines = append ( filteredLines , line )
}
val = strings . Join ( filteredLines , "\n" )
val += ` hishtory redact --force ` + op . redactQuery + "\n"
}
2022-09-23 08:12:41 +02:00
keyToCommands [ op . device . key ] = val
// Run hishtory export and check the output
out , err := tester . RunInteractiveShellRelaxed ( t , ` hishtory export | grep -v export ` )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-09-23 08:12:41 +02:00
expectedOutput := keyToCommands [ op . device . key ]
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch for input=%#v key=%s (-expected +got):\n%s\nout=%#v" , input , op . device . key , diff , out )
}
}
// Check that hishtory export has the expected results
for _ , op := range ops {
switchToDevice ( & devices , op . device )
out , err := tester . RunInteractiveShellRelaxed ( t , ` hishtory export | grep -v export ` )
2022-10-28 06:53:47 +02:00
testutils . Check ( t , err )
2022-09-23 08:12:41 +02:00
expectedOutput := keyToCommands [ op . device . key ]
if diff := cmp . Diff ( expectedOutput , out ) ; diff != "" {
t . Fatalf ( "hishtory export mismatch for key=%s (-expected +got):\n%s\nout=%#v" , op . device . key , diff , out )
}
}
}
2022-09-23 08:06:28 +02:00
func FuzzTestMultipleUsers ( f * testing . F ) {
2022-09-28 07:03:37 +02:00
if skipSlowTests ( ) {
f . Skip ( "skipping slow tests" )
}
2022-09-25 01:00:03 +02:00
// Format:
// $Op = $Key;$Device|$Command\n
// $Key;$Device|$Command\n$Op
// $Command = !$ThingToRedact
// $CommandToRun
//
// Running repeated commands
2022-09-23 08:06:28 +02:00
f . Add ( "a;b|2\n" )
f . Add ( "a;b|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" )
f . Add ( "a;b|aaaBBcccDD\n" )
f . Add ( "a;a|hello\na;a|world" )
f . Add ( "a;a|hello\na;a|world\na;b|3" )
f . Add ( "a;a|1\na;a|2\na;b|3\nb;a|4\na;b|5" )
f . Add ( "a;a|1\na;a|2\na;b|1\n" )
f . Add ( "a;a|1\na;a|2\na;b|1\nz;z|1\na;a|1\n" )
f . Add ( "a;a|hello\na;a|wobld" )
2022-09-23 08:12:41 +02:00
f . Add ( "a;a|hello\na;a|hello" )
2022-09-23 08:06:28 +02:00
f . Add ( "a;a|1\nb;a|2\nc;a|2\nd;a|2\na;b|2\na;b|3\na;b|4\na;b|8\na;d|2\nb;a|1" )
f . Add ( "a;a|1\na;b|1\na;c|1\na;d|1\na;e|1\na;f|1\na;g|1\na;b|1\na;b|1\na;b|1\na;b|1" )
2022-09-23 08:25:06 +02:00
f . Add ( "a;a|1\nb;b|1\na;c|1\na;d|1\na;e|1\na;f|1\na;g|1\na;b|1\na;b|1\na;b|1\na;b|1" )
f . Add ( "a;a|1\na;a|1\na;c|1\na;d|1\na;e|1\na;f|1\na;g|1\na;b|1\na;b|1\na;b|1\na;b|1" )
2022-09-25 01:00:03 +02:00
// Running repeated commands with redaction
f . Add ( "a;b|!hello\n" )
f . Add ( "a;b|hello\na;b|world\na;b|!hello\n" )
f . Add ( "a;b|hello\na;b|world\na;a|hello2\na;b|!hello\na;b|hello3\na;b|hello4\n" )
f . Add ( "a;b|hello\na;b|world\na;a|hello2\na;b|!h\na;b|!h\na;b|hello3\na;b|hello4\n" )
2022-09-23 08:06:28 +02:00
f . Fuzz ( func ( t * testing . T , input string ) {
2022-09-23 08:12:41 +02:00
fuzzTest ( t , bashTester { } , input )
fuzzTest ( t , zshTester { } , input )
2022-09-23 08:06:28 +02:00
} )
}
2022-10-19 04:55:41 +02:00
// TODO: somehow test/confirm that hishtory works even if only bash/only zsh is installed