返回 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-arm64.dmg",
59 "Reasonix-darwin-amd64.dmg",
60 "Reasonix-darwin-universal.dmg",
61 "Reasonix-windows-amd64-installer.exe",
62 "Reasonix-windows-amd64.zip", // portable download, not the updater channel
63 "Reasonix-windows-arm64-installer.exe",
64 "Reasonix-windows-arm64.zip", // portable download, not the updater channel
65 "Reasonix-linux-amd64.tar.gz",
66 "Reasonix-linux-amd64.deb", // human download, not the updater channel
67 "Reasonix-linux-amd64.tar.gz.minisig", // must be skipped
68 "README.txt", // unmatched, must be skipped
69 }
70 for _, n := range names {
71 if err := os.WriteFile(filepath.Join(dir, n), []byte(n), 0o644); err != nil {
72 t.Fatal(err)
73 }
74 }
75 t.Setenv("GITHUB_REPOSITORY", "esengine/reasonix")
76
77 if err := genManifest(dir, "v1.2.0", "desktop-v1.2.0"); err != nil {
78 t.Fatalf("genManifest: %v", err)
79 }
80 raw, err := os.ReadFile(filepath.Join(dir, "latest.json"))
81 if err != nil {
82 t.Fatal(err)
83 }
84 var m update.Manifest
85 if err := json.Unmarshal(raw, &m); err != nil {
86 t.Fatalf("latest.json is not valid: %v", err)
87 }
88 if m.Version != "v1.2.0" {
89 t.Fatalf("version = %q, want v1.2.0", m.Version)
90 }
91 // Published v1.38.x readers accept only empty/versioned-v1. They must stop
92 // before the old helper can discard the app tree. DownloadPage stays usable.
93 for group, assets := range map[string]map[string]update.Asset{"platforms": m.Platforms, "native_packages": m.NativePackages, "downloads": m.Downloads} {
94 for name, asset := range assets {
95 if asset.InstallLayout != update.ElectronInstallLayout {
96 t.Errorf("%s/%s lacks the manual migration boundary: %q", group, name, asset.InstallLayout)
97 }
98 if asset.InstallLayout == "" || asset.InstallLayout == "versioned-v1" {
99 t.Errorf("v1.38.x would hand %s to the incompatible old installer", name)
100 }
101 }
102 }
103 if m.DownloadPage != "https://reasonix.io/?download=desktop#start" {
104 t.Fatalf("download_page = %q, want official install page", m.DownloadPage)
105 }
106 if m.ReleaseNotesURL != "https://reasonix.io/changelog/v1.2.0/" {
107 t.Fatalf("release_notes_url = %q, want exact version history", m.ReleaseNotesURL)
108 }
109 if len(m.Platforms) != 5 {
110 t.Fatalf("want 5 platforms, got %d: %v", len(m.Platforms), m.Platforms)
111 }
112 win, ok := m.Platforms["windows-amd64"]
113 if !ok {
114 t.Fatal("windows-amd64 missing")
115 }
116 wantURL := "https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.2.0/Reasonix-windows-amd64-installer.exe"
117 if win.URL != wantURL {
118 t.Fatalf("windows url = %q, want %q", win.URL, wantURL)
119 }
120 if win.Sig != wantURL+".minisig" {
121 t.Fatalf("windows sig = %q, want %q.minisig", win.Sig, wantURL)
122 }
123 if win.SHA256 == "" || win.Size == 0 {
124 t.Fatalf("windows asset missing digest/size: %+v", win)
125 }
126 // The Windows updater channel is the per-arch -installer.exe; the portable .zip
127 // must not shadow the windows-arm64 key.
128 arm, ok := m.Platforms["windows-arm64"]
129 if !ok {
130 t.Fatal("windows-arm64 missing")
131 }
132 if !strings.HasSuffix(arm.URL, "/Reasonix-windows-arm64-installer.exe") {
133 t.Fatalf("windows-arm64 url = %q, want the installer, not the portable zip", arm.URL)
134 }
135 // The Linux portable channel stays the .tar.gz; the co-located .deb lands
136 // only in native_packages so older clients keep resolving platforms["linux-amd64"].
137 lin, ok := m.Platforms["linux-amd64"]
138 if !ok {
139 t.Fatal("linux-amd64 missing")
140 }
141 if !strings.HasSuffix(lin.URL, "/Reasonix-linux-amd64.tar.gz") {
142 t.Fatalf("linux-amd64 url = %q, want the .tar.gz, not the .deb", lin.URL)
143 }
144 if lin.Sig == "" || lin.SHA256 == "" || lin.Size == 0 {
145 t.Fatalf("linux portable asset incomplete: %+v", lin)
146 }
147 deb, ok := m.NativePackages["linux-amd64"]
148 if !ok {
149 t.Fatal("native_packages linux-amd64 missing")
150 }
151 if !strings.HasSuffix(deb.URL, "/Reasonix-linux-amd64.deb") {
152 t.Fatalf("native linux-amd64 url = %q, want the .deb", deb.URL)
153 }
154 if deb.Sig != deb.URL+".minisig" || deb.SHA256 == "" || deb.Size == 0 {
155 t.Fatalf("native linux asset incomplete: %+v", deb)
156 }
157 if len(m.Downloads) != 4 {
158 t.Fatalf("want 4 website downloads, got %d: %+v", len(m.Downloads), m.Downloads)
159 }
160 for _, name := range []string{"Reasonix-darwin-arm64.dmg", "Reasonix-darwin-amd64.dmg", "Reasonix-darwin-universal.dmg", "Reasonix-windows-amd64.zip"} {
161 asset, ok := m.Downloads[name]
162 if !ok {
163 t.Fatalf("website download %q missing", name)
164 }
165 if !strings.HasSuffix(asset.URL, "/"+name) ||
166 asset.Sig != asset.URL+".minisig" ||
167 asset.SHA256 == "" ||
168 asset.Size == 0 {
169 t.Fatalf("website download %q incomplete: %+v", name, asset)
170 }
171 }
172 }
173
174 func TestGenManifestCanReuseStableNotesForStandaloneRC(t *testing.T) {
175 dir := t.TempDir()
176 if err := os.WriteFile(filepath.Join(dir, "Reasonix-linux-amd64.tar.gz"), []byte("rc"), 0o644); err != nil {
177 t.Fatal(err)
178 }
179 if err := genManifest(dir, "v1.3.0-rc.1", "desktop-v1.3.0-rc.1", "v1.3.0"); err != nil {
180 t.Fatalf("genManifest: %v", err)
181 }
182 raw, err := os.ReadFile(filepath.Join(dir, "latest.json"))
183 if err != nil {
184 t.Fatal(err)
185 }
186 var m update.Manifest
187 if err := json.Unmarshal(raw, &m); err != nil {
188 t.Fatal(err)
189 }
190 if m.ReleaseNotesURL != "https://reasonix.io/changelog/v1.3.0/" {
191 t.Fatalf("release_notes_url = %q, want stable base history", m.ReleaseNotesURL)
192 }
193 }
194
195 // TestGenManifestIgnoresUnknownNativePackages ensures a .deb without a known
196 // platform key is skipped rather than inventing a native_packages entry.
197 func TestGenManifestIgnoresUnknownNativePackages(t *testing.T) {
198 dir := t.TempDir()
199 for _, n := range []string{
200 "Reasonix-linux-amd64.tar.gz",
201 "Reasonix-mystery.deb",
202 } {
203 if err := os.WriteFile(filepath.Join(dir, n), []byte(n), 0o644); err != nil {
204 t.Fatal(err)
205 }
206 }
207 t.Setenv("GITHUB_REPOSITORY", "esengine/DeepSeek-Reasonix")
208 if err := genManifest(dir, "v1.2.0", "desktop-v1.2.0"); err != nil {
209 t.Fatalf("genManifest: %v", err)
210 }
211 raw, err := os.ReadFile(filepath.Join(dir, "latest.json"))
212 if err != nil {
213 t.Fatal(err)
214 }
215 var m update.Manifest
216 if err := json.Unmarshal(raw, &m); err != nil {
217 t.Fatal(err)
218 }
219 if len(m.NativePackages) != 0 {
220 t.Fatalf("unexpected native_packages: %+v", m.NativePackages)
221 }
222 }
223
224 func TestGenWindowsPayloadManifestHashesExactReleaseUnit(t *testing.T) {
225 dir := t.TempDir()
226 for _, name := range update.WindowsPayloadFileNames() {
227 if err := os.WriteFile(filepath.Join(dir, name), []byte("payload:"+name), 0o700); err != nil {
228 t.Fatal(err)
229 }
230 }
231 if err := genWindowsPayloadManifest(dir, "v2.3.4"); err != nil {
232 t.Fatal(err)
233 }
234 b, err := os.ReadFile(filepath.Join(dir, update.WindowsPayloadManifestName))
235 if err != nil {
236 t.Fatal(err)
237 }
238 hashes, err := update.DecodeWindowsPayloadManifest(b, "v2.3.4")
239 if err != nil {
240 t.Fatal(err)
241 }
242 for _, name := range update.WindowsPayloadFileNames() {
243 want := update.WindowsPayloadSHA256([]byte("payload:" + name))
244 if hashes[name] != want {
245 t.Fatalf("manifest hash for %s = %q, want %q", name, hashes[name], want)
246 }
247 }
248 }
249
249 lines GO