package e2e import ( "context" "encoding/json" "io" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/shcizo/package-updater/internal/master/model" "github.com/shcizo/package-updater/internal/master/webhook" "github.com/shcizo/package-updater/internal/protocol" ) // webhookServer mounts the webhook handler on a test server, wired to the // job environment's store, jobs service and broker. func (e *jobEnv) webhookServer(t *testing.T) *httptest.Server { t.Helper() h := webhook.New(e.store, e.jobs, e.m.broker, webhook.Config{ Token: "ci-key", Timeout: 5 * time.Second, PublicURL: "https://updater.example.com", }, slog.New(slog.NewTextHandler(io.Discard, nil))) srv := httptest.NewServer(h) t.Cleanup(srv.Close) return srv } func (e *jobEnv) setAuto(id int64) { e.store.mu.Lock() defer e.store.mu.Unlock() svc := e.store.svcRows[id] svc.UpdateMode = model.ModeAuto e.store.svcRows[id] = svc } type webhookResponse struct { Matched int `json:"matched"` Results []struct { Status string `json:"status"` JobID int64 `json:"job_id"` ToRef string `json:"to_ref"` Error string `json:"error"` } `json:"results"` } func callWebhook(t *testing.T, srv *httptest.Server, body string) (int, webhookResponse) { t.Helper() req, err := http.NewRequest(http.MethodPost, srv.URL, strings.NewReader(body)) require.NoError(t, err) req.Header.Set("Authorization", "Bearer ci-key") resp, err := http.DefaultClient.Do(req) require.NoError(t, err) defer resp.Body.Close() var out webhookResponse require.NoError(t, json.NewDecoder(resp.Body).Decode(&out)) return resp.StatusCode, out } func TestWebhookWaitsForAutoJob(t *testing.T) { e := newJobEnv(t) e.setAuto(1) srv := e.webhookServer(t) type reply struct { code int body webhookResponse } done := make(chan reply, 1) go func() { code, body := callWebhook(t, srv, `{"image":"`+slusk+`:ignored","tag":"2.0.0"}`) done <- reply{code, body} }() e.waitStarted(t) select { case <-done: t.Fatal("the webhook answered before the job finished") case <-time.After(100 * time.Millisecond): } close(e.exec.release) var r reply select { case r = <-done: case <-time.After(waitFor): t.Fatal("the webhook never answered") } require.Equal(t, http.StatusOK, r.code) require.Len(t, r.body.Results, 1) assert.Equal(t, "succeeded", r.body.Results[0].Status) assert.Equal(t, slusk+":2.0.0", r.body.Results[0].ToRef) job := e.job(t, r.body.Results[0].JobID) assert.Equal(t, model.TriggerWebhook, job.Trigger) assert.Equal(t, model.JobSucceeded, job.Status) assert.Equal(t, slusk+":2.0.0@sha256:e2e", job.ToRef) svc, err := e.store.GetService(context.Background(), 1) require.NoError(t, err) assert.Empty(t, svc.AvailableRef, "the job deployed the suggestion, so it is cleared") } func TestWebhookFailedAutoJobIs500(t *testing.T) { e := newJobEnv(t) e.setAuto(1) srv := e.webhookServer(t) e.agent.stopConn() // no agent connection: the job fails at once with "agent offline" require.Eventually(t, func() bool { svc, _ := e.store.GetService(context.Background(), 1) _, err := e.m.hub.Route(svc, protocol.MinJobVersion) return err != nil }, waitFor, pollEvery) code, body := callWebhook(t, srv, `{"image":"`+slusk+`","tag":"2.0.0"}`) assert.Equal(t, http.StatusInternalServerError, code) require.Len(t, body.Results, 1) assert.Equal(t, "failed", body.Results[0].Status) assert.Equal(t, "agent offline", body.Results[0].Error) assert.Equal(t, model.JobFailed, e.job(t, body.Results[0].JobID).Status) svc, err := e.store.GetService(context.Background(), 1) require.NoError(t, err) assert.Equal(t, slusk+":2.0.0", svc.AvailableRef, "a failed job keeps the suggestion") }