mirror of
https://github.com/TronoSfera/backupy-agent.git
synced 2026-05-18 18:13:30 +03:00
The .gitignore rule "state/" was unanchored, so git also ignored apps/agent/internal/state/ — the BoltDB-backed queue persistence package. CI build failed with: internal/queue/queue.go:13:2: no required module provides package github.com/backupy/backupy/apps/agent/internal/state Anchored the rule to repo root (/state/, /var/) so it only matches the runtime data directory, never a Go package.
168 lines
4.7 KiB
Go
168 lines
4.7 KiB
Go
package state
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const testAgentKey = "bkpy_test_abcdefghijklmnopqrstuvwxyz012345"
|
|
|
|
func newStore(t *testing.T) (*Store, string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "state.db")
|
|
s, err := Open(path, Options{AgentKey: testAgentKey})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = s.Close() })
|
|
return s, path
|
|
}
|
|
|
|
func TestOpen_Empty(t *testing.T) {
|
|
s, _ := newStore(t)
|
|
_, _, err := s.LoadConfig()
|
|
require.ErrorIs(t, err, ErrNotFound)
|
|
|
|
_, err = s.LoadSession()
|
|
require.ErrorIs(t, err, ErrNotFound)
|
|
|
|
n, err := s.QueueDepth()
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, n)
|
|
}
|
|
|
|
func TestConfig_Roundtrip(t *testing.T) {
|
|
s, _ := newStore(t)
|
|
payload := []byte("hello protobuf bytes")
|
|
require.NoError(t, s.SaveConfig(42, payload))
|
|
|
|
v, got, err := s.LoadConfig()
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint64(42), v)
|
|
require.Equal(t, payload, got)
|
|
}
|
|
|
|
func TestQueue_EnqueueDequeueAck(t *testing.T) {
|
|
s, _ := newStore(t)
|
|
|
|
require.NoError(t, s.EnqueueJob("run-a", []byte("aaa")))
|
|
require.NoError(t, s.EnqueueJob("run-b", []byte("bbb")))
|
|
require.NoError(t, s.EnqueueJob("run-c", []byte("ccc")))
|
|
|
|
n, err := s.QueueDepth()
|
|
require.NoError(t, err)
|
|
require.Equal(t, 3, n)
|
|
|
|
jobs, err := s.DequeueJobs(2)
|
|
require.NoError(t, err)
|
|
require.Len(t, jobs, 2)
|
|
require.Equal(t, "run-a", jobs[0].RunID)
|
|
require.Equal(t, []byte("aaa"), jobs[0].Payload)
|
|
require.Equal(t, "run-b", jobs[1].RunID)
|
|
|
|
// Ack pops the head; depth drops.
|
|
require.NoError(t, s.AckJob("run-a"))
|
|
n, _ = s.QueueDepth()
|
|
require.Equal(t, 2, n)
|
|
|
|
// Idempotent re-enqueue overwrites payload.
|
|
require.NoError(t, s.EnqueueJob("run-b", []byte("BBB")))
|
|
jobs, _ = s.DequeueJobs(10)
|
|
require.Equal(t, []byte("BBB"), jobs[0].Payload)
|
|
}
|
|
|
|
func TestSession(t *testing.T) {
|
|
s, _ := newStore(t)
|
|
require.NoError(t, s.SaveSession("sess-123", 1700000000000))
|
|
got, err := s.LoadSession()
|
|
require.NoError(t, err)
|
|
require.Equal(t, "sess-123", got)
|
|
}
|
|
|
|
func TestHeartbeat(t *testing.T) {
|
|
s, _ := newStore(t)
|
|
ts, err := s.LastHeartbeat()
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(0), ts)
|
|
|
|
require.NoError(t, s.RecordHeartbeat(1700000000000))
|
|
ts, err = s.LastHeartbeat()
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(1700000000000), ts)
|
|
}
|
|
|
|
func TestLogs_BufferAndDrain(t *testing.T) {
|
|
s, _ := newStore(t)
|
|
require.NoError(t, s.BufferLog(100, []byte("first")))
|
|
require.NoError(t, s.BufferLog(200, []byte("second")))
|
|
require.NoError(t, s.BufferLog(150, []byte("middle")))
|
|
|
|
drained, err := s.DrainLogs(10)
|
|
require.NoError(t, err)
|
|
require.Len(t, drained, 3)
|
|
// Chronological order — ts_ms wins, so: 100, 150, 200.
|
|
require.Equal(t, []byte("first"), drained[0])
|
|
require.Equal(t, []byte("middle"), drained[1])
|
|
require.Equal(t, []byte("second"), drained[2])
|
|
|
|
// Second drain returns nothing.
|
|
drained, err = s.DrainLogs(10)
|
|
require.NoError(t, err)
|
|
require.Empty(t, drained)
|
|
}
|
|
|
|
// TestEncryption_AtRest ensures values written to disk are NOT plaintext.
|
|
// This is the headline guarantee of the state package and the basis for
|
|
// task D-17 ("local state encryption").
|
|
func TestEncryption_AtRest(t *testing.T) {
|
|
s, path := newStore(t)
|
|
plaintext := []byte("super-secret-config-payload")
|
|
require.NoError(t, s.SaveConfig(1, plaintext))
|
|
require.NoError(t, s.EnqueueJob("run-x", []byte("queued-secret-x")))
|
|
|
|
// Close to flush before reading raw.
|
|
require.NoError(t, s.Close())
|
|
|
|
raw, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
require.False(t, bytes.Contains(raw, plaintext), "plaintext config leaked to disk")
|
|
require.False(t, bytes.Contains(raw, []byte("queued-secret-x")), "plaintext queue payload leaked to disk")
|
|
}
|
|
|
|
// TestEncryption_WrongKey ensures a different agent key cannot decrypt
|
|
// existing state — corruption / key rotation surfaces as a hard error
|
|
// rather than silently overwriting good data.
|
|
func TestEncryption_WrongKey(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "state.db")
|
|
|
|
s, err := Open(path, Options{AgentKey: testAgentKey})
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.SaveConfig(7, []byte("payload")))
|
|
require.NoError(t, s.Close())
|
|
|
|
// Reopen with a different key — must fail to decrypt.
|
|
wrong := "bkpy_test_ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"
|
|
s2, err := Open(path, Options{AgentKey: wrong})
|
|
require.NoError(t, err)
|
|
defer s2.Close()
|
|
|
|
_, _, err = s2.LoadConfig()
|
|
require.Error(t, err)
|
|
require.True(t, errors.Is(err, errCipher) || err.Error() != "", "expected cipher error, got: %v", err)
|
|
}
|
|
|
|
func TestOpen_EmptyKey(t *testing.T) {
|
|
dir := t.TempDir()
|
|
_, err := Open(filepath.Join(dir, "state.db"), Options{AgentKey: ""})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestEnqueue_EmptyRunID(t *testing.T) {
|
|
s, _ := newStore(t)
|
|
require.Error(t, s.EnqueueJob("", []byte("x")))
|
|
}
|