]> git.giacomolli.uk - onechat/blob - backup_archive_test.go
Define child agent areas and reporting rules
[onechat] / backup_archive_test.go
1 package main
2
3 import (
4         "archive/tar"
5         "os"
6         "path/filepath"
7         "testing"
8
9         "github.com/stretchr/testify/require"
10 )
11
12 func TestBackupArchiveRestoresBytesAndModes(t *testing.T) {
13         root := t.TempDir()
14         require.NoError(t, os.Mkdir(filepath.Join(root, "agent"), 0o750))
15         require.NoError(t, os.WriteFile(filepath.Join(root, "agent", "instructions.md"), []byte("Keep the whole chat.\n"), 0o640))
16
17         archive := filepath.Join(t.TempDir(), "backup.tar.gz")
18         size, err := backupArchive(t.Context(), root, archive)
19         require.NoError(t, err)
20
21         info, err := os.Stat(archive)
22         require.NoError(t, err)
23         require.Equal(t, size, info.Size())
24         require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
25
26         restored := t.TempDir()
27         require.NoError(t, backupExtract(t.Context(), archive, restored))
28
29         data, err := os.ReadFile(filepath.Join(restored, "agent", "instructions.md"))
30         require.NoError(t, err)
31         require.Equal(t, "Keep the whole chat.\n", string(data))
32
33         info, err = os.Stat(filepath.Join(restored, "agent"))
34         require.NoError(t, err)
35         require.Equal(t, os.FileMode(0o750), info.Mode().Perm())
36
37         info, err = os.Stat(filepath.Join(restored, "agent", "instructions.md"))
38         require.NoError(t, err)
39         require.Equal(t, os.FileMode(0o640), info.Mode().Perm())
40 }
41
42 func TestRestoreRejectsUnsafeArchiveEntries(t *testing.T) {
43         testCases := []struct {
44                 name   string
45                 header tar.Header
46         }{
47                 {
48                         name: "absolute paths fail",
49                         header: tar.Header{
50                                 Name:     "/escape",
51                                 Typeflag: tar.TypeReg,
52                                 Mode:     0o600,
53                         },
54                 },
55                 {
56                         name: "parent paths fail",
57                         header: tar.Header{
58                                 Name:     "../escape",
59                                 Typeflag: tar.TypeReg,
60                                 Mode:     0o600,
61                         },
62                 },
63                 {
64                         name: "embedded parent paths fail",
65                         header: tar.Header{
66                                 Name:     "agent/../../escape",
67                                 Typeflag: tar.TypeReg,
68                                 Mode:     0o600,
69                         },
70                 },
71                 {
72                         name: "symbolic links fail",
73                         header: tar.Header{
74                                 Name:     "agent",
75                                 Typeflag: tar.TypeSymlink,
76                                 Linkname: "../escape",
77                                 Mode:     0o777,
78                         },
79                 },
80                 {
81                         name: "hard links fail",
82                         header: tar.Header{
83                                 Name:     "agent",
84                                 Typeflag: tar.TypeLink,
85                                 Linkname: "/escape",
86                                 Mode:     0o600,
87                         },
88                 },
89                 {
90                         name: "device files fail",
91                         header: tar.Header{
92                                 Name:     "device",
93                                 Typeflag: tar.TypeChar,
94                                 Mode:     0o600,
95                         },
96                 },
97                 {
98                         name: "setuid modes fail",
99                         header: tar.Header{
100                                 Name:     "agent",
101                                 Typeflag: tar.TypeReg,
102                                 Mode:     0o4600,
103                         },
104                 },
105                 {
106                         name: "lock replacement fails",
107                         header: tar.Header{
108                                 Name:     "auth.lock",
109                                 Typeflag: tar.TypeReg,
110                                 Mode:     0o600,
111                         },
112                 },
113         }
114         for _, tc := range testCases {
115                 t.Run(tc.name, func(t *testing.T) {
116                         archive := backupTestArchive(t, tc.header)
117                         require.Error(t, backupExtract(t.Context(), archive, t.TempDir()))
118                 })
119         }
120 }
121
122 func TestBackupFailureKeepsTheDestination(t *testing.T) {
123         root := t.TempDir()
124         require.NoError(t, os.Symlink("missing", filepath.Join(root, "link")))
125
126         archive := filepath.Join(t.TempDir(), "backup.tar.gz")
127         require.NoError(t, os.WriteFile(archive, []byte("keep"), 0o600))
128
129         _, err := backupArchive(t.Context(), root, archive)
130         require.ErrorContains(t, err, "unsupported backup entry")
131
132         data, err := os.ReadFile(archive)
133         require.NoError(t, err)
134         require.Equal(t, "keep", string(data))
135
136         entries, err := os.ReadDir(filepath.Dir(archive))
137         require.NoError(t, err)
138         require.Len(t, entries, 1)
139 }