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  }