| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "encoding/json" |
| 7 | "net/http" |
| 8 | "net/http/httptest" |
| 9 | "os" |
| 10 | "os/exec" |
| 11 | "path/filepath" |
| 12 | "strings" |
| 13 | "testing" |
| 14 | "time" |
| 15 | |
| 16 | "github.com/FelineStateMachine/tinyrelay/internal/replication" |
| 17 | ) |
| 18 | |
| 19 | func TestMountedHTTPBackupRestoreIncludesGitInventory(t *testing.T) { |
| 20 | ctx := context.Background() |
| 21 | _, source := testTenant(t) |
| 22 | app, target := testTenant(t) |
| 23 | repoPath := filepath.Join(source.meta.Paths.Git, source.Policy().Owner, "portable.git" ) |
| 24 | if err := os.MkdirAll(filepath.Dir(repoPath), 0700); err != nil { |
| 25 | t.Fatal(err) |
| 26 | } |
| 27 | if output, err := exec.Command("git" , "init" , "--bare" , "--initial-branch=main" , repoPath).CombinedOutput(); err != nil { |
| 28 | t.Fatalf("git init: %v: %s" , err, output) |
| 29 | } |
| 30 | work := filepath.Join(t.TempDir(), "work" ) |
| 31 | if err := os.MkdirAll(work, 0700); err != nil { |
| 32 | t.Fatal(err) |
| 33 | } |
| 34 | gitRun(t, work, "init" ) |
| 35 | gitRun(t, work, "config" , "user.email" , "acceptance@example.com" ) |
| 36 | gitRun(t, work, "config" , "user.name" , "Acceptance" ) |
| 37 | if err := os.WriteFile(filepath.Join(work, "README.txt" ), []byte("portable git text\n" ), 0600); err != nil { |
| 38 | t.Fatal(err) |
| 39 | } |
| 40 | binary := []byte{0, 1, 2, 127, 128, 255} |
| 41 | if err := os.WriteFile(filepath.Join(work, "payload.bin" ), binary, 0600); err != nil { |
| 42 | t.Fatal(err) |
| 43 | } |
| 44 | gitRun(t, work, "add" , "README.txt" , "payload.bin" ) |
| 45 | gitRun(t, work, "commit" , "-m" , "portable inventory" ) |
| 46 | gitRun(t, work, "branch" , "-M" , "main" ) |
| 47 | gitRun(t, work, "tag" , "v1" ) |
| 48 | gitRun(t, work, "remote" , "add" , "origin" , repoPath) |
| 49 | gitRun(t, work, "push" , "--all" , "origin" ) |
| 50 | gitRun(t, work, "push" , "--tags" , "origin" ) |
| 51 | commit := strings.TrimSpace(string(backupGitOutput(t, work, "rev-parse" , "HEAD" ))) |
| 52 | binaryOID := strings.TrimSpace(string(backupGitOutput(t, work, "rev-parse" , "HEAD:payload.bin" ))) |
| 53 | archive, err := replication.CreateBackupWithProvider(ctx, source.store, source.ReplicationBackupProvider(), time.Now().Unix()) |
| 54 | if err != nil { |
| 55 | t.Fatal(err) |
| 56 | } |
| 57 | body, err := json.Marshal(archive) |
| 58 | if err != nil { |
| 59 | t.Fatal(err) |
| 60 | } |
| 61 | req := httptest.NewRequest(http.MethodPost, "http://relay.test/r/main/backups/restore" , bytes.NewReader(body)) |
| 62 | signRequest(t, req, string(body)) |
| 63 | res := httptest.NewRecorder() |
| 64 | app.ServeHTTP(res, req) |
| 65 | if res.Code != http.StatusOK { |
| 66 | t.Fatalf("mounted Git restore %d: %s" , res.Code, res.Body.String()) |
| 67 | } |
| 68 | restoredRepo := filepath.Join(target.meta.Paths.Git, source.Policy().Owner, "portable.git" ) |
| 69 | if got := strings.TrimSpace(string(backupGitOutput(t, restoredRepo, "rev-parse" , "refs/heads/main" ))); got != commit { |
| 70 | t.Fatalf("restored main ref = %s, want %s" , got, commit) |
| 71 | } |
| 72 | if got := strings.TrimSpace(string(backupGitOutput(t, restoredRepo, "rev-parse" , "refs/tags/v1" ))); got != commit { |
| 73 | t.Fatalf("restored tag ref = %s, want %s" , got, commit) |
| 74 | } |
| 75 | if got := string(backupGitOutput(t, restoredRepo, "show" , "HEAD:README.txt" )); got != "portable git text\n" { |
| 76 | t.Fatalf("restored text = %q" , got) |
| 77 | } |
| 78 | if got := strings.TrimSpace(string(backupGitOutput(t, restoredRepo, "rev-parse" , "HEAD:payload.bin" ))); got != binaryOID { |
| 79 | t.Fatalf("restored binary oid = %s, want %s" , got, binaryOID) |
| 80 | } |
| 81 | if output, err := exec.Command("git" , "--git-dir" , restoredRepo, "cat-file" , "blob" , "HEAD:payload.bin" ).Output(); err != nil || string(output) != string(binary) { |
| 82 | t.Fatalf("restored binary content mismatch: %v" , err) |
| 83 | } |
| 84 | if output, err := exec.Command("git" , "--git-dir" , restoredRepo, "fsck" , "--full" , "--no-progress" ).CombinedOutput(); err != nil { |
| 85 | t.Fatalf("restored Git fsck: %v: %s" , err, output) |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | func gitRun(t *testing.T, dir string, args ...string) { |
| 90 | t.Helper() |
| 91 | command := exec.Command("git" , append([]string{"-C" , dir}, args...)...) |
| 92 | if output, err := command.CombinedOutput(); err != nil { |
| 93 | t.Fatalf("git %v: %v: %s" , args, err, output) |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | func backupGitOutput(t *testing.T, dir string, args ...string) []byte { |
| 98 | t.Helper() |
| 99 | command := exec.Command("git" , append([]string{"--git-dir" , dir}, args...)...) |
| 100 | if dir != "" && !strings.HasSuffix(dir, ".git" ) { |
| 101 | command = exec.Command("git" , append([]string{"-C" , dir}, args...)...) |
| 102 | } |
| 103 | output, err := command.Output() |
| 104 | if err != nil { |
| 105 | t.Fatalf("git %v: %v" , args, err) |
| 106 | } |
| 107 | return output |
| 108 | } |