Files
package-updater/internal/e2e/webhook_test.go
2026-10-13 07:02:34 +00:00

130 lines
3.7 KiB
Go

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@"+e2eDigest, 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")
}