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()) } }