mirror of
https://github.com/ddworken/hishtory.git
synced 2025-06-20 11:57:50 +02:00
Merge branch 'panic-handling' into sergio/panic-guard
This commit is contained in:
commit
2f5288f832
@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ddworken/hishtory/shared"
|
"github.com/ddworken/hishtory/shared"
|
||||||
"github.com/jackc/pgx/v4/stdlib"
|
"github.com/jackc/pgx/v4/stdlib"
|
||||||
@ -50,14 +52,13 @@ func (db *DB) AddDatabaseTables() error {
|
|||||||
&shared.DumpRequest{},
|
&shared.DumpRequest{},
|
||||||
&shared.DeletionRequest{},
|
&shared.DeletionRequest{},
|
||||||
&shared.Feedback{},
|
&shared.Feedback{},
|
||||||
|
&ActiveUserStats{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, model := range models {
|
for _, model := range models {
|
||||||
fmt.Printf("Beginning migration of %#v\n", model)
|
|
||||||
if err := db.AutoMigrate(model); err != nil {
|
if err := db.AutoMigrate(model); err != nil {
|
||||||
return fmt.Errorf("db.AutoMigrate: %w", err)
|
return fmt.Errorf("db.AutoMigrate: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Done migrating %#v\n", model)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -67,15 +68,25 @@ func (db *DB) CreateIndices() error {
|
|||||||
// Note: If adding a new index here, consider manually running it on the prod DB using CONCURRENTLY to
|
// Note: If adding a new index here, consider manually running it on the prod DB using CONCURRENTLY to
|
||||||
// make server startup non-blocking. The benefit of this function is primarily for other people so they
|
// make server startup non-blocking. The benefit of this function is primarily for other people so they
|
||||||
// don't have to manually create these indexes.
|
// don't have to manually create these indexes.
|
||||||
var indices = []string{
|
indices := []struct {
|
||||||
`CREATE INDEX IF NOT EXISTS entry_id_idx ON enc_history_entries USING btree(encrypted_id);`,
|
name string
|
||||||
`CREATE INDEX IF NOT EXISTS device_id_idx ON enc_history_entries USING btree(device_id);`,
|
table string
|
||||||
`CREATE INDEX IF NOT EXISTS read_count_idx ON enc_history_entries USING btree(read_count);`,
|
columns []string
|
||||||
`CREATE INDEX IF NOT EXISTS redact_idx ON enc_history_entries USING btree(user_id, device_id, date);`,
|
}{
|
||||||
`CREATE INDEX IF NOT EXISTS del_user_idx ON deletion_requests USING btree(user_id);`,
|
{"entry_id_idx", "enc_history_entries", []string{"encrypted_id"}},
|
||||||
|
{"device_id_idx", "enc_history_entries", []string{"device_id"}},
|
||||||
|
{"read_count_idx", "enc_history_entries", []string{"read_count"}},
|
||||||
|
{"redact_idx", "enc_history_entries", []string{"user_id", "device_id", "date"}},
|
||||||
|
{"del_user_idx", "deletion_requests", []string{"user_id"}},
|
||||||
}
|
}
|
||||||
for _, index := range indices {
|
for _, index := range indices {
|
||||||
r := db.Exec(index)
|
sql := ""
|
||||||
|
if db.Name() == "sqlite" {
|
||||||
|
sql = fmt.Sprintf("CREATE INDEX IF NOT EXISTS %s ON %s (%s)", index.name, index.table, strings.Join(index.columns, ","))
|
||||||
|
} else {
|
||||||
|
sql = fmt.Sprintf("CREATE INDEX IF NOT EXISTS %s ON %s USING btree(%s)", index.name, index.table, strings.Join(index.columns, ","))
|
||||||
|
}
|
||||||
|
r := db.Exec(sql)
|
||||||
if r.Error != nil {
|
if r.Error != nil {
|
||||||
return fmt.Errorf("failed to execute index creation sql=%#v: %w", index, r.Error)
|
return fmt.Errorf("failed to execute index creation sql=%#v: %w", index, r.Error)
|
||||||
}
|
}
|
||||||
@ -255,6 +266,79 @@ func (db *DB) Clean(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractInt64FromRow(row *sql.Row) (int64, error) {
|
||||||
|
var ret int64
|
||||||
|
err := row.Scan(&ret)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("extractInt64FromRow: %w", err)
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveUserStats struct {
|
||||||
|
Date time.Time
|
||||||
|
TotalNumDevices int64
|
||||||
|
TotalNumUsers int64
|
||||||
|
DailyActiveSubmitUsers int64
|
||||||
|
DailyActiveQueryUsers int64
|
||||||
|
WeeklyActiveSubmitUsers int64
|
||||||
|
WeeklyActiveQueryUsers int64
|
||||||
|
DailyInstalls int64
|
||||||
|
DailyUninstalls int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GenerateAndStoreActiveUserStats(ctx context.Context) error {
|
||||||
|
if db.DB.Name() == "sqlite" {
|
||||||
|
// Not supported on sqlite
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
totalNumDevices, err := extractInt64FromRow(db.WithContext(ctx).Raw("SELECT COUNT(DISTINCT devices.device_id) FROM devices").Row())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
totalNumUsers, err := extractInt64FromRow(db.WithContext(ctx).Raw("SELECT COUNT(DISTINCT devices.user_id) FROM devices").Row())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dauSubmit, err := extractInt64FromRow(db.WithContext(ctx).Raw("SELECT COUNT(DISTINCT user_id) FROM usage_data WHERE last_used > (now()::date-1)::timestamp").Row())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dauQuery, err := extractInt64FromRow(db.WithContext(ctx).Raw("SELECT COUNT(DISTINCT user_id) FROM usage_data WHERE last_queried > (now()::date-1)::timestamp").Row())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
wauSubmit, err := extractInt64FromRow(db.WithContext(ctx).Raw("SELECT COUNT(DISTINCT user_id) FROM usage_data WHERE last_used > (now()::date-7)::timestamp").Row())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
wauQuery, err := extractInt64FromRow(db.WithContext(ctx).Raw("SELECT COUNT(DISTINCT user_id) FROM usage_data WHERE last_queried > (now()::date-7)::timestamp").Row())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dailyInstalls, err := extractInt64FromRow(db.WithContext(ctx).Raw("SELECT count(distinct device_id) FROM devices WHERE registration_date > (now()::date-1)::timestamp").Row())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dailyUninstalls, err := extractInt64FromRow(db.WithContext(ctx).Raw("SELECT COUNT(*) FROM feedbacks WHERE date > (now()::date-1)::timestamp").Row())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Create(ActiveUserStats{
|
||||||
|
Date: time.Now(),
|
||||||
|
TotalNumDevices: totalNumDevices,
|
||||||
|
TotalNumUsers: totalNumUsers,
|
||||||
|
DailyActiveSubmitUsers: dauSubmit,
|
||||||
|
DailyActiveQueryUsers: dauQuery,
|
||||||
|
WeeklyActiveSubmitUsers: wauSubmit,
|
||||||
|
WeeklyActiveQueryUsers: wauQuery,
|
||||||
|
DailyInstalls: dailyInstalls,
|
||||||
|
DailyUninstalls: dailyUninstalls,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (db *DB) DeepClean(ctx context.Context) error {
|
func (db *DB) DeepClean(ctx context.Context) error {
|
||||||
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
r := tx.Exec(`
|
r := tx.Exec(`
|
||||||
|
@ -74,9 +74,9 @@ func TestESubmitThenQuery(t *testing.T) {
|
|||||||
// Submit a few entries for different devices
|
// Submit a few entries for different devices
|
||||||
entry := testutils.MakeFakeHistoryEntry("ls ~/")
|
entry := testutils.MakeFakeHistoryEntry("ls ~/")
|
||||||
encEntry, err := data.EncryptHistoryEntry("key", entry)
|
encEntry, err := data.EncryptHistoryEntry("key", entry)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
reqBody, err := json.Marshal([]shared.EncHistoryEntry{encEntry})
|
reqBody, err := json.Marshal([]shared.EncHistoryEntry{encEntry})
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
submitReq := httptest.NewRequest(http.MethodPost, "/?source_device_id="+devId1, bytes.NewReader(reqBody))
|
submitReq := httptest.NewRequest(http.MethodPost, "/?source_device_id="+devId1, bytes.NewReader(reqBody))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
s.apiSubmitHandler(w, submitReq)
|
s.apiSubmitHandler(w, submitReq)
|
||||||
@ -92,16 +92,16 @@ func TestESubmitThenQuery(t *testing.T) {
|
|||||||
res := w.Result()
|
res := w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err := io.ReadAll(res.Body)
|
respBody, err := io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
var retrievedEntries []*shared.EncHistoryEntry
|
var retrievedEntries []*shared.EncHistoryEntry
|
||||||
testutils.Check(t, json.Unmarshal(respBody, &retrievedEntries))
|
require.NoError(t, json.Unmarshal(respBody, &retrievedEntries))
|
||||||
require.Equal(t, 1, len(retrievedEntries))
|
require.Equal(t, 1, len(retrievedEntries))
|
||||||
dbEntry := retrievedEntries[0]
|
dbEntry := retrievedEntries[0]
|
||||||
require.Equal(t, devId1, dbEntry.DeviceId)
|
require.Equal(t, devId1, dbEntry.DeviceId)
|
||||||
require.Equal(t, data.UserId("key"), dbEntry.UserId)
|
require.Equal(t, data.UserId("key"), dbEntry.UserId)
|
||||||
require.Equal(t, 0, dbEntry.ReadCount)
|
require.Equal(t, 0, dbEntry.ReadCount)
|
||||||
decEntry, err := data.DecryptHistoryEntry("key", *dbEntry)
|
decEntry, err := data.DecryptHistoryEntry("key", *dbEntry)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, data.EntryEquals(decEntry, entry))
|
require.True(t, data.EntryEquals(decEntry, entry))
|
||||||
|
|
||||||
// Same for device id 2
|
// Same for device id 2
|
||||||
@ -111,8 +111,8 @@ func TestESubmitThenQuery(t *testing.T) {
|
|||||||
res = w.Result()
|
res = w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err = io.ReadAll(res.Body)
|
respBody, err = io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
testutils.Check(t, json.Unmarshal(respBody, &retrievedEntries))
|
require.NoError(t, json.Unmarshal(respBody, &retrievedEntries))
|
||||||
if len(retrievedEntries) != 1 {
|
if len(retrievedEntries) != 1 {
|
||||||
t.Fatalf("Expected to retrieve 1 entry, found %d", len(retrievedEntries))
|
t.Fatalf("Expected to retrieve 1 entry, found %d", len(retrievedEntries))
|
||||||
}
|
}
|
||||||
@ -127,7 +127,7 @@ func TestESubmitThenQuery(t *testing.T) {
|
|||||||
t.Fatalf("db.ReadCount should have been 1, was %v", dbEntry.ReadCount)
|
t.Fatalf("db.ReadCount should have been 1, was %v", dbEntry.ReadCount)
|
||||||
}
|
}
|
||||||
decEntry, err = data.DecryptHistoryEntry("key", *dbEntry)
|
decEntry, err = data.DecryptHistoryEntry("key", *dbEntry)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if !data.EntryEquals(decEntry, entry) {
|
if !data.EntryEquals(decEntry, entry) {
|
||||||
t.Fatalf("DB data is different than input! \ndb =%#v\ninput=%#v", *dbEntry, entry)
|
t.Fatalf("DB data is different than input! \ndb =%#v\ninput=%#v", *dbEntry, entry)
|
||||||
}
|
}
|
||||||
@ -139,8 +139,8 @@ func TestESubmitThenQuery(t *testing.T) {
|
|||||||
res = w.Result()
|
res = w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err = io.ReadAll(res.Body)
|
respBody, err = io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
testutils.Check(t, json.Unmarshal(respBody, &retrievedEntries))
|
require.NoError(t, json.Unmarshal(respBody, &retrievedEntries))
|
||||||
if len(retrievedEntries) != 2 {
|
if len(retrievedEntries) != 2 {
|
||||||
t.Fatalf("Expected to retrieve 2 entries, found %d", len(retrievedEntries))
|
t.Fatalf("Expected to retrieve 2 entries, found %d", len(retrievedEntries))
|
||||||
}
|
}
|
||||||
@ -175,9 +175,9 @@ func TestDumpRequestAndResponse(t *testing.T) {
|
|||||||
res := w.Result()
|
res := w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err := io.ReadAll(res.Body)
|
respBody, err := io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
var dumpRequests []*shared.DumpRequest
|
var dumpRequests []*shared.DumpRequest
|
||||||
testutils.Check(t, json.Unmarshal(respBody, &dumpRequests))
|
require.NoError(t, json.Unmarshal(respBody, &dumpRequests))
|
||||||
if len(dumpRequests) != 1 {
|
if len(dumpRequests) != 1 {
|
||||||
t.Fatalf("expected one pending dump request, got %#v", dumpRequests)
|
t.Fatalf("expected one pending dump request, got %#v", dumpRequests)
|
||||||
}
|
}
|
||||||
@ -195,9 +195,9 @@ func TestDumpRequestAndResponse(t *testing.T) {
|
|||||||
res = w.Result()
|
res = w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err = io.ReadAll(res.Body)
|
respBody, err = io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
dumpRequests = make([]*shared.DumpRequest, 0)
|
dumpRequests = make([]*shared.DumpRequest, 0)
|
||||||
testutils.Check(t, json.Unmarshal(respBody, &dumpRequests))
|
require.NoError(t, json.Unmarshal(respBody, &dumpRequests))
|
||||||
if len(dumpRequests) != 1 {
|
if len(dumpRequests) != 1 {
|
||||||
t.Fatalf("expected one pending dump request, got %#v", dumpRequests)
|
t.Fatalf("expected one pending dump request, got %#v", dumpRequests)
|
||||||
}
|
}
|
||||||
@ -215,7 +215,7 @@ func TestDumpRequestAndResponse(t *testing.T) {
|
|||||||
res = w.Result()
|
res = w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err = io.ReadAll(res.Body)
|
respBody, err = io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
resp := strings.TrimSpace(string(respBody))
|
resp := strings.TrimSpace(string(respBody))
|
||||||
require.Equalf(t, "[]", resp, "got unexpected respBody: %#v", string(resp))
|
require.Equalf(t, "[]", resp, "got unexpected respBody: %#v", string(resp))
|
||||||
|
|
||||||
@ -225,19 +225,19 @@ func TestDumpRequestAndResponse(t *testing.T) {
|
|||||||
res = w.Result()
|
res = w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err = io.ReadAll(res.Body)
|
respBody, err = io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
resp = strings.TrimSpace(string(respBody))
|
resp = strings.TrimSpace(string(respBody))
|
||||||
require.Equalf(t, "[]", resp, "got unexpected respBody: %#v", string(resp))
|
require.Equalf(t, "[]", resp, "got unexpected respBody: %#v", string(resp))
|
||||||
|
|
||||||
// Now submit a dump for userId
|
// Now submit a dump for userId
|
||||||
entry1Dec := testutils.MakeFakeHistoryEntry("ls ~/")
|
entry1Dec := testutils.MakeFakeHistoryEntry("ls ~/")
|
||||||
entry1, err := data.EncryptHistoryEntry("dkey", entry1Dec)
|
entry1, err := data.EncryptHistoryEntry("dkey", entry1Dec)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
entry2Dec := testutils.MakeFakeHistoryEntry("aaaaaaáaaa")
|
entry2Dec := testutils.MakeFakeHistoryEntry("aaaaaaáaaa")
|
||||||
entry2, err := data.EncryptHistoryEntry("dkey", entry1Dec)
|
entry2, err := data.EncryptHistoryEntry("dkey", entry1Dec)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
reqBody, err := json.Marshal([]shared.EncHistoryEntry{entry1, entry2})
|
reqBody, err := json.Marshal([]shared.EncHistoryEntry{entry1, entry2})
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
submitReq := httptest.NewRequest(http.MethodPost, "/?user_id="+userId+"&requesting_device_id="+devId2+"&source_device_id="+devId1, bytes.NewReader(reqBody))
|
submitReq := httptest.NewRequest(http.MethodPost, "/?user_id="+userId+"&requesting_device_id="+devId2+"&source_device_id="+devId1, bytes.NewReader(reqBody))
|
||||||
s.apiSubmitDumpHandler(httptest.NewRecorder(), submitReq)
|
s.apiSubmitDumpHandler(httptest.NewRecorder(), submitReq)
|
||||||
|
|
||||||
@ -247,7 +247,7 @@ func TestDumpRequestAndResponse(t *testing.T) {
|
|||||||
res = w.Result()
|
res = w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err = io.ReadAll(res.Body)
|
respBody, err = io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
resp = strings.TrimSpace(string(respBody))
|
resp = strings.TrimSpace(string(respBody))
|
||||||
require.Equalf(t, "[]", resp, "got unexpected respBody: %#v", string(respBody))
|
require.Equalf(t, "[]", resp, "got unexpected respBody: %#v", string(respBody))
|
||||||
|
|
||||||
@ -258,7 +258,7 @@ func TestDumpRequestAndResponse(t *testing.T) {
|
|||||||
res = w.Result()
|
res = w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err = io.ReadAll(res.Body)
|
respBody, err = io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
resp = strings.TrimSpace(string(respBody))
|
resp = strings.TrimSpace(string(respBody))
|
||||||
require.Equalf(t, "[]", resp, "got unexpected respBody: %#v", string(respBody))
|
require.Equalf(t, "[]", resp, "got unexpected respBody: %#v", string(respBody))
|
||||||
|
|
||||||
@ -268,9 +268,9 @@ func TestDumpRequestAndResponse(t *testing.T) {
|
|||||||
res = w.Result()
|
res = w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err = io.ReadAll(res.Body)
|
respBody, err = io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
dumpRequests = make([]*shared.DumpRequest, 0)
|
dumpRequests = make([]*shared.DumpRequest, 0)
|
||||||
testutils.Check(t, json.Unmarshal(respBody, &dumpRequests))
|
require.NoError(t, json.Unmarshal(respBody, &dumpRequests))
|
||||||
if len(dumpRequests) != 1 {
|
if len(dumpRequests) != 1 {
|
||||||
t.Fatalf("expected one pending dump request, got %#v", dumpRequests)
|
t.Fatalf("expected one pending dump request, got %#v", dumpRequests)
|
||||||
}
|
}
|
||||||
@ -289,9 +289,9 @@ func TestDumpRequestAndResponse(t *testing.T) {
|
|||||||
res = w.Result()
|
res = w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err = io.ReadAll(res.Body)
|
respBody, err = io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
var retrievedEntries []*shared.EncHistoryEntry
|
var retrievedEntries []*shared.EncHistoryEntry
|
||||||
testutils.Check(t, json.Unmarshal(respBody, &retrievedEntries))
|
require.NoError(t, json.Unmarshal(respBody, &retrievedEntries))
|
||||||
if len(retrievedEntries) != 2 {
|
if len(retrievedEntries) != 2 {
|
||||||
t.Fatalf("Expected to retrieve 2 entries, found %d", len(retrievedEntries))
|
t.Fatalf("Expected to retrieve 2 entries, found %d", len(retrievedEntries))
|
||||||
}
|
}
|
||||||
@ -306,7 +306,7 @@ func TestDumpRequestAndResponse(t *testing.T) {
|
|||||||
t.Fatalf("db.ReadCount should have been 1, was %v", dbEntry.ReadCount)
|
t.Fatalf("db.ReadCount should have been 1, was %v", dbEntry.ReadCount)
|
||||||
}
|
}
|
||||||
decEntry, err := data.DecryptHistoryEntry("dkey", *dbEntry)
|
decEntry, err := data.DecryptHistoryEntry("dkey", *dbEntry)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if !data.EntryEquals(decEntry, entry1Dec) && !data.EntryEquals(decEntry, entry2Dec) {
|
if !data.EntryEquals(decEntry, entry1Dec) && !data.EntryEquals(decEntry, entry2Dec) {
|
||||||
t.Fatalf("DB data is different than input! \ndb =%#v\nentry1=%#v\nentry2=%#v", *dbEntry, entry1Dec, entry2Dec)
|
t.Fatalf("DB data is different than input! \ndb =%#v\nentry1=%#v\nentry2=%#v", *dbEntry, entry1Dec, entry2Dec)
|
||||||
}
|
}
|
||||||
@ -340,9 +340,9 @@ func TestDeletionRequests(t *testing.T) {
|
|||||||
entry1 := testutils.MakeFakeHistoryEntry("ls ~/")
|
entry1 := testutils.MakeFakeHistoryEntry("ls ~/")
|
||||||
entry1.DeviceId = devId1
|
entry1.DeviceId = devId1
|
||||||
encEntry, err := data.EncryptHistoryEntry("dkey", entry1)
|
encEntry, err := data.EncryptHistoryEntry("dkey", entry1)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
reqBody, err := json.Marshal([]shared.EncHistoryEntry{encEntry})
|
reqBody, err := json.Marshal([]shared.EncHistoryEntry{encEntry})
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
submitReq := httptest.NewRequest(http.MethodPost, "/?source_device_id="+devId1, bytes.NewReader(reqBody))
|
submitReq := httptest.NewRequest(http.MethodPost, "/?source_device_id="+devId1, bytes.NewReader(reqBody))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
s.apiSubmitHandler(w, submitReq)
|
s.apiSubmitHandler(w, submitReq)
|
||||||
@ -354,9 +354,9 @@ func TestDeletionRequests(t *testing.T) {
|
|||||||
entry2 := testutils.MakeFakeHistoryEntry("ls /foo/bar")
|
entry2 := testutils.MakeFakeHistoryEntry("ls /foo/bar")
|
||||||
entry2.DeviceId = devId2
|
entry2.DeviceId = devId2
|
||||||
encEntry, err = data.EncryptHistoryEntry("dkey", entry2)
|
encEntry, err = data.EncryptHistoryEntry("dkey", entry2)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
reqBody, err = json.Marshal([]shared.EncHistoryEntry{encEntry})
|
reqBody, err = json.Marshal([]shared.EncHistoryEntry{encEntry})
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
submitReq = httptest.NewRequest(http.MethodPost, "/?source_device_id="+devId2, bytes.NewReader(reqBody))
|
submitReq = httptest.NewRequest(http.MethodPost, "/?source_device_id="+devId2, bytes.NewReader(reqBody))
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
s.apiSubmitHandler(w, submitReq)
|
s.apiSubmitHandler(w, submitReq)
|
||||||
@ -369,9 +369,9 @@ func TestDeletionRequests(t *testing.T) {
|
|||||||
entry3.StartTime = entry1.StartTime
|
entry3.StartTime = entry1.StartTime
|
||||||
entry3.EndTime = entry1.EndTime
|
entry3.EndTime = entry1.EndTime
|
||||||
encEntry, err = data.EncryptHistoryEntry("dOtherkey", entry3)
|
encEntry, err = data.EncryptHistoryEntry("dOtherkey", entry3)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
reqBody, err = json.Marshal([]shared.EncHistoryEntry{encEntry})
|
reqBody, err = json.Marshal([]shared.EncHistoryEntry{encEntry})
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
submitReq = httptest.NewRequest(http.MethodPost, "/?source_device_id="+devId1, bytes.NewReader(reqBody))
|
submitReq = httptest.NewRequest(http.MethodPost, "/?source_device_id="+devId1, bytes.NewReader(reqBody))
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
s.apiSubmitHandler(w, submitReq)
|
s.apiSubmitHandler(w, submitReq)
|
||||||
@ -386,9 +386,9 @@ func TestDeletionRequests(t *testing.T) {
|
|||||||
res := w.Result()
|
res := w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err := io.ReadAll(res.Body)
|
respBody, err := io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
var retrievedEntries []*shared.EncHistoryEntry
|
var retrievedEntries []*shared.EncHistoryEntry
|
||||||
testutils.Check(t, json.Unmarshal(respBody, &retrievedEntries))
|
require.NoError(t, json.Unmarshal(respBody, &retrievedEntries))
|
||||||
if len(retrievedEntries) != 2 {
|
if len(retrievedEntries) != 2 {
|
||||||
t.Fatalf("Expected to retrieve 1 entry, found %d", len(retrievedEntries))
|
t.Fatalf("Expected to retrieve 1 entry, found %d", len(retrievedEntries))
|
||||||
}
|
}
|
||||||
@ -403,7 +403,7 @@ func TestDeletionRequests(t *testing.T) {
|
|||||||
t.Fatalf("db.ReadCount should have been 1, was %v", dbEntry.ReadCount)
|
t.Fatalf("db.ReadCount should have been 1, was %v", dbEntry.ReadCount)
|
||||||
}
|
}
|
||||||
decEntry, err := data.DecryptHistoryEntry("dkey", *dbEntry)
|
decEntry, err := data.DecryptHistoryEntry("dkey", *dbEntry)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if !data.EntryEquals(decEntry, entry1) && !data.EntryEquals(decEntry, entry2) {
|
if !data.EntryEquals(decEntry, entry1) && !data.EntryEquals(decEntry, entry2) {
|
||||||
t.Fatalf("DB data is different than input! \ndb =%#v\nentry1=%#v\nentry2=%#v", *dbEntry, entry1, entry2)
|
t.Fatalf("DB data is different than input! \ndb =%#v\nentry1=%#v\nentry2=%#v", *dbEntry, entry1, entry2)
|
||||||
}
|
}
|
||||||
@ -419,7 +419,7 @@ func TestDeletionRequests(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
reqBody, err = json.Marshal(delReq)
|
reqBody, err = json.Marshal(delReq)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(reqBody))
|
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(reqBody))
|
||||||
s.addDeletionRequestHandler(httptest.NewRecorder(), req)
|
s.addDeletionRequestHandler(httptest.NewRecorder(), req)
|
||||||
|
|
||||||
@ -431,8 +431,8 @@ func TestDeletionRequests(t *testing.T) {
|
|||||||
res = w.Result()
|
res = w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err = io.ReadAll(res.Body)
|
respBody, err = io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
testutils.Check(t, json.Unmarshal(respBody, &retrievedEntries))
|
require.NoError(t, json.Unmarshal(respBody, &retrievedEntries))
|
||||||
if len(retrievedEntries) != 1 {
|
if len(retrievedEntries) != 1 {
|
||||||
t.Fatalf("Expected to retrieve 1 entry, found %d", len(retrievedEntries))
|
t.Fatalf("Expected to retrieve 1 entry, found %d", len(retrievedEntries))
|
||||||
}
|
}
|
||||||
@ -447,7 +447,7 @@ func TestDeletionRequests(t *testing.T) {
|
|||||||
t.Fatalf("db.ReadCount should have been 1, was %v", dbEntry.ReadCount)
|
t.Fatalf("db.ReadCount should have been 1, was %v", dbEntry.ReadCount)
|
||||||
}
|
}
|
||||||
decEntry, err := data.DecryptHistoryEntry("dkey", *dbEntry)
|
decEntry, err := data.DecryptHistoryEntry("dkey", *dbEntry)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if !data.EntryEquals(decEntry, entry2) {
|
if !data.EntryEquals(decEntry, entry2) {
|
||||||
t.Fatalf("DB data is different than input! \ndb =%#v\nentry=%#v", *dbEntry, entry2)
|
t.Fatalf("DB data is different than input! \ndb =%#v\nentry=%#v", *dbEntry, entry2)
|
||||||
}
|
}
|
||||||
@ -459,8 +459,8 @@ func TestDeletionRequests(t *testing.T) {
|
|||||||
res = w.Result()
|
res = w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err = io.ReadAll(res.Body)
|
respBody, err = io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
testutils.Check(t, json.Unmarshal(respBody, &retrievedEntries))
|
require.NoError(t, json.Unmarshal(respBody, &retrievedEntries))
|
||||||
if len(retrievedEntries) != 1 {
|
if len(retrievedEntries) != 1 {
|
||||||
t.Fatalf("Expected to retrieve 1 entry, found %d", len(retrievedEntries))
|
t.Fatalf("Expected to retrieve 1 entry, found %d", len(retrievedEntries))
|
||||||
}
|
}
|
||||||
@ -475,16 +475,16 @@ func TestDeletionRequests(t *testing.T) {
|
|||||||
t.Fatalf("db.ReadCount should have been 1, was %v", dbEntry.ReadCount)
|
t.Fatalf("db.ReadCount should have been 1, was %v", dbEntry.ReadCount)
|
||||||
}
|
}
|
||||||
decEntry, err = data.DecryptHistoryEntry("dOtherkey", *dbEntry)
|
decEntry, err = data.DecryptHistoryEntry("dOtherkey", *dbEntry)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if !data.EntryEquals(decEntry, entry3) {
|
if !data.EntryEquals(decEntry, entry3) {
|
||||||
t.Fatalf("DB data is different than input! \ndb =%#v\nentry=%#v", *dbEntry, entry3)
|
t.Fatalf("DB data is different than input! \ndb =%#v\nentry=%#v", *dbEntry, entry3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that apiSubmit tells the client that there is a pending deletion request
|
// Check that apiSubmit tells the client that there is a pending deletion request
|
||||||
encEntry, err = data.EncryptHistoryEntry("dkey", entry2)
|
encEntry, err = data.EncryptHistoryEntry("dkey", entry2)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
reqBody, err = json.Marshal([]shared.EncHistoryEntry{encEntry})
|
reqBody, err = json.Marshal([]shared.EncHistoryEntry{encEntry})
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
submitReq = httptest.NewRequest(http.MethodPost, "/?source_device_id="+devId2, bytes.NewReader(reqBody))
|
submitReq = httptest.NewRequest(http.MethodPost, "/?source_device_id="+devId2, bytes.NewReader(reqBody))
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
s.apiSubmitHandler(w, submitReq)
|
s.apiSubmitHandler(w, submitReq)
|
||||||
@ -499,9 +499,9 @@ func TestDeletionRequests(t *testing.T) {
|
|||||||
res = w.Result()
|
res = w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err = io.ReadAll(res.Body)
|
respBody, err = io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
var deletionRequests []*shared.DeletionRequest
|
var deletionRequests []*shared.DeletionRequest
|
||||||
testutils.Check(t, json.Unmarshal(respBody, &deletionRequests))
|
require.NoError(t, json.Unmarshal(respBody, &deletionRequests))
|
||||||
if len(deletionRequests) != 1 {
|
if len(deletionRequests) != 1 {
|
||||||
t.Fatalf("received %d deletion requests, expected only one", len(deletionRequests))
|
t.Fatalf("received %d deletion requests, expected only one", len(deletionRequests))
|
||||||
}
|
}
|
||||||
@ -533,7 +533,7 @@ func TestHealthcheck(t *testing.T) {
|
|||||||
res := w.Result()
|
res := w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
respBody, err := io.ReadAll(res.Body)
|
respBody, err := io.ReadAll(res.Body)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if string(respBody) != "OK" {
|
if string(respBody) != "OK" {
|
||||||
t.Fatalf("expected healthcheckHandler to return OK")
|
t.Fatalf("expected healthcheckHandler to return OK")
|
||||||
}
|
}
|
||||||
@ -583,9 +583,9 @@ func TestCleanDatabaseNoErrors(t *testing.T) {
|
|||||||
entry1 := testutils.MakeFakeHistoryEntry("ls ~/")
|
entry1 := testutils.MakeFakeHistoryEntry("ls ~/")
|
||||||
entry1.DeviceId = devId1
|
entry1.DeviceId = devId1
|
||||||
encEntry, err := data.EncryptHistoryEntry("dkey", entry1)
|
encEntry, err := data.EncryptHistoryEntry("dkey", entry1)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
reqBody, err := json.Marshal([]shared.EncHistoryEntry{encEntry})
|
reqBody, err := json.Marshal([]shared.EncHistoryEntry{encEntry})
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
submitReq := httptest.NewRequest(http.MethodPost, "/?source_device_id="+devId1, bytes.NewReader(reqBody))
|
submitReq := httptest.NewRequest(http.MethodPost, "/?source_device_id="+devId1, bytes.NewReader(reqBody))
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
s.apiSubmitHandler(w, submitReq)
|
s.apiSubmitHandler(w, submitReq)
|
||||||
@ -594,7 +594,7 @@ func TestCleanDatabaseNoErrors(t *testing.T) {
|
|||||||
require.NotEmpty(t, deserializeSubmitResponse(t, w).DumpRequests)
|
require.NotEmpty(t, deserializeSubmitResponse(t, w).DumpRequests)
|
||||||
|
|
||||||
// Call cleanDatabase and just check that there are no panics
|
// Call cleanDatabase and just check that there are no panics
|
||||||
testutils.Check(t, DB.Clean(context.TODO()))
|
require.NoError(t, DB.Clean(context.TODO()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertNoLeakedConnections(t *testing.T, db *database.DB) {
|
func assertNoLeakedConnections(t *testing.T, db *database.DB) {
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/DataDog/datadog-go/statsd"
|
"github.com/DataDog/datadog-go/statsd"
|
||||||
@ -156,7 +157,7 @@ func (s *Server) updateUsageData(ctx context.Context, version string, remoteAddr
|
|||||||
}
|
}
|
||||||
var usageData []shared.UsageData
|
var usageData []shared.UsageData
|
||||||
usageData, err := s.db.UsageDataFindByUserAndDevice(ctx, userId, deviceId)
|
usageData, err := s.db.UsageDataFindByUserAndDevice(ctx, userId, deviceId)
|
||||||
if err != nil {
|
if err != nil && !strings.Contains(err.Error(), "record not found") {
|
||||||
return fmt.Errorf("db.UsageDataFindByUserAndDevice: %w", err)
|
return fmt.Errorf("db.UsageDataFindByUserAndDevice: %w", err)
|
||||||
}
|
}
|
||||||
if len(usageData) == 0 {
|
if len(usageData) == 0 {
|
||||||
|
@ -102,19 +102,36 @@ func OpenDB() (*database.DB, error) {
|
|||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var CRON_COUNTER = 0
|
||||||
|
|
||||||
func cron(ctx context.Context, db *database.DB, stats *statsd.Client) error {
|
func cron(ctx context.Context, db *database.DB, stats *statsd.Client) error {
|
||||||
|
// Determine the latest released version of hishtory to serve via the /api/v1/download
|
||||||
|
// endpoint for hishtory updates.
|
||||||
if err := release.UpdateReleaseVersion(); err != nil {
|
if err := release.UpdateReleaseVersion(); err != nil {
|
||||||
return fmt.Errorf("updateReleaseVersion: %w", err)
|
return fmt.Errorf("updateReleaseVersion: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean the DB to remove entries that have already been read
|
||||||
if err := db.Clean(ctx); err != nil {
|
if err := db.Clean(ctx); err != nil {
|
||||||
return fmt.Errorf("db.Clean: %w", err)
|
return fmt.Errorf("db.Clean: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flush out datadog statsd
|
||||||
if stats != nil {
|
if stats != nil {
|
||||||
if err := stats.Flush(); err != nil {
|
if err := stats.Flush(); err != nil {
|
||||||
return fmt.Errorf("stats.Flush: %w", err)
|
return fmt.Errorf("stats.Flush: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect and store metrics on active users so we can track trends over time. This doesn't
|
||||||
|
// have to be run as often, so only run it for a fraction of cron jobs
|
||||||
|
if CRON_COUNTER%40 == 0 {
|
||||||
|
if err := db.GenerateAndStoreActiveUserStats(ctx); err != nil {
|
||||||
|
return fmt.Errorf("db.GenerateAndStoreActiveUserStats: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CRON_COUNTER += 1
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,10 +140,7 @@ func runBackgroundJobs(ctx context.Context, srv *server.Server, db *database.DB,
|
|||||||
for {
|
for {
|
||||||
err := cron(ctx, db, stats)
|
err := cron(ctx, db, stats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Cron failure: %v", err)
|
panic(fmt.Sprintf("Cron failure: %v", err))
|
||||||
|
|
||||||
// cron no longer panics, panicking here.
|
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
srv.UpdateReleaseVersion(release.Version, release.BuildUpdateInfo(release.Version))
|
srv.UpdateReleaseVersion(release.Version, release.BuildUpdateInfo(release.Version))
|
||||||
time.Sleep(10 * time.Minute)
|
time.Sleep(10 * time.Minute)
|
||||||
|
@ -111,6 +111,7 @@ func TestParam(t *testing.T) {
|
|||||||
runTestsWithRetries(t, "testTui/scroll", testTui_scroll)
|
runTestsWithRetries(t, "testTui/scroll", testTui_scroll)
|
||||||
runTestsWithRetries(t, "testTui/resize", testTui_resize)
|
runTestsWithRetries(t, "testTui/resize", testTui_resize)
|
||||||
runTestsWithRetries(t, "testTui/delete", testTui_delete)
|
runTestsWithRetries(t, "testTui/delete", testTui_delete)
|
||||||
|
runTestsWithRetries(t, "testTui/color", testTui_color)
|
||||||
|
|
||||||
// Assert there are no leaked connections
|
// Assert there are no leaked connections
|
||||||
assertNoLeakedConnections(t)
|
assertNoLeakedConnections(t)
|
||||||
@ -168,7 +169,7 @@ func testIntegrationWithNewDevice(t *testing.T, tester shellTester) {
|
|||||||
// Set the secret key to the previous secret key
|
// Set the secret key to the previous secret key
|
||||||
out, err := tester.RunInteractiveShellRelaxed(t, ` export HISHTORY_SKIP_INIT_IMPORT=1
|
out, err := tester.RunInteractiveShellRelaxed(t, ` export HISHTORY_SKIP_INIT_IMPORT=1
|
||||||
yes | hishtory init `+userSecret)
|
yes | hishtory init `+userSecret)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
require.Contains(t, out, "Setting secret hishtory key to "+userSecret, "Failed to re-init with the user secret")
|
require.Contains(t, out, "Setting secret hishtory key to "+userSecret, "Failed to re-init with the user secret")
|
||||||
|
|
||||||
// Querying shouldn't show the entry from the previous account
|
// Querying shouldn't show the entry from the previous account
|
||||||
@ -297,22 +298,22 @@ echo thisisrecorded`)
|
|||||||
line2Matcher := hostnameMatcher + tableDividerMatcher + pathMatcher + tableDividerMatcher + datetimeMatcher + tableDividerMatcher + runtimeMatcher + tableDividerMatcher + exitCodeMatcher + tableDividerMatcher + pipefailMatcher + tableDividerMatcher + `\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`
|
line3Matcher := hostnameMatcher + tableDividerMatcher + pathMatcher + tableDividerMatcher + datetimeMatcher + tableDividerMatcher + runtimeMatcher + tableDividerMatcher + exitCodeMatcher + tableDividerMatcher + `echo thisisrecorded` + tableDividerMatcher + `\n`
|
||||||
match, err := regexp.MatchString(line3Matcher, out)
|
match, err := regexp.MatchString(line3Matcher, out)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if !match {
|
if !match {
|
||||||
t.Fatalf("output is missing the row for `echo thisisrecorded`: %v", out)
|
t.Fatalf("output is missing the row for `echo thisisrecorded`: %v", out)
|
||||||
}
|
}
|
||||||
match, err = regexp.MatchString(line1Matcher, out)
|
match, err = regexp.MatchString(line1Matcher, out)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if !match {
|
if !match {
|
||||||
t.Fatalf("output is missing the headings: %v", out)
|
t.Fatalf("output is missing the headings: %v", out)
|
||||||
}
|
}
|
||||||
match, err = regexp.MatchString(line2Matcher, out)
|
match, err = regexp.MatchString(line2Matcher, out)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if !match {
|
if !match {
|
||||||
t.Fatalf("output is missing the pipefail: %v", out)
|
t.Fatalf("output is missing the pipefail: %v", out)
|
||||||
}
|
}
|
||||||
match, err = regexp.MatchString(line1Matcher+line2Matcher+line3Matcher, out)
|
match, err = regexp.MatchString(line1Matcher+line2Matcher+line3Matcher, out)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if !match {
|
if !match {
|
||||||
t.Fatalf("output doesn't match the expected table: %v", out)
|
t.Fatalf("output doesn't match the expected table: %v", out)
|
||||||
}
|
}
|
||||||
@ -790,7 +791,7 @@ func testHishtoryBackgroundSaving(t *testing.T, tester shellTester) {
|
|||||||
|
|
||||||
// Check that we can find the go binary
|
// Check that we can find the go binary
|
||||||
_, err := exec.LookPath("go")
|
_, err := exec.LookPath("go")
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test install with an unset HISHTORY_TEST var so that we save in the background (this is likely to be flakey!)
|
// Test install with an unset HISHTORY_TEST var so that we save in the background (this is likely to be flakey!)
|
||||||
out := tester.RunInteractiveShell(t, `unset HISHTORY_TEST
|
out := tester.RunInteractiveShell(t, `unset HISHTORY_TEST
|
||||||
@ -889,7 +890,7 @@ func testDisplayTable(t *testing.T, tester shellTester) {
|
|||||||
|
|
||||||
// Add a custom column
|
// Add a custom column
|
||||||
tester.RunInteractiveShell(t, `hishtory config-add custom-columns foo "echo aaaaaaaaaaaaa"`)
|
tester.RunInteractiveShell(t, `hishtory config-add custom-columns foo "echo aaaaaaaaaaaaa"`)
|
||||||
testutils.Check(t, os.Chdir("/"))
|
require.NoError(t, os.Chdir("/"))
|
||||||
tester.RunInteractiveShell(t, ` hishtory enable`)
|
tester.RunInteractiveShell(t, ` hishtory enable`)
|
||||||
tester.RunInteractiveShell(t, `echo table-1`)
|
tester.RunInteractiveShell(t, `echo table-1`)
|
||||||
tester.RunInteractiveShell(t, `echo table-2`)
|
tester.RunInteractiveShell(t, `echo table-2`)
|
||||||
@ -1355,7 +1356,7 @@ ls /tmp`, randomCmdUuid, randomCmdUuid)
|
|||||||
|
|
||||||
// Redact it without HISHTORY_REDACT_FORCE
|
// Redact it without HISHTORY_REDACT_FORCE
|
||||||
out, err := tester.RunInteractiveShellRelaxed(t, `yes | hishtory redact hello`)
|
out, err := tester.RunInteractiveShellRelaxed(t, `yes | hishtory redact hello`)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if out != "This will permanently delete 1 entries, are you sure? [y/N]" {
|
if out != "This will permanently delete 1 entries, are you sure? [y/N]" {
|
||||||
t.Fatalf("hishtory redact gave unexpected output=%#v", out)
|
t.Fatalf("hishtory redact gave unexpected output=%#v", out)
|
||||||
}
|
}
|
||||||
@ -1473,12 +1474,12 @@ func testConfigGetSet(t *testing.T, tester shellTester) {
|
|||||||
|
|
||||||
func clearControlRSearchFromConfig(t testing.TB) {
|
func clearControlRSearchFromConfig(t testing.TB) {
|
||||||
configContents, err := hctx.GetConfigContents()
|
configContents, err := hctx.GetConfigContents()
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
configContents = []byte(strings.ReplaceAll(string(configContents), "enable_control_r_search", "something-else"))
|
configContents = []byte(strings.ReplaceAll(string(configContents), "enable_control_r_search", "something-else"))
|
||||||
homedir, err := os.UserHomeDir()
|
homedir, err := os.UserHomeDir()
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
err = os.WriteFile(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH), configContents, 0o644)
|
err = os.WriteFile(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH), configContents, 0o644)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testHandleUpgradedFeatures(t *testing.T, tester shellTester) {
|
func testHandleUpgradedFeatures(t *testing.T, tester shellTester) {
|
||||||
@ -1488,9 +1489,9 @@ func testHandleUpgradedFeatures(t *testing.T, tester shellTester) {
|
|||||||
|
|
||||||
// Install, and there is no prompt since the config already mentions control-r
|
// Install, and there is no prompt since the config already mentions control-r
|
||||||
_, err := tester.RunInteractiveShellRelaxed(t, `/tmp/client install`)
|
_, err := tester.RunInteractiveShellRelaxed(t, `/tmp/client install`)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
_, err = tester.RunInteractiveShellRelaxed(t, `hishtory disable`)
|
_, err = tester.RunInteractiveShellRelaxed(t, `hishtory disable`)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Ensure that the config doesn't mention control-r
|
// Ensure that the config doesn't mention control-r
|
||||||
clearControlRSearchFromConfig(t)
|
clearControlRSearchFromConfig(t)
|
||||||
@ -1519,7 +1520,7 @@ func TestFish(t *testing.T) {
|
|||||||
installHishtory(t, tester, "")
|
installHishtory(t, tester, "")
|
||||||
|
|
||||||
// Test recording in fish
|
// Test recording in fish
|
||||||
testutils.Check(t, os.Chdir("/"))
|
require.NoError(t, os.Chdir("/"))
|
||||||
out := captureTerminalOutputWithShellName(t, tester, "fish", []string{
|
out := captureTerminalOutputWithShellName(t, tester, "fish", []string{
|
||||||
"echo SPACE foo ENTER",
|
"echo SPACE foo ENTER",
|
||||||
"ENTER",
|
"ENTER",
|
||||||
@ -1558,10 +1559,10 @@ func setupTestTui(t testing.TB) (shellTester, string, *gorm.DB) {
|
|||||||
// Insert a couple hishtory entries
|
// Insert a couple hishtory entries
|
||||||
db := hctx.GetDb(hctx.MakeContext())
|
db := hctx.GetDb(hctx.MakeContext())
|
||||||
e1 := testutils.MakeFakeHistoryEntry("ls ~/")
|
e1 := testutils.MakeFakeHistoryEntry("ls ~/")
|
||||||
testutils.Check(t, db.Create(e1).Error)
|
require.NoError(t, db.Create(e1).Error)
|
||||||
manuallySubmitHistoryEntry(t, userSecret, e1)
|
manuallySubmitHistoryEntry(t, userSecret, e1)
|
||||||
e2 := testutils.MakeFakeHistoryEntry("echo 'aaaaaa bbbb'")
|
e2 := testutils.MakeFakeHistoryEntry("echo 'aaaaaa bbbb'")
|
||||||
testutils.Check(t, db.Create(e2).Error)
|
require.NoError(t, db.Create(e2).Error)
|
||||||
manuallySubmitHistoryEntry(t, userSecret, e2)
|
manuallySubmitHistoryEntry(t, userSecret, e2)
|
||||||
return tester, userSecret, db
|
return tester, userSecret, db
|
||||||
}
|
}
|
||||||
@ -1604,9 +1605,6 @@ func testTui_resize(t testing.TB) {
|
|||||||
})
|
})
|
||||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||||
testutils.CompareGoldens(t, out, "TestTui-LongQuery")
|
testutils.CompareGoldens(t, out, "TestTui-LongQuery")
|
||||||
|
|
||||||
// Assert there are no leaked connections
|
|
||||||
assertNoLeakedConnections(t)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTui_scroll(t testing.TB) {
|
func testTui_scroll(t testing.TB) {
|
||||||
@ -1646,6 +1644,36 @@ func testTui_scroll(t testing.TB) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testTui_color(t testing.TB) {
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
// For some reason, this test fails on linux. Since this test isn't critical and is expected to be
|
||||||
|
// flaky, we can just skip it on linux.
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
defer testutils.BackupAndRestore(t)()
|
||||||
|
tester, _, _ := setupTestTui(t)
|
||||||
|
|
||||||
|
// Capture the TUI with full colored output, note that this golden will be harder to undersand
|
||||||
|
// from inspection and primarily servers to detect unintended changes in hishtory's output.
|
||||||
|
out := captureTerminalOutputComplex(t, TmuxCaptureConfig{tester: tester, complexCommands: []TmuxCommand{{Keys: "hishtory SPACE tquery ENTER"}}, includeEscapeSequences: true})
|
||||||
|
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||||
|
testutils.CompareGoldens(t, out, "TestTui-ColoredOutput")
|
||||||
|
|
||||||
|
// And the same once a search query has been typed in
|
||||||
|
out = captureTerminalOutputComplex(t, TmuxCaptureConfig{tester: tester, complexCommands: []TmuxCommand{{Keys: "hishtory SPACE tquery ENTER"}, {Keys: "ech"}}, includeEscapeSequences: true})
|
||||||
|
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||||
|
testutils.CompareGoldens(t, out, "TestTui-ColoredOutputWithSearch")
|
||||||
|
|
||||||
|
// And one more time with beta-mode for highlighting matches
|
||||||
|
tester.RunInteractiveShell(t, ` hishtory config-set beta-mode true`)
|
||||||
|
require.Equal(t, "true", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get beta-mode`)))
|
||||||
|
out = captureTerminalOutputComplex(t, TmuxCaptureConfig{tester: tester, complexCommands: []TmuxCommand{{Keys: "hishtory SPACE tquery ENTER"}, {Keys: "ech"}}, includeEscapeSequences: true})
|
||||||
|
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||||
|
testutils.CompareGoldens(t, out, "TestTui-ColoredOutputWithSearch-BetaMode")
|
||||||
|
}
|
||||||
|
|
||||||
func testTui_delete(t testing.TB) {
|
func testTui_delete(t testing.TB) {
|
||||||
// Setup
|
// Setup
|
||||||
defer testutils.BackupAndRestore(t)()
|
defer testutils.BackupAndRestore(t)()
|
||||||
@ -1724,7 +1752,7 @@ func testTui_search(t testing.TB) {
|
|||||||
// Check the output when the initial search is invalid
|
// Check the output when the initial search is invalid
|
||||||
out = captureTerminalOutputWithComplexCommands(t, tester, []TmuxCommand{
|
out = captureTerminalOutputWithComplexCommands(t, tester, []TmuxCommand{
|
||||||
// ExtraDelay to ensure that after searching for 'foo:' it gets the real results for an empty query
|
// ExtraDelay to ensure that after searching for 'foo:' it gets the real results for an empty query
|
||||||
{Keys: "hishtory SPACE tquery SPACE foo: ENTER", ExtraDelay: 1.0},
|
{Keys: "hishtory SPACE tquery SPACE foo: ENTER", ExtraDelay: 1.5},
|
||||||
{Keys: "ls", ExtraDelay: 1.0},
|
{Keys: "ls", ExtraDelay: 1.0},
|
||||||
})
|
})
|
||||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||||
@ -1734,7 +1762,7 @@ func testTui_search(t testing.TB) {
|
|||||||
out = captureTerminalOutputWithComplexCommands(t, tester, []TmuxCommand{
|
out = captureTerminalOutputWithComplexCommands(t, tester, []TmuxCommand{
|
||||||
{Keys: "hishtory SPACE tquery ENTER"},
|
{Keys: "hishtory SPACE tquery ENTER"},
|
||||||
// ExtraDelay to ensure that the search for 'ls' finishes before we make it invalid by adding ':'
|
// ExtraDelay to ensure that the search for 'ls' finishes before we make it invalid by adding ':'
|
||||||
{Keys: "ls", ExtraDelay: 1.0},
|
{Keys: "ls", ExtraDelay: 1.5},
|
||||||
{Keys: ":"},
|
{Keys: ":"},
|
||||||
})
|
})
|
||||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||||
@ -1747,7 +1775,6 @@ func testTui_search(t testing.TB) {
|
|||||||
})
|
})
|
||||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||||
testutils.CompareGoldens(t, out, "TestTui-InvalidSearchBecomesValid")
|
testutils.CompareGoldens(t, out, "TestTui-InvalidSearchBecomesValid")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTui_general(t testing.TB) {
|
func testTui_general(t testing.TB) {
|
||||||
@ -1826,11 +1853,11 @@ func testControlR(t testing.TB, tester shellTester, shellName string, onlineStat
|
|||||||
e1.CurrentWorkingDirectory = "/etc/"
|
e1.CurrentWorkingDirectory = "/etc/"
|
||||||
e1.Hostname = "server"
|
e1.Hostname = "server"
|
||||||
e1.ExitCode = 127
|
e1.ExitCode = 127
|
||||||
testutils.Check(t, db.Create(e1).Error)
|
require.NoError(t, db.Create(e1).Error)
|
||||||
testutils.Check(t, db.Create(testutils.MakeFakeHistoryEntry("ls ~/foo/")).Error)
|
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("ls ~/foo/")).Error)
|
||||||
testutils.Check(t, db.Create(testutils.MakeFakeHistoryEntry("ls ~/bar/")).Error)
|
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("ls ~/bar/")).Error)
|
||||||
testutils.Check(t, db.Create(testutils.MakeFakeHistoryEntry("echo 'aaaaaa bbbb'")).Error)
|
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("echo 'aaaaaa bbbb'")).Error)
|
||||||
testutils.Check(t, db.Create(testutils.MakeFakeHistoryEntry("echo 'bar' &")).Error)
|
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("echo 'bar' &")).Error)
|
||||||
|
|
||||||
// Check that they're there
|
// Check that they're there
|
||||||
var historyEntries []*data.HistoryEntry
|
var historyEntries []*data.HistoryEntry
|
||||||
@ -1965,7 +1992,7 @@ func testControlR(t testing.TB, tester shellTester, shellName string, onlineStat
|
|||||||
|
|
||||||
// Re-enable control-r
|
// Re-enable control-r
|
||||||
_, err := tester.RunInteractiveShellRelaxed(t, `hishtory config-set enable-control-r true`)
|
_, err := tester.RunInteractiveShellRelaxed(t, `hishtory config-set enable-control-r true`)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// And check that the control-r bindings work again
|
// And check that the control-r bindings work again
|
||||||
out = captureTerminalOutputWithShellName(t, tester, shellName, []string{"C-R", "-pipefail SPACE -exit_code:0"})
|
out = captureTerminalOutputWithShellName(t, tester, shellName, []string{"C-R", "-pipefail SPACE -exit_code:0"})
|
||||||
@ -2051,20 +2078,18 @@ func testPresaving(t *testing.T, tester shellTester) {
|
|||||||
manuallySubmitHistoryEntry(t, userSecret, testutils.MakeFakeHistoryEntry("table_sizing aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
|
manuallySubmitHistoryEntry(t, userSecret, testutils.MakeFakeHistoryEntry("table_sizing aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
|
||||||
|
|
||||||
// Enable beta-mode since presaving is behind that feature flag
|
// Enable beta-mode since presaving is behind that feature flag
|
||||||
out := strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get beta-mode`))
|
require.Equal(t, "false", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get beta-mode`)))
|
||||||
require.Equal(t, out, "false")
|
|
||||||
tester.RunInteractiveShell(t, `hishtory config-set beta-mode true`)
|
tester.RunInteractiveShell(t, `hishtory config-set beta-mode true`)
|
||||||
out = strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get beta-mode`))
|
require.Equal(t, "true", strings.TrimSpace(tester.RunInteractiveShell(t, `hishtory config-get beta-mode`)))
|
||||||
require.Equal(t, out, "true")
|
|
||||||
|
|
||||||
// Start a command that will take a long time to execute in the background, so
|
// Start a command that will take a long time to execute in the background, so
|
||||||
// we can check that it was recorded even though it never finished.
|
// we can check that it was recorded even though it never finished.
|
||||||
require.NoError(t, os.Chdir("/"))
|
require.NoError(t, os.Chdir("/"))
|
||||||
go tester.RunInteractiveShell(t, `sleep 13371337`)
|
require.NoError(t, tester.RunInteractiveShellBackground(t, `sleep 13371337`))
|
||||||
time.Sleep(time.Millisecond * 500)
|
time.Sleep(time.Millisecond * 500)
|
||||||
|
|
||||||
// Test that it shows up in hishtory export
|
// Test that it shows up in hishtory export
|
||||||
out = tester.RunInteractiveShell(t, ` hishtory export sleep -export`)
|
out := tester.RunInteractiveShell(t, ` hishtory export sleep -export`)
|
||||||
expectedOutput := "sleep 13371337\n"
|
expectedOutput := "sleep 13371337\n"
|
||||||
if diff := cmp.Diff(expectedOutput, out); diff != "" {
|
if diff := cmp.Diff(expectedOutput, out); diff != "" {
|
||||||
t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out)
|
t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out)
|
||||||
@ -2075,6 +2100,16 @@ func testPresaving(t *testing.T, tester shellTester) {
|
|||||||
out = tester.RunInteractiveShell(t, ` hishtory query sleep 13371337 -export -tquery`)
|
out = tester.RunInteractiveShell(t, ` hishtory query sleep 13371337 -export -tquery`)
|
||||||
testutils.CompareGoldens(t, out, "testPresaving-query")
|
testutils.CompareGoldens(t, out, "testPresaving-query")
|
||||||
|
|
||||||
|
// And then record a few other commands, and run an export of all commands, to ensure no funkiness happened
|
||||||
|
tester.RunInteractiveShell(t, `ls /`)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
tester.RunInteractiveShell(t, `sleep 0.5`)
|
||||||
|
out = tester.RunInteractiveShell(t, ` hishtory export -hishtory -table_sizing -pipefail`)
|
||||||
|
expectedOutput = "sleep 13371337\nls /\nsleep 0.5\n"
|
||||||
|
if diff := cmp.Diff(expectedOutput, out); diff != "" {
|
||||||
|
t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out)
|
||||||
|
}
|
||||||
|
|
||||||
// Create a new device, and confirm it shows up there too
|
// Create a new device, and confirm it shows up there too
|
||||||
restoreDevice1 := testutils.BackupAndRestoreWithId(t, "device1")
|
restoreDevice1 := testutils.BackupAndRestoreWithId(t, "device1")
|
||||||
installHishtory(t, tester, userSecret)
|
installHishtory(t, tester, userSecret)
|
||||||
@ -2082,17 +2117,31 @@ func testPresaving(t *testing.T, tester shellTester) {
|
|||||||
out = tester.RunInteractiveShell(t, ` hishtory query sleep 13371337 -export -tquery`)
|
out = tester.RunInteractiveShell(t, ` hishtory query sleep 13371337 -export -tquery`)
|
||||||
testutils.CompareGoldens(t, out, "testPresaving-query")
|
testutils.CompareGoldens(t, out, "testPresaving-query")
|
||||||
|
|
||||||
|
// And that all the other commands do to
|
||||||
|
out = tester.RunInteractiveShell(t, ` hishtory export -hishtory -table_sizing -pipefail`)
|
||||||
|
expectedOutput = "sleep 13371337\nls /\nsleep 0.5\n"
|
||||||
|
if diff := cmp.Diff(expectedOutput, out); diff != "" {
|
||||||
|
t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out)
|
||||||
|
}
|
||||||
|
|
||||||
// And then redact it from device2
|
// And then redact it from device2
|
||||||
tester.RunInteractiveShell(t, ` HISHTORY_REDACT_FORCE=true hishtory redact sleep 13371337`)
|
tester.RunInteractiveShell(t, ` HISHTORY_REDACT_FORCE=true hishtory redact sleep 13371337`)
|
||||||
|
|
||||||
// And confirm it was redacted
|
// And confirm it was redacted
|
||||||
out = tester.RunInteractiveShell(t, ` hishtory export sleep -export`)
|
out = tester.RunInteractiveShell(t, ` hishtory export sleep -export`)
|
||||||
require.Equal(t, "", out)
|
require.Equal(t, "sleep 0.5\n", out)
|
||||||
|
|
||||||
// Then go back to device1 and confirm it was redacted there too
|
// Then go back to device1 and confirm it was redacted there too
|
||||||
restoreDevice1()
|
restoreDevice1()
|
||||||
out = tester.RunInteractiveShell(t, ` hishtory export sleep -export`)
|
out = tester.RunInteractiveShell(t, ` hishtory export sleep -export`)
|
||||||
require.Equal(t, "", out)
|
require.Equal(t, "sleep 0.5\n", out)
|
||||||
|
|
||||||
|
// And then record a few commands, and run a final export of all commands, to ensure no funkiness happened
|
||||||
|
out = tester.RunInteractiveShell(t, ` hishtory export -hishtory -table_sizing -pipefail`)
|
||||||
|
expectedOutput = "ls /\nsleep 0.5\n"
|
||||||
|
if diff := cmp.Diff(expectedOutput, out); diff != "" {
|
||||||
|
t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUninstall(t *testing.T, tester shellTester) {
|
func testUninstall(t *testing.T, tester shellTester) {
|
||||||
@ -2108,14 +2157,14 @@ echo baz`)
|
|||||||
|
|
||||||
// And then uninstall
|
// And then uninstall
|
||||||
out, err := tester.RunInteractiveShellRelaxed(t, `yes | hishtory uninstall`)
|
out, err := tester.RunInteractiveShellRelaxed(t, `yes | hishtory uninstall`)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
testutils.CompareGoldens(t, out, "testUninstall-uninstall")
|
testutils.CompareGoldens(t, out, "testUninstall-uninstall")
|
||||||
|
|
||||||
// And check that hishtory has been uninstalled
|
// And check that hishtory has been uninstalled
|
||||||
out, err = tester.RunInteractiveShellRelaxed(t, `echo foo
|
out, err = tester.RunInteractiveShellRelaxed(t, `echo foo
|
||||||
hishtory
|
hishtory
|
||||||
echo bar`)
|
echo bar`)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
testutils.CompareGoldens(t, out, "testUninstall-post-uninstall")
|
testutils.CompareGoldens(t, out, "testUninstall-post-uninstall")
|
||||||
|
|
||||||
// And check again, but in a way that shows the full terminal output
|
// And check again, but in a way that shows the full terminal output
|
||||||
@ -2182,15 +2231,15 @@ func TestSortByConsistentTimezone(t *testing.T) {
|
|||||||
entry1 := testutils.MakeFakeHistoryEntry("first_entry")
|
entry1 := testutils.MakeFakeHistoryEntry("first_entry")
|
||||||
entry1.StartTime = time.Unix(timestamp, 0).In(ny_time)
|
entry1.StartTime = time.Unix(timestamp, 0).In(ny_time)
|
||||||
entry1.EndTime = time.Unix(timestamp+1, 0).In(ny_time)
|
entry1.EndTime = time.Unix(timestamp+1, 0).In(ny_time)
|
||||||
testutils.Check(t, lib.ReliableDbCreate(db, entry1))
|
require.NoError(t, lib.ReliableDbCreate(db, entry1))
|
||||||
entry2 := testutils.MakeFakeHistoryEntry("second_entry")
|
entry2 := testutils.MakeFakeHistoryEntry("second_entry")
|
||||||
entry2.StartTime = time.Unix(timestamp+1000, 0).In(la_time)
|
entry2.StartTime = time.Unix(timestamp+1000, 0).In(la_time)
|
||||||
entry2.EndTime = time.Unix(timestamp+1001, 0).In(la_time)
|
entry2.EndTime = time.Unix(timestamp+1001, 0).In(la_time)
|
||||||
testutils.Check(t, lib.ReliableDbCreate(db, entry2))
|
require.NoError(t, lib.ReliableDbCreate(db, entry2))
|
||||||
entry3 := testutils.MakeFakeHistoryEntry("third_entry")
|
entry3 := testutils.MakeFakeHistoryEntry("third_entry")
|
||||||
entry3.StartTime = time.Unix(timestamp+2000, 0).In(ny_time)
|
entry3.StartTime = time.Unix(timestamp+2000, 0).In(ny_time)
|
||||||
entry3.EndTime = time.Unix(timestamp+2001, 0).In(ny_time)
|
entry3.EndTime = time.Unix(timestamp+2001, 0).In(ny_time)
|
||||||
testutils.Check(t, lib.ReliableDbCreate(db, entry3))
|
require.NoError(t, lib.ReliableDbCreate(db, entry3))
|
||||||
|
|
||||||
// And check that they're displayed in the correct order
|
// And check that they're displayed in the correct order
|
||||||
out := hishtoryQuery(t, tester, "-pipefail -tablesizing")
|
out := hishtoryQuery(t, tester, "-pipefail -tablesizing")
|
||||||
@ -2208,13 +2257,13 @@ func TestZDotDir(t *testing.T) {
|
|||||||
defer testutils.BackupAndRestore(t)()
|
defer testutils.BackupAndRestore(t)()
|
||||||
defer testutils.BackupAndRestoreEnv("ZDOTDIR")()
|
defer testutils.BackupAndRestoreEnv("ZDOTDIR")()
|
||||||
homedir, err := os.UserHomeDir()
|
homedir, err := os.UserHomeDir()
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
zdotdir := path.Join(homedir, "foo")
|
zdotdir := path.Join(homedir, "foo")
|
||||||
testutils.Check(t, os.MkdirAll(zdotdir, 0o744))
|
require.NoError(t, os.MkdirAll(zdotdir, 0o744))
|
||||||
os.Setenv("ZDOTDIR", zdotdir)
|
os.Setenv("ZDOTDIR", zdotdir)
|
||||||
userSecret := installHishtory(t, tester, "")
|
userSecret := installHishtory(t, tester, "")
|
||||||
defer func() {
|
defer func() {
|
||||||
testutils.Check(t, os.Remove(path.Join(zdotdir, ".zshrc")))
|
require.NoError(t, os.Remove(path.Join(zdotdir, ".zshrc")))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Check the status command
|
// Check the status command
|
||||||
@ -2232,7 +2281,7 @@ func TestZDotDir(t *testing.T) {
|
|||||||
|
|
||||||
// Check that hishtory respected ZDOTDIR
|
// Check that hishtory respected ZDOTDIR
|
||||||
zshrc, err := os.ReadFile(path.Join(zdotdir, ".zshrc"))
|
zshrc, err := os.ReadFile(path.Join(zdotdir, ".zshrc"))
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
require.Contains(t, string(zshrc), "# Hishtory Config:", "zshrc had unexpected contents")
|
require.Contains(t, string(zshrc), "# Hishtory Config:", "zshrc had unexpected contents")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2264,7 +2313,7 @@ echo foo`)
|
|||||||
out = tester.RunInteractiveShell(t, `hishtory query -pipefail`)
|
out = tester.RunInteractiveShell(t, `hishtory query -pipefail`)
|
||||||
testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-enabled-query")
|
testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-enabled-query")
|
||||||
out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery SPACE -pipefail ENTER"})
|
out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery SPACE -pipefail ENTER"})
|
||||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
out = strings.TrimSpace(strings.Split(out, "hishtory tquery -pipefail")[1])
|
||||||
testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-enabled-tquery")
|
testutils.CompareGoldens(t, out, "testRemoveDuplicateRows-enabled-tquery")
|
||||||
out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery SPACE -pipefail ENTER", "Down Down", "ENTER"})
|
out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery SPACE -pipefail ENTER", "Down Down", "ENTER"})
|
||||||
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
|
||||||
@ -2280,7 +2329,7 @@ func TestSetConfigNoCorruption(t *testing.T) {
|
|||||||
|
|
||||||
// A test that tries writing a config many different times in parallel, and confirms there is no corruption
|
// A test that tries writing a config many different times in parallel, and confirms there is no corruption
|
||||||
conf, err := hctx.GetConfig()
|
conf, err := hctx.GetConfig()
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
var doneWg sync.WaitGroup
|
var doneWg sync.WaitGroup
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
doneWg.Add(1)
|
doneWg.Add(1)
|
||||||
@ -2291,7 +2340,7 @@ func TestSetConfigNoCorruption(t *testing.T) {
|
|||||||
c.DeviceId = strings.Repeat("B", i*2)
|
c.DeviceId = strings.Repeat("B", i*2)
|
||||||
c.HaveMissedUploads = (i % 2) == 0
|
c.HaveMissedUploads = (i % 2) == 0
|
||||||
// Write it
|
// Write it
|
||||||
err := hctx.SetConfig(&c)
|
require.NoError(t, hctx.SetConfig(&c))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
// Check that we can read
|
// Check that we can read
|
||||||
c2, err := hctx.GetConfig()
|
c2, err := hctx.GetConfig()
|
||||||
@ -2377,7 +2426,7 @@ func testMultipleUsers(t *testing.T, tester shellTester) {
|
|||||||
for _, d := range []device{u1d1, u1d2} {
|
for _, d := range []device{u1d1, u1d2} {
|
||||||
switchToDevice(&devices, d)
|
switchToDevice(&devices, d)
|
||||||
out, err := tester.RunInteractiveShellRelaxed(t, `hishtory export -pipefail -export`)
|
out, err := tester.RunInteractiveShellRelaxed(t, `hishtory export -pipefail -export`)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
expectedOutput := "echo u1d1\necho u1d2\necho u1d1-b\necho u1d1-c\necho u1d2-b\necho u1d2-c\n"
|
expectedOutput := "echo u1d1\necho u1d2\necho u1d1-b\necho u1d1-c\necho u1d2-b\necho u1d2-c\n"
|
||||||
if diff := cmp.Diff(expectedOutput, out); diff != "" {
|
if diff := cmp.Diff(expectedOutput, out); diff != "" {
|
||||||
t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out)
|
t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out)
|
||||||
@ -2396,7 +2445,7 @@ func testMultipleUsers(t *testing.T, tester shellTester) {
|
|||||||
for _, d := range []device{u2d1, u2d2, u2d3} {
|
for _, d := range []device{u2d1, u2d2, u2d3} {
|
||||||
switchToDevice(&devices, d)
|
switchToDevice(&devices, d)
|
||||||
out, err := tester.RunInteractiveShellRelaxed(t, `hishtory export -export -pipefail`)
|
out, err := tester.RunInteractiveShellRelaxed(t, `hishtory export -export -pipefail`)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
expectedOutput := "echo u2d1\necho u2d2\necho u2d3\necho u1d1-b\necho u1d1-c\necho u2d3-b\necho u2d3-c\n"
|
expectedOutput := "echo u2d1\necho u2d2\necho u2d3\necho u1d1-b\necho u1d1-c\necho u2d3-b\necho u2d3-c\n"
|
||||||
if diff := cmp.Diff(expectedOutput, out); diff != "" {
|
if diff := cmp.Diff(expectedOutput, out); diff != "" {
|
||||||
t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out)
|
t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out)
|
||||||
|
@ -144,8 +144,11 @@ func presaveHistoryEntry(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Augment it with os.Args
|
// Augment it with os.Args
|
||||||
entry.Command = trimTrailingWhitespace(os.Args[3])
|
shellName := os.Args[2]
|
||||||
if strings.HasPrefix(" ", entry.Command) || entry.Command == "" {
|
cmd, err := extractCommandFromArg(ctx, shellName, os.Args[3] /* isPresave = */, true)
|
||||||
|
lib.CheckFatalError(err)
|
||||||
|
entry.Command = cmd
|
||||||
|
if strings.HasPrefix(" ", entry.Command) || strings.TrimSpace(entry.Command) == "" {
|
||||||
// Don't save commands that start with a space
|
// Don't save commands that start with a space
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -154,11 +157,6 @@ func presaveHistoryEntry(ctx context.Context) {
|
|||||||
entry.StartTime = time.Unix(startTime, 0).UTC()
|
entry.StartTime = time.Unix(startTime, 0).UTC()
|
||||||
entry.EndTime = time.Unix(0, 0).UTC()
|
entry.EndTime = time.Unix(0, 0).UTC()
|
||||||
|
|
||||||
// Skip saving references to presaving
|
|
||||||
if strings.Contains(entry.Command, "presaveHistoryEntry") || strings.Contains(entry.Command, "__history_control_r") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// And persist it locally.
|
// And persist it locally.
|
||||||
db := hctx.GetDb(ctx)
|
db := hctx.GetDb(ctx)
|
||||||
err = lib.ReliableDbCreate(db, *entry)
|
err = lib.ReliableDbCreate(db, *entry)
|
||||||
@ -381,34 +379,11 @@ func buildHistoryEntry(ctx context.Context, args []string) (*data.HistoryEntry,
|
|||||||
entry.EndTime = time.Now().UTC()
|
entry.EndTime = time.Now().UTC()
|
||||||
|
|
||||||
// command
|
// command
|
||||||
if shell == "bash" {
|
cmd, err := extractCommandFromArg(ctx, shell, args[4] /* isPresave = */, false)
|
||||||
cmd, err := getLastCommand(args[4])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to build history entry: %w", err)
|
|
||||||
}
|
|
||||||
shouldBeSkipped, err := shouldSkipHiddenCommand(ctx, args[4])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to check if command was hidden: %w", err)
|
|
||||||
}
|
|
||||||
if shouldBeSkipped || strings.HasPrefix(cmd, " ") {
|
|
||||||
// Don't save commands that start with a space
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
cmd, err = maybeSkipBashHistTimePrefix(cmd)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
entry.Command = cmd
|
entry.Command = cmd
|
||||||
} else if shell == "zsh" || shell == "fish" {
|
|
||||||
cmd := trimTrailingWhitespace(args[4])
|
|
||||||
if strings.HasPrefix(cmd, " ") {
|
|
||||||
// Don't save commands that start with a space
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
entry.Command = cmd
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("tried to save a hishtory entry from an unsupported shell=%#v", shell)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(entry.Command) == "" {
|
if strings.TrimSpace(entry.Command) == "" {
|
||||||
// Skip recording empty commands where the user just hits enter in their terminal
|
// Skip recording empty commands where the user just hits enter in their terminal
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -417,6 +392,38 @@ func buildHistoryEntry(ctx context.Context, args []string) (*data.HistoryEntry,
|
|||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractCommandFromArg(ctx context.Context, shell, arg string, isPresave bool) (string, error) {
|
||||||
|
if shell == "bash" {
|
||||||
|
cmd, err := getLastCommand(arg)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to build history entry: %w", err)
|
||||||
|
}
|
||||||
|
shouldBeSkipped, err := shouldSkipHiddenCommand(ctx, arg, isPresave)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to check if command was hidden: %w", err)
|
||||||
|
}
|
||||||
|
if shouldBeSkipped || strings.HasPrefix(cmd, " ") {
|
||||||
|
// Don't save commands that start with a space
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
cmd, err = maybeSkipBashHistTimePrefix(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
} else if shell == "zsh" || shell == "fish" {
|
||||||
|
cmd := trimTrailingWhitespace(arg)
|
||||||
|
if strings.HasPrefix(cmd, " ") {
|
||||||
|
// Don't save commands that start with a space
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("tried to save a hishtory entry from an unsupported shell=%#v", shell)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func trimTrailingWhitespace(s string) string {
|
func trimTrailingWhitespace(s string) string {
|
||||||
return strings.TrimSuffix(strings.TrimSuffix(s, "\n"), " ")
|
return strings.TrimSuffix(strings.TrimSuffix(s, "\n"), " ")
|
||||||
}
|
}
|
||||||
@ -559,12 +566,19 @@ func getLastCommand(history string) (string, error) {
|
|||||||
return split[1], nil
|
return split[1], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldSkipHiddenCommand(ctx context.Context, historyLine string) (bool, error) {
|
func shouldSkipHiddenCommand(ctx context.Context, historyLine string, isPresave bool) (bool, error) {
|
||||||
config := hctx.GetConf(ctx)
|
config := hctx.GetConf(ctx)
|
||||||
if config.LastSavedHistoryLine == historyLine {
|
if isPresave && config.LastPreSavedHistoryLine == historyLine {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
if !isPresave && config.LastSavedHistoryLine == historyLine {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if isPresave {
|
||||||
|
config.LastPreSavedHistoryLine = historyLine
|
||||||
|
} else {
|
||||||
config.LastSavedHistoryLine = historyLine
|
config.LastSavedHistoryLine = historyLine
|
||||||
|
}
|
||||||
err := hctx.SetConfig(config)
|
err := hctx.SetConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
@ -10,16 +10,17 @@ import (
|
|||||||
"github.com/ddworken/hishtory/client/hctx"
|
"github.com/ddworken/hishtory/client/hctx"
|
||||||
"github.com/ddworken/hishtory/client/lib"
|
"github.com/ddworken/hishtory/client/lib"
|
||||||
"github.com/ddworken/hishtory/shared/testutils"
|
"github.com/ddworken/hishtory/shared/testutils"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuildHistoryEntry(t *testing.T) {
|
func TestBuildHistoryEntry(t *testing.T) {
|
||||||
defer testutils.BackupAndRestore(t)()
|
defer testutils.BackupAndRestore(t)()
|
||||||
defer testutils.RunTestServer()()
|
defer testutils.RunTestServer()()
|
||||||
testutils.Check(t, lib.Setup("", false))
|
require.NoError(t, lib.Setup("", false))
|
||||||
|
|
||||||
// Test building an actual entry for bash
|
// Test building an actual entry for bash
|
||||||
entry, err := buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "bash", "120", " 123 ls /foo ", "1641774958"})
|
entry, err := buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "bash", "120", " 123 ls /foo ", "1641774958"})
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if entry.ExitCode != 120 {
|
if entry.ExitCode != 120 {
|
||||||
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
|
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
|
||||||
}
|
}
|
||||||
@ -48,7 +49,7 @@ func TestBuildHistoryEntry(t *testing.T) {
|
|||||||
|
|
||||||
// Test building an entry for zsh
|
// Test building an entry for zsh
|
||||||
entry, err = buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "zsh", "120", "ls /foo\n", "1641774958"})
|
entry, err = buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "zsh", "120", "ls /foo\n", "1641774958"})
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if entry.ExitCode != 120 {
|
if entry.ExitCode != 120 {
|
||||||
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
|
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
|
||||||
}
|
}
|
||||||
@ -73,7 +74,7 @@ func TestBuildHistoryEntry(t *testing.T) {
|
|||||||
|
|
||||||
// Test building an entry for fish
|
// Test building an entry for fish
|
||||||
entry, err = buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "fish", "120", "ls /foo\n", "1641774958"})
|
entry, err = buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "fish", "120", "ls /foo\n", "1641774958"})
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if entry.ExitCode != 120 {
|
if entry.ExitCode != 120 {
|
||||||
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
|
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
|
||||||
}
|
}
|
||||||
@ -98,7 +99,7 @@ func TestBuildHistoryEntry(t *testing.T) {
|
|||||||
|
|
||||||
// Test building an entry that is empty, and thus not saved
|
// Test building an entry that is empty, and thus not saved
|
||||||
entry, err = buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "zsh", "120", " \n", "1641774958"})
|
entry, err = buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "zsh", "120", " \n", "1641774958"})
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if entry != nil {
|
if entry != nil {
|
||||||
t.Fatalf("expected history entry to be nil")
|
t.Fatalf("expected history entry to be nil")
|
||||||
}
|
}
|
||||||
@ -108,7 +109,7 @@ func TestBuildHistoryEntryWithTimestampStripping(t *testing.T) {
|
|||||||
defer testutils.BackupAndRestoreEnv("HISTTIMEFORMAT")()
|
defer testutils.BackupAndRestoreEnv("HISTTIMEFORMAT")()
|
||||||
defer testutils.BackupAndRestore(t)()
|
defer testutils.BackupAndRestore(t)()
|
||||||
defer testutils.RunTestServer()()
|
defer testutils.RunTestServer()()
|
||||||
testutils.Check(t, lib.Setup("", false))
|
require.NoError(t, lib.Setup("", false))
|
||||||
|
|
||||||
testcases := []struct {
|
testcases := []struct {
|
||||||
input, histtimeformat, expectedCommand string
|
input, histtimeformat, expectedCommand string
|
||||||
@ -120,11 +121,11 @@ func TestBuildHistoryEntryWithTimestampStripping(t *testing.T) {
|
|||||||
for _, tc := range testcases {
|
for _, tc := range testcases {
|
||||||
conf := hctx.GetConf(hctx.MakeContext())
|
conf := hctx.GetConf(hctx.MakeContext())
|
||||||
conf.LastSavedHistoryLine = ""
|
conf.LastSavedHistoryLine = ""
|
||||||
testutils.Check(t, hctx.SetConfig(conf))
|
require.NoError(t, hctx.SetConfig(conf))
|
||||||
|
|
||||||
os.Setenv("HISTTIMEFORMAT", tc.histtimeformat)
|
os.Setenv("HISTTIMEFORMAT", tc.histtimeformat)
|
||||||
entry, err := buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "bash", "120", tc.input, "1641774958"})
|
entry, err := buildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "bash", "120", tc.input, "1641774958"})
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
t.Fatalf("entry is unexpectedly nil")
|
t.Fatalf("entry is unexpectedly nil")
|
||||||
}
|
}
|
||||||
@ -136,12 +137,12 @@ func TestBuildHistoryEntryWithTimestampStripping(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseCrossPlatformInt(t *testing.T) {
|
func TestParseCrossPlatformInt(t *testing.T) {
|
||||||
res, err := parseCrossPlatformInt("123")
|
res, err := parseCrossPlatformInt("123")
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if res != 123 {
|
if res != 123 {
|
||||||
t.Fatalf("failed to parse cross platform int %d", res)
|
t.Fatalf("failed to parse cross platform int %d", res)
|
||||||
}
|
}
|
||||||
res, err = parseCrossPlatformInt("123N")
|
res, err = parseCrossPlatformInt("123N")
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if res != 123 {
|
if res != 123 {
|
||||||
t.Fatalf("failed to parse cross platform int %d", res)
|
t.Fatalf("failed to parse cross platform int %d", res)
|
||||||
}
|
}
|
||||||
@ -177,7 +178,7 @@ func TestGetLastCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tc := range testcases {
|
for _, tc := range testcases {
|
||||||
actualOutput, err := getLastCommand(tc.input)
|
actualOutput, err := getLastCommand(tc.input)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if actualOutput != tc.expectedOutput {
|
if actualOutput != tc.expectedOutput {
|
||||||
t.Fatalf("getLastCommand(%#v) returned %#v (expected=%#v)", tc.input, actualOutput, tc.expectedOutput)
|
t.Fatalf("getLastCommand(%#v) returned %#v (expected=%#v)", tc.input, actualOutput, tc.expectedOutput)
|
||||||
}
|
}
|
||||||
@ -219,7 +220,7 @@ func TestMaybeSkipBashHistTimePrefix(t *testing.T) {
|
|||||||
for _, tc := range testcases {
|
for _, tc := range testcases {
|
||||||
os.Setenv("HISTTIMEFORMAT", tc.env)
|
os.Setenv("HISTTIMEFORMAT", tc.env)
|
||||||
stripped, err := maybeSkipBashHistTimePrefix(tc.cmdLine)
|
stripped, err := maybeSkipBashHistTimePrefix(tc.cmdLine)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if stripped != tc.expected {
|
if stripped != tc.expected {
|
||||||
t.Fatalf("skipping the time prefix returned %#v (expected=%#v for %#v)", stripped, tc.expected, tc.cmdLine)
|
t.Fatalf("skipping the time prefix returned %#v (expected=%#v for %#v)", stripped, tc.expected, tc.cmdLine)
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/ddworken/hishtory/shared/testutils"
|
"github.com/ddworken/hishtory/shared/testutils"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type operation struct {
|
type operation struct {
|
||||||
@ -79,11 +80,11 @@ func fuzzTest(t *testing.T, tester shellTester, input string) {
|
|||||||
switchToDevice(&devices, op.device)
|
switchToDevice(&devices, op.device)
|
||||||
if op.cmd != "" {
|
if op.cmd != "" {
|
||||||
_, err := tester.RunInteractiveShellRelaxed(t, op.cmd)
|
_, err := tester.RunInteractiveShellRelaxed(t, op.cmd)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
if op.redactQuery != "" {
|
if op.redactQuery != "" {
|
||||||
_, err := tester.RunInteractiveShellRelaxed(t, `HISHTORY_REDACT_FORCE=1 hishtory redact `+op.redactQuery)
|
_, err := tester.RunInteractiveShellRelaxed(t, `HISHTORY_REDACT_FORCE=1 hishtory redact `+op.redactQuery)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the expected output of hishtory export
|
// Calculate the expected output of hishtory export
|
||||||
@ -111,7 +112,7 @@ func fuzzTest(t *testing.T, tester shellTester, input string) {
|
|||||||
|
|
||||||
// Run hishtory export and check the output
|
// Run hishtory export and check the output
|
||||||
out, err := tester.RunInteractiveShellRelaxed(t, `hishtory export -export -pipefail`)
|
out, err := tester.RunInteractiveShellRelaxed(t, `hishtory export -export -pipefail`)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
expectedOutput := keyToCommands[op.device.key]
|
expectedOutput := keyToCommands[op.device.key]
|
||||||
if diff := cmp.Diff(expectedOutput, out); diff != "" {
|
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)
|
t.Fatalf("hishtory export mismatch for input=%#v key=%s (-expected +got):\n%s\nout=%#v", input, op.device.key, diff, out)
|
||||||
@ -122,7 +123,7 @@ func fuzzTest(t *testing.T, tester shellTester, input string) {
|
|||||||
for _, op := range ops {
|
for _, op := range ops {
|
||||||
switchToDevice(&devices, op.device)
|
switchToDevice(&devices, op.device)
|
||||||
out, err := tester.RunInteractiveShellRelaxed(t, `hishtory export -export -pipefail`)
|
out, err := tester.RunInteractiveShellRelaxed(t, `hishtory export -export -pipefail`)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
expectedOutput := keyToCommands[op.device.key]
|
expectedOutput := keyToCommands[op.device.key]
|
||||||
if diff := cmp.Diff(expectedOutput, out); diff != "" {
|
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)
|
t.Fatalf("hishtory export mismatch for key=%s (-expected +got):\n%s\nout=%#v", op.device.key, diff, out)
|
||||||
|
@ -168,6 +168,8 @@ type ClientConfig struct {
|
|||||||
// A device ID used to track which history entry came from which device for remote syncing
|
// A device ID used to track which history entry came from which device for remote syncing
|
||||||
DeviceId string `json:"device_id"`
|
DeviceId string `json:"device_id"`
|
||||||
// Used for skipping history entries prefixed with a space in bash
|
// Used for skipping history entries prefixed with a space in bash
|
||||||
|
LastPreSavedHistoryLine string `json:"last_presaved_history_line"`
|
||||||
|
// Used for skipping history entries prefixed with a space in bash
|
||||||
LastSavedHistoryLine string `json:"last_saved_history_line"`
|
LastSavedHistoryLine string `json:"last_saved_history_line"`
|
||||||
// Used for uploading history entries that we failed to upload due to a missing network connection
|
// Used for uploading history entries that we failed to upload due to a missing network connection
|
||||||
HaveMissedUploads bool `json:"have_missed_uploads"`
|
HaveMissedUploads bool `json:"have_missed_uploads"`
|
||||||
|
@ -2,8 +2,8 @@ function _hishtory_post_exec --on-event fish_preexec
|
|||||||
# Runs after <ENTER>, but before the command is executed
|
# Runs after <ENTER>, but before the command is executed
|
||||||
set --global _hishtory_command $argv
|
set --global _hishtory_command $argv
|
||||||
set --global _hishtory_start_time (date +%s)
|
set --global _hishtory_start_time (date +%s)
|
||||||
hishtory presaveHistoryEntry bash "$_hishtory_command" $_hishtory_start_time & # Background Run
|
hishtory presaveHistoryEntry fish "$_hishtory_command" $_hishtory_start_time & # Background Run
|
||||||
# hishtory presaveHistoryEntry bash "$_hishtory_command" $_hishtory_start_time # Foreground Run
|
# hishtory presaveHistoryEntry fish "$_hishtory_command" $_hishtory_start_time # Foreground Run
|
||||||
end
|
end
|
||||||
|
|
||||||
set --global _hishtory_first_prompt 1
|
set --global _hishtory_first_prompt 1
|
||||||
|
@ -14,9 +14,10 @@ function __hishtory_precommand() {
|
|||||||
|
|
||||||
# Run before every command
|
# Run before every command
|
||||||
HISHTORY_START_TIME=`date +%s`
|
HISHTORY_START_TIME=`date +%s`
|
||||||
if ! [ -z "BASH_COMMAND " ] && [ "$BASH_COMMAND" != "__hishtory_postcommand" ]; then
|
CMD=`history 1`
|
||||||
(hishtory presaveHistoryEntry bash "$BASH_COMMAND" $HISHTORY_START_TIME &) # Background Run
|
if ! [ -z "CMD " ] ; then
|
||||||
# hishtory presaveHistoryEntry bash "$BASH_COMMAND" $HISHTORY_START_TIME # Foreground Run
|
(hishtory presaveHistoryEntry bash "$CMD" $HISHTORY_START_TIME &) # Background Run
|
||||||
|
# hishtory presaveHistoryEntry bash "$CMD" $HISHTORY_START_TIME # Foreground Run
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
trap "__hishtory_precommand" DEBUG
|
trap "__hishtory_precommand" DEBUG
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
-pipefail
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Search Query: > -pipefail
|
Search Query: > -pipefail
|
||||||
|
|
||||||
┌───────────────────────────────────────────────────────────────────────────┐
|
┌───────────────────────────────────────────────────────────────────────────┐
|
||||||
|
@ -170,6 +170,21 @@ func BuildTableRow(ctx context.Context, columnNames []string, entry data.History
|
|||||||
return row, nil
|
return row, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make a regex that matches the non-tokenized bits of the given query
|
||||||
|
func MakeRegexFromQuery(query string) string {
|
||||||
|
tokens := tokenize(strings.TrimSpace(query))
|
||||||
|
r := ""
|
||||||
|
for _, token := range tokens {
|
||||||
|
if !strings.HasPrefix(token, "-") && !containsUnescaped(token, ":") {
|
||||||
|
if r != "" {
|
||||||
|
r += "|"
|
||||||
|
}
|
||||||
|
r += fmt.Sprintf("(%s)", regexp.QuoteMeta(token))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func stringArrayToAnyArray(arr []string) []any {
|
func stringArrayToAnyArray(arr []string) []any {
|
||||||
ret := make([]any, 0)
|
ret := make([]any, 0)
|
||||||
for _, item := range arr {
|
for _, item := range arr {
|
||||||
@ -711,10 +726,7 @@ func where(tx *gorm.DB, s string, v1 any, v2 any) *gorm.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func MakeWhereQueryFromSearch(ctx context.Context, db *gorm.DB, query string) (*gorm.DB, error) {
|
func MakeWhereQueryFromSearch(ctx context.Context, db *gorm.DB, query string) (*gorm.DB, error) {
|
||||||
tokens, err := tokenize(query)
|
tokens := tokenize(query)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to tokenize query: %w", err)
|
|
||||||
}
|
|
||||||
tx := db.Model(&data.HistoryEntry{}).Where("true")
|
tx := db.Model(&data.HistoryEntry{}).Where("true")
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
if strings.HasPrefix(token, "-") {
|
if strings.HasPrefix(token, "-") {
|
||||||
@ -891,11 +903,11 @@ func getAllCustomColumnNames(ctx context.Context) ([]string, error) {
|
|||||||
return ccNames, nil
|
return ccNames, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func tokenize(query string) ([]string, error) {
|
func tokenize(query string) []string {
|
||||||
if query == "" {
|
if query == "" {
|
||||||
return []string{}, nil
|
return []string{}
|
||||||
}
|
}
|
||||||
return splitEscaped(query, ' ', -1), nil
|
return splitEscaped(query, ' ', -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitEscaped(query string, separator rune, maxSplit int) []string {
|
func splitEscaped(query string, separator rune, maxSplit int) []string {
|
||||||
|
@ -26,16 +26,16 @@ func TestSetup(t *testing.T) {
|
|||||||
defer testutils.RunTestServer()()
|
defer testutils.RunTestServer()()
|
||||||
|
|
||||||
homedir, err := os.UserHomeDir()
|
homedir, err := os.UserHomeDir()
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if _, err := os.Stat(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH)); err == nil {
|
if _, err := os.Stat(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH)); err == nil {
|
||||||
t.Fatalf("hishtory secret file already exists!")
|
t.Fatalf("hishtory secret file already exists!")
|
||||||
}
|
}
|
||||||
testutils.Check(t, Setup("", false))
|
require.NoError(t, Setup("", false))
|
||||||
if _, err := os.Stat(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH)); err != nil {
|
if _, err := os.Stat(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH)); err != nil {
|
||||||
t.Fatalf("hishtory secret file does not exist after Setup()!")
|
t.Fatalf("hishtory secret file does not exist after Setup()!")
|
||||||
}
|
}
|
||||||
data, err := os.ReadFile(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH))
|
data, err := os.ReadFile(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH))
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if len(data) < 10 {
|
if len(data) < 10 {
|
||||||
t.Fatalf("hishtory secret has unexpected length: %d", len(data))
|
t.Fatalf("hishtory secret has unexpected length: %d", len(data))
|
||||||
}
|
}
|
||||||
@ -50,16 +50,16 @@ func TestSetupOffline(t *testing.T) {
|
|||||||
defer testutils.RunTestServer()()
|
defer testutils.RunTestServer()()
|
||||||
|
|
||||||
homedir, err := os.UserHomeDir()
|
homedir, err := os.UserHomeDir()
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if _, err := os.Stat(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH)); err == nil {
|
if _, err := os.Stat(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH)); err == nil {
|
||||||
t.Fatalf("hishtory secret file already exists!")
|
t.Fatalf("hishtory secret file already exists!")
|
||||||
}
|
}
|
||||||
testutils.Check(t, Setup("", true))
|
require.NoError(t, Setup("", true))
|
||||||
if _, err := os.Stat(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH)); err != nil {
|
if _, err := os.Stat(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH)); err != nil {
|
||||||
t.Fatalf("hishtory secret file does not exist after Setup()!")
|
t.Fatalf("hishtory secret file does not exist after Setup()!")
|
||||||
}
|
}
|
||||||
data, err := os.ReadFile(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH))
|
data, err := os.ReadFile(path.Join(homedir, data.GetHishtoryPath(), data.CONFIG_PATH))
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if len(data) < 10 {
|
if len(data) < 10 {
|
||||||
t.Fatalf("hishtory secret has unexpected length: %d", len(data))
|
t.Fatalf("hishtory secret has unexpected length: %d", len(data))
|
||||||
}
|
}
|
||||||
@ -70,14 +70,14 @@ func TestSetupOffline(t *testing.T) {
|
|||||||
}
|
}
|
||||||
func TestPersist(t *testing.T) {
|
func TestPersist(t *testing.T) {
|
||||||
defer testutils.BackupAndRestore(t)()
|
defer testutils.BackupAndRestore(t)()
|
||||||
testutils.Check(t, hctx.InitConfig())
|
require.NoError(t, hctx.InitConfig())
|
||||||
db := hctx.GetDb(hctx.MakeContext())
|
db := hctx.GetDb(hctx.MakeContext())
|
||||||
|
|
||||||
entry := testutils.MakeFakeHistoryEntry("ls ~/")
|
entry := testutils.MakeFakeHistoryEntry("ls ~/")
|
||||||
testutils.Check(t, db.Create(entry).Error)
|
require.NoError(t, db.Create(entry).Error)
|
||||||
var historyEntries []*data.HistoryEntry
|
var historyEntries []*data.HistoryEntry
|
||||||
result := db.Find(&historyEntries)
|
result := db.Find(&historyEntries)
|
||||||
testutils.Check(t, result.Error)
|
require.NoError(t, result.Error)
|
||||||
if len(historyEntries) != 1 {
|
if len(historyEntries) != 1 {
|
||||||
t.Fatalf("DB has %d entries, expected 1!", len(historyEntries))
|
t.Fatalf("DB has %d entries, expected 1!", len(historyEntries))
|
||||||
}
|
}
|
||||||
@ -89,19 +89,19 @@ func TestPersist(t *testing.T) {
|
|||||||
|
|
||||||
func TestSearch(t *testing.T) {
|
func TestSearch(t *testing.T) {
|
||||||
defer testutils.BackupAndRestore(t)()
|
defer testutils.BackupAndRestore(t)()
|
||||||
testutils.Check(t, hctx.InitConfig())
|
require.NoError(t, hctx.InitConfig())
|
||||||
ctx := hctx.MakeContext()
|
ctx := hctx.MakeContext()
|
||||||
db := hctx.GetDb(ctx)
|
db := hctx.GetDb(ctx)
|
||||||
|
|
||||||
// Insert data
|
// Insert data
|
||||||
entry1 := testutils.MakeFakeHistoryEntry("ls /foo")
|
entry1 := testutils.MakeFakeHistoryEntry("ls /foo")
|
||||||
testutils.Check(t, db.Create(entry1).Error)
|
require.NoError(t, db.Create(entry1).Error)
|
||||||
entry2 := testutils.MakeFakeHistoryEntry("ls /bar")
|
entry2 := testutils.MakeFakeHistoryEntry("ls /bar")
|
||||||
testutils.Check(t, db.Create(entry2).Error)
|
require.NoError(t, db.Create(entry2).Error)
|
||||||
|
|
||||||
// Search for data
|
// Search for data
|
||||||
results, err := Search(ctx, db, "ls", 5)
|
results, err := Search(ctx, db, "ls", 5)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if len(results) != 2 {
|
if len(results) != 2 {
|
||||||
t.Fatalf("Search() returned %d results, expected 2, results=%#v", len(results), results)
|
t.Fatalf("Search() returned %d results, expected 2, results=%#v", len(results), results)
|
||||||
}
|
}
|
||||||
@ -114,61 +114,61 @@ func TestSearch(t *testing.T) {
|
|||||||
|
|
||||||
// Search but exclude bar
|
// Search but exclude bar
|
||||||
results, err = Search(ctx, db, "ls -bar", 5)
|
results, err = Search(ctx, db, "ls -bar", 5)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if len(results) != 1 {
|
if len(results) != 1 {
|
||||||
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
|
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search but exclude foo
|
// Search but exclude foo
|
||||||
results, err = Search(ctx, db, "ls -foo", 5)
|
results, err = Search(ctx, db, "ls -foo", 5)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if len(results) != 1 {
|
if len(results) != 1 {
|
||||||
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
|
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search but include / also
|
// Search but include / also
|
||||||
results, err = Search(ctx, db, "ls /", 5)
|
results, err = Search(ctx, db, "ls /", 5)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if len(results) != 2 {
|
if len(results) != 2 {
|
||||||
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
|
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search but exclude slash
|
// Search but exclude slash
|
||||||
results, err = Search(ctx, db, "ls -/", 5)
|
results, err = Search(ctx, db, "ls -/", 5)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if len(results) != 0 {
|
if len(results) != 0 {
|
||||||
t.Fatalf("Search() returned %d results, expected 0, results=%#v", len(results), results)
|
t.Fatalf("Search() returned %d results, expected 0, results=%#v", len(results), results)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tests for escaping
|
// Tests for escaping
|
||||||
testutils.Check(t, db.Create(testutils.MakeFakeHistoryEntry("ls -baz")).Error)
|
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("ls -baz")).Error)
|
||||||
results, err = Search(ctx, db, "ls", 5)
|
results, err = Search(ctx, db, "ls", 5)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if len(results) != 3 {
|
if len(results) != 3 {
|
||||||
t.Fatalf("Search() returned %d results, expected 3, results=%#v", len(results), results)
|
t.Fatalf("Search() returned %d results, expected 3, results=%#v", len(results), results)
|
||||||
}
|
}
|
||||||
results, err = Search(ctx, db, "ls -baz", 5)
|
results, err = Search(ctx, db, "ls -baz", 5)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if len(results) != 2 {
|
if len(results) != 2 {
|
||||||
t.Fatalf("Search() returned %d results, expected 2, results=%#v", len(results), results)
|
t.Fatalf("Search() returned %d results, expected 2, results=%#v", len(results), results)
|
||||||
}
|
}
|
||||||
results, err = Search(ctx, db, "ls \\-baz", 5)
|
results, err = Search(ctx, db, "ls \\-baz", 5)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if len(results) != 1 {
|
if len(results) != 1 {
|
||||||
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
|
t.Fatalf("Search() returned %d results, expected 1, results=%#v", len(results), results)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A malformed search query, but we should just ignore the dash since this is a common enough thing
|
// A malformed search query, but we should just ignore the dash since this is a common enough thing
|
||||||
results, err = Search(ctx, db, "ls -", 5)
|
results, err = Search(ctx, db, "ls -", 5)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if len(results) != 3 {
|
if len(results) != 3 {
|
||||||
t.Fatalf("Search() returned %d results, expected 3, results=%#v", len(results), results)
|
t.Fatalf("Search() returned %d results, expected 3, results=%#v", len(results), results)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A search for an entry containing a backslash
|
// A search for an entry containing a backslash
|
||||||
testutils.Check(t, db.Create(testutils.MakeFakeHistoryEntry("echo '\\'")).Error)
|
require.NoError(t, db.Create(testutils.MakeFakeHistoryEntry("echo '\\'")).Error)
|
||||||
results, err = Search(ctx, db, "\\\\", 5)
|
results, err = Search(ctx, db, "\\\\", 5)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if len(results) != 1 {
|
if len(results) != 1 {
|
||||||
t.Fatalf("Search() returned %d results, expected 3, results=%#v", len(results), results)
|
t.Fatalf("Search() returned %d results, expected 3, results=%#v", len(results), results)
|
||||||
}
|
}
|
||||||
@ -177,7 +177,7 @@ func TestSearch(t *testing.T) {
|
|||||||
func TestAddToDbIfNew(t *testing.T) {
|
func TestAddToDbIfNew(t *testing.T) {
|
||||||
// Set up
|
// Set up
|
||||||
defer testutils.BackupAndRestore(t)()
|
defer testutils.BackupAndRestore(t)()
|
||||||
testutils.Check(t, hctx.InitConfig())
|
require.NoError(t, hctx.InitConfig())
|
||||||
db := hctx.GetDb(hctx.MakeContext())
|
db := hctx.GetDb(hctx.MakeContext())
|
||||||
|
|
||||||
// Add duplicate entries
|
// Add duplicate entries
|
||||||
@ -239,52 +239,52 @@ func TestZshWeirdness(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseTimeGenerously(t *testing.T) {
|
func TestParseTimeGenerously(t *testing.T) {
|
||||||
ts, err := parseTimeGenerously("2006-01-02T15:04:00-08:00")
|
ts, err := parseTimeGenerously("2006-01-02T15:04:00-08:00")
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if ts.Unix() != 1136243040 {
|
if ts.Unix() != 1136243040 {
|
||||||
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
||||||
}
|
}
|
||||||
ts, err = parseTimeGenerously("2006-01-02 T15:04:00 -08:00")
|
ts, err = parseTimeGenerously("2006-01-02 T15:04:00 -08:00")
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if ts.Unix() != 1136243040 {
|
if ts.Unix() != 1136243040 {
|
||||||
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
||||||
}
|
}
|
||||||
ts, err = parseTimeGenerously("2006-01-02_T15:04:00_-08:00")
|
ts, err = parseTimeGenerously("2006-01-02_T15:04:00_-08:00")
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if ts.Unix() != 1136243040 {
|
if ts.Unix() != 1136243040 {
|
||||||
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
||||||
}
|
}
|
||||||
ts, err = parseTimeGenerously("2006-01-02T15:04:00")
|
ts, err = parseTimeGenerously("2006-01-02T15:04:00")
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
|
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
|
||||||
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
||||||
}
|
}
|
||||||
ts, err = parseTimeGenerously("2006-01-02_T15:04:00")
|
ts, err = parseTimeGenerously("2006-01-02_T15:04:00")
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
|
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
|
||||||
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
||||||
}
|
}
|
||||||
ts, err = parseTimeGenerously("2006-01-02_15:04:00")
|
ts, err = parseTimeGenerously("2006-01-02_15:04:00")
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
|
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
|
||||||
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
||||||
}
|
}
|
||||||
ts, err = parseTimeGenerously("2006-01-02T15:04")
|
ts, err = parseTimeGenerously("2006-01-02T15:04")
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
|
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
|
||||||
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
||||||
}
|
}
|
||||||
ts, err = parseTimeGenerously("2006-01-02_15:04")
|
ts, err = parseTimeGenerously("2006-01-02_15:04")
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
|
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
|
||||||
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
||||||
}
|
}
|
||||||
ts, err = parseTimeGenerously("2006-01-02")
|
ts, err = parseTimeGenerously("2006-01-02")
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 0 || ts.Minute() != 0 || ts.Second() != 0 {
|
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 0 || ts.Minute() != 0 || ts.Second() != 0 {
|
||||||
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
|
||||||
}
|
}
|
||||||
ts, err = parseTimeGenerously("1693163976")
|
ts, err = parseTimeGenerously("1693163976")
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if ts.Year() != 2023 || ts.Month() != time.August || ts.Day() != 27 || ts.Hour() != 12 || ts.Minute() != 19 || ts.Second() != 36 {
|
if ts.Year() != 2023 || ts.Month() != time.August || ts.Day() != 27 || ts.Hour() != 12 || ts.Minute() != 19 || ts.Second() != 36 {
|
||||||
t.Fatalf("parsed time incorrectly: %d %s", ts.Unix(), ts.GoString())
|
t.Fatalf("parsed time incorrectly: %d %s", ts.Unix(), ts.GoString())
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// Forked from https://github.com/charmbracelet/bubbles/blob/master/table/table.go to add horizontal scrolling
|
// Forked from https://github.com/charmbracelet/bubbles/blob/master/table/table.go to add horizontal scrolling
|
||||||
|
// Also includes https://github.com/charmbracelet/bubbles/pull/397/files to support cell styling
|
||||||
|
|
||||||
package table
|
package table
|
||||||
|
|
||||||
@ -31,6 +32,13 @@ type Model struct {
|
|||||||
hcursor int
|
hcursor int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CellPosition holds row and column indexes.
|
||||||
|
type CellPosition struct {
|
||||||
|
RowID int
|
||||||
|
Column int
|
||||||
|
IsRowSelected bool
|
||||||
|
}
|
||||||
|
|
||||||
// Row represents one line in the table.
|
// Row represents one line in the table.
|
||||||
type Row []string
|
type Row []string
|
||||||
|
|
||||||
@ -108,6 +116,32 @@ type Styles struct {
|
|||||||
Header lipgloss.Style
|
Header lipgloss.Style
|
||||||
Cell lipgloss.Style
|
Cell lipgloss.Style
|
||||||
Selected lipgloss.Style
|
Selected lipgloss.Style
|
||||||
|
|
||||||
|
// RenderCell is a low-level primitive for stylizing cells.
|
||||||
|
// It is responsible for rendering the selection style. Styles.Cell is ignored.
|
||||||
|
//
|
||||||
|
// Example implementation:
|
||||||
|
// s.RenderCell = func(model table.Model, value string, position table.CellPosition) string {
|
||||||
|
// cellStyle := s.Cell.Copy()
|
||||||
|
//
|
||||||
|
// switch {
|
||||||
|
// case position.IsRowSelected:
|
||||||
|
// return cellStyle.Background(lipgloss.Color("57")).Render(value)
|
||||||
|
// case position.Column == 1:
|
||||||
|
// return cellStyle.Foreground(lipgloss.Color("21")).Render(value)
|
||||||
|
// default:
|
||||||
|
// return cellStyle.Render(value)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
RenderCell func(model Model, value string, position CellPosition) string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Styles) renderCell(model Model, value string, position CellPosition) string {
|
||||||
|
if s.RenderCell != nil {
|
||||||
|
return s.RenderCell(model, value, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Cell.Render(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultStyles returns a set of default style definitions for this table.
|
// DefaultStyles returns a set of default style definitions for this table.
|
||||||
@ -445,21 +479,30 @@ func (m Model) headersView() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderRow(rowID int) string {
|
func (m *Model) renderRow(rowID int) string {
|
||||||
|
isRowSelected := rowID == m.cursor
|
||||||
var s = make([]string, 0, len(m.cols))
|
var s = make([]string, 0, len(m.cols))
|
||||||
for i, value := range m.rows[rowID] {
|
for i, value := range m.rows[rowID] {
|
||||||
style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true)
|
style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true)
|
||||||
|
|
||||||
|
position := CellPosition{
|
||||||
|
RowID: rowID,
|
||||||
|
Column: i,
|
||||||
|
IsRowSelected: isRowSelected,
|
||||||
|
}
|
||||||
|
|
||||||
var renderedCell string
|
var renderedCell string
|
||||||
if i == m.ColIndex(m.hcol) && m.hcursor > 0 {
|
if i == m.ColIndex(m.hcol) && m.hcursor > 0 {
|
||||||
renderedCell = m.styles.Cell.Render(style.Render(runewidth.Truncate(runewidth.TruncateLeft(value, m.hcursor, "…"), m.cols[i].Width, "…")))
|
renderedCell = style.Render(runewidth.Truncate(runewidth.TruncateLeft(value, m.hcursor, "…"), m.cols[i].Width, "…"))
|
||||||
} else {
|
} else {
|
||||||
renderedCell = m.styles.Cell.Render(style.Render(runewidth.Truncate(value, m.cols[i].Width, "…")))
|
renderedCell = style.Render(runewidth.Truncate(value, m.cols[i].Width, "…"))
|
||||||
}
|
}
|
||||||
|
renderedCell = m.styles.renderCell(*m, renderedCell, position)
|
||||||
s = append(s, renderedCell)
|
s = append(s, renderedCell)
|
||||||
}
|
}
|
||||||
|
|
||||||
row := lipgloss.JoinHorizontal(lipgloss.Left, s...)
|
row := lipgloss.JoinHorizontal(lipgloss.Left, s...)
|
||||||
|
|
||||||
if rowID == m.cursor {
|
if isRowSelected {
|
||||||
return m.styles.Selected.Render(row)
|
return m.styles.Selected.Render(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/DataDog/datadog-go/statsd"
|
"github.com/DataDog/datadog-go/statsd"
|
||||||
@ -27,6 +28,7 @@ var GLOBAL_STATSD *statsd.Client
|
|||||||
type shellTester interface {
|
type shellTester interface {
|
||||||
RunInteractiveShell(t testing.TB, script string) string
|
RunInteractiveShell(t testing.TB, script string) string
|
||||||
RunInteractiveShellRelaxed(t testing.TB, script string) (string, error)
|
RunInteractiveShellRelaxed(t testing.TB, script string) (string, error)
|
||||||
|
RunInteractiveShellBackground(t testing.TB, script string) error
|
||||||
ShellName() string
|
ShellName() string
|
||||||
}
|
}
|
||||||
type bashTester struct {
|
type bashTester struct {
|
||||||
@ -58,6 +60,16 @@ func (b bashTester) RunInteractiveShellRelaxed(t testing.TB, script string) (str
|
|||||||
return outStr, nil
|
return outStr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b bashTester) RunInteractiveShellBackground(t testing.TB, script string) error {
|
||||||
|
cmd := exec.Command("bash", "-i")
|
||||||
|
// SetSid: true is required to prevent SIGTTIN signal killing the entire test
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
cmd.Stdin = strings.NewReader(script)
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = nil
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
|
|
||||||
func (b bashTester) ShellName() string {
|
func (b bashTester) ShellName() string {
|
||||||
return "bash"
|
return "bash"
|
||||||
}
|
}
|
||||||
@ -88,6 +100,14 @@ func (z zshTester) RunInteractiveShellRelaxed(t testing.TB, script string) (stri
|
|||||||
return outStr, nil
|
return outStr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (z zshTester) RunInteractiveShellBackground(t testing.TB, script string) error {
|
||||||
|
cmd := exec.Command("zsh", "-is")
|
||||||
|
cmd.Stdin = strings.NewReader(script)
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = nil
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
|
|
||||||
func (z zshTester) ShellName() string {
|
func (z zshTester) ShellName() string {
|
||||||
return "zsh"
|
return "zsh"
|
||||||
}
|
}
|
||||||
@ -198,15 +218,15 @@ func hishtoryQuery(t testing.TB, tester shellTester, query string) string {
|
|||||||
|
|
||||||
func manuallySubmitHistoryEntry(t testing.TB, userSecret string, entry data.HistoryEntry) {
|
func manuallySubmitHistoryEntry(t testing.TB, userSecret string, entry data.HistoryEntry) {
|
||||||
encEntry, err := data.EncryptHistoryEntry(userSecret, entry)
|
encEntry, err := data.EncryptHistoryEntry(userSecret, entry)
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if encEntry.Date != entry.EndTime {
|
if encEntry.Date != entry.EndTime {
|
||||||
t.Fatalf("encEntry.Date does not match the entry")
|
t.Fatalf("encEntry.Date does not match the entry")
|
||||||
}
|
}
|
||||||
jsonValue, err := json.Marshal([]shared.EncHistoryEntry{encEntry})
|
jsonValue, err := json.Marshal([]shared.EncHistoryEntry{encEntry})
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEqual(t, "", entry.DeviceId)
|
require.NotEqual(t, "", entry.DeviceId)
|
||||||
resp, err := http.Post("http://localhost:8080/api/v1/submit?source_device_id="+entry.DeviceId, "application/json", bytes.NewBuffer(jsonValue))
|
resp, err := http.Post("http://localhost:8080/api/v1/submit?source_device_id="+entry.DeviceId, "application/json", bytes.NewBuffer(jsonValue))
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
t.Fatalf("failed to submit result to backend, status_code=%d", resp.StatusCode)
|
t.Fatalf("failed to submit result to backend, status_code=%d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
@ -246,11 +266,41 @@ func captureTerminalOutputWithShellName(t testing.TB, tester shellTester, overri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func captureTerminalOutputWithShellNameAndDimensions(t testing.TB, tester shellTester, overriddenShellName string, width, height int, commands []TmuxCommand) string {
|
func captureTerminalOutputWithShellNameAndDimensions(t testing.TB, tester shellTester, overriddenShellName string, width, height int, commands []TmuxCommand) string {
|
||||||
|
return captureTerminalOutputComplex(t,
|
||||||
|
TmuxCaptureConfig{
|
||||||
|
tester: tester,
|
||||||
|
overriddenShellName: overriddenShellName,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
complexCommands: commands,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type TmuxCaptureConfig struct {
|
||||||
|
tester shellTester
|
||||||
|
overriddenShellName string
|
||||||
|
commands []string
|
||||||
|
complexCommands []TmuxCommand
|
||||||
|
width, height int
|
||||||
|
includeEscapeSequences bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func captureTerminalOutputComplex(t testing.TB, captureConfig TmuxCaptureConfig) string {
|
||||||
|
require.NotNil(t, captureConfig.tester)
|
||||||
|
if captureConfig.overriddenShellName == "" {
|
||||||
|
captureConfig.overriddenShellName = captureConfig.tester.ShellName()
|
||||||
|
}
|
||||||
|
if captureConfig.width == 0 {
|
||||||
|
captureConfig.width = 200
|
||||||
|
}
|
||||||
|
if captureConfig.height == 0 {
|
||||||
|
captureConfig.height = 50
|
||||||
|
}
|
||||||
sleepAmount := "0.1"
|
sleepAmount := "0.1"
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" {
|
||||||
sleepAmount = "0.2"
|
sleepAmount = "0.2"
|
||||||
}
|
}
|
||||||
if overriddenShellName == "fish" {
|
if captureConfig.overriddenShellName == "fish" {
|
||||||
// Fish is considerably slower so this is sadly necessary
|
// Fish is considerably slower so this is sadly necessary
|
||||||
sleepAmount = "0.5"
|
sleepAmount = "0.5"
|
||||||
}
|
}
|
||||||
@ -259,13 +309,20 @@ func captureTerminalOutputWithShellNameAndDimensions(t testing.TB, tester shellT
|
|||||||
}
|
}
|
||||||
fullCommand := ""
|
fullCommand := ""
|
||||||
fullCommand += " tmux kill-session -t foo || true\n"
|
fullCommand += " tmux kill-session -t foo || true\n"
|
||||||
fullCommand += fmt.Sprintf(" tmux -u new-session -d -x %d -y %d -s foo %s\n", width, height, overriddenShellName)
|
fullCommand += fmt.Sprintf(" tmux -u new-session -d -x %d -y %d -s foo %s\n", captureConfig.width, captureConfig.height, captureConfig.overriddenShellName)
|
||||||
fullCommand += " sleep 1\n"
|
fullCommand += " sleep 1\n"
|
||||||
if overriddenShellName == "bash" {
|
if captureConfig.overriddenShellName == "bash" {
|
||||||
fullCommand += " tmux send -t foo SPACE source SPACE ~/.bashrc ENTER\n"
|
fullCommand += " tmux send -t foo SPACE source SPACE ~/.bashrc ENTER\n"
|
||||||
}
|
}
|
||||||
fullCommand += " sleep " + sleepAmount + "\n"
|
fullCommand += " sleep " + sleepAmount + "\n"
|
||||||
for _, cmd := range commands {
|
if len(captureConfig.commands) > 0 {
|
||||||
|
require.Empty(t, captureConfig.complexCommands)
|
||||||
|
for _, command := range captureConfig.commands {
|
||||||
|
captureConfig.complexCommands = append(captureConfig.complexCommands, TmuxCommand{Keys: command})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotEmpty(t, captureConfig.complexCommands)
|
||||||
|
for _, cmd := range captureConfig.complexCommands {
|
||||||
if cmd.Keys != "" {
|
if cmd.Keys != "" {
|
||||||
fullCommand += " tmux send -t foo -- "
|
fullCommand += " tmux send -t foo -- "
|
||||||
fullCommand += cmd.Keys
|
fullCommand += cmd.Keys
|
||||||
@ -283,17 +340,21 @@ func captureTerminalOutputWithShellNameAndDimensions(t testing.TB, tester shellT
|
|||||||
if testutils.IsGithubAction() {
|
if testutils.IsGithubAction() {
|
||||||
fullCommand += " sleep 2.5\n"
|
fullCommand += " sleep 2.5\n"
|
||||||
}
|
}
|
||||||
fullCommand += " tmux capture-pane -t foo -p\n"
|
fullCommand += " tmux capture-pane -t foo -p"
|
||||||
|
if captureConfig.includeEscapeSequences {
|
||||||
|
fullCommand += "e"
|
||||||
|
}
|
||||||
|
fullCommand += "\n"
|
||||||
fullCommand += " tmux kill-session -t foo\n"
|
fullCommand += " tmux kill-session -t foo\n"
|
||||||
testutils.TestLog(t, "Running tmux command: "+fullCommand)
|
testutils.TestLog(t, "Running tmux command: "+fullCommand)
|
||||||
return strings.TrimSpace(tester.RunInteractiveShell(t, fullCommand))
|
return strings.TrimSpace(captureConfig.tester.RunInteractiveShell(t, fullCommand))
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertNoLeakedConnections(t testing.TB) {
|
func assertNoLeakedConnections(t testing.TB) {
|
||||||
resp, err := lib.ApiGet("/api/v1/get-num-connections")
|
resp, err := lib.ApiGet("/api/v1/get-num-connections")
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
numConnections, err := strconv.Atoi(string(resp))
|
numConnections, err := strconv.Atoi(string(resp))
|
||||||
testutils.Check(t, err)
|
require.NoError(t, err)
|
||||||
if numConnections > 1 {
|
if numConnections > 1 {
|
||||||
t.Fatalf("DB has %d open connections, expected to have 1 or less", numConnections)
|
t.Fatalf("DB has %d open connections, expected to have 1 or less", numConnections)
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ import (
|
|||||||
const TABLE_HEIGHT = 20
|
const TABLE_HEIGHT = 20
|
||||||
const PADDED_NUM_ENTRIES = TABLE_HEIGHT * 5
|
const PADDED_NUM_ENTRIES = TABLE_HEIGHT * 5
|
||||||
|
|
||||||
|
var CURRENT_QUERY_FOR_HIGHLIGHTING string = ""
|
||||||
var SELECTED_COMMAND string = ""
|
var SELECTED_COMMAND string = ""
|
||||||
|
|
||||||
var baseStyle = lipgloss.NewStyle().
|
var baseStyle = lipgloss.NewStyle().
|
||||||
@ -207,6 +209,7 @@ func initialModel(ctx context.Context, initialQuery string) model {
|
|||||||
if initialQuery != "" {
|
if initialQuery != "" {
|
||||||
queryInput.SetValue(initialQuery)
|
queryInput.SetValue(initialQuery)
|
||||||
}
|
}
|
||||||
|
CURRENT_QUERY_FOR_HIGHLIGHTING = initialQuery
|
||||||
return model{ctx: ctx, spinner: s, isLoading: true, table: nil, tableEntries: []*data.HistoryEntry{}, runQuery: &initialQuery, queryInput: queryInput, help: help.New()}
|
return model{ctx: ctx, spinner: s, isLoading: true, table: nil, tableEntries: []*data.HistoryEntry{}, runQuery: &initialQuery, queryInput: queryInput, help: help.New()}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,6 +322,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.queryInput = i
|
m.queryInput = i
|
||||||
searchQuery := m.queryInput.Value()
|
searchQuery := m.queryInput.Value()
|
||||||
m.runQuery = &searchQuery
|
m.runQuery = &searchQuery
|
||||||
|
CURRENT_QUERY_FOR_HIGHLIGHTING = searchQuery
|
||||||
cmd3 := runQueryAndUpdateTable(m, false, false)
|
cmd3 := runQueryAndUpdateTable(m, false, false)
|
||||||
preventTableOverscrolling(m)
|
preventTableOverscrolling(m)
|
||||||
return m, tea.Batch(pendingCommands, cmd2, cmd3)
|
return m, tea.Batch(pendingCommands, cmd2, cmd3)
|
||||||
@ -579,6 +583,73 @@ func makeTable(ctx context.Context, rows []table.Row) (table.Model, error) {
|
|||||||
Foreground(lipgloss.Color("229")).
|
Foreground(lipgloss.Color("229")).
|
||||||
Background(lipgloss.Color("57")).
|
Background(lipgloss.Color("57")).
|
||||||
Bold(false)
|
Bold(false)
|
||||||
|
if config.BetaMode {
|
||||||
|
MATCH_NOTHING_REGEXP := regexp.MustCompile("a^")
|
||||||
|
s.RenderCell = func(model table.Model, value string, position table.CellPosition) string {
|
||||||
|
var re *regexp.Regexp
|
||||||
|
CURRENT_QUERY_FOR_HIGHLIGHTING = strings.TrimSpace(CURRENT_QUERY_FOR_HIGHLIGHTING)
|
||||||
|
if CURRENT_QUERY_FOR_HIGHLIGHTING == "" {
|
||||||
|
// If there is no search query, then there is nothing to highlight
|
||||||
|
re = MATCH_NOTHING_REGEXP
|
||||||
|
} else {
|
||||||
|
queryRegex := lib.MakeRegexFromQuery(CURRENT_QUERY_FOR_HIGHLIGHTING)
|
||||||
|
r, err := regexp.Compile(queryRegex)
|
||||||
|
if err != nil {
|
||||||
|
// Failed to compile the regex for highlighting matches, this should never happen. In this
|
||||||
|
// case, just use a regexp that matches nothing to ensure that the TUI doesn't crash.
|
||||||
|
re = MATCH_NOTHING_REGEXP
|
||||||
|
} else {
|
||||||
|
re = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func to render a given chunk of `value`. `isMatching` is whether `v` matches the search query (and
|
||||||
|
// thus needs to be highlighted). `isLeftMost` and `isRightMost` determines whether additional
|
||||||
|
// padding is added (to reproduce the padding that `s.Cell` normally adds).
|
||||||
|
renderChunk := func(v string, isMatching, isLeftMost, isRightMost bool) string {
|
||||||
|
chunkStyle := lipgloss.NewStyle()
|
||||||
|
if position.IsRowSelected {
|
||||||
|
// Apply the selected style as the base style if this is the highlighted row of the table
|
||||||
|
chunkStyle = s.Selected.Copy()
|
||||||
|
}
|
||||||
|
if isLeftMost {
|
||||||
|
chunkStyle = chunkStyle.PaddingLeft(1)
|
||||||
|
}
|
||||||
|
if isRightMost {
|
||||||
|
chunkStyle = chunkStyle.PaddingRight(1)
|
||||||
|
}
|
||||||
|
if isMatching {
|
||||||
|
chunkStyle = chunkStyle.Bold(true)
|
||||||
|
}
|
||||||
|
return chunkStyle.Render(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := re.FindAllStringIndex(value, -1)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
// No matches, so render the entire value
|
||||||
|
return renderChunk(value /*isMatching = */, false /*isLeftMost = */, true /*isRightMost = */, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through the chunks of the value and highlight the relevant pieces
|
||||||
|
ret := ""
|
||||||
|
lastIncludedIdx := 0
|
||||||
|
for _, match := range re.FindAllStringIndex(value, -1) {
|
||||||
|
matchStartIdx := match[0]
|
||||||
|
matchEndIdx := match[1]
|
||||||
|
beforeMatch := value[lastIncludedIdx:matchStartIdx]
|
||||||
|
if beforeMatch != "" {
|
||||||
|
ret += renderChunk(beforeMatch, false, lastIncludedIdx == 0, false)
|
||||||
|
}
|
||||||
|
match := value[matchStartIdx:matchEndIdx]
|
||||||
|
ret += renderChunk(match, true, matchStartIdx == 0, matchEndIdx == len(value))
|
||||||
|
lastIncludedIdx = matchEndIdx
|
||||||
|
}
|
||||||
|
if lastIncludedIdx != len(value) {
|
||||||
|
ret += renderChunk(value[lastIncludedIdx:], false, false, true)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
||||||
t.SetStyles(s)
|
t.SetStyles(s)
|
||||||
t.Focus()
|
t.Focus()
|
||||||
return t, nil
|
return t, nil
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/ddworken/hishtory/client/data"
|
"github.com/ddworken/hishtory/client/data"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -49,7 +50,7 @@ func getInitialWd() string {
|
|||||||
|
|
||||||
func ResetLocalState(t *testing.T) {
|
func ResetLocalState(t *testing.T) {
|
||||||
homedir, err := os.UserHomeDir()
|
homedir, err := os.UserHomeDir()
|
||||||
Check(t, err)
|
require.NoError(t, err)
|
||||||
persistLog()
|
persistLog()
|
||||||
_ = BackupAndRestoreWithId(t, "-reset-local-state")
|
_ = BackupAndRestoreWithId(t, "-reset-local-state")
|
||||||
_ = os.RemoveAll(path.Join(homedir, data.GetHishtoryPath()))
|
_ = os.RemoveAll(path.Join(homedir, data.GetHishtoryPath()))
|
||||||
@ -69,10 +70,10 @@ func getBackPath(file, id string) string {
|
|||||||
func BackupAndRestoreWithId(t testing.TB, id string) func() {
|
func BackupAndRestoreWithId(t testing.TB, id string) func() {
|
||||||
ResetFakeHistoryTimestamp()
|
ResetFakeHistoryTimestamp()
|
||||||
homedir, err := os.UserHomeDir()
|
homedir, err := os.UserHomeDir()
|
||||||
Check(t, err)
|
require.NoError(t, err)
|
||||||
initialWd, err := os.Getwd()
|
initialWd, err := os.Getwd()
|
||||||
Check(t, err)
|
require.NoError(t, err)
|
||||||
Check(t, os.MkdirAll(path.Join(homedir, data.GetHishtoryPath()+".test"), os.ModePerm))
|
require.NoError(t, os.MkdirAll(path.Join(homedir, data.GetHishtoryPath()+".test"), os.ModePerm))
|
||||||
|
|
||||||
renameFiles := []string{
|
renameFiles := []string{
|
||||||
path.Join(homedir, data.GetHishtoryPath(), data.DB_PATH),
|
path.Join(homedir, data.GetHishtoryPath(), data.DB_PATH),
|
||||||
@ -89,7 +90,7 @@ func BackupAndRestoreWithId(t testing.TB, id string) func() {
|
|||||||
}
|
}
|
||||||
for _, file := range renameFiles {
|
for _, file := range renameFiles {
|
||||||
touchFile(file)
|
touchFile(file)
|
||||||
Check(t, os.Rename(file, getBackPath(file, id)))
|
require.NoError(t, os.Rename(file, getBackPath(file, id)))
|
||||||
}
|
}
|
||||||
copyFiles := []string{
|
copyFiles := []string{
|
||||||
path.Join(homedir, ".zshrc"),
|
path.Join(homedir, ".zshrc"),
|
||||||
@ -98,7 +99,7 @@ func BackupAndRestoreWithId(t testing.TB, id string) func() {
|
|||||||
}
|
}
|
||||||
for _, file := range copyFiles {
|
for _, file := range copyFiles {
|
||||||
touchFile(file)
|
touchFile(file)
|
||||||
Check(t, copy(file, getBackPath(file, id)))
|
require.NoError(t, copy(file, getBackPath(file, id)))
|
||||||
}
|
}
|
||||||
configureZshrc(homedir)
|
configureZshrc(homedir)
|
||||||
touchFile(path.Join(homedir, ".bash_history"))
|
touchFile(path.Join(homedir, ".bash_history"))
|
||||||
@ -111,8 +112,8 @@ func BackupAndRestoreWithId(t testing.TB, id string) func() {
|
|||||||
t.Fatalf("failed to execute killall hishtory, stdout=%#v: %v", string(stdout), err)
|
t.Fatalf("failed to execute killall hishtory, stdout=%#v: %v", string(stdout), err)
|
||||||
}
|
}
|
||||||
persistLog()
|
persistLog()
|
||||||
Check(t, os.RemoveAll(path.Join(homedir, data.GetHishtoryPath())))
|
require.NoError(t, os.RemoveAll(path.Join(homedir, data.GetHishtoryPath())))
|
||||||
Check(t, os.MkdirAll(path.Join(homedir, data.GetHishtoryPath()), os.ModePerm))
|
require.NoError(t, os.MkdirAll(path.Join(homedir, data.GetHishtoryPath()), os.ModePerm))
|
||||||
for _, file := range renameFiles {
|
for _, file := range renameFiles {
|
||||||
checkError(os.Rename(getBackPath(file, id), file))
|
checkError(os.Rename(getBackPath(file, id), file))
|
||||||
}
|
}
|
||||||
@ -290,20 +291,6 @@ func RunTestServer() func() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Check(t testing.TB, err error) {
|
|
||||||
if err != nil {
|
|
||||||
_, filename, line, _ := runtime.Caller(1)
|
|
||||||
t.Fatalf("Unexpected error at %s:%d: %v", filename, line, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckWithInfo(t *testing.T, err error, additionalInfo string) {
|
|
||||||
if err != nil {
|
|
||||||
_, filename, line, _ := runtime.Caller(1)
|
|
||||||
t.Fatalf("Unexpected error: %v at %s:%d! Additional info: %v", err, filename, line, additionalInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsOnline() bool {
|
func IsOnline() bool {
|
||||||
_, err := http.Get("https://hishtory.dev")
|
_, err := http.Get("https://hishtory.dev")
|
||||||
return err == nil
|
return err == nil
|
||||||
@ -338,12 +325,12 @@ func IsGithubAction() bool {
|
|||||||
func TestLog(t testing.TB, line string) {
|
func TestLog(t testing.TB, line string) {
|
||||||
f, err := os.OpenFile("/tmp/test.log", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
|
f, err := os.OpenFile("/tmp/test.log", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Check(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
_, err = f.WriteString(line + "\n")
|
_, err = f.WriteString(line + "\n")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Check(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,7 +359,7 @@ func CompareGoldens(t testing.TB, out, goldenName string) {
|
|||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
expected = []byte("ERR_FILE_NOT_FOUND:" + goldenPath)
|
expected = []byte("ERR_FILE_NOT_FOUND:" + goldenPath)
|
||||||
} else {
|
} else {
|
||||||
Check(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if diff := cmp.Diff(string(expected), out); diff != "" {
|
if diff := cmp.Diff(string(expected), out); diff != "" {
|
||||||
@ -380,7 +367,7 @@ func CompareGoldens(t testing.TB, out, goldenName string) {
|
|||||||
_, filename, line, _ := runtime.Caller(1)
|
_, filename, line, _ := runtime.Caller(1)
|
||||||
t.Fatalf("hishtory golden mismatch for %s at %s:%d (-expected +got):\n%s\nactual=\n%s", goldenName, filename, line, diff, out)
|
t.Fatalf("hishtory golden mismatch for %s at %s:%d (-expected +got):\n%s\nactual=\n%s", goldenName, filename, line, diff, out)
|
||||||
} else {
|
} else {
|
||||||
Check(t, os.WriteFile(goldenPath, []byte(out), 0644))
|
require.NoError(t, os.WriteFile(goldenPath, []byte(out), 0644))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user