Files
authz_client/client_test.go
T
argoyleandClaude Opus 5 fc0a4a0e31
authz_client / test (push) Skipped
authz_client / vulnerabilities (push) Skipped
pre-commit / pre-commit (push) Skipped
authz_client / vulnerabilities (pull_request) Successful in 46s
authz_client / test (pull_request) Successful in 59s
pre-commit / pre-commit (pull_request) Successful in 2m39s
test: assert which event type each privilege routing key decodes to
Swapping the handler for Privilege.Removed to PrivilegeAdded passed the suite, and would turn every revocation into a grant. The Setup test now asserts each key's message type, the adapter is exercised for all four events (grant then revoke), and process only accepts the four privilege event types. CLAUDE.md warns against combining Setup() with WithReconnect (per-replica queues are re-created on reconnect, losing revocations sent meanwhile).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SV1epy2pKvBx3yDfhxATk2
2026-09-11 22:57:25 +02:00

688 lines
17 KiB
Go

package client
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"sort"
"sync"
"testing"
goamqp "codeberg.org/messaging/go-messaging-amqp"
spec "codeberg.org/messaging/messaging"
"github.com/stretchr/testify/assert"
)
func TestPrivilegeHandler_Process_InvalidType(t *testing.T) {
handler := New(WithBaseURL("base"))
err := handler.Process("abc")
assert.EqualError(t, err, "unexpected event type: 'string'")
}
func TestPrivilegeHandler_Process_PrivilegeRemoved(t *testing.T) {
handler := New(WithBaseURL("base"))
err := handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeAdmin,
})
assert.NoError(t, err)
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
assert.Equal(t, []string{"abc-123"}, companies)
err = handler.Process(&PrivilegeRemoved{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeAdmin,
})
assert.NoError(t, err)
companies = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
assert.Empty(t, companies)
}
func TestPrivilegeHandler_Process_UserAdded_And_UserRemoved(t *testing.T) {
handler := New(WithBaseURL("base"))
err := handler.Process(&UserAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
})
assert.NoError(t, err)
err = handler.Process(&UserAdded{
Email: "jim@example.org",
CompanyID: "abc-456",
})
assert.NoError(t, err)
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return true
})
sort.Strings(companies)
assert.Equal(t, []string{"abc-123", "abc-456"}, companies)
err = handler.Process(&UserRemoved{
Email: "jim@example.org",
CompanyID: "abc-123",
})
assert.NoError(t, err)
err = handler.Process(&UserRemoved{
Email: "jim@example.org",
CompanyID: "abc-456",
})
assert.NoError(t, err)
companies = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return true
})
assert.Empty(t, companies)
}
func TestPrivilegeHandler_GetCompanies_Email_Not_Found(t *testing.T) {
handler := New(WithBaseURL("base"))
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return true
})
assert.Empty(t, companies)
}
func TestPrivilegeHandler_GetCompanies_No_Companies_Found(t *testing.T) {
handler := New(WithBaseURL("base"))
err := handler.Process(&UserAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
})
assert.NoError(t, err)
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
assert.Empty(t, companies)
companies = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return true
})
assert.Equal(t, []string{"abc-123"}, companies)
err = handler.Process(&UserRemoved{
Email: "jim@example.org",
CompanyID: "abc-123",
})
assert.NoError(t, err)
companies = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return true
})
assert.Empty(t, companies)
}
func TestPrivilegeHandler_GetCompanies_Company_With_Company_Access_Found(t *testing.T) {
handler := New(WithBaseURL("base"))
err := handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeCompany,
})
assert.NoError(t, err)
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Company
})
assert.Equal(t, []string{"abc-123"}, companies)
}
func TestPrivilegeHandler_GetCompanies_Company_With_Admin_Access_Found(t *testing.T) {
handler := New(WithBaseURL("base"))
err := handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeConsumer,
})
assert.NoError(t, err)
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Consumer
})
assert.Equal(t, []string{"abc-123"}, companies)
}
func TestPrivilegeHandler_IsAllowed_Return_False_If_No_Privileges(t *testing.T) {
handler := New(WithBaseURL("base"))
result := handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Company
})
assert.False(t, result)
}
func TestPrivilegeHandler_IsAllowed_Return_True_If_Privilege_Exists(t *testing.T) {
handler := New(WithBaseURL("base"))
_ = handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeTime,
})
result := handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Time
})
assert.True(t, result)
_ = handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeInvoicing,
})
result = handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Invoicing
})
assert.True(t, result)
_ = handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeAccounting,
})
result = handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Accounting
})
assert.True(t, result)
_ = handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeSupplier,
})
result = handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Supplier
})
assert.True(t, result)
_ = handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeSalary,
})
result = handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Salary
})
assert.True(t, result)
}
func TestPrivilegeHandler_Fetch_Sends_Authorization_Header_When_APIKey_Set(t *testing.T) {
var receivedAuth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedAuth = r.Header.Get("Authorization")
_, _ = w.Write([]byte("{}"))
}))
defer server.Close()
handler := New(
WithBaseURL(server.URL),
WithAPIKey("my-secret-key"),
)
err := handler.Fetch()
assert.NoError(t, err)
assert.Equal(t, "Bearer my-secret-key", receivedAuth)
}
func TestPrivilegeHandler_Fetch_No_Authorization_Header_Without_APIKey(t *testing.T) {
var receivedAuth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedAuth = r.Header.Get("Authorization")
_, _ = w.Write([]byte("{}"))
}))
defer server.Close()
handler := New(WithBaseURL(server.URL))
err := handler.Fetch()
assert.NoError(t, err)
assert.Empty(t, receivedAuth)
}
func TestPrivilegeHandler_Fetch_Error_Response(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
}))
baseURL := server.Listener.Addr().String()
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
server.Close()
err := handler.Fetch()
assert.EqualError(t, err, fmt.Sprintf("Get \"http://%s/authz\": dial tcp %s: connect: connection refused", baseURL, baseURL))
}
func TestPrivilegeHandler_Fetch_Error_Unreadable_Body(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", "1")
}))
defer server.Close()
baseURL := server.Listener.Addr().String()
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
err := handler.Fetch()
assert.EqualError(t, err, "unexpected EOF")
}
func TestPrivilegeHandler_Fetch_Error_Broken_JSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("{abc"))
}))
defer server.Close()
baseURL := server.Listener.Addr().String()
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
err := handler.Fetch()
assert.EqualError(t, err, "invalid character 'a' looking for beginning of object key string")
}
func TestPrivilegeHandler_Fetch_Valid(t *testing.T) {
privileges := `
{
"jim@example.org": {
"00010203-0405-4607-8809-0a0b0c0d0e0f": {
"admin": false,
"company": true,
"consumer": false,
"time": true,
"invoicing": true,
"accounting": false,
"supplier": false,
"salary": true
}
}
}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(privileges))
}))
defer server.Close()
baseURL := server.Listener.Addr().String()
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
err := handler.Fetch()
assert.NoError(t, err)
expectedPrivileges := map[string]map[string]*CompanyPrivileges{
"jim@example.org": {
"00010203-0405-4607-8809-0a0b0c0d0e0f": {
Admin: false,
Company: true,
Consumer: false,
Time: true,
Invoicing: true,
Accounting: false,
Supplier: false,
Salary: true,
},
},
}
assert.Equal(t, expectedPrivileges, handler.privileges)
}
func TestPrivilegeHandler_Fetch_Concurrent_Fetches(t *testing.T) {
privileges := `
{
"jim@example.org": {
"00010203-0405-4607-8809-0a0b0c0d0e0f": {
"admin": false,
"company": true,
"consumer": false,
"time": true,
"invoicing": true,
"accounting": false,
"supplier": false,
"salary": true
}
}
}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(privileges))
}))
defer server.Close()
baseURL := server.Listener.Addr().String()
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
// Run multiple Fetch calls concurrently to test thread-safety
var wg sync.WaitGroup
errors := make(chan error, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if err := handler.Fetch(); err != nil {
errors <- err
}
}()
}
wg.Wait()
close(errors)
// Check no errors occurred
for err := range errors {
assert.NoError(t, err)
}
// Verify privileges were set correctly
expectedPrivileges := map[string]map[string]*CompanyPrivileges{
"jim@example.org": {
"00010203-0405-4607-8809-0a0b0c0d0e0f": {
Admin: false,
Company: true,
Consumer: false,
Time: true,
Invoicing: true,
Accounting: false,
Supplier: false,
Salary: true,
},
},
}
assert.Equal(t, expectedPrivileges, handler.privileges)
}
func TestPrivilegeHandler_Concurrent_Fetch_And_Read(t *testing.T) {
privileges := `
{
"jim@example.org": {
"abc-123": {
"admin": true,
"company": true,
"consumer": false,
"time": false,
"invoicing": false,
"accounting": false,
"supplier": false,
"salary": false
}
}
}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(privileges))
}))
defer server.Close()
baseURL := server.Listener.Addr().String()
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
var wg sync.WaitGroup
errors := make(chan error, 100)
// Start multiple Fetch operations
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if err := handler.Fetch(); err != nil {
errors <- err
}
}()
}
// Concurrently read privileges while Fetch is running
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
}()
}
// Concurrently check privileges while Fetch is running
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
}()
}
wg.Wait()
close(errors)
// Check no errors occurred
for err := range errors {
assert.NoError(t, err)
}
// Verify privileges are correct after all concurrent operations
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
assert.Equal(t, []string{"abc-123"}, companies)
isAllowed := handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Admin && privileges.Company
})
assert.True(t, isAllowed)
}
func TestPrivilegeHandler_Concurrent_Process_And_Read(t *testing.T) {
handler := New(WithBaseURL("base"))
var wg sync.WaitGroup
// Concurrently add privileges via Process
for i := 0; i < 100; i++ {
wg.Add(1)
companyID := fmt.Sprintf("company-%d", i%10)
go func(id string) {
defer wg.Done()
_ = handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: id,
Privilege: PrivilegeAdmin,
})
}(companyID)
}
// Concurrently read privileges while Process is running
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
}()
}
wg.Wait()
// Verify all companies were added
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
sort.Strings(companies)
expected := make([]string, 10)
for i := 0; i < 10; i++ {
expected[i] = fmt.Sprintf("company-%d", i)
}
sort.Strings(expected)
assert.Equal(t, expected, companies)
}
func TestPrivilegeHandler_Concurrent_Multiple_Operations(t *testing.T) {
privileges := `
{
"jim@example.org": {
"initial-company": {
"admin": true,
"company": true,
"consumer": false,
"time": false,
"invoicing": false,
"accounting": false,
"supplier": false,
"salary": false
}
}
}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(privileges))
}))
defer server.Close()
baseURL := server.Listener.Addr().String()
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
var wg sync.WaitGroup
// Fetch
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = handler.Fetch()
}()
}
// Process PrivilegeAdded
for i := 0; i < 20; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
_ = handler.Process(&PrivilegeAdded{
Email: "jane@example.org",
CompanyID: fmt.Sprintf("company-%d", idx%5),
Privilege: PrivilegeCompany,
})
}(i)
}
// CompaniesByUser reads
for i := 0; i < 50; i++ {
wg.Add(1)
email := "jim@example.org"
if i%2 == 0 {
email = "jane@example.org"
}
go func(e string) {
defer wg.Done()
_ = handler.CompaniesByUser(e, func(privileges CompanyPrivileges) bool {
return privileges.Admin || privileges.Company
})
}(email)
}
// IsAllowed reads
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = handler.IsAllowed("jim@example.org", "initial-company", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
}()
}
wg.Wait()
// Verify final state is consistent
jimCompanies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
assert.Contains(t, jimCompanies, "initial-company")
janeCompanies := handler.CompaniesByUser("jane@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Company
})
sort.Strings(janeCompanies)
expectedJane := []string{"company-0", "company-1", "company-2", "company-3", "company-4"}
assert.Equal(t, expectedJane, janeCompanies)
}
func TestPrivilegeHandler_Setup(t *testing.T) {
handler := New(WithBaseURL("base"))
topology, err := goamqp.CollectTopology("some-service", handler.Setup()...)
assert.NoError(t, err)
wiring := map[string]string{}
for _, e := range topology.Endpoints {
assert.Equal(t, spec.DirectionConsume, e.Direction)
assert.True(t, e.Ephemeral, "%s must be a per-replica consumer", e.RoutingKey)
wiring[e.RoutingKey] = e.MessageType
}
// A key wired to the wrong type could turn a revocation into a grant.
assert.Equal(t, map[string]string{
"User.Added": "client.UserAdded",
"User.Removed": "client.UserRemoved",
"Privilege.Added": "client.PrivilegeAdded",
"Privilege.Removed": "client.PrivilegeRemoved",
}, wiring)
}
func TestPrivilegeHandler_process(t *testing.T) {
ctx := context.Background()
handler := New(WithBaseURL("base"))
admin := func(p CompanyPrivileges) bool { return p.Admin }
assert.NoError(t, process[UserAdded](handler)(ctx, spec.ConsumableEvent[UserAdded]{
Payload: UserAdded{Email: "jim@example.org", CompanyID: "abc-123"},
}))
assert.False(t, handler.IsAllowed("jim@example.org", "abc-123", admin))
assert.NoError(t, process[PrivilegeAdded](handler)(ctx, spec.ConsumableEvent[PrivilegeAdded]{
Payload: PrivilegeAdded{Email: "jim@example.org", CompanyID: "abc-123", Privilege: PrivilegeAdmin},
}))
assert.True(t, handler.IsAllowed("jim@example.org", "abc-123", admin))
assert.NoError(t, process[PrivilegeRemoved](handler)(ctx, spec.ConsumableEvent[PrivilegeRemoved]{
Payload: PrivilegeRemoved{Email: "jim@example.org", CompanyID: "abc-123", Privilege: PrivilegeAdmin},
}))
assert.False(t, handler.IsAllowed("jim@example.org", "abc-123", admin))
assert.NoError(t, process[UserRemoved](handler)(ctx, spec.ConsumableEvent[UserRemoved]{
Payload: UserRemoved{Email: "jim@example.org", CompanyID: "abc-123"},
}))
assert.Empty(t, handler.CompaniesByUser("jim@example.org", func(CompanyPrivileges) bool { return true }))
}