diff --git a/openapi.json b/openapi.json index 453f973f..b4fcbc84 100644 --- a/openapi.json +++ b/openapi.json @@ -2404,7 +2404,7 @@ "Organizations" ], "summary": "List organizations", - "description": "Lists every organization the authenticated user can access, both the ones they own and the ones they are a member of, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10, with a hard maximum of 100. Out-of-range values are clamped silently rather than rejected.", + "description": "Lists every organization the authenticated user can access, both the ones they own and the ones they are a member of, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected.", "operationId": "listOrganizations", "parameters": [ { @@ -2572,7 +2572,7 @@ "Organization Invitations" ], "summary": "List invitations", - "description": "Lists the invitations for an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10, with a hard maximum of 100. Out-of-range values are clamped silently rather than rejected.", + "description": "Lists the invitations for an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected.", "operationId": "listOrganizationInvitations", "parameters": [ { @@ -2840,7 +2840,7 @@ "Organization Members" ], "summary": "List members", - "description": "Lists the members of an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10, with a hard maximum of 100. Out-of-range values are clamped silently rather than rejected.", + "description": "Lists the members of an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected.", "operationId": "listOrganizationMembers", "parameters": [ { @@ -3090,7 +3090,7 @@ "Organization Teams" ], "summary": "List teams", - "description": "Lists the teams within an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10, with a hard maximum of 100. Out-of-range values are clamped silently rather than rejected.", + "description": "Lists the teams within an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected.", "operationId": "listOrganizationTeams", "parameters": [ { @@ -3300,7 +3300,7 @@ "Organization Team Members" ], "summary": "List team members", - "description": "Lists the members of a team, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10, with a hard maximum of 100. Out-of-range values are clamped silently rather than rejected.", + "description": "Lists the members of a team, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected.", "operationId": "listOrganizationTeamMembers", "parameters": [ { diff --git a/plugins/organizations/api.go b/plugins/organizations/api.go index 8913a8ca..7c1f3f71 100644 --- a/plugins/organizations/api.go +++ b/plugins/organizations/api.go @@ -60,8 +60,18 @@ func (a *API) CreateOrganization(ctx context.Context, actor *models.Actor, reque return a.organizationService.CreateOrganization(ctx, actor, request) } -func (a *API) GetAllOrganizations(ctx context.Context, actor *models.Actor, params pagination.Params) (*types.ListOrganizationsResponse, error) { - return a.organizationService.GetAllOrganizations(ctx, actor, params) +func (a *API) ListAllOrganizations(ctx context.Context, actor *models.Actor, params pagination.Params) (*types.ListOrganizationsResponse, error) { + return a.organizationService.ListAllOrganizations(ctx, actor, params) +} + +func (a *API) GetAllOrganizations(ctx context.Context, actor *models.Actor) ([]types.Organization, error) { + return a.organizationService.GetAllOrganizations(ctx, actor) +} + +// GetAllOrganizationsUnscoped returns every organization in the system with no +// access filtering. Authorize the caller before using it. +func (a *API) GetAllOrganizationsUnscoped(ctx context.Context) ([]types.Organization, error) { + return a.organizationService.GetAllOrganizationsUnscoped(ctx) } func (a *API) GetOrganizationByID(ctx context.Context, actor *models.Actor, organizationID string) (*types.Organization, error) { @@ -82,8 +92,12 @@ func (a *API) CreateInvitation(ctx context.Context, actor *models.Actor, organiz return a.invitationService.CreateOrganizationInvitation(ctx, actor, organizationID, request, redirectURL) } -func (a *API) GetAllInvitations(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationInvitationsResponse, error) { - return a.invitationService.GetAllOrganizationInvitationsByOrgIDWithOrg(ctx, organizationID, params) +func (a *API) ListAllInvitations(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationInvitationsResponse, error) { + return a.invitationService.ListAllOrganizationInvitationsByOrgIDWithOrg(ctx, organizationID, params) +} + +func (a *API) GetAllInvitations(ctx context.Context, actor *models.Actor, organizationID string) ([]types.GetOrganizationInvitationResponse, error) { + return a.invitationService.GetAllOrganizationInvitationsByOrgIDWithOrg(ctx, organizationID) } func (a *API) GetInvitation(ctx context.Context, actor *models.Actor, organizationID string, invitationID string) (*types.GetOrganizationInvitationResponse, error) { @@ -116,8 +130,12 @@ func (a *API) AddMember(ctx context.Context, actor *models.Actor, organizationID return a.memberService.AddMember(ctx, actor, organizationID, request) } -func (a *API) GetAllMembers(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationMembersResponse, error) { - return a.memberService.GetAllMembers(ctx, actor, organizationID, params) +func (a *API) ListAllMembers(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationMembersResponse, error) { + return a.memberService.ListAllMembers(ctx, actor, organizationID, params) +} + +func (a *API) GetAllMembers(ctx context.Context, actor *models.Actor, organizationID string) ([]types.OrganizationMemberResponse, error) { + return a.memberService.GetAllMembers(ctx, actor, organizationID) } func (a *API) GetMember(ctx context.Context, actor *models.Actor, organizationID string, memberID string) (*types.OrganizationMemberResponse, error) { @@ -142,8 +160,12 @@ func (a *API) CreateTeam(ctx context.Context, actor *models.Actor, organizationI return a.teamService.CreateTeam(ctx, actor, organizationID, request) } -func (a *API) GetAllTeams(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationTeamsResponse, error) { - return a.teamService.GetAllTeams(ctx, actor, organizationID, params) +func (a *API) ListAllTeams(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationTeamsResponse, error) { + return a.teamService.ListAllTeams(ctx, actor, organizationID, params) +} + +func (a *API) GetAllTeams(ctx context.Context, actor *models.Actor, organizationID string) ([]types.OrganizationTeam, error) { + return a.teamService.GetAllTeams(ctx, actor, organizationID) } func (a *API) GetTeam(ctx context.Context, actor *models.Actor, organizationID string, teamID string) (*types.OrganizationTeam, error) { @@ -164,8 +186,12 @@ func (a *API) AddTeamMember(ctx context.Context, actor *models.Actor, organizati return a.teamMemberService.AddTeamMember(ctx, actor, organizationID, teamID, request) } -func (a *API) GetAllTeamMembers(ctx context.Context, actor *models.Actor, organizationID string, teamID string, params pagination.Params) (*types.ListOrganizationTeamMembersResponse, error) { - return a.teamMemberService.GetAllTeamMembers(ctx, actor, organizationID, teamID, params) +func (a *API) ListAllTeamMembers(ctx context.Context, actor *models.Actor, organizationID string, teamID string, params pagination.Params) (*types.ListOrganizationTeamMembersResponse, error) { + return a.teamMemberService.ListAllTeamMembers(ctx, actor, organizationID, teamID, params) +} + +func (a *API) GetAllTeamMembers(ctx context.Context, actor *models.Actor, organizationID string, teamID string) ([]types.OrganizationTeamMemberResponse, error) { + return a.teamMemberService.GetAllTeamMembers(ctx, actor, organizationID, teamID) } func (a *API) GetTeamMember(ctx context.Context, actor *models.Actor, organizationID string, teamID string, memberID string) (*types.OrganizationTeamMemberResponse, error) { diff --git a/plugins/organizations/handlers/organization_handlers.go b/plugins/organizations/handlers/organization_handlers.go index 8d542172..9dd58abe 100644 --- a/plugins/organizations/handlers/organization_handlers.go +++ b/plugins/organizations/handlers/organization_handlers.go @@ -49,17 +49,17 @@ func (h *CreateOrganizationHandler) Handle() http.HandlerFunc { } } -type GetAllOrganizationsHandler struct { +type ListAllOrganizationsHandler struct { UseCases *orgusecases.UseCases } -func (h *GetAllOrganizationsHandler) Handle() http.HandlerFunc { +func (h *ListAllOrganizationsHandler) Handle() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reqCtx, _ := models.GetRequestContext(ctx) actor := reqCtx.Actor paginationParams := pagination.ParseFromRequest(r) - organizations, err := h.UseCases.GetAllOrganizations(ctx, actor, paginationParams) + organizations, err := h.UseCases.ListAllOrganizations(ctx, actor, paginationParams) if err != nil { orgconstants.HandleError(err, reqCtx) return diff --git a/plugins/organizations/handlers/organization_handlers_test.go b/plugins/organizations/handlers/organization_handlers_test.go index a78d9ae2..366388c3 100644 --- a/plugins/organizations/handlers/organization_handlers_test.go +++ b/plugins/organizations/handlers/organization_handlers_test.go @@ -180,7 +180,7 @@ func TestCreateOrganizationHandler(t *testing.T) { } } -func TestGetAllOrganizationsHandler(t *testing.T) { +func TestListAllOrganizationsHandler(t *testing.T) { t.Parallel() tests := []organizationHandlerCase{ @@ -193,7 +193,7 @@ func TestGetAllOrganizationsHandler(t *testing.T) { name: "service_error", userID: new("user-1"), prepare: func(f *organizationHandlerFixture) { - f.service.On("GetAllOrganizations", mock.Anything, "user-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). + f.service.On("ListAllOrganizations", mock.Anything, "user-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). Return((*orgtypes.ListOrganizationsResponse)(nil), errors.New("some error")).Once() }, expectedStatus: http.StatusBadRequest, @@ -203,7 +203,7 @@ func TestGetAllOrganizationsHandler(t *testing.T) { name: "success", userID: new("user-1"), prepare: func(f *organizationHandlerFixture) { - f.service.On("GetAllOrganizations", mock.Anything, "user-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). + f.service.On("ListAllOrganizations", mock.Anything, "user-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). Return(&orgtypes.ListOrganizationsResponse{ Data: []orgtypes.Organization{{ID: "org-1", OwnerID: "user-1", Name: "Acme Inc", Slug: "acme-inc"}}, Pagination: pagination.New(1, 10, 1), @@ -230,7 +230,7 @@ func TestGetAllOrganizationsHandler(t *testing.T) { tt.prepare(fixture) } - handler := &GetAllOrganizationsHandler{UseCases: newOrgUseCases(fixture.service)} + handler := &ListAllOrganizationsHandler{UseCases: newOrgUseCases(fixture.service)} req, w, reqCtx := fixture.newRequest(t, http.MethodGet, "/organizations", nil, tt.userID, "") if tt.name == "missing_user" { reqCtx.SetJSONResponse(http.StatusUnauthorized, map[string]any{"message": "Unauthorized"}) diff --git a/plugins/organizations/handlers/organization_invitation_handlers.go b/plugins/organizations/handlers/organization_invitation_handlers.go index 951b2998..a1ab681b 100644 --- a/plugins/organizations/handlers/organization_invitation_handlers.go +++ b/plugins/organizations/handlers/organization_invitation_handlers.go @@ -46,11 +46,11 @@ func (h *CreateOrganizationInvitationHandler) Handle() http.HandlerFunc { } } -type GetAllOrganizationInvitationsHandler struct { +type ListAllOrganizationInvitationsHandler struct { UseCases *orgusecases.UseCases } -func (h *GetAllOrganizationInvitationsHandler) Handle() http.HandlerFunc { +func (h *ListAllOrganizationInvitationsHandler) Handle() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reqCtx, _ := models.GetRequestContext(ctx) @@ -58,7 +58,7 @@ func (h *GetAllOrganizationInvitationsHandler) Handle() http.HandlerFunc { organizationID := r.PathValue("organization_id") paginationParams := pagination.ParseFromRequest(r) - invitations, err := h.UseCases.GetAllOrganizationInvitations(ctx, actor, organizationID, paginationParams) + invitations, err := h.UseCases.ListAllOrganizationInvitations(ctx, actor, organizationID, paginationParams) if err != nil { orgconstants.HandleError(err, reqCtx) return diff --git a/plugins/organizations/handlers/organization_invitation_handlers_test.go b/plugins/organizations/handlers/organization_invitation_handlers_test.go index 74932215..e23f34c6 100644 --- a/plugins/organizations/handlers/organization_invitation_handlers_test.go +++ b/plugins/organizations/handlers/organization_invitation_handlers_test.go @@ -163,11 +163,11 @@ func TestCreateOrganizationInvitationHandler(t *testing.T) { }) } -func TestGetAllOrganizationInvitationsHandler(t *testing.T) { +func TestListAllOrganizationInvitationsHandler(t *testing.T) { t.Parallel() runOrganizationInvitationHandlerCases(t, http.MethodGet, "/organizations/org-1/invitations", func(fixture *organizationInvitationHandlerFixture) http.HandlerFunc { - return (&GetAllOrganizationInvitationsHandler{UseCases: newInvitationUseCases(fixture.orgSvc, fixture.service)}).Handle() + return (&ListAllOrganizationInvitationsHandler{UseCases: newInvitationUseCases(fixture.orgSvc, fixture.service)}).Handle() }, []organizationInvitationHandlerCase{ { name: "missing_user", @@ -180,7 +180,7 @@ func TestGetAllOrganizationInvitationsHandler(t *testing.T) { userID: new("user-1"), organizationID: "org-1", prepare: func(fixture *organizationInvitationHandlerFixture) { - fixture.service.On("GetAllOrganizationInvitationsByOrgIDWithOrg", mock.Anything, "org-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). + fixture.service.On("ListAllOrganizationInvitationsByOrgIDWithOrg", mock.Anything, "org-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). Return((*orgtypes.ListOrganizationInvitationsResponse)(nil), errors.New("some error")).Once() }, expectedStatus: http.StatusBadRequest, @@ -191,7 +191,7 @@ func TestGetAllOrganizationInvitationsHandler(t *testing.T) { userID: new("user-1"), organizationID: "org-1", prepare: func(fixture *organizationInvitationHandlerFixture) { - fixture.service.On("GetAllOrganizationInvitationsByOrgIDWithOrg", mock.Anything, "org-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). + fixture.service.On("ListAllOrganizationInvitationsByOrgIDWithOrg", mock.Anything, "org-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). Return(&orgtypes.ListOrganizationInvitationsResponse{ Data: []orgtypes.GetOrganizationInvitationResponse{ { diff --git a/plugins/organizations/handlers/organization_member_handlers.go b/plugins/organizations/handlers/organization_member_handlers.go index c2173e35..ca1a4fff 100644 --- a/plugins/organizations/handlers/organization_member_handlers.go +++ b/plugins/organizations/handlers/organization_member_handlers.go @@ -51,11 +51,11 @@ func (h *AddOrganizationMemberHandler) Handle() http.HandlerFunc { } } -type GetAllOrganizationMembersHandler struct { +type ListAllOrganizationMembersHandler struct { UseCases *orgusecases.UseCases } -func (h *GetAllOrganizationMembersHandler) Handle() http.HandlerFunc { +func (h *ListAllOrganizationMembersHandler) Handle() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reqCtx, _ := models.GetRequestContext(ctx) @@ -63,7 +63,7 @@ func (h *GetAllOrganizationMembersHandler) Handle() http.HandlerFunc { organizationID := r.PathValue("organization_id") paginationParams := pagination.ParseFromRequest(r) - members, err := h.UseCases.GetAllMembers(ctx, actor, organizationID, paginationParams) + members, err := h.UseCases.ListAllMembers(ctx, actor, organizationID, paginationParams) if err != nil { orgconstants.HandleError(err, reqCtx) return diff --git a/plugins/organizations/handlers/organization_member_handlers_test.go b/plugins/organizations/handlers/organization_member_handlers_test.go index 782a138b..a02ab6be 100644 --- a/plugins/organizations/handlers/organization_member_handlers_test.go +++ b/plugins/organizations/handlers/organization_member_handlers_test.go @@ -160,13 +160,13 @@ func TestAddOrganizationMemberHandler(t *testing.T) { }) } -func TestGetAllOrganizationMembersHandler(t *testing.T) { +func TestListAllOrganizationMembersHandler(t *testing.T) { t.Parallel() defaultParams := pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit} runOrganizationMemberHandlerCases(t, http.MethodGet, "/organizations/org-1/members", func(fixture *organizationMemberHandlerFixture) http.HandlerFunc { - return (&GetAllOrganizationMembersHandler{UseCases: newMemberUseCases(fixture.service)}).Handle() + return (&ListAllOrganizationMembersHandler{UseCases: newMemberUseCases(fixture.service)}).Handle() }, []organizationMemberHandlerCase{ { name: "missing_user", @@ -179,7 +179,7 @@ func TestGetAllOrganizationMembersHandler(t *testing.T) { userID: new("user-1"), organizationID: "org-1", prepare: func(fixture *organizationMemberHandlerFixture) { - fixture.service.On("GetAllMembers", mock.Anything, "user-1", "org-1", defaultParams). + fixture.service.On("ListAllMembers", mock.Anything, "user-1", "org-1", defaultParams). Return((*orgtypes.ListOrganizationMembersResponse)(nil), errors.New("some error")).Once() }, expectedStatus: http.StatusBadRequest, @@ -190,7 +190,7 @@ func TestGetAllOrganizationMembersHandler(t *testing.T) { userID: new("user-1"), organizationID: "org-1", prepare: func(fixture *organizationMemberHandlerFixture) { - fixture.service.On("GetAllMembers", mock.Anything, "user-1", "org-1", defaultParams). + fixture.service.On("ListAllMembers", mock.Anything, "user-1", "org-1", defaultParams). Return(&orgtypes.ListOrganizationMembersResponse{ Data: []orgtypes.OrganizationMemberResponse{{ID: "mem-1", OrganizationID: "org-1", Role: "member"}}, Pagination: pagination.New(1, 10, 25), @@ -207,7 +207,7 @@ func TestGetAllOrganizationMembersHandler(t *testing.T) { }) } -func TestGetAllOrganizationMembersHandlerParsesPagination(t *testing.T) { +func TestListAllOrganizationMembersHandlerParsesPagination(t *testing.T) { t.Parallel() tests := []struct { @@ -227,14 +227,14 @@ func TestGetAllOrganizationMembersHandlerParsesPagination(t *testing.T) { t.Parallel() runOrganizationMemberHandlerCases(t, http.MethodGet, "/organizations/org-1/members"+tt.query, func(fixture *organizationMemberHandlerFixture) http.HandlerFunc { - return (&GetAllOrganizationMembersHandler{UseCases: newMemberUseCases(fixture.service)}).Handle() + return (&ListAllOrganizationMembersHandler{UseCases: newMemberUseCases(fixture.service)}).Handle() }, []organizationMemberHandlerCase{ { name: "forwards_parsed_params", userID: new("user-1"), organizationID: "org-1", prepare: func(fixture *organizationMemberHandlerFixture) { - fixture.service.On("GetAllMembers", mock.Anything, "user-1", "org-1", tt.expectedParams). + fixture.service.On("ListAllMembers", mock.Anything, "user-1", "org-1", tt.expectedParams). Return(&orgtypes.ListOrganizationMembersResponse{ Data: []orgtypes.OrganizationMemberResponse{}, Pagination: pagination.New(1, 10, 0), diff --git a/plugins/organizations/handlers/organization_team_handlers.go b/plugins/organizations/handlers/organization_team_handlers.go index 02f5a307..9f5e2f30 100644 --- a/plugins/organizations/handlers/organization_team_handlers.go +++ b/plugins/organizations/handlers/organization_team_handlers.go @@ -45,11 +45,11 @@ func (h *CreateOrganizationTeamHandler) Handle() http.HandlerFunc { } } -type GetAllOrganizationTeamsHandler struct { +type ListAllOrganizationTeamsHandler struct { UseCases *orgusecases.UseCases } -func (h *GetAllOrganizationTeamsHandler) Handle() http.HandlerFunc { +func (h *ListAllOrganizationTeamsHandler) Handle() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reqCtx, _ := models.GetRequestContext(ctx) @@ -57,7 +57,7 @@ func (h *GetAllOrganizationTeamsHandler) Handle() http.HandlerFunc { organizationID := r.PathValue("organization_id") paginationParams := pagination.ParseFromRequest(r) - teams, err := h.UseCases.GetAllTeams(ctx, actor, organizationID, paginationParams) + teams, err := h.UseCases.ListAllTeams(ctx, actor, organizationID, paginationParams) if err != nil { orgconstants.HandleError(err, reqCtx) return diff --git a/plugins/organizations/handlers/organization_team_handlers_test.go b/plugins/organizations/handlers/organization_team_handlers_test.go index 9c849727..92e9e918 100644 --- a/plugins/organizations/handlers/organization_team_handlers_test.go +++ b/plugins/organizations/handlers/organization_team_handlers_test.go @@ -138,11 +138,11 @@ func TestCreateOrganizationTeamHandler(t *testing.T) { }) } -func TestGetAllOrganizationTeamsHandler(t *testing.T) { +func TestListAllOrganizationTeamsHandler(t *testing.T) { t.Parallel() runOrganizationTeamHandlerCases(t, http.MethodGet, "/organizations/org-1/teams", func(fixture *organizationTeamHandlerFixture) http.HandlerFunc { - return (&GetAllOrganizationTeamsHandler{UseCases: newTeamUseCases(fixture.service)}).Handle() + return (&ListAllOrganizationTeamsHandler{UseCases: newTeamUseCases(fixture.service)}).Handle() }, []organizationTeamHandlerCase{ { name: "missing_user", @@ -155,7 +155,7 @@ func TestGetAllOrganizationTeamsHandler(t *testing.T) { userID: new("user-1"), organizationID: "org-1", prepare: func(fixture *organizationTeamHandlerFixture) { - fixture.service.On("GetAllTeams", mock.Anything, "user-1", "org-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). + fixture.service.On("ListAllTeams", mock.Anything, "user-1", "org-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). Return((*orgtypes.ListOrganizationTeamsResponse)(nil), errors.New("some error")).Once() }, expectedStatus: http.StatusBadRequest, @@ -166,7 +166,7 @@ func TestGetAllOrganizationTeamsHandler(t *testing.T) { userID: new("user-1"), organizationID: "org-1", prepare: func(fixture *organizationTeamHandlerFixture) { - fixture.service.On("GetAllTeams", mock.Anything, "user-1", "org-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). + fixture.service.On("ListAllTeams", mock.Anything, "user-1", "org-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). Return(&orgtypes.ListOrganizationTeamsResponse{ Data: []orgtypes.OrganizationTeam{{ID: "team-1", OrganizationID: "org-1", Name: "Platform", Slug: "platform"}}, Pagination: pagination.New(1, 10, 1), diff --git a/plugins/organizations/handlers/organization_team_member_handlers.go b/plugins/organizations/handlers/organization_team_member_handlers.go index c46d1b30..63b75ccd 100644 --- a/plugins/organizations/handlers/organization_team_member_handlers.go +++ b/plugins/organizations/handlers/organization_team_member_handlers.go @@ -46,11 +46,11 @@ func (h *AddOrganizationTeamMemberHandler) Handle() http.HandlerFunc { } } -type GetAllOrganizationTeamMembersHandler struct { +type ListAllOrganizationTeamMembersHandler struct { UseCases *orgusecases.UseCases } -func (h *GetAllOrganizationTeamMembersHandler) Handle() http.HandlerFunc { +func (h *ListAllOrganizationTeamMembersHandler) Handle() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reqCtx, _ := models.GetRequestContext(ctx) @@ -59,7 +59,7 @@ func (h *GetAllOrganizationTeamMembersHandler) Handle() http.HandlerFunc { organizationID := r.PathValue("organization_id") teamID := r.PathValue("team_id") paginationParams := pagination.ParseFromRequest(r) - teamMembers, err := h.UseCases.GetAllTeamMembers(ctx, actor, organizationID, teamID, paginationParams) + teamMembers, err := h.UseCases.ListAllTeamMembers(ctx, actor, organizationID, teamID, paginationParams) if err != nil { orgconstants.HandleError(err, reqCtx) return diff --git a/plugins/organizations/handlers/organization_team_member_handlers_test.go b/plugins/organizations/handlers/organization_team_member_handlers_test.go index a6714de3..2466a468 100644 --- a/plugins/organizations/handlers/organization_team_member_handlers_test.go +++ b/plugins/organizations/handlers/organization_team_member_handlers_test.go @@ -146,11 +146,11 @@ func TestAddOrganizationTeamMemberHandler(t *testing.T) { }) } -func TestGetAllOrganizationTeamMembersHandler(t *testing.T) { +func TestListAllOrganizationTeamMembersHandler(t *testing.T) { t.Parallel() runOrganizationTeamMemberHandlerCases(t, http.MethodGet, "/organizations/org-1/teams/team-1/members", func(fixture *organizationTeamMemberHandlerFixture) http.HandlerFunc { - return (&GetAllOrganizationTeamMembersHandler{UseCases: newTeamMemberUseCases(fixture.service)}).Handle() + return (&ListAllOrganizationTeamMembersHandler{UseCases: newTeamMemberUseCases(fixture.service)}).Handle() }, []organizationTeamMemberHandlerCase{ { name: "missing_user", @@ -165,7 +165,7 @@ func TestGetAllOrganizationTeamMembersHandler(t *testing.T) { organizationID: "org-1", teamID: "team-1", prepare: func(fixture *organizationTeamMemberHandlerFixture) { - fixture.service.On("GetAllTeamMembers", mock.Anything, "user-1", "org-1", "team-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). + fixture.service.On("ListAllTeamMembers", mock.Anything, "user-1", "org-1", "team-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). Return((*orgtypes.ListOrganizationTeamMembersResponse)(nil), errors.New("some error")).Once() }, expectedStatus: http.StatusBadRequest, @@ -177,7 +177,7 @@ func TestGetAllOrganizationTeamMembersHandler(t *testing.T) { organizationID: "org-1", teamID: "team-1", prepare: func(fixture *organizationTeamMemberHandlerFixture) { - fixture.service.On("GetAllTeamMembers", mock.Anything, "user-1", "org-1", "team-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). + fixture.service.On("ListAllTeamMembers", mock.Anything, "user-1", "org-1", "team-1", pagination.Params{Page: pagination.DefaultPage, Limit: pagination.DefaultLimit}). Return(&orgtypes.ListOrganizationTeamMembersResponse{ Data: []orgtypes.OrganizationTeamMemberResponse{{ID: "team-mem-1", TeamID: "team-1"}}, Pagination: pagination.New(1, 10, 1), diff --git a/plugins/organizations/openapi/openapi_docs.go b/plugins/organizations/openapi/openapi_docs.go index 40542590..d94cd3df 100644 --- a/plugins/organizations/openapi/openapi_docs.go +++ b/plugins/organizations/openapi/openapi_docs.go @@ -26,7 +26,7 @@ func RegisterOpenAPIDocs(svc openapi.OpenAPIService) error { "/organizations", openapi.WithOperationID("listOrganizations"), openapi.WithSummary("List organizations"), - openapi.WithDescription("Lists every organization the authenticated user can access, both the ones they own and the ones they are a member of, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10, with a hard maximum of 100. Out-of-range values are clamped silently rather than rejected."), + openapi.WithDescription("Lists every organization the authenticated user can access, both the ones they own and the ones they are a member of, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected."), openapi.WithTags("Organizations"), openapi.WithRequest(&types.ListOrganizationsRequest{}), openapi.WithResponseStatus(http.StatusOK, &types.ListOrganizationsResponse{}), @@ -81,7 +81,7 @@ func RegisterOpenAPIDocs(svc openapi.OpenAPIService) error { "/organizations/{organization_id}/invitations", openapi.WithOperationID("listOrganizationInvitations"), openapi.WithSummary("List invitations"), - openapi.WithDescription("Lists the invitations for an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10, with a hard maximum of 100. Out-of-range values are clamped silently rather than rejected."), + openapi.WithDescription("Lists the invitations for an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected."), openapi.WithTags("Organization Invitations"), openapi.WithRequest(&types.ListOrganizationInvitationsRequest{}), openapi.WithResponseStatus(http.StatusOK, &types.ListOrganizationInvitationsResponse{}), @@ -144,7 +144,7 @@ func RegisterOpenAPIDocs(svc openapi.OpenAPIService) error { "/organizations/{organization_id}/members", openapi.WithOperationID("listOrganizationMembers"), openapi.WithSummary("List members"), - openapi.WithDescription("Lists the members of an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10, with a hard maximum of 100. Out-of-range values are clamped silently rather than rejected."), + openapi.WithDescription("Lists the members of an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected."), openapi.WithTags("Organization Members"), openapi.WithRequest(&types.ListOrganizationMembersRequest{}), openapi.WithResponseStatus(http.StatusOK, &types.ListOrganizationMembersResponse{}), @@ -208,7 +208,7 @@ func RegisterOpenAPIDocs(svc openapi.OpenAPIService) error { "/organizations/{organization_id}/teams", openapi.WithOperationID("listOrganizationTeams"), openapi.WithSummary("List teams"), - openapi.WithDescription("Lists the teams within an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10, with a hard maximum of 100. Out-of-range values are clamped silently rather than rejected."), + openapi.WithDescription("Lists the teams within an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected."), openapi.WithTags("Organization Teams"), openapi.WithRequest(&types.ListOrganizationTeamsRequest{}), openapi.WithResponseStatus(http.StatusOK, &types.ListOrganizationTeamsResponse{}), @@ -262,7 +262,7 @@ func RegisterOpenAPIDocs(svc openapi.OpenAPIService) error { "/organizations/{organization_id}/teams/{team_id}/members", openapi.WithOperationID("listOrganizationTeamMembers"), openapi.WithSummary("List team members"), - openapi.WithDescription("Lists the members of a team, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10, with a hard maximum of 100. Out-of-range values are clamped silently rather than rejected."), + openapi.WithDescription("Lists the members of a team, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected."), openapi.WithTags("Organization Team Members"), openapi.WithRequest(&types.ListOrganizationTeamMembersRequest{}), openapi.WithResponseStatus(http.StatusOK, &types.ListOrganizationTeamMembersResponse{}), diff --git a/plugins/organizations/repositories/bun_organization_invitation_repository.go b/plugins/organizations/repositories/bun_organization_invitation_repository.go index 5e02d8af..40a3e31b 100644 --- a/plugins/organizations/repositories/bun_organization_invitation_repository.go +++ b/plugins/organizations/repositories/bun_organization_invitation_repository.go @@ -69,16 +69,30 @@ func (r *BunOrganizationInvitationRepository) GetByOrganizationIDAndEmail(ctx co return invitation, err } -func (r *BunOrganizationInvitationRepository) GetAllPendingByEmail(ctx context.Context, email string, limit int) ([]types.OrganizationInvitation, error) { - if limit <= 0 || limit > MaxPendingInvitationsPerBatch { - limit = MaxPendingInvitationsPerBatch +// pendingByEmailWhere matches invitations that are still awaiting a decision and +// have not expired. Both pending lookups order oldest first, because acceptance +// resolves role conflicts in favour of the earliest invitation. +const pendingByEmailWhere = "email = ? AND status = ? AND expires_at > ?" + +func (r *BunOrganizationInvitationRepository) ListAllPendingByEmail(ctx context.Context, email string, page int, limit int) ([]types.OrganizationInvitation, int, error) { + invites := make([]types.OrganizationInvitation, 0) + limit = pageLimit(limit) + total, err := r.db.NewSelect().Model(&invites). + Where(pendingByEmailWhere, email, types.OrganizationInvitationStatusPending, time.Now().UTC()). + OrderExpr("created_at ASC, id ASC"). + Offset(pageOffset(page, limit)).Limit(limit). + ScanAndCount(ctx) + if err == sql.ErrNoRows { + return []types.OrganizationInvitation{}, total, nil } + return invites, total, err +} +func (r *BunOrganizationInvitationRepository) GetAllPendingByEmail(ctx context.Context, email string) ([]types.OrganizationInvitation, error) { invites := make([]types.OrganizationInvitation, 0) err := r.db.NewSelect().Model(&invites). - Where("email = ? AND status = ? AND expires_at > ?", email, types.OrganizationInvitationStatusPending, time.Now().UTC()). + Where(pendingByEmailWhere, email, types.OrganizationInvitationStatusPending, time.Now().UTC()). OrderExpr("created_at ASC, id ASC"). - Limit(limit). Scan(ctx) if err == sql.ErrNoRows { return []types.OrganizationInvitation{}, nil @@ -181,7 +195,7 @@ func (r *BunOrganizationInvitationRepository) GetByIDWithOrg(ctx context.Context return &result, nil } -func (r *BunOrganizationInvitationRepository) GetAllByOrganizationIDWithOrg(ctx context.Context, organizationID string, page int, limit int) ([]types.GetOrganizationInvitationResponse, int, error) { +func (r *BunOrganizationInvitationRepository) ListAllByOrganizationIDWithOrg(ctx context.Context, organizationID string, page int, limit int) ([]types.GetOrganizationInvitationResponse, int, error) { limit = pageLimit(limit) var total int @@ -206,3 +220,18 @@ func (r *BunOrganizationInvitationRepository) GetAllByOrganizationIDWithOrg(ctx } return results, total, nil } + +func (r *BunOrganizationInvitationRepository) GetAllByOrganizationIDWithOrg(ctx context.Context, organizationID string) ([]types.GetOrganizationInvitationResponse, error) { + var rows []invitationOrgRow + err := r.db.NewRaw(`SELECT `+invitationWithOrgColumns+invitationWithOrgByOrganizationFrom+` + ORDER BY i.created_at DESC, i.id DESC + `, organizationID).Scan(ctx, &rows) + if err != nil && err != sql.ErrNoRows { + return nil, err + } + results := make([]types.GetOrganizationInvitationResponse, len(rows)) + for i, row := range rows { + results[i] = mapToInvitationWithOrgResponse(row) + } + return results, nil +} diff --git a/plugins/organizations/repositories/bun_organization_invitation_repository_test.go b/plugins/organizations/repositories/bun_organization_invitation_repository_test.go index e8d891a1..17de1fa8 100644 --- a/plugins/organizations/repositories/bun_organization_invitation_repository_test.go +++ b/plugins/organizations/repositories/bun_organization_invitation_repository_test.go @@ -198,7 +198,7 @@ func TestBunOrganizationInvitationRepository_GetByOrganizationIDAndEmail(t *test } } -func TestBunOrganizationInvitationRepository_GetAllPendingByEmail(t *testing.T) { +func TestBunOrganizationInvitationRepository_ListAllPendingByEmail(t *testing.T) { t.Parallel() setup := func(t *testing.T) (repositories.OrganizationInvitationRepository, context.Context) { @@ -231,15 +231,77 @@ func TestBunOrganizationInvitationRepository_GetAllPendingByEmail(t *testing.T) return repo, ctx } + tests := []struct { + name string + email string + page int + limit int + expectedIDs []string + expectedTotal int + }{ + {name: "first page returns the oldest pending invitations", email: "user@example.com", page: 1, limit: 10, expectedIDs: []string{"inv-1", "inv-2", "inv-3"}, expectedTotal: 3}, + {name: "page size splits the pending invitations", email: "user@example.com", page: 1, limit: 2, expectedIDs: []string{"inv-1", "inv-2"}, expectedTotal: 3}, + {name: "second page returns the remainder", email: "user@example.com", page: 2, limit: 2, expectedIDs: []string{"inv-3"}, expectedTotal: 3}, + {name: "unknown email has nothing pending", email: "missing@example.com", page: 1, limit: 10, expectedIDs: []string{}, expectedTotal: 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + repo, ctx := setup(t) + + pending, total, err := repo.ListAllPendingByEmail(ctx, tt.email, tt.page, tt.limit) + require.NoError(t, err) + require.Equal(t, tt.expectedTotal, total) + + ids := make([]string, 0, len(pending)) + for _, invitation := range pending { + ids = append(ids, invitation.ID) + } + require.Equal(t, tt.expectedIDs, ids) + }) + } +} + +func TestBunOrganizationInvitationRepository_GetAllPendingByEmail(t *testing.T) { + t.Parallel() + + setup := func(t *testing.T) (repositories.OrganizationInvitationRepository, context.Context) { + t.Helper() + + db := plugintests.SetupRepoDB(t) + plugintests.SeedOrganization(t, db, "org-1", "user-1", "Acme Inc", "acme-inc") + + repo := repositories.NewBunOrganizationInvitationRepository(db) + ctx := context.Background() + + for i, invitation := range []*types.OrganizationInvitation{ + {ID: "inv-1", Status: types.OrganizationInvitationStatusPending, ExpiresAt: time.Now().UTC().Add(time.Hour)}, + {ID: "inv-2", Status: types.OrganizationInvitationStatusPending, ExpiresAt: time.Now().UTC().Add(time.Hour)}, + {ID: "inv-3", Status: types.OrganizationInvitationStatusPending, ExpiresAt: time.Now().UTC().Add(time.Hour)}, + {ID: "inv-4", Status: types.OrganizationInvitationStatusAccepted, ExpiresAt: time.Now().UTC().Add(time.Hour)}, + {ID: "inv-5", Status: types.OrganizationInvitationStatusPending, ExpiresAt: time.Now().UTC().Add(-time.Hour)}, + } { + invitation.OrganizationID = "org-1" + invitation.Email = "user@example.com" + invitation.InviterID = "user-1" + invitation.Role = "member" + _, err := repo.Create(ctx, invitation) + require.NoError(t, err, "seeding invitation %d", i) + } + + return repo, ctx + } + tests := []struct { name string email string - limit int expectedIDs []string }{ - {name: "returns every pending invitation", email: "user@example.com", limit: 10, expectedIDs: []string{"inv-1", "inv-2", "inv-3"}}, - {name: "capped batch returns the oldest pending invitations", email: "user@example.com", limit: 2, expectedIDs: []string{"inv-1", "inv-2"}}, - {name: "unknown email has nothing pending", email: "missing@example.com", limit: 10, expectedIDs: []string{}}, + {name: "returns every pending invitation oldest first", email: "user@example.com", expectedIDs: []string{"inv-1", "inv-2", "inv-3"}}, + {name: "accepted and expired invitations are excluded", email: "user@example.com", expectedIDs: []string{"inv-1", "inv-2", "inv-3"}}, + {name: "unknown email has nothing pending", email: "missing@example.com", expectedIDs: []string{}}, } for _, tt := range tests { @@ -248,7 +310,7 @@ func TestBunOrganizationInvitationRepository_GetAllPendingByEmail(t *testing.T) repo, ctx := setup(t) - pending, err := repo.GetAllPendingByEmail(ctx, tt.email, tt.limit) + pending, err := repo.GetAllPendingByEmail(ctx, tt.email) require.NoError(t, err) ids := make([]string, 0, len(pending)) @@ -260,7 +322,40 @@ func TestBunOrganizationInvitationRepository_GetAllPendingByEmail(t *testing.T) } } -func TestBunOrganizationInvitationRepository_GetAllByOrganizationIDWithOrg(t *testing.T) { +// GetAllPendingByEmail must not silently truncate. Its paginated sibling stops at +// the requested limit; this one is expected to return the whole set in one query, +// well past the 500-row batch cap that used to apply here. +func TestBunOrganizationInvitationRepository_GetAllPendingByEmailIsNotCapped(t *testing.T) { + t.Parallel() + + const pendingCount = 600 + + db := plugintests.SetupRepoDB(t) + plugintests.SeedOrganization(t, db, "org-1", "user-1", "Acme Inc", "acme-inc") + repo := repositories.NewBunOrganizationInvitationRepository(db) + ctx := context.Background() + + for i := 1; i <= pendingCount; i++ { + _, err := repo.Create(ctx, &types.OrganizationInvitation{ + ID: fmt.Sprintf("inv-%04d", i), + OrganizationID: "org-1", + Email: "user@example.com", + InviterID: "user-1", + Role: "member", + Status: types.OrganizationInvitationStatusPending, + ExpiresAt: time.Now().UTC().Add(time.Hour), + }) + require.NoError(t, err, "seeding invitation %d", i) + } + + pending, err := repo.GetAllPendingByEmail(ctx, "user@example.com") + require.NoError(t, err) + require.Len(t, pending, pendingCount) + require.Equal(t, "inv-0001", pending[0].ID, "oldest invitation must come first") + require.Equal(t, fmt.Sprintf("inv-%04d", pendingCount), pending[len(pending)-1].ID) +} + +func TestBunOrganizationInvitationRepository_ListAllByOrganizationIDWithOrg(t *testing.T) { t.Parallel() setup := func(t *testing.T) (repositories.OrganizationInvitationRepository, context.Context) { @@ -311,7 +406,7 @@ func TestBunOrganizationInvitationRepository_GetAllByOrganizationIDWithOrg(t *te repo, ctx := setup(t) - invitations, total, err := repo.GetAllByOrganizationIDWithOrg(ctx, tt.organizationID, tt.page, tt.limit) + invitations, total, err := repo.ListAllByOrganizationIDWithOrg(ctx, tt.organizationID, tt.page, tt.limit) require.NoError(t, err) require.Len(t, invitations, tt.expectCount) require.Equal(t, tt.expectTotal, total) @@ -322,7 +417,7 @@ func TestBunOrganizationInvitationRepository_GetAllByOrganizationIDWithOrg(t *te } } -func TestBunOrganizationInvitationRepository_GetAllByOrganizationIDWithOrgPagesPartitionCleanly(t *testing.T) { +func TestBunOrganizationInvitationRepository_ListAllByOrganizationIDWithOrgPagesPartitionCleanly(t *testing.T) { t.Parallel() db := plugintests.SetupRepoDB(t) @@ -345,7 +440,7 @@ func TestBunOrganizationInvitationRepository_GetAllByOrganizationIDWithOrgPagesP seen := make([]string, 0, 5) for page := 1; page <= 3; page++ { - invitations, total, err := repo.GetAllByOrganizationIDWithOrg(ctx, "org-1", page, 2) + invitations, total, err := repo.ListAllByOrganizationIDWithOrg(ctx, "org-1", page, 2) require.NoError(t, err) require.Equal(t, 5, total) for _, invitation := range invitations { @@ -600,3 +695,91 @@ func TestBunOrganizationInvitationRepository_WithTx(t *testing.T) { }) } } + +func TestBunOrganizationInvitationRepository_GetAllByOrganizationIDWithOrg(t *testing.T) { + t.Parallel() + + setup := func(t *testing.T, count int) (repositories.OrganizationInvitationRepository, context.Context) { + t.Helper() + + db := plugintests.SetupRepoDB(t) + plugintests.SeedOrganization(t, db, "org-1", "user-1", "Acme Inc", "acme-inc") + plugintests.SeedOrganization(t, db, "org-2", "user-2", "Beta Inc", "beta-inc") + + repo := repositories.NewBunOrganizationInvitationRepository(db) + ctx := context.Background() + + for i := 1; i <= count; i++ { + _, err := repo.Create(ctx, &types.OrganizationInvitation{ + ID: fmt.Sprintf("inv-%d", i), + Email: fmt.Sprintf("user%d@example.com", i), + InviterID: "user-1", + OrganizationID: "org-1", + Role: "member", + Status: types.OrganizationInvitationStatusPending, + ExpiresAt: time.Now().UTC().Add(time.Hour), + }) + require.NoError(t, err) + } + + return repo, ctx + } + + tests := []struct { + name string + seedCount int + organizationID string + expectCount int + }{ + {name: "returns every invitation for the organization", seedCount: 5, organizationID: "org-1", expectCount: 5}, + {name: "organization without invitations is empty", seedCount: 5, organizationID: "org-2", expectCount: 0}, + {name: "unknown organization is empty", seedCount: 5, organizationID: "org-99", expectCount: 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + repo, ctx := setup(t, tt.seedCount) + + invitations, err := repo.GetAllByOrganizationIDWithOrg(ctx, tt.organizationID) + require.NoError(t, err) + require.NotNil(t, invitations, "an empty result must be an empty slice, not nil") + require.Len(t, invitations, tt.expectCount) + for _, invitation := range invitations { + require.NotNil(t, invitation.Invitation) + require.Equal(t, tt.organizationID, invitation.Invitation.OrganizationID) + require.Equal(t, tt.organizationID, invitation.Organization.ID, "the organization must be hydrated by the join") + require.NotEmpty(t, invitation.Organization.Name) + } + }) + } +} + +func TestBunOrganizationInvitationRepository_GetAllByOrganizationIDWithOrgIgnoresTheDefaultLimit(t *testing.T) { + t.Parallel() + + const invitationCount = 25 + + db := plugintests.SetupRepoDB(t) + plugintests.SeedOrganization(t, db, "org-1", "user-1", "Acme Inc", "acme-inc") + repo := repositories.NewBunOrganizationInvitationRepository(db) + ctx := context.Background() + + for i := 1; i <= invitationCount; i++ { + _, err := repo.Create(ctx, &types.OrganizationInvitation{ + ID: fmt.Sprintf("inv-%02d", i), + Email: fmt.Sprintf("user%d@example.com", i), + InviterID: "user-1", + OrganizationID: "org-1", + Role: "member", + Status: types.OrganizationInvitationStatusPending, + ExpiresAt: time.Now().UTC().Add(time.Hour), + }) + require.NoError(t, err) + } + + invitations, err := repo.GetAllByOrganizationIDWithOrg(ctx, "org-1") + require.NoError(t, err) + require.Len(t, invitations, invitationCount) +} diff --git a/plugins/organizations/repositories/bun_organization_member_repository.go b/plugins/organizations/repositories/bun_organization_member_repository.go index c43da1f5..bd4ff531 100644 --- a/plugins/organizations/repositories/bun_organization_member_repository.go +++ b/plugins/organizations/repositories/bun_organization_member_repository.go @@ -99,7 +99,7 @@ func (r *BunOrganizationMemberRepository) GetByID(ctx context.Context, memberID return member, err } -func (r *BunOrganizationMemberRepository) GetAllByOrganizationID(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationMember, int, error) { +func (r *BunOrganizationMemberRepository) ListAllByOrganizationID(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationMember, int, error) { members := make([]types.OrganizationMember, 0) limit = pageLimit(limit) total, err := r.db.NewSelect().Model(&members). @@ -113,7 +113,19 @@ func (r *BunOrganizationMemberRepository) GetAllByOrganizationID(ctx context.Con return members, total, err } -func (r *BunOrganizationMemberRepository) GetAllByOrganizationIDWithUser(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationMemberResponse, int, error) { +func (r *BunOrganizationMemberRepository) GetAllByOrganizationID(ctx context.Context, organizationID string) ([]types.OrganizationMember, error) { + members := make([]types.OrganizationMember, 0) + err := r.db.NewSelect().Model(&members). + Where("organization_id = ?", organizationID). + OrderExpr("created_at DESC, id DESC"). + Scan(ctx) + if err == sql.ErrNoRows { + return []types.OrganizationMember{}, nil + } + return members, err +} + +func (r *BunOrganizationMemberRepository) ListAllByOrganizationIDWithUser(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationMemberResponse, int, error) { limit = pageLimit(limit) var total int @@ -136,6 +148,21 @@ func (r *BunOrganizationMemberRepository) GetAllByOrganizationIDWithUser(ctx con return results, total, nil } +func (r *BunOrganizationMemberRepository) GetAllByOrganizationIDWithUser(ctx context.Context, organizationID string) ([]types.OrganizationMemberResponse, error) { + var rows []memberUserRow + err := r.db.NewRaw(`SELECT `+memberWithUserColumns+memberWithUserByOrganizationFrom+` + ORDER BY m.created_at DESC, m.id DESC + `, organizationID).Scan(ctx, &rows) + if err != nil && err != sql.ErrNoRows { + return nil, err + } + results := make([]types.OrganizationMemberResponse, len(rows)) + for i, row := range rows { + results[i] = mapToMemberResponse(row) + } + return results, nil +} + func (r *BunOrganizationMemberRepository) GetByIDWithUser(ctx context.Context, memberID string) (*types.OrganizationMemberResponse, error) { var row memberUserRow err := r.db.NewRaw(` diff --git a/plugins/organizations/repositories/bun_organization_member_repository_test.go b/plugins/organizations/repositories/bun_organization_member_repository_test.go index e35025a1..cf01a2ab 100644 --- a/plugins/organizations/repositories/bun_organization_member_repository_test.go +++ b/plugins/organizations/repositories/bun_organization_member_repository_test.go @@ -34,7 +34,7 @@ func TestBunOrganizationMemberRepository_CreateGetUpdateDelete(t *testing.T) { name: "list by organization returns created member", run: func(t *testing.T, orgMemberRepo repositories.OrganizationMemberRepository, ctx context.Context, created *types.OrganizationMember) { t.Helper() - members, total, err := orgMemberRepo.GetAllByOrganizationID(ctx, "org-1", 1, 10) + members, total, err := orgMemberRepo.ListAllByOrganizationID(ctx, "org-1", 1, 10) require.NoError(t, err) require.Len(t, members, 1) require.Equal(t, 1, total) @@ -183,7 +183,7 @@ func memberResponseIDs(members []types.OrganizationMemberResponse) []string { return ids } -func TestBunOrganizationMemberRepository_GetAllByOrganizationID(t *testing.T) { +func TestBunOrganizationMemberRepository_ListAllByOrganizationID(t *testing.T) { t.Parallel() tests := []struct { @@ -210,7 +210,7 @@ func TestBunOrganizationMemberRepository_GetAllByOrganizationID(t *testing.T) { repo, ctx := seedMembers(t, 5) - members, total, err := repo.GetAllByOrganizationID(ctx, tt.organizationID, tt.page, tt.limit) + members, total, err := repo.ListAllByOrganizationID(ctx, tt.organizationID, tt.page, tt.limit) require.NoError(t, err) require.Len(t, members, tt.expectCount) require.Equal(t, tt.expectTotal, total) @@ -218,14 +218,14 @@ func TestBunOrganizationMemberRepository_GetAllByOrganizationID(t *testing.T) { } } -func TestBunOrganizationMemberRepository_GetAllByOrganizationIDPagesPartitionCleanly(t *testing.T) { +func TestBunOrganizationMemberRepository_ListAllByOrganizationIDPagesPartitionCleanly(t *testing.T) { t.Parallel() repo, ctx := seedMembers(t, 5) seen := make([]string, 0, 5) for page := 1; page <= 3; page++ { - members, total, err := repo.GetAllByOrganizationID(ctx, "org-1", page, 2) + members, total, err := repo.ListAllByOrganizationID(ctx, "org-1", page, 2) require.NoError(t, err) require.Equal(t, 5, total) seen = append(seen, memberIDs(members)...) @@ -234,7 +234,7 @@ func TestBunOrganizationMemberRepository_GetAllByOrganizationIDPagesPartitionCle require.ElementsMatch(t, []string{"mem-1", "mem-2", "mem-3", "mem-4", "mem-5"}, seen) } -func TestBunOrganizationMemberRepository_GetAllByOrganizationIDWithUser(t *testing.T) { +func TestBunOrganizationMemberRepository_ListAllByOrganizationIDWithUser(t *testing.T) { t.Parallel() tests := []struct { @@ -261,7 +261,7 @@ func TestBunOrganizationMemberRepository_GetAllByOrganizationIDWithUser(t *testi repo, ctx := seedMembers(t, 5) - members, total, err := repo.GetAllByOrganizationIDWithUser(ctx, tt.organizationID, tt.page, tt.limit) + members, total, err := repo.ListAllByOrganizationIDWithUser(ctx, tt.organizationID, tt.page, tt.limit) require.NoError(t, err) require.Len(t, members, tt.expectCount) require.Equal(t, tt.expectTotal, total) @@ -272,14 +272,14 @@ func TestBunOrganizationMemberRepository_GetAllByOrganizationIDWithUser(t *testi } } -func TestBunOrganizationMemberRepository_GetAllByOrganizationIDWithUserPagesPartitionCleanly(t *testing.T) { +func TestBunOrganizationMemberRepository_ListAllByOrganizationIDWithUserPagesPartitionCleanly(t *testing.T) { t.Parallel() repo, ctx := seedMembers(t, 5) seen := make([]string, 0, 5) for page := 1; page <= 3; page++ { - members, total, err := repo.GetAllByOrganizationIDWithUser(ctx, "org-1", page, 2) + members, total, err := repo.ListAllByOrganizationIDWithUser(ctx, "org-1", page, 2) require.NoError(t, err) require.Equal(t, 5, total) seen = append(seen, memberResponseIDs(members)...) @@ -539,3 +539,120 @@ func TestBunOrganizationMemberRepository_WithTx(t *testing.T) { }) } } + +func TestBunOrganizationMemberRepository_GetAllByOrganizationID(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + seedCount int + organizationID string + expectIDs []string + }{ + {name: "returns every member newest first", seedCount: 5, organizationID: "org-1", expectIDs: []string{"mem-5", "mem-4", "mem-3", "mem-2", "mem-1"}}, + {name: "unknown organization is empty", seedCount: 5, organizationID: "org-2", expectIDs: []string{}}, + {name: "organization with no members is empty", seedCount: 0, organizationID: "org-1", expectIDs: []string{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + repo, ctx := seedMembers(t, tt.seedCount) + + members, err := repo.GetAllByOrganizationID(ctx, tt.organizationID) + require.NoError(t, err) + require.NotNil(t, members, "an empty result must be an empty slice, not nil") + require.Equal(t, tt.expectIDs, memberIDs(members)) + }) + } +} + +// The unconstrained fetch must ignore pagination.DefaultLimit entirely. +func TestBunOrganizationMemberRepository_GetAllByOrganizationIDIgnoresTheDefaultLimit(t *testing.T) { + t.Parallel() + + const memberCount = 25 + + repo, ctx := seedMembers(t, memberCount) + + members, err := repo.GetAllByOrganizationID(ctx, "org-1") + require.NoError(t, err) + require.Len(t, members, memberCount) + + withUser, err := repo.GetAllByOrganizationIDWithUser(ctx, "org-1") + require.NoError(t, err) + require.Len(t, withUser, memberCount) +} + +func TestBunOrganizationMemberRepository_GetAllByOrganizationIDWithUser(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + seedCount int + organizationID string + expectIDs []string + }{ + {name: "returns every member newest first", seedCount: 5, organizationID: "org-1", expectIDs: []string{"mem-5", "mem-4", "mem-3", "mem-2", "mem-1"}}, + {name: "unknown organization is empty", seedCount: 5, organizationID: "org-2", expectIDs: []string{}}, + {name: "organization with no members is empty", seedCount: 0, organizationID: "org-1", expectIDs: []string{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + repo, ctx := seedMembers(t, tt.seedCount) + + members, err := repo.GetAllByOrganizationIDWithUser(ctx, tt.organizationID) + require.NoError(t, err) + require.NotNil(t, members, "an empty result must be an empty slice, not nil") + require.Equal(t, tt.expectIDs, memberResponseIDs(members)) + }) + } +} + +// The joined variant must hydrate the user, not just the membership row. +func TestBunOrganizationMemberRepository_GetAllByOrganizationIDWithUserHydratesTheUser(t *testing.T) { + t.Parallel() + + repo, ctx := seedMembers(t, 3) + + members, err := repo.GetAllByOrganizationIDWithUser(ctx, "org-1") + require.NoError(t, err) + require.Len(t, members, 3) + for _, member := range members { + require.NotEmpty(t, member.User.ID) + require.NotEmpty(t, member.User.Email) + require.Equal(t, "org-1", member.OrganizationID) + } +} + +// Members of one organization must never leak into another organization's fetch. +func TestBunOrganizationMemberRepository_GetAllByOrganizationIDScopesToTheOrganization(t *testing.T) { + t.Parallel() + + db := plugintests.SetupRepoDB(t) + plugintests.SeedUsers(t, db, 4) + plugintests.SeedOrganization(t, db, "org-1", "user-1", "Acme Inc", "acme-inc") + plugintests.SeedOrganization(t, db, "org-2", "user-3", "Beta Inc", "beta-inc") + + repo := repositories.NewBunOrganizationMemberRepository(db) + ctx := context.Background() + plugintests.SeedOrganizationMember(t, db, "mem-a1", "org-1", "user-1", "owner") + plugintests.SeedOrganizationMember(t, db, "mem-a2", "org-1", "user-2", "member") + plugintests.SeedOrganizationMember(t, db, "mem-b1", "org-2", "user-3", "owner") + + first, err := repo.GetAllByOrganizationID(ctx, "org-1") + require.NoError(t, err) + require.ElementsMatch(t, []string{"mem-a1", "mem-a2"}, memberIDs(first)) + + second, err := repo.GetAllByOrganizationID(ctx, "org-2") + require.NoError(t, err) + require.ElementsMatch(t, []string{"mem-b1"}, memberIDs(second)) + + joined, err := repo.GetAllByOrganizationIDWithUser(ctx, "org-2") + require.NoError(t, err) + require.ElementsMatch(t, []string{"mem-b1"}, memberResponseIDs(joined)) +} diff --git a/plugins/organizations/repositories/bun_organization_repository.go b/plugins/organizations/repositories/bun_organization_repository.go index d593148b..c7ee6af6 100644 --- a/plugins/organizations/repositories/bun_organization_repository.go +++ b/plugins/organizations/repositories/bun_organization_repository.go @@ -58,7 +58,7 @@ func (r *BunOrganizationRepository) GetBySlug(ctx context.Context, slug string) const organizationAccessibleWhere = `o.owner_id = ? OR EXISTS (` + `SELECT 1 FROM organization_members m WHERE m.organization_id = o.id AND m.user_id = ?)` -func (r *BunOrganizationRepository) GetAllAccessibleByUserID(ctx context.Context, userID string, page int, limit int) ([]types.Organization, int, error) { +func (r *BunOrganizationRepository) ListAllAccessibleByUserID(ctx context.Context, userID string, page int, limit int) ([]types.Organization, int, error) { organizations := make([]types.Organization, 0) limit = pageLimit(limit) total, err := r.db.NewSelect().Model(&organizations). @@ -74,6 +74,31 @@ func (r *BunOrganizationRepository) GetAllAccessibleByUserID(ctx context.Context return organizations, total, err } +func (r *BunOrganizationRepository) GetAllAccessibleByUserID(ctx context.Context, userID string) ([]types.Organization, error) { + organizations := make([]types.Organization, 0) + err := r.db.NewSelect().Model(&organizations). + ModelTableExpr("organizations AS o"). + ColumnExpr("o.*"). + Where(organizationAccessibleWhere, userID, userID). + OrderExpr("o.created_at DESC, o.id DESC"). + Scan(ctx) + if err == sql.ErrNoRows { + return []types.Organization{}, nil + } + return organizations, err +} + +func (r *BunOrganizationRepository) GetAll(ctx context.Context) ([]types.Organization, error) { + organizations := make([]types.Organization, 0) + err := r.db.NewSelect().Model(&organizations). + OrderExpr("created_at DESC, id DESC"). + Scan(ctx) + if err == sql.ErrNoRows { + return []types.Organization{}, nil + } + return organizations, err +} + func (r *BunOrganizationRepository) CountAccessibleByUserID(ctx context.Context, userID string) (int, error) { return r.db.NewSelect(). Model((*types.Organization)(nil)). diff --git a/plugins/organizations/repositories/bun_organization_repository_test.go b/plugins/organizations/repositories/bun_organization_repository_test.go index d76c2728..da30d836 100644 --- a/plugins/organizations/repositories/bun_organization_repository_test.go +++ b/plugins/organizations/repositories/bun_organization_repository_test.go @@ -2,6 +2,7 @@ package repositories_test import ( "context" + "fmt" "testing" "github.com/stretchr/testify/require" @@ -178,7 +179,7 @@ func organizationIDs(organizations []types.Organization) []string { return ids } -func TestBunOrganizationRepository_GetAllAccessibleByUserID(t *testing.T) { +func TestBunOrganizationRepository_ListAllAccessibleByUserID(t *testing.T) { t.Parallel() tests := []struct { @@ -229,7 +230,7 @@ func TestBunOrganizationRepository_GetAllAccessibleByUserID(t *testing.T) { repo, ctx := seedAccessibleOrganizations(t) - found, total, err := repo.GetAllAccessibleByUserID(ctx, tt.userID, tt.page, tt.limit) + found, total, err := repo.ListAllAccessibleByUserID(ctx, tt.userID, tt.page, tt.limit) require.NoError(t, err) require.Equal(t, tt.expectTotal, total) require.ElementsMatch(t, tt.expectedIDs, organizationIDs(found)) @@ -237,14 +238,14 @@ func TestBunOrganizationRepository_GetAllAccessibleByUserID(t *testing.T) { } } -func TestBunOrganizationRepository_GetAllAccessibleByUserIDPagesWithoutLoss(t *testing.T) { +func TestBunOrganizationRepository_ListAllAccessibleByUserIDPagesWithoutLoss(t *testing.T) { t.Parallel() repo, ctx := seedAccessibleOrganizations(t) seen := make([]string, 0, 3) for page := 1; page <= 3; page++ { - found, total, err := repo.GetAllAccessibleByUserID(ctx, "user-1", page, 1) + found, total, err := repo.ListAllAccessibleByUserID(ctx, "user-1", page, 1) require.NoError(t, err) require.Equal(t, 3, total) require.Len(t, found, 1) @@ -277,7 +278,7 @@ func TestBunOrganizationRepository_CountAccessibleByUserID(t *testing.T) { require.NoError(t, err) require.Equal(t, tt.expected, count) - _, total, err := repo.GetAllAccessibleByUserID(ctx, tt.userID, 1, 10) + _, total, err := repo.ListAllAccessibleByUserID(ctx, tt.userID, 1, 10) require.NoError(t, err) require.Equal(t, count, total, "count must agree with the list total") }) @@ -428,3 +429,75 @@ func TestBunOrganizationRepository_WithTx(t *testing.T) { }) } } + +func TestBunOrganizationRepository_GetAllAccessibleByUserID(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + userID string + expectedIDs []string + }{ + {name: "returns owned and joined organizations without duplicates", userID: "user-1", expectedIDs: []string{"org-a", "org-b", "org-c"}}, + {name: "returns only what the other user can reach", userID: "user-2", expectedIDs: []string{"org-b", "org-d"}}, + {name: "unknown user reaches nothing", userID: "user-99", expectedIDs: []string{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + repo, ctx := seedAccessibleOrganizations(t) + + organizations, err := repo.GetAllAccessibleByUserID(ctx, tt.userID) + require.NoError(t, err) + require.NotNil(t, organizations, "an empty result must be an empty slice, not nil") + require.ElementsMatch(t, tt.expectedIDs, organizationIDs(organizations)) + }) + } +} + +// GetAll applies no access filtering at all, unlike GetAllAccessibleByUserID. +func TestBunOrganizationRepository_GetAll(t *testing.T) { + t.Parallel() + + repo, ctx := seedAccessibleOrganizations(t) + + organizations, err := repo.GetAll(ctx) + require.NoError(t, err) + require.ElementsMatch(t, []string{"org-a", "org-b", "org-c", "org-d"}, organizationIDs(organizations)) +} + +func TestBunOrganizationRepository_GetAllOnAnEmptyTable(t *testing.T) { + t.Parallel() + + db := plugintests.SetupRepoDB(t) + repo := repositories.NewBunOrganizationRepository(db) + + organizations, err := repo.GetAll(context.Background()) + require.NoError(t, err) + require.NotNil(t, organizations, "an empty result must be an empty slice, not nil") + require.Empty(t, organizations) +} + +// Neither unconstrained fetch may stop at pagination.DefaultLimit. +func TestBunOrganizationRepository_GetAllIgnoresTheDefaultLimit(t *testing.T) { + t.Parallel() + + const organizationCount = 25 + + db := plugintests.SetupRepoDB(t) + for i := 1; i <= organizationCount; i++ { + plugintests.SeedOrganization(t, db, fmt.Sprintf("org-%02d", i), "user-1", fmt.Sprintf("Org %d", i), fmt.Sprintf("org-%02d", i)) + } + repo := repositories.NewBunOrganizationRepository(db) + ctx := context.Background() + + all, err := repo.GetAll(ctx) + require.NoError(t, err) + require.Len(t, all, organizationCount) + + accessible, err := repo.GetAllAccessibleByUserID(ctx, "user-1") + require.NoError(t, err) + require.Len(t, accessible, organizationCount) +} diff --git a/plugins/organizations/repositories/bun_organization_team_member_repository.go b/plugins/organizations/repositories/bun_organization_team_member_repository.go index 51533ab8..da2c54fa 100644 --- a/plugins/organizations/repositories/bun_organization_team_member_repository.go +++ b/plugins/organizations/repositories/bun_organization_team_member_repository.go @@ -118,7 +118,7 @@ func (r *BunOrganizationTeamMemberRepository) GetByTeamIDAndMemberID(ctx context return teamMember, err } -func (r *BunOrganizationTeamMemberRepository) GetAllByTeamID(ctx context.Context, teamID string, page int, limit int) ([]types.OrganizationTeamMember, int, error) { +func (r *BunOrganizationTeamMemberRepository) ListAllByTeamID(ctx context.Context, teamID string, page int, limit int) ([]types.OrganizationTeamMember, int, error) { teamMembers := make([]types.OrganizationTeamMember, 0) limit = pageLimit(limit) total, err := r.db.NewSelect().Model(&teamMembers). @@ -132,7 +132,19 @@ func (r *BunOrganizationTeamMemberRepository) GetAllByTeamID(ctx context.Context return teamMembers, total, err } -func (r *BunOrganizationTeamMemberRepository) GetAllByTeamIDWithMemberAndUser(ctx context.Context, teamID string, page int, limit int) ([]types.OrganizationTeamMemberResponse, int, error) { +func (r *BunOrganizationTeamMemberRepository) GetAllByTeamID(ctx context.Context, teamID string) ([]types.OrganizationTeamMember, error) { + teamMembers := make([]types.OrganizationTeamMember, 0) + err := r.db.NewSelect().Model(&teamMembers). + Where("team_id = ?", teamID). + OrderExpr("created_at DESC, id DESC"). + Scan(ctx) + if err == sql.ErrNoRows { + return []types.OrganizationTeamMember{}, nil + } + return teamMembers, err +} + +func (r *BunOrganizationTeamMemberRepository) ListAllByTeamIDWithMemberAndUser(ctx context.Context, teamID string, page int, limit int) ([]types.OrganizationTeamMemberResponse, int, error) { limit = pageLimit(limit) var total int @@ -155,6 +167,21 @@ func (r *BunOrganizationTeamMemberRepository) GetAllByTeamIDWithMemberAndUser(ct return results, total, nil } +func (r *BunOrganizationTeamMemberRepository) GetAllByTeamIDWithMemberAndUser(ctx context.Context, teamID string) ([]types.OrganizationTeamMemberResponse, error) { + var rows []teamMemberMemberUserRow + err := r.db.NewRaw(`SELECT `+teamMemberWithMemberAndUserColumns+teamMemberWithMemberAndUserByTeamFrom+` + ORDER BY tm.created_at DESC, tm.id DESC + `, teamID).Scan(ctx, &rows) + if err != nil && err != sql.ErrNoRows { + return nil, err + } + results := make([]types.OrganizationTeamMemberResponse, len(rows)) + for i, row := range rows { + results[i] = mapToTeamMemberResponse(row) + } + return results, nil +} + func (r *BunOrganizationTeamMemberRepository) GetByIDWithMemberAndUser(ctx context.Context, teamMemberID string) (*types.OrganizationTeamMemberResponse, error) { var row teamMemberMemberUserRow err := r.db.NewRaw(` diff --git a/plugins/organizations/repositories/bun_organization_team_member_repository_test.go b/plugins/organizations/repositories/bun_organization_team_member_repository_test.go index 2a20cf10..fca091af 100644 --- a/plugins/organizations/repositories/bun_organization_team_member_repository_test.go +++ b/plugins/organizations/repositories/bun_organization_team_member_repository_test.go @@ -274,7 +274,7 @@ func teamMemberResponseIDs(teamMembers []types.OrganizationTeamMemberResponse) [ return ids } -func TestBunOrganizationTeamMemberRepository_GetAllByTeamID(t *testing.T) { +func TestBunOrganizationTeamMemberRepository_ListAllByTeamID(t *testing.T) { t.Parallel() tests := []struct { @@ -300,7 +300,7 @@ func TestBunOrganizationTeamMemberRepository_GetAllByTeamID(t *testing.T) { repo, ctx := seedTeamMembers(t, 5) - found, total, err := repo.GetAllByTeamID(ctx, tt.teamID, tt.page, tt.limit) + found, total, err := repo.ListAllByTeamID(ctx, tt.teamID, tt.page, tt.limit) require.NoError(t, err) require.Len(t, found, tt.expectCount) require.Equal(t, tt.expectTotal, total) @@ -311,14 +311,14 @@ func TestBunOrganizationTeamMemberRepository_GetAllByTeamID(t *testing.T) { } } -func TestBunOrganizationTeamMemberRepository_GetAllByTeamIDPagesPartitionCleanly(t *testing.T) { +func TestBunOrganizationTeamMemberRepository_ListAllByTeamIDPagesPartitionCleanly(t *testing.T) { t.Parallel() repo, ctx := seedTeamMembers(t, 5) seen := make([]string, 0, 5) for page := 1; page <= 3; page++ { - found, total, err := repo.GetAllByTeamID(ctx, "team-1", page, 2) + found, total, err := repo.ListAllByTeamID(ctx, "team-1", page, 2) require.NoError(t, err) require.Equal(t, 5, total) seen = append(seen, teamMemberIDs(found)...) @@ -327,7 +327,7 @@ func TestBunOrganizationTeamMemberRepository_GetAllByTeamIDPagesPartitionCleanly require.ElementsMatch(t, []string{"team-member-1", "team-member-2", "team-member-3", "team-member-4", "team-member-5"}, seen) } -func TestBunOrganizationTeamMemberRepository_GetAllByTeamIDWithMemberAndUser(t *testing.T) { +func TestBunOrganizationTeamMemberRepository_ListAllByTeamIDWithMemberAndUser(t *testing.T) { t.Parallel() tests := []struct { @@ -353,7 +353,7 @@ func TestBunOrganizationTeamMemberRepository_GetAllByTeamIDWithMemberAndUser(t * repo, ctx := seedTeamMembers(t, 5) - found, total, err := repo.GetAllByTeamIDWithMemberAndUser(ctx, tt.teamID, tt.page, tt.limit) + found, total, err := repo.ListAllByTeamIDWithMemberAndUser(ctx, tt.teamID, tt.page, tt.limit) require.NoError(t, err) require.Len(t, found, tt.expectCount) require.Equal(t, tt.expectTotal, total) @@ -364,14 +364,14 @@ func TestBunOrganizationTeamMemberRepository_GetAllByTeamIDWithMemberAndUser(t * } } -func TestBunOrganizationTeamMemberRepository_GetAllByTeamIDWithMemberAndUserPagesPartitionCleanly(t *testing.T) { +func TestBunOrganizationTeamMemberRepository_ListAllByTeamIDWithMemberAndUserPagesPartitionCleanly(t *testing.T) { t.Parallel() repo, ctx := seedTeamMembers(t, 5) seen := make([]string, 0, 5) for page := 1; page <= 3; page++ { - found, total, err := repo.GetAllByTeamIDWithMemberAndUser(ctx, "team-1", page, 2) + found, total, err := repo.ListAllByTeamIDWithMemberAndUser(ctx, "team-1", page, 2) require.NoError(t, err) require.Equal(t, 5, total) seen = append(seen, teamMemberResponseIDs(found)...) @@ -514,3 +514,92 @@ func TestBunOrganizationTeamMemberRepository_WithTx(t *testing.T) { }) } } + +func TestBunOrganizationTeamMemberRepository_GetAllByTeamID(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + seedCount int + teamID string + expectIDs []string + }{ + {name: "returns every team member newest first", seedCount: 4, teamID: "team-1", expectIDs: []string{"team-member-4", "team-member-3", "team-member-2", "team-member-1"}}, + {name: "unknown team is empty", seedCount: 4, teamID: "team-2", expectIDs: []string{}}, + {name: "team with no members is empty", seedCount: 0, teamID: "team-1", expectIDs: []string{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + repo, ctx := seedTeamMembers(t, tt.seedCount) + + teamMembers, err := repo.GetAllByTeamID(ctx, tt.teamID) + require.NoError(t, err) + require.NotNil(t, teamMembers, "an empty result must be an empty slice, not nil") + require.Equal(t, tt.expectIDs, teamMemberIDs(teamMembers)) + }) + } +} + +func TestBunOrganizationTeamMemberRepository_GetAllByTeamIDWithMemberAndUser(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + seedCount int + teamID string + expectIDs []string + }{ + {name: "returns every team member newest first", seedCount: 4, teamID: "team-1", expectIDs: []string{"team-member-4", "team-member-3", "team-member-2", "team-member-1"}}, + {name: "unknown team is empty", seedCount: 4, teamID: "team-2", expectIDs: []string{}}, + {name: "team with no members is empty", seedCount: 0, teamID: "team-1", expectIDs: []string{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + repo, ctx := seedTeamMembers(t, tt.seedCount) + + teamMembers, err := repo.GetAllByTeamIDWithMemberAndUser(ctx, tt.teamID) + require.NoError(t, err) + require.NotNil(t, teamMembers, "an empty result must be an empty slice, not nil") + require.Equal(t, tt.expectIDs, teamMemberResponseIDs(teamMembers)) + }) + } +} + +// The joined variant must hydrate both the membership and its user. +func TestBunOrganizationTeamMemberRepository_GetAllByTeamIDWithMemberAndUserHydratesRelations(t *testing.T) { + t.Parallel() + + repo, ctx := seedTeamMembers(t, 3) + + teamMembers, err := repo.GetAllByTeamIDWithMemberAndUser(ctx, "team-1") + require.NoError(t, err) + require.Len(t, teamMembers, 3) + for _, teamMember := range teamMembers { + require.Equal(t, "team-1", teamMember.TeamID) + require.NotEmpty(t, teamMember.Member.ID) + require.NotEmpty(t, teamMember.Member.User.ID) + require.NotEmpty(t, teamMember.Member.User.Email) + } +} + +func TestBunOrganizationTeamMemberRepository_GetAllByTeamIDIgnoresTheDefaultLimit(t *testing.T) { + t.Parallel() + + const teamMemberCount = 25 + + repo, ctx := seedTeamMembers(t, teamMemberCount) + + teamMembers, err := repo.GetAllByTeamID(ctx, "team-1") + require.NoError(t, err) + require.Len(t, teamMembers, teamMemberCount) + + joined, err := repo.GetAllByTeamIDWithMemberAndUser(ctx, "team-1") + require.NoError(t, err) + require.Len(t, joined, teamMemberCount) +} diff --git a/plugins/organizations/repositories/bun_organization_team_repository.go b/plugins/organizations/repositories/bun_organization_team_repository.go index 82ec1f10..b0f3bca2 100644 --- a/plugins/organizations/repositories/bun_organization_team_repository.go +++ b/plugins/organizations/repositories/bun_organization_team_repository.go @@ -57,7 +57,7 @@ func (r *BunOrganizationTeamRepository) GetByOrganizationIDAndSlug(ctx context.C return team, err } -func (r *BunOrganizationTeamRepository) GetAllByOrganizationID(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationTeam, int, error) { +func (r *BunOrganizationTeamRepository) ListAllByOrganizationID(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationTeam, int, error) { teams := make([]types.OrganizationTeam, 0) limit = pageLimit(limit) total, err := r.db.NewSelect().Model(&teams). @@ -71,6 +71,18 @@ func (r *BunOrganizationTeamRepository) GetAllByOrganizationID(ctx context.Conte return teams, total, err } +func (r *BunOrganizationTeamRepository) GetAllByOrganizationID(ctx context.Context, organizationID string) ([]types.OrganizationTeam, error) { + teams := make([]types.OrganizationTeam, 0) + err := r.db.NewSelect().Model(&teams). + Where("organization_id = ?", organizationID). + OrderExpr("created_at DESC, id DESC"). + Scan(ctx) + if err == sql.ErrNoRows { + return []types.OrganizationTeam{}, nil + } + return teams, err +} + func (r *BunOrganizationTeamRepository) Update(ctx context.Context, team *types.OrganizationTeam) (*types.OrganizationTeam, error) { err := r.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { _, err := tx.NewUpdate().Model(team).WherePK().Exec(ctx) diff --git a/plugins/organizations/repositories/bun_organization_team_repository_test.go b/plugins/organizations/repositories/bun_organization_team_repository_test.go index df3c9977..c9bb2823 100644 --- a/plugins/organizations/repositories/bun_organization_team_repository_test.go +++ b/plugins/organizations/repositories/bun_organization_team_repository_test.go @@ -188,7 +188,7 @@ func teamIDs(teams []types.OrganizationTeam) []string { return ids } -func TestBunOrganizationTeamRepository_GetAllByOrganizationID(t *testing.T) { +func TestBunOrganizationTeamRepository_ListAllByOrganizationID(t *testing.T) { t.Parallel() tests := []struct { @@ -214,7 +214,7 @@ func TestBunOrganizationTeamRepository_GetAllByOrganizationID(t *testing.T) { repo, ctx := seedTeams(t, 5) - found, total, err := repo.GetAllByOrganizationID(ctx, tt.organizationID, tt.page, tt.limit) + found, total, err := repo.ListAllByOrganizationID(ctx, tt.organizationID, tt.page, tt.limit) require.NoError(t, err) require.Len(t, found, tt.expectCount) require.Equal(t, tt.expectTotal, total) @@ -222,14 +222,14 @@ func TestBunOrganizationTeamRepository_GetAllByOrganizationID(t *testing.T) { } } -func TestBunOrganizationTeamRepository_GetAllByOrganizationIDPagesPartitionCleanly(t *testing.T) { +func TestBunOrganizationTeamRepository_ListAllByOrganizationIDPagesPartitionCleanly(t *testing.T) { t.Parallel() repo, ctx := seedTeams(t, 5) seen := make([]string, 0, 5) for page := 1; page <= 3; page++ { - found, total, err := repo.GetAllByOrganizationID(ctx, "org-1", page, 2) + found, total, err := repo.ListAllByOrganizationID(ctx, "org-1", page, 2) require.NoError(t, err) require.Equal(t, 5, total) seen = append(seen, teamIDs(found)...) @@ -383,3 +383,43 @@ func TestBunOrganizationTeamRepository_WithTx(t *testing.T) { }) } } + +func TestBunOrganizationTeamRepository_GetAllByOrganizationID(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + seedCount int + organizationID string + expectIDs []string + }{ + {name: "returns every team newest first", seedCount: 5, organizationID: "org-1", expectIDs: []string{"team-5", "team-4", "team-3", "team-2", "team-1"}}, + {name: "unknown organization is empty", seedCount: 5, organizationID: "org-2", expectIDs: []string{}}, + {name: "organization with no teams is empty", seedCount: 0, organizationID: "org-1", expectIDs: []string{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + repo, ctx := seedTeams(t, tt.seedCount) + + teams, err := repo.GetAllByOrganizationID(ctx, tt.organizationID) + require.NoError(t, err) + require.NotNil(t, teams, "an empty result must be an empty slice, not nil") + require.Equal(t, tt.expectIDs, teamIDs(teams)) + }) + } +} + +func TestBunOrganizationTeamRepository_GetAllByOrganizationIDIgnoresTheDefaultLimit(t *testing.T) { + t.Parallel() + + const teamCount = 25 + + repo, ctx := seedTeams(t, teamCount) + + teams, err := repo.GetAllByOrganizationID(ctx, "org-1") + require.NoError(t, err) + require.Len(t, teams, teamCount) +} diff --git a/plugins/organizations/repositories/interfaces.go b/plugins/organizations/repositories/interfaces.go index 5a684e00..602dd2dc 100644 --- a/plugins/organizations/repositories/interfaces.go +++ b/plugins/organizations/repositories/interfaces.go @@ -12,7 +12,11 @@ type OrganizationRepository interface { Create(ctx context.Context, organization *types.Organization) (*types.Organization, error) GetByID(ctx context.Context, organizationID string) (*types.Organization, error) GetBySlug(ctx context.Context, slug string) (*types.Organization, error) - GetAllAccessibleByUserID(ctx context.Context, userID string, page int, limit int) ([]types.Organization, int, error) + ListAllAccessibleByUserID(ctx context.Context, userID string, page int, limit int) ([]types.Organization, int, error) + GetAllAccessibleByUserID(ctx context.Context, userID string) ([]types.Organization, error) + // GetAll returns every organization in the system. It applies no access + // filtering, so callers are responsible for authorizing the request. + GetAll(ctx context.Context) ([]types.Organization, error) CountAccessibleByUserID(ctx context.Context, userID string) (int, error) Update(ctx context.Context, organization *types.Organization) (*types.Organization, error) Delete(ctx context.Context, organizationID string) error @@ -24,8 +28,10 @@ type OrganizationInvitationRepository interface { GetByID(ctx context.Context, invitationID string) (*types.OrganizationInvitation, error) GetByIDWithOrg(ctx context.Context, invitationID string) (*types.GetOrganizationInvitationResponse, error) GetByOrganizationIDAndEmail(ctx context.Context, organizationID string, email string, status ...types.OrganizationInvitationStatus) (*types.OrganizationInvitation, error) - GetAllByOrganizationIDWithOrg(ctx context.Context, organizationID string, page int, limit int) ([]types.GetOrganizationInvitationResponse, int, error) - GetAllPendingByEmail(ctx context.Context, email string, limit int) ([]types.OrganizationInvitation, error) + ListAllByOrganizationIDWithOrg(ctx context.Context, organizationID string, page int, limit int) ([]types.GetOrganizationInvitationResponse, int, error) + GetAllByOrganizationIDWithOrg(ctx context.Context, organizationID string) ([]types.GetOrganizationInvitationResponse, error) + ListAllPendingByEmail(ctx context.Context, email string, page int, limit int) ([]types.OrganizationInvitation, int, error) + GetAllPendingByEmail(ctx context.Context, email string) ([]types.OrganizationInvitation, error) Update(ctx context.Context, invitation *types.OrganizationInvitation) (*types.OrganizationInvitation, error) CountByOrganizationIDAndEmail(ctx context.Context, organizationID string, email string) (int, error) WithTx(tx bun.IDB) OrganizationInvitationRepository @@ -34,8 +40,10 @@ type OrganizationInvitationRepository interface { type OrganizationMemberRepository interface { Create(ctx context.Context, member *types.OrganizationMember) (*types.OrganizationMember, error) CountByOrganizationID(ctx context.Context, organizationID string) (int, error) - GetAllByOrganizationID(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationMember, int, error) - GetAllByOrganizationIDWithUser(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationMemberResponse, int, error) + ListAllByOrganizationID(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationMember, int, error) + GetAllByOrganizationID(ctx context.Context, organizationID string) ([]types.OrganizationMember, error) + ListAllByOrganizationIDWithUser(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationMemberResponse, int, error) + GetAllByOrganizationIDWithUser(ctx context.Context, organizationID string) ([]types.OrganizationMemberResponse, error) GetByID(ctx context.Context, memberID string) (*types.OrganizationMember, error) GetByIDWithUser(ctx context.Context, memberID string) (*types.OrganizationMemberResponse, error) GetByOrganizationIDAndUserID(ctx context.Context, organizationID string, userID string) (*types.OrganizationMember, error) @@ -49,7 +57,8 @@ type OrganizationTeamRepository interface { Create(ctx context.Context, team *types.OrganizationTeam) (*types.OrganizationTeam, error) GetByID(ctx context.Context, teamID string) (*types.OrganizationTeam, error) GetByOrganizationIDAndSlug(ctx context.Context, organizationID, slug string) (*types.OrganizationTeam, error) - GetAllByOrganizationID(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationTeam, int, error) + ListAllByOrganizationID(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationTeam, int, error) + GetAllByOrganizationID(ctx context.Context, organizationID string) ([]types.OrganizationTeam, error) Update(ctx context.Context, team *types.OrganizationTeam) (*types.OrganizationTeam, error) Delete(ctx context.Context, teamID string) error WithTx(tx bun.IDB) OrganizationTeamRepository @@ -59,8 +68,10 @@ type OrganizationTeamMemberRepository interface { Create(ctx context.Context, teamMember *types.OrganizationTeamMember) (*types.OrganizationTeamMember, error) GetByID(ctx context.Context, teamMemberID string) (*types.OrganizationTeamMember, error) GetByTeamIDAndMemberID(ctx context.Context, teamID, memberID string) (*types.OrganizationTeamMember, error) - GetAllByTeamID(ctx context.Context, teamID string, page int, limit int) ([]types.OrganizationTeamMember, int, error) - GetAllByTeamIDWithMemberAndUser(ctx context.Context, teamID string, page int, limit int) ([]types.OrganizationTeamMemberResponse, int, error) + ListAllByTeamID(ctx context.Context, teamID string, page int, limit int) ([]types.OrganizationTeamMember, int, error) + GetAllByTeamID(ctx context.Context, teamID string) ([]types.OrganizationTeamMember, error) + ListAllByTeamIDWithMemberAndUser(ctx context.Context, teamID string, page int, limit int) ([]types.OrganizationTeamMemberResponse, int, error) + GetAllByTeamIDWithMemberAndUser(ctx context.Context, teamID string) ([]types.OrganizationTeamMemberResponse, error) GetByIDWithMemberAndUser(ctx context.Context, teamMemberID string) (*types.OrganizationTeamMemberResponse, error) DeleteByTeamIDAndMemberID(ctx context.Context, teamID, memberID string) error WithTx(tx bun.IDB) OrganizationTeamMemberRepository diff --git a/plugins/organizations/repositories/pagination.go b/plugins/organizations/repositories/pagination.go index 27237f42..cf73a77d 100644 --- a/plugins/organizations/repositories/pagination.go +++ b/plugins/organizations/repositories/pagination.go @@ -4,8 +4,6 @@ import ( "github.com/Authula/authula/core/pagination" ) -const MaxPendingInvitationsPerBatch = 500 - func pageLimit(limit int) int { if limit <= 0 { return pagination.DefaultLimit diff --git a/plugins/organizations/routes.go b/plugins/organizations/routes.go index 5d32bd27..cc2852bd 100644 --- a/plugins/organizations/routes.go +++ b/plugins/organizations/routes.go @@ -10,33 +10,33 @@ import ( func Routes(plugin *OrganizationsPlugin) []models.Route { createOrganizationHandler := &handlers.CreateOrganizationHandler{UseCases: plugin.useCases} - getAllOrganizationsHandler := &handlers.GetAllOrganizationsHandler{UseCases: plugin.useCases} + listAllOrganizationsHandler := &handlers.ListAllOrganizationsHandler{UseCases: plugin.useCases} getOrganizationByIDHandler := &handlers.GetOrganizationByIDHandler{UseCases: plugin.useCases} updateOrganizationHandler := &handlers.UpdateOrganizationHandler{UseCases: plugin.useCases} deleteOrganizationHandler := &handlers.DeleteOrganizationHandler{UseCases: plugin.useCases} createInvitationHandler := &handlers.CreateOrganizationInvitationHandler{UseCases: plugin.useCases} getInvitationHandler := &handlers.GetOrganizationInvitationHandler{UseCases: plugin.useCases} - getAllInvitationsHandler := &handlers.GetAllOrganizationInvitationsHandler{UseCases: plugin.useCases} + listAllInvitationsHandler := &handlers.ListAllOrganizationInvitationsHandler{UseCases: plugin.useCases} revokeInvitationHandler := &handlers.RevokeOrganizationInvitationHandler{UseCases: plugin.useCases} acceptInvitationHandler := &handlers.AcceptOrganizationInvitationHandler{UseCases: plugin.useCases, TrustedOrigins: plugin.globalConfig.Security.TrustedOrigins} rejectInvitationHandler := &handlers.RejectOrganizationInvitationHandler{UseCases: plugin.useCases} addMemberHandler := &handlers.AddOrganizationMemberHandler{UseCases: plugin.useCases} - getAllMembersHandler := &handlers.GetAllOrganizationMembersHandler{UseCases: plugin.useCases} + listAllMembersHandler := &handlers.ListAllOrganizationMembersHandler{UseCases: plugin.useCases} getMemberHandler := &handlers.GetOrganizationMemberHandler{UseCases: plugin.useCases} getMemberByUserIDHandler := &handlers.GetOrganizationMemberByUserIDHandler{UseCases: plugin.useCases} updateMemberHandler := &handlers.UpdateOrganizationMemberHandler{UseCases: plugin.useCases} deleteMemberHandler := &handlers.DeleteOrganizationMemberHandler{UseCases: plugin.useCases} createTeamHandler := &handlers.CreateOrganizationTeamHandler{UseCases: plugin.useCases} - getAllTeamsHandler := &handlers.GetAllOrganizationTeamsHandler{UseCases: plugin.useCases} + listAllTeamsHandler := &handlers.ListAllOrganizationTeamsHandler{UseCases: plugin.useCases} getTeamHandler := &handlers.GetOrganizationTeamHandler{UseCases: plugin.useCases} updateTeamHandler := &handlers.UpdateOrganizationTeamHandler{UseCases: plugin.useCases} deleteTeamHandler := &handlers.DeleteOrganizationTeamHandler{UseCases: plugin.useCases} addTeamMemberHandler := &handlers.AddOrganizationTeamMemberHandler{UseCases: plugin.useCases} - getAllTeamMembersHandler := &handlers.GetAllOrganizationTeamMembersHandler{UseCases: plugin.useCases} + listAllTeamMembersHandler := &handlers.ListAllOrganizationTeamMembersHandler{UseCases: plugin.useCases} getTeamMemberHandler := &handlers.GetOrganizationTeamMemberHandler{UseCases: plugin.useCases} deleteTeamMemberHandler := &handlers.DeleteOrganizationTeamMemberHandler{UseCases: plugin.useCases} @@ -56,7 +56,7 @@ func Routes(plugin *OrganizationsPlugin) []models.Route { Middleware: []func(http.Handler) http.Handler{ middleware.RequireAuthenticated(), }, - Handler: getAllOrganizationsHandler.Handle(), + Handler: listAllOrganizationsHandler.Handle(), }, { Method: http.MethodGet, @@ -97,7 +97,7 @@ func Routes(plugin *OrganizationsPlugin) []models.Route { Middleware: []func(http.Handler) http.Handler{ middleware.RequireAuthenticated(), }, - Handler: getAllInvitationsHandler.Handle(), + Handler: listAllInvitationsHandler.Handle(), }, { Method: http.MethodGet, @@ -146,7 +146,7 @@ func Routes(plugin *OrganizationsPlugin) []models.Route { Middleware: []func(http.Handler) http.Handler{ middleware.RequireAuthenticated(), }, - Handler: getAllMembersHandler.Handle(), + Handler: listAllMembersHandler.Handle(), }, { Method: http.MethodGet, @@ -195,7 +195,7 @@ func Routes(plugin *OrganizationsPlugin) []models.Route { Middleware: []func(http.Handler) http.Handler{ middleware.RequireAuthenticated(), }, - Handler: getAllTeamsHandler.Handle(), + Handler: listAllTeamsHandler.Handle(), }, { Method: http.MethodGet, @@ -236,7 +236,7 @@ func Routes(plugin *OrganizationsPlugin) []models.Route { Middleware: []func(http.Handler) http.Handler{ middleware.RequireAuthenticated(), }, - Handler: getAllTeamMembersHandler.Handle(), + Handler: listAllTeamMembersHandler.Handle(), }, { Method: http.MethodGet, diff --git a/plugins/organizations/services/interfaces.go b/plugins/organizations/services/interfaces.go index cea5fb2c..9999860a 100644 --- a/plugins/organizations/services/interfaces.go +++ b/plugins/organizations/services/interfaces.go @@ -10,7 +10,13 @@ import ( type OrganizationService interface { CreateOrganization(ctx context.Context, actor *models.Actor, request types.CreateOrganizationRequest) (*types.Organization, error) - GetAllOrganizations(ctx context.Context, actor *models.Actor, params pagination.Params) (*types.ListOrganizationsResponse, error) + ListAllOrganizations(ctx context.Context, actor *models.Actor, params pagination.Params) (*types.ListOrganizationsResponse, error) + GetAllOrganizations(ctx context.Context, actor *models.Actor) ([]types.Organization, error) + // GetAllOrganizationsUnscoped returns every organization in the system, + // ignoring the caller entirely. It is intended for administrative and + // system callers and must never be wired to an HTTP route without a + // permission check in front of it. + GetAllOrganizationsUnscoped(ctx context.Context) ([]types.Organization, error) GetOrganizationByID(ctx context.Context, actor *models.Actor, organizationID string) (*types.Organization, error) UpdateOrganization(ctx context.Context, actor *models.Actor, organizationID string, request types.UpdateOrganizationRequest) (*types.Organization, error) DeleteOrganization(ctx context.Context, actor *models.Actor, organizationID string) error @@ -22,7 +28,8 @@ type OrganizationInvitationService interface { GetOrganizationInvitation(ctx context.Context, actor *models.Actor, organizationID string, invitationID string) (*types.OrganizationInvitation, error) GetOrganizationInvitationByID(ctx context.Context, invitationID string) (*types.OrganizationInvitation, error) GetOrganizationInvitationByIDWithOrg(ctx context.Context, invitationID string) (*types.GetOrganizationInvitationResponse, error) - GetAllOrganizationInvitationsByOrgIDWithOrg(ctx context.Context, organizationID string, params pagination.Params) (*types.ListOrganizationInvitationsResponse, error) + ListAllOrganizationInvitationsByOrgIDWithOrg(ctx context.Context, organizationID string, params pagination.Params) (*types.ListOrganizationInvitationsResponse, error) + GetAllOrganizationInvitationsByOrgIDWithOrg(ctx context.Context, organizationID string) ([]types.GetOrganizationInvitationResponse, error) RevokeOrganizationInvitation(ctx context.Context, actor *models.Actor, organizationID string, invitationID string) (*types.OrganizationInvitation, error) AcceptOrganizationInvitation(ctx context.Context, actor *models.Actor, organizationID string, invitationID string) (*types.OrganizationInvitation, error) RejectOrganizationInvitation(ctx context.Context, actor *models.Actor, organizationID string, invitationID string) (*types.OrganizationInvitation, error) @@ -30,7 +37,8 @@ type OrganizationInvitationService interface { type OrganizationMemberService interface { AddMember(ctx context.Context, actor *models.Actor, organizationID string, request types.AddOrganizationMemberRequest) (*types.OrganizationMember, error) - GetAllMembers(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationMembersResponse, error) + ListAllMembers(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationMembersResponse, error) + GetAllMembers(ctx context.Context, actor *models.Actor, organizationID string) ([]types.OrganizationMemberResponse, error) GetMember(ctx context.Context, actor *models.Actor, organizationID string, memberID string) (*types.OrganizationMemberResponse, error) GetMemberByUserID(ctx context.Context, actor *models.Actor, organizationID string, userID string) (*types.OrganizationMemberResponse, error) UpdateMember(ctx context.Context, actor *models.Actor, organizationID string, memberID string, request types.UpdateOrganizationMemberRequest) (*types.OrganizationMember, error) @@ -39,7 +47,8 @@ type OrganizationMemberService interface { type OrganizationTeamService interface { CreateTeam(ctx context.Context, actor *models.Actor, organizationID string, request types.CreateOrganizationTeamRequest) (*types.OrganizationTeam, error) - GetAllTeams(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationTeamsResponse, error) + ListAllTeams(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationTeamsResponse, error) + GetAllTeams(ctx context.Context, actor *models.Actor, organizationID string) ([]types.OrganizationTeam, error) GetTeam(ctx context.Context, actor *models.Actor, organizationID string, teamID string) (*types.OrganizationTeam, error) UpdateTeam(ctx context.Context, actor *models.Actor, organizationID string, teamID string, request types.UpdateOrganizationTeamRequest) (*types.OrganizationTeam, error) DeleteTeam(ctx context.Context, actor *models.Actor, organizationID string, teamID string) error @@ -47,7 +56,8 @@ type OrganizationTeamService interface { type OrganizationTeamMemberService interface { AddTeamMember(ctx context.Context, actor *models.Actor, organizationID string, teamID string, request types.AddOrganizationTeamMemberRequest) (*types.OrganizationTeamMember, error) - GetAllTeamMembers(ctx context.Context, actor *models.Actor, organizationID string, teamID string, params pagination.Params) (*types.ListOrganizationTeamMembersResponse, error) + ListAllTeamMembers(ctx context.Context, actor *models.Actor, organizationID string, teamID string, params pagination.Params) (*types.ListOrganizationTeamMembersResponse, error) + GetAllTeamMembers(ctx context.Context, actor *models.Actor, organizationID string, teamID string) ([]types.OrganizationTeamMemberResponse, error) GetTeamMember(ctx context.Context, actor *models.Actor, organizationID string, teamID string, memberID string) (*types.OrganizationTeamMemberResponse, error) RemoveTeamMember(ctx context.Context, actor *models.Actor, organizationID string, teamID string, memberID string) error } diff --git a/plugins/organizations/services/organization_invitation_service.go b/plugins/organizations/services/organization_invitation_service.go index ec8a9c47..d1daa348 100644 --- a/plugins/organizations/services/organization_invitation_service.go +++ b/plugins/organizations/services/organization_invitation_service.go @@ -309,14 +309,14 @@ func (s *organizationInvitationService) GetOrganizationInvitationByIDWithOrg(ctx return resp, nil } -func (s *organizationInvitationService) GetAllOrganizationInvitationsByOrgIDWithOrg(ctx context.Context, organizationID string, params pagination.Params) (*types.ListOrganizationInvitationsResponse, error) { +func (s *organizationInvitationService) ListAllOrganizationInvitationsByOrgIDWithOrg(ctx context.Context, organizationID string, params pagination.Params) (*types.ListOrganizationInvitationsResponse, error) { if organizationID == "" { return nil, coreerrors.ErrNotFound } params = pagination.Clamp(params) - invitations, total, err := s.orgInvitationRepo.GetAllByOrganizationIDWithOrg(ctx, organizationID, params.Page, params.Limit) + invitations, total, err := s.orgInvitationRepo.ListAllByOrganizationIDWithOrg(ctx, organizationID, params.Page, params.Limit) if err != nil { return nil, err } @@ -330,6 +330,22 @@ func (s *organizationInvitationService) GetAllOrganizationInvitationsByOrgIDWith }, nil } +func (s *organizationInvitationService) GetAllOrganizationInvitationsByOrgIDWithOrg(ctx context.Context, organizationID string) ([]types.GetOrganizationInvitationResponse, error) { + if organizationID == "" { + return nil, coreerrors.ErrNotFound + } + + invitations, err := s.orgInvitationRepo.GetAllByOrganizationIDWithOrg(ctx, organizationID) + if err != nil { + return nil, err + } + if invitations == nil { + invitations = []types.GetOrganizationInvitationResponse{} + } + + return invitations, nil +} + func (s *organizationInvitationService) RevokeOrganizationInvitation(ctx context.Context, actor *models.Actor, organizationID string, invitationID string) (*types.OrganizationInvitation, error) { if actor == nil || actor.ID == "" || organizationID == "" || invitationID == "" { return nil, coreerrors.ErrUnauthorized @@ -494,20 +510,13 @@ func (s *organizationInvitationService) AcceptPendingOrganizationInvitationsForE } } - pendingInvitations, err := s.orgInvitationRepo.GetAllPendingByEmail(ctx, email, repositories.MaxPendingInvitationsPerBatch) + pendingInvitations, err := s.orgInvitationRepo.GetAllPendingByEmail(ctx, email) if err != nil { return nil, err } if len(pendingInvitations) == 0 { return []types.OrganizationInvitation{}, nil } - if len(pendingInvitations) == repositories.MaxPendingInvitationsPerBatch { - s.logger.Warn( - "pending organization invitation batch hit the per-call cap; remaining invitations will be accepted on the next call", - "email", email, - "limit", repositories.MaxPendingInvitationsPerBatch, - ) - } return s.acceptOrganizationInvitations(ctx, userID, pendingInvitations) } diff --git a/plugins/organizations/services/organization_invitation_service_test.go b/plugins/organizations/services/organization_invitation_service_test.go index ef3c0d21..addab64b 100644 --- a/plugins/organizations/services/organization_invitation_service_test.go +++ b/plugins/organizations/services/organization_invitation_service_test.go @@ -3,6 +3,7 @@ package services import ( "context" "errors" + "fmt" "strings" "sync" "testing" @@ -721,7 +722,7 @@ func TestOrganizationInvitationService_GetOrganizationInvitation(t *testing.T) { } } -func TestOrganizationInvitationService_GetAllOrganizationInvitationsByOrgIDWithOrg(t *testing.T) { +func TestOrganizationInvitationService_ListAllOrganizationInvitationsByOrgIDWithOrg(t *testing.T) { t.Parallel() repoErr := errors.New("repository error") @@ -740,7 +741,7 @@ func TestOrganizationInvitationService_GetAllOrganizationInvitationsByOrgIDWithO organizationID: "org-1", params: pagination.Params{Page: 2, Limit: 2}, setup: func(invRepo *orgtests.MockOrganizationInvitationRepository) { - invRepo.On("GetAllByOrganizationIDWithOrg", mock.Anything, "org-1", 2, 2). + invRepo.On("ListAllByOrganizationIDWithOrg", mock.Anything, "org-1", 2, 2). Return([]types.GetOrganizationInvitationResponse{ {Invitation: &types.OrganizationInvitation{ID: "inv-3", OrganizationID: "org-1"}}, {Invitation: &types.OrganizationInvitation{ID: "inv-4", OrganizationID: "org-1"}}, @@ -754,7 +755,7 @@ func TestOrganizationInvitationService_GetAllOrganizationInvitationsByOrgIDWithO organizationID: "org-1", params: pagination.Params{Page: -4, Limit: 5000}, setup: func(invRepo *orgtests.MockOrganizationInvitationRepository) { - invRepo.On("GetAllByOrganizationIDWithOrg", mock.Anything, "org-1", 1, 5000). + invRepo.On("ListAllByOrganizationIDWithOrg", mock.Anything, "org-1", 1, 5000). Return([]types.GetOrganizationInvitationResponse{}, 0, nil).Once() }, expectLen: 0, @@ -765,7 +766,7 @@ func TestOrganizationInvitationService_GetAllOrganizationInvitationsByOrgIDWithO organizationID: "org-1", params: pagination.Params{Page: 1, Limit: 10}, setup: func(invRepo *orgtests.MockOrganizationInvitationRepository) { - invRepo.On("GetAllByOrganizationIDWithOrg", mock.Anything, "org-1", 1, 10). + invRepo.On("ListAllByOrganizationIDWithOrg", mock.Anything, "org-1", 1, 10). Return(([]types.GetOrganizationInvitationResponse)(nil), 0, nil).Once() }, expectLen: 0, @@ -782,7 +783,7 @@ func TestOrganizationInvitationService_GetAllOrganizationInvitationsByOrgIDWithO organizationID: "org-1", params: pagination.Params{Page: 1, Limit: 10}, setup: func(invRepo *orgtests.MockOrganizationInvitationRepository) { - invRepo.On("GetAllByOrganizationIDWithOrg", mock.Anything, "org-1", 1, 10). + invRepo.On("ListAllByOrganizationIDWithOrg", mock.Anything, "org-1", 1, 10). Return(([]types.GetOrganizationInvitationResponse)(nil), 0, repoErr).Once() }, expectErr: repoErr, @@ -805,7 +806,7 @@ func TestOrganizationInvitationService_GetAllOrganizationInvitationsByOrgIDWithO } svc := newTestOrganizationInvitationService(&orgtests.MockOrganizationInvitationTxRunner{}, pluginConfig, &internaltests.MockUserService{}, orgtests.NewAccessControlServiceStub(), orgRepo, invRepo, memberRepo) - resp, err := svc.GetAllOrganizationInvitationsByOrgIDWithOrg(context.Background(), tt.organizationID, tt.params) + resp, err := svc.ListAllOrganizationInvitationsByOrgIDWithOrg(context.Background(), tt.organizationID, tt.params) if tt.expectErr != nil { require.Error(t, err) require.ErrorIs(t, err, tt.expectErr) @@ -955,7 +956,7 @@ func TestOrganizationInvitationService_AcceptPendingOrganizationInvitationsForEm userID: "user-2", email: "USER@EXAMPLE.COM", setup: func(userSvc *internaltests.MockUserService, orgRepo *orgtests.MockOrganizationRepository, invRepo *orgtests.MockOrganizationInvitationRepository, memberRepo *orgtests.MockOrganizationMemberRepository, hooks *orgtests.MockOrganizationInvitationHooks, memberHooks *orgtests.MockOrganizationMemberHooks) { - invRepo.On("GetAllPendingByEmail", mock.Anything, "user@example.com", repositories.MaxPendingInvitationsPerBatch).Return([]types.OrganizationInvitation{{ID: "inv-1", OrganizationID: "org-1", Email: "user@example.com", Role: "member", Status: types.OrganizationInvitationStatusPending, ExpiresAt: time.Now().UTC().Add(time.Hour)}}, nil).Once() + invRepo.On("GetAllPendingByEmail", mock.Anything, "user@example.com").Return([]types.OrganizationInvitation{{ID: "inv-1", OrganizationID: "org-1", Email: "user@example.com", Role: "member", Status: types.OrganizationInvitationStatusPending, ExpiresAt: time.Now().UTC().Add(time.Hour)}}, nil).Once() memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-2").Return(nil, nil).Once() memberRepo.On("Create", mock.Anything, mock.MatchedBy(func(member *types.OrganizationMember) bool { return member != nil && member.OrganizationID == "org-1" && member.UserID == "user-2" && member.Role == "member" @@ -1425,3 +1426,136 @@ func TestOrganizationInvitationService_RejectOrganizationInvitation(t *testing.T }) } } + +func TestOrganizationInvitationService_GetAllOrganizationInvitationsByOrgIDWithOrg(t *testing.T) { + t.Parallel() + + repoErr := errors.New("repository error") + + tests := []struct { + name string + organizationID string + setup func(*orgtests.MockOrganizationInvitationRepository) + expectErr error + expectIDs []string + }{ + { + name: "returns every invitation for the organization", + organizationID: "org-1", + setup: func(invRepo *orgtests.MockOrganizationInvitationRepository) { + invRepo.On("GetAllByOrganizationIDWithOrg", mock.Anything, "org-1"). + Return([]types.GetOrganizationInvitationResponse{ + {Invitation: &types.OrganizationInvitation{ID: "inv-1", OrganizationID: "org-1"}}, + {Invitation: &types.OrganizationInvitation{ID: "inv-2", OrganizationID: "org-1"}}, + {Invitation: &types.OrganizationInvitation{ID: "inv-3", OrganizationID: "org-1"}}, + }, nil).Once() + }, + expectIDs: []string{"inv-1", "inv-2", "inv-3"}, + }, + { + name: "missing organization id is not found", + organizationID: "", + expectErr: coreerrors.ErrNotFound, + }, + { + name: "repository error is propagated", + organizationID: "org-1", + setup: func(invRepo *orgtests.MockOrganizationInvitationRepository) { + invRepo.On("GetAllByOrganizationIDWithOrg", mock.Anything, "org-1"). + Return(([]types.GetOrganizationInvitationResponse)(nil), repoErr).Once() + }, + expectErr: repoErr, + }, + { + name: "nil result is normalised to an empty slice", + organizationID: "org-1", + setup: func(invRepo *orgtests.MockOrganizationInvitationRepository) { + invRepo.On("GetAllByOrganizationIDWithOrg", mock.Anything, "org-1"). + Return(([]types.GetOrganizationInvitationResponse)(nil), nil).Once() + }, + expectIDs: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + pluginConfig := &types.OrganizationsPluginConfig{ + Enabled: true, + InvitationExpiresIn: 24 * time.Hour, + } + orgRepo := &orgtests.MockOrganizationRepository{} + invRepo := &orgtests.MockOrganizationInvitationRepository{} + memberRepo := &orgtests.MockOrganizationMemberRepository{} + if tt.setup != nil { + tt.setup(invRepo) + } + + svc := newTestOrganizationInvitationService(&orgtests.MockOrganizationInvitationTxRunner{}, pluginConfig, &internaltests.MockUserService{}, orgtests.NewAccessControlServiceStub(), orgRepo, invRepo, memberRepo) + invitations, err := svc.GetAllOrganizationInvitationsByOrgIDWithOrg(context.Background(), tt.organizationID) + if tt.expectErr != nil { + require.Error(t, err) + require.ErrorIs(t, err, tt.expectErr) + require.Nil(t, invitations) + require.True(t, invRepo.AssertExpectations(t)) + return + } + + require.NoError(t, err) + require.NotNil(t, invitations) + ids := make([]string, 0, len(invitations)) + for _, invitation := range invitations { + ids = append(ids, invitation.Invitation.ID) + } + require.Equal(t, tt.expectIDs, ids) + require.True(t, invRepo.AssertExpectations(t)) + }) + } +} + +// The 500-row batch cap used to silently drop pending invitations past the limit; +// the unconstrained fetch must hand every one of them to the acceptance loop. +func TestOrganizationInvitationService_AcceptPendingOrganizationInvitationsForEmailIsNotCapped(t *testing.T) { + t.Parallel() + + const pendingCount = 600 + + pluginConfig := &types.OrganizationsPluginConfig{ + Enabled: true, + InvitationExpiresIn: 24 * time.Hour, + } + orgRepo := &orgtests.MockOrganizationRepository{} + invRepo := &orgtests.MockOrganizationInvitationRepository{} + memberRepo := &orgtests.MockOrganizationMemberRepository{} + + pending := make([]types.OrganizationInvitation, 0, pendingCount) + for i := 1; i <= pendingCount; i++ { + pending = append(pending, types.OrganizationInvitation{ + ID: fmt.Sprintf("inv-%04d", i), + OrganizationID: fmt.Sprintf("org-%04d", i), + Email: "user@example.com", + Role: "member", + Status: types.OrganizationInvitationStatusPending, + ExpiresAt: time.Now().UTC().Add(time.Hour), + }) + } + + invRepo.On("GetAllPendingByEmail", mock.Anything, "user@example.com").Return(pending, nil).Once() + // One expectation per repository call rather than per invitation: testify + // matches arguments linearly, so 3 x pendingCount expectations is quadratic. + memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, mock.Anything, "user-2").Return(nil, nil).Times(pendingCount) + memberRepo.On("Create", mock.Anything, mock.Anything).Return(&types.OrganizationMember{ID: "mem-1", UserID: "user-2", Role: "member"}, nil).Times(pendingCount) + invRepo.On("Update", mock.Anything, mock.MatchedBy(func(invitation *types.OrganizationInvitation) bool { + return invitation != nil && invitation.Status == types.OrganizationInvitationStatusAccepted + })).Return(&types.OrganizationInvitation{ID: "inv-accepted", Status: types.OrganizationInvitationStatusAccepted}, nil).Times(pendingCount) + + svc := newTestOrganizationInvitationService(&orgtests.MockOrganizationInvitationTxRunner{}, pluginConfig, &internaltests.MockUserService{}, orgtests.NewAccessControlServiceStub(), orgRepo, invRepo, memberRepo) + accepted, err := svc.AcceptPendingOrganizationInvitationsForEmail(context.Background(), "user-2", "user@example.com") + require.NoError(t, err) + require.Len(t, accepted, pendingCount, "every pending invitation must be accepted, not just the first 500") + // AssertExpectations enforces the exact call count, so each of the 600 + // invitations reached Update rather than being dropped past a batch cap. + require.True(t, invRepo.AssertExpectations(t)) + require.True(t, memberRepo.AssertExpectations(t)) +} diff --git a/plugins/organizations/services/organization_member_service.go b/plugins/organizations/services/organization_member_service.go index 0df6c333..ce800dde 100644 --- a/plugins/organizations/services/organization_member_service.go +++ b/plugins/organizations/services/organization_member_service.go @@ -115,14 +115,14 @@ func (s *organizationMemberService) AddMember(ctx context.Context, actor *models return created, nil } -func (s *organizationMemberService) GetAllMembers(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationMembersResponse, error) { +func (s *organizationMemberService) ListAllMembers(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationMembersResponse, error) { if _, _, err := s.serviceUtils.AuthorizeOrganizationAccess(ctx, actor, organizationID); err != nil { return nil, err } params = pagination.Clamp(params) - members, total, err := s.orgMemberRepo.GetAllByOrganizationIDWithUser(ctx, organizationID, params.Page, params.Limit) + members, total, err := s.orgMemberRepo.ListAllByOrganizationIDWithUser(ctx, organizationID, params.Page, params.Limit) if err != nil { return nil, err } @@ -136,6 +136,22 @@ func (s *organizationMemberService) GetAllMembers(ctx context.Context, actor *mo }, nil } +func (s *organizationMemberService) GetAllMembers(ctx context.Context, actor *models.Actor, organizationID string) ([]types.OrganizationMemberResponse, error) { + if _, _, err := s.serviceUtils.AuthorizeOrganizationAccess(ctx, actor, organizationID); err != nil { + return nil, err + } + + members, err := s.orgMemberRepo.GetAllByOrganizationIDWithUser(ctx, organizationID) + if err != nil { + return nil, err + } + if members == nil { + members = []types.OrganizationMemberResponse{} + } + + return members, nil +} + func (s *organizationMemberService) GetMember(ctx context.Context, actor *models.Actor, organizationID string, memberID string) (*types.OrganizationMemberResponse, error) { if _, _, err := s.serviceUtils.AuthorizeOrganizationAccess(ctx, actor, organizationID); err != nil { return nil, err diff --git a/plugins/organizations/services/organization_member_service_test.go b/plugins/organizations/services/organization_member_service_test.go index cd3508c4..65dfc952 100644 --- a/plugins/organizations/services/organization_member_service_test.go +++ b/plugins/organizations/services/organization_member_service_test.go @@ -304,7 +304,7 @@ func TestOrganizationMemberService_AddMember(t *testing.T) { } } -func TestOrganizationMemberService_GetAllMembers(t *testing.T) { +func TestOrganizationMemberService_ListAllMembers(t *testing.T) { t.Parallel() repoErr := errors.New("repository error") @@ -354,7 +354,7 @@ func TestOrganizationMemberService_GetAllMembers(t *testing.T) { params: pagination.Params{Page: 1, Limit: 10}, setup: func(orgRepo *orgtests.MockOrganizationRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() - memberRepo.On("GetAllByOrganizationIDWithUser", mock.Anything, "org-1", 1, 10).Return(nil, 0, repoErr).Once() + memberRepo.On("ListAllByOrganizationIDWithUser", mock.Anything, "org-1", 1, 10).Return(nil, 0, repoErr).Once() }, expectErr: repoErr, }, @@ -365,7 +365,7 @@ func TestOrganizationMemberService_GetAllMembers(t *testing.T) { params: pagination.Params{Page: 1, Limit: 10}, setup: func(orgRepo *orgtests.MockOrganizationRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() - memberRepo.On("GetAllByOrganizationIDWithUser", mock.Anything, "org-1", 1, 10). + memberRepo.On("ListAllByOrganizationIDWithUser", mock.Anything, "org-1", 1, 10). Return([]types.OrganizationMemberResponse{{ID: "mem-1", OrganizationID: "org-1", Role: "member"}}, 25, nil).Once() }, expectLen: 1, @@ -378,7 +378,7 @@ func TestOrganizationMemberService_GetAllMembers(t *testing.T) { params: pagination.Params{Page: -4, Limit: 5000}, setup: func(orgRepo *orgtests.MockOrganizationRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() - memberRepo.On("GetAllByOrganizationIDWithUser", mock.Anything, "org-1", 1, 5000). + memberRepo.On("ListAllByOrganizationIDWithUser", mock.Anything, "org-1", 1, 5000). Return([]types.OrganizationMemberResponse{}, 0, nil).Once() }, expectLen: 0, @@ -391,7 +391,7 @@ func TestOrganizationMemberService_GetAllMembers(t *testing.T) { params: pagination.Params{Page: 1, Limit: 10}, setup: func(orgRepo *orgtests.MockOrganizationRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() - memberRepo.On("GetAllByOrganizationIDWithUser", mock.Anything, "org-1", 1, 10). + memberRepo.On("ListAllByOrganizationIDWithUser", mock.Anything, "org-1", 1, 10). Return(([]types.OrganizationMemberResponse)(nil), 0, nil).Once() }, expectLen: 0, @@ -412,7 +412,7 @@ func TestOrganizationMemberService_GetAllMembers(t *testing.T) { expectActorMember(memberRepo, tt.organizationID, tt.actorUserID) svc := newTestOrganizationMemberService(userService, orgtests.NewAccessControlServiceStub(), orgRepo, memberRepo, nil) - resp, err := svc.GetAllMembers(context.Background(), orgtests.Actor(tt.actorUserID), tt.organizationID, tt.params) + resp, err := svc.ListAllMembers(context.Background(), orgtests.Actor(tt.actorUserID), tt.organizationID, tt.params) if tt.expectErr != nil { require.Error(t, err) require.ErrorIs(t, err, tt.expectErr) @@ -1119,3 +1119,114 @@ func TestOrganizationMemberService_RemoveMemberGuards(t *testing.T) { memberRepo.AssertExpectations(t) }) } + +func TestOrganizationMemberService_GetAllMembers(t *testing.T) { + t.Parallel() + + repoErr := errors.New("repository error") + + tests := []struct { + name string + actorUserID string + organizationID string + setup func(*orgtests.MockOrganizationRepository, *orgtests.MockOrganizationMemberRepository) + expectErr error + expectIDs []string + }{ + { + name: "unauthorized", + actorUserID: "", + organizationID: "org-1", + expectErr: coreerrors.ErrUnauthorized, + }, + { + name: "organization not found", + actorUserID: "user-1", + organizationID: "org-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(nil, nil).Once() + }, + expectErr: coreerrors.ErrNotFound, + }, + { + name: "forbidden when the actor is not a member", + actorUserID: "user-1", + organizationID: "org-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "owner-1"}, nil).Once() + memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(nil, nil).Once() + }, + expectErr: coreerrors.ErrForbidden, + }, + { + name: "repository error", + actorUserID: "user-1", + organizationID: "org-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() + memberRepo.On("GetAllByOrganizationIDWithUser", mock.Anything, "org-1").Return(nil, repoErr).Once() + }, + expectErr: repoErr, + }, + { + name: "success returns every member", + actorUserID: "user-1", + organizationID: "org-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() + memberRepo.On("GetAllByOrganizationIDWithUser", mock.Anything, "org-1").Return([]types.OrganizationMemberResponse{ + {ID: "mem-1", OrganizationID: "org-1", Role: "owner"}, + {ID: "mem-2", OrganizationID: "org-1", Role: "member"}, + }, nil).Once() + }, + expectIDs: []string{"mem-1", "mem-2"}, + }, + { + name: "nil result is normalised to an empty slice", + actorUserID: "user-1", + organizationID: "org-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() + memberRepo.On("GetAllByOrganizationIDWithUser", mock.Anything, "org-1").Return(([]types.OrganizationMemberResponse)(nil), nil).Once() + }, + expectIDs: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + orgRepo := &orgtests.MockOrganizationRepository{} + memberRepo := &orgtests.MockOrganizationMemberRepository{} + userService := &internaltests.MockUserService{} + if tt.setup != nil { + tt.setup(orgRepo, memberRepo) + } + expectActorMember(memberRepo, tt.organizationID, tt.actorUserID) + + svc := newTestOrganizationMemberService(userService, orgtests.NewAccessControlServiceStub(), orgRepo, memberRepo, nil) + members, err := svc.GetAllMembers(context.Background(), orgtests.Actor(tt.actorUserID), tt.organizationID) + if tt.expectErr != nil { + require.Error(t, err) + require.ErrorIs(t, err, tt.expectErr) + require.Nil(t, members) + if tt.setup != nil { + require.True(t, orgRepo.AssertExpectations(t)) + require.True(t, memberRepo.AssertExpectations(t)) + } + return + } + + require.NoError(t, err) + require.NotNil(t, members) + ids := make([]string, 0, len(members)) + for _, member := range members { + ids = append(ids, member.ID) + } + require.Equal(t, tt.expectIDs, ids) + require.True(t, orgRepo.AssertExpectations(t)) + require.True(t, memberRepo.AssertExpectations(t)) + }) + } +} diff --git a/plugins/organizations/services/organization_service.go b/plugins/organizations/services/organization_service.go index 6ccc8c2f..a60ef07d 100644 --- a/plugins/organizations/services/organization_service.go +++ b/plugins/organizations/services/organization_service.go @@ -179,14 +179,14 @@ func (s *organizationService) ensureOrganizationLimit(ctx context.Context, actor return nil } -func (s *organizationService) GetAllOrganizations(ctx context.Context, actor *models.Actor, params pagination.Params) (*types.ListOrganizationsResponse, error) { +func (s *organizationService) ListAllOrganizations(ctx context.Context, actor *models.Actor, params pagination.Params) (*types.ListOrganizationsResponse, error) { if actor == nil || actor.ID == "" { return nil, coreerrors.ErrUnauthorized } params = pagination.Clamp(params) - organizations, total, err := s.orgRepo.GetAllAccessibleByUserID(ctx, actor.ID, params.Page, params.Limit) + organizations, total, err := s.orgRepo.ListAllAccessibleByUserID(ctx, actor.ID, params.Page, params.Limit) if err != nil { return nil, err } @@ -200,6 +200,36 @@ func (s *organizationService) GetAllOrganizations(ctx context.Context, actor *mo }, nil } +func (s *organizationService) GetAllOrganizations(ctx context.Context, actor *models.Actor) ([]types.Organization, error) { + if actor == nil || actor.ID == "" { + return nil, coreerrors.ErrUnauthorized + } + + organizations, err := s.orgRepo.GetAllAccessibleByUserID(ctx, actor.ID) + if err != nil { + return nil, err + } + if organizations == nil { + organizations = []types.Organization{} + } + + return organizations, nil +} + +// GetAllOrganizationsUnscoped bypasses actor scoping entirely. See the interface +// documentation before adding a caller. +func (s *organizationService) GetAllOrganizationsUnscoped(ctx context.Context) ([]types.Organization, error) { + organizations, err := s.orgRepo.GetAll(ctx) + if err != nil { + return nil, err + } + if organizations == nil { + organizations = []types.Organization{} + } + + return organizations, nil +} + func (s *organizationService) GetOrganizationByID(ctx context.Context, actor *models.Actor, organizationID string) (*types.Organization, error) { organization, _, err := s.serviceUtils.AuthorizeOrganizationAccess(ctx, actor, organizationID) if err != nil { diff --git a/plugins/organizations/services/organization_service_test.go b/plugins/organizations/services/organization_service_test.go index e20a8df7..0d5cf903 100644 --- a/plugins/organizations/services/organization_service_test.go +++ b/plugins/organizations/services/organization_service_test.go @@ -223,7 +223,7 @@ func TestOrganizationService_CreateOrganizationRequiresPrivilegedRole(t *testing } } -func TestOrganizationService_GetAllOrganizations(t *testing.T) { +func TestOrganizationService_ListAllOrganizations(t *testing.T) { t.Parallel() repoErr := errors.New("repository error") @@ -248,7 +248,7 @@ func TestOrganizationService_GetAllOrganizations(t *testing.T) { actorUserID: "user-1", params: pagination.Params{Page: 1, Limit: 10}, setup: func(repo *orgtests.MockOrganizationRepository) { - repo.On("GetAllAccessibleByUserID", mock.Anything, "user-1", 1, 10). + repo.On("ListAllAccessibleByUserID", mock.Anything, "user-1", 1, 10). Return([]types.Organization{ {ID: "org-1", OwnerID: "user-1", Name: "Acme"}, {ID: "org-2", OwnerID: "owner-2", Name: "Platform"}, @@ -262,7 +262,7 @@ func TestOrganizationService_GetAllOrganizations(t *testing.T) { actorUserID: "user-1", params: pagination.Params{Page: -4, Limit: 5000}, setup: func(repo *orgtests.MockOrganizationRepository) { - repo.On("GetAllAccessibleByUserID", mock.Anything, "user-1", 1, 5000). + repo.On("ListAllAccessibleByUserID", mock.Anything, "user-1", 1, 5000). Return([]types.Organization{}, 0, nil).Once() }, expectLen: 0, @@ -273,7 +273,7 @@ func TestOrganizationService_GetAllOrganizations(t *testing.T) { actorUserID: "user-1", params: pagination.Params{Page: 1, Limit: 10}, setup: func(repo *orgtests.MockOrganizationRepository) { - repo.On("GetAllAccessibleByUserID", mock.Anything, "user-1", 1, 10). + repo.On("ListAllAccessibleByUserID", mock.Anything, "user-1", 1, 10). Return(([]types.Organization)(nil), 0, nil).Once() }, expectLen: 0, @@ -284,7 +284,7 @@ func TestOrganizationService_GetAllOrganizations(t *testing.T) { actorUserID: "user-1", params: pagination.Params{Page: 1, Limit: 10}, setup: func(repo *orgtests.MockOrganizationRepository) { - repo.On("GetAllAccessibleByUserID", mock.Anything, "user-1", 1, 10). + repo.On("ListAllAccessibleByUserID", mock.Anything, "user-1", 1, 10). Return(([]types.Organization)(nil), 0, repoErr).Once() }, expectErr: repoErr, @@ -303,7 +303,7 @@ func TestOrganizationService_GetAllOrganizations(t *testing.T) { serviceUtils := &ServiceUtils{orgRepo: repo, orgMemberRepo: memberRepo} svc := NewOrganizationService(repo, memberRepo, serviceUtils, nil, nil, nil) - resp, err := svc.GetAllOrganizations(context.Background(), orgtests.Actor(tt.actorUserID), tt.params) + resp, err := svc.ListAllOrganizations(context.Background(), orgtests.Actor(tt.actorUserID), tt.params) if tt.expectErr != nil { require.Error(t, err) require.ErrorIs(t, err, tt.expectErr) @@ -600,3 +600,154 @@ func TestOrganizationService_DeleteOrganization(t *testing.T) { }) } } + +func TestOrganizationService_GetAllOrganizations(t *testing.T) { + t.Parallel() + + repoErr := errors.New("repository error") + + tests := []struct { + name string + actorUserID string + setup func(*orgtests.MockOrganizationRepository) + expectErr error + expectIDs []string + }{ + { + name: "unauthorized without an actor", + actorUserID: "", + expectErr: coreerrors.ErrUnauthorized, + }, + { + name: "repository error", + actorUserID: "user-1", + setup: func(repo *orgtests.MockOrganizationRepository) { + repo.On("GetAllAccessibleByUserID", mock.Anything, "user-1").Return(nil, repoErr).Once() + }, + expectErr: repoErr, + }, + { + name: "success returns every accessible organization", + actorUserID: "user-1", + setup: func(repo *orgtests.MockOrganizationRepository) { + repo.On("GetAllAccessibleByUserID", mock.Anything, "user-1").Return([]types.Organization{ + {ID: "org-1", OwnerID: "user-1"}, + {ID: "org-2", OwnerID: "user-2"}, + }, nil).Once() + }, + expectIDs: []string{"org-1", "org-2"}, + }, + { + name: "nil result is normalised to an empty slice", + actorUserID: "user-1", + setup: func(repo *orgtests.MockOrganizationRepository) { + repo.On("GetAllAccessibleByUserID", mock.Anything, "user-1").Return(([]types.Organization)(nil), nil).Once() + }, + expectIDs: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + repo := &orgtests.MockOrganizationRepository{} + memberRepo := &orgtests.MockOrganizationMemberRepository{} + if tt.setup != nil { + tt.setup(repo) + } + + serviceUtils := &ServiceUtils{orgRepo: repo, orgMemberRepo: memberRepo} + svc := NewOrganizationService(repo, memberRepo, serviceUtils, nil, nil, nil) + organizations, err := svc.GetAllOrganizations(context.Background(), orgtests.Actor(tt.actorUserID)) + if tt.expectErr != nil { + require.Error(t, err) + require.ErrorIs(t, err, tt.expectErr) + require.Nil(t, organizations) + require.True(t, repo.AssertExpectations(t)) + return + } + + require.NoError(t, err) + require.NotNil(t, organizations) + ids := make([]string, 0, len(organizations)) + for _, organization := range organizations { + ids = append(ids, organization.ID) + } + require.Equal(t, tt.expectIDs, ids) + require.True(t, repo.AssertExpectations(t)) + }) + } +} + +// GetAllOrganizationsUnscoped deliberately ignores the caller, so it must never +// consult the accessible-by-user predicate. +func TestOrganizationService_GetAllOrganizationsUnscoped(t *testing.T) { + t.Parallel() + + repoErr := errors.New("repository error") + + tests := []struct { + name string + setup func(*orgtests.MockOrganizationRepository) + expectErr error + expectIDs []string + }{ + { + name: "repository error", + setup: func(repo *orgtests.MockOrganizationRepository) { + repo.On("GetAll", mock.Anything).Return(nil, repoErr).Once() + }, + expectErr: repoErr, + }, + { + name: "success returns every organization in the system", + setup: func(repo *orgtests.MockOrganizationRepository) { + repo.On("GetAll", mock.Anything).Return([]types.Organization{ + {ID: "org-1", OwnerID: "user-1"}, + {ID: "org-2", OwnerID: "user-2"}, + {ID: "org-3", OwnerID: "user-3"}, + }, nil).Once() + }, + expectIDs: []string{"org-1", "org-2", "org-3"}, + }, + { + name: "nil result is normalised to an empty slice", + setup: func(repo *orgtests.MockOrganizationRepository) { + repo.On("GetAll", mock.Anything).Return(([]types.Organization)(nil), nil).Once() + }, + expectIDs: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + repo := &orgtests.MockOrganizationRepository{} + memberRepo := &orgtests.MockOrganizationMemberRepository{} + tt.setup(repo) + + serviceUtils := &ServiceUtils{orgRepo: repo, orgMemberRepo: memberRepo} + svc := NewOrganizationService(repo, memberRepo, serviceUtils, nil, nil, nil) + organizations, err := svc.GetAllOrganizationsUnscoped(context.Background()) + if tt.expectErr != nil { + require.Error(t, err) + require.ErrorIs(t, err, tt.expectErr) + require.Nil(t, organizations) + require.True(t, repo.AssertExpectations(t)) + return + } + + require.NoError(t, err) + require.NotNil(t, organizations) + ids := make([]string, 0, len(organizations)) + for _, organization := range organizations { + ids = append(ids, organization.ID) + } + require.Equal(t, tt.expectIDs, ids) + require.True(t, repo.AssertExpectations(t)) + require.True(t, memberRepo.AssertExpectations(t)) + }) + } +} diff --git a/plugins/organizations/services/organization_team_member_service.go b/plugins/organizations/services/organization_team_member_service.go index 31266ba1..4fa16433 100644 --- a/plugins/organizations/services/organization_team_member_service.go +++ b/plugins/organizations/services/organization_team_member_service.go @@ -97,26 +97,52 @@ func (s *organizationTeamMemberService) AddTeamMember(ctx context.Context, actor return created, nil } -func (s *organizationTeamMemberService) GetAllTeamMembers(ctx context.Context, actor *models.Actor, organizationID string, teamID string, params pagination.Params) (*types.ListOrganizationTeamMembersResponse, error) { +// authorizeTeamListing checks that the actor may read the team's members and +// that the team actually belongs to the organization in the request path. +func (s *organizationTeamMemberService) authorizeTeamListing(ctx context.Context, actor *models.Actor, organizationID string, teamID string) error { if _, _, err := s.serviceUtils.AuthorizeOrganizationAccess(ctx, actor, organizationID); err != nil { - return nil, err + return err } if err := s.serviceUtils.authorizeTeamAccess(ctx, actor, organizationID, teamID); err != nil { - return nil, err + return err } team, err := s.orgTeamRepo.GetByID(ctx, teamID) if err != nil { - return nil, err + return err } if team == nil || team.OrganizationID != organizationID { - return nil, coreerrors.ErrNotFound + return coreerrors.ErrNotFound + } + + return nil +} + +func (s *organizationTeamMemberService) GetAllTeamMembers(ctx context.Context, actor *models.Actor, organizationID string, teamID string) ([]types.OrganizationTeamMemberResponse, error) { + if err := s.authorizeTeamListing(ctx, actor, organizationID, teamID); err != nil { + return nil, err + } + + teamMembers, err := s.orgTeamMemberRepo.GetAllByTeamIDWithMemberAndUser(ctx, teamID) + if err != nil { + return nil, err + } + if teamMembers == nil { + teamMembers = []types.OrganizationTeamMemberResponse{} + } + + return teamMembers, nil +} + +func (s *organizationTeamMemberService) ListAllTeamMembers(ctx context.Context, actor *models.Actor, organizationID string, teamID string, params pagination.Params) (*types.ListOrganizationTeamMembersResponse, error) { + if err := s.authorizeTeamListing(ctx, actor, organizationID, teamID); err != nil { + return nil, err } params = pagination.Clamp(params) - teamMembers, total, err := s.orgTeamMemberRepo.GetAllByTeamIDWithMemberAndUser(ctx, teamID, params.Page, params.Limit) + teamMembers, total, err := s.orgTeamMemberRepo.ListAllByTeamIDWithMemberAndUser(ctx, teamID, params.Page, params.Limit) if err != nil { return nil, err } diff --git a/plugins/organizations/services/organization_team_member_service_test.go b/plugins/organizations/services/organization_team_member_service_test.go index aac52310..afd53753 100644 --- a/plugins/organizations/services/organization_team_member_service_test.go +++ b/plugins/organizations/services/organization_team_member_service_test.go @@ -24,7 +24,7 @@ func newTestOrganizationTeamMemberService(orgRepo *orgtests.MockOrganizationRepo return NewOrganizationTeamMemberService(orgRepo, memberRepo, teamRepo, teamMemberRepo, serviceUtils) } -func TestOrganizationTeamService_GetAllTeamMembers(t *testing.T) { +func TestOrganizationTeamService_ListAllTeamMembers(t *testing.T) { t.Parallel() repoErr := errors.New("repository error") @@ -49,7 +49,7 @@ func TestOrganizationTeamService_GetAllTeamMembers(t *testing.T) { orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(&types.OrganizationMember{ID: "owner-member-1", OrganizationID: "org-1", UserID: "user-1", Role: "owner"}, nil).Twice() teamRepo.On("GetByID", mock.Anything, "team-1").Return(&types.OrganizationTeam{ID: "team-1", OrganizationID: "org-1"}, nil).Twice() - teamMemberRepo.On("GetAllByTeamIDWithMemberAndUser", mock.Anything, "team-1", 1, 10).Return([]types.OrganizationTeamMemberResponse{{ID: "tm-1", TeamID: "team-1"}}, 1, nil).Once() + teamMemberRepo.On("ListAllByTeamIDWithMemberAndUser", mock.Anything, "team-1", 1, 10).Return([]types.OrganizationTeamMemberResponse{{ID: "tm-1", TeamID: "team-1"}}, 1, nil).Once() }, expectLen: 1, expectCalled: true, @@ -63,7 +63,7 @@ func TestOrganizationTeamService_GetAllTeamMembers(t *testing.T) { orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "owner-1"}, nil).Once() memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-2").Return(&types.OrganizationMember{ID: "org-member-1", OrganizationID: "org-1", UserID: "user-2", Role: "member"}, nil).Twice() teamRepo.On("GetByID", mock.Anything, "team-1").Return(&types.OrganizationTeam{ID: "team-1", OrganizationID: "org-1"}, nil).Twice() - teamMemberRepo.On("GetAllByTeamIDWithMemberAndUser", mock.Anything, "team-1", 1, 10).Return([]types.OrganizationTeamMemberResponse{{ID: "tm-1", TeamID: "team-1"}}, 1, nil).Once() + teamMemberRepo.On("ListAllByTeamIDWithMemberAndUser", mock.Anything, "team-1", 1, 10).Return([]types.OrganizationTeamMemberResponse{{ID: "tm-1", TeamID: "team-1"}}, 1, nil).Once() }, expectLen: 1, expectCalled: true, @@ -146,7 +146,7 @@ func TestOrganizationTeamService_GetAllTeamMembers(t *testing.T) { orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(&types.OrganizationMember{ID: "owner-member-1", OrganizationID: "org-1", UserID: "user-1", Role: "owner"}, nil).Twice() teamRepo.On("GetByID", mock.Anything, "team-1").Return(&types.OrganizationTeam{ID: "team-1", OrganizationID: "org-1"}, nil).Twice() - teamMemberRepo.On("GetAllByTeamIDWithMemberAndUser", mock.Anything, "team-1", 1, 10).Return(([]types.OrganizationTeamMemberResponse)(nil), 0, repoErr).Once() + teamMemberRepo.On("ListAllByTeamIDWithMemberAndUser", mock.Anything, "team-1", 1, 10).Return(([]types.OrganizationTeamMemberResponse)(nil), 0, repoErr).Once() }, expectErr: repoErr, expectCalled: true, @@ -161,7 +161,7 @@ func TestOrganizationTeamService_GetAllTeamMembers(t *testing.T) { orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(&types.OrganizationMember{ID: "owner-member-1", OrganizationID: "org-1", UserID: "user-1", Role: "owner"}, nil).Twice() teamRepo.On("GetByID", mock.Anything, "team-1").Return(&types.OrganizationTeam{ID: "team-1", OrganizationID: "org-1"}, nil).Twice() - teamMemberRepo.On("GetAllByTeamIDWithMemberAndUser", mock.Anything, "team-1", 1, 5000).Return(([]types.OrganizationTeamMemberResponse)(nil), 0, nil).Once() + teamMemberRepo.On("ListAllByTeamIDWithMemberAndUser", mock.Anything, "team-1", 1, 5000).Return(([]types.OrganizationTeamMemberResponse)(nil), 0, nil).Once() }, expectLen: 0, expectCalled: true, @@ -186,7 +186,7 @@ func TestOrganizationTeamService_GetAllTeamMembers(t *testing.T) { } svc := newTestOrganizationTeamMemberService(orgRepo, memberRepo, teamRepo, teamMemberRepo) - resp, err := svc.GetAllTeamMembers(context.Background(), orgtests.Actor(tt.actorUserID), tt.organizationID, tt.teamID, params) + resp, err := svc.ListAllTeamMembers(context.Background(), orgtests.Actor(tt.actorUserID), tt.organizationID, tt.teamID, params) if tt.expectErr != nil { require.Error(t, err) require.ErrorIs(t, err, tt.expectErr) @@ -908,3 +908,148 @@ func TestOrganizationTeamService_RemoveTeamMember(t *testing.T) { }) } } + +func TestOrganizationTeamMemberService_GetAllTeamMembers(t *testing.T) { + t.Parallel() + + repoErr := errors.New("repository error") + + tests := []struct { + name string + actorUserID string + organizationID string + teamID string + setup func(*orgtests.MockOrganizationRepository, *orgtests.MockOrganizationTeamRepository, *orgtests.MockOrganizationMemberRepository, *orgtests.MockOrganizationTeamMemberRepository) + expectErr error + expectIDs []string + expectCalled bool + }{ + { + name: "success returns every team member", + actorUserID: "user-1", + organizationID: "org-1", + teamID: "team-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository, teamMemberRepo *orgtests.MockOrganizationTeamMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() + memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(&types.OrganizationMember{ID: "owner-member-1", OrganizationID: "org-1", UserID: "user-1", Role: "owner"}, nil).Twice() + teamRepo.On("GetByID", mock.Anything, "team-1").Return(&types.OrganizationTeam{ID: "team-1", OrganizationID: "org-1"}, nil).Twice() + teamMemberRepo.On("GetAllByTeamIDWithMemberAndUser", mock.Anything, "team-1").Return([]types.OrganizationTeamMemberResponse{ + {ID: "tm-1", TeamID: "team-1"}, + {ID: "tm-2", TeamID: "team-1"}, + }, nil).Once() + }, + expectIDs: []string{"tm-1", "tm-2"}, + expectCalled: true, + }, + { + name: "unauthorized", + actorUserID: "", + organizationID: "org-1", + teamID: "team-1", + expectErr: coreerrors.ErrUnauthorized, + }, + { + name: "forbidden when the actor is not an organization member", + actorUserID: "user-1", + organizationID: "org-1", + teamID: "team-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository, teamMemberRepo *orgtests.MockOrganizationTeamMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "owner-1"}, nil).Once() + memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(nil, nil).Once() + }, + expectErr: coreerrors.ErrForbidden, + expectCalled: true, + }, + { + name: "team not found", + actorUserID: "user-1", + organizationID: "org-1", + teamID: "team-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository, teamMemberRepo *orgtests.MockOrganizationTeamMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() + memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(&types.OrganizationMember{ID: "owner-member-1", OrganizationID: "org-1", UserID: "user-1", Role: "owner"}, nil).Once() + teamRepo.On("GetByID", mock.Anything, "team-1").Return(nil, nil).Once() + }, + expectErr: coreerrors.ErrNotFound, + expectCalled: true, + }, + { + name: "team belonging to another organization is not found", + actorUserID: "user-1", + organizationID: "org-1", + teamID: "team-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository, teamMemberRepo *orgtests.MockOrganizationTeamMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() + memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(&types.OrganizationMember{ID: "owner-member-1", OrganizationID: "org-1", UserID: "user-1", Role: "owner"}, nil).Once() + teamRepo.On("GetByID", mock.Anything, "team-1").Return(&types.OrganizationTeam{ID: "team-1", OrganizationID: "org-2"}, nil).Once() + }, + expectErr: coreerrors.ErrNotFound, + expectCalled: true, + }, + { + name: "repository error", + actorUserID: "user-1", + organizationID: "org-1", + teamID: "team-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository, teamMemberRepo *orgtests.MockOrganizationTeamMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() + memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(&types.OrganizationMember{ID: "owner-member-1", OrganizationID: "org-1", UserID: "user-1", Role: "owner"}, nil).Twice() + teamRepo.On("GetByID", mock.Anything, "team-1").Return(&types.OrganizationTeam{ID: "team-1", OrganizationID: "org-1"}, nil).Twice() + teamMemberRepo.On("GetAllByTeamIDWithMemberAndUser", mock.Anything, "team-1").Return(([]types.OrganizationTeamMemberResponse)(nil), repoErr).Once() + }, + expectErr: repoErr, + expectCalled: true, + }, + { + name: "nil result is normalised to an empty slice", + actorUserID: "user-1", + organizationID: "org-1", + teamID: "team-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository, teamMemberRepo *orgtests.MockOrganizationTeamMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() + memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(&types.OrganizationMember{ID: "owner-member-1", OrganizationID: "org-1", UserID: "user-1", Role: "owner"}, nil).Twice() + teamRepo.On("GetByID", mock.Anything, "team-1").Return(&types.OrganizationTeam{ID: "team-1", OrganizationID: "org-1"}, nil).Twice() + teamMemberRepo.On("GetAllByTeamIDWithMemberAndUser", mock.Anything, "team-1").Return(([]types.OrganizationTeamMemberResponse)(nil), nil).Once() + }, + expectIDs: []string{}, + expectCalled: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + orgRepo := &orgtests.MockOrganizationRepository{} + teamRepo := &orgtests.MockOrganizationTeamRepository{} + memberRepo := &orgtests.MockOrganizationMemberRepository{} + teamMemberRepo := &orgtests.MockOrganizationTeamMemberRepository{} + if tt.setup != nil { + tt.setup(orgRepo, teamRepo, memberRepo, teamMemberRepo) + } + + svc := newTestOrganizationTeamMemberService(orgRepo, memberRepo, teamRepo, teamMemberRepo) + teamMembers, err := svc.GetAllTeamMembers(context.Background(), orgtests.Actor(tt.actorUserID), tt.organizationID, tt.teamID) + if tt.expectErr != nil { + require.Error(t, err) + require.ErrorIs(t, err, tt.expectErr) + require.Nil(t, teamMembers) + } else { + require.NoError(t, err) + require.NotNil(t, teamMembers) + ids := make([]string, 0, len(teamMembers)) + for _, teamMember := range teamMembers { + ids = append(ids, teamMember.ID) + } + require.Equal(t, tt.expectIDs, ids) + } + + if tt.expectCalled { + require.True(t, orgRepo.AssertExpectations(t)) + require.True(t, teamRepo.AssertExpectations(t)) + require.True(t, memberRepo.AssertExpectations(t)) + require.True(t, teamMemberRepo.AssertExpectations(t)) + } + }) + } +} diff --git a/plugins/organizations/services/organization_team_service.go b/plugins/organizations/services/organization_team_service.go index b95a7151..94f35b8f 100644 --- a/plugins/organizations/services/organization_team_service.go +++ b/plugins/organizations/services/organization_team_service.go @@ -151,14 +151,14 @@ func (s *organizationTeamService) CreateTeam(ctx context.Context, actor *models. return created, nil } -func (s *organizationTeamService) GetAllTeams(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationTeamsResponse, error) { +func (s *organizationTeamService) ListAllTeams(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationTeamsResponse, error) { if _, _, err := s.serviceUtils.AuthorizeOrganizationAccess(ctx, actor, organizationID); err != nil { return nil, err } params = pagination.Clamp(params) - teams, total, err := s.orgTeamRepo.GetAllByOrganizationID(ctx, organizationID, params.Page, params.Limit) + teams, total, err := s.orgTeamRepo.ListAllByOrganizationID(ctx, organizationID, params.Page, params.Limit) if err != nil { return nil, err } @@ -172,6 +172,22 @@ func (s *organizationTeamService) GetAllTeams(ctx context.Context, actor *models }, nil } +func (s *organizationTeamService) GetAllTeams(ctx context.Context, actor *models.Actor, organizationID string) ([]types.OrganizationTeam, error) { + if _, _, err := s.serviceUtils.AuthorizeOrganizationAccess(ctx, actor, organizationID); err != nil { + return nil, err + } + + teams, err := s.orgTeamRepo.GetAllByOrganizationID(ctx, organizationID) + if err != nil { + return nil, err + } + if teams == nil { + teams = []types.OrganizationTeam{} + } + + return teams, nil +} + func (s *organizationTeamService) GetTeam(ctx context.Context, actor *models.Actor, organizationID string, teamID string) (*types.OrganizationTeam, error) { if _, _, err := s.serviceUtils.AuthorizeOrganizationAccess(ctx, actor, organizationID); err != nil { return nil, err diff --git a/plugins/organizations/services/organization_team_service_test.go b/plugins/organizations/services/organization_team_service_test.go index 38e660e4..ee5e284c 100644 --- a/plugins/organizations/services/organization_team_service_test.go +++ b/plugins/organizations/services/organization_team_service_test.go @@ -234,7 +234,7 @@ func TestOrganizationTeamService_CreateTeam(t *testing.T) { } } -func TestOrganizationTeamService_GetAllTeams(t *testing.T) { +func TestOrganizationTeamService_ListAllTeams(t *testing.T) { t.Parallel() repoErr := errors.New("repository error") @@ -257,7 +257,7 @@ func TestOrganizationTeamService_GetAllTeams(t *testing.T) { setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(&types.OrganizationMember{ID: "mem-1", OrganizationID: "org-1", UserID: "user-1", Role: "owner"}, nil).Once() - teamRepo.On("GetAllByOrganizationID", mock.Anything, "org-1", 1, 10).Return([]types.OrganizationTeam{{ID: "team-1", OrganizationID: "org-1", Name: "Platform", Slug: "platform"}}, 1, nil).Once() + teamRepo.On("ListAllByOrganizationID", mock.Anything, "org-1", 1, 10).Return([]types.OrganizationTeam{{ID: "team-1", OrganizationID: "org-1", Name: "Platform", Slug: "platform"}}, 1, nil).Once() }, expectLen: 1, expectPagination: pagination.Pagination{Page: 1, Limit: 10, Total: 1, TotalPages: 1, HasMore: false}, @@ -270,7 +270,7 @@ func TestOrganizationTeamService_GetAllTeams(t *testing.T) { setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "owner-1"}, nil).Once() memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-2").Return(&types.OrganizationMember{ID: "mem-1", OrganizationID: "org-1", UserID: "user-2", Role: "member"}, nil).Once() - teamRepo.On("GetAllByOrganizationID", mock.Anything, "org-1", 1, 10).Return([]types.OrganizationTeam{{ID: "team-1", OrganizationID: "org-1", Name: "Platform", Slug: "platform"}}, 1, nil).Once() + teamRepo.On("ListAllByOrganizationID", mock.Anything, "org-1", 1, 10).Return([]types.OrganizationTeam{{ID: "team-1", OrganizationID: "org-1", Name: "Platform", Slug: "platform"}}, 1, nil).Once() }, expectLen: 1, expectPagination: pagination.Pagination{Page: 1, Limit: 10, Total: 1, TotalPages: 1, HasMore: false}, @@ -283,7 +283,7 @@ func TestOrganizationTeamService_GetAllTeams(t *testing.T) { setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(&types.OrganizationMember{ID: "mem-1", OrganizationID: "org-1", UserID: "user-1", Role: "owner"}, nil).Once() - teamRepo.On("GetAllByOrganizationID", mock.Anything, "org-1", 1, 5000).Return(([]types.OrganizationTeam)(nil), 0, nil).Once() + teamRepo.On("ListAllByOrganizationID", mock.Anything, "org-1", 1, 5000).Return(([]types.OrganizationTeam)(nil), 0, nil).Once() }, expectLen: 0, expectPagination: pagination.Pagination{Page: 1, Limit: 5000, Total: 0, TotalPages: 0, HasMore: false}, @@ -333,7 +333,7 @@ func TestOrganizationTeamService_GetAllTeams(t *testing.T) { setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(&types.OrganizationMember{ID: "mem-1", OrganizationID: "org-1", UserID: "user-1", Role: "owner"}, nil).Once() - teamRepo.On("GetAllByOrganizationID", mock.Anything, "org-1", 1, 10).Return(([]types.OrganizationTeam)(nil), 0, repoErr).Once() + teamRepo.On("ListAllByOrganizationID", mock.Anything, "org-1", 1, 10).Return(([]types.OrganizationTeam)(nil), 0, repoErr).Once() }, expectErr: repoErr, }, @@ -351,7 +351,7 @@ func TestOrganizationTeamService_GetAllTeams(t *testing.T) { } svc := newTestOrganizationTeamService(orgRepo, memberRepo, teamRepo, &orgtests.MockOrganizationTeamMemberRepository{}) - resp, err := svc.GetAllTeams(context.Background(), orgtests.Actor(tt.actorUserID), tt.organizationID, tt.params) + resp, err := svc.ListAllTeams(context.Background(), orgtests.Actor(tt.actorUserID), tt.organizationID, tt.params) if tt.expectErr != nil { require.Error(t, err) require.ErrorIs(t, err, tt.expectErr) @@ -874,3 +874,112 @@ func TestOrganizationTeamService_DeleteTeam(t *testing.T) { }) } } + +func TestOrganizationTeamService_GetAllTeams(t *testing.T) { + t.Parallel() + + repoErr := errors.New("repository error") + + tests := []struct { + name string + actorUserID string + organizationID string + setup func(*orgtests.MockOrganizationRepository, *orgtests.MockOrganizationTeamRepository, *orgtests.MockOrganizationMemberRepository) + expectErr error + expectIDs []string + }{ + { + name: "unauthorized", + actorUserID: "", + organizationID: "org-1", + expectErr: coreerrors.ErrUnauthorized, + }, + { + name: "organization not found", + actorUserID: "user-1", + organizationID: "org-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(nil, nil).Once() + }, + expectErr: coreerrors.ErrNotFound, + }, + { + name: "forbidden when the actor is not a member", + actorUserID: "user-1", + organizationID: "org-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "owner-1"}, nil).Once() + memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(nil, nil).Once() + }, + expectErr: coreerrors.ErrForbidden, + }, + { + name: "repository error", + actorUserID: "user-1", + organizationID: "org-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() + memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(&types.OrganizationMember{ID: "mem-1", OrganizationID: "org-1", UserID: "user-1", Role: "owner"}, nil).Once() + teamRepo.On("GetAllByOrganizationID", mock.Anything, "org-1").Return(nil, repoErr).Once() + }, + expectErr: repoErr, + }, + { + name: "success returns every team", + actorUserID: "user-1", + organizationID: "org-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() + memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(&types.OrganizationMember{ID: "mem-1", OrganizationID: "org-1", UserID: "user-1", Role: "owner"}, nil).Once() + teamRepo.On("GetAllByOrganizationID", mock.Anything, "org-1").Return([]types.OrganizationTeam{ + {ID: "team-1", OrganizationID: "org-1"}, + {ID: "team-2", OrganizationID: "org-1"}, + }, nil).Once() + }, + expectIDs: []string{"team-1", "team-2"}, + }, + { + name: "nil result is normalised to an empty slice", + actorUserID: "user-1", + organizationID: "org-1", + setup: func(orgRepo *orgtests.MockOrganizationRepository, teamRepo *orgtests.MockOrganizationTeamRepository, memberRepo *orgtests.MockOrganizationMemberRepository) { + orgRepo.On("GetByID", mock.Anything, "org-1").Return(&types.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Once() + memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, "org-1", "user-1").Return(&types.OrganizationMember{ID: "mem-1", OrganizationID: "org-1", UserID: "user-1", Role: "owner"}, nil).Once() + teamRepo.On("GetAllByOrganizationID", mock.Anything, "org-1").Return(([]types.OrganizationTeam)(nil), nil).Once() + }, + expectIDs: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + orgRepo := &orgtests.MockOrganizationRepository{} + teamRepo := &orgtests.MockOrganizationTeamRepository{} + memberRepo := &orgtests.MockOrganizationMemberRepository{} + if tt.setup != nil { + tt.setup(orgRepo, teamRepo, memberRepo) + } + + svc := newTestOrganizationTeamService(orgRepo, memberRepo, teamRepo, &orgtests.MockOrganizationTeamMemberRepository{}) + teams, err := svc.GetAllTeams(context.Background(), orgtests.Actor(tt.actorUserID), tt.organizationID) + if tt.expectErr != nil { + require.Error(t, err) + require.ErrorIs(t, err, tt.expectErr) + require.Nil(t, teams) + return + } + + require.NoError(t, err) + require.NotNil(t, teams) + ids := make([]string, 0, len(teams)) + for _, team := range teams { + ids = append(ids, team.ID) + } + require.Equal(t, tt.expectIDs, ids) + require.True(t, orgRepo.AssertExpectations(t)) + require.True(t, teamRepo.AssertExpectations(t)) + }) + } +} diff --git a/plugins/organizations/services/pagination_integration_test.go b/plugins/organizations/services/pagination_integration_test.go index 5b7bed47..b062497c 100644 --- a/plugins/organizations/services/pagination_integration_test.go +++ b/plugins/organizations/services/pagination_integration_test.go @@ -14,7 +14,7 @@ import ( "github.com/Authula/authula/plugins/organizations/types" ) -func TestOrganizationMemberService_GetAllMembersEnforcesLimitsAgainstSQL(t *testing.T) { +func TestOrganizationMemberService_ListAllMembersEnforcesLimitsAgainstSQL(t *testing.T) { t.Parallel() const memberCount = 12 @@ -89,7 +89,7 @@ func TestOrganizationMemberService_GetAllMembersEnforcesLimitsAgainstSQL(t *test svc, ctx := setup(t) - resp, err := svc.GetAllMembers(ctx, orgtests.Actor("user-1"), "org-1", tt.params) + resp, err := svc.ListAllMembers(ctx, orgtests.Actor("user-1"), "org-1", tt.params) require.NoError(t, err) require.NotNil(t, resp) require.Len(t, resp.Data, tt.expectLen) @@ -125,16 +125,65 @@ func TestOrganizationService_QuotaSurvivesPagination(t *testing.T) { require.Error(t, svc.ensureOrganizationLimit(ctx, orgtests.Actor("user-1"), orgRepo), "the quota must reject at the boundary") // The quota counts the same set the list endpoint returns, deduped. - resp, err := svc.GetAllOrganizations(ctx, orgtests.Actor("user-1"), pagination.Params{Page: 1, Limit: 1}) + resp, err := svc.ListAllOrganizations(ctx, orgtests.Actor("user-1"), pagination.Params{Page: 1, Limit: 1}) require.NoError(t, err) require.Len(t, resp.Data, 1, "a single-row page") require.Equal(t, limit, resp.Pagination.Total, "the total must not be capped by the page size") var seen []types.Organization for page := 1; page <= limit; page++ { - pageResp, err := svc.GetAllOrganizations(ctx, orgtests.Actor("user-1"), pagination.Params{Page: page, Limit: 1}) + pageResp, err := svc.ListAllOrganizations(ctx, orgtests.Actor("user-1"), pagination.Params{Page: page, Limit: 1}) require.NoError(t, err) seen = append(seen, pageResp.Data...) } require.Len(t, seen, limit, "paging must yield every accessible organization exactly once") } + +// The unconstrained sibling of ListAllMembers must return the whole collection in +// one call, against real SQL, where the paginated path needs two pages. +func TestOrganizationMemberService_GetAllMembersReturnsEverythingInOneCallAgainstSQL(t *testing.T) { + t.Parallel() + + const memberCount = 12 + + db := orgtests.SetupRepoDB(t) + orgtests.SeedUsers(t, db, memberCount) + orgtests.SeedOrganization(t, db, "org-1", "user-1", "Acme Inc", "acme-inc") + orgtests.SeedOrganization(t, db, "org-2", "user-1", "Beta Inc", "beta-inc") + for i := 1; i <= memberCount; i++ { + orgtests.SeedOrganizationMember(t, db, fmt.Sprintf("mem-%02d", i), "org-1", fmt.Sprintf("user-%d", i), "member") + } + orgtests.SeedOrganizationMember(t, db, "mem-other", "org-2", "user-1", "owner") + + orgRepo := repositories.NewBunOrganizationRepository(db) + memberRepo := repositories.NewBunOrganizationMemberRepository(db) + serviceUtils := &ServiceUtils{orgRepo: orgRepo, orgMemberRepo: memberRepo} + svc := NewOrganizationMemberService( + &internaltests.MockUserService{}, + orgtests.NewAccessControlServiceStub(), + orgRepo, + memberRepo, + nil, + &orgtests.MockTxRunner{}, + serviceUtils, + ) + ctx := context.Background() + actor := orgtests.Actor("user-1") + + // The paginated path only reaches the whole set by walking pages. + page, err := svc.ListAllMembers(ctx, actor, "org-1", pagination.Params{}) + require.NoError(t, err) + require.Len(t, page.Data, pagination.DefaultLimit) + require.True(t, page.Pagination.HasMore, "the default page must not cover the whole collection") + + members, err := svc.GetAllMembers(ctx, actor, "org-1") + require.NoError(t, err) + require.Len(t, members, memberCount, "one call must return every member") + + // Members of another organization must not leak in, and each row is hydrated. + for _, member := range members { + require.Equal(t, "org-1", member.OrganizationID) + require.NotEmpty(t, member.User.ID) + require.NotEmpty(t, member.User.Email) + } +} diff --git a/plugins/organizations/tests/repositories.go b/plugins/organizations/tests/repositories.go index 0ce10761..3af4d825 100644 --- a/plugins/organizations/tests/repositories.go +++ b/plugins/organizations/tests/repositories.go @@ -53,7 +53,7 @@ func (m *MockOrganizationRepository) GetBySlug(ctx context.Context, slug string) return args.Get(0).(*types.Organization), args.Error(1) } -func (m *MockOrganizationRepository) GetAllAccessibleByUserID(ctx context.Context, userID string, page int, limit int) ([]types.Organization, int, error) { +func (m *MockOrganizationRepository) ListAllAccessibleByUserID(ctx context.Context, userID string, page int, limit int) ([]types.Organization, int, error) { args := m.Called(ctx, userID, page, limit) if args.Get(0) == nil { return nil, args.Int(1), args.Error(2) @@ -61,6 +61,22 @@ func (m *MockOrganizationRepository) GetAllAccessibleByUserID(ctx context.Contex return args.Get(0).([]types.Organization), args.Int(1), args.Error(2) } +func (m *MockOrganizationRepository) GetAllAccessibleByUserID(ctx context.Context, userID string) ([]types.Organization, error) { + args := m.Called(ctx, userID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]types.Organization), args.Error(1) +} + +func (m *MockOrganizationRepository) GetAll(ctx context.Context) ([]types.Organization, error) { + args := m.Called(ctx) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]types.Organization), args.Error(1) +} + func (m *MockOrganizationRepository) CountAccessibleByUserID(ctx context.Context, userID string) (int, error) { args := m.Called(ctx, userID) return args.Int(0), args.Error(1) @@ -112,7 +128,7 @@ func (m *MockOrganizationMemberRepository) GetByOrganizationIDAndUserID(ctx cont return nil, nil } -func (m *MockOrganizationMemberRepository) GetAllByOrganizationID(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationMember, int, error) { +func (m *MockOrganizationMemberRepository) ListAllByOrganizationID(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationMember, int, error) { args := m.Called(ctx, organizationID, page, limit) if args.Get(0) == nil { return nil, args.Int(1), args.Error(2) @@ -120,7 +136,15 @@ func (m *MockOrganizationMemberRepository) GetAllByOrganizationID(ctx context.Co return args.Get(0).([]types.OrganizationMember), args.Int(1), args.Error(2) } -func (m *MockOrganizationMemberRepository) GetAllByOrganizationIDWithUser(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationMemberResponse, int, error) { +func (m *MockOrganizationMemberRepository) GetAllByOrganizationID(ctx context.Context, organizationID string) ([]types.OrganizationMember, error) { + args := m.Called(ctx, organizationID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]types.OrganizationMember), args.Error(1) +} + +func (m *MockOrganizationMemberRepository) ListAllByOrganizationIDWithUser(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationMemberResponse, int, error) { args := m.Called(ctx, organizationID, page, limit) if args.Get(0) == nil { return nil, args.Int(1), args.Error(2) @@ -128,6 +152,14 @@ func (m *MockOrganizationMemberRepository) GetAllByOrganizationIDWithUser(ctx co return args.Get(0).([]types.OrganizationMemberResponse), args.Int(1), args.Error(2) } +func (m *MockOrganizationMemberRepository) GetAllByOrganizationIDWithUser(ctx context.Context, organizationID string) ([]types.OrganizationMemberResponse, error) { + args := m.Called(ctx, organizationID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]types.OrganizationMemberResponse), args.Error(1) +} + func (m *MockOrganizationMemberRepository) GetByIDWithUser(ctx context.Context, memberID string) (*types.OrganizationMemberResponse, error) { args := m.Called(ctx, memberID) if args.Get(0) == nil { @@ -208,7 +240,7 @@ func (m *MockOrganizationInvitationRepository) GetByIDWithOrg(ctx context.Contex return args.Get(0).(*types.GetOrganizationInvitationResponse), args.Error(1) } -func (m *MockOrganizationInvitationRepository) GetAllByOrganizationIDWithOrg(ctx context.Context, organizationID string, page int, limit int) ([]types.GetOrganizationInvitationResponse, int, error) { +func (m *MockOrganizationInvitationRepository) ListAllByOrganizationIDWithOrg(ctx context.Context, organizationID string, page int, limit int) ([]types.GetOrganizationInvitationResponse, int, error) { args := m.Called(ctx, organizationID, page, limit) if args.Get(0) == nil { return nil, args.Int(1), args.Error(2) @@ -216,8 +248,24 @@ func (m *MockOrganizationInvitationRepository) GetAllByOrganizationIDWithOrg(ctx return args.Get(0).([]types.GetOrganizationInvitationResponse), args.Int(1), args.Error(2) } -func (m *MockOrganizationInvitationRepository) GetAllPendingByEmail(ctx context.Context, email string, limit int) ([]types.OrganizationInvitation, error) { - args := m.Called(ctx, email, limit) +func (m *MockOrganizationInvitationRepository) GetAllByOrganizationIDWithOrg(ctx context.Context, organizationID string) ([]types.GetOrganizationInvitationResponse, error) { + args := m.Called(ctx, organizationID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]types.GetOrganizationInvitationResponse), args.Error(1) +} + +func (m *MockOrganizationInvitationRepository) ListAllPendingByEmail(ctx context.Context, email string, page int, limit int) ([]types.OrganizationInvitation, int, error) { + args := m.Called(ctx, email, page, limit) + if args.Get(0) == nil { + return nil, args.Int(1), args.Error(2) + } + return args.Get(0).([]types.OrganizationInvitation), args.Int(1), args.Error(2) +} + +func (m *MockOrganizationInvitationRepository) GetAllPendingByEmail(ctx context.Context, email string) ([]types.OrganizationInvitation, error) { + args := m.Called(ctx, email) if args.Get(0) == nil { return nil, args.Error(1) } @@ -269,7 +317,7 @@ func (m *MockOrganizationTeamRepository) GetByOrganizationIDAndSlug(ctx context. return args.Get(0).(*types.OrganizationTeam), args.Error(1) } -func (m *MockOrganizationTeamRepository) GetAllByOrganizationID(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationTeam, int, error) { +func (m *MockOrganizationTeamRepository) ListAllByOrganizationID(ctx context.Context, organizationID string, page int, limit int) ([]types.OrganizationTeam, int, error) { args := m.Called(ctx, organizationID, page, limit) if args.Get(0) == nil { return nil, args.Int(1), args.Error(2) @@ -277,6 +325,14 @@ func (m *MockOrganizationTeamRepository) GetAllByOrganizationID(ctx context.Cont return args.Get(0).([]types.OrganizationTeam), args.Int(1), args.Error(2) } +func (m *MockOrganizationTeamRepository) GetAllByOrganizationID(ctx context.Context, organizationID string) ([]types.OrganizationTeam, error) { + args := m.Called(ctx, organizationID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]types.OrganizationTeam), args.Error(1) +} + func (m *MockOrganizationTeamRepository) Update(ctx context.Context, team *types.OrganizationTeam) (*types.OrganizationTeam, error) { args := m.Called(ctx, team) if mockResultIsNil(args.Get(0)) { @@ -321,7 +377,7 @@ func (m *MockOrganizationTeamMemberRepository) GetByTeamIDAndMemberID(ctx contex return args.Get(0).(*types.OrganizationTeamMember), args.Error(1) } -func (m *MockOrganizationTeamMemberRepository) GetAllByTeamID(ctx context.Context, teamID string, page int, limit int) ([]types.OrganizationTeamMember, int, error) { +func (m *MockOrganizationTeamMemberRepository) ListAllByTeamID(ctx context.Context, teamID string, page int, limit int) ([]types.OrganizationTeamMember, int, error) { args := m.Called(ctx, teamID, page, limit) if args.Get(0) == nil { return nil, args.Int(1), args.Error(2) @@ -329,7 +385,15 @@ func (m *MockOrganizationTeamMemberRepository) GetAllByTeamID(ctx context.Contex return args.Get(0).([]types.OrganizationTeamMember), args.Int(1), args.Error(2) } -func (m *MockOrganizationTeamMemberRepository) GetAllByTeamIDWithMemberAndUser(ctx context.Context, teamID string, page int, limit int) ([]types.OrganizationTeamMemberResponse, int, error) { +func (m *MockOrganizationTeamMemberRepository) GetAllByTeamID(ctx context.Context, teamID string) ([]types.OrganizationTeamMember, error) { + args := m.Called(ctx, teamID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]types.OrganizationTeamMember), args.Error(1) +} + +func (m *MockOrganizationTeamMemberRepository) ListAllByTeamIDWithMemberAndUser(ctx context.Context, teamID string, page int, limit int) ([]types.OrganizationTeamMemberResponse, int, error) { args := m.Called(ctx, teamID, page, limit) if args.Get(0) == nil { return nil, args.Int(1), args.Error(2) @@ -337,6 +401,14 @@ func (m *MockOrganizationTeamMemberRepository) GetAllByTeamIDWithMemberAndUser(c return args.Get(0).([]types.OrganizationTeamMemberResponse), args.Int(1), args.Error(2) } +func (m *MockOrganizationTeamMemberRepository) GetAllByTeamIDWithMemberAndUser(ctx context.Context, teamID string) ([]types.OrganizationTeamMemberResponse, error) { + args := m.Called(ctx, teamID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]types.OrganizationTeamMemberResponse), args.Error(1) +} + func (m *MockOrganizationTeamMemberRepository) GetByIDWithMemberAndUser(ctx context.Context, teamMemberID string) (*types.OrganizationTeamMemberResponse, error) { args := m.Called(ctx, teamMemberID) if args.Get(0) == nil { diff --git a/plugins/organizations/tests/services.go b/plugins/organizations/tests/services.go index 48c8fe2b..bcf8abbf 100644 --- a/plugins/organizations/tests/services.go +++ b/plugins/organizations/tests/services.go @@ -30,7 +30,7 @@ func (m *MockOrganizationService) CreateOrganization(ctx context.Context, actor return args.Get(0).(*types.Organization), args.Error(1) } -func (m *MockOrganizationService) GetAllOrganizations(ctx context.Context, actor *models.Actor, params pagination.Params) (*types.ListOrganizationsResponse, error) { +func (m *MockOrganizationService) ListAllOrganizations(ctx context.Context, actor *models.Actor, params pagination.Params) (*types.ListOrganizationsResponse, error) { args := m.Called(ctx, actorID(actor), params) if args.Get(0) == nil { return nil, args.Error(1) @@ -38,6 +38,22 @@ func (m *MockOrganizationService) GetAllOrganizations(ctx context.Context, actor return args.Get(0).(*types.ListOrganizationsResponse), args.Error(1) } +func (m *MockOrganizationService) GetAllOrganizations(ctx context.Context, actor *models.Actor) ([]types.Organization, error) { + args := m.Called(ctx, actor) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]types.Organization), args.Error(1) +} + +func (m *MockOrganizationService) GetAllOrganizationsUnscoped(ctx context.Context) ([]types.Organization, error) { + args := m.Called(ctx) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]types.Organization), args.Error(1) +} + func (m *MockOrganizationService) GetOrganizationByID(ctx context.Context, actor *models.Actor, organizationID string) (*types.Organization, error) { args := m.Called(ctx, actorID(actor), organizationID) if args.Get(0) == nil { @@ -100,7 +116,7 @@ func (m *MockOrganizationInvitationService) GetOrganizationInvitationByIDWithOrg return args.Get(0).(*types.GetOrganizationInvitationResponse), args.Error(1) } -func (m *MockOrganizationInvitationService) GetAllOrganizationInvitationsByOrgIDWithOrg(ctx context.Context, organizationID string, params pagination.Params) (*types.ListOrganizationInvitationsResponse, error) { +func (m *MockOrganizationInvitationService) ListAllOrganizationInvitationsByOrgIDWithOrg(ctx context.Context, organizationID string, params pagination.Params) (*types.ListOrganizationInvitationsResponse, error) { args := m.Called(ctx, organizationID, params) if args.Get(0) == nil { return nil, args.Error(1) @@ -108,6 +124,14 @@ func (m *MockOrganizationInvitationService) GetAllOrganizationInvitationsByOrgID return args.Get(0).(*types.ListOrganizationInvitationsResponse), args.Error(1) } +func (m *MockOrganizationInvitationService) GetAllOrganizationInvitationsByOrgIDWithOrg(ctx context.Context, organizationID string) ([]types.GetOrganizationInvitationResponse, error) { + args := m.Called(ctx, organizationID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]types.GetOrganizationInvitationResponse), args.Error(1) +} + func (m *MockOrganizationInvitationService) RevokeOrganizationInvitation(ctx context.Context, actor *models.Actor, organizationID string, invitationID string) (*types.OrganizationInvitation, error) { args := m.Called(ctx, actorID(actor), organizationID, invitationID) if args.Get(0) == nil { @@ -144,7 +168,7 @@ func (m *MockOrganizationMemberService) AddMember(ctx context.Context, actor *mo return args.Get(0).(*types.OrganizationMember), args.Error(1) } -func (m *MockOrganizationMemberService) GetAllMembers(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationMembersResponse, error) { +func (m *MockOrganizationMemberService) ListAllMembers(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationMembersResponse, error) { args := m.Called(ctx, actorID(actor), organizationID, params) if args.Get(0) == nil { return nil, args.Error(1) @@ -152,6 +176,14 @@ func (m *MockOrganizationMemberService) GetAllMembers(ctx context.Context, actor return args.Get(0).(*types.ListOrganizationMembersResponse), args.Error(1) } +func (m *MockOrganizationMemberService) GetAllMembers(ctx context.Context, actor *models.Actor, organizationID string) ([]types.OrganizationMemberResponse, error) { + args := m.Called(ctx, actor, organizationID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]types.OrganizationMemberResponse), args.Error(1) +} + func (m *MockOrganizationMemberService) GetMember(ctx context.Context, actor *models.Actor, organizationID string, memberID string) (*types.OrganizationMemberResponse, error) { args := m.Called(ctx, actorID(actor), organizationID, memberID) if args.Get(0) == nil { @@ -193,7 +225,7 @@ func (m *MockOrganizationTeamService) CreateTeam(ctx context.Context, actor *mod return args.Get(0).(*types.OrganizationTeam), args.Error(1) } -func (m *MockOrganizationTeamService) GetAllTeams(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationTeamsResponse, error) { +func (m *MockOrganizationTeamService) ListAllTeams(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationTeamsResponse, error) { args := m.Called(ctx, actorID(actor), organizationID, params) if args.Get(0) == nil { return nil, args.Error(1) @@ -201,6 +233,14 @@ func (m *MockOrganizationTeamService) GetAllTeams(ctx context.Context, actor *mo return args.Get(0).(*types.ListOrganizationTeamsResponse), args.Error(1) } +func (m *MockOrganizationTeamService) GetAllTeams(ctx context.Context, actor *models.Actor, organizationID string) ([]types.OrganizationTeam, error) { + args := m.Called(ctx, actor, organizationID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]types.OrganizationTeam), args.Error(1) +} + func (m *MockOrganizationTeamService) GetTeam(ctx context.Context, actor *models.Actor, organizationID string, teamID string) (*types.OrganizationTeam, error) { args := m.Called(ctx, actorID(actor), organizationID, teamID) if args.Get(0) == nil { @@ -234,7 +274,7 @@ func (m *MockOrganizationTeamMemberService) AddTeamMember(ctx context.Context, a return args.Get(0).(*types.OrganizationTeamMember), args.Error(1) } -func (m *MockOrganizationTeamMemberService) GetAllTeamMembers(ctx context.Context, actor *models.Actor, organizationID string, teamID string, params pagination.Params) (*types.ListOrganizationTeamMembersResponse, error) { +func (m *MockOrganizationTeamMemberService) ListAllTeamMembers(ctx context.Context, actor *models.Actor, organizationID string, teamID string, params pagination.Params) (*types.ListOrganizationTeamMembersResponse, error) { args := m.Called(ctx, actorID(actor), organizationID, teamID, params) if args.Get(0) == nil { return nil, args.Error(1) @@ -242,6 +282,14 @@ func (m *MockOrganizationTeamMemberService) GetAllTeamMembers(ctx context.Contex return args.Get(0).(*types.ListOrganizationTeamMembersResponse), args.Error(1) } +func (m *MockOrganizationTeamMemberService) GetAllTeamMembers(ctx context.Context, actor *models.Actor, organizationID string, teamID string) ([]types.OrganizationTeamMemberResponse, error) { + args := m.Called(ctx, actor, organizationID, teamID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]types.OrganizationTeamMemberResponse), args.Error(1) +} + func (m *MockOrganizationTeamMemberService) GetTeamMember(ctx context.Context, actor *models.Actor, organizationID string, teamID string, memberID string) (*types.OrganizationTeamMemberResponse, error) { args := m.Called(ctx, actorID(actor), organizationID, teamID, memberID) if args.Get(0) == nil { diff --git a/plugins/organizations/usecases/usecases.go b/plugins/organizations/usecases/usecases.go index 4de128b5..c151fe9f 100644 --- a/plugins/organizations/usecases/usecases.go +++ b/plugins/organizations/usecases/usecases.go @@ -99,8 +99,8 @@ func (u *UseCases) CreateOrganization(ctx context.Context, actor *models.Actor, return u.orgService.CreateOrganization(ctx, actor, request) } -func (u *UseCases) GetAllOrganizations(ctx context.Context, actor *models.Actor, params pagination.Params) (*types.ListOrganizationsResponse, error) { - return u.orgService.GetAllOrganizations(ctx, actor, params) +func (u *UseCases) ListAllOrganizations(ctx context.Context, actor *models.Actor, params pagination.Params) (*types.ListOrganizationsResponse, error) { + return u.orgService.ListAllOrganizations(ctx, actor, params) } func (u *UseCases) GetOrganizationByID(ctx context.Context, actor *models.Actor, organizationID string) (*types.Organization, error) { @@ -131,12 +131,12 @@ func (u *UseCases) CreateOrganizationInvitation(ctx context.Context, actor *mode return u.invitationService.CreateOrganizationInvitation(ctx, actor, organizationID, request, redirectURL) } -func (u *UseCases) GetAllOrganizationInvitations(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationInvitationsResponse, error) { +func (u *UseCases) ListAllOrganizationInvitations(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationInvitationsResponse, error) { if err := u.authorizeOrgAccess(ctx, actor, organizationID, orgconstants.OrganizationsInvitationsListPermission); err != nil { return nil, err } - resp, err := u.invitationService.GetAllOrganizationInvitationsByOrgIDWithOrg(ctx, organizationID, params) + resp, err := u.invitationService.ListAllOrganizationInvitationsByOrgIDWithOrg(ctx, organizationID, params) if err != nil { return nil, err } @@ -194,11 +194,11 @@ func (u *UseCases) AddMember(ctx context.Context, actor *models.Actor, organizat return u.memberService.AddMember(ctx, actor, organizationID, request) } -func (u *UseCases) GetAllMembers(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationMembersResponse, error) { +func (u *UseCases) ListAllMembers(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationMembersResponse, error) { if err := u.authorizeOrgAccess(ctx, actor, organizationID, orgconstants.OrganizationsMembersListPermission); err != nil { return nil, err } - return u.memberService.GetAllMembers(ctx, actor, organizationID, params) + return u.memberService.ListAllMembers(ctx, actor, organizationID, params) } func (u *UseCases) GetMember(ctx context.Context, actor *models.Actor, organizationID string, memberID string) (*types.OrganizationMemberResponse, error) { @@ -238,11 +238,11 @@ func (u *UseCases) CreateTeam(ctx context.Context, actor *models.Actor, organiza return u.teamService.CreateTeam(ctx, actor, organizationID, request) } -func (u *UseCases) GetAllTeams(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationTeamsResponse, error) { +func (u *UseCases) ListAllTeams(ctx context.Context, actor *models.Actor, organizationID string, params pagination.Params) (*types.ListOrganizationTeamsResponse, error) { if err := u.authorizeOrgAccess(ctx, actor, organizationID, orgconstants.OrganizationsTeamsListPermission); err != nil { return nil, err } - return u.teamService.GetAllTeams(ctx, actor, organizationID, params) + return u.teamService.ListAllTeams(ctx, actor, organizationID, params) } func (u *UseCases) GetTeam(ctx context.Context, actor *models.Actor, organizationID string, teamID string) (*types.OrganizationTeam, error) { @@ -275,11 +275,11 @@ func (u *UseCases) AddTeamMember(ctx context.Context, actor *models.Actor, organ return u.teamMemberService.AddTeamMember(ctx, actor, organizationID, teamID, request) } -func (u *UseCases) GetAllTeamMembers(ctx context.Context, actor *models.Actor, organizationID string, teamID string, params pagination.Params) (*types.ListOrganizationTeamMembersResponse, error) { +func (u *UseCases) ListAllTeamMembers(ctx context.Context, actor *models.Actor, organizationID string, teamID string, params pagination.Params) (*types.ListOrganizationTeamMembersResponse, error) { if err := u.authorizeOrgAccess(ctx, actor, organizationID, orgconstants.OrganizationsTeamMembersListPermission); err != nil { return nil, err } - return u.teamMemberService.GetAllTeamMembers(ctx, actor, organizationID, teamID, params) + return u.teamMemberService.ListAllTeamMembers(ctx, actor, organizationID, teamID, params) } func (u *UseCases) GetTeamMember(ctx context.Context, actor *models.Actor, organizationID string, teamID string, memberID string) (*types.OrganizationTeamMemberResponse, error) {