fix: harden DynDNS recovery and publish v1.5.1
This commit is contained in:
1 parent
3383b91e49
commit
81def77e3c
37 files changed
+2415
-250
No files matched your search
@@ -4,11 +4,13 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -65,6 +67,7 @@ func (a *fakeUpdateAgent) counts() (status, start int) {
|
||||
|
||||
func TestManagerKeepsUpdateReminderWhenGiteaBecomesUnavailable(t *testing.T) {
|
||||
t.Parallel()
|
||||
nextVersion := nextReleaseVersion(t)
|
||||
const credential = " test-password-or-token-that-must-stay-secret "
|
||||
var unavailable atomic.Bool
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -78,7 +81,7 @@ func TestManagerKeepsUpdateReminderWhenGiteaBecomesUnavailable(t *testing.T) {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"tag_name":"v1.5.1","html_url":"https://example.invalid/releases/v1.5.1","draft":false,"prerelease":false}`))
|
||||
_, _ = fmt.Fprintf(w, `{"tag_name":%q,"html_url":%q,"draft":false,"prerelease":false}`, nextVersion, "https://example.invalid/releases/"+nextVersion)
|
||||
}))
|
||||
defer api.Close()
|
||||
|
||||
@@ -111,7 +114,7 @@ func TestManagerKeepsUpdateReminderWhenGiteaBecomesUnavailable(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state := manager.State()
|
||||
if !state.UpdateAvailable || state.LatestVersion != "v1.5.1" || state.LastError != "" || state.CredentialUsername != "registry-user" {
|
||||
if !state.UpdateAvailable || state.LatestVersion != nextVersion || state.LastError != "" || state.CredentialUsername != "registry-user" {
|
||||
t.Fatalf("new release was not detected: %#v", state)
|
||||
}
|
||||
settings, err := database.GetSoftwareUpdateSettings(ctx)
|
||||
@@ -127,7 +130,7 @@ func TestManagerKeepsUpdateReminderWhenGiteaBecomesUnavailable(t *testing.T) {
|
||||
t.Fatal("unavailable Gitea endpoint was reported as successful")
|
||||
}
|
||||
state = manager.State()
|
||||
if !state.UpdateAvailable || state.LatestVersion != "v1.5.1" || state.LastError == "" {
|
||||
if !state.UpdateAvailable || state.LatestVersion != nextVersion || state.LastError == "" {
|
||||
t.Fatalf("temporary outage removed the last known update reminder: %#v", state)
|
||||
}
|
||||
}
|
||||
@@ -184,12 +187,13 @@ func TestAgentCompatibilityStartsAtStableV14Protocol(t *testing.T) {
|
||||
|
||||
func TestRequestUpdateRefreshesStaleAgentStateBeforeStarting(t *testing.T) {
|
||||
t.Parallel()
|
||||
nextVersion := nextReleaseVersion(t)
|
||||
agent := &fakeUpdateAgent{status: compatibleAgentStatus(updateprotocol.PhaseIdle)}
|
||||
manager, database := updateReadyManager(t, agent)
|
||||
if manager.State().AgentAvailable {
|
||||
t.Fatal("test precondition failed: cached agent state is already available")
|
||||
}
|
||||
if err := manager.RequestUpdate(context.Background(), "v1.5.1"); err != nil {
|
||||
if err := manager.RequestUpdate(context.Background(), nextVersion); err != nil {
|
||||
t.Fatalf("live agent was rejected because of stale cached state: %v", err)
|
||||
}
|
||||
statusCalls, startCalls := agent.counts()
|
||||
@@ -200,13 +204,14 @@ func TestRequestUpdateRefreshesStaleAgentStateBeforeStarting(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if settings.DesiredVersion != "v1.5.1" {
|
||||
if settings.DesiredVersion != nextVersion {
|
||||
t.Fatalf("target version was not persisted: %#v", settings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestUpdateRejectsUnavailableIncompatibleAndBusyAgents(t *testing.T) {
|
||||
t.Parallel()
|
||||
nextVersion := nextReleaseVersion(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
status updateprotocol.Status
|
||||
@@ -222,7 +227,7 @@ func TestRequestUpdateRejectsUnavailableIncompatibleAndBusyAgents(t *testing.T)
|
||||
t.Parallel()
|
||||
agent := &fakeUpdateAgent{status: test.status, statusErr: test.statusErr}
|
||||
manager, database := updateReadyManager(t, agent)
|
||||
if err := manager.RequestUpdate(context.Background(), "v1.5.1"); err == nil {
|
||||
if err := manager.RequestUpdate(context.Background(), nextVersion); err == nil {
|
||||
t.Fatal("unsafe update request was accepted")
|
||||
}
|
||||
_, startCalls := agent.counts()
|
||||
@@ -242,6 +247,7 @@ func TestRequestUpdateRejectsUnavailableIncompatibleAndBusyAgents(t *testing.T)
|
||||
|
||||
func TestRequestUpdateSerializesParallelClicks(t *testing.T) {
|
||||
t.Parallel()
|
||||
nextVersion := nextReleaseVersion(t)
|
||||
agent := &fakeUpdateAgent{status: compatibleAgentStatus(updateprotocol.PhaseIdle)}
|
||||
manager, _ := updateReadyManager(t, agent)
|
||||
results := make(chan error, 2)
|
||||
@@ -249,7 +255,7 @@ func TestRequestUpdateSerializesParallelClicks(t *testing.T) {
|
||||
for index := 0; index < 2; index++ {
|
||||
go func() {
|
||||
<-start
|
||||
results <- manager.RequestUpdate(context.Background(), "v1.5.1")
|
||||
results <- manager.RequestUpdate(context.Background(), nextVersion)
|
||||
}()
|
||||
}
|
||||
close(start)
|
||||
@@ -337,6 +343,7 @@ func compatibleAgentStatus(phase string) updateprotocol.Status {
|
||||
|
||||
func updateReadyManager(t *testing.T, agent updateAgent) (*Manager, *store.Store) {
|
||||
t.Helper()
|
||||
nextVersion := nextReleaseVersion(t)
|
||||
ctx := context.Background()
|
||||
database, err := store.Open(ctx, filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
@@ -363,7 +370,7 @@ func updateReadyManager(t *testing.T, agent updateAgent) (*Manager, *store.Store
|
||||
if err := manager.SaveCredential(ctx, "registry-user", "registry-password"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.RecordSoftwareUpdateSuccess(ctx, "v1.5.1", "https://git.example/releases/v1.5.1", time.Now()); err != nil {
|
||||
if err := database.RecordSoftwareUpdateSuccess(ctx, nextVersion, "https://git.example/releases/"+nextVersion, time.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings, err := database.GetSoftwareUpdateSettings(ctx)
|
||||
@@ -385,3 +392,110 @@ func waitForCondition(t *testing.T, timeout time.Duration, condition func() bool
|
||||
}
|
||||
t.Fatal("condition did not become true in time")
|
||||
}
|
||||
|
||||
func TestRepositoryCanSwitchPublicPrivateAndBack(t *testing.T) {
|
||||
t.Parallel()
|
||||
nextVersion := nextReleaseVersion(t)
|
||||
var private atomic.Bool
|
||||
var authenticated atomic.Int32
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !private.Load() {
|
||||
if r.Header.Get("Authorization") != "" {
|
||||
t.Error("credentials sent to public repository")
|
||||
}
|
||||
} else {
|
||||
user, password, ok := r.BasicAuth()
|
||||
if !ok || user != "registry-user" || password != "registry-password" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
authenticated.Add(1)
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, `{"tag_name":%q,"draft":false,"prerelease":false}`, nextVersion)
|
||||
}))
|
||||
defer api.Close()
|
||||
manager, database := updateReadyManager(t, &fakeUpdateAgent{status: compatibleAgentStatus(updateprotocol.PhaseIdle)})
|
||||
manager.apiURL = api.URL
|
||||
manager.httpClient = api.Client()
|
||||
ctx := context.Background()
|
||||
if err := database.SaveSoftwareUpdateCredential(ctx, "", []byte{}, []byte{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := manager.CheckNow(ctx); err != nil {
|
||||
t.Fatalf("public release requires login: %v", err)
|
||||
}
|
||||
private.Store(true)
|
||||
if err := manager.CheckNow(ctx); !errors.Is(err, ErrCredentialMissing) {
|
||||
t.Fatalf("missing private access not explained: %v", err)
|
||||
}
|
||||
if !manager.State().UpdateAvailable {
|
||||
t.Fatal("visibility change removed last known release")
|
||||
}
|
||||
if err := manager.SaveCredential(ctx, "registry-user", "registry-password"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := manager.CheckNow(ctx); err != nil {
|
||||
t.Fatalf("private 404 did not retry credentials: %v", err)
|
||||
}
|
||||
private.Store(false)
|
||||
if err := database.SaveSoftwareUpdateCredential(ctx, "registry-user", []byte("corrupt"), []byte("corrupt")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := manager.CheckNow(ctx); err != nil {
|
||||
t.Fatalf("public check depends on old secret: %v", err)
|
||||
}
|
||||
if authenticated.Load() != 1 {
|
||||
t.Fatalf("unexpected authenticated requests: %d", authenticated.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnonymousUpdateRequiresCapableAgent(t *testing.T) {
|
||||
t.Parallel()
|
||||
nextVersion := nextReleaseVersion(t)
|
||||
for _, capable := range []bool{false, true} {
|
||||
t.Run(strconv.FormatBool(capable), func(t *testing.T) {
|
||||
status := compatibleAgentStatus(updateprotocol.PhaseIdle)
|
||||
status.AnonymousPull = capable
|
||||
agent := &fakeUpdateAgent{status: status}
|
||||
manager, database := updateReadyManager(t, agent)
|
||||
if err := database.SaveSoftwareUpdateCredential(context.Background(), "", []byte{}, []byte{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings, err := database.GetSoftwareUpdateSettings(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manager.setStoredState(settings)
|
||||
err = manager.RequestUpdate(context.Background(), nextVersion)
|
||||
if !capable {
|
||||
if err == nil || !strings.Contains(err.Error(), "Compose") || !manager.State().ComposeSyncRequired {
|
||||
t.Fatalf("legacy agent not handled: %v", err)
|
||||
}
|
||||
if len(agent.requests) != 0 {
|
||||
t.Fatal("legacy agent received incompatible anonymous request")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(agent.requests) != 1 || agent.requests[0].RegistryUsername != "" || agent.requests[0].RegistryToken != "" {
|
||||
t.Fatal("anonymous request not forwarded")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Keep update scenarios newer than the source fallback after every release bump.
|
||||
func nextReleaseVersion(t *testing.T) string {
|
||||
t.Helper()
|
||||
parts := strings.Split(strings.TrimPrefix(buildinfo.Version, "v"), ".")
|
||||
if len(parts) != 3 {
|
||||
t.Fatalf("unexpected build version %q", buildinfo.Version)
|
||||
}
|
||||
patch, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return fmt.Sprintf("v%s.%s.%d", parts[0], parts[1], patch+1)
|
||||
}
|
||||
Reference in new issue
Block a user