返回 DeepSeek-Reasonix
main_test.go
根目录 / desktop / cmd / sign / main_test.go
1 package main
2
3 import (
4 "crypto/rand"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "aead.dev/minisign"
12
13 "reasonix/desktop/internal/update"
14 )
15
16 // TestSignFiles signs a file with a throwaway key pair (injected via env, exactly
17 // as CI passes the real key) and verifies the produced .minisig validates under the
18 // matching public key.
19 func TestSignFiles(t *testing.T) {
20 pub, priv, err := minisign.GenerateKey(rand.Reader)
21 if err != nil {
22 t.Fatal(err)
23 }
24 enc, err := minisign.EncryptKey("pw", priv)
25 if err != nil {
26 t.Fatal(err)
27 }
28 t.Setenv("MINISIGN_PRIVATE_KEY", string(enc))
29 t.Setenv("MINISIGN_PASSWORD", "pw")
30
31 dir := t.TempDir()
32 artifact := filepath.Join(dir, "Reasonix-linux-amd64.tar.gz")
33 payload := []byte("pretend this is a release tarball")
34 if err := os.WriteFile(artifact, payload, 0o644); err != nil {
35 t.Fatal(err)
36 }
37
38 if err := signFiles([]string{artifact}); err != nil {
39 t.Fatalf("signFiles: %v", err)
40 }
41 sig, err := os.ReadFile(artifact + ".minisig")
42 if err != nil {
43 t.Fatalf("read signature: %v", err)
44 }
45 if !minisign.Verify(pub, payload, sig) {
46 t.Fatal("produced signature does not verify under the signing key")
47 }
48 }
49
50 // TestGenManifest builds a manifest from a directory of fake artifacts and checks
51 // every platform is listed with a download URL, a parallel .minisig URL, and a
52 // non-empty digest. The .minisig and latest.json files must be ignored.
53 func TestGenManifest(t *testing.T) {
54 dir := t.TempDir()
55 names := []string{
56 "Reasonix-darwin-arm64.zip",
57 "Reasonix-darwin-amd64.zip",
58 "Reasonix-darwin-universal.dmg",
59 "Reasonix-windows-amd64-installer.exe",
60 "Reasonix-windows-amd64.zip", // portable download, not the updater channel
61 "Reasonix-windows-arm64-installer.exe",
62 "Reasonix-windows-arm64.zip", // portable download, not the updater channel
63 "Reasonix-linux-amd64.tar.gz",
64 "Reasonix-linux-amd64.deb", // human download, not the updater channel
65 "Reasonix-linux-amd64.tar.gz.minisig", // must be skipped
66 "README.txt", // unmatched, must be skipped
67 }
68 for _, n := range names {
69 if err := os.WriteFile(filepath.Join(dir, n), []byte(n), 0o644); err != nil {
70 t.Fatal(err)
71 }
72 }
73 t.Setenv("GITHUB_REPOSITORY", "esengine/reasonix")
74
75 if err := genManifest(dir, "v1.2.0", "desktop-v1.2.0"); err != nil {
76 t.Fatalf("genManifest: %v", err)
77 }
78 raw, err := os.ReadFile(filepath.Join(dir, "latest.json"))
79 if err != nil {
80 t.Fatal(err)
81 }
82 var m update.Manifest
83 if err := json.Unmarshal(raw, &m); err != nil {
84 t.Fatalf("latest.json is not valid: %v", err)
85 }
86 if m.Version != "v1.2.0" {
87 t.Fatalf("version = %q, want v1.2.0", m.Version)
88 }
89 if m.DownloadPage != "https://reasonix.io/?download=desktop#start" {
90 t.Fatalf("download_page = %q, want official install page", m.DownloadPage)
91 }
92 if m.ReleaseNotesURL != "https://reasonix.io/changelog/v1.2.0/" {
93 t.Fatalf("release_notes_url = %q, want exact version history", m.ReleaseNotesURL)
94 }
95 if len(m.Platforms) != 5 {
96 t.Fatalf("want 5 platforms, got %d: %v", len(m.Platforms), m.Platforms)
97 }
98 win, ok := m.Platforms["windows-amd64"]
99 if !ok {
100 t.Fatal("windows-amd64 missing")
101 }
102 wantURL := "https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.2.0/Reasonix-windows-amd64-installer.exe"
103 if win.URL != wantURL {
104 t.Fatalf("windows url = %q, want %q", win.URL, wantURL)
105 }
106 if win.Sig != wantURL+".minisig" {
107 t.Fatalf("windows sig = %q, want %q.minisig", win.Sig, wantURL)
108 }
109 if win.SHA256 == "" || win.Size == 0 {
110 t.Fatalf("windows asset missing digest/size: %+v", win)
111 }
112 // The Windows updater channel is the per-arch -installer.exe; the portable .zip
113 // must not shadow the windows-arm64 key.
114 arm, ok := m.Platforms["windows-arm64"]
115 if !ok {
116 t.Fatal("windows-arm64 missing")
117 }
118 if !strings.HasSuffix(arm.URL, "/Reasonix-windows-arm64-installer.exe") {
119 t.Fatalf("windows-arm64 url = %q, want the installer, not the portable zip", arm.URL)
120 }
121 // The Linux portable channel stays the .tar.gz; the co-located .deb lands
122 // only in native_packages so older clients keep resolving platforms["linux-amd64"].
123 lin, ok := m.Platforms["linux-amd64"]
124 if !ok {
125 t.Fatal("linux-amd64 missing")
126 }
127 if !strings.HasSuffix(lin.URL, "/Reasonix-linux-amd64.tar.gz") {
128 t.Fatalf("linux-amd64 url = %q, want the .tar.gz, not the .deb", lin.URL)
129 }
130 if lin.Sig == "" || lin.SHA256 == "" || lin.Size == 0 {
131 t.Fatalf("linux portable asset incomplete: %+v", lin)
132 }
133 deb, ok := m.NativePackages["linux-amd64"]
134 if !ok {
135 t.Fatal("native_packages linux-amd64 missing")
136 }
137 if !strings.HasSuffix(deb.URL, "/Reasonix-linux-amd64.deb") {
138 t.Fatalf("native linux-amd64 url = %q, want the .deb", deb.URL)
139 }
140 if deb.Sig != deb.URL+".minisig" || deb.SHA256 == "" || deb.Size == 0 {
141 t.Fatalf("native linux asset incomplete: %+v", deb)
142 }
143 if len(m.Downloads) != 2 {
144 t.Fatalf("want 2 website downloads, got %d: %+v", len(m.Downloads), m.Downloads)
145 }
146 for _, name := range []string{"Reasonix-darwin-universal.dmg", "Reasonix-windows-amd64.zip"} {
147 asset, ok := m.Downloads[name]
148 if !ok {
149 t.Fatalf("website download %q missing", name)
150 }
151 if !strings.HasSuffix(asset.URL, "/"+name) ||
152 asset.Sig != asset.URL+".minisig" ||
153 asset.SHA256 == "" ||
154 asset.Size == 0 {
155 t.Fatalf("website download %q incomplete: %+v", name, asset)
156 }
157 }
158 }
159
160 func TestGenManifestCanReuseStableNotesForStandaloneRC(t *testing.T) {
161 dir := t.TempDir()
162 if err := os.WriteFile(filepath.Join(dir, "Reasonix-linux-amd64.tar.gz"), []byte("rc"), 0o644); err != nil {
163 t.Fatal(err)
164 }
165 if err := genManifest(dir, "v1.3.0-rc.1", "desktop-v1.3.0-rc.1", "v1.3.0"); err != nil {
166 t.Fatalf("genManifest: %v", err)
167 }
168 raw, err := os.ReadFile(filepath.Join(dir, "latest.json"))
169 if err != nil {
170 t.Fatal(err)
171 }
172 var m update.Manifest
173 if err := json.Unmarshal(raw, &m); err != nil {
174 t.Fatal(err)
175 }
176 if m.ReleaseNotesURL != "https://reasonix.io/changelog/v1.3.0/" {
177 t.Fatalf("release_notes_url = %q, want stable base history", m.ReleaseNotesURL)
178 }
179 }
180
181 // TestGenManifestIgnoresUnknownNativePackages ensures a .deb without a known
182 // platform key is skipped rather than inventing a native_packages entry.
183 func TestGenManifestIgnoresUnknownNativePackages(t *testing.T) {
184 dir := t.TempDir()
185 for _, n := range []string{
186 "Reasonix-linux-amd64.tar.gz",
187 "Reasonix-mystery.deb",
188 } {
189 if err := os.WriteFile(filepath.Join(dir, n), []byte(n), 0o644); err != nil {
190 t.Fatal(err)
191 }
192 }
193 t.Setenv("GITHUB_REPOSITORY", "esengine/DeepSeek-Reasonix")
194 if err := genManifest(dir, "v1.2.0", "desktop-v1.2.0"); err != nil {
195 t.Fatalf("genManifest: %v", err)
196 }
197 raw, err := os.ReadFile(filepath.Join(dir, "latest.json"))
198 if err != nil {
199 t.Fatal(err)
200 }
201 var m update.Manifest
202 if err := json.Unmarshal(raw, &m); err != nil {
203 t.Fatal(err)
204 }
205 if len(m.NativePackages) != 0 {
206 t.Fatalf("unexpected native_packages: %+v", m.NativePackages)
207 }
208 }
209
210 func TestGenWindowsPayloadManifestHashesExactReleaseUnit(t *testing.T) {
211 dir := t.TempDir()
212 for _, name := range update.WindowsPayloadFileNames() {
213 if err := os.WriteFile(filepath.Join(dir, name), []byte("payload:"+name), 0o700); err != nil {
214 t.Fatal(err)
215 }
216 }
217 if err := genWindowsPayloadManifest(dir, "v2.3.4"); err != nil {
218 t.Fatal(err)
219 }
220 b, err := os.ReadFile(filepath.Join(dir, update.WindowsPayloadManifestName))
221 if err != nil {
222 t.Fatal(err)
223 }
224 hashes, err := update.DecodeWindowsPayloadManifest(b, "v2.3.4")
225 if err != nil {
226 t.Fatal(err)
227 }
228 for _, name := range update.WindowsPayloadFileNames() {
229 want := update.WindowsPayloadSHA256([]byte("payload:" + name))
230 if hashes[name] != want {
231 t.Fatalf("manifest hash for %s = %q, want %q", name, hashes[name], want)
232 }
233 }
234 }
235
235 lines GO