e2e/release_test.go
121 lines · 5394 bytes
1package e2e
2
3import (
4 "fmt"
5 "net/http"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10)
11
12func TestReleases(t *testing.T) {
13 inst := startInstance(t)
14 aliceKey := inst.newKey(t, "alice")
15 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
16
17 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
18 t.Fatalf("repo create: %s", errOut)
19 }
20 work := t.TempDir()
21 env := inst.gitEnv(aliceKey)
22 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
23 dir := filepath.Join(work, "w")
24 os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644)
25 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
26 mustGit(t, dir, env, "add", ".")
27 mustGit(t, dir, env, "commit", "-q", "-m", "base")
28 mustGit(t, dir, env, "tag", "-a", "v1.0", "-m", "first")
29 mustGit(t, dir, env, "push", "-q", "origin", "main", "v1.0")
30
31 // Create: tag must exist; duplicates refused.
32 if _, errOut, code := inst.ssh(t, aliceKey, "", "release", "create", "alice/app", "v9.9"); code != 3 || !strings.Contains(errOut, "push the tag first") {
33 t.Fatalf("missing tag: exit %d, %s", code, errOut)
34 }
35 if _, errOut, code := inst.ssh(t, aliceKey, "", "release", "create", "alice/app", "v1.0",
36 "--title", "'First light'", "--notes", "'the **first** release'"); code != 0 {
37 t.Fatalf("release create: %s", errOut)
38 }
39 if _, _, code := inst.ssh(t, aliceKey, "", "release", "create", "alice/app", "v1.0"); code != 2 {
40 t.Fatal("duplicate release accepted")
41 }
42
43 // Assets: upload from stdin, validation, dedup, round-trip.
44 payload := "BINARY\x01\x02payload for the release asset\n"
45 if _, errOut, code := inst.ssh(t, aliceKey, payload, "release", "asset", "add", "alice/app", "v1.0", "tool-linux-amd64"); code != 0 {
46 t.Fatalf("asset add: %s", errOut)
47 }
48 if _, _, code := inst.ssh(t, aliceKey, payload, "release", "asset", "add", "alice/app", "v1.0", "tool-linux-amd64"); code != 2 {
49 t.Fatal("duplicate asset accepted")
50 }
51 if _, errOut, code := inst.ssh(t, aliceKey, "", "release", "asset", "add", "alice/app", "v1.0", "empty-file"); code != 2 || !strings.Contains(errOut, "empty asset") {
52 t.Fatalf("empty asset: exit %d, %s", code, errOut)
53 }
54 if _, _, code := inst.ssh(t, aliceKey, "x", "release", "asset", "add", "alice/app", "v1.0", "../evil"); code != 2 {
55 t.Fatal("bad asset name accepted")
56 }
57 got, _, code := inst.ssh(t, aliceKey, "", "release", "asset", "get", "alice/app", "v1.0", "tool-linux-amd64")
58 if code != 0 || got != payload {
59 t.Fatalf("asset round-trip: exit %d, %q", code, got)
60 }
61 out, _, _ := inst.ssh(t, aliceKey, "", "release", "show", "alice/app", "v1.0", "--json")
62 if !strings.Contains(out, `"name":"tool-linux-amd64"`) ||
63 !strings.Contains(out, fmt.Sprintf(`"size":%d`, len(payload))) ||
64 !strings.Contains(out, `"sha256":"`) || !strings.Contains(out, "First light") {
65 t.Fatalf("release show: %s", out)
66 }
67
68 // Web: page renders notes and assets; download streams exact bytes.
69 status, body := inst.get(t, "/alice/app/releases")
70 if status != 200 || !strings.Contains(body, "First light") ||
71 !strings.Contains(body, "<strong>first</strong>") ||
72 !strings.Contains(body, "tool-linux-amd64") {
73 t.Fatalf("releases page: %d\n%s", status, body)
74 }
75 resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/alice/app/releases/download/v1.0/tool-linux-amd64", inst.httpPort))
76 if err != nil {
77 t.Fatal(err)
78 }
79 dl := make([]byte, len(payload)+10)
80 n, _ := resp.Body.Read(dl)
81 resp.Body.Close()
82 if resp.StatusCode != 200 || string(dl[:n]) != payload ||
83 !strings.Contains(resp.Header.Get("Content-Disposition"), "tool-linux-amd64") {
84 t.Fatalf("web download: %d, %q", resp.StatusCode, dl[:n])
85 }
86 if resp, _ := http.Get(fmt.Sprintf("http://127.0.0.1:%d/alice/app/releases/download/v1.0/nope", inst.httpPort)); resp.StatusCode != 404 {
87 t.Fatalf("missing asset download: %d", resp.StatusCode)
88 }
89
90 // Remove, then delete: DB rows and disk both go.
91 if _, _, code := inst.ssh(t, aliceKey, "", "release", "asset", "remove", "alice/app", "v1.0", "tool-linux-amd64"); code != 0 {
92 t.Fatal("asset remove failed")
93 }
94 if _, _, code := inst.ssh(t, aliceKey, "", "release", "asset", "get", "alice/app", "v1.0", "tool-linux-amd64"); code != 3 {
95 t.Fatal("removed asset still served")
96 }
97 if _, _, code := inst.ssh(t, aliceKey, payload, "release", "asset", "add", "alice/app", "v1.0", "again"); code != 0 {
98 t.Fatal("re-add failed")
99 }
100 assetRoot := filepath.Join(inst.root, "repos", "alice", "app.git", "gitbay-releases")
101 if fis, err := os.ReadDir(assetRoot); err != nil || len(fis) == 0 {
102 t.Fatal("asset dir missing on disk")
103 }
104 if _, _, code := inst.ssh(t, aliceKey, "", "release", "delete", "alice/app", "v1.0", "--yes"); code != 0 {
105 t.Fatal("release delete failed")
106 }
107 if fis, _ := os.ReadDir(assetRoot); len(fis) != 0 {
108 t.Fatal("asset files survived release delete")
109 }
110 if out, _, _ := inst.ssh(t, aliceKey, "", "release", "list", "alice/app", "--json"); strings.Contains(out, "v1.0") {
111 t.Fatalf("deleted release listed: %s", out)
112 }
113
114 // Archived repos refuse release writes.
115 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "archive", "alice/app"); code != 0 {
116 t.Fatal("archive failed")
117 }
118 if _, errOut, code := inst.ssh(t, aliceKey, "", "release", "create", "alice/app", "v1.0"); code != 4 || !strings.Contains(errOut, "archived") {
119 t.Fatalf("archived release create: exit %d, %s", code, errOut)
120 }
121}