]> git.giacomolli.uk - onechat/blob - backup_archive_test.go
Pass caller contexts through WhatsApp callbacks
[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 }