backupy-agent/apps/backupy-decrypt/integration_test.go
TronoSfera 8b0c978337 feat(initial): Backupy agent + backupy-decrypt CLI
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).
2026-05-17 20:22:35 +03:00

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")
})
}
}