Files
forge/routers/api/packages/conan/upstream_test.go
zeekayandHanzo Dev 92357a1325 feat(packages): read-through cache for Rust and C++ too
Extends the Go module cache to cargo and conan, so one registry fronts the
ecosystems our builds actually pull from.

CARGO. crates.io does not serve the publish wire format the local parser reads,
so the cache synthesises it (uint32-framed metadata + .crate) and hands it to
the SAME cargo_module.ParsePackage the publish path uses -- a hand-built Package
struct would have duplicated the name/semver/URL/dep-rename validation and could
drift into accepting what upload refuses. It enforces the published sha256
unconditionally: an upstream that will not say what the bytes hash to does not
get cached. Deps are fetched rather than left empty because our sparse index is
built from stored metadata, and an entry claiming 'no dependencies' is a WRONG
answer, worse than a missing one. Yanked state is carried from upstream, so a
lockfile pinned to a yanked crate still resolves.

CONAN. A recipe is many files across recipe and package revisions, so the cache
mirrors a revision's file set and REFUSES to resolve /latest. Pinning whatever
ConanCenter published at the moment of a cold request into a name that promises
immutability would be worse than a miss. Upload and cache were refactored to
share one literal writer (storeFile), not a same-shaped copy.

Known ceilings, stated not hidden: a plain  on a cold
cache still 404s (it fills for lockfiles and explicit revisions); conan v1 and
binary packages are uncached; a cached crate loses a dependency RENAME because
the crates.io API omits explicit_name_in_toml.

Build, vet and tests green across all three caches.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 10:50:11 -07:00

