diff --git a/.gitignore b/.gitignore index 9ada4d7..660ad3d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea /release +coverage.txt diff --git a/client_test.go b/client_test.go index 5ceca6b..d8cfe4e 100644 --- a/client_test.go +++ b/client_test.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httptest" "sort" + "sync" "testing" "github.com/sparetimecoders/goamqp" @@ -332,3 +333,285 @@ func TestPrivilegeHandler_Fetch_Valid(t *testing.T) { } 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, + }, goamqp.Headers{}) + }(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, + }, goamqp.Headers{}) + }(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) +}