mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 11:34:59 +08:00
fix(repo):统一发布健康与只读质量门禁
This commit is contained in:
+297
-17
@@ -32,6 +32,89 @@ func fixtureRoot(t *testing.T) string {
|
||||
return abs
|
||||
}
|
||||
|
||||
func copyFixtureRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
source := fixtureRoot(t)
|
||||
target := filepath.Join(t.TempDir(), "release")
|
||||
if err := filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(source, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destination := filepath.Join(target, rel)
|
||||
if info.IsDir() {
|
||||
return os.MkdirAll(destination, info.Mode().Perm())
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(destination, data, info.Mode().Perm())
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
func fixtureRPCBackend(t *testing.T, root string) *fakeBackend {
|
||||
t.Helper()
|
||||
idx, err := LoadIndexFromResourceRoot(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manifestEntries := make([]backendrpc.ResourceManifestEntry, 0, len(idx.Entries))
|
||||
for _, entry := range idx.Entries {
|
||||
size := entry.Bytes
|
||||
manifestEntries = append(manifestEntries, backendrpc.ResourceManifestEntry{
|
||||
URL: entry.URL,
|
||||
Destination: entry.RelativePath,
|
||||
Bytes: &size,
|
||||
BLAKE3: entry.BLAKE3,
|
||||
})
|
||||
}
|
||||
catalog, err := json.Marshal(map[string]any{
|
||||
"available": true,
|
||||
"status": "published",
|
||||
"version": map[string]any{
|
||||
"id": "official-fixture",
|
||||
"resource_root": root,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &fakeBackend{
|
||||
status: &backendrpc.DaemonStatusReport{Status: "ok", Running: true, RPCAvailable: true},
|
||||
doctor: &backendrpc.DoctorReport{Healthy: true, Status: "ok"},
|
||||
catalog: catalog,
|
||||
manifest: &backendrpc.ResourceManifestPage{
|
||||
Available: true,
|
||||
ResourceRoot: root,
|
||||
ManifestVersion: 1,
|
||||
TotalEntries: len(manifestEntries),
|
||||
Entries: manifestEntries,
|
||||
},
|
||||
releaseStatus: &backendrpc.ReleaseStatusReport{
|
||||
Status: "ready",
|
||||
StatusCode: "distribution.ready",
|
||||
OfficialCurrentReleaseID: "official-fixture",
|
||||
DefaultDistributionChannel: "official",
|
||||
OfficialDistributionReady: true,
|
||||
Releases: []backendrpc.ReleaseSummary{{
|
||||
Channel: "official",
|
||||
ID: "official-fixture",
|
||||
Current: true,
|
||||
DistributionIntegrityStatus: "valid",
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const fixtureCurrentCDNPath = "/prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes"
|
||||
|
||||
func TestLoadIndexFromResourceRoot(t *testing.T) {
|
||||
idx, err := LoadIndexFromResourceRoot(fixtureRoot(t))
|
||||
if err != nil {
|
||||
@@ -89,6 +172,188 @@ func TestReleaseSummaryRequiresCompleteManifest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentCDNRequiresRustWholeReleaseHealth(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(t *testing.T, root string)
|
||||
}{
|
||||
{
|
||||
name: "missing entry",
|
||||
mutate: func(t *testing.T, root string) {
|
||||
t.Helper()
|
||||
if err := os.Remove(filepath.Join(root, "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "size mismatch",
|
||||
mutate: func(t *testing.T, root string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(filepath.Join(root, "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash"), []byte("too-large"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same-size corruption",
|
||||
mutate: func(t *testing.T, root string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(filepath.Join(root, "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash"), []byte("CORRUPTED!"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
root := copyFixtureRoot(t)
|
||||
tt.mutate(t, root)
|
||||
backend := fixtureRPCBackend(t, root)
|
||||
backend.releaseStatus = &backendrpc.ReleaseStatusReport{
|
||||
Status: "blocked",
|
||||
StatusCode: "distribution.blocked",
|
||||
OfficialCurrentReleaseID: "official-fixture",
|
||||
DefaultDistributionChannel: "official",
|
||||
OfficialDistributionReady: false,
|
||||
Releases: []backendrpc.ReleaseSummary{{
|
||||
Channel: "official",
|
||||
ID: "official-fixture",
|
||||
Current: true,
|
||||
DistributionIntegrityStatus: "invalid",
|
||||
Diagnostics: []string{"fixture integrity failure"},
|
||||
}},
|
||||
}
|
||||
cfg := DefaultConfig()
|
||||
cfg.RequireIndexed = false
|
||||
cfg.RefreshInterval = 0
|
||||
if err := cfg.Normalize(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server := NewServer(cfg, backend, log.New(io.Discard, "", 0))
|
||||
if err := server.Refresh(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
summary := server.index().Summary()
|
||||
if summary.Ready || summary.Distribution.Ready ||
|
||||
summary.Distribution.StatusCode != "distribution.blocked" {
|
||||
t.Fatalf("summary=%+v", summary)
|
||||
}
|
||||
for _, path := range []string{"/readyz", "/v1/bootstrap"} {
|
||||
recorder := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
if recorder.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("%s status=%d body=%s", path, recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, fixtureCurrentCDNPath, nil))
|
||||
if recorder.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("current CDN status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if recorder.Header().Get("ETag") != "" || recorder.Header().Get("Cache-Control") != "" {
|
||||
t.Fatalf("unhealthy CDN headers=%v", recorder.Header())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshCurrentReleaseHealthTransitionsAndClearsFailure(t *testing.T) {
|
||||
root := copyFixtureRoot(t)
|
||||
backend := fixtureRPCBackend(t, root)
|
||||
cfg := DefaultConfig()
|
||||
cfg.RefreshInterval = 0
|
||||
if err := cfg.Normalize(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server := NewServer(cfg, backend, log.New(io.Discard, "", 0))
|
||||
|
||||
if err := server.Refresh(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !server.index().Summary().Ready {
|
||||
t.Fatal("initial release is not ready")
|
||||
}
|
||||
get := func() *httptest.ResponseRecorder {
|
||||
recorder := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, fixtureCurrentCDNPath, nil))
|
||||
return recorder
|
||||
}
|
||||
if recorder := get(); recorder.Code != http.StatusOK || recorder.Body.String() != "TABLE_CATALOG_FIXTURE" {
|
||||
t.Fatalf("healthy CDN status=%d body=%q", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
|
||||
// The bytes stay the same size, but Rust's next release.status result
|
||||
// revokes whole-release distribution authorization.
|
||||
if err := os.WriteFile(filepath.Join(root, "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash"), []byte("CORRUPTED!"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backend.releaseStatus = &backendrpc.ReleaseStatusReport{
|
||||
Status: "blocked",
|
||||
StatusCode: "distribution.blocked",
|
||||
OfficialCurrentReleaseID: "official-fixture",
|
||||
DefaultDistributionChannel: "official",
|
||||
Releases: []backendrpc.ReleaseSummary{{
|
||||
Channel: "official",
|
||||
ID: "official-fixture",
|
||||
Current: true,
|
||||
DistributionIntegrityStatus: "invalid",
|
||||
}},
|
||||
}
|
||||
if err := server.Refresh(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if server.index().Summary().Ready || get().Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("unhealthy refresh summary=%+v", server.index().Summary())
|
||||
}
|
||||
|
||||
backend.releaseStatusErr = errors.New("release.status transport failure")
|
||||
if err := server.Refresh(context.Background()); err == nil {
|
||||
t.Fatal("expected refresh failure")
|
||||
}
|
||||
if summary := server.index().Summary(); summary.Ready || summary.ResourceRoot != "" {
|
||||
t.Fatalf("failed refresh retained snapshot=%+v", summary)
|
||||
}
|
||||
healthRecorder := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(healthRecorder, httptest.NewRequest(http.MethodGet, "/healthz", nil))
|
||||
var health map[string]any
|
||||
if err := json.Unmarshal(healthRecorder.Body.Bytes(), &health); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
refresh := health["refresh"].(map[string]any)
|
||||
if refresh["last_error"] == "" {
|
||||
t.Fatalf("refresh diagnostics=%v", refresh)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(root, "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash"), []byte("1234567890"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backend.releaseStatusErr = nil
|
||||
backend.releaseStatus = &backendrpc.ReleaseStatusReport{
|
||||
Status: "ready",
|
||||
StatusCode: "distribution.ready",
|
||||
OfficialCurrentReleaseID: "official-fixture",
|
||||
DefaultDistributionChannel: "official",
|
||||
OfficialDistributionReady: true,
|
||||
Releases: []backendrpc.ReleaseSummary{{
|
||||
Channel: "official",
|
||||
ID: "official-fixture",
|
||||
Current: true,
|
||||
DistributionIntegrityStatus: "valid",
|
||||
}},
|
||||
}
|
||||
if err := server.Refresh(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if summary := server.index().Summary(); !summary.Ready || !summary.Distribution.Ready {
|
||||
t.Fatalf("recovered summary=%+v", summary)
|
||||
}
|
||||
if recorder := get(); recorder.Code != http.StatusOK || recorder.Body.String() != "TABLE_CATALOG_FIXTURE" {
|
||||
t.Fatalf("recovered CDN status=%d body=%q", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitCDNPathRejectsEscape(t *testing.T) {
|
||||
if _, _, err := SplitCDNPath("/prod-clientpatch.bluearchiveyostar.com/../etc/passwd"); err == nil {
|
||||
t.Fatal("expected error")
|
||||
@@ -553,18 +818,21 @@ func TestServerInfoRewritesAddressablesOnly(t *testing.T) {
|
||||
}
|
||||
|
||||
type fakeBackend struct {
|
||||
statusCalls int
|
||||
doctorCalls int
|
||||
status *backendrpc.DaemonStatusReport
|
||||
doctor *backendrpc.DoctorReport
|
||||
catalog json.RawMessage
|
||||
resource *backendrpc.ResourceState
|
||||
manifest *backendrpc.ResourceManifestPage
|
||||
daemonLogs *backendrpc.LogsReport
|
||||
taskList *backendrpc.TaskList
|
||||
taskStatus *backendrpc.TaskRecord
|
||||
taskLogs *backendrpc.TaskLogs
|
||||
taskCancel *backendrpc.TaskCancelResult
|
||||
statusCalls int
|
||||
doctorCalls int
|
||||
releaseStatusCalls int
|
||||
status *backendrpc.DaemonStatusReport
|
||||
doctor *backendrpc.DoctorReport
|
||||
releaseStatus *backendrpc.ReleaseStatusReport
|
||||
releaseStatusErr error
|
||||
catalog json.RawMessage
|
||||
resource *backendrpc.ResourceState
|
||||
manifest *backendrpc.ResourceManifestPage
|
||||
daemonLogs *backendrpc.LogsReport
|
||||
taskList *backendrpc.TaskList
|
||||
taskStatus *backendrpc.TaskRecord
|
||||
taskLogs *backendrpc.TaskLogs
|
||||
taskCancel *backendrpc.TaskCancelResult
|
||||
}
|
||||
|
||||
func (f *fakeBackend) DaemonStatus(ctx context.Context) (*backendrpc.DaemonStatusReport, error) {
|
||||
@@ -573,11 +841,23 @@ func (f *fakeBackend) DaemonStatus(ctx context.Context) (*backendrpc.DaemonStatu
|
||||
}
|
||||
func (f *fakeBackend) DaemonDoctor(ctx context.Context) (*backendrpc.DoctorReport, error) {
|
||||
f.doctorCalls++
|
||||
if f.statusCalls == 0 {
|
||||
// status must be called first in real DiscoverAndIndex; this is asserted by call order.
|
||||
}
|
||||
return f.doctor, nil
|
||||
}
|
||||
func (f *fakeBackend) ReleaseStatus(ctx context.Context) (*backendrpc.ReleaseStatusReport, error) {
|
||||
f.releaseStatusCalls++
|
||||
if f.releaseStatusErr != nil {
|
||||
return nil, f.releaseStatusErr
|
||||
}
|
||||
if f.releaseStatus != nil {
|
||||
return f.releaseStatus, nil
|
||||
}
|
||||
return &backendrpc.ReleaseStatusReport{
|
||||
Status: "ready",
|
||||
StatusCode: "distribution.ready",
|
||||
DefaultDistributionChannel: "official",
|
||||
OfficialDistributionReady: true,
|
||||
}, nil
|
||||
}
|
||||
func (f *fakeBackend) ResourceState(ctx context.Context) (*backendrpc.ResourceState, error) {
|
||||
if f.resource != nil {
|
||||
return f.resource, nil
|
||||
@@ -1243,8 +1523,8 @@ func TestDiscoverCallsStatusBeforeDoctor(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fb.statusCalls != 1 || fb.doctorCalls != 1 {
|
||||
t.Fatalf("status=%d doctor=%d", fb.statusCalls, fb.doctorCalls)
|
||||
if fb.statusCalls != 1 || fb.doctorCalls != 1 || fb.releaseStatusCalls != 1 {
|
||||
t.Fatalf("status=%d doctor=%d release_status=%d", fb.statusCalls, fb.doctorCalls, fb.releaseStatusCalls)
|
||||
}
|
||||
if !result.RPCAvailable || result.Index == nil || !result.Index.Summary().Ready {
|
||||
t.Fatalf("result=%+v summary=%+v", result, result.Index.Summary())
|
||||
|
||||
Reference in New Issue
Block a user