Container-Image veröffentlichen / container (push) v1.4.0 wurde manuell veröffentlicht und verifiziert
106 lines
4.4 KiB
Go
106 lines
4.4 KiB
Go
package updateagent
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/updateprotocol"
|
|
)
|
|
|
|
func TestRedactSecretRemovesCredentialFromErrors(t *testing.T) {
|
|
t.Parallel()
|
|
const secret = "sensitive-registry-token"
|
|
redacted := redactSecret(errors.New("registry rejected "+secret), secret)
|
|
if strings.Contains(redacted.Error(), secret) || !strings.Contains(redacted.Error(), "[redacted]") {
|
|
t.Fatalf("credential was not redacted: %q", redacted)
|
|
}
|
|
}
|
|
|
|
type blockingRunner struct {
|
|
started chan updateprotocol.Request
|
|
release chan struct{}
|
|
}
|
|
|
|
func (r *blockingRunner) Update(ctx context.Context, request updateprotocol.Request, progress func(phase, message string)) error {
|
|
progress(updateprotocol.PhasePulling, "image is being pulled")
|
|
r.started <- request
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-r.release:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func TestControllerValidatesRequestsAndRunsOnlyOneUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
runner := &blockingRunner{started: make(chan updateprotocol.Request, 1), release: make(chan struct{})}
|
|
controller := NewController(runner, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
handler := controller.Handler()
|
|
|
|
invalid := httptest.NewRecorder()
|
|
handler.ServeHTTP(invalid, httptest.NewRequest(http.MethodPost, "/v1/update", strings.NewReader(`{"version":"stable","registry_username":"user","registry_token":"secret-token"}`)))
|
|
if invalid.Code != http.StatusBadRequest {
|
|
t.Fatalf("invalid version accepted: status=%d", invalid.Code)
|
|
}
|
|
trailing := httptest.NewRecorder()
|
|
handler.ServeHTTP(trailing, httptest.NewRequest(http.MethodPost, "/v1/update", strings.NewReader(`{"version":"v1.2.3","registry_username":"user","registry_token":"secret-token"} {}`)))
|
|
if trailing.Code != http.StatusBadRequest {
|
|
t.Fatalf("request with trailing JSON accepted: status=%d", trailing.Code)
|
|
}
|
|
invalidCurrent := httptest.NewRecorder()
|
|
handler.ServeHTTP(invalidCurrent, httptest.NewRequest(http.MethodPost, "/v1/update", strings.NewReader(`{"version":"v2.0.0","current_version":"1.3","registry_username":"user","registry_token":"secret-token"}`)))
|
|
if invalidCurrent.Code != http.StatusBadRequest {
|
|
t.Fatalf("invalid current version accepted: status=%d", invalidCurrent.Code)
|
|
}
|
|
missingCurrent := httptest.NewRecorder()
|
|
handler.ServeHTTP(missingCurrent, httptest.NewRequest(http.MethodPost, "/v1/update", strings.NewReader(`{"version":"v2.0.0","registry_username":"user","registry_token":"secret-token"}`)))
|
|
if missingCurrent.Code != http.StatusBadRequest {
|
|
t.Fatalf("request without current version accepted: status=%d", missingCurrent.Code)
|
|
}
|
|
|
|
const secret = "registry-token-must-not-leak"
|
|
body := []byte(`{"version":"v2.0.0","current_version":"v1.4.0","registry_username":"user","registry_token":"` + secret + `"}`)
|
|
accepted := httptest.NewRecorder()
|
|
handler.ServeHTTP(accepted, httptest.NewRequest(http.MethodPost, "/v1/update", bytes.NewReader(body)))
|
|
if accepted.Code != http.StatusAccepted {
|
|
t.Fatalf("valid update rejected: status=%d body=%s", accepted.Code, accepted.Body.String())
|
|
}
|
|
select {
|
|
case request := <-runner.started:
|
|
if request.Version != "v2.0.0" || request.CurrentVersion != "v1.4.0" || request.RegistryToken != secret {
|
|
t.Fatalf("runner received an altered request: %#v", request)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("update runner did not start")
|
|
}
|
|
|
|
busy := httptest.NewRecorder()
|
|
handler.ServeHTTP(busy, httptest.NewRequest(http.MethodPost, "/v1/update", bytes.NewReader(body)))
|
|
if busy.Code != http.StatusConflict {
|
|
t.Fatalf("parallel update was not rejected: status=%d", busy.Code)
|
|
}
|
|
status := httptest.NewRecorder()
|
|
handler.ServeHTTP(status, httptest.NewRequest(http.MethodGet, "/v1/status", nil))
|
|
if status.Code != http.StatusOK || strings.Contains(status.Body.String(), secret) || !strings.Contains(status.Body.String(), `"phase":"pulling"`) || !strings.Contains(status.Body.String(), `"protocol_version":2`) {
|
|
t.Fatalf("unsafe or invalid public status: status=%d body=%s", status.Code, status.Body.String())
|
|
}
|
|
|
|
close(runner.release)
|
|
deadline := time.Now().Add(time.Second)
|
|
for controller.Status().Phase != updateprotocol.PhaseComplete && time.Now().Before(deadline) {
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
if controller.Status().Phase != updateprotocol.PhaseComplete {
|
|
t.Fatalf("update did not complete: %#v", controller.Status())
|
|
}
|
|
}
|