222 lines
8.2 KiB
Go
222 lines
8.2 KiB
Go
package web
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/shcizo/package-updater/internal/master/model"
|
|
)
|
|
|
|
const sluskRepo = "gitea.shcizo.se/shcizo/slusk"
|
|
|
|
func TestServicePage_VersionPicker(t *testing.T) {
|
|
srv, store, _, _, _ := newTestEnv(t)
|
|
store.services[0].ImageRepo = sluskRepo
|
|
store.services[0].ActualDigest = "sha256:cur"
|
|
first := time.Date(2026, 10, 12, 9, 5, 0, 0, time.UTC)
|
|
store.versions = []model.ImageVersion{
|
|
{ID: 31, Repo: sluskRepo, Tag: "1.5.0", Digest: "sha256:" + strings.Repeat("a", 40), Source: "poll", FirstSeenAt: first},
|
|
{ID: 32, Repo: sluskRepo, Tag: "1.4.2", Digest: "sha256:cur", Source: "running", FirstSeenAt: first},
|
|
{ID: 33, Repo: sluskRepo, Digest: "sha256:" + strings.Repeat("b", 40), Source: "running", FirstSeenAt: first},
|
|
{ID: 34, Repo: sluskRepo, Tag: "main", Source: "webhook", FirstSeenAt: first},
|
|
{ID: 40, Repo: "other/repo", Tag: "x", Source: "poll", FirstSeenAt: first},
|
|
}
|
|
|
|
_, body := get(t, srv, "/services/1")
|
|
|
|
for _, id := range []string{"31", "32", "33", "34"} {
|
|
assert.Contains(t, body, `<input type="hidden" name="version" value="`+id+`">`, id)
|
|
}
|
|
assert.NotContains(t, body, `name="version" value="40"`, "versions of other repos are not listed")
|
|
assert.Equal(t, 4, strings.Count(body, `name="version"`))
|
|
assert.Contains(t, body, `name="csrf_token" value="csrf"`)
|
|
assert.Contains(t, body, `title="`+sluskRepo+`:1.5.0@sha256:`+strings.Repeat("a", 40)+`"`, "full ref as title")
|
|
assert.Contains(t, body, sluskRepo+":1.5.0@sha256:"+strings.Repeat("a", 12), "short ref as text")
|
|
assert.Equal(t, 1, strings.Count(body, `<span class="badge">running</span>`), "only the running digest is marked")
|
|
assert.Equal(t, 1, strings.Count(body, "the service will follow no tag afterwards"), "only the tagless row")
|
|
assert.Contains(t, body, "2026-10-12 09:05")
|
|
assert.NotContains(t, body, "auto mode", "no auto warning for a manual service")
|
|
}
|
|
|
|
func TestServicePage_VersionPickerShowsAtMostTenRows(t *testing.T) {
|
|
srv, store, _, _, _ := newTestEnv(t)
|
|
store.services[0].ImageRepo = sluskRepo
|
|
for i := int64(1); i <= 12; i++ {
|
|
store.versions = append(store.versions, model.ImageVersion{ID: i, Repo: sluskRepo, Tag: "t", Digest: "sha256:d", Source: "poll"})
|
|
}
|
|
|
|
_, body := get(t, srv, "/services/1")
|
|
|
|
assert.Equal(t, 10, strings.Count(body, `name="version"`))
|
|
}
|
|
|
|
func TestServicePage_VersionPickerWarnsForAutoServices(t *testing.T) {
|
|
srv, store, _, _, _ := newTestEnv(t)
|
|
store.services[0].UpdateMode = "auto"
|
|
|
|
_, body := get(t, srv, "/services/1")
|
|
|
|
assert.Contains(t, body, "This service is in auto mode")
|
|
}
|
|
|
|
func TestServicePage_VersionPickerHiddenWhenServiceCannotBeUpdated(t *testing.T) {
|
|
for _, reason := range []string{model.ReasonContainer, model.ReasonRemoved, model.ReasonBuilt} {
|
|
t.Run(reason, func(t *testing.T) {
|
|
srv, store, _, _, jobs := newTestEnv(t)
|
|
store.services[0].ImageRepo = sluskRepo
|
|
store.versions = []model.ImageVersion{{ID: 31, Repo: sluskRepo, Tag: "1.5.0", Source: "poll"}}
|
|
jobs.autoErr = &model.NotUpdatableError{Reason: reason}
|
|
|
|
_, body := get(t, srv, "/services/1")
|
|
|
|
assert.Contains(t, body, "<h2>Versions</h2>")
|
|
assert.Contains(t, body, "1.5.0", "the history is still shown")
|
|
assert.NotContains(t, body, `name="version"`)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServicePage_VersionPickerOffersVersionsToAServiceThatFollowsNoTag(t *testing.T) {
|
|
srv, store, _, _, jobs := newTestEnv(t)
|
|
store.services[0].ImageRepo = sluskRepo
|
|
store.versions = []model.ImageVersion{{ID: 31, Repo: sluskRepo, Tag: "1.5.0", Digest: "sha256:a", Source: "poll"}}
|
|
jobs.updatable = &model.NotUpdatableError{Reason: model.ReasonNoTag} // autoErr stays nil: an explicit ref is fine
|
|
|
|
_, body := get(t, srv, "/services/1")
|
|
|
|
assert.Contains(t, body, `name="version" value="31"`)
|
|
assert.NotContains(t, body, `<button type="submit">Update</button>`, "the plain Update button stays hidden")
|
|
}
|
|
|
|
func TestServicePage_PollingRow(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
edit func(*model.Service)
|
|
want string
|
|
failed bool
|
|
}{
|
|
{"polled", func(s *model.Service) { s.ActualDigest = "sha256:cur" }, "every 6h", false},
|
|
{"poll error", func(s *model.Service) { s.ActualDigest = "sha256:cur"; s.LastPollError = "head x: 401 <b>" }, "last check failed: head x: 401 <b>", true},
|
|
{"built", func(s *model.Service) { s.ActualDigest = "sha256:cur"; s.Built = true }, "not polled: built from source", false},
|
|
{"no digest", func(s *model.Service) {}, "not polled: no registry digest", false},
|
|
{"no tag", func(s *model.Service) { s.ActualRef = "r@sha256:a"; s.ActualDigest = "sha256:a" }, "not polled: follows no tag", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
srv, store, _, _, _ := newTestEnvPolling(t, 6*time.Hour)
|
|
tt.edit(&store.services[0])
|
|
|
|
_, body := get(t, srv, "/services/1")
|
|
|
|
assert.Contains(t, body, "<th>Polling</th><td")
|
|
assert.Contains(t, body, tt.want)
|
|
assert.Equal(t, tt.failed, strings.Contains(body, `<td class="error">`+tt.want))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestShortDuration(t *testing.T) {
|
|
tests := []struct {
|
|
d time.Duration
|
|
want string
|
|
}{
|
|
{6 * time.Hour, "6h"},
|
|
{90 * time.Minute, "1h30m"},
|
|
{30 * time.Minute, "30m"},
|
|
{90 * time.Second, "1m30s"},
|
|
{45 * time.Second, "45s"},
|
|
{6*time.Hour + 5*time.Second, "6h0m5s"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.want, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, shortDuration(tt.d))
|
|
})
|
|
}
|
|
}
|
|
|
|
func postVersion(t *testing.T, srv *httptest.Server, version string) (*http.Response, string) {
|
|
t.Helper()
|
|
resp, err := noRedirectClient().PostForm(srv.URL+"/services/1/update", url.Values{"csrf_token": {"csrf"}, "version": {version}})
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return resp, string(body)
|
|
}
|
|
|
|
func TestUpdateService_VersionStartsThatVersion(t *testing.T) {
|
|
srv, _, _, _, jobs := newTestEnv(t)
|
|
jobs.job = model.Job{ID: 42}
|
|
|
|
resp, _ := postVersion(t, srv, "31")
|
|
|
|
assert.Equal(t, http.StatusSeeOther, resp.StatusCode)
|
|
assert.Equal(t, "/jobs/42", resp.Header.Get("Location"))
|
|
assert.Equal(t, []int64{31}, jobs.triggered)
|
|
assert.Equal(t, []int64{1}, jobs.triggerSvc)
|
|
assert.Equal(t, "alice", jobs.triggeredBy())
|
|
assert.Zero(t, jobs.plainCalls)
|
|
}
|
|
|
|
func TestUpdateService_WithoutVersionTriggersTheFollowedTag(t *testing.T) {
|
|
srv, _, _, _, jobs := newTestEnv(t)
|
|
jobs.job = model.Job{ID: 42}
|
|
|
|
resp, _ := postVersion(t, srv, "")
|
|
|
|
assert.Equal(t, http.StatusSeeOther, resp.StatusCode)
|
|
assert.Equal(t, 1, jobs.plainCalls)
|
|
assert.Empty(t, jobs.triggered)
|
|
}
|
|
|
|
func TestUpdateService_VersionErrors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
version string
|
|
err error
|
|
wantCode int
|
|
wantBody string
|
|
wantJob string // Location header
|
|
}{
|
|
{"not a number", "abc", nil, http.StatusBadRequest, "invalid version", ""},
|
|
{"a ref instead of an id", "repo:evil@sha256:x", nil, http.StatusBadRequest, "invalid version", ""},
|
|
{"unknown version or other repo", "77", model.ErrNotFound, http.StatusNotFound, "not found", ""},
|
|
{"not updatable", "31", &model.NotUpdatableError{Reason: model.ReasonBuilt}, http.StatusConflict, "Cannot update: " + model.ReasonBuilt + ".", ""},
|
|
{"job already active", "31", model.ErrJobActive, http.StatusSeeOther, "", "/jobs/41"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
srv, _, _, _, jobs := newTestEnv(t)
|
|
jobs.err, jobs.job = tt.err, model.Job{ID: 41}
|
|
|
|
resp, body := postVersion(t, srv, tt.version)
|
|
|
|
assert.Equal(t, tt.wantCode, resp.StatusCode)
|
|
assert.Contains(t, body, tt.wantBody)
|
|
assert.Equal(t, tt.wantJob, resp.Header.Get("Location"))
|
|
if tt.wantCode == http.StatusBadRequest {
|
|
assert.Empty(t, jobs.triggered, "no job for a malformed version")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOverview_ShowsFollowsNoTagAndPollFailedBadges(t *testing.T) {
|
|
srv, store, _, _, _ := newTestEnv(t)
|
|
store.services[0].LastPollError = `head x: "boom" <script>`
|
|
|
|
for _, path := range []string{"/", "/fragments/services"} {
|
|
_, body := get(t, srv, path)
|
|
assert.Equal(t, 1, strings.Count(body, "Follows no tag"), "only the digest-only Swarm service: "+path)
|
|
assert.Contains(t, body, `class="tag f-err" title="head x: "boom" <script>">Poll failed</span>`, path)
|
|
assert.Equal(t, 1, strings.Count(body, "Poll failed"), path)
|
|
}
|
|
}
|