Files
artifactapi/internal/api/v2/remotes_flush_test.go
T
unkin-agent d69a58e54a Fix mutable path refresh: default mutable_ttl, revalidate after expiry (#151)
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>
2026-10-17 17:17:41 +11:00

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)
}
}
}