mirror of
https://github.com/replicatedhq/ttl.sh.git
synced 2026-08-25 03:07:15 +00:00
220 lines
6.3 KiB
Go
220 lines
6.3 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/events"
|
|
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/store"
|
|
)
|
|
|
|
// Server.Store is documented as safe for concurrent use, so the fake locks its
|
|
// own state rather than leaning on the caller to serialize.
|
|
type fakeStore struct {
|
|
mu sync.Mutex
|
|
rows []store.Row
|
|
upsertErr error // if set, Upsert returns it
|
|
}
|
|
|
|
func (f *fakeStore) Upsert(repo, tag, digest string, expiresAt, createdAt time.Time) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.upsertErr != nil {
|
|
return f.upsertErr
|
|
}
|
|
f.rows = append(f.rows, store.Row{
|
|
Repository: repo,
|
|
Tag: tag,
|
|
ManifestDigest: digest,
|
|
ExpiresAt: expiresAt.Unix(),
|
|
CreatedAt: createdAt.Unix(),
|
|
})
|
|
return nil
|
|
}
|
|
|
|
const testEpoch = 1_700_000_000
|
|
|
|
func fixedClock() time.Time { return time.Unix(testEpoch, 0) }
|
|
|
|
// newTestServer returns a Server backed by an in-memory fake plus the fake
|
|
// itself so tests can assert on what was recorded.
|
|
func newTestServer() (*Server, *fakeStore) {
|
|
fs := &fakeStore{}
|
|
return New(fs, 24*time.Hour, 24*time.Hour, fixedClock), fs
|
|
}
|
|
|
|
func TestHandleEventsRejectsNonPost(t *testing.T) {
|
|
srv, _ := newTestServer()
|
|
req := httptest.NewRequest(http.MethodGet, "/events", nil)
|
|
rec := httptest.NewRecorder()
|
|
srv.handleEvents(rec, req)
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("got %d want 405", rec.Code)
|
|
}
|
|
if rec.Header().Get("Allow") != http.MethodPost {
|
|
t.Errorf("Allow = %q", rec.Header().Get("Allow"))
|
|
}
|
|
}
|
|
|
|
func TestHandleEventsImageUpdatedUpserts(t *testing.T) {
|
|
srv, fs := newTestServer()
|
|
body, _ := json.Marshal(events.ImageUpdatedData{
|
|
Name: "foo/bar", Reference: "1h", Digest: "sha256:deadbeef",
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/events", bytes.NewReader(body))
|
|
req.Header.Set("Ce-Type", events.ImageUpdatedType)
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleEvents(rec, req)
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("got %d want 204", rec.Code)
|
|
}
|
|
if len(fs.rows) != 1 {
|
|
t.Fatalf("got %d rows want 1", len(fs.rows))
|
|
}
|
|
r := fs.rows[0]
|
|
if r.Repository != "foo/bar" || r.Tag != "1h" || r.ManifestDigest != "sha256:deadbeef" {
|
|
t.Errorf("row = %+v", r)
|
|
}
|
|
wantExpires := fixedClock().Add(time.Hour).Unix()
|
|
if r.ExpiresAt != wantExpires {
|
|
t.Errorf("expires_at = %d want %d", r.ExpiresAt, wantExpires)
|
|
}
|
|
}
|
|
|
|
func TestHandleEventsOtherTypeAcked(t *testing.T) {
|
|
srv, fs := newTestServer()
|
|
body := []byte(`{"name":"foo","reference":"1h","digest":"sha256:x"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/events", bytes.NewReader(body))
|
|
req.Header.Set("Ce-Type", "zotregistry.image.deleted")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleEvents(rec, req)
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("got %d want 204", rec.Code)
|
|
}
|
|
if len(fs.rows) != 0 {
|
|
t.Fatalf("non-update event should not store; got %d rows", len(fs.rows))
|
|
}
|
|
}
|
|
|
|
func TestHandleEventsMissingFieldsAcked(t *testing.T) {
|
|
srv, fs := newTestServer()
|
|
body := []byte(`{"digest":"sha256:x"}`) // no name, no reference
|
|
req := httptest.NewRequest(http.MethodPost, "/events", bytes.NewReader(body))
|
|
req.Header.Set("Ce-Type", events.ImageUpdatedType)
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleEvents(rec, req)
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("got %d want 204", rec.Code)
|
|
}
|
|
if len(fs.rows) != 0 {
|
|
t.Fatalf("missing fields should not store; got %d rows", len(fs.rows))
|
|
}
|
|
}
|
|
|
|
func TestHandleEventsMalformedReturns400(t *testing.T) {
|
|
srv, _ := newTestServer()
|
|
body := []byte(`{not json`)
|
|
req := httptest.NewRequest(http.MethodPost, "/events", bytes.NewReader(body))
|
|
req.Header.Set("Ce-Type", events.ImageUpdatedType)
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleEvents(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("got %d want 400", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleEventsStoreErrorReturns500(t *testing.T) {
|
|
fs := &fakeStore{upsertErr: errors.New("boom")}
|
|
srv := New(fs, 24*time.Hour, 24*time.Hour, fixedClock)
|
|
body, _ := json.Marshal(events.ImageUpdatedData{
|
|
Name: "foo", Reference: "1h", Digest: "sha256:x",
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/events", bytes.NewReader(body))
|
|
req.Header.Set("Ce-Type", events.ImageUpdatedType)
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.handleEvents(rec, req)
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Fatalf("got %d want 500", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleHealthz(t *testing.T) {
|
|
srv, _ := newTestServer()
|
|
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
|
rec := httptest.NewRecorder()
|
|
srv.handleHealthz(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("got %d want 200", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "ok") {
|
|
t.Errorf("body = %q", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestShortDigest(t *testing.T) {
|
|
cases := []struct {
|
|
in, want string
|
|
}{
|
|
{"", ""},
|
|
{"sha256:abc", "sha256:abc"}, // shorter than threshold
|
|
{"sha256:0123456789ab", "sha256:0123456789ab"}, // exactly at threshold (len 19)
|
|
{"sha256:0123456789abcdef", "sha256:0123456789ab"}, // longer; truncated
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.in, func(t *testing.T) {
|
|
got := shortDigest(tc.in)
|
|
if got != tc.want {
|
|
t.Errorf("got %q want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRoutesWiring exercises New + Routes over real HTTP round-trips.
|
|
func TestRoutesWiring(t *testing.T) {
|
|
srv := New(&fakeStore{}, 24*time.Hour, 24*time.Hour, fixedClock)
|
|
ts := httptest.NewServer(srv.Routes())
|
|
defer ts.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/healthz")
|
|
if err != nil {
|
|
t.Fatalf("GET /healthz: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("GET /healthz = %d want 200", resp.StatusCode)
|
|
}
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
_ = resp.Body.Close()
|
|
|
|
body, _ := json.Marshal(events.ImageUpdatedData{
|
|
Name: "foo/bar", Reference: "1h", Digest: "sha256:deadbeef",
|
|
})
|
|
req, err := http.NewRequest(http.MethodPost, ts.URL+"/events", bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("new request: %v", err)
|
|
}
|
|
req.Header.Set("Ce-Type", events.ImageUpdatedType)
|
|
resp, err = http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("POST /events: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusNoContent {
|
|
t.Errorf("POST /events = %d want 204", resp.StatusCode)
|
|
}
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
_ = resp.Body.Close()
|
|
}
|