package e2e import ( "context" "io" "net/http" "os/exec" "strings" "testing" ) // A webhook aimed at the server's own network is the classic SSRF, and it must fail loudly. 23.3. func TestAWebhookCannotReachThePrivateNetwork(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } ctx := context.Background() in := newInstance(t) john := in.account("john") work := seed(t, in, john, "john", "johnbot") // The cursor starts at the newest event, so only what happens next is delivered. if err := in.db.StartWebhooksHere(ctx); err != nil { t.Fatal(err) } const hook = "https://localhost/deploy" write(t, work, ".barerepo/config", "[repo]\nvisibility = \"public\"\n\n"+ "[[webhook]]\nurl = \""+hook+"\"\nevents = [\"push\"]\n") run(t, work, "git", "commit", "-qam", "add a webhook") run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master") in.srv.DeliverHooks(ctx) state, err := in.db.HooksOf(ctx, "john/johnbot") if err != nil { t.Fatal(err) } got, ok := state[hook] if !ok { t.Fatal("the push delivered nothing and recorded nothing, so a broken hook is silent") } if got.Failures != 1 { t.Errorf("the hook counted %d failures, wanted 1", got.Failures) } if !strings.Contains(got.LastError, "not a public address") && !strings.Contains(got.LastError, "which webhooks may not reach") { t.Errorf("the reason was %q, which does not say the address was denied", got.LastError) } // The config page is the only report a webhook has, so the failure must be on it. 23.4. resp, err := http.Get(in.http.URL + "/john/johnbot/config") if err != nil { t.Fatal(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if !strings.Contains(string(body), hook) { t.Errorf("the config page does not name the webhook:\n%s", body) } if !strings.Contains(string(body), "since the last delivery") { t.Errorf("the config page does not say the hook is failing:\n%s", body) } } // An event no hook named must not be delivered, or the events list means nothing. 23.2. func TestAnUnnamedEventIsNotDelivered(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } ctx := context.Background() in := newInstance(t) john := in.account("john") work := seed(t, in, john, "john", "johnbot") if err := in.db.StartWebhooksHere(ctx); err != nil { t.Fatal(err) } write(t, work, ".barerepo/config", "[repo]\nvisibility = \"public\"\n\n"+ "[[webhook]]\nurl = \"https://localhost/deploy\"\nevents = [\"thread.opened\"]\n") run(t, work, "git", "commit", "-qam", "a hook that only wants threads") run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master") in.srv.DeliverHooks(ctx) state, err := in.db.HooksOf(ctx, "john/johnbot") if err != nil { t.Fatal(err) } if len(state) != 0 { t.Errorf("a push reached a hook that only asked for thread.opened: %v", state) } } // The config page is a hook's only report, so it has to name an event that will never fire. 23.4. func TestTheConfigPageNamesAnEventBarerepoNeverSends(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) john := in.account("john") work := seed(t, in, john, "john", "johnbot") write(t, work, ".barerepo/config", "[repo]\nvisibility = \"public\"\n\n"+ "[[webhook]]\nurl = \"https://deploy.example/hook\"\nevents = [\"push\", \"run.succeeded\"]\n") run(t, work, "git", "commit", "-qam", "a hook that asks for a green build") run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master") resp, err := http.Get(in.http.URL + "/john/johnbot/config") if err != nil { t.Fatal(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if !strings.Contains(string(body), "run.succeeded is not an event barerepo sends") { t.Errorf("the config page does not say the hook asked for something that never fires:\n%s", body) } }