149 lines
5.6 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package conan
import (
"net/http"
"net/http/httptest"
"testing"
conan_module "github.com/hanzoai/git/modules/packages/conan"
"github.com/hanzoai/git/modules/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUpstreamEnabled(t *testing.T) {
defer func(v string) { setting.Packages.ConanUpstream = v }(setting.Packages.ConanUpstream)
// Empty means publish-only -- the behaviour every deployment has until
// someone opts in. Whitespace must not accidentally enable it.
setting.Packages.ConanUpstream = ""
assert.False(t, upstreamEnabled())
setting.Packages.ConanUpstream = " "
assert.False(t, upstreamEnabled())
setting.Packages.ConanUpstream = "https://center.conan.io"
assert.True(t, upstreamEnabled())
}
func TestIsPrivateRecipe(t *testing.T) {
defer func(v string) { setting.Packages.ConanPrivate = v }(setting.Packages.ConanPrivate)
setting.Packages.ConanPrivate = "hanzo-,lux-,luxcpp,zoo-"
// Private names must never reach a public remote -- the name alone leaks an
// internal library that may not be public yet.
assert.True(t, isPrivateRecipe("lux-crypto"))
assert.True(t, isPrivateRecipe("luxcpp"))
assert.True(t, isPrivateRecipe("hanzo-runtime"))
// Public recipes still cache.
assert.False(t, isPrivateRecipe("zlib"))
assert.False(t, isPrivateRecipe("boost"))
// Near-misses must NOT be treated as private (no accidental over-blocking).
assert.False(t, isPrivateRecipe("luxon"))
assert.False(t, isPrivateRecipe("hanzoai"))
assert.False(t, isPrivateRecipe("zoology"))
// Empty config blocks nothing.
setting.Packages.ConanPrivate = ""
assert.False(t, isPrivateRecipe("lux-crypto"))
}
func TestHasConcreteRevision(t *testing.T) {
newRef := func(t *testing.T, revision string) *conan_module.RecipeReference {
t.Helper()
rref, err := conan_module.NewRecipeReference("zlib", "1.3.1", "", "", revision)
require.NoError(t, err)
return rref
}
// Only a real revision names one immutable thing. No revision at all, or the
// "0" sentinel every v1 URL arrives as, is a question -- not something that
// can be fetched without guessing.
assert.False(t, hasConcreteRevision(newRef(t, "")))
assert.False(t, hasConcreteRevision(newRef(t, conan_module.DefaultRevision)))
assert.True(t, hasConcreteRevision(newRef(t, "6a7e1a92e3c5e1d5f3b8b8f6e9d0c1a2")))
}
func TestUpstreamRecipePath(t *testing.T) {
rref, err := conan_module.NewRecipeReference("zlib", "1.3.1", "", "", "abc123")
require.NoError(t, err)
elem, ok := upstreamRecipePath(rref)
require.True(t, ok)
// A recipe with no user/channel is "_/_" upstream, not an empty segment.
assert.Equal(t, "v2/conans/zlib/1.3.1/_/_/revisions/abc123", elem)
rref, err = conan_module.NewRecipeReference("zlib", "1.3.1", "lux", "stable", "abc123")
require.NoError(t, err)
elem, ok = upstreamRecipePath(rref)
require.True(t, ok)
assert.Equal(t, "v2/conans/zlib/1.3.1/lux/stable/revisions/abc123", elem)
// NewRecipeReference only requires the version to be non-empty, so a dot
// segment gets this far. It must never become part of an upstream URL.
rref, err = conan_module.NewRecipeReference("zlib", "..", "", "", "abc123")
require.NoError(t, err)
_, ok = upstreamRecipePath(rref)
assert.False(t, ok)
rref, err = conan_module.NewRecipeReference("zlib", "1.3.1/../../etc", "", "", "abc123")
require.NoError(t, err)
_, ok = upstreamRecipePath(rref)
assert.False(t, ok)
}
func TestUpstreamFetch(t *testing.T) {
defer func(v string) { setting.Packages.ConanUpstream = v }(setting.Packages.ConanUpstream)
var gotPath string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
if r.URL.Path == "/v2/conans/zlib/1.3.1/_/_/revisions/abc123/files/conanfile.py" {
w.Write([]byte("from conan import ConanFile"))
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
// A trailing slash on the configured base must not produce a doubled slash.
setting.Packages.ConanUpstream = srv.URL + "/"
body, _, err := upstreamFetch("v2/conans/zlib/1.3.1/_/_/revisions/abc123/files/conanfile.py")
require.NoError(t, err)
defer body.Close()
assert.Equal(t, "/v2/conans/zlib/1.3.1/_/_/revisions/abc123/files/conanfile.py", gotPath)
// A non-200 is an error, never an empty body silently cached as a recipe.
setting.Packages.ConanUpstream = srv.URL
_, _, err = upstreamFetch("v2/conans/zlib/9.9.9/_/_/revisions/abc123/files/conanfile.py")
require.Error(t, err)
}
func TestUpstreamRecipeFileList(t *testing.T) {
defer func(v string) { setting.Packages.ConanUpstream = v }(setting.Packages.ConanUpstream)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v2/conans/zlib/1.3.1/_/_/revisions/abc123/files" {
w.WriteHeader(http.StatusNotFound)
return
}
w.Write([]byte(`{"files": {"conanfile.py": {}, "conanmanifest.txt": {}, "conan_export.tgz": {}, "evil.sh": {}}}`))
}))
defer srv.Close()
setting.Packages.ConanUpstream = srv.URL
names, err := upstreamRecipeFileList("v2/conans/zlib/1.3.1/_/_/revisions/abc123")
require.NoError(t, err)
// Sorted, and filtered to what the upload path accepts -- the cache can only
// ever hold files this registry would have taken from a publish.
assert.Equal(t, []string{"conan_export.tgz", "conanfile.py", "conanmanifest.txt"}, names)
// An upstream that does not have the revision is an error, so the caller
// falls back to the ordinary not-found.
_, err = upstreamRecipeFileList("v2/conans/zlib/9.9.9/_/_/revisions/abc123")
require.Error(t, err)
}