1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6 "bytes"
7 "context"
8 "errors"
9 "net/http"
10 "net/http/httptest"
11 "regexp"
12 "testing"
13
14 "connectrpc.com/connect"
15 "github.com/bnema/zerowrap"
16 "github.com/stretchr/testify/assert"
17 "github.com/stretchr/testify/require"
18
19 pb "bnema.dev/ermite-api/gen/ermite"
20 "bnema.dev/ermite-api/gen/ermite/ermiteconnect"
21)
22
23// failing is a repository client that fails every call with err.
24type failing struct {
25 ermiteconnect.RepoServiceClient
26 err error
27}
28
29func (f failing) ListRepos(context.Context, *connect.Request[pb.ListReposRequest]) (*connect.Response[pb.ListReposResponse], error) {
30 return nil, f.err
31}
32
33func (f failing) GetRepo(context.Context, *connect.Request[pb.GetRepoRequest]) (*connect.Response[pb.GetRepoResponse], error) {
34 return nil, f.err
35}
36
37func serve(t *testing.T, err error, path string) (*httptest.ResponseRecorder, string) {
38 t.Helper()
39 srv, nerr := New(Config{
40 Repos: failing{err: err}, Git: ermiteconnect.NewGitServiceClient(http.DefaultClient, "http://unused.invalid"),
41 PublicURL: "https://forge.example", SourceURL: "https://forge.example/ermite", Version: "v1",
42 })
43 require.NoError(t, nerr)
44 var logs bytes.Buffer
45 ctx := zerowrap.WithCtx(context.Background(), zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &logs}))
46 rec := httptest.NewRecorder()
47 srv.ServeHTTP(rec, httptest.NewRequestWithContext(ctx, http.MethodGet, path, nil))
48 return rec, logs.String()
49}
50
51// Errors of the API: not found (and an invalid name) are the 404 page, any
52// other is the 500 page with an id that the log carries.
53func TestErrorPages(t *testing.T) {
54 rec, logs := serve(t, connect.NewError(connect.CodeNotFound, errors.New("repository not found")), "/gone")
55 assert.Equal(t, http.StatusNotFound, rec.Code)
56 assert.Contains(t, rec.Body.String(), "not found")
57 assert.Empty(t, logs, "a 404 is not an error")
58
59 rec, _ = serve(t, connect.NewError(connect.CodeInvalidArgument, errors.New("bad name")), "/Bad")
60 assert.Equal(t, http.StatusNotFound, rec.Code)
61 assert.NotContains(t, rec.Body.String(), "bad name")
62
63 secret := "dial tcp 10.0.0.5: kv exploded"
64 rec, logs = serve(t, connect.NewError(connect.CodeInternal, errors.New(secret)), "/demo")
65 assert.Equal(t, http.StatusInternalServerError, rec.Code)
66 assert.Equal(t, "no-cache", rec.Header().Get("Cache-Control"))
67 assert.Contains(t, rec.Body.String(), "internal error")
68 assert.NotContains(t, rec.Body.String(), secret, "the cause stays in the log")
69 m := regexp.MustCompile(`request id: ([0-9a-f]{8})`).FindStringSubmatch(rec.Body.String())
70 require.Len(t, m, 2, "the page shows a request id")
71 assert.Contains(t, logs, m[1], "the log carries the same id")
72 assert.Contains(t, logs, "kv exploded")
73
74 rec, _ = serve(t, connect.NewError(connect.CodeInternal, errors.New("x")), "/")
75 assert.Equal(t, http.StatusInternalServerError, rec.Code, "the home page too")
76}
77
78func TestNewValidates(t *testing.T) {
79 _, err := New(Config{PublicURL: "https://forge.example"})
80 assert.Error(t, err, "clients are required")
81 git := ermiteconnect.NewGitServiceClient(http.DefaultClient, "http://x.invalid")
82 _, err = New(Config{Repos: failing{}, Git: git, PublicURL: "not a url"})
83 assert.Error(t, err)
84}