mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-28 23:39:07 +01:00
230 lines
8.3 KiB
Go
230 lines
8.3 KiB
Go
package manager
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs"
|
|
"github.com/netbirdio/netbird/management/internals/shared/db"
|
|
"github.com/netbirdio/netbird/management/internals/shared/db/dbtest"
|
|
"github.com/netbirdio/netbird/management/server/testutil"
|
|
)
|
|
|
|
func newTestRepository(t *testing.T) (accesslogs.Repository, *db.Conn) {
|
|
conn := dbtest.NewConn(t, &accesslogs.AccessLogEntry{})
|
|
return NewRepository(conn), conn
|
|
}
|
|
|
|
func newEntry(id, accountID, method string, age time.Duration) *accesslogs.AccessLogEntry {
|
|
return &accesslogs.AccessLogEntry{
|
|
ID: id,
|
|
AccountID: accountID,
|
|
Method: method,
|
|
Host: "app.example.com",
|
|
Path: "/",
|
|
StatusCode: 200,
|
|
Timestamp: time.Now().Add(-age),
|
|
}
|
|
}
|
|
|
|
func TestSqlRepository_ListByAccount(t *testing.T) {
|
|
repo, _ := newTestRepository(t)
|
|
ctx := context.Background()
|
|
for _, entry := range []*accesslogs.AccessLogEntry{
|
|
newEntry("a1", "acc-a", "GET", 3*time.Hour),
|
|
newEntry("a2", "acc-a", "POST", 2*time.Hour),
|
|
newEntry("a3", "acc-a", "GET", time.Hour),
|
|
newEntry("b1", "acc-b", "GET", time.Hour),
|
|
} {
|
|
require.NoError(t, repo.Create(ctx, entry))
|
|
}
|
|
|
|
logs, total, err := repo.ListByAccount(ctx, db.LockingStrengthNone, "acc-a", accesslogs.AccessLogFilter{Page: 1, PageSize: 2})
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, 3, total)
|
|
require.Len(t, logs, 2)
|
|
assert.Equal(t, "a3", logs[0].ID)
|
|
assert.Equal(t, "a2", logs[1].ID)
|
|
|
|
method := "GET"
|
|
logs, total, err = repo.ListByAccount(ctx, db.LockingStrengthNone, "acc-a", accesslogs.AccessLogFilter{Page: 1, PageSize: 10, Method: &method, SortOrder: "asc"})
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, 2, total)
|
|
require.Len(t, logs, 2)
|
|
assert.Equal(t, "a1", logs[0].ID)
|
|
assert.Equal(t, "a3", logs[1].ID)
|
|
}
|
|
|
|
func TestSqlRepository_DeleteOlderThan(t *testing.T) {
|
|
repo, _ := newTestRepository(t)
|
|
ctx := context.Background()
|
|
require.NoError(t, repo.Create(ctx, newEntry("old", "acc", "GET", 48*time.Hour)))
|
|
require.NoError(t, repo.Create(ctx, newEntry("new", "acc", "GET", time.Hour)))
|
|
|
|
deleted, err := repo.DeleteOlderThan(ctx, time.Now().Add(-24*time.Hour))
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, 1, deleted)
|
|
|
|
logs, total, err := repo.ListByAccount(ctx, db.LockingStrengthNone, "acc", accesslogs.AccessLogFilter{Page: 1, PageSize: 10})
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, 1, total)
|
|
require.Len(t, logs, 1)
|
|
assert.Equal(t, "new", logs[0].ID)
|
|
}
|
|
|
|
func TestSqlRepository_CreateInsideTransactionRollsBack(t *testing.T) {
|
|
repo, conn := newTestRepository(t)
|
|
ctx := context.Background()
|
|
failure := errors.New("abort")
|
|
|
|
err := conn.RunInTx(ctx, func(tx *db.Tx) error {
|
|
txRepo := repo.WithTx(tx)
|
|
require.NoError(t, txRepo.Create(ctx, newEntry("tx", "acc", "GET", 0)))
|
|
_, total, err := txRepo.ListByAccount(ctx, db.LockingStrengthNone, "acc", accesslogs.AccessLogFilter{Page: 1, PageSize: 10})
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, 1, total)
|
|
return failure
|
|
})
|
|
require.ErrorIs(t, err, failure)
|
|
|
|
_, total, err := repo.ListByAccount(ctx, db.LockingStrengthNone, "acc", accesslogs.AccessLogFilter{Page: 1, PageSize: 10})
|
|
require.NoError(t, err)
|
|
assert.Zero(t, total)
|
|
}
|
|
|
|
func TestSqlRepository_ListByAccount_StatusFilter(t *testing.T) {
|
|
repo, _ := newTestRepository(t)
|
|
ctx := context.Background()
|
|
statusCodes := map[string]int{"l4": 0, "info": 101, "ok": 200, "notfound": 404}
|
|
for id, code := range statusCodes {
|
|
entry := newEntry(id, "acc", "GET", time.Hour)
|
|
entry.StatusCode = code
|
|
require.NoError(t, repo.Create(ctx, entry))
|
|
}
|
|
foreign := newEntry("foreign", "other", "GET", time.Hour)
|
|
foreign.StatusCode = 500
|
|
require.NoError(t, repo.Create(ctx, foreign))
|
|
|
|
listIDs := func(status string) []string {
|
|
logs, total, err := repo.ListByAccount(ctx, db.LockingStrengthNone, "acc", accesslogs.AccessLogFilter{Page: 1, PageSize: 10, Status: &status, SortBy: "status_code", SortOrder: "asc"})
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, len(logs), total)
|
|
ids := make([]string, 0, len(logs))
|
|
for _, entry := range logs {
|
|
ids = append(ids, entry.ID)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
assert.Equal(t, []string{"info", "notfound"}, listIDs("failed"))
|
|
assert.Equal(t, []string{"ok"}, listIDs("success"))
|
|
}
|
|
|
|
func TestSqlRepository_ListByAccount_Search(t *testing.T) {
|
|
repo, _ := newTestRepository(t)
|
|
ctx := context.Background()
|
|
api := newEntry("req-api", "acc", "GET", time.Hour)
|
|
api.Host = "api.example.com"
|
|
api.Path = "/v1/peers"
|
|
web := newEntry("req-web", "acc", "GET", 2*time.Hour)
|
|
web.Host = "web.example.com"
|
|
web.Path = "/login"
|
|
foreign := newEntry("foreign", "other", "GET", time.Hour)
|
|
foreign.Host = "api.example.com"
|
|
for _, entry := range []*accesslogs.AccessLogEntry{api, web, foreign} {
|
|
require.NoError(t, repo.Create(ctx, entry))
|
|
}
|
|
|
|
search := func(term string) []string {
|
|
logs, total, err := repo.ListByAccount(ctx, db.LockingStrengthNone, "acc", accesslogs.AccessLogFilter{Page: 1, PageSize: 10, Search: &term})
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, len(logs), total)
|
|
ids := make([]string, 0, len(logs))
|
|
for _, entry := range logs {
|
|
ids = append(ids, entry.ID)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
assert.Equal(t, []string{"req-api"}, search("api.example"), "search should match the host")
|
|
assert.Equal(t, []string{"req-web"}, search("/login"), "search should match the path")
|
|
assert.Equal(t, []string{"req-api"}, search("example.com/v1"), "search should match host and path joined")
|
|
assert.Equal(t, []string{"req-web"}, search("req-web"), "search should match the log ID")
|
|
assert.Equal(t, []string{"req-api", "req-web"}, search("example.com"), "search should list every match newest first")
|
|
assert.Empty(t, search("nothing-matches"))
|
|
}
|
|
|
|
func TestSqlRepository_DeleteOlderThan_DeletesInBatches(t *testing.T) {
|
|
cases := map[string]struct{ old, recent int }{
|
|
"partial last batch": {old: 5, recent: 2},
|
|
"exact multiple of batch": {old: 4, recent: 1},
|
|
"nothing to delete": {old: 0, recent: 3},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
repo, _ := newTestRepository(t)
|
|
assertBatchedDelete(t, repo, tc.old, tc.recent)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSqlRepository_DeleteOlderThan_DeletesInBatchesOnPostgres(t *testing.T) {
|
|
if os.Getenv("NETBIRD_STORE_ENGINE") != string(db.PostgresStoreEngine) {
|
|
t.Skip("requires NETBIRD_STORE_ENGINE=postgres")
|
|
}
|
|
cleanup, dsn, err := testutil.CreatePostgresTestContainer()
|
|
require.NoError(t, err)
|
|
t.Cleanup(cleanup)
|
|
conn, err := db.OpenPostgres(context.Background(), dsn, db.DefaultPoolConfig)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = conn.Close() })
|
|
require.NoError(t, conn.AutoMigrate(&accesslogs.AccessLogEntry{}))
|
|
require.NoError(t, conn.DB(nil).Exec("DELETE FROM access_log_entries").Error)
|
|
|
|
assertBatchedDelete(t, NewRepository(conn), 5, 2)
|
|
}
|
|
|
|
func TestSqlRepository_DeleteOlderThan_StopsWhenContextDone(t *testing.T) {
|
|
repo, _ := newTestRepository(t)
|
|
require.NoError(t, repo.Create(context.Background(), newEntry("old", "acc", "GET", 48*time.Hour)))
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
deleted, err := repo.DeleteOlderThan(ctx, time.Now().Add(-24*time.Hour))
|
|
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
assert.Zero(t, deleted)
|
|
assert.EqualValues(t, 1, countEntries(t, repo), "nothing should be deleted once the context is done")
|
|
}
|
|
|
|
func assertBatchedDelete(t *testing.T, repo accesslogs.Repository, old, recent int) {
|
|
t.Helper()
|
|
repo.(*sqlRepository).deleteBatchSize = 2
|
|
ctx := context.Background()
|
|
for i := 0; i < old; i++ {
|
|
require.NoError(t, repo.Create(ctx, newEntry(fmt.Sprintf("old-%d", i), "acc", "GET", 48*time.Hour)))
|
|
}
|
|
for i := 0; i < recent; i++ {
|
|
require.NoError(t, repo.Create(ctx, newEntry(fmt.Sprintf("new-%d", i), "acc", "GET", time.Hour)))
|
|
}
|
|
|
|
deleted, err := repo.DeleteOlderThan(ctx, time.Now().Add(-24*time.Hour))
|
|
require.NoError(t, err)
|
|
assert.EqualValues(t, old, deleted, "every old entry should be deleted across batches")
|
|
assert.EqualValues(t, recent, countEntries(t, repo), "recent entries should be kept")
|
|
}
|
|
|
|
func countEntries(t *testing.T, repo accesslogs.Repository) int64 {
|
|
t.Helper()
|
|
_, total, err := repo.ListByAccount(context.Background(), db.LockingStrengthNone, "acc", accesslogs.AccessLogFilter{Page: 1, PageSize: 10})
|
|
require.NoError(t, err)
|
|
return total
|
|
}
|