2026-03-19 21:06:02 +00:00
|
|
|
package database
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"database/sql"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func setupMetadataCacheDB(t *testing.T) *DB {
|
|
|
|
|
t.Helper()
|
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
t.Fatalf("MigrateSchema failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
|
return db
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUpsertAndGetMetadataCache(t *testing.T) {
|
|
|
|
|
db := setupMetadataCacheDB(t)
|
|
|
|
|
|
|
|
|
|
entry := &MetadataCacheEntry{
|
|
|
|
|
Ecosystem: testEcosystemNPM,
|
|
|
|
|
Name: "lodash",
|
|
|
|
|
StoragePath: "_metadata/npm/lodash/metadata",
|
|
|
|
|
ETag: sql.NullString{String: `"abc123"`, Valid: true},
|
2026-09-02 17:10:35 +05:30
|
|
|
Link: sql.NullString{String: `<https://registry.example.test/next>; rel="next"`, Valid: true},
|
2026-03-19 21:06:02 +00:00
|
|
|
ContentType: sql.NullString{String: "application/json", Valid: true},
|
2026-08-13 07:35:07 +01:00
|
|
|
ContentDigest: sql.NullString{
|
|
|
|
|
String: "sha256:0123456789abcdef",
|
|
|
|
|
Valid: true,
|
|
|
|
|
},
|
|
|
|
|
Size: sql.NullInt64{Int64: 1024, Valid: true},
|
|
|
|
|
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
2026-03-19 21:06:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := db.UpsertMetadataCache(entry)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("UpsertMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
got, err := db.GetMetadataCache(testEcosystemNPM, "lodash")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got == nil {
|
|
|
|
|
t.Fatal("GetMetadataCache() returned nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if got.Ecosystem != testEcosystemNPM {
|
|
|
|
|
t.Errorf("ecosystem = %q, want %q", got.Ecosystem, testEcosystemNPM)
|
|
|
|
|
}
|
|
|
|
|
if got.Name != "lodash" {
|
|
|
|
|
t.Errorf("name = %q, want %q", got.Name, "lodash")
|
|
|
|
|
}
|
|
|
|
|
if got.StoragePath != "_metadata/npm/lodash/metadata" {
|
|
|
|
|
t.Errorf("storage_path = %q, want %q", got.StoragePath, "_metadata/npm/lodash/metadata")
|
|
|
|
|
}
|
|
|
|
|
if !got.ETag.Valid || got.ETag.String != `"abc123"` {
|
|
|
|
|
t.Errorf("etag = %v, want %q", got.ETag, `"abc123"`)
|
|
|
|
|
}
|
2026-09-02 17:10:35 +05:30
|
|
|
if !got.Link.Valid || got.Link.String != `<https://registry.example.test/next>; rel="next"` {
|
|
|
|
|
t.Errorf("link = %v, want next link", got.Link)
|
|
|
|
|
}
|
2026-03-19 21:06:02 +00:00
|
|
|
if !got.ContentType.Valid || got.ContentType.String != "application/json" {
|
|
|
|
|
t.Errorf("content_type = %v, want %q", got.ContentType, "application/json")
|
|
|
|
|
}
|
2026-08-13 07:35:07 +01:00
|
|
|
if !got.ContentDigest.Valid || got.ContentDigest.String != "sha256:0123456789abcdef" {
|
|
|
|
|
t.Errorf("content_digest = %v, want %q", got.ContentDigest, "sha256:0123456789abcdef")
|
|
|
|
|
}
|
2026-03-19 21:06:02 +00:00
|
|
|
if !got.Size.Valid || got.Size.Int64 != 1024 {
|
|
|
|
|
t.Errorf("size = %v, want 1024", got.Size)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGetMetadataCacheMiss(t *testing.T) {
|
|
|
|
|
db := setupMetadataCacheDB(t)
|
|
|
|
|
|
|
|
|
|
got, err := db.GetMetadataCache(testEcosystemNPM, "nonexistent")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got != nil {
|
|
|
|
|
t.Errorf("expected nil for cache miss, got %v", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUpsertMetadataCacheOverwrite(t *testing.T) {
|
|
|
|
|
db := setupMetadataCacheDB(t)
|
|
|
|
|
|
|
|
|
|
// First insert
|
|
|
|
|
entry1 := &MetadataCacheEntry{
|
|
|
|
|
Ecosystem: testEcosystemNPM,
|
|
|
|
|
Name: "lodash",
|
|
|
|
|
StoragePath: "_metadata/npm/lodash/metadata",
|
|
|
|
|
ETag: sql.NullString{String: `"v1"`, Valid: true},
|
|
|
|
|
ContentType: sql.NullString{String: "application/json", Valid: true},
|
|
|
|
|
Size: sql.NullInt64{Int64: 100, Valid: true},
|
|
|
|
|
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertMetadataCache(entry1); err != nil {
|
|
|
|
|
t.Fatalf("first UpsertMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Second insert (same ecosystem+name, different etag and size)
|
|
|
|
|
entry2 := &MetadataCacheEntry{
|
|
|
|
|
Ecosystem: testEcosystemNPM,
|
|
|
|
|
Name: "lodash",
|
|
|
|
|
StoragePath: "_metadata/npm/lodash/metadata",
|
|
|
|
|
ETag: sql.NullString{String: `"v2"`, Valid: true},
|
|
|
|
|
ContentType: sql.NullString{String: "application/json", Valid: true},
|
|
|
|
|
Size: sql.NullInt64{Int64: 200, Valid: true},
|
|
|
|
|
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertMetadataCache(entry2); err != nil {
|
|
|
|
|
t.Fatalf("second UpsertMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
got, err := db.GetMetadataCache(testEcosystemNPM, "lodash")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got == nil {
|
|
|
|
|
t.Fatal("expected entry after overwrite")
|
|
|
|
|
}
|
|
|
|
|
if got.ETag.String != `"v2"` {
|
|
|
|
|
t.Errorf("etag = %q, want %q", got.ETag.String, `"v2"`)
|
|
|
|
|
}
|
|
|
|
|
if got.Size.Int64 != 200 {
|
|
|
|
|
t.Errorf("size = %d, want 200", got.Size.Int64)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUpsertMetadataCacheNullableFields(t *testing.T) {
|
|
|
|
|
db := setupMetadataCacheDB(t)
|
|
|
|
|
|
|
|
|
|
entry := &MetadataCacheEntry{
|
|
|
|
|
Ecosystem: "pypi",
|
|
|
|
|
Name: "requests",
|
|
|
|
|
StoragePath: "_metadata/pypi/requests/metadata",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := db.UpsertMetadataCache(entry); err != nil {
|
|
|
|
|
t.Fatalf("UpsertMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
got, err := db.GetMetadataCache("pypi", "requests")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got == nil {
|
|
|
|
|
t.Fatal("expected entry")
|
|
|
|
|
}
|
|
|
|
|
if got.ETag.Valid {
|
|
|
|
|
t.Error("expected null etag")
|
|
|
|
|
}
|
|
|
|
|
if got.ContentType.Valid {
|
|
|
|
|
t.Error("expected null content_type")
|
|
|
|
|
}
|
2026-09-02 17:10:35 +05:30
|
|
|
if got.Link.Valid {
|
|
|
|
|
t.Error("expected null link")
|
|
|
|
|
}
|
2026-03-19 21:06:02 +00:00
|
|
|
if got.Size.Valid {
|
|
|
|
|
t.Error("expected null size")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMetadataCacheTableCreatedByMigration(t *testing.T) {
|
|
|
|
|
// Create a DB without the metadata_cache table, then migrate
|
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
// MigrateSchema should create the metadata_cache table
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
t.Fatalf("MigrateSchema() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
has, err := db.HasTable("metadata_cache")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("HasTable() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !has {
|
|
|
|
|
t.Error("metadata_cache table should exist after migration")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-13 07:35:07 +01:00
|
|
|
|
|
|
|
|
func TestMetadataCacheContentDigestMigrationPreservesExistingRows(t *testing.T) {
|
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
if _, err := db.Exec("ALTER TABLE metadata_cache DROP COLUMN content_digest"); err != nil {
|
|
|
|
|
t.Fatalf("dropping content_digest: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := db.Exec("DELETE FROM migrations WHERE name = ?", "006_add_metadata_content_digest"); err != nil {
|
|
|
|
|
t.Fatalf("resetting digest migration: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := db.Exec(`
|
|
|
|
|
INSERT INTO metadata_cache (ecosystem, name, storage_path, content_type, size, fetched_at, created_at, updated_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
`, "oci-manifest", "cache-key", "_metadata/oci-manifest/cache-key/metadata", "application/json", 2, time.Now(), time.Now(), time.Now()); err != nil {
|
|
|
|
|
t.Fatalf("inserting legacy cache row: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
t.Fatalf("MigrateSchema() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
hasDigest, err := db.HasColumn("metadata_cache", "content_digest")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("HasColumn() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !hasDigest {
|
|
|
|
|
t.Fatal("metadata_cache.content_digest was not added")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entry, err := db.GetMetadataCache("oci-manifest", "cache-key")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if entry == nil || entry.StoragePath != "_metadata/oci-manifest/cache-key/metadata" {
|
|
|
|
|
t.Fatalf("existing metadata cache row was not preserved: %#v", entry)
|
|
|
|
|
}
|
|
|
|
|
if entry.ContentDigest.Valid {
|
|
|
|
|
t.Errorf("legacy content digest = %q, want NULL", entry.ContentDigest.String)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-02 17:10:35 +05:30
|
|
|
|
|
|
|
|
func TestMetadataCacheLinkMigrationPreservesExistingRows(t *testing.T) {
|
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
if _, err := db.Exec("ALTER TABLE metadata_cache DROP COLUMN link"); err != nil {
|
|
|
|
|
t.Fatalf("dropping link: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := db.Exec("DELETE FROM migrations WHERE name = ?", "007_add_metadata_link"); err != nil {
|
|
|
|
|
t.Fatalf("resetting link migration: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := db.Exec(`
|
|
|
|
|
INSERT INTO metadata_cache (ecosystem, name, storage_path, content_type, size, fetched_at, created_at, updated_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
`, "oci-tags", "cache-key", "_metadata/oci-tags/cache-key/metadata", "application/json", 2, time.Now(), time.Now(), time.Now()); err != nil {
|
|
|
|
|
t.Fatalf("inserting legacy cache row: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
t.Fatalf("MigrateSchema() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
hasLink, err := db.HasColumn("metadata_cache", "link")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("HasColumn() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !hasLink {
|
|
|
|
|
t.Fatal("metadata_cache.link was not added")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entry, err := db.GetMetadataCache("oci-tags", "cache-key")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if entry == nil || entry.StoragePath != "_metadata/oci-tags/cache-key/metadata" {
|
|
|
|
|
t.Fatalf("existing metadata cache row was not preserved: %#v", entry)
|
|
|
|
|
}
|
|
|
|
|
if entry.Link.Valid {
|
|
|
|
|
t.Errorf("legacy link = %q, want NULL", entry.Link.String)
|
|
|
|
|
}
|
|
|
|
|
}
|
fix(handler): preserve upstream content-encoding for cached metadata (#304)
* fix(handler): preserve upstream content-encoding for cached metadata
Signed and hash-pinned index files (debian Release/Packages.gz, rpm
repomd.xml, helm index.yaml, conda repodata.json, apk APKINDEX.tar.gz)
were cached after Go's transport transparently decompressed any
Content-Encoding: gzip response, so the proxy served bytes that differ
from what the upstream signed and broke client verification.
Request the identity encoding on the shared metadata fetch so Go no
longer auto-decompresses, and persist the upstream Content-Encoding in
a new metadata_cache column so both the cached and offline responses
replay the exact bytes and header. The uncached streaming path now
forwards Content-Encoding too.
Fixes #300
* fix(handler): scope verbatim metadata fetch to the ProxyCached path
The first cut forced Accept-Encoding: identity in the shared
fetchUpstreamMetadata, which an adversarial review showed both missed
the bug and regressed unrelated ecosystems:
- proxyMetadataStream (the default path, since cache_metadata is off)
never forced identity, so a client sending no Accept-Encoding still
triggered Go's transparent gzip decompression and served altered bytes
for apk/debian/rpm/conda indexes.
- Direct FetchOrCacheMetadata callers that parse or rewrite the body
(npm, pypi, cargo, composer, pub, nuget, swift, maven, helm) were
forced to identity too, losing wire compression and 502-ing against
upstreams that ignore identity and gzip anyway (helm rewriteIndex).
- Rows cached before the fix kept serving decompressed bytes via ETag
304 revalidation.
Scope the verbatim behavior to the ProxyCached code path, which serves
upstream bytes through unchanged (apk, debian, rpm, go, hex, conda,
cran, gem, conan, julia). That path now requests identity on both the
cached fetch and the uncached stream branch and replays Content-Encoding;
direct callers keep transparent compression, matching main. Migration
008 clears etag/fetched_at so legacy rows refetch once with identity.
Tests now exercise the stream path with no client Accept-Encoding and
pin that direct callers are not forced to identity.
2026-09-03 10:58:58 +02:00
|
|
|
|
|
|
|
|
func TestMetadataCacheContentEncodingMigrationPreservesExistingRows(t *testing.T) {
|
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
if _, err := db.Exec("ALTER TABLE metadata_cache DROP COLUMN content_encoding"); err != nil {
|
|
|
|
|
t.Fatalf("dropping content_encoding: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := db.Exec("DELETE FROM migrations WHERE name = ?", "008_add_metadata_content_encoding"); err != nil {
|
|
|
|
|
t.Fatalf("resetting content_encoding migration: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := db.Exec(`
|
|
|
|
|
INSERT INTO metadata_cache (ecosystem, name, storage_path, etag, content_type, size, fetched_at, created_at, updated_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
`, "apk", "cache-key", "_metadata/apk/cache-key/metadata", "\"legacy-etag\"", "application/octet-stream", 2, time.Now(), time.Now(), time.Now()); err != nil {
|
|
|
|
|
t.Fatalf("inserting legacy cache row: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := db.MigrateSchema(); err != nil {
|
|
|
|
|
t.Fatalf("MigrateSchema() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
hasEncoding, err := db.HasColumn("metadata_cache", "content_encoding")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("HasColumn() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !hasEncoding {
|
|
|
|
|
t.Fatal("metadata_cache.content_encoding was not added")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entry, err := db.GetMetadataCache("apk", "cache-key")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if entry == nil || entry.StoragePath != "_metadata/apk/cache-key/metadata" {
|
|
|
|
|
t.Fatalf("existing metadata cache row was not preserved: %#v", entry)
|
|
|
|
|
}
|
|
|
|
|
if entry.ContentEncoding.Valid {
|
|
|
|
|
t.Errorf("legacy content encoding = %q, want NULL", entry.ContentEncoding.String)
|
|
|
|
|
}
|
|
|
|
|
// The migration must clear the pre-fix validators so the row is refetched
|
|
|
|
|
// once with identity instead of a 304 re-serving the decompressed copy.
|
|
|
|
|
if entry.ETag.Valid {
|
|
|
|
|
t.Errorf("legacy etag = %q, want cleared", entry.ETag.String)
|
|
|
|
|
}
|
|
|
|
|
if entry.FetchedAt.Valid {
|
|
|
|
|
t.Errorf("legacy fetched_at = %v, want cleared", entry.FetchedAt.Time)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMetadataCacheRoundTripsContentEncoding(t *testing.T) {
|
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "test.db")
|
|
|
|
|
db, err := Create(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Create failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = db.Close() }()
|
|
|
|
|
|
|
|
|
|
entry := &MetadataCacheEntry{
|
|
|
|
|
Ecosystem: "apk",
|
|
|
|
|
Name: "index-key",
|
|
|
|
|
StoragePath: "_metadata/apk/index-key/metadata",
|
|
|
|
|
ContentType: sql.NullString{String: "application/octet-stream", Valid: true},
|
|
|
|
|
ContentEncoding: sql.NullString{String: "gzip", Valid: true},
|
|
|
|
|
Size: sql.NullInt64{Int64: 10, Valid: true},
|
|
|
|
|
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertMetadataCache(entry); err != nil {
|
|
|
|
|
t.Fatalf("UpsertMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
got, err := db.GetMetadataCache("apk", "index-key")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got == nil || !got.ContentEncoding.Valid || got.ContentEncoding.String != "gzip" {
|
|
|
|
|
t.Fatalf("content encoding round-trip failed: %#v", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-12 09:53:53 +02:00
|
|
|
|
|
|
|
|
// On Postgres, fetched_at is a TIMESTAMP column without a zone. East of UTC
|
|
|
|
|
// it must still read back as the moment it was written, or metadata_ttl
|
|
|
|
|
// expires hours late. last_modified is an HTTP date in UTC and must keep it.
|
|
|
|
|
func TestMetadataCacheTimesOffUTC(t *testing.T) {
|
|
|
|
|
tokyo, err := time.LoadLocation("Asia/Tokyo")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Skipf("time zone data unavailable: %v", err)
|
|
|
|
|
}
|
|
|
|
|
prev := time.Local
|
|
|
|
|
time.Local = tokyo
|
|
|
|
|
t.Cleanup(func() { time.Local = prev })
|
|
|
|
|
|
|
|
|
|
runWithBothDatabases(t, func(t *testing.T, db *DB) {
|
|
|
|
|
fetched := time.Now().Add(-90 * time.Minute)
|
|
|
|
|
modified := time.Date(2026, 10, 1, 12, 0, 0, 0, time.UTC)
|
|
|
|
|
entry := &MetadataCacheEntry{
|
|
|
|
|
Ecosystem: testEcosystemNPM,
|
|
|
|
|
Name: "tz",
|
|
|
|
|
StoragePath: "_metadata/npm/tz/metadata",
|
|
|
|
|
LastModified: sql.NullTime{Time: modified, Valid: true},
|
|
|
|
|
FetchedAt: sql.NullTime{Time: fetched, Valid: true},
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpsertMetadataCache(entry); err != nil {
|
|
|
|
|
t.Fatalf("UpsertMetadataCache() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
got, err := db.GetMetadataCache(testEcosystemNPM, "tz")
|
|
|
|
|
if err != nil || got == nil {
|
|
|
|
|
t.Fatalf("GetMetadataCache() = %v, %v", got, err)
|
|
|
|
|
}
|
|
|
|
|
if diff := got.FetchedAt.Time.Sub(fetched).Abs(); diff > time.Second {
|
|
|
|
|
t.Errorf("fetched_at read back as %v, want %v", got.FetchedAt.Time, fetched)
|
|
|
|
|
}
|
|
|
|
|
if !got.LastModified.Time.Equal(modified) {
|
|
|
|
|
t.Errorf("last_modified read back as %v, want %v", got.LastModified.Time, modified)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|