mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 12:17:00 +00:00
485 lines
17 KiB
Go
485 lines
17 KiB
Go
package authgate
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"atcr.io/pkg/atproto"
|
|
"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_UnderLimit(t *testing.T) {
|
|
atproto.SetTestMode(true)
|
|
t.Cleanup(func() { atproto.SetTestMode(false) })
|
|
|
|
srv := quotaServer(t, 200, `{"totalSize":100,"limit":1000}`)
|
|
a := New(newTestDB(t), fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
|
|
|
if err := a.checkQuota(context.Background(), "did:plc:alice", srv.holdDID); err != nil {
|
|
t.Errorf("checkQuota under limit = %v, want nil", err)
|
|
}
|
|
if srv.hits != 1 {
|
|
t.Errorf("expected 1 hit on quota endpoint, got %d", srv.hits)
|
|
}
|
|
// The query value is percent-encoded by the client (see
|
|
// TestCheckQuota_EncodesUserDID), so plain "did:plc:alice" becomes
|
|
// "did%3Aplc%3Aalice" on the wire.
|
|
if !strings.Contains(srv.lastURL, "userDid=did%3Aplc%3Aalice") {
|
|
t.Errorf("expected userDid query param, got URL %q", srv.lastURL)
|
|
}
|
|
if !strings.Contains(srv.lastURL, atproto.HoldGetQuota) {
|
|
t.Errorf("expected URL path to contain %q, got %q", atproto.HoldGetQuota, srv.lastURL)
|
|
}
|
|
}
|
|
|
|
func TestCheckQuota_OverLimit(t *testing.T) {
|
|
atproto.SetTestMode(true)
|
|
t.Cleanup(func() { atproto.SetTestMode(false) })
|
|
|
|
// 5 GiB exactly so the formatted message shows "5.00 GB / 5.00 GB".
|
|
srv := quotaServer(t, 200, `{"totalSize":5368709120,"limit":5368709120}`)
|
|
d := newTestDB(t)
|
|
seedUser(t, d, "did:plc:alice", "alice.bsky.social", "")
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
|
|
|
err := a.checkQuota(context.Background(), "did:plc:alice", srv.holdDID)
|
|
if err == nil {
|
|
t.Fatal("checkQuota at limit should deny")
|
|
}
|
|
msg := err.Error()
|
|
for _, want := range []string{"quota exceeded", "5.00 GB", "did:plc:alice", "alice.bsky.social"} {
|
|
if !strings.Contains(msg, want) {
|
|
t.Errorf("expected %q in error %q", want, msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// When no users row exists for the DID (handle unknown), the error still
|
|
// formats correctly with the bare DID.
|
|
func TestCheckQuota_OverLimit_NoHandle(t *testing.T) {
|
|
atproto.SetTestMode(true)
|
|
t.Cleanup(func() { atproto.SetTestMode(false) })
|
|
|
|
srv := quotaServer(t, 200, `{"totalSize":5368709120,"limit":5368709120}`)
|
|
a := New(newTestDB(t), fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
|
|
|
err := a.checkQuota(context.Background(), "did:plc:bob", srv.holdDID)
|
|
if err == nil {
|
|
t.Fatal("checkQuota at limit should deny")
|
|
}
|
|
msg := err.Error()
|
|
if !strings.Contains(msg, "did:plc:bob") || strings.Contains(msg, "(did:") {
|
|
t.Errorf("expected bare DID (no parenthesized form) in error %q", msg)
|
|
}
|
|
}
|
|
|
|
func TestCheckQuota_NilLimitAllows(t *testing.T) {
|
|
// A user on the unlimited tier has limit == nil. Even huge totalSize
|
|
// must not deny.
|
|
atproto.SetTestMode(true)
|
|
t.Cleanup(func() { atproto.SetTestMode(false) })
|
|
|
|
srv := quotaServer(t, 200, `{"totalSize":99999999}`)
|
|
a := New(newTestDB(t), fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
|
|
|
if err := a.checkQuota(context.Background(), "did:plc:alice", srv.holdDID); err != nil {
|
|
t.Errorf("checkQuota with nil limit = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckQuota_500FailsOpen(t *testing.T) {
|
|
atproto.SetTestMode(true)
|
|
t.Cleanup(func() { atproto.SetTestMode(false) })
|
|
|
|
srv := quotaServer(t, 500, `oops`)
|
|
a := New(newTestDB(t), fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
|
|
|
if err := a.checkQuota(context.Background(), "did:plc:alice", srv.holdDID); err != nil {
|
|
t.Errorf("checkQuota with 500 should fail open, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckQuota_BadJSONFailsOpen(t *testing.T) {
|
|
atproto.SetTestMode(true)
|
|
t.Cleanup(func() { atproto.SetTestMode(false) })
|
|
|
|
srv := quotaServer(t, 200, `not-json`)
|
|
a := New(newTestDB(t), fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
|
|
|
if err := a.checkQuota(context.Background(), "did:plc:alice", srv.holdDID); err != nil {
|
|
t.Errorf("checkQuota with malformed JSON should fail open, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckQuota_EncodesUserDID(t *testing.T) {
|
|
// did:web DIDs may contain percent-encoded characters (e.g. "%3A" for
|
|
// the port colon). Without proper query encoding the receiving server's
|
|
// query parser decodes "%3A" → ":", mangling the DID and missing the
|
|
// records that were keyed by the original form. The fix encodes the
|
|
// DID once at the client side so the server decodes it back exactly.
|
|
atproto.SetTestMode(true)
|
|
t.Cleanup(func() { atproto.SetTestMode(false) })
|
|
|
|
srv := quotaServer(t, 200, `{"totalSize":100,"limit":1000}`)
|
|
a := New(newTestDB(t), fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
|
|
|
encodedDID := "did:web:127.0.0.1%3A45397:user:alice.test"
|
|
if err := a.checkQuota(context.Background(), encodedDID, srv.holdDID); err != nil {
|
|
t.Errorf("checkQuota with encoded DID = %v, want nil", err)
|
|
}
|
|
// The URL the server saw should contain the double-encoded form, so
|
|
// that its single decode pass yields the original DID back.
|
|
if !strings.Contains(srv.lastURL, "did%3Aweb%3A127.0.0.1%253A45397%3Auser%3Aalice.test") {
|
|
t.Errorf("expected query value to be percent-encoded; got URL %q", srv.lastURL)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAuthorize_CaptainBypassesCrewCheck(t *testing.T) {
|
|
atproto.SetTestMode(true)
|
|
t.Cleanup(func() { atproto.SetTestMode(false) })
|
|
|
|
srv := quotaServer(t, 200, `{"totalSize":1,"limit":1000}`)
|
|
d := newTestDB(t)
|
|
seedUser(t, d, "did:plc:alice", "alice.test", srv.holdDID)
|
|
seedCaptain(t, d, srv.holdDID, "did:plc:alice")
|
|
// A contradictory crew row should NOT trip the gate — captain bypass.
|
|
seedCrewMember(t, d, srv.holdDID, "did:plc:alice", `["blob:read"]`)
|
|
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
|
if err := a.Authorize(context.Background(), "did:plc:alice", "", pushAccess("alice/x")); err != nil {
|
|
t.Errorf("Authorize(captain push) = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthorize_NonCaptainPushWithoutCrewDenied(t *testing.T) {
|
|
atproto.SetTestMode(true)
|
|
t.Cleanup(func() { atproto.SetTestMode(false) })
|
|
|
|
srv := quotaServer(t, 200, `{"totalSize":1,"limit":1000}`)
|
|
d := newTestDB(t)
|
|
seedUser(t, d, "did:plc:bob", "bob.test", srv.holdDID)
|
|
seedCaptain(t, d, srv.holdDID, "did:plc:alice") // alice owns; bob is not crew
|
|
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
|
err := a.Authorize(context.Background(), "did:plc:bob", "", pushAccess("bob/x"))
|
|
if err == nil || !strings.Contains(err.Error(), "crew membership required") {
|
|
t.Errorf("expected 'crew membership required', got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthorize_NonCaptainPushWithoutBlobWriteDenied(t *testing.T) {
|
|
atproto.SetTestMode(true)
|
|
t.Cleanup(func() { atproto.SetTestMode(false) })
|
|
|
|
srv := quotaServer(t, 200, `{"totalSize":1,"limit":1000}`)
|
|
d := newTestDB(t)
|
|
seedUser(t, d, "did:plc:bob", "bob.test", srv.holdDID)
|
|
seedCaptain(t, d, srv.holdDID, "did:plc:alice")
|
|
seedCrewMember(t, d, srv.holdDID, "did:plc:bob", `["blob:read"]`)
|
|
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
|
err := a.Authorize(context.Background(), "did:plc:bob", "", pushAccess("bob/x"))
|
|
if err == nil || !strings.Contains(err.Error(), "lacks blob:write") {
|
|
t.Errorf("expected 'lacks blob:write', got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthorize_NonCaptainPushUnderQuotaAllowed(t *testing.T) {
|
|
atproto.SetTestMode(true)
|
|
t.Cleanup(func() { atproto.SetTestMode(false) })
|
|
|
|
srv := quotaServer(t, 200, `{"totalSize":100,"limit":1000}`)
|
|
d := newTestDB(t)
|
|
seedUser(t, d, "did:plc:bob", "bob.test", srv.holdDID)
|
|
seedCaptain(t, d, srv.holdDID, "did:plc:alice")
|
|
seedCrewMember(t, d, srv.holdDID, "did:plc:bob", `["blob:write"]`)
|
|
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
|
if err := a.Authorize(context.Background(), "did:plc:bob", "", pushAccess("bob/x")); err != nil {
|
|
t.Errorf("Authorize(crew blob:write under quota) = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthorize_NonCaptainPushOverQuotaDenied(t *testing.T) {
|
|
atproto.SetTestMode(true)
|
|
t.Cleanup(func() { atproto.SetTestMode(false) })
|
|
|
|
srv := quotaServer(t, 200, `{"totalSize":1000,"limit":1000}`)
|
|
d := newTestDB(t)
|
|
seedUser(t, d, "did:plc:bob", "bob.test", srv.holdDID)
|
|
seedCaptain(t, d, srv.holdDID, "did:plc:alice")
|
|
seedCrewMember(t, d, srv.holdDID, "did:plc:bob", `["blob:write"]`)
|
|
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
|
err := a.Authorize(context.Background(), "did:plc:bob", "", pushAccess("bob/x"))
|
|
if err == nil || !strings.Contains(err.Error(), "quota exceeded") {
|
|
t.Errorf("expected 'quota exceeded', got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthorize_PullOnlySkipsMembershipAndQuota(t *testing.T) {
|
|
atproto.SetTestMode(true)
|
|
t.Cleanup(func() { atproto.SetTestMode(false) })
|
|
|
|
// Quota server installed but should never be hit: pull bypasses both
|
|
// the membership requirement and the quota call.
|
|
srv := quotaServer(t, 200, `{"totalSize":1000,"limit":1000}`)
|
|
d := newTestDB(t)
|
|
seedUser(t, d, "did:plc:bob", "bob.test", srv.holdDID)
|
|
seedCaptain(t, d, srv.holdDID, "did:plc:alice") // bob is NOT captain, NOT crew
|
|
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
|
if err := a.Authorize(context.Background(), "did:plc:bob", "", pullAccess("alice/x")); err != nil {
|
|
t.Errorf("Authorize(pull only) = %v, want nil", err)
|
|
}
|
|
if srv.hits != 0 {
|
|
t.Errorf("quota endpoint hit %d times for pull-only request, want 0", srv.hits)
|
|
}
|
|
}
|
|
|
|
func TestAuthorize_WildcardPushTreatedAsPull(t *testing.T) {
|
|
atproto.SetTestMode(true)
|
|
t.Cleanup(func() { atproto.SetTestMode(false) })
|
|
|
|
srv := quotaServer(t, 200, `{"totalSize":1000,"limit":1000}`)
|
|
d := newTestDB(t)
|
|
seedUser(t, d, "did:plc:bob", "bob.test", srv.holdDID)
|
|
seedCaptain(t, d, srv.holdDID, "did:plc:alice")
|
|
|
|
a := New(d, fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
|
wildcard := []auth.AccessEntry{{Type: "repository", Name: "*", Actions: []string{"pull", "push"}}}
|
|
if err := a.Authorize(context.Background(), "did:plc:bob", "", wildcard); err != nil {
|
|
t.Errorf("Authorize(wildcard push) = %v, want nil (treated as pull)", err)
|
|
}
|
|
if srv.hits != 0 {
|
|
t.Errorf("quota endpoint hit %d times for wildcard scope, want 0", srv.hits)
|
|
}
|
|
}
|