mirror of
https://github.com/TronoSfera/backupy-agent.git
synced 2026-05-18 18:13:30 +03:00
Source ports from the TronoSfera/backupy-cloud monorepo:
- apps/agent/ — Go agent (WSS client, persistent queue, Docker
discovery, 5 DB drivers: PG/MySQL/Mongo/Redis/SQLite,
pre/post hooks, Prometheus metrics)
- apps/backupy-decrypt/ — standalone CLI for client-side decryption
- packages/proto/ — protobuf wire format (generated .pb.go committed
so the repo builds without protoc)
- docs/ — agent spec + wire-protocol contract
Apache-2.0 license. Image published to ghcr.io/tronosfera/backupy-agent
on every v* tag via .github/workflows/release.yml (multi-arch amd64+arm64).
124 lines
3.8 KiB
Go
124 lines
3.8 KiB
Go
//go:build integration
|
|
|
|
// Cross-binary integration test: prove the agent's pipeline.Encryptor
|
|
// produces a byte stream that the backupy-decrypt CLI can consume.
|
|
//
|
|
// This is the GOLDEN TEST for Phase 1. If this passes, the entire
|
|
// backup → download → decrypt loop works end-to-end at the byte level.
|
|
//
|
|
// Run via:
|
|
//
|
|
// cd apps/backupy-decrypt && go test -tags=integration ./...
|
|
//
|
|
// The test depends on the agent module via a `replace` directive in
|
|
// go.mod, which in turn depends on the generated proto bindings. So
|
|
// `make proto` must have been run from the repo root first.
|
|
package backupydecrypt_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/backupy/backupy/apps/agent/internal/pipeline"
|
|
"github.com/backupy/backupy/apps/backupy-decrypt/internal/decrypt"
|
|
)
|
|
|
|
// craftToken builds a JWT in the shape the CLI expects. The signature
|
|
// is ignored — the CLI verifies neither HMAC nor RSA, only the claim
|
|
// envelope and the SHA-256 inside it.
|
|
func craftToken(t *testing.T, dek []byte, sha string) string {
|
|
t.Helper()
|
|
hdr := map[string]string{"alg": "HS256", "typ": "JWT"}
|
|
hdrJSON, err := json.Marshal(hdr)
|
|
require.NoError(t, err)
|
|
claims := map[string]any{
|
|
"iss": "backupy-server",
|
|
"sub": "user-1",
|
|
"aud": "backupy-decrypt",
|
|
"iat": time.Now().Unix(),
|
|
"exp": time.Now().Add(15 * time.Minute).Unix(),
|
|
"run_id": "run-integration",
|
|
"company_id": "co-integration",
|
|
"dek": base64.StdEncoding.EncodeToString(dek),
|
|
"alg": "AES-256-GCM",
|
|
"format_version": 1,
|
|
"sha256": sha,
|
|
}
|
|
pld, err := json.Marshal(claims)
|
|
require.NoError(t, err)
|
|
enc := base64.RawURLEncoding
|
|
signingInput := enc.EncodeToString(hdrJSON) + "." + enc.EncodeToString(pld)
|
|
return signingInput + "." + enc.EncodeToString([]byte("ignored"))
|
|
}
|
|
|
|
// TestCrossBinary_PipelineToDecrypt is the bytewise round-trip check.
|
|
func TestCrossBinary_PipelineToDecrypt(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
size int
|
|
}{
|
|
{"small_46B", 46},
|
|
{"single_full_chunk_1MiB", pipeline.ChunkPlainSize},
|
|
{"two_chunks_plus_remainder", 2*pipeline.ChunkPlainSize + 1234},
|
|
{"empty", 0},
|
|
}
|
|
// Compile-time sanity: the two sides MUST agree on the chunk size.
|
|
require.Equal(t, pipeline.ChunkPlainSize, decrypt.ChunkPlaintextSize,
|
|
"chunk size constant drift between pipeline and decrypt — re-sync")
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
plaintext := make([]byte, tc.size)
|
|
_, err := rand.Read(plaintext)
|
|
require.NoError(t, err)
|
|
|
|
dek := make([]byte, 32)
|
|
_, err = rand.Read(dek)
|
|
require.NoError(t, err)
|
|
|
|
// Encrypt with the agent's pipeline.
|
|
enc, err := pipeline.NewEncryptor(dek)
|
|
require.NoError(t, err)
|
|
var ctBuf bytes.Buffer
|
|
n, err := enc.Stream(bytes.NewReader(plaintext), &ctBuf)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(len(plaintext)), n)
|
|
ciphertext := ctBuf.Bytes()
|
|
|
|
// SHA-256 of the on-wire bytes — what the JWT carries.
|
|
sum := sha256.Sum256(ciphertext)
|
|
shaHex := hex.EncodeToString(sum[:])
|
|
|
|
// Write to disk, decrypt with the CLI's package.
|
|
dir := t.TempDir()
|
|
inPath := filepath.Join(dir, "in.enc")
|
|
outPath := filepath.Join(dir, "out.bin")
|
|
require.NoError(t, os.WriteFile(inPath, ciphertext, 0o600))
|
|
|
|
tok := craftToken(t, dek, shaHex)
|
|
err = decrypt.Run(context.Background(), decrypt.Options{
|
|
InputPath: inPath,
|
|
OutputPath: outPath,
|
|
Token: tok,
|
|
VerifySHA256: true,
|
|
SkipDecompress: true,
|
|
})
|
|
require.NoError(t, err, "decrypt.Run must accept pipeline output")
|
|
|
|
got, err := os.ReadFile(outPath)
|
|
require.NoError(t, err)
|
|
require.Equal(t, plaintext, got, "round-trip must be bytewise identical")
|
|
})
|
|
}
|
|
}
|