build_status_test.go
6941 bytes
1package web
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "sort"
9 "strings"
10 "sync"
11 "testing"
12 "time"
13
14 "github.com/charmbracelet/soft-serve/pkg/access"
15 "github.com/charmbracelet/soft-serve/pkg/config"
16 "github.com/charmbracelet/soft-serve/pkg/proto"
17)
18
19const buildStatusTestSHA = "0123456789abcdef0123456789abcdef01234567"
20
21type buildStatusTestResponse struct {
22 Statuses map[string]buildStatusTestEntry `json:"statuses"`
23}
24
25type buildStatusTestEntry struct {
26 SHA string `json:"sha"`
27 State string `json:"state"`
28 QueuedAt string `json:"queued_at"`
29}
30
31func TestHomepageRendersBuildStatusesOnlyForVisiblePublicRepositories(t *testing.T) {
32 var requested []string
33 server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
34 if request.Method != http.MethodPost || request.URL.Path != "/build-statuses" {
35 t.Fatalf("worker request = %s %s", request.Method, request.URL.Path)
36 }
37 var payload struct {
38 Repositories []string `json:"repositories"`
39 }
40 if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
41 t.Fatal(err)
42 }
43 requested = append(requested, payload.Repositories...)
44 response := buildStatusTestResponse{Statuses: map[string]buildStatusTestEntry{
45 "queued": buildStatusEntry("queued"),
46 "running": buildStatusEntry("running"),
47 "passed": buildStatusEntry("passed"),
48 "failed": buildStatusEntry("failed"),
49 }}
50 writer.Header().Set("Content-Type", "application/json")
51 if err := json.NewEncoder(writer).Encode(response); err != nil {
52 t.Fatal(err)
53 }
54 }))
55 defer server.Close()
56
57 ctx, be, _ := newLFSTestContext(t)
58 configureBuildStatusTest(ctx, server.URL)
59 owner, err := be.CreateUser(ctx, "owner", proto.UserOptions{})
60 if err != nil {
61 t.Fatal(err)
62 }
63 for _, repository := range []string{"queued", "running", "passed", "failed", "missing"} {
64 createHomepageRepository(t, be, ctx, repository, owner, proto.RepositoryOptions{})
65 }
66 createHomepageRepository(t, be, ctx, "private", owner, proto.RepositoryOptions{Private: true})
67 createHomepageRepository(t, be, ctx, "hidden", owner, proto.RepositoryOptions{Hidden: true})
68
69 response := httptest.NewRecorder()
70 NewRouter(ctx).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/", nil))
71 if response.Code != http.StatusOK {
72 t.Fatalf("homepage = %d: %s", response.Code, response.Body.String())
73 }
74 for _, label := range []string{"Queued", "Building", "Passed", "Failed"} {
75 if !strings.Contains(response.Body.String(), "Build: <span") || !strings.Contains(response.Body.String(), ">"+label+"</span>") {
76 t.Errorf("homepage did not render %q: %s", label, response.Body.String())
77 }
78 }
79 if strings.Contains(response.Body.String(), "No build") {
80 t.Errorf("homepage rendered a missing build status: %s", response.Body.String())
81 }
82 sort.Strings(requested)
83 if got, want := strings.Join(requested, ","), "failed,missing,passed,queued,running"; got != want {
84 t.Errorf("worker status requested repositories = %q, want %q", got, want)
85 }
86}
87
88func TestRepositoryOverviewRendersBuildStatusAndChecksAccessBeforeWorker(t *testing.T) {
89 var mutex sync.Mutex
90 requests := 0
91 server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
92 mutex.Lock()
93 requests++
94 mutex.Unlock()
95 var payload struct {
96 Repositories []string `json:"repositories"`
97 }
98 if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
99 t.Fatal(err)
100 }
101 response := buildStatusTestResponse{Statuses: make(map[string]buildStatusTestEntry)}
102 for _, repository := range payload.Repositories {
103 if repository != "no-build" {
104 response.Statuses[repository] = buildStatusEntry(strings.TrimPrefix(repository, "status-"))
105 }
106 }
107 if err := json.NewEncoder(writer).Encode(response); err != nil {
108 t.Fatal(err)
109 }
110 }))
111 defer server.Close()
112
113 ctx, be, _ := newLFSTestContext(t)
114 configureBuildStatusTest(ctx, server.URL)
115 owner, err := be.CreateUser(ctx, "owner", proto.UserOptions{})
116 if err != nil {
117 t.Fatal(err)
118 }
119 for _, repository := range []string{"status-queued", "status-running", "status-passed", "status-failed", "no-build"} {
120 createHomepageRepository(t, be, ctx, repository, owner, proto.RepositoryOptions{})
121 }
122 createHomepageRepository(t, be, ctx, "private", owner, proto.RepositoryOptions{Private: true})
123
124 router := NewRouter(ctx)
125 private := httptest.NewRecorder()
126 router.ServeHTTP(private, httptest.NewRequest(http.MethodGet, "/private", nil))
127 if private.Code != http.StatusNotFound {
128 t.Fatalf("private overview = %d", private.Code)
129 }
130 mutex.Lock()
131 privateRequests := requests
132 mutex.Unlock()
133 if privateRequests != 0 {
134 t.Fatalf("private repository queried worker %d times", privateRequests)
135 }
136 for _, test := range []struct {
137 repository string
138 label string
139 }{
140 {"status-queued", "Queued"},
141 {"status-running", "Building"},
142 {"status-passed", "Passed"},
143 {"status-failed", "Failed"},
144 } {
145 response := httptest.NewRecorder()
146 router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/"+test.repository, nil))
147 if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), ">"+test.label+"</span>") {
148 t.Errorf("overview %s = %d: %s", test.repository, response.Code, response.Body.String())
149 }
150 }
151
152 response := httptest.NewRecorder()
153 router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/no-build", nil))
154 if response.Code != http.StatusOK || strings.Contains(response.Body.String(), "status-badge") {
155 t.Errorf("overview without a build status = %d: %s", response.Code, response.Body.String())
156 }
157}
158
159func TestRepositoryOverviewBuildStatusFailureDoesNotFailPage(t *testing.T) {
160 server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
161 http.Error(writer, "worker unavailable", http.StatusServiceUnavailable)
162 }))
163 defer server.Close()
164
165 ctx, be, _ := newLFSTestContext(t)
166 configureBuildStatusTest(ctx, server.URL)
167 owner, err := be.CreateUser(ctx, "owner", proto.UserOptions{})
168 if err != nil {
169 t.Fatal(err)
170 }
171 createHomepageRepository(t, be, ctx, "public", owner, proto.RepositoryOptions{})
172
173 response := httptest.NewRecorder()
174 NewRouter(ctx).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/public", nil))
175 if response.Code != http.StatusOK || strings.Contains(response.Body.String(), "status-badge") {
176 t.Fatalf("overview = %d: %s", response.Code, response.Body.String())
177 }
178}
179
180func buildStatusEntry(state string) buildStatusTestEntry {
181 return buildStatusTestEntry{SHA: buildStatusTestSHA, State: state, QueuedAt: time.Now().UTC().Format(time.RFC3339)}
182}
183
184func configureBuildStatusTest(ctx context.Context, workerURL string) {
185 cfg := config.FromContext(ctx)
186 cfg.HTTP.WebUI.Enabled = true
187 cfg.HTTP.WebUI.BuildStatusURL = workerURL
188 allowKeyless := true
189 cfg.AllowKeyless = &allowKeyless
190 anonymous := access.ReadOnlyAccess
191 cfg.AnonAccess = &anonymous
192}