| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "io" |
| 7 | "net" |
| 8 | "net/http" |
| 9 | "os" |
| 10 | "os/exec" |
| 11 | "path/filepath" |
| 12 | "runtime" |
| 13 | "strings" |
| 14 | "testing" |
| 15 | "time" |
| 16 | ) |
| 17 | |
| 18 | // TestReleaseBinaryReportsItsVersion builds the real release artifact and |
| 19 | // asks the running process what it is. The failure this guards against is |
| 20 | // a release that answers "dev": rastrillo's BuildVersion default, which |
| 21 | // the platform's deploy check compares against the release it believes it |
| 22 | // adopted — so an unstamped binary makes that check structurally unable to |
| 23 | // fail while still reporting green. |
| 24 | // |
| 25 | // The expected value is whatever scripts/build-release.sh stamps, computed |
| 26 | // the same way it computes it: the tag when this revision carries one, and |
| 27 | // the abbreviated sha when it does not. Asserting the full sha instead |
| 28 | // would pass only for untagged builds, which is the case that matters |
| 29 | // least. |
| 30 | func TestReleaseBinaryReportsItsVersion(t *testing.T) { |
| 31 | root, err := filepath.Abs("../..") |
| 32 | if err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | described, err := exec.Command("git", "-C", root, "describe", "--tags", "--always", "HEAD").Output() |
| 36 | if err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | want := strings.TrimSpace(string(described)) |
| 40 | if want == "" || want == "dev" { |
| 41 | t.Fatalf("git describe gave %q, which is not a version this test can check for", want) |
| 42 | } |
| 43 | dir := t.TempDir() |
| 44 | binary := filepath.Join(dir, "docs") |
| 45 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) |
| 46 | defer cancel() |
| 47 | build := exec.CommandContext(ctx, "bash", filepath.Join(root, "scripts/build-release.sh"), binary) |
| 48 | build.Dir = root |
| 49 | build.Env = append(os.Environ(), "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH) |
| 50 | if output, err := build.CombinedOutput(); err != nil { |
| 51 | t.Fatalf("build: %v\n%s", err, output) |
| 52 | } |
| 53 | socket := filepath.Join(dir, "docs.sock") |
| 54 | var output bytes.Buffer |
| 55 | server := exec.CommandContext(ctx, binary, "-env", "development", "-socket", socket, "-db", filepath.Join(dir, "docs.db")) |
| 56 | server.Env = append(os.Environ(), "DOCS_ORIGIN=http://localhost", "DOCS_SEED_ADDRESS=", "DOCS_SAMPLE_DOCUMENTS=0") |
| 57 | server.Stdout = &output |
| 58 | server.Stderr = &output |
| 59 | if err := server.Start(); err != nil { |
| 60 | t.Fatal(err) |
| 61 | } |
| 62 | t.Cleanup(func() { |
| 63 | _ = server.Process.Kill() |
| 64 | _ = server.Wait() |
| 65 | }) |
| 66 | transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { |
| 67 | return (&net.Dialer{}).DialContext(ctx, "unix", socket) |
| 68 | }} |
| 69 | defer transport.CloseIdleConnections() |
| 70 | client := &http.Client{Transport: transport, Timeout: time.Second} |
| 71 | deadline := time.Now().Add(15 * time.Second) |
| 72 | for time.Now().Before(deadline) { |
| 73 | response, err := client.Get("http://localhost/api/version") |
| 74 | if err != nil { |
| 75 | time.Sleep(25 * time.Millisecond) |
| 76 | continue |
| 77 | } |
| 78 | body, readErr := io.ReadAll(response.Body) |
| 79 | _ = response.Body.Close() |
| 80 | if readErr != nil { |
| 81 | t.Fatal(readErr) |
| 82 | } |
| 83 | if response.StatusCode != http.StatusOK || strings.TrimSpace(string(body)) != want { |
| 84 | t.Fatalf("version response %d %q; want %q", response.StatusCode, body, want) |
| 85 | } |
| 86 | return |
| 87 | } |
| 88 | _ = server.Process.Kill() |
| 89 | _ = server.Wait() |
| 90 | t.Fatalf("process did not answer /api/version: %s", output.String()) |
| 91 | } |
| 92 | |