Repository navigation
Expand file tree
/
Copy pathinstall_test.go
More file actions
179 lines (169 loc) · 6.71 KB
/
Copy pathinstall_test.go
File metadata and controls
179 lines (169 loc) · 6.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// Package install holds tests for install.sh — the curl|sh entry point new
// users run first. It lives at the repo root because that's where the script
// is: a real user's `curl .../install.sh | sh` fetches this exact file.
package install
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)
// TestInstallShShellcheckClean lints install.sh with shellcheck's `sh` dialect
// when shellcheck is available. It skips (not fails) when the tool isn't
// installed — CI images that carry it get the check; a laptop without it
// still gets every other test.
func TestInstallShShellcheckClean(t *testing.T) {
if _, err := exec.LookPath("shellcheck"); err != nil {
t.Skip("shellcheck not installed")
}
out, err := exec.Command("shellcheck", "-s", "sh", "install.sh").CombinedOutput()
if err != nil {
t.Fatalf("shellcheck found issues in install.sh:\n%s", out)
}
}
// TestInstallShAgainstFakeReleaseServer runs the real install.sh end to end
// against a local httptest server standing in for GitHub's release CDN
// (LECTERN_RELEASE_BASE), so this test never touches the network and never
// depends on a real Lectern release existing. It checks the whole path a new
// user's terminal takes: download, checksum verification, install into
// LECTERN_INSTALL_DIR, and a single clear next step at the end.
func TestInstallShAgainstFakeReleaseServer(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("install.sh targets POSIX sh; Windows uses install.ps1")
}
for _, tool := range []string{"sh", "curl", "tar", "install"} {
if _, err := exec.LookPath(tool); err != nil {
t.Skipf("%s not available", tool)
}
}
arch := runtime.GOARCH
if arch != "amd64" && arch != "arm64" {
t.Skipf("unsupported CPU for this test: %s", arch)
}
osName := runtime.GOOS
archiveName := fmt.Sprintf("lectern_%s_%s.tar.gz", osName, arch)
work := t.TempDir()
fakeBin := filepath.Join(work, "lectern")
if err := os.WriteFile(fakeBin, []byte("#!/bin/sh\necho 'lectern version vTEST (fake)'\n"), 0o755); err != nil {
t.Fatal(err)
}
archivePath := filepath.Join(work, archiveName)
if out, err := exec.Command("tar", "czf", archivePath, "-C", work, "lectern").CombinedOutput(); err != nil {
t.Fatalf("build fake release archive: %v\n%s", err, out)
}
archiveBytes, err := os.ReadFile(archivePath)
if err != nil {
t.Fatal(err)
}
sum := sha256.Sum256(archiveBytes)
checksums := fmt.Sprintf("%s %s\n", hex.EncodeToString(sum[:]), archiveName)
mux := http.NewServeMux()
mux.HandleFunc("/"+archiveName, func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(archiveBytes)
})
mux.HandleFunc("/checksums.txt", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(checksums))
})
srv := httptest.NewServer(mux)
defer srv.Close()
t.Run("fresh install", func(t *testing.T) {
installDir := t.TempDir()
out := runInstallSh(t, srv.URL, installDir)
installed := filepath.Join(installDir, "lectern")
if _, err := os.Stat(installed); err != nil {
t.Fatalf("lectern was not installed at %s: %v\noutput:\n%s", installed, err, out)
}
if !strings.Contains(out, "'"+filepath.Join(installDir, "lectern")+" up'") {
t.Errorf("install.sh should end with one clear next step naming `lectern up`:\n%s", out)
}
if !strings.Contains(out, "vTEST") {
t.Errorf("install.sh should print the installed version:\n%s", out)
}
})
t.Run("PATH: says the exact line, adds it only with consent", func(t *testing.T) {
installDir, home := t.TempDir(), t.TempDir()
line := `export PATH="` + installDir + `:$PATH"`
out := runInstallSh(t, srv.URL, installDir, "HOME="+home, "SHELL=/bin/zsh")
if !strings.Contains(out, "add this line to "+filepath.Join(home, ".zshrc")) || !strings.Contains(out, line) {
t.Errorf("without consent install.sh must name the file and line:\n%s", out)
}
if _, err := os.Stat(filepath.Join(home, ".zshrc")); err == nil {
t.Errorf("install.sh changed .zshrc without consent")
}
out = runInstallSh(t, srv.URL, installDir, "HOME="+home, "SHELL=/bin/bash", "LECTERN_MODIFY_PATH=1")
rc, _ := os.ReadFile(filepath.Join(home, ".bashrc"))
if runtime.GOOS == "darwin" {
rc, _ = os.ReadFile(filepath.Join(home, ".bash_profile"))
}
if !strings.Contains(string(rc), line) || !strings.Contains(out, "open a new terminal") && !strings.Contains(out, "Open a new terminal") {
t.Errorf("consented PATH line missing: rc=%q\n%s", rc, out)
}
out = runInstallSh(t, srv.URL, installDir, "HOME="+home, "SHELL=/bin/bash", "LECTERN_MODIFY_PATH=1")
rc2, _ := os.ReadFile(filepath.Join(home, ".bashrc"))
if runtime.GOOS != "darwin" && strings.Count(string(rc2), line) != 1 {
t.Errorf("a second run added the line again:\n%s", rc2)
}
})
t.Run("idempotent re-run", func(t *testing.T) {
installDir := t.TempDir()
runInstallSh(t, srv.URL, installDir)
out := runInstallSh(t, srv.URL, installDir) // second run, same dir
installed := filepath.Join(installDir, "lectern")
if _, err := os.Stat(installed); err != nil {
t.Fatalf("lectern missing after a second run: %v\noutput:\n%s", err, out)
}
})
t.Run("bad checksum is rejected", func(t *testing.T) {
badMux := http.NewServeMux()
badMux.HandleFunc("/"+archiveName, func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(archiveBytes)
})
badMux.HandleFunc("/checksums.txt", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("0000000000000000000000000000000000000000000000000000000000000000 " + archiveName + "\n"))
})
badSrv := httptest.NewServer(badMux)
defer badSrv.Close()
installDir := t.TempDir()
cmd := exec.Command("sh", "install.sh")
cmd.Env = append(os.Environ(),
"LECTERN_RELEASE_BASE="+badSrv.URL,
"LECTERN_INSTALL_DIR="+installDir,
"LECTERN_VERSION=vTEST",
)
out, err := cmd.CombinedOutput()
if err == nil {
t.Fatalf("install.sh should refuse a checksum mismatch:\n%s", out)
}
if !strings.Contains(string(out), "checksum mismatch") {
t.Errorf("expected a checksum mismatch message, got:\n%s", out)
}
if _, statErr := os.Stat(filepath.Join(installDir, "lectern")); statErr == nil {
t.Errorf("a rejected checksum must not leave a binary installed")
}
})
}
func runInstallSh(t *testing.T, releaseBase, installDir string, env ...string) string {
t.Helper()
cmd := exec.Command("sh", "install.sh")
// A throwaway HOME unless the caller names one: install.sh may edit a
// shell rc file there.
cmd.Env = append(os.Environ(),
"HOME="+t.TempDir(),
"LECTERN_RELEASE_BASE="+releaseBase,
"LECTERN_INSTALL_DIR="+installDir,
"LECTERN_VERSION=vTEST",
)
cmd.Env = append(cmd.Env, env...)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("install.sh failed: %v\n%s", err, out)
}
return string(out)
}