Merge master
This commit is contained in:
@@ -2,6 +2,7 @@ package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
@@ -121,7 +123,7 @@ func (h *ProxyHandler) serveFetch(w http.ResponseWriter, r *http.Request, remote
|
||||
if err != nil {
|
||||
var proxyErr *proxy.ProxyError
|
||||
if errors.As(err, &proxyErr) {
|
||||
http.Error(w, proxyErr.Message, proxyErr.Status)
|
||||
writeProxyError(w, proxyErr)
|
||||
return
|
||||
}
|
||||
slog.Error("proxy fetch failed", "remote", remote.Name, "path", path, "error", err)
|
||||
@@ -149,6 +151,24 @@ func (h *ProxyHandler) serveFetch(w http.ResponseWriter, r *http.Request, remote
|
||||
io.Copy(w, body)
|
||||
}
|
||||
|
||||
// writeProxyError answers a quarantine refusal as JSON and anything else as text.
|
||||
func writeProxyError(w http.ResponseWriter, pe *proxy.ProxyError) {
|
||||
if pe.RetryAfter > 0 {
|
||||
w.Header().Set("Retry-After", strconv.Itoa(pe.RetryAfter))
|
||||
}
|
||||
if pe.Published.IsZero() {
|
||||
http.Error(w, pe.Message, pe.Status)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(pe.Status)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": pe.Message,
|
||||
"published": pe.Published.Format(time.RFC3339),
|
||||
"available": pe.Available.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
func rewritesMutable(prov provider.Provider, path string) bool {
|
||||
_, sr := prov.(provider.ServeRewriter)
|
||||
_, rr := prov.(provider.ResponseRewriter)
|
||||
@@ -176,7 +196,7 @@ func (h *ProxyHandler) handleProxyHead(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
var proxyErr *proxy.ProxyError
|
||||
if errors.As(err, &proxyErr) {
|
||||
http.Error(w, proxyErr.Message, proxyErr.Status)
|
||||
writeProxyError(w, proxyErr)
|
||||
return
|
||||
}
|
||||
slog.Error("proxy head failed", "remote", remote.Name, "path", path, "error", err)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/proxy"
|
||||
)
|
||||
|
||||
func TestWriteProxyErrorRetryAfter(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
writeProxyError(w, &proxy.ProxyError{Status: 503, Message: "quarantine check unavailable", RetryAfter: 30})
|
||||
if w.Code != 503 || w.Header().Get("Retry-After") != "30" {
|
||||
t.Errorf("code %d, Retry-After %q", w.Code, w.Header().Get("Retry-After"))
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func (h *RemotesHandler) get(w http.ResponseWriter, r *http.Request) {
|
||||
const defaultMutableTTL = 3600
|
||||
|
||||
func (h *RemotesHandler) create(w http.ResponseWriter, r *http.Request) {
|
||||
remote := models.Remote{MutableTTL: defaultMutableTTL}
|
||||
remote := models.Remote{MutableTTL: defaultMutableTTL, QuarantineDays: models.DefaultQuarantineDays}
|
||||
if err := json.NewDecoder(r.Body).Decode(&remote); err != nil {
|
||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||
return
|
||||
@@ -108,6 +108,10 @@ func (h *RemotesHandler) create(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := remote.ValidateQuarantine(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.db.CreateRemote(r.Context(), &remote); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -130,6 +134,7 @@ func (h *RemotesHandler) update(w http.ResponseWriter, r *http.Request) {
|
||||
var remote models.Remote
|
||||
if oldKnown {
|
||||
remote.MutableTTL = existing.MutableTTL
|
||||
remote.QuarantineDays = existing.QuarantineDays
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&remote); err != nil {
|
||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||
@@ -152,6 +157,10 @@ func (h *RemotesHandler) update(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := remote.ValidateQuarantine(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.db.UpdateRemote(r.Context(), &remote); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -100,3 +100,67 @@ func TestSyncIntervalAPI(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/database"
|
||||
)
|
||||
|
||||
func TestVirtualParallelFetchRoundTrip(t *testing.T) {
|
||||
if testDSN == "" {
|
||||
t.Skip("Docker unavailable")
|
||||
}
|
||||
db, err := database.New(testDSN)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
h := NewVirtualsHandler(db).Routes()
|
||||
parallel := func() bool {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, httptest.NewRequest("GET", "/vpf", nil))
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil || w.Code != 200 {
|
||||
t.Fatalf("get = %d %s", w.Code, w.Body)
|
||||
}
|
||||
v, ok := got["parallel_fetch"].(bool)
|
||||
if !ok {
|
||||
t.Fatalf("parallel_fetch missing from %s", w.Body)
|
||||
}
|
||||
return v
|
||||
}
|
||||
const body = `"package_type":"rpm","members":["a"]`
|
||||
|
||||
steps := []struct {
|
||||
method, path, json string
|
||||
want bool
|
||||
}{
|
||||
{"POST", "/", `{"name":"vpf",` + body + `}`, false},
|
||||
{"PUT", "/vpf", `{` + body + `,"parallel_fetch":true}`, true},
|
||||
{"PUT", "/vpf", `{` + body + `,"parallel_fetch":false}`, false},
|
||||
}
|
||||
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 := parallel(); got != s.want {
|
||||
t.Errorf("step %d: parallel_fetch = %v, want %v", i, got, s.want)
|
||||
}
|
||||
}
|
||||
if c := do(t, h, "DELETE", "/vpf", ""); c != 204 {
|
||||
t.Fatalf("delete = %d", c)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user