github.com/kubri/kubri@v0.5.1-0.20240317001612-bda2aaef967e/integrations/sparkle/build_test.go (about) 1 package sparkle_test 2 3 import ( 4 "context" 5 "crypto/sha1" 6 "encoding/base64" 7 "encoding/xml" 8 "io" 9 "testing" 10 "time" 11 12 "github.com/google/go-cmp/cmp" 13 14 "github.com/kubri/kubri/integrations/sparkle" 15 "github.com/kubri/kubri/internal/testsource" 16 "github.com/kubri/kubri/pkg/crypto/dsa" 17 "github.com/kubri/kubri/pkg/crypto/ed25519" 18 "github.com/kubri/kubri/source" 19 target "github.com/kubri/kubri/target/file" 20 ) 21 22 func TestBuild(t *testing.T) { 23 ctx := context.Background() 24 data := []byte("test") 25 ts := time.Now().UTC() 26 src := testsource.New([]*source.Release{ 27 { 28 Version: "v1.0.0", 29 Date: ts, 30 }, 31 { 32 Version: "v1.1.0", 33 Date: ts, 34 Description: `## New Features 35 - Something 36 - Something else`, 37 }, 38 }) 39 src.UploadAsset(ctx, "v1.0.0", "test.dmg", data) 40 src.UploadAsset(ctx, "v1.0.0", "test_32-bit.exe", data) 41 src.UploadAsset(ctx, "v1.0.0", "test_64-bit.msi", data) 42 src.UploadAsset(ctx, "v1.1.0", "test.dmg", data) 43 src.UploadAsset(ctx, "v1.1.0", "test_32-bit.exe", data) 44 src.UploadAsset(ctx, "v1.1.0", "test_64-bit.msi", data) 45 46 tgt, err := target.New(target.Config{Path: t.TempDir(), URL: "https://example.com"}) 47 if err != nil { 48 t.Fatal(err) 49 } 50 51 w, err := tgt.NewWriter(ctx, "appcast.xml") 52 if err != nil { 53 t.Fatal(err) 54 } 55 w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?> 56 <rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/"> 57 <channel> 58 <item> 59 <title>v1.0.0</title> 60 <pubDate>Mon, 02 Jan 2006 15:04:05 +0000</pubDate> 61 <sparkle:version>1.0.0</sparkle:version> 62 <enclosure url="https://example.com/v1.0.0/test.dmg" sparkle:os="macos" sparkle:version="1.0.0" sparkle:edSignature="test" sparkle:minimumSystemVersion="10.13.0" length="4" type="application/x-apple-diskimage" /> 63 </item> 64 <item> 65 <title>v1.0.0</title> 66 <pubDate>Mon, 02 Jan 2006 15:04:05 +0000</pubDate> 67 <sparkle:version>1.0.0</sparkle:version> 68 <sparkle:tags> 69 <sparkle:criticalUpdate /> 70 </sparkle:tags> 71 <enclosure url="https://example.com/v1.0.0/test_32-bit.exe" sparkle:os="windows-x86" sparkle:version="1.0.0" sparkle:dsaSignature="test" sparkle:installerArguments="/passive" length="4" type="application/vnd.microsoft.portable-executable" /> 72 </item> 73 <item> 74 <title>v1.0.0</title> 75 <pubDate>Mon, 02 Jan 2006 15:04:05 +0000</pubDate> 76 <sparkle:version>1.0.0</sparkle:version> 77 <sparkle:tags> 78 <sparkle:criticalUpdate /> 79 </sparkle:tags> 80 <enclosure url="https://example.com/v1.0.0/test_64-bit.msi" sparkle:os="windows-x64" sparkle:version="1.0.0" sparkle:dsaSignature="test" sparkle:installerArguments="/passive" length="4" type="application/x-msi" /> 81 </item> 82 </channel> 83 </rss>`)) 84 w.Close() 85 86 c := &sparkle.Config{ 87 Title: "Test", 88 Description: "Test", 89 URL: "https://example.com/appcast.xml", 90 Source: src, 91 Target: tgt, 92 FileName: "appcast.xml", 93 Settings: []sparkle.Rule{ 94 { 95 OS: sparkle.Windows, 96 Settings: &sparkle.Settings{ 97 InstallerArguments: "/passive", 98 }, 99 }, 100 { 101 OS: sparkle.MacOS, 102 Settings: &sparkle.Settings{ 103 MinimumSystemVersion: "10.13.0", 104 }, 105 }, 106 { 107 Version: "v1.0", 108 Settings: &sparkle.Settings{ 109 CriticalUpdate: true, 110 }, 111 }, 112 { 113 Version: ">= v1.1", 114 Settings: &sparkle.Settings{ 115 CriticalUpdateBelowVersion: "1.0.0", 116 MinimumAutoupdateVersion: "1.0.0", 117 IgnoreSkippedUpgradesBelowVersion: "1.0.0", 118 }, 119 }, 120 }, 121 } 122 123 pubDate := ts.Format(time.RFC1123) 124 125 want := `<?xml version="1.0" encoding="UTF-8"?> 126 <rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/"> 127 <channel> 128 <title>Test</title> 129 <link>https://example.com/appcast.xml</link> 130 <description>Test</description> 131 <item> 132 <title>v1.1.0</title> 133 <pubDate>` + pubDate + `</pubDate> 134 <description><![CDATA[ 135 <h2>New Features</h2> 136 <ul> 137 <li>Something</li> 138 <li>Something else</li> 139 </ul> 140 ]]></description> 141 <sparkle:version>1.1.0</sparkle:version> 142 <sparkle:criticalUpdate sparkle:version="1.0.0" /> 143 <sparkle:minimumAutoupdateVersion>1.0.0</sparkle:minimumAutoupdateVersion> 144 <sparkle:ignoreSkippedUpgradesBelowVersion>1.0.0</sparkle:ignoreSkippedUpgradesBelowVersion> 145 <enclosure url="https://example.com/v1.1.0/test.dmg" sparkle:os="macos" sparkle:version="1.1.0" sparkle:minimumSystemVersion="10.13.0" length="4" type="application/x-apple-diskimage" /> 146 </item> 147 <item> 148 <title>v1.1.0</title> 149 <pubDate>` + pubDate + `</pubDate> 150 <description><![CDATA[ 151 <h2>New Features</h2> 152 <ul> 153 <li>Something</li> 154 <li>Something else</li> 155 </ul> 156 ]]></description> 157 <sparkle:version>1.1.0</sparkle:version> 158 <sparkle:criticalUpdate sparkle:version="1.0.0" /> 159 <sparkle:minimumAutoupdateVersion>1.0.0</sparkle:minimumAutoupdateVersion> 160 <sparkle:ignoreSkippedUpgradesBelowVersion>1.0.0</sparkle:ignoreSkippedUpgradesBelowVersion> 161 <enclosure url="https://example.com/v1.1.0/test_32-bit.exe" sparkle:os="windows-x86" sparkle:version="1.1.0" sparkle:installerArguments="/passive" length="4" type="application/vnd.microsoft.portable-executable" /> 162 </item> 163 <item> 164 <title>v1.1.0</title> 165 <pubDate>` + pubDate + `</pubDate> 166 <description><![CDATA[ 167 <h2>New Features</h2> 168 <ul> 169 <li>Something</li> 170 <li>Something else</li> 171 </ul> 172 ]]></description> 173 <sparkle:version>1.1.0</sparkle:version> 174 <sparkle:criticalUpdate sparkle:version="1.0.0" /> 175 <sparkle:minimumAutoupdateVersion>1.0.0</sparkle:minimumAutoupdateVersion> 176 <sparkle:ignoreSkippedUpgradesBelowVersion>1.0.0</sparkle:ignoreSkippedUpgradesBelowVersion> 177 <enclosure url="https://example.com/v1.1.0/test_64-bit.msi" sparkle:os="windows-x64" sparkle:version="1.1.0" sparkle:installerArguments="/passive" length="4" type="application/x-msi" /> 178 </item> 179 <item> 180 <title>v1.0.0</title> 181 <pubDate>Mon, 02 Jan 2006 15:04:05 +0000</pubDate> 182 <sparkle:version>1.0.0</sparkle:version> 183 <enclosure url="https://example.com/v1.0.0/test.dmg" sparkle:os="macos" sparkle:version="1.0.0" sparkle:edSignature="test" sparkle:minimumSystemVersion="10.13.0" length="4" type="application/x-apple-diskimage" /> 184 </item> 185 <item> 186 <title>v1.0.0</title> 187 <pubDate>Mon, 02 Jan 2006 15:04:05 +0000</pubDate> 188 <sparkle:version>1.0.0</sparkle:version> 189 <sparkle:tags> 190 <sparkle:criticalUpdate /> 191 </sparkle:tags> 192 <enclosure url="https://example.com/v1.0.0/test_32-bit.exe" sparkle:os="windows-x86" sparkle:version="1.0.0" sparkle:dsaSignature="test" sparkle:installerArguments="/passive" length="4" type="application/vnd.microsoft.portable-executable" /> 193 </item> 194 <item> 195 <title>v1.0.0</title> 196 <pubDate>Mon, 02 Jan 2006 15:04:05 +0000</pubDate> 197 <sparkle:version>1.0.0</sparkle:version> 198 <sparkle:tags> 199 <sparkle:criticalUpdate /> 200 </sparkle:tags> 201 <enclosure url="https://example.com/v1.0.0/test_64-bit.msi" sparkle:os="windows-x64" sparkle:version="1.0.0" sparkle:dsaSignature="test" sparkle:installerArguments="/passive" length="4" type="application/x-msi" /> 202 </item> 203 </channel> 204 </rss>` 205 206 testBuild(t, c, want) 207 208 // Should be no-op as nothing changed so timestamp should still be valid. 209 time.Sleep(time.Second) 210 testBuild(t, c, want) 211 } 212 213 func testBuild(t *testing.T, c *sparkle.Config, want string) { 214 t.Helper() 215 216 ctx := context.Background() 217 if err := sparkle.Build(ctx, c); err != nil { 218 t.Fatal(err) 219 } 220 221 r, err := c.Target.NewReader(ctx, "appcast.xml") 222 if err != nil { 223 t.Fatal(err) 224 } 225 defer r.Close() 226 227 got, err := io.ReadAll(r) 228 if err != nil { 229 t.Fatal(err) 230 } 231 232 if diff := cmp.Diff(want, string(got)); diff != "" { 233 t.Error(diff) 234 } 235 } 236 237 func TestBuildSign(t *testing.T) { 238 ctx := context.Background() 239 data := []byte("test") 240 ts := time.Now().UTC() 241 src := testsource.New([]*source.Release{{Version: "v1.0.0", Date: ts}}) 242 src.UploadAsset(ctx, "v1.0.0", "test.dmg", data) 243 src.UploadAsset(ctx, "v1.0.0", "test.msi", data) 244 245 tgt, err := target.New(target.Config{Path: t.TempDir(), URL: "https://example.com"}) 246 if err != nil { 247 t.Fatal(err) 248 } 249 250 dsaKey, err := dsa.NewPrivateKey() 251 if err != nil { 252 t.Fatal(err) 253 } 254 255 edKey, err := ed25519.NewPrivateKey() 256 if err != nil { 257 t.Fatal(err) 258 } 259 260 c := &sparkle.Config{ 261 Title: "Test", 262 Description: "Test", 263 URL: "https://example.com/appcast.xml", 264 Source: src, 265 Target: tgt, 266 FileName: "appcast.xml", 267 Settings: []sparkle.Rule{}, 268 DSAKey: dsaKey, 269 Ed25519Key: edKey, 270 } 271 272 pubDate := ts.Format(time.RFC1123) 273 274 want := sparkle.RSS{ 275 Channels: []*sparkle.Channel{{ 276 Title: "Test", 277 Link: "https://example.com/appcast.xml", 278 Description: "Test", 279 Items: []*sparkle.Item{ 280 { 281 Title: "v1.0.0", 282 PubDate: pubDate, 283 Version: "1.0.0", 284 Enclosure: &sparkle.Enclosure{ 285 URL: "https://example.com/v1.0.0/test.dmg", 286 OS: "macos", 287 Version: "1.0.0", 288 EDSignature: func() string { 289 sig, _ := ed25519.Sign(edKey, data) 290 return base64.StdEncoding.EncodeToString(sig) 291 }(), 292 Length: 4, 293 Type: "application/x-apple-diskimage", 294 }, 295 }, 296 { 297 Title: "v1.0.0", 298 PubDate: pubDate, 299 Version: "1.0.0", 300 Enclosure: &sparkle.Enclosure{ 301 URL: "https://example.com/v1.0.0/test.msi", 302 OS: "windows", 303 Version: "1.0.0", 304 DSASignature: func() string { 305 sum := sha1.Sum(data) 306 sig, _ := dsa.Sign(dsaKey, sum[:]) 307 return base64.StdEncoding.EncodeToString(sig) 308 }(), 309 Length: 4, 310 Type: "application/x-msi", 311 }, 312 }, 313 }, 314 }}, 315 } 316 317 if err = sparkle.Build(ctx, c); err != nil { 318 t.Fatal(err) 319 } 320 321 r, err := tgt.NewReader(ctx, "appcast.xml") 322 if err != nil { 323 t.Fatal(err) 324 } 325 defer r.Close() 326 327 var got sparkle.RSS 328 if err = xml.NewDecoder(r).Decode(&got); err != nil { 329 t.Fatal(err) 330 } 331 332 compareDSA := cmp.FilterPath(func(p cmp.Path) bool { 333 return p.String() == "Channels.Items.Enclosure.DSASignature" 334 }, cmp.Comparer(func(a, b string) bool { 335 if a == "" || b == "" { 336 return a == b 337 } 338 x, _ := base64.StdEncoding.DecodeString(a) 339 y, _ := base64.StdEncoding.DecodeString(b) 340 pub := dsa.Public(dsaKey) 341 sum := sha1.Sum(data) 342 return dsa.Verify(pub, sum[:], x) && dsa.Verify(pub, sum[:], y) 343 })) 344 345 compareED := cmp.FilterPath(func(p cmp.Path) bool { 346 return p.String() == "Channels.Items.Enclosure.EDSignature" 347 }, cmp.Comparer(func(a, b string) bool { 348 if a == "" || b == "" { 349 return a == b 350 } 351 x, _ := base64.StdEncoding.DecodeString(a) 352 y, _ := base64.StdEncoding.DecodeString(b) 353 pub := ed25519.Public(edKey) 354 return ed25519.Verify(pub, data, x) && ed25519.Verify(pub, data, y) 355 })) 356 357 if diff := cmp.Diff(want, got, compareDSA, compareED); diff != "" { 358 t.Error(diff) 359 } 360 } 361 362 func TestBuildUpload(t *testing.T) { 363 ctx := context.Background() 364 data := []byte("test") 365 ts := time.Now().UTC() 366 src := testsource.New([]*source.Release{{Version: "v1.0.0", Date: ts}}) 367 src.UploadAsset(ctx, "v1.0.0", "test.dmg", data) 368 src.UploadAsset(ctx, "v1.0.0", "test.msi", data) 369 370 for _, upload := range []bool{true, false} { 371 tgt, err := target.New(target.Config{Path: t.TempDir()}) 372 if err != nil { 373 t.Fatal(err) 374 } 375 376 c := &sparkle.Config{ 377 Title: "Test", 378 Description: "Test", 379 URL: "https://example.com/appcast.xml", 380 Source: src, 381 Target: tgt, 382 FileName: "appcast.xml", 383 Settings: []sparkle.Rule{}, 384 UploadPackages: upload, 385 } 386 387 if err := sparkle.Build(ctx, c); err != nil { 388 t.Fatal(err) 389 } 390 391 r, err := src.GetRelease(ctx, "v1.0.0") 392 if err != nil { 393 t.Fatal(err) 394 } 395 396 for _, asset := range r.Assets { 397 rd, err := tgt.NewReader(ctx, r.Version+"/"+asset.Name) 398 if err == nil { 399 rd.Close() 400 } 401 if upload && err != nil { 402 t.Fatalf("should upload assets: %v", err) 403 } 404 if !upload && err == nil { 405 t.Fatal("should not upload assets") 406 } 407 } 408 } 409 }