Skip to content

Commit 805a210

Browse files
Lyckabcclaude
andcommitted
test: add install E2E integration tests
Six end-to-end tests for the install flow that orchestrate against a real httptest hub server: happy path, required dep install/fail, optional dep three-way branch (install / URL / skip). Were accidentally not committed during the feature work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a688155 commit 805a210

1 file changed

Lines changed: 281 additions & 0 deletions

File tree

cmd/install_e2e_test.go

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
package cmd
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/http/httptest"
10+
"os"
11+
"path/filepath"
12+
"strings"
13+
"testing"
14+
"time"
15+
16+
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
18+
"github.com/tojiuni/morphso/internal/hub"
19+
"github.com/tojiuni/morphso/internal/spec"
20+
)
21+
22+
// e2eScript computes the SHA256 so the integrity check in runScriptFlow passes.
23+
func e2eScript(content string) hub.InstallScript {
24+
h := sha256.Sum256([]byte(content))
25+
return hub.InstallScript{
26+
Script: content,
27+
SHA256: fmt.Sprintf("sha256:%x", h),
28+
Version: "latest",
29+
}
30+
}
31+
32+
const (
33+
e2eOkScript = "#!/bin/sh\ntrue\n"
34+
e2eFailScript = "#!/bin/sh\nexit 1\n"
35+
)
36+
37+
// e2eHub is an httptest stub for the morphso-hub endpoints exercised by runInstall.
38+
type e2eHub struct {
39+
*httptest.Server
40+
pkgs map[string]hub.Package
41+
scripts map[string]hub.InstallScript // missing slug → 404
42+
deps map[string]hub.DependencyResponse
43+
recorded []string // package_slug from each POST /installs
44+
}
45+
46+
func newE2EHub(t *testing.T) *e2eHub {
47+
h := &e2eHub{
48+
pkgs: make(map[string]hub.Package),
49+
scripts: make(map[string]hub.InstallScript),
50+
deps: make(map[string]hub.DependencyResponse),
51+
}
52+
53+
mux := http.NewServeMux()
54+
55+
// GET /packages/<slug>[/install-script|/dependencies]
56+
mux.HandleFunc("/packages/", func(w http.ResponseWriter, r *http.Request) {
57+
path := strings.TrimPrefix(r.URL.Path, "/packages/")
58+
slug, sub, _ := strings.Cut(path, "/")
59+
w.Header().Set("Content-Type", "application/json")
60+
switch sub {
61+
case "install-script":
62+
sc, ok := h.scripts[slug]
63+
if !ok {
64+
w.WriteHeader(http.StatusNotFound)
65+
fmt.Fprint(w, `{"error":"not found"}`)
66+
return
67+
}
68+
json.NewEncoder(w).Encode(sc)
69+
case "dependencies":
70+
json.NewEncoder(w).Encode(h.deps[slug]) // zero value → empty deps
71+
default:
72+
pkg, ok := h.pkgs[slug]
73+
if !ok {
74+
w.WriteHeader(http.StatusNotFound)
75+
return
76+
}
77+
json.NewEncoder(w).Encode(pkg)
78+
}
79+
})
80+
81+
// POST /installs
82+
mux.HandleFunc("/installs", func(w http.ResponseWriter, r *http.Request) {
83+
var body struct {
84+
PackageSlug string `json:"package_slug"`
85+
}
86+
json.NewDecoder(r.Body).Decode(&body)
87+
h.recorded = append(h.recorded, body.PackageSlug)
88+
w.WriteHeader(http.StatusCreated)
89+
fmt.Fprint(w, `{"id":"test-id"}`)
90+
})
91+
92+
// GET /users/me/installs (dep planning needs install history)
93+
mux.HandleFunc("/users/me/installs", func(w http.ResponseWriter, r *http.Request) {
94+
w.Header().Set("Content-Type", "application/json")
95+
fmt.Fprint(w, `[]`)
96+
})
97+
98+
h.Server = httptest.NewServer(mux)
99+
t.Cleanup(h.Server.Close)
100+
return h
101+
}
102+
103+
// seedHome creates a temp HOME directory with config.yaml (hub URL + token) and a
104+
// pre-cached spec so runInstall doesn't attempt live OS collection.
105+
func seedHome(t *testing.T, hubURL string) {
106+
home := t.TempDir()
107+
t.Setenv("HOME", home)
108+
morphsoDir := filepath.Join(home, ".morphso")
109+
require.NoError(t, os.MkdirAll(morphsoDir, 0755))
110+
111+
cfgContent := fmt.Sprintf("hub_url: %s\ntoken: test-token\n", hubURL)
112+
require.NoError(t, os.WriteFile(filepath.Join(morphsoDir, "config.yaml"), []byte(cfgContent), 0600))
113+
114+
s := &spec.Spec{
115+
CollectedAt: time.Now(),
116+
OS: "linux",
117+
Arch: "amd64",
118+
MemoryTotalGB: 16,
119+
MemoryFreeGB: 8,
120+
DiskTotalGB: 100,
121+
DiskFreeGB: 50,
122+
InstalledTools: map[string]string{},
123+
}
124+
require.NoError(t, spec.SaveCache(morphsoDir, s))
125+
}
126+
127+
// saveInstallFlags saves package-level install flag state and restores on cleanup.
128+
func saveInstallFlags(t *testing.T) {
129+
yes, noDeps, native, docker, k8s, helm := installYes, installNoDeps, installNative, installDocker, installK8s, installHelm
130+
strategy, cfg, tmpl, reconf := installStrategy, installConfig, installTemplate, installReconfigure
131+
t.Cleanup(func() {
132+
installYes, installNoDeps = yes, noDeps
133+
installNative, installDocker, installK8s, installHelm = native, docker, k8s, helm
134+
installStrategy, installConfig, installTemplate, installReconfigure = strategy, cfg, tmpl, reconf
135+
})
136+
}
137+
138+
// redirectStdin replaces os.Stdin with a pipe whose write end contains input.
139+
func redirectStdin(t *testing.T, input string) {
140+
r, w, err := os.Pipe()
141+
require.NoError(t, err)
142+
old := os.Stdin
143+
os.Stdin = r
144+
t.Cleanup(func() { os.Stdin = old; r.Close() })
145+
go func() { io.WriteString(w, input); w.Close() }()
146+
}
147+
148+
// ── tests ───────────────────────────────────────────────────────────────────
149+
150+
// TestInstallE2E_HappyPath verifies the minimal success path: package with no
151+
// dependencies installs and is recorded.
152+
func TestInstallE2E_HappyPath(t *testing.T) {
153+
h := newE2EHub(t)
154+
h.pkgs["gopedia"] = hub.Package{Slug: "gopedia", Name: "Gopedia", Type: "binary"}
155+
h.scripts["gopedia"] = e2eScript(e2eOkScript)
156+
157+
seedHome(t, h.URL)
158+
saveInstallFlags(t)
159+
installYes = true
160+
installNoDeps = true
161+
162+
require.NoError(t, runInstall(installCmd, []string{"gopedia"}))
163+
assert.Contains(t, h.recorded, "gopedia")
164+
}
165+
166+
// TestInstallE2E_RequiredDep_InstallsBeforeMain verifies that a required dependency
167+
// is installed and recorded before the main package.
168+
func TestInstallE2E_RequiredDep_InstallsBeforeMain(t *testing.T) {
169+
h := newE2EHub(t)
170+
h.pkgs["gopedia"] = hub.Package{Slug: "gopedia", Name: "Gopedia", Type: "binary"}
171+
h.pkgs["ollama"] = hub.Package{Slug: "ollama", Name: "Ollama", Type: "binary"}
172+
h.scripts["gopedia"] = e2eScript(e2eOkScript)
173+
h.scripts["ollama"] = e2eScript(e2eOkScript)
174+
h.deps["gopedia"] = hub.DependencyResponse{
175+
Dependencies: []hub.DependencyInfo{
176+
{Package: hub.Package{Slug: "ollama", Name: "Ollama"}, MinVersion: "latest"},
177+
},
178+
}
179+
180+
seedHome(t, h.URL)
181+
saveInstallFlags(t)
182+
installYes = true
183+
184+
require.NoError(t, runInstall(installCmd, []string{"gopedia"}))
185+
require.Len(t, h.recorded, 2)
186+
assert.Equal(t, "ollama", h.recorded[0], "dep must be recorded before main")
187+
assert.Equal(t, "gopedia", h.recorded[1])
188+
}
189+
190+
// TestInstallE2E_RequiredDep_FailureHaltsInstall verifies fix from PR #6:
191+
// a failing required dependency stops the install with an error and the main
192+
// package is never installed.
193+
func TestInstallE2E_RequiredDep_FailureHaltsInstall(t *testing.T) {
194+
h := newE2EHub(t)
195+
h.pkgs["gopedia"] = hub.Package{Slug: "gopedia", Name: "Gopedia", Type: "binary"}
196+
h.pkgs["ollama"] = hub.Package{Slug: "ollama", Name: "Ollama", Type: "binary"}
197+
h.scripts["gopedia"] = e2eScript(e2eOkScript)
198+
h.scripts["ollama"] = e2eScript(e2eFailScript) // exits 1
199+
h.deps["gopedia"] = hub.DependencyResponse{
200+
Dependencies: []hub.DependencyInfo{
201+
{Package: hub.Package{Slug: "ollama", Name: "Ollama"}, MinVersion: "latest"},
202+
},
203+
}
204+
205+
seedHome(t, h.URL)
206+
saveInstallFlags(t)
207+
installYes = true
208+
209+
err := runInstall(installCmd, []string{"gopedia"})
210+
require.Error(t, err)
211+
assert.Contains(t, err.Error(), "ollama")
212+
assert.NotContains(t, h.recorded, "gopedia", "main must not be recorded after dep failure")
213+
}
214+
215+
// TestInstallE2E_OptionalDep_UserInstalls verifies that choosing [1] (install)
216+
// for an optional dep installs it and records both packages.
217+
func TestInstallE2E_OptionalDep_UserInstalls(t *testing.T) {
218+
h := newE2EHub(t)
219+
h.pkgs["gopedia"] = hub.Package{Slug: "gopedia", Name: "Gopedia", Type: "binary"}
220+
h.pkgs["ollama"] = hub.Package{Slug: "ollama", Name: "Ollama", Type: "binary"}
221+
h.scripts["gopedia"] = e2eScript(e2eOkScript)
222+
h.scripts["ollama"] = e2eScript(e2eOkScript)
223+
h.deps["gopedia"] = hub.DependencyResponse{
224+
Dependencies: []hub.DependencyInfo{
225+
{Package: hub.Package{Slug: "ollama", Name: "Ollama"}, MinVersion: "latest", Optional: true},
226+
},
227+
}
228+
229+
seedHome(t, h.URL)
230+
saveInstallFlags(t)
231+
installYes = true
232+
redirectStdin(t, "1\n") // [1] install
233+
234+
require.NoError(t, runInstall(installCmd, []string{"gopedia"}))
235+
assert.Contains(t, h.recorded, "ollama")
236+
assert.Contains(t, h.recorded, "gopedia")
237+
}
238+
239+
// TestInstallE2E_OptionalDep_UserProvidesURL verifies that choosing [2] (URL)
240+
// skips the dep install but proceeds with the main package.
241+
func TestInstallE2E_OptionalDep_UserProvidesURL(t *testing.T) {
242+
h := newE2EHub(t)
243+
h.pkgs["gopedia"] = hub.Package{Slug: "gopedia", Name: "Gopedia", Type: "binary"}
244+
h.scripts["gopedia"] = e2eScript(e2eOkScript)
245+
h.deps["gopedia"] = hub.DependencyResponse{
246+
Dependencies: []hub.DependencyInfo{
247+
{Package: hub.Package{Slug: "ollama", Name: "Ollama"}, MinVersion: "latest", Optional: true},
248+
},
249+
}
250+
251+
seedHome(t, h.URL)
252+
saveInstallFlags(t)
253+
installYes = true
254+
redirectStdin(t, "2\nhttp://custom:11434\n") // [2] URL → http://custom:11434
255+
256+
require.NoError(t, runInstall(installCmd, []string{"gopedia"}))
257+
assert.Contains(t, h.recorded, "gopedia")
258+
assert.NotContains(t, h.recorded, "ollama", "ollama must not be recorded when user chose URL instead")
259+
}
260+
261+
// TestInstallE2E_OptionalDep_UserSkips verifies that choosing [4] (skip) for an
262+
// optional dep still allows the main package to install.
263+
func TestInstallE2E_OptionalDep_UserSkips(t *testing.T) {
264+
h := newE2EHub(t)
265+
h.pkgs["gopedia"] = hub.Package{Slug: "gopedia", Name: "Gopedia", Type: "binary"}
266+
h.scripts["gopedia"] = e2eScript(e2eOkScript)
267+
h.deps["gopedia"] = hub.DependencyResponse{
268+
Dependencies: []hub.DependencyInfo{
269+
{Package: hub.Package{Slug: "ollama", Name: "Ollama"}, MinVersion: "latest", Optional: true},
270+
},
271+
}
272+
273+
seedHome(t, h.URL)
274+
saveInstallFlags(t)
275+
installYes = true
276+
redirectStdin(t, "4\n") // [4] skip
277+
278+
require.NoError(t, runInstall(installCmd, []string{"gopedia"}))
279+
assert.Contains(t, h.recorded, "gopedia")
280+
assert.NotContains(t, h.recorded, "ollama")
281+
}

0 commit comments

Comments
 (0)