Parent directory

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}