diff --git a/backend/server/server.go b/backend/server/server.go index 3882556..99ee1f3 100644 --- a/backend/server/server.go +++ b/backend/server/server.go @@ -442,12 +442,20 @@ func applyDeletionRequestsToBackend(ctx context.Context, request shared.Deletion } func wipeDbHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { - if r.Host == "api.hishtory.dev" { + if r.Host == "api.hishtory.dev" || isProductionEnvironment() { panic("refusing to wipe the DB for prod") } checkGormResult(GLOBAL_DB.WithContext(ctx).Exec("DELETE FROM enc_history_entries")) } +func getNumConnectionsHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + sqlDb, err := GLOBAL_DB.DB() + if err != nil { + panic(err) + } + w.Write([]byte(fmt.Sprintf("%#v", sqlDb.Stats().OpenConnections))) +} + func isTestEnvironment() bool { return os.Getenv("HISHTORY_TEST") != "" } @@ -636,6 +644,9 @@ func InitDB() { if isProductionEnvironment() { sqlDb.SetMaxIdleConns(10) } + if isTestEnvironment() { + sqlDb.SetMaxIdleConns(1) + } } func decrementVersion(version string) (string, error) { @@ -828,6 +839,7 @@ func main() { mux.Handle("/internal/api/v1/stats", withLogging(statsHandler)) if isTestEnvironment() { mux.Handle("/api/v1/wipe-db", withLogging(wipeDbHandler)) + mux.Handle("/api/v1/get-num-connections", withLogging(getNumConnectionsHandler)) } fmt.Println("Listening on localhost:8080") log.Fatal(http.ListenAndServe(":8080", mux)) diff --git a/backend/server/server_test.go b/backend/server/server_test.go index 074cc9d..b08e0c3 100644 --- a/backend/server/server_test.go +++ b/backend/server/server_test.go @@ -16,6 +16,7 @@ import ( "github.com/ddworken/hishtory/shared/testutils" "github.com/go-test/deep" "github.com/google/uuid" + "gorm.io/gorm" ) func TestESubmitThenQuery(t *testing.T) { @@ -113,6 +114,9 @@ func TestESubmitThenQuery(t *testing.T) { if len(retrievedEntries) != 2 { t.Fatalf("Expected to retrieve 2 entries, found %d", len(retrievedEntries)) } + + // Assert that we aren't leaking connections + assertNoLeakedConnections(t, GLOBAL_DB) } func TestDumpRequestAndResponse(t *testing.T) { @@ -279,6 +283,9 @@ func TestDumpRequestAndResponse(t *testing.T) { t.Fatalf("DB data is different than input! \ndb =%#v\nentry1=%#v\nentry2=%#v", *dbEntry, entry1Dec, entry2Dec) } } + + // Assert that we aren't leaking connections + assertNoLeakedConnections(t, GLOBAL_DB) } func TestUpdateReleaseVersion(t *testing.T) { @@ -308,6 +315,9 @@ func TestUpdateReleaseVersion(t *testing.T) { if !strings.HasPrefix(ReleaseVersion, "v0.") { t.Fatalf("ReleaseVersion wasn't updated to contain a version: %#v", ReleaseVersion) } + + // Assert that we aren't leaking connections + assertNoLeakedConnections(t, GLOBAL_DB) } func TestDeletionRequests(t *testing.T) { @@ -488,4 +498,18 @@ func TestDeletionRequests(t *testing.T) { if diff := deep.Equal(*deletionRequest, expected); diff != nil { t.Error(diff) } + + // Assert that we aren't leaking connections + assertNoLeakedConnections(t, GLOBAL_DB) +} + +func assertNoLeakedConnections(t *testing.T, db *gorm.DB) { + sqlDB, err := db.DB() + if err != nil { + t.Fatal(err) + } + numConns := sqlDB.Stats().OpenConnections + if numConns > 1 { + t.Fatalf("expected DB to have not leak connections, actually have %d", numConns) + } } diff --git a/client/client_test.go b/client/client_test.go index c250f14..ca53c20 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -172,6 +172,9 @@ func TestP(t *testing.T) { } t.Run("testControlR/offline/bash", func(t *testing.T) { testControlR(t, bashTester{}, "bash", Offline) }) t.Run("testControlR/fish", func(t *testing.T) { testControlR(t, bashTester{}, "fish", Online) }) + + // Assert there are no leaked connections + assertNoLeakedConnections(t) } func testIntegration(t *testing.T, tester shellTester, onlineStatus OnlineStatus) { @@ -298,6 +301,9 @@ yes | hishtory init `+userSecret) tester.RunInteractiveShell(t, `hishtory config-set displayed-columns Hostname 'Exit Code' Command`) out = tester.RunInteractiveShell(t, `hishtory query -pipefail | grep -v 'hishtory init ' | grep -v 'ls /'`) compareGoldens(t, out, "testIntegrationWithNewDevice-table"+tester.ShellName()) + + // Assert there are no leaked connections + assertNoLeakedConnections(t) } func installHishtory(t *testing.T, tester shellTester, userSecret string) string { @@ -453,6 +459,9 @@ echo thisisrecorded`) t.Fatalf("hishtory query doesn't contain the expected complex command, out=%#v", out) } + // Assert there are no leaked connections + assertNoLeakedConnections(t) + return userSecret } @@ -2078,6 +2087,9 @@ func testControlR(t *testing.T, tester shellTester, shellName string, onlineStat if !testutils.IsGithubAction() { compareGoldens(t, out, "testControlR-SelectMultiline-"+shellName) } + + // Assert there are no leaked connections + assertNoLeakedConnections(t) } func testCustomColumns(t *testing.T, tester shellTester) { @@ -2524,6 +2536,16 @@ func FuzzTestMultipleUsers(f *testing.F) { }) } +func assertNoLeakedConnections(t *testing.T) { + resp, err := lib.ApiGet("/api/v1/get-num-connections") + testutils.Check(t, err) + numConnections, err := strconv.Atoi(string(resp)) + testutils.Check(t, err) + if numConnections > 1 { + t.Fatalf("DB has %d open connections, expected to have 1 or less", numConnections) + } +} + // TODO: somehow test/confirm that hishtory works even if only bash/only zsh is installed // TODO: TUI tests for multiline commands