ee55ef3fcd
ci/woodpecker/tag/docker Pipeline was successful
github_* remotes sharing a mutable_ttl all become due at the same moment, so many remotes created or synced together burst GitHub at once. - add per-remote `sync_interval` (seconds) for github_rpm/github_deb/github_alpine remotes - offset each remote's slots by a stable name-hashed phase so remotes on one interval spread out - compute slots and the first-sync gate in the lease claim from the DB clock so replicas agree - wait for the first slot on a scheduled first sync and skip the create-time prime; cold-cache primes stay immediate - keep the mutable_ttl freshness predicate unchanged when `sync_interval` is unset - document `sync_interval` in README, rpm docs and UI types Reviewed-on: #166 Co-authored-by: unkin-agent <unkin-agent@unkin.net> Co-committed-by: unkin-agent <unkin-agent@unkin.net>
167 lines
4.9 KiB
Go
167 lines
4.9 KiB
Go
package v2
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"git.unkin.net/unkin/artifactapi/internal/database"
|
|
)
|
|
|
|
// An omitted mutable_ttl defaults to 3600 on create and keeps the stored value
|
|
// on update; an explicit 0 is stored as 0 in both.
|
|
func TestMutableTTLOmittedVsZero(t *testing.T) {
|
|
if testDSN == "" {
|
|
t.Skip("Docker unavailable")
|
|
}
|
|
db, err := database.New(testDSN)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
h := NewRemotesHandler(db, nil, nil).Routes()
|
|
ttl := func(name string) int {
|
|
t.Helper()
|
|
r, err := db.GetRemote(context.Background(), name)
|
|
if err != nil {
|
|
t.Fatalf("get %s: %v", name, err)
|
|
}
|
|
return r.MutableTTL
|
|
}
|
|
const body = `"package_type":"rpm","base_url":"https://example.com/repo"`
|
|
|
|
steps := []struct {
|
|
method, path, json string
|
|
want int
|
|
}{
|
|
{"POST", "/", `{"name":"ttl-omit",` + body + `}`, 3600},
|
|
{"PUT", "/ttl-omit", `{` + body + `,"mutable_ttl":120}`, 120},
|
|
{"PUT", "/ttl-omit", `{` + body + `}`, 120},
|
|
{"PUT", "/ttl-omit", `{` + body + `,"mutable_ttl":0}`, 0},
|
|
{"PUT", "/ttl-omit", `{` + body + `}`, 0},
|
|
}
|
|
for i, s := range steps {
|
|
if c := do(t, h, s.method, s.path, s.json); c/100 != 2 {
|
|
t.Fatalf("step %d %s %s = %d", i, s.method, s.path, c)
|
|
}
|
|
if got := ttl("ttl-omit"); got != s.want {
|
|
t.Errorf("step %d: mutable_ttl = %d, want %d", i, got, s.want)
|
|
}
|
|
}
|
|
|
|
if c := do(t, h, "POST", "/", `{"name":"ttl-zero",`+body+`,"mutable_ttl":0}`); c != 201 {
|
|
t.Fatalf("create explicit 0 = %d", c)
|
|
}
|
|
if got := ttl("ttl-zero"); got != 0 {
|
|
t.Errorf("explicit 0 stored as %d", got)
|
|
}
|
|
}
|
|
|
|
// sync_interval round-trips through the API and DB on github_* remotes and is
|
|
// rejected when negative or on a type without a background syncer.
|
|
func TestSyncIntervalAPI(t *testing.T) {
|
|
if testDSN == "" {
|
|
t.Skip("Docker unavailable")
|
|
}
|
|
db, err := database.New(testDSN)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
h := NewRemotesHandler(db, nil, nil).Routes()
|
|
interval := func(name string) int {
|
|
t.Helper()
|
|
r, err := db.GetRemote(context.Background(), name)
|
|
if err != nil {
|
|
t.Fatalf("get %s: %v", name, err)
|
|
}
|
|
return r.SyncInterval
|
|
}
|
|
const gh = `"package_type":"github_deb","base_url":"https://api.github.com/repos/acme/tools","releases_remote":"github"`
|
|
|
|
if c := do(t, h, "POST", "/", `{"name":"si-gh",`+gh+`}`); c != 201 {
|
|
t.Fatalf("create unset = %d", c)
|
|
}
|
|
if got := interval("si-gh"); got != 0 {
|
|
t.Fatalf("unset sync_interval stored as %d", got)
|
|
}
|
|
if c := do(t, h, "PUT", "/si-gh", `{`+gh+`,"sync_interval":3600}`); c != 200 {
|
|
t.Fatalf("update = %d", c)
|
|
}
|
|
if got := interval("si-gh"); got != 3600 {
|
|
t.Fatalf("sync_interval = %d, want 3600", got)
|
|
}
|
|
|
|
for _, body := range []string{
|
|
`{"name":"si-neg",` + gh + `,"sync_interval":-1}`,
|
|
`{"name":"si-rpm","package_type":"rpm","base_url":"https://example.com/repo","sync_interval":60}`,
|
|
} {
|
|
if c := do(t, h, "POST", "/", body); c != 400 {
|
|
t.Errorf("%s = %d, want 400", body, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQuarantineDaysDefault(t *testing.T) {
|
|
if testDSN == "" {
|
|
t.Skip("Docker unavailable")
|
|
}
|
|
db, err := database.New(testDSN)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
h := NewRemotesHandler(db, nil, nil).Routes()
|
|
days := func() int {
|
|
t.Helper()
|
|
r, err := db.GetRemote(context.Background(), "q-days")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return r.QuarantineDays
|
|
}
|
|
const body = `"package_type":"npm","base_url":"https://example.com","quarantine_enabled":true`
|
|
for i, s := range []struct {
|
|
method, path, json string
|
|
want int
|
|
}{
|
|
{"POST", "/", `{"name":"q-days",` + body + `}`, 3},
|
|
{"PUT", "/q-days", `{` + body + `,"quarantine_days":14}`, 14},
|
|
{"PUT", "/q-days", `{` + body + `}`, 14},
|
|
} {
|
|
if c := do(t, h, s.method, s.path, s.json); c/100 != 2 {
|
|
t.Fatalf("step %d = %d", i, c)
|
|
}
|
|
if got := days(); got != s.want {
|
|
t.Errorf("step %d: quarantine_days = %d, want %d", i, got, s.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQuarantineDaysValidation(t *testing.T) {
|
|
if testDSN == "" {
|
|
t.Skip("Docker unavailable")
|
|
}
|
|
db, err := database.New(testDSN)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
h := NewRemotesHandler(db, nil, nil).Routes()
|
|
const body = `"package_type":"npm","base_url":"https://example.com"`
|
|
for i, s := range []struct {
|
|
method, path, json string
|
|
want int
|
|
}{
|
|
{"POST", "/", `{"name":"q-val",` + body + `,"quarantine_enabled":true,"quarantine_days":0}`, 400},
|
|
{"POST", "/", `{"name":"q-val",` + body + `,"quarantine_enabled":true,"quarantine_days":-1}`, 400},
|
|
{"POST", "/", `{"name":"q-val",` + body + `,"quarantine_days":0}`, 201},
|
|
{"PUT", "/q-val", `{` + body + `,"quarantine_enabled":true}`, 400},
|
|
{"PUT", "/q-val", `{` + body + `,"quarantine_enabled":true,"quarantine_days":1}`, 200},
|
|
{"PUT", "/q-val", `{` + body + `,"quarantine_enabled":true,"quarantine_days":0}`, 400},
|
|
} {
|
|
if c := do(t, h, s.method, s.path, s.json); c != s.want {
|
|
t.Errorf("step %d %s %s = %d, want %d", i, s.method, s.path, c, s.want)
|
|
}
|
|
}
|
|
}
|