mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
377 lines
15 KiB
Go
377 lines
15 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"bat-api/internal/backendrpc"
|
|
)
|
|
|
|
type releaseBackendStub struct {
|
|
*fakeBackend
|
|
root string
|
|
available bool
|
|
distributionParams []backendrpc.ReleaseDistributionParams
|
|
largeDistribution bool
|
|
}
|
|
|
|
type variableDistributionBackend struct {
|
|
*releaseBackendStub
|
|
officialRoot string
|
|
localizedRoot string
|
|
officialBytes []byte
|
|
localizedBytes []byte
|
|
}
|
|
|
|
func (b *variableDistributionBackend) ReleaseDistribution(_ context.Context, params backendrpc.ReleaseDistributionParams) (*backendrpc.ReleaseDistributionPage, error) {
|
|
b.distributionParams = append(b.distributionParams, params)
|
|
root := b.officialRoot
|
|
data := b.officialBytes
|
|
channel := "official"
|
|
releaseID := "official-1"
|
|
hash := "official-b3"
|
|
if params.Channel == "localized" {
|
|
root = b.localizedRoot
|
|
data = b.localizedBytes
|
|
channel = "localized"
|
|
releaseID = "localized-1"
|
|
hash = "localized-b3"
|
|
}
|
|
return &backendrpc.ReleaseDistributionPage{
|
|
Available: true,
|
|
Channel: channel,
|
|
ReleaseID: releaseID,
|
|
ResourceRoot: root,
|
|
Status: "ready",
|
|
StatusCode: "distribution.ready",
|
|
ArtifactIntegrityStatus: "valid",
|
|
Total: 1,
|
|
Offset: 0,
|
|
Limit: 1,
|
|
Entries: []backendrpc.ReleaseDistributionEntry{{
|
|
URL: "https://prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes",
|
|
Destination: "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes",
|
|
Bytes: uint64(len(data)),
|
|
BLAKE3: hash,
|
|
}},
|
|
}, nil
|
|
}
|
|
|
|
func (b *controlBackend) ReleaseStatus(context.Context) (*backendrpc.ReleaseStatusReport, error) {
|
|
b.calls = append(b.calls, "release.status")
|
|
return &backendrpc.ReleaseStatusReport{Status: "ready", StatusCode: "distribution.ready"}, nil
|
|
}
|
|
|
|
func (b *controlBackend) ReleaseList(context.Context, backendrpc.ReleaseListParams) (*backendrpc.ReleaseListReport, error) {
|
|
b.calls = append(b.calls, "release.list")
|
|
return &backendrpc.ReleaseListReport{Status: "ready", StatusCode: "distribution.ready"}, nil
|
|
}
|
|
|
|
func (b *controlBackend) ReleaseDistribution(context.Context, backendrpc.ReleaseDistributionParams) (*backendrpc.ReleaseDistributionPage, error) {
|
|
b.calls = append(b.calls, "release.distribution")
|
|
return &backendrpc.ReleaseDistributionPage{Available: false, StatusCode: "distribution.blocked"}, nil
|
|
}
|
|
|
|
func (b *controlBackend) ReleaseCleanup(context.Context, backendrpc.ReleaseCleanupParams) (*backendrpc.ReleaseCleanupReport, error) {
|
|
b.calls = append(b.calls, "release.cleanup")
|
|
return &backendrpc.ReleaseCleanupReport{PlanID: "plan-1"}, nil
|
|
}
|
|
|
|
func (b *releaseBackendStub) ReleaseStatus(context.Context) (*backendrpc.ReleaseStatusReport, error) {
|
|
return &backendrpc.ReleaseStatusReport{
|
|
Status: "ready",
|
|
StatusCode: "distribution.ready",
|
|
OfficialCurrentReleaseID: "official-1",
|
|
DefaultDistributionChannel: "official",
|
|
OfficialDistributionReady: true,
|
|
}, nil
|
|
}
|
|
|
|
func (b *releaseBackendStub) ReleaseList(context.Context, backendrpc.ReleaseListParams) (*backendrpc.ReleaseListReport, error) {
|
|
return &backendrpc.ReleaseListReport{
|
|
Status: "ready",
|
|
StatusCode: "distribution.ready",
|
|
Releases: []backendrpc.ReleaseSummary{{
|
|
Channel: "localized",
|
|
ID: "localized-1",
|
|
ManifestContractStatus: "valid",
|
|
ArtifactIntegrityStatus: "valid",
|
|
DistributionIntegrityStatus: "valid",
|
|
}},
|
|
}, nil
|
|
}
|
|
|
|
func (b *releaseBackendStub) ReleaseDistribution(_ context.Context, params backendrpc.ReleaseDistributionParams) (*backendrpc.ReleaseDistributionPage, error) {
|
|
b.distributionParams = append(b.distributionParams, params)
|
|
if b.largeDistribution {
|
|
target := "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes"
|
|
if params.Destination != "" {
|
|
return &backendrpc.ReleaseDistributionPage{
|
|
Available: b.available,
|
|
Channel: params.Channel,
|
|
ReleaseID: params.ReleaseID,
|
|
ResourceRoot: b.root,
|
|
Status: "ready",
|
|
StatusCode: "distribution.ready",
|
|
ArtifactIntegrityStatus: "valid",
|
|
Total: 1,
|
|
Offset: 0,
|
|
Limit: 1,
|
|
Entries: []backendrpc.ReleaseDistributionEntry{{
|
|
URL: "https://prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes",
|
|
Destination: target,
|
|
Bytes: 21,
|
|
BLAKE3: "not-used-by-http-index",
|
|
}},
|
|
}, nil
|
|
}
|
|
entries := make([]backendrpc.ReleaseDistributionEntry, 5000)
|
|
for index := range entries {
|
|
entries[index] = backendrpc.ReleaseDistributionEntry{
|
|
URL: "https://prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/resource.bytes",
|
|
Destination: "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/resource-" + strconv.Itoa(index) + ".bytes",
|
|
Bytes: 21,
|
|
BLAKE3: "not-used-by-http-index",
|
|
}
|
|
}
|
|
entries[0].Destination = target
|
|
return &backendrpc.ReleaseDistributionPage{
|
|
Available: b.available,
|
|
Channel: params.Channel,
|
|
ReleaseID: params.ReleaseID,
|
|
ResourceRoot: b.root,
|
|
Status: "ready",
|
|
StatusCode: "distribution.ready",
|
|
ArtifactIntegrityStatus: "valid",
|
|
Total: len(entries),
|
|
Offset: 0,
|
|
Limit: len(entries),
|
|
Entries: entries,
|
|
}, nil
|
|
}
|
|
return &backendrpc.ReleaseDistributionPage{
|
|
Available: b.available,
|
|
Channel: params.Channel,
|
|
ReleaseID: params.ReleaseID,
|
|
ResourceRoot: b.root,
|
|
Status: "ready",
|
|
StatusCode: "distribution.ready",
|
|
ArtifactIntegrityStatus: "valid",
|
|
Total: 1,
|
|
Offset: 0,
|
|
Limit: 1,
|
|
Entries: []backendrpc.ReleaseDistributionEntry{{
|
|
URL: "https://prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes",
|
|
Destination: "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes",
|
|
Bytes: 21,
|
|
BLAKE3: "not-used-by-http-index",
|
|
}},
|
|
}, nil
|
|
}
|
|
|
|
func TestCDNSingleEntryLookupDoesNotPaginateLargeDistribution(t *testing.T) {
|
|
cfg := DefaultConfig()
|
|
cfg.RequireIndexed = false
|
|
if err := cfg.Normalize(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
backend := &releaseBackendStub{
|
|
fakeBackend: &fakeBackend{},
|
|
root: fixtureRoot(t),
|
|
available: true,
|
|
largeDistribution: true,
|
|
}
|
|
server := NewServer(cfg, backend, nil)
|
|
request := httptest.NewRequest(
|
|
http.MethodGet,
|
|
"/prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes?channel=localized&release_id=localized-1",
|
|
nil,
|
|
)
|
|
recorder := httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(recorder, request)
|
|
if recorder.Code != http.StatusOK || recorder.Body.String() != "TABLE_CATALOG_FIXTURE" {
|
|
t.Fatalf("large distribution CDN status=%d body=%q", recorder.Code, recorder.Body.String())
|
|
}
|
|
if len(backend.distributionParams) != 1 {
|
|
t.Fatalf("single-entry lookup made %d backend calls", len(backend.distributionParams))
|
|
}
|
|
params := backend.distributionParams[0]
|
|
if params.Destination != "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes" {
|
|
t.Fatalf("single-entry destination=%q", params.Destination)
|
|
}
|
|
}
|
|
|
|
func TestCDNUsesLocalizedBytesAndHashForGetAndHead(t *testing.T) {
|
|
cfg := DefaultConfig()
|
|
cfg.RequireIndexed = false
|
|
if err := cfg.Normalize(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rel := filepath.FromSlash("prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes")
|
|
officialBytes := []byte("official-A")
|
|
localizedBytes := []byte("localized-B-with-a-different-length")
|
|
officialRoot := t.TempDir()
|
|
localizedRoot := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Dir(filepath.Join(officialRoot, rel)), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(filepath.Join(localizedRoot, rel)), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(officialRoot, rel), officialBytes, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(localizedRoot, rel), localizedBytes, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
backend := &variableDistributionBackend{
|
|
releaseBackendStub: &releaseBackendStub{fakeBackend: &fakeBackend{}},
|
|
officialRoot: officialRoot,
|
|
localizedRoot: localizedRoot,
|
|
officialBytes: officialBytes,
|
|
localizedBytes: localizedBytes,
|
|
}
|
|
server := NewServer(cfg, backend, nil)
|
|
|
|
localizedURL := "/prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes?channel=localized&release_id=localized-1"
|
|
recorder := httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, localizedURL, nil))
|
|
if recorder.Code != http.StatusOK || recorder.Body.String() != string(localizedBytes) {
|
|
t.Fatalf("localized GET status=%d body=%q", recorder.Code, recorder.Body.String())
|
|
}
|
|
if recorder.Header().Get("ETag") != `"blake3-localized-b3"` {
|
|
t.Fatalf("localized ETag=%q", recorder.Header().Get("ETag"))
|
|
}
|
|
if recorder.Result().ContentLength != int64(len(localizedBytes)) {
|
|
t.Fatalf("localized Content-Length=%d", recorder.Result().ContentLength)
|
|
}
|
|
|
|
recorder = httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodHead, localizedURL, nil))
|
|
if recorder.Code != http.StatusOK || recorder.Body.Len() != 0 {
|
|
t.Fatalf("localized HEAD status=%d body=%d", recorder.Code, recorder.Body.Len())
|
|
}
|
|
if recorder.Header().Get("ETag") != `"blake3-localized-b3"` ||
|
|
recorder.Result().ContentLength != int64(len(localizedBytes)) {
|
|
t.Fatalf("localized HEAD headers etag=%q length=%d", recorder.Header().Get("ETag"), recorder.Result().ContentLength)
|
|
}
|
|
|
|
officialURL := "/prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes?channel=official&release_id=official-1"
|
|
recorder = httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, officialURL, nil))
|
|
if recorder.Code != http.StatusOK || recorder.Body.String() != string(officialBytes) {
|
|
t.Fatalf("official GET status=%d body=%q", recorder.Code, recorder.Body.String())
|
|
}
|
|
if recorder.Header().Get("ETag") != `"blake3-official-b3"` ||
|
|
recorder.Result().ContentLength != int64(len(officialBytes)) {
|
|
t.Fatalf("official headers etag=%q length=%d", recorder.Header().Get("ETag"), recorder.Result().ContentLength)
|
|
}
|
|
if len(backend.distributionParams) != 3 {
|
|
t.Fatalf("backend calls=%d want=3", len(backend.distributionParams))
|
|
}
|
|
for _, params := range backend.distributionParams {
|
|
if params.Destination != "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes" {
|
|
t.Fatalf("backend destination=%q", params.Destination)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (*releaseBackendStub) ReleaseCleanup(context.Context, backendrpc.ReleaseCleanupParams) (*backendrpc.ReleaseCleanupReport, error) {
|
|
return &backendrpc.ReleaseCleanupReport{PlanID: "plan-1"}, nil
|
|
}
|
|
|
|
func TestReleaseHTTPForwardsTypedSelectionAndDoesNotFallback(t *testing.T) {
|
|
cfg := DefaultConfig()
|
|
if err := cfg.Normalize(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
backend := &releaseBackendStub{fakeBackend: &fakeBackend{}, root: fixtureRoot(t), available: true}
|
|
server := NewServer(cfg, backend, nil)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/v1/releases?channel=localized", nil))
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("release list status=%d body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
|
|
recorder = httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/v1/distribution?channel=localized&release_id=localized-1&destination=TableBundles%2FTableCatalog.bytes", nil))
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("distribution status=%d body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
if len(backend.distributionParams) != 1 ||
|
|
backend.distributionParams[0].Channel != "localized" ||
|
|
backend.distributionParams[0].ReleaseID != "localized-1" ||
|
|
backend.distributionParams[0].Destination != "TableBundles/TableCatalog.bytes" ||
|
|
backend.distributionParams[0].Offset != 0 ||
|
|
backend.distributionParams[0].Limit != 0 {
|
|
t.Fatalf("distribution params=%#v", backend.distributionParams)
|
|
}
|
|
|
|
recorder = httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes?channel=localized&release_id=localized-1", nil))
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("localized CDN status=%d body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
if recorder.Body.String() != "TABLE_CATALOG_FIXTURE" {
|
|
t.Fatalf("localized CDN body=%q", recorder.Body.String())
|
|
}
|
|
|
|
cfg.RequireIndexed = false
|
|
unindexedServer := NewServer(cfg, backend, nil)
|
|
recorder = httptest.NewRecorder()
|
|
unindexedServer.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/not-listed.bytes?channel=localized&release_id=localized-1", nil))
|
|
if recorder.Code != http.StatusNotFound {
|
|
t.Fatalf("unlisted localized CDN status=%d body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
|
|
blocked := &releaseBackendStub{fakeBackend: &fakeBackend{}, root: fixtureRoot(t), available: false}
|
|
blockedServer := NewServer(cfg, blocked, nil)
|
|
recorder = httptest.NewRecorder()
|
|
blockedServer.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes?channel=localized", nil))
|
|
if recorder.Code != http.StatusConflict {
|
|
t.Fatalf("blocked localized CDN status=%d body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAdminReleaseCleanupRequiresAuthAndForwards(t *testing.T) {
|
|
cfg := DefaultConfig()
|
|
cfg.AuthToken = "control-token"
|
|
if err := cfg.Normalize(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
backend := &controlBackend{fakeBackend: &fakeBackend{}}
|
|
server := NewServer(cfg, backend, nil)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodPost, "/admin/control/release-cleanup", nil))
|
|
if recorder.Code != http.StatusUnauthorized {
|
|
t.Fatalf("unauthenticated cleanup status=%d", recorder.Code)
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/admin/control/release-cleanup", strings.NewReader(`{"execute":false}`))
|
|
request.Header.Set("Authorization", "Bearer control-token")
|
|
recorder = httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(recorder, request)
|
|
if recorder.Code != http.StatusAccepted {
|
|
t.Fatalf("dry-run cleanup status=%d body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
if len(backend.calls) != 1 || backend.calls[0] != "release.cleanup" {
|
|
t.Fatalf("calls=%v", backend.calls)
|
|
}
|
|
|
|
request = httptest.NewRequest(http.MethodPost, "/admin/control/release-cleanup", strings.NewReader(`{"execute":true}`))
|
|
request.Header.Set("Authorization", "Bearer control-token")
|
|
recorder = httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(recorder, request)
|
|
if recorder.Code != http.StatusBadRequest {
|
|
t.Fatalf("missing plan cleanup status=%d body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
}
|