d69a58e54a
A remote created through the API without `mutable_ttl` was stored as 0, so its mutable paths never refreshed. With `check_mutable`, the ETag expired together with the freshness key, so an expired mutable path was always refetched in full and never revalidated. - default an omitted `mutable_ttl` to 3600 on create; keep the stored value on update; explicit 0 still means never expire - keep mutable ETags for `max(10 × mutable_ttl, 7d)`, re-armed on each 304, only when `check_mutable` is set and `mutable_ttl` is non-zero - drop the stored ETag when a refetch returns none; never store one for immutable paths - flush a remote's Redis keys on delete - update docs/common/caching.md Reviewed-on: #151 Co-authored-by: unkin-agent <unkin-agent@unkin.net> Co-committed-by: unkin-agent <unkin-agent@unkin.net>
122 lines
3.5 KiB
Go
122 lines
3.5 KiB
Go
package v2
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"git.unkin.net/unkin/artifactapi/internal/database"
|
|
"git.unkin.net/unkin/artifactapi/pkg/models"
|
|
)
|
|
|
|
// fakeFlusher records FlushRemote calls so a test can assert whether — and how
|
|
// often — a remote's cached metadata was purged.
|
|
type fakeFlusher struct {
|
|
calls []string
|
|
err error
|
|
}
|
|
|
|
func (f *fakeFlusher) FlushRemote(_ context.Context, remote string) error {
|
|
f.calls = append(f.calls, remote)
|
|
return f.err
|
|
}
|
|
|
|
func seedRemote(t *testing.T, db *database.DB, name, baseURL string) {
|
|
t.Helper()
|
|
err := db.CreateRemote(context.Background(), &models.Remote{
|
|
Name: name,
|
|
PackageType: models.PackageRPM,
|
|
RepoType: models.RepoTypeRemote,
|
|
BaseURL: baseURL,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("seed remote: %v", err)
|
|
}
|
|
}
|
|
|
|
// A base_url change must flush the remote's cached metadata exactly once, while
|
|
// an update that leaves base_url untouched must not flush at all.
|
|
func TestUpdateFlushesCacheOnBaseURLChange(t *testing.T) {
|
|
if testDSN == "" {
|
|
t.Skip("Docker unavailable")
|
|
}
|
|
db, err := database.New(testDSN)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
const name = "rpm-flush-change"
|
|
seedRemote(t, db, name, "https://old.example.com/repo")
|
|
|
|
ff := &fakeFlusher{}
|
|
h := NewRemotesHandler(db, ff, nil).Routes()
|
|
|
|
if c := do(t, h, "PUT", "/"+name, `{"package_type":"rpm","repo_type":"remote","base_url":"https://new.example.com/repo"}`); c != 200 {
|
|
t.Fatalf("update (backend change) = %d, want 200", c)
|
|
}
|
|
if len(ff.calls) != 1 || ff.calls[0] != name {
|
|
t.Fatalf("flush calls = %v, want exactly one flush of %q", ff.calls, name)
|
|
}
|
|
|
|
// Re-updating with the same (now current) base_url must not flush again.
|
|
ff.calls = nil
|
|
if c := do(t, h, "PUT", "/"+name, `{"package_type":"rpm","repo_type":"remote","base_url":"https://new.example.com/repo"}`); c != 200 {
|
|
t.Fatalf("update (no backend change) = %d, want 200", c)
|
|
}
|
|
if len(ff.calls) != 0 {
|
|
t.Fatalf("flush calls = %v, want no flush when base_url is unchanged", ff.calls)
|
|
}
|
|
}
|
|
|
|
// A flush error must be swallowed: the DB update already succeeded, so the
|
|
// request still returns 200.
|
|
func TestUpdateFlushFailureStillSucceeds(t *testing.T) {
|
|
if testDSN == "" {
|
|
t.Skip("Docker unavailable")
|
|
}
|
|
db, err := database.New(testDSN)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
const name = "rpm-flush-error"
|
|
seedRemote(t, db, name, "https://old.example.com/repo")
|
|
|
|
ff := &fakeFlusher{err: errors.New("redis down")}
|
|
h := NewRemotesHandler(db, ff, nil).Routes()
|
|
|
|
if c := do(t, h, "PUT", "/"+name, `{"package_type":"rpm","repo_type":"remote","base_url":"https://new.example.com/repo"}`); c != 200 {
|
|
t.Fatalf("update with failing flush = %d, want 200", c)
|
|
}
|
|
if len(ff.calls) != 1 {
|
|
t.Fatalf("flush calls = %v, want exactly one attempted flush", ff.calls)
|
|
}
|
|
}
|
|
|
|
// Deleting a remote flushes its cached state so a re-created remote of the
|
|
// same name starts cold; a flush error does not fail the delete.
|
|
func TestDeleteFlushesCache(t *testing.T) {
|
|
if testDSN == "" {
|
|
t.Skip("Docker unavailable")
|
|
}
|
|
db, err := database.New(testDSN)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
for _, ff := range []*fakeFlusher{{}, {err: errors.New("redis down")}} {
|
|
const name = "rpm-flush-delete"
|
|
seedRemote(t, db, name, "https://example.com/repo")
|
|
h := NewRemotesHandler(db, ff, nil).Routes()
|
|
if c := do(t, h, "DELETE", "/"+name, ""); c != 204 {
|
|
t.Fatalf("delete = %d, want 204", c)
|
|
}
|
|
if len(ff.calls) != 1 || ff.calls[0] != name {
|
|
t.Fatalf("flush calls = %v, want exactly one flush of %q", ff.calls, name)
|
|
}
|
|
}
|
|
}
|