package main
import (
"crypto/rand"
"encoding/base64"
"fmt"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/web"
)
// pickupAsk files an approved request for repo bound to a fresh key and
// holding a secret, and returns the key.
func pickupAsk(t *testing.T, g siteRig, repo string, n int) string {
t.Helper()
raw := make([]byte, 32)
if _, err := rand.Read(raw); err != nil {
t.Fatal(err)
}
s, err := g.db.RequestSignOff(oltp.SignOff{Agent: "bot", Owner: "alice", Op: "ci.worker.token.new", Repo: repo,
Input: fmt.Sprintf(`{"n":%d}`, n), Summary: "x", Pickup: api.PickupCodeOf(raw)})
if err != nil {
t.Fatal(err)
}
if _, err := g.db.DecideSignOff(s.ID, "alice", true); err != nil {
t.Fatal(err)
}
if err := g.db.FinishSignOff(s.ID, `{}`, `{"value":"the-credential"}`); err != nil {
t.Fatal(err)
}
return base64.RawURLEncoding.EncodeToString(raw)
}
// Both pickup routes reach the collector through the real handler, for
// a private repository, a namespaced path and one that does not exist,
// before any visibility gate: a wrong key and no key answer all of them
// with the one JSON 404, and the right key is served (or, for a
// repository the credential does not reach, told so with a 409).
func TestThePickupRoutesAreReachedBeforeAnyGate(t *testing.T) {
g := newSiteRig(t)
calls := 0
do := func(path, auth string) (int, string, string) {
calls++
req := httptest.NewRequest("POST", path, nil)
req.RemoteAddr = fmt.Sprintf("192.0.2.%d:1", calls)
if auth != "" {
req.Header.Set("Authorization", auth)
}
w := httptest.NewRecorder()
g.h.ServeHTTP(w, req)
return w.Code, w.Body.String(), w.Header().Get("Content-Type")
}
fresh := func() string {
raw := make([]byte, 32)
rand.Read(raw)
return "Pickup " + base64.RawURLEncoding.EncodeToString(raw)
}
paths := []string{"/api/v1/pickups/collect", "/r/pub/pickup", "/r/secret/pickup", "/r/alice/secret/pickup", "/r/alice/pub/pickup", "/r/nonexistent/pickup", "/r/alice/nonexistent/pickup"}
var wantStatus int
var wantBody string
for i, path := range paths {
// A request without the Pickup scheme is not the collector's
// (see TestOnlyAPickupSchemeReachesTheCollector).
for _, auth := range []string{fresh()} {
st, body, ct := do(path, auth)
if !strings.Contains(ct, "json") {
t.Errorf("FAIL: %s with %q answered %q, not the collector's JSON", path, auth, ct)
}
if i == 0 {
wantStatus, wantBody = st, body
}
if st != wantStatus || body != wantBody || st != 404 {
t.Errorf("FAIL: %s with %q answered %d %s, want the one 404 %s", path, auth, st, body, wantBody)
}
}
}
for i, c := range []struct {
path, repo string
want int
}{
{"/api/v1/pickups/collect", "secret", 200},
{"/r/secret/pickup", "secret", 200},
{"/r/alice/secret/pickup", "secret", 200},
{"/r/alice/pub/pickup", "pub", 200},
{"/r/nonexistent/pickup", "secret", 409},
{"/r/alice/nonexistent/pickup", "secret", 409},
} {
key := pickupAsk(t, g, c.repo, i)
st, body, _ := do(c.path, "Pickup "+key)
if st != c.want || (st == 200 && !strings.Contains(body, "the-credential")) {
t.Errorf("FAIL: right key at %s answered %d %s", c.path, st, body)
}
if st == 409 {
if again, _, _ := do("/r/secret/pickup", "Pickup "+key); again != 200 {
t.Errorf("FAIL: a 409 consumed the secret: %d", again)
}
}
}
if !t.Failed() {
t.Log("ok")
}
}
// The forge's own sign-in limiter, as the collect route is given it:
// six wrong keys from one address answer 404, and from then on its wrong
// keys are held with 429.
func TestThePickupRouteHoldsAnAddressAfterWrongKeys(t *testing.T) {
db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
var lim api.PickupLimiter = web.NewLoginLimiter()
c := api.NewPickupCollector(api.Deps{SignOffs: signOffStore{db}}, lim, nil)
key := func(c byte) string { return "Pickup " + strings.Repeat(string(rune(c)), 43) }
do := func(auth string) int {
req := httptest.NewRequest("POST", "/api/v1/pickups/collect", nil)
req.Header.Set("Authorization", auth)
req.RemoteAddr = "192.0.2.9:1"
w := httptest.NewRecorder()
c.ServeHTTP(w, req)
return w.Code
}
for i := 0; i < 6; i++ {
if st := do(key(byte('A' + i))); st != 404 {
t.Fatalf("FAIL: wrong key %d answered %d", i, st)
}
}
if st := do(key('Z')); st != 429 {
t.Errorf("FAIL: a held address answered %d", st)
}
if !t.Failed() {
t.Log("ok")
}
}