diff --git a/db/config.go b/db/config.go index 635a96c25..957b4f89d 100644 --- a/db/config.go +++ b/db/config.go @@ -262,6 +262,17 @@ func UserHasAccess(pubKeyFromAuth string, uuid string, role string) bool { return true } +func (db database) UserHasAccess(pubKeyFromAuth string, uuid string, role string) bool { + org := DB.GetOrganizationByUuid(uuid) + var hasRole bool = false + if pubKeyFromAuth != org.OwnerPubKey { + userRoles := DB.GetUserRoles(uuid, pubKeyFromAuth) + hasRole = RolesCheck(userRoles, role) + return hasRole + } + return true +} + func (db database) UserHasManageBountyRoles(pubKeyFromAuth string, uuid string) bool { var manageRolesCount = len(ManageBountiesGroup) org := DB.GetOrganizationByUuid(uuid) diff --git a/db/interface.go b/db/interface.go index acdfc6c70..48e51df95 100644 --- a/db/interface.go +++ b/db/interface.go @@ -135,4 +135,5 @@ type Database interface { GetBountiesByDateRangeCount(r PaymentDateRange, re *http.Request) int64 PersonUniqueNameFromName(name string) (string, error) ProcessAlerts(p Person) + UserHasAccess(pubKeyFromAuth string, uuid string, role string) bool } diff --git a/handlers/organization_test.go b/handlers/organization_test.go index 154cb2c0e..10b6016f4 100644 --- a/handlers/organization_test.go +++ b/handlers/organization_test.go @@ -305,3 +305,169 @@ func TestDeleteOrganization(t *testing.T) { mockDb.AssertExpectations(t) }) } + +func TestGetOrganizationBounties(t *testing.T) { + ctx := context.WithValue(context.Background(), auth.ContextKey, "test-key") + mockDb := mocks.NewDatabase(t) + mockGenerateBountyHandler := func(bounties []db.Bounty) []db.BountyResponse { + return []db.BountyResponse{} // Mocked response + } + oHandler := NewOrganizationHandler(mockDb) + + t.Run("Should test that an organization's bounties can be listed without authentication", func(t *testing.T) { + orgUUID := "valid-uuid" + oHandler.generateBountyHandler = mockGenerateBountyHandler + + expectedBounties := []db.Bounty{{}, {}} // Mocked response + mockDb.On("GetOrganizationBounties", mock.AnythingOfType("*http.Request"), orgUUID).Return(expectedBounties).Once() + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(oHandler.GetOrganizationBounties) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", orgUUID) + req, err := http.NewRequestWithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx), http.MethodGet, "/bounties/"+orgUUID, nil) + if err != nil { + t.Fatal(err) + } + + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + }) + + t.Run("should return empty array when wrong organization UUID is passed", func(t *testing.T) { + orgUUID := "wrong-uuid" + + mockDb.On("GetOrganizationBounties", mock.AnythingOfType("*http.Request"), orgUUID).Return([]db.Bounty{}).Once() + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(oHandler.GetOrganizationBounties) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", orgUUID) + req, err := http.NewRequestWithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx), http.MethodGet, "/bounties/"+orgUUID+"?limit=10&sortBy=created&search=test&page=1&resetPage=true", nil) + if err != nil { + t.Fatal(err) + } + + handler.ServeHTTP(rr, req) + + // Assert that the response status code is as expected + assert.Equal(t, http.StatusOK, rr.Code) + + // Assert that the response body is an empty array + assert.Equal(t, "[]\n", rr.Body.String()) + }) +} + +func TestGetOrganizationBudget(t *testing.T) { + ctx := context.WithValue(context.Background(), auth.ContextKey, "test-key") + mockDb := mocks.NewDatabase(t) + oHandler := NewOrganizationHandler(mockDb) + + t.Run("Should test that a 401 is returned when trying to view an organization's budget without a token", func(t *testing.T) { + orgUUID := "valid-uuid" + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", orgUUID) + req, err := http.NewRequestWithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx), http.MethodGet, "/budget/"+orgUUID, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + http.HandlerFunc(oHandler.GetOrganizationBudget).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + + t.Run("Should test that the right organization budget is returned, if the user is the organization admin or has the ViewReport role", func(t *testing.T) { + orgUUID := "valid-uuid" + expectedBudget := db.BountyBudget{ + ID: 1, + OrgUuid: orgUUID, + TotalBudget: 1000, + Created: nil, + Updated: nil, + } + + mockDb.On("UserHasAccess", "test-key", orgUUID, "VIEW REPORT").Return(true).Once() + mockDb.On("GetOrganizationBudget", orgUUID).Return(expectedBudget).Once() + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", orgUUID) + req, err := http.NewRequestWithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx), http.MethodGet, "/budget/"+orgUUID, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + http.HandlerFunc(oHandler.GetOrganizationBudget).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + + var responseBudget db.BountyBudget + err = json.Unmarshal(rr.Body.Bytes(), &responseBudget) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, expectedBudget, responseBudget) + }) +} + +func TestGetOrganizationBudgetHistory(t *testing.T) { + ctx := context.WithValue(context.Background(), auth.ContextKey, "test-key") + mockDb := mocks.NewDatabase(t) + oHandler := NewOrganizationHandler(mockDb) + + t.Run("Should test that a 401 is returned when trying to view an organization's budget history without a token", func(t *testing.T) { + orgUUID := "valid-uuid" + + mockDb.On("UserHasAccess", "", orgUUID, "VIEW REPORT").Return(false).Once() + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", orgUUID) + req, err := http.NewRequestWithContext(context.WithValue(context.Background(), chi.RouteCtxKey, rctx), http.MethodGet, "/budget/history/"+orgUUID, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + http.HandlerFunc(oHandler.GetOrganizationBudgetHistory).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + + t.Run("Should test that the right budget history is returned, if the user is the organization admin or has the ViewReport role", func(t *testing.T) { + orgUUID := "valid-uuid" + expectedBudgetHistory := []db.BudgetHistoryData{ + {BudgetHistory: db.BudgetHistory{ID: 1, OrgUuid: orgUUID, Created: nil, Updated: nil}, SenderName: "Sender1"}, + {BudgetHistory: db.BudgetHistory{ID: 2, OrgUuid: orgUUID, Created: nil, Updated: nil}, SenderName: "Sender2"}, + } + + mockDb.On("UserHasAccess", "test-key", orgUUID, "VIEW REPORT").Return(true).Once() + mockDb.On("GetOrganizationBudgetHistory", orgUUID).Return(expectedBudgetHistory).Once() + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", orgUUID) + req, err := http.NewRequestWithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx), http.MethodGet, "/budget/history/"+orgUUID, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + http.HandlerFunc(oHandler.GetOrganizationBudgetHistory).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + + var responseBudgetHistory []db.BudgetHistoryData + err = json.Unmarshal(rr.Body.Bytes(), &responseBudgetHistory) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, expectedBudgetHistory, responseBudgetHistory) + }) +} diff --git a/handlers/organizations.go b/handlers/organizations.go index a9faf1ee7..d88bd5897 100644 --- a/handlers/organizations.go +++ b/handlers/organizations.go @@ -16,11 +16,15 @@ import ( ) type organizationHandler struct { - db db.Database + db db.Database + generateBountyHandler func(bounties []db.Bounty) []db.BountyResponse } func NewOrganizationHandler(db db.Database) *organizationHandler { - return &organizationHandler{db: db} + return &organizationHandler{ + db: db, + generateBountyHandler: GenerateBountyResponse, + } } func (oh *organizationHandler) CreateOrEditOrganization(w http.ResponseWriter, r *http.Request) { @@ -517,18 +521,18 @@ func GetCreatedOrganizations(pubkey string) []db.Organization { return organizations } -func GetOrganizationBounties(w http.ResponseWriter, r *http.Request) { +func (oh *organizationHandler) GetOrganizationBounties(w http.ResponseWriter, r *http.Request) { uuid := chi.URLParam(r, "uuid") // get the organization bounties - organizationBounties := db.DB.GetOrganizationBounties(r, uuid) + organizationBounties := oh.db.GetOrganizationBounties(r, uuid) - var bountyResponse []db.BountyResponse = GenerateBountyResponse(organizationBounties) + var bountyResponse []db.BountyResponse = oh.generateBountyHandler(organizationBounties) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(bountyResponse) } -func GetOrganizationBudget(w http.ResponseWriter, r *http.Request) { +func (oh *organizationHandler) GetOrganizationBudget(w http.ResponseWriter, r *http.Request) { ctx := r.Context() pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) uuid := chi.URLParam(r, "uuid") @@ -540,7 +544,7 @@ func GetOrganizationBudget(w http.ResponseWriter, r *http.Request) { } // if not the organization admin - hasRole := db.UserHasAccess(pubKeyFromAuth, uuid, db.ViewReport) + hasRole := oh.db.UserHasAccess(pubKeyFromAuth, uuid, db.ViewReport) if !hasRole { w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode("Don't have access to view budget") @@ -548,19 +552,19 @@ func GetOrganizationBudget(w http.ResponseWriter, r *http.Request) { } // get the organization budget - organizationBudget := db.DB.GetOrganizationBudget(uuid) + organizationBudget := oh.db.GetOrganizationBudget(uuid) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(organizationBudget) } -func GetOrganizationBudgetHistory(w http.ResponseWriter, r *http.Request) { +func (oh *organizationHandler) GetOrganizationBudgetHistory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) uuid := chi.URLParam(r, "uuid") // if not the organization admin - hasRole := db.UserHasAccess(pubKeyFromAuth, uuid, db.ViewReport) + hasRole := oh.db.UserHasAccess(pubKeyFromAuth, uuid, db.ViewReport) if !hasRole { w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode("Don't have access to view budget history") @@ -568,7 +572,7 @@ func GetOrganizationBudgetHistory(w http.ResponseWriter, r *http.Request) { } // get the organization budget - organizationBudget := db.DB.GetOrganizationBudgetHistory(uuid) + organizationBudget := oh.db.GetOrganizationBudgetHistory(uuid) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(organizationBudget) diff --git a/mocks/Database.go b/mocks/Database.go index 4fa56f85a..aea3af257 100644 --- a/mocks/Database.go +++ b/mocks/Database.go @@ -6166,6 +6166,54 @@ func (_c *Database_UpdateTwitterConfirmed_Call) RunAndReturn(run func(uint, bool return _c } +// UserHasAccess provides a mock function with given fields: pubKeyFromAuth, uuid, role +func (_m *Database) UserHasAccess(pubKeyFromAuth string, uuid string, role string) bool { + ret := _m.Called(pubKeyFromAuth, uuid, role) + + if len(ret) == 0 { + panic("no return value specified for UserHasAccess") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(string, string, string) bool); ok { + r0 = rf(pubKeyFromAuth, uuid, role) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Database_UserHasAccess_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UserHasAccess' +type Database_UserHasAccess_Call struct { + *mock.Call +} + +// UserHasAccess is a helper method to define mock.On call +// - pubKeyFromAuth string +// - uuid string +// - role string +func (_e *Database_Expecter) UserHasAccess(pubKeyFromAuth interface{}, uuid interface{}, role interface{}) *Database_UserHasAccess_Call { + return &Database_UserHasAccess_Call{Call: _e.mock.On("UserHasAccess", pubKeyFromAuth, uuid, role)} +} + +func (_c *Database_UserHasAccess_Call) Run(run func(pubKeyFromAuth string, uuid string, role string)) *Database_UserHasAccess_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *Database_UserHasAccess_Call) Return(_a0 bool) *Database_UserHasAccess_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Database_UserHasAccess_Call) RunAndReturn(run func(string, string, string) bool) *Database_UserHasAccess_Call { + _c.Call.Return(run) + return _c +} + // UserHasManageBountyRoles provides a mock function with given fields: pubKeyFromAuth, uuid func (_m *Database) UserHasManageBountyRoles(pubKeyFromAuth string, uuid string) bool { ret := _m.Called(pubKeyFromAuth, uuid) diff --git a/routes/organizations.go b/routes/organizations.go index 2568f1c38..a46a1fdf7 100644 --- a/routes/organizations.go +++ b/routes/organizations.go @@ -16,7 +16,7 @@ func OrganizationRoutes() chi.Router { r.Get("/{uuid}", handlers.GetOrganizationByUuid) r.Get("/users/{uuid}", handlers.GetOrganizationUsers) r.Get("/users/{uuid}/count", handlers.GetOrganizationUsersCount) - r.Get("/bounties/{uuid}", handlers.GetOrganizationBounties) + r.Get("/bounties/{uuid}", organizationHandlers.GetOrganizationBounties) r.Get("/user/{userId}", handlers.GetUserOrganizations) r.Get("/user/dropdown/{userId}", organizationHandlers.GetUserDropdownOrganizations) }) @@ -31,8 +31,8 @@ func OrganizationRoutes() chi.Router { r.Get("/foruser/{uuid}", handlers.GetOrganizationUser) r.Get("/bounty/roles", handlers.GetBountyRoles) r.Get("/users/role/{uuid}/{user}", handlers.GetUserRoles) - r.Get("/budget/{uuid}", handlers.GetOrganizationBudget) - r.Get("/budget/history/{uuid}", handlers.GetOrganizationBudgetHistory) + r.Get("/budget/{uuid}", organizationHandlers.GetOrganizationBudget) + r.Get("/budget/history/{uuid}", organizationHandlers.GetOrganizationBudgetHistory) r.Get("/payments/{uuid}", handlers.GetPaymentHistory) r.Get("/poll/invoices/{uuid}", handlers.PollBudgetInvoices) r.Get("/invoices/count/{uuid}", handlers.GetInvoicesCount)