Files
ARTEX/server/update_test.go
T
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

162 lines
5.2 KiB
Go

package server
import (
"context"
"errors"
"net/http"
"testing"
"time"
"github.com/Autumn-27/artex/selfupdate"
)
// releaseCache is the layer that protects the GitHub quota: the unauthenticated
// API allows only 60 requests/hour/IP, and the top-bar "new version available"
// indicator queries once per full page load. Once the cache stops working, a
// user opening a few tabs would burn through the quota, and real update checks
// would then fail to get through.
func newTestCache(fetch func(context.Context, *http.Client) (*selfupdate.Release, error)) *releaseCache {
return &releaseCache{fetch: fetch}
}
func TestReleaseCacheServesFromCache(t *testing.T) {
calls := 0
c := newTestCache(func(context.Context, *http.Client) (*selfupdate.Release, error) {
calls++
return &selfupdate.Release{TagName: "v0.3.8"}, nil
})
for range 5 {
rel, err := c.get(t.Context(), nil, false)
if err != nil {
t.Fatalf("get: %v", err)
}
if rel.TagName != "v0.3.8" {
t.Fatalf("TagName = %q", rel.TagName)
}
}
if calls != 1 {
t.Errorf("5 queries should hit the origin only once, got %d", calls)
}
}
func TestReleaseCacheForceBypasses(t *testing.T) {
calls := 0
c := newTestCache(func(context.Context, *http.Client) (*selfupdate.Release, error) {
calls++
return &selfupdate.Release{TagName: "v0.3.8"}, nil
})
if _, err := c.get(t.Context(), nil, false); err != nil {
t.Fatal(err)
}
// Clicking "check for updates" must return a live result, otherwise a freshly
// published release stays invisible until the cache expires.
if _, err := c.get(t.Context(), nil, true); err != nil {
t.Fatal(err)
}
if calls != 2 {
t.Errorf("force should bypass the cache, want 2 origin fetches, got %d", calls)
}
}
func TestReleaseCacheExpiresAfterTTL(t *testing.T) {
calls := 0
c := newTestCache(func(context.Context, *http.Client) (*selfupdate.Release, error) {
calls++
return &selfupdate.Release{TagName: "v0.3.8"}, nil
})
if _, err := c.get(t.Context(), nil, false); err != nil {
t.Fatal(err)
}
// Move the stored timestamp back to just past expiry, simulating TTL timeout.
c.at = time.Now().Add(-releaseTTL - time.Second)
if _, err := c.get(t.Context(), nil, false); err != nil {
t.Fatal(err)
}
if calls != 2 {
t.Errorf("after TTL expiry it should fetch from the origin again, want 2, got %d", calls)
}
}
func TestReleaseCacheUsesShorterTTLForErrors(t *testing.T) {
calls := 0
c := newTestCache(func(context.Context, *http.Client) (*selfupdate.Release, error) {
calls++
return nil, errors.New("github unreachable")
})
if _, err := c.get(t.Context(), nil, false); err == nil {
t.Fatal("expected an error")
}
// Failed results must also be cached briefly, otherwise every page load waits
// through a timeout for nothing while GitHub is unreachable.
if _, err := c.get(t.Context(), nil, false); err == nil {
t.Fatal("expected an error")
}
if calls != 1 {
t.Errorf("errors should be cached briefly, want 1 origin fetch, got %d", calls)
}
// But the error TTL must be noticeably shorter than the success one, so the
// cache can heal quickly once the network recovers.
if releaseErrTTL >= releaseTTL {
t.Fatalf("error TTL (%v) must be shorter than success TTL (%v)", releaseErrTTL, releaseTTL)
}
c.at = time.Now().Add(-releaseErrTTL - time.Second)
if _, err := c.get(t.Context(), nil, false); err == nil {
t.Fatal("expected an error")
}
if calls != 2 {
t.Errorf("after error TTL expiry it should retry, want 2, got %d", calls)
}
}
func TestReleaseCacheDoesNotPoisonOnCallerCancel(t *testing.T) {
good := &selfupdate.Release{TagName: "v0.3.8"}
c := newTestCache(func(ctx context.Context, _ *http.Client) (*selfupdate.Release, error) {
return good, nil
})
if _, err := c.get(t.Context(), nil, false); err != nil {
t.Fatal(err)
}
// A visitor closing the tab cancels the request. That does not mean GitHub is
// broken, and the "cancelled" error must never be written into the cache —
// otherwise every visitor would get a baffling error for the next 30 minutes.
c.fetch = func(ctx context.Context, _ *http.Client) (*selfupdate.Release, error) {
return nil, ctx.Err()
}
c.at = time.Now().Add(-releaseTTL - time.Second) // expire the cache to force a refetch
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := c.get(ctx, nil, false); err == nil {
t.Fatal("when the caller has cancelled, the error should be passed through to it")
}
// Key invariant: the cancelled call leaves no trace — the cache holds neither
// the "cancelled" error nor loses the previous good result.
if c.err != nil {
t.Fatalf("cancel error must not be written into the cache, got %v", c.err)
}
if c.rel == nil || c.rel.TagName != "v0.3.8" {
t.Fatalf("cache should keep the previous good result, got %+v", c.rel)
}
// The cancellation fetched no new data, so the next visitor should refetch
// from the origin — and get a normal result, unaffected by the earlier cancel.
c.fetch = func(context.Context, *http.Client) (*selfupdate.Release, error) {
return good, nil
}
rel, err := c.get(t.Context(), nil, false)
if err != nil {
t.Fatalf("normal requests after a cancellation must not fail: %v", err)
}
if rel == nil || rel.TagName != "v0.3.8" {
t.Fatalf("should get a normal result, got %+v", rel)
}
}