mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-23 10:44:16 +00:00
server.test_mode survived the build-tag refactor only to feed five behavioral branches: the registry's fall-back to the default hold when the user's hold is unreachable, backfill warning suppression for external holds, the appview listener close on shutdown, the hold's relay-crawl skip, and the hold's appview-issuer tolerance. Every one of them is a "this is a local development build" decision, which is what the tag already says, and local development has to build with the tag or nothing resolves. So they read atproto.TestModeBuild now, and the flag, SetTestMode, IsTestMode, the middleware option, the backfill constructor parameter, the never-read field on RemoteHoldAuthorizer, the example and template YAML lines, and the docker-compose env vars are gone. The registry keeps the fallback as a field seeded from the constant so the production-path tests can pin it off under the tag. The 24 SetTestMode calls in tests were dead already: stripping them and running the affected packages tagged changed nothing. Tests that resolve a loopback did:web used to t.Fatal naming the tag, which left a bare `go test ./...` permanently red in five packages. They now live under `//go:build testmode`: whole-file constraints where every test needs it, and sibling *_testmode_test.go files holding the moved tests plus their fixtures where a file mixed. The harness carries the constraint too, with its package doc in an untagged doc.go so the package still exists without it. An untagged run compiles those tests out and passes; make test keeps the tag and runs everything. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwYzaG3Yy7uA8FbZ5qk3tQ
236 lines
7.2 KiB
Go
236 lines
7.2 KiB
Go
package authgate
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"atcr.io/pkg/auth"
|
|
)
|
|
|
|
func TestHasNonWildcardPushScope(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
access []auth.AccessEntry
|
|
want bool
|
|
}{
|
|
{
|
|
name: "empty",
|
|
access: nil,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "pull only",
|
|
access: []auth.AccessEntry{
|
|
{Type: "repository", Name: "alice/myapp", Actions: []string{"pull"}},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "specific repo with push",
|
|
access: []auth.AccessEntry{
|
|
{Type: "repository", Name: "alice/myapp", Actions: []string{"pull", "push"}},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "wildcard repo with push is bypassed",
|
|
access: []auth.AccessEntry{
|
|
{Type: "repository", Name: "*", Actions: []string{"pull", "push"}},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "wildcard plus specific push",
|
|
access: []auth.AccessEntry{
|
|
{Type: "repository", Name: "*", Actions: []string{"pull", "push"}},
|
|
{Type: "repository", Name: "alice/myapp", Actions: []string{"push"}},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "non-repository class ignored",
|
|
access: []auth.AccessEntry{
|
|
{Type: "registry", Name: "catalog", Actions: []string{"push"}},
|
|
},
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := hasNonWildcardPushScope(tc.access); got != tc.want {
|
|
t.Errorf("hasNonWildcardPushScope(%v) = %v, want %v", tc.access, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPermissionsAllowBlobWrite(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
json string
|
|
want bool
|
|
}{
|
|
{name: "empty string", json: "", want: false},
|
|
{name: "null", json: "null", want: false},
|
|
{name: "empty array", json: "[]", want: false},
|
|
{name: "blob:write present", json: `["blob:write"]`, want: true},
|
|
{name: "blob:write among others", json: `["blob:read","blob:write","manifest:write"]`, want: true},
|
|
{name: "only blob:read", json: `["blob:read"]`, want: false},
|
|
{name: "garbage json", json: `not-json`, want: false},
|
|
{name: "object instead of array", json: `{"x":1}`, want: false},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := permissionsAllowBlobWrite(tc.json); got != tc.want {
|
|
t.Errorf("permissionsAllowBlobWrite(%q) = %v, want %v", tc.json, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- isCaptain -------------------------------------------------------------
|
|
|
|
func TestIsCaptain_Match(t *testing.T) {
|
|
d := newTestDB(t)
|
|
seedCaptain(t, d, "did:plc:hold1", "did:plc:alice")
|
|
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "")
|
|
got, err := a.isCaptain(context.Background(), "did:plc:alice", "did:plc:hold1")
|
|
if err != nil {
|
|
t.Fatalf("isCaptain: %v", err)
|
|
}
|
|
if !got {
|
|
t.Error("isCaptain(alice, hold1) = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestIsCaptain_NonOwnerNotMistakenForCaptain(t *testing.T) {
|
|
// A captain row exists for the hold but names someone else as owner.
|
|
// We must not fall through to "isCrew → captain" — captaincy is the
|
|
// owner check specifically.
|
|
d := newTestDB(t)
|
|
seedCaptain(t, d, "did:plc:hold1", "did:plc:alice")
|
|
seedCrewMember(t, d, "did:plc:hold1", "did:plc:bob", `["blob:write"]`)
|
|
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "")
|
|
got, err := a.isCaptain(context.Background(), "did:plc:bob", "did:plc:hold1")
|
|
if err != nil {
|
|
t.Fatalf("isCaptain: %v", err)
|
|
}
|
|
if got {
|
|
t.Error("isCaptain(bob, hold1) = true, want false (bob is crew, not owner)")
|
|
}
|
|
}
|
|
|
|
func TestIsCaptain_NoCaptainRecord(t *testing.T) {
|
|
d := newTestDB(t)
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "")
|
|
|
|
got, err := a.isCaptain(context.Background(), "did:plc:alice", "did:plc:unknownhold")
|
|
if err != nil {
|
|
t.Fatalf("isCaptain: %v", err)
|
|
}
|
|
if got {
|
|
t.Error("isCaptain on missing hold should be false, not error")
|
|
}
|
|
}
|
|
|
|
func TestIsCaptain_DBError(t *testing.T) {
|
|
d := newTestDB(t)
|
|
_ = d.Close()
|
|
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "")
|
|
_, err := a.isCaptain(context.Background(), "did:plc:alice", "did:plc:hold1")
|
|
if err == nil {
|
|
t.Fatal("expected error from closed DB")
|
|
}
|
|
if !strings.Contains(err.Error(), "look up hold captain") {
|
|
t.Errorf("error %q should mention 'look up hold captain'", err)
|
|
}
|
|
}
|
|
|
|
// --- checkCrewBlobWrite ----------------------------------------------------
|
|
|
|
func TestCheckCrewBlobWrite_HasWrite(t *testing.T) {
|
|
d := newTestDB(t)
|
|
seedCrewMember(t, d, "did:plc:hold1", "did:plc:alice", `["blob:write"]`)
|
|
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "")
|
|
if err := a.checkCrewBlobWrite(context.Background(), "did:plc:alice", "did:plc:hold1"); err != nil {
|
|
t.Errorf("checkCrewBlobWrite = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckCrewBlobWrite_OnlyRead(t *testing.T) {
|
|
d := newTestDB(t)
|
|
seedCrewMember(t, d, "did:plc:hold1", "did:plc:alice", `["blob:read"]`)
|
|
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "")
|
|
err := a.checkCrewBlobWrite(context.Background(), "did:plc:alice", "did:plc:hold1")
|
|
if err == nil || !strings.Contains(err.Error(), "lacks blob:write") {
|
|
t.Errorf("expected 'lacks blob:write' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckCrewBlobWrite_NotAMember(t *testing.T) {
|
|
d := newTestDB(t)
|
|
// Crew table populated for someone else.
|
|
seedCrewMember(t, d, "did:plc:hold1", "did:plc:bob", `["blob:write"]`)
|
|
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "")
|
|
err := a.checkCrewBlobWrite(context.Background(), "did:plc:alice", "did:plc:hold1")
|
|
if err == nil || !strings.Contains(err.Error(), "crew membership required") {
|
|
t.Errorf("expected 'crew membership required' error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckCrewBlobWrite_NullPermissions(t *testing.T) {
|
|
d := newTestDB(t)
|
|
// Empty Permissions string is written as NULL by BatchUpsertCrewMembers.
|
|
seedCrewMember(t, d, "did:plc:hold1", "did:plc:alice", "")
|
|
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "")
|
|
err := a.checkCrewBlobWrite(context.Background(), "did:plc:alice", "did:plc:hold1")
|
|
if err == nil || !strings.Contains(err.Error(), "lacks blob:write") {
|
|
t.Errorf("expected 'lacks blob:write' for NULL permissions, got %v", err)
|
|
}
|
|
}
|
|
|
|
// --- checkQuota ------------------------------------------------------------
|
|
|
|
func TestCheckQuota_HoldURLResolutionFailsOpen(t *testing.T) {
|
|
// A "did:" prefixed but otherwise malformed identifier makes
|
|
// ResolveHoldURL → ResolveHoldDIDToURL → syntax.ParseDID error out
|
|
// synchronously (no network call). The contract is fail-open so push
|
|
// isn't blocked on resolver issues.
|
|
a := New(newTestDB(t), fakeHoldAuthorizer{}, nil, "")
|
|
if err := a.checkQuota(context.Background(), "did:plc:alice", "did:bogusmethod:no-host"); err != nil {
|
|
t.Errorf("checkQuota with bad hold DID should fail open, got %v", err)
|
|
}
|
|
}
|
|
|
|
// --- Authorize orchestration ----------------------------------------------
|
|
|
|
func pushAccess(name string) []auth.AccessEntry {
|
|
return []auth.AccessEntry{{Type: "repository", Name: name, Actions: []string{"pull", "push"}}}
|
|
}
|
|
|
|
func pullAccess(name string) []auth.AccessEntry {
|
|
return []auth.AccessEntry{{Type: "repository", Name: name, Actions: []string{"pull"}}}
|
|
}
|
|
|
|
func TestAuthorize_NoHoldAllowsAll(t *testing.T) {
|
|
d := newTestDB(t)
|
|
seedUser(t, d, "did:plc:alice", "alice.test", "")
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "")
|
|
|
|
for _, scope := range [][]auth.AccessEntry{nil, pullAccess("alice/x"), pushAccess("alice/x")} {
|
|
if err := a.Authorize(context.Background(), "did:plc:alice", "", scope); err != nil {
|
|
t.Errorf("Authorize(%v) with no hold = %v, want nil", scope, err)
|
|
}
|
|
}
|
|
}
|