mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
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
162 lines
5.2 KiB
Go
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)
|
|
}
|
|
}
|