diff --git a/.gitignore b/.gitignore index 61618e3a..7321972f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,18 @@ audit.log identity-api !chart/identity-api +# vscode stuff +.vscode/* +.vscode/settings.json +!.vscode/tasks.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# binary files +tmp diff --git a/internal/api/httpsrv/handler_group.go b/internal/api/httpsrv/handler_group.go index 612ad608..bc09944a 100644 --- a/internal/api/httpsrv/handler_group.go +++ b/internal/api/httpsrv/handler_group.go @@ -237,6 +237,15 @@ func (h *apiHandler) DeleteGroup(ctx context.Context, req DeleteGroupRequestObje group, err := h.engine.GetGroupByID(ctx, gid) if err != nil { + if errors.Is(err, types.ErrNotFound) { + err = echo.NewHTTPError( + http.StatusNotFound, + fmt.Sprintf("group %s not found", gid), + ) + + return nil, err + } + return nil, err } diff --git a/internal/api/httpsrv/handler_group_members_test.go b/internal/api/httpsrv/handler_group_members_test.go index 4f94e7ed..de3c523d 100644 --- a/internal/api/httpsrv/handler_group_members_test.go +++ b/internal/api/httpsrv/handler_group_members_test.go @@ -2,12 +2,14 @@ package httpsrv import ( "context" + "fmt" "net/http" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" pagination "go.infratographer.com/identity-api/internal/crdbx" + "go.infratographer.com/identity-api/internal/events" "go.infratographer.com/identity-api/internal/storage" "go.infratographer.com/identity-api/internal/testingx" "go.infratographer.com/identity-api/internal/types" @@ -44,13 +46,22 @@ func TestGroupMembersAPIHandler(t *testing.T) { assert.FailNow(t, "initialization failed") } - setupFn := func(ctx context.Context) context.Context { - ctx, err := store.BeginContext(ctx) + events := events.NewEvents() + + beginTx := func(ctx context.Context) context.Context { + tx, err := store.BeginContext(ctx) if !assert.NoError(t, err) { assert.FailNow(t, "setup failed") } - return ctx + return tx + } + + setupFn := func(ctx context.Context) context.Context { + pub := testingx.NewTestPublisher() + ctxp := pub.ContextWithPublisher(ctx) + + return beginTx(ctxp) } cleanupFn := func(ctx context.Context) { @@ -61,7 +72,10 @@ func TestGroupMembersAPIHandler(t *testing.T) { t.Run("ListGroupMembers", func(t *testing.T) { t.Parallel() - handler := apiHandler{engine: store} + handler := apiHandler{ + engine: store, + eventService: events, + } testGroup := &types.Group{ ID: gidx.MustNewID(types.IdentityGroupIDPrefix), @@ -149,12 +163,21 @@ func TestGroupMembersAPIHandler(t *testing.T) { t.Run("AddGroupMembers", func(t *testing.T) { t.Parallel() - handler := apiHandler{engine: store} + handler := apiHandler{ + engine: store, + eventService: events, + } + + testGroupWithNoMember := &types.Group{ + ID: gidx.MustNewID(types.IdentityGroupIDPrefix), + OwnerID: ownerID, + Name: "test-add-group-member", + } - testGroupWithNoMembers := &types.Group{ + theOtherTestGroupWithNoMember := &types.Group{ ID: gidx.MustNewID(types.IdentityGroupIDPrefix), OwnerID: ownerID, - Name: "test-add-group-members", + Name: "test-add-group-member-1", } testGroupWithSomeMembers := &types.Group{ @@ -169,7 +192,8 @@ func TestGroupMembersAPIHandler(t *testing.T) { gidx.MustNewID(types.IdentityUserIDPrefix), } - withStoredGroupAndMembers(t, store, testGroupWithNoMembers) + withStoredGroupAndMembers(t, store, testGroupWithNoMember) + withStoredGroupAndMembers(t, store, theOtherTestGroupWithNoMember) withStoredGroupAndMembers(t, store, testGroupWithSomeMembers, someMembers...) tc := []testingx.TestCase[AddGroupMembersRequestObject, []gidx.PrefixedID]{ @@ -194,41 +218,82 @@ func TestGroupMembersAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: cleanupFn, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { assert.Nil(t, res.Success) assert.IsType(t, &echo.HTTPError{}, res.Err) assert.Equal(t, http.StatusNotFound, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) }, }, { Name: "Invalid member id", Input: AddGroupMembersRequestObject{ - GroupID: testGroupWithNoMembers.ID, + GroupID: testGroupWithNoMember.ID, Body: &v1.AddGroupMembersJSONRequestBody{ MemberIDs: []gidx.PrefixedID{"definitely not a valid member id"}, }, }, SetupFn: setupFn, CleanupFn: cleanupFn, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { assert.Nil(t, res.Success) assert.IsType(t, &echo.HTTPError{}, res.Err) assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) + }, + }, + { + Name: "Failed to publish event", + Input: AddGroupMembersRequestObject{ + GroupID: theOtherTestGroupWithNoMember.ID, + Body: &v1.AddGroupMembersJSONRequestBody{ + MemberIDs: []gidx.PrefixedID{gidx.MustNewID(types.IdentityUserIDPrefix)}, + }, + }, + SetupFn: func(ctx context.Context) context.Context { + pub := testingx.NewTestPublisher(testingx.TestPublisherWithError(fmt.Errorf("you bad bad"))) // nolint: goerr113 + ctxp := pub.ContextWithPublisher(ctx) + + return beginTx(ctxp) + }, + CleanupFn: func(_ context.Context) {}, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Error(t, res.Err) + assert.ErrorContains(t, res.Err, "failed to add group members in permissions API") + + // ensure no members were added + mc, err := store.GroupMembersCount(context.Background(), theOtherTestGroupWithNoMember.ID) + assert.NoError(t, err) + assert.Equal(t, 0, mc) }, }, { Name: "Success", Input: AddGroupMembersRequestObject{ - GroupID: testGroupWithNoMembers.ID, + GroupID: testGroupWithNoMember.ID, Body: &v1.AddGroupMembersJSONRequestBody{ MemberIDs: []gidx.PrefixedID{gidx.MustNewID(types.IdentityUserIDPrefix)}, }, }, SetupFn: setupFn, CleanupFn: func(_ context.Context) {}, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { assert.Nil(t, res.Err) assert.Len(t, res.Success, 1) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Len(t, pub.CalledWith(), 1) + + cw := pub.CalledWith()[0] + assert.Equal(t, testingx.TestPublisherMethodCreate, cw.Method) + assert.Len(t, cw.Relations, len(res.Success)) }, }, { @@ -241,9 +306,17 @@ func TestGroupMembersAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: func(_ context.Context) {}, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { assert.Nil(t, res.Err) assert.Len(t, res.Success, len(someMembers)) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Len(t, pub.CalledWith(), 1) + + cw := pub.CalledWith()[0] + assert.Equal(t, testingx.TestPublisherMethodCreate, cw.Method) + assert.Len(t, cw.Relations, 1) }, }, } @@ -259,20 +332,22 @@ func TestGroupMembersAPIHandler(t *testing.T) { } ctx = context.Background() - ctx = pagination.AsOfSystemTime(ctx, "") - p := v1.ListGroupMembersParams{} - mm, err := store.ListGroupMembers(ctx, input.GroupID, p) + mm, err := store.ListGroupMembers(ctx, input.GroupID, nil) return testingx.TestResult[[]gidx.PrefixedID]{Success: mm, Err: err} } - testingx.RunTests(ctxPermsAllow(context.Background()), t, tc, runFn) + ctx := testingx.NewTestPublisher().ContextWithPublisher(ctxPermsAllow(context.Background())) + testingx.RunTests(ctx, t, tc, runFn) }) t.Run("RemoveGroupMember", func(t *testing.T) { t.Parallel() - handler := apiHandler{engine: store} + handler := apiHandler{ + engine: store, + eventService: events, + } testGroup := &types.Group{ ID: gidx.MustNewID(types.IdentityGroupIDPrefix), @@ -280,6 +355,12 @@ func TestGroupMembersAPIHandler(t *testing.T) { Name: "test-remove-group-member", } + theOtherTestGroup := &types.Group{ + ID: gidx.MustNewID(types.IdentityGroupIDPrefix), + OwnerID: ownerID, + Name: "test-remove-group-member-1", + } + someMembers := []gidx.PrefixedID{ gidx.MustNewID(types.IdentityUserIDPrefix), gidx.MustNewID(types.IdentityUserIDPrefix), @@ -287,6 +368,7 @@ func TestGroupMembersAPIHandler(t *testing.T) { } withStoredGroupAndMembers(t, store, testGroup, someMembers...) + withStoredGroupAndMembers(t, store, theOtherTestGroup, someMembers...) tc := []testingx.TestCase[RemoveGroupMemberRequestObject, []gidx.PrefixedID]{ { @@ -297,10 +379,14 @@ func TestGroupMembersAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: cleanupFn, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { assert.Nil(t, res.Success) assert.IsType(t, &echo.HTTPError{}, res.Err) assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) }, }, { @@ -311,10 +397,14 @@ func TestGroupMembersAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: cleanupFn, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { assert.Nil(t, res.Success) assert.IsType(t, &echo.HTTPError{}, res.Err) assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) }, }, { @@ -325,10 +415,14 @@ func TestGroupMembersAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: cleanupFn, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { assert.Nil(t, res.Success) assert.IsType(t, &echo.HTTPError{}, res.Err) assert.Equal(t, http.StatusNotFound, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) }, }, { @@ -339,10 +433,36 @@ func TestGroupMembersAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: cleanupFn, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { assert.Nil(t, res.Success) assert.IsType(t, &echo.HTTPError{}, res.Err) assert.Equal(t, http.StatusNotFound, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) + }, + }, + { + Name: "Failed to publish event", + Input: RemoveGroupMemberRequestObject{ + GroupID: theOtherTestGroup.ID, + SubjectID: someMembers[0], + }, + SetupFn: func(ctx context.Context) context.Context { + pub := testingx.NewTestPublisher(testingx.TestPublisherWithError(fmt.Errorf("you bad bad"))) // nolint: goerr113 + ctxp := pub.ContextWithPublisher(ctx) + + return beginTx(ctxp) + }, + CleanupFn: func(_ context.Context) {}, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Error(t, res.Err) + + // ensure the member is still in the group + mc, err := store.GroupMembersCount(context.Background(), theOtherTestGroup.ID) + assert.NoError(t, err) + assert.Len(t, someMembers, mc) }, }, { @@ -353,9 +473,19 @@ func TestGroupMembersAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: func(_ context.Context) {}, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { assert.Nil(t, res.Err) assert.Len(t, res.Success, len(someMembers)-1) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Len(t, pub.CalledWith(), 1) + + cw := pub.CalledWith()[0] + assert.Equal(t, testingx.TestPublisherMethodDelete, cw.Method) + assert.Equal(t, cw.ResourceID, testGroup.ID) + assert.Len(t, cw.Relations, 1) + assert.Equal(t, someMembers[0], cw.Relations[0].SubjectID) }, }, } @@ -371,9 +501,7 @@ func TestGroupMembersAPIHandler(t *testing.T) { } ctx = context.Background() - ctx = pagination.AsOfSystemTime(ctx, "") - p := v1.ListGroupMembersParams{} - mm, err := store.ListGroupMembers(ctx, input.GroupID, p) + mm, err := store.ListGroupMembers(ctx, input.GroupID, nil) return testingx.TestResult[[]gidx.PrefixedID]{Success: mm, Err: err} } @@ -384,7 +512,10 @@ func TestGroupMembersAPIHandler(t *testing.T) { t.Run("PutGroupMembers", func(t *testing.T) { t.Parallel() - handler := apiHandler{engine: store} + handler := apiHandler{ + engine: store, + eventService: events, + } testGroup := &types.Group{ ID: gidx.MustNewID(types.IdentityGroupIDPrefix), @@ -392,6 +523,12 @@ func TestGroupMembersAPIHandler(t *testing.T) { Name: "test-put-group-members", } + theOtherTestGroup := &types.Group{ + ID: gidx.MustNewID(types.IdentityGroupIDPrefix), + OwnerID: ownerID, + Name: "test-put-group-members-1", + } + someMembers := []gidx.PrefixedID{ gidx.MustNewID(types.IdentityUserIDPrefix), gidx.MustNewID(types.IdentityUserIDPrefix), @@ -399,6 +536,7 @@ func TestGroupMembersAPIHandler(t *testing.T) { } withStoredGroupAndMembers(t, store, testGroup, someMembers...) + withStoredGroupAndMembers(t, store, theOtherTestGroup, someMembers...) tc := []testingx.TestCase[ReplaceGroupMembersRequestObject, []gidx.PrefixedID]{ { @@ -411,10 +549,14 @@ func TestGroupMembersAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: cleanupFn, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { assert.Nil(t, res.Success) assert.IsType(t, &echo.HTTPError{}, res.Err) assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) }, }, { @@ -427,10 +569,14 @@ func TestGroupMembersAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: cleanupFn, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { assert.Nil(t, res.Success) assert.IsType(t, &echo.HTTPError{}, res.Err) assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) }, }, { @@ -443,10 +589,39 @@ func TestGroupMembersAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: cleanupFn, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { assert.Nil(t, res.Success) assert.IsType(t, &echo.HTTPError{}, res.Err) assert.Equal(t, http.StatusNotFound, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) + }, + }, + { + Name: "Failed to publish event", + Input: ReplaceGroupMembersRequestObject{ + GroupID: theOtherTestGroup.ID, + Body: &v1.ReplaceGroupMembersJSONRequestBody{ + MemberIDs: []gidx.PrefixedID{gidx.MustNewID(types.IdentityUserIDPrefix)}, + }, + }, + SetupFn: func(ctx context.Context) context.Context { + pub := testingx.NewTestPublisher(testingx.TestPublisherWithError(fmt.Errorf("you bad bad"))) // nolint: goerr113 + ctxp := pub.ContextWithPublisher(ctx) + + return beginTx(ctxp) + }, + CleanupFn: func(_ context.Context) {}, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + assert.Error(t, res.Err) + assert.ErrorContains(t, res.Err, "failed to replace group members in permissions API") + + // ensure no members were added + mc, err := store.GroupMembersCount(context.Background(), theOtherTestGroup.ID) + assert.NoError(t, err) + assert.Equal(t, len(someMembers), mc) }, }, { @@ -459,9 +634,23 @@ func TestGroupMembersAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: func(_ context.Context) {}, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[[]gidx.PrefixedID]) { assert.Nil(t, res.Err) assert.Len(t, res.Success, 1) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Len(t, pub.CalledWith(), 2) + + for _, cw := range pub.CalledWith() { + assert.Equal(t, testGroup.ID, cw.ResourceID) + + if cw.Method == testingx.TestPublisherMethodCreate { + assert.Len(t, cw.Relations, 1) + } else if cw.Method == testingx.TestPublisherMethodDelete { + assert.Len(t, cw.Relations, len(someMembers)) + } + } }, }, } @@ -477,14 +666,13 @@ func TestGroupMembersAPIHandler(t *testing.T) { } ctx = context.Background() - ctx = pagination.AsOfSystemTime(ctx, "") - p := v1.ListGroupMembersParams{} - mm, err := store.ListGroupMembers(ctx, input.GroupID, p) + mm, err := store.ListGroupMembers(ctx, input.GroupID, nil) return testingx.TestResult[[]gidx.PrefixedID]{Success: mm, Err: err} } - testingx.RunTests(ctxPermsAllow(context.Background()), t, tc, runFn) + ctx := testingx.NewTestPublisher().ContextWithPublisher(ctxPermsAllow(context.Background())) + testingx.RunTests(ctx, t, tc, runFn) }) } diff --git a/internal/api/httpsrv/handler_group_test.go b/internal/api/httpsrv/handler_group_test.go index 0797d6ee..1e0ab059 100644 --- a/internal/api/httpsrv/handler_group_test.go +++ b/internal/api/httpsrv/handler_group_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" pagination "go.infratographer.com/identity-api/internal/crdbx" + "go.infratographer.com/identity-api/internal/events" "go.infratographer.com/identity-api/internal/storage" "go.infratographer.com/identity-api/internal/testingx" "go.infratographer.com/identity-api/internal/types" @@ -47,11 +48,14 @@ func TestGroupAPIHandler(t *testing.T) { assert.FailNow(t, "initialization failed") } + es := events.NewEvents() + t.Run("GetGroup", func(t *testing.T) { t.Parallel() handler := apiHandler{ - engine: store, + engine: store, + eventService: es, } getGroupTestGroup := &types.Group{ @@ -133,7 +137,10 @@ func TestGroupAPIHandler(t *testing.T) { t.Run("CreateGroup", func(t *testing.T) { t.Parallel() - handler := apiHandler{engine: store} + handler := apiHandler{ + engine: store, + eventService: es, + } createGroupTestGroup := &types.Group{ ID: gidx.MustNewID(types.IdentityGroupIDPrefix), @@ -144,13 +151,20 @@ func TestGroupAPIHandler(t *testing.T) { withStoredGroups(t, store, createGroupTestGroup) - setupFn := func(ctx context.Context) context.Context { - ctx, err := store.BeginContext(ctx) + beginTx := func(ctx context.Context) context.Context { + tx, err := store.BeginContext(ctx) if !assert.NoError(t, err) { assert.FailNow(t, "setup failed") } - return ctx + return tx + } + + setupFn := func(ctx context.Context) context.Context { + pub := testingx.NewTestPublisher() + ctxp := pub.ContextWithPublisher(ctx) + + return beginTx(ctxp) } cleanupFn := func(ctx context.Context) { @@ -160,6 +174,7 @@ func TestGroupAPIHandler(t *testing.T) { runFn := func(ctx context.Context, input CreateGroupRequestObject) testingx.TestResult[CreateGroupResponseObject] { resp, err := handler.CreateGroup(ctx, input) + return testingx.TestResult[CreateGroupResponseObject]{Success: resp, Err: err} } @@ -171,10 +186,14 @@ func TestGroupAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: cleanupFn, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[CreateGroupResponseObject]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[CreateGroupResponseObject]) { assert.Error(t, res.Err) assert.IsType(t, &echo.HTTPError{}, res.Err) assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) }, }, { @@ -187,10 +206,14 @@ func TestGroupAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: cleanupFn, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[CreateGroupResponseObject]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[CreateGroupResponseObject]) { assert.Error(t, res.Err) assert.IsType(t, &echo.HTTPError{}, res.Err) assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) }, }, { @@ -203,10 +226,48 @@ func TestGroupAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: cleanupFn, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[CreateGroupResponseObject]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[CreateGroupResponseObject]) { assert.Error(t, res.Err) assert.IsType(t, &echo.HTTPError{}, res.Err, "unexpected error type", res.Err.Error()) assert.Equal(t, http.StatusConflict, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) + }, + }, + { + Name: "Fail to publish group relationship", + Input: CreateGroupRequestObject{ + OwnerID: ownerID, + Body: &v1.CreateGroupJSONRequestBody{ + Name: "test-creategroup-1", + Description: ptr("it's a group for testing create group"), + }, + }, + CleanupFn: func(_ context.Context) {}, + SetupFn: func(ctx context.Context) context.Context { + p := testingx.NewTestPublisher(testingx.TestPublisherWithError(fmt.Errorf("you bad bad"))) // nolint: goerr113 + ctxp := p.ContextWithPublisher(ctx) + + return beginTx(ctxp) + }, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[CreateGroupResponseObject]) { + assert.NotNil(t, res.Err) + assert.Nil(t, res.Success) + + // ensure group will not be created + ctx := context.Background() + ctx = pagination.AsOfSystemTime(ctx, "") + p := v1.ListGroupsParams{} + g, err := store.ListGroupsByOwner(ctx, ownerID, p) + assert.NoError(t, err) + + for _, gg := range g { + if gg.OwnerID == ownerID { + assert.NotEqual(t, "test-creategroup-1", gg.Name) + } + } }, }, { @@ -233,6 +294,16 @@ func TestGroupAPIHandler(t *testing.T) { assert.Equal(t, item.ID, group.ID) assert.Equal(t, item.Name, group.Name) assert.Equal(t, *item.Description, group.Description) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Len(t, pub.CalledWith(), 1) + + cw := pub.CalledWith()[0] + assert.Equal(t, testingx.TestPublisherMethodCreate, cw.Method) + assert.Equal(t, events.GroupTopic, cw.Topic) + assert.Equal(t, item.ID, cw.ResourceID) + assert.Equal(t, *item.OwnerID, cw.Relations[0].SubjectID) }, }, } @@ -245,9 +316,12 @@ func TestGroupAPIHandler(t *testing.T) { const numOfGroups = 5 - theOtherOwnerID := gidx.MustNewID("testten") - handler := apiHandler{engine: store} listGroupsTestGroups := make([]*types.Group, numOfGroups) + theOtherOwnerID := gidx.MustNewID("testten") + handler := apiHandler{ + engine: store, + eventService: es, + } for i := 0; i < numOfGroups; i++ { listGroupsTestGroups[i] = &types.Group{ @@ -351,7 +425,10 @@ func TestGroupAPIHandler(t *testing.T) { t.Run("UpdateGroup", func(t *testing.T) { t.Parallel() - handler := apiHandler{engine: store} + handler := apiHandler{ + engine: store, + eventService: es, + } theOtherOwnerID := gidx.MustNewID("testten") @@ -494,7 +571,11 @@ func TestGroupAPIHandler(t *testing.T) { t.Run("DeleteGroup", func(t *testing.T) { t.Parallel() - handler := apiHandler{engine: store} + handler := apiHandler{ + engine: store, + eventService: es, + } + deleteGroupTestGroup := &types.Group{ ID: gidx.MustNewID(types.IdentityGroupIDPrefix), OwnerID: ownerID, @@ -504,13 +585,35 @@ func TestGroupAPIHandler(t *testing.T) { withStoredGroups(t, store, deleteGroupTestGroup) - setupFn := func(ctx context.Context) context.Context { - ctx, err := store.BeginContext(ctx) + testGroupWithSomeMembers := &types.Group{ + ID: gidx.MustNewID(types.IdentityGroupIDPrefix), + OwnerID: ownerID, + Name: "test-deletegroup-2", + Description: "it's a group for testing delete group", + } + + someMembers := []gidx.PrefixedID{ + gidx.MustNewID(types.IdentityUserIDPrefix), + gidx.MustNewID(types.IdentityUserIDPrefix), + gidx.MustNewID(types.IdentityUserIDPrefix), + } + + withStoredGroupAndMembers(t, store, testGroupWithSomeMembers, someMembers...) + + beginTx := func(ctx context.Context) context.Context { + tx, err := store.BeginContext(ctx) if !assert.NoError(t, err) { assert.FailNow(t, "setup failed") } - return ctx + return tx + } + + setupFn := func(ctx context.Context) context.Context { + pub := testingx.NewTestPublisher() + ctxp := pub.ContextWithPublisher(ctx) + + return beginTx(ctxp) } cleanupFn := func(ctx context.Context) { @@ -531,10 +634,14 @@ func TestGroupAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: cleanupFn, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[DeleteGroupResponseObject]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[DeleteGroupResponseObject]) { assert.Error(t, res.Err) assert.IsType(t, &echo.HTTPError{}, res.Err) assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) }, }, { @@ -544,10 +651,53 @@ func TestGroupAPIHandler(t *testing.T) { }, SetupFn: setupFn, CleanupFn: cleanupFn, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[DeleteGroupResponseObject]) { + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[DeleteGroupResponseObject]) { assert.Error(t, res.Err) assert.IsType(t, &echo.HTTPError{}, res.Err) assert.Equal(t, http.StatusNotFound, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) + }, + }, + { + Name: "Group with members", + Input: DeleteGroupRequestObject{ + GroupID: testGroupWithSomeMembers.ID, + }, + SetupFn: setupFn, + CleanupFn: cleanupFn, + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[DeleteGroupResponseObject]) { + assert.Error(t, res.Err) + assert.IsType(t, &echo.HTTPError{}, res.Err) + assert.Equal(t, http.StatusBadRequest, res.Err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Empty(t, pub.CalledWith()) + }, + }, + { + Name: "Fail to publish group relationship", + Input: DeleteGroupRequestObject{ + GroupID: deleteGroupTestGroup.ID, + }, + SetupFn: func(ctx context.Context) context.Context { + p := testingx.NewTestPublisher(testingx.TestPublisherWithError(fmt.Errorf("you bad bad"))) // nolint: goerr113 + ctxp := p.ContextWithPublisher(ctx) + + return beginTx(ctxp) + }, + CleanupFn: func(_ context.Context) {}, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[DeleteGroupResponseObject]) { + assert.NotNil(t, res.Err) + assert.Nil(t, res.Success) + + // ensure group will not be deleted + group, err := store.GetGroupByID(context.Background(), deleteGroupTestGroup.ID) + assert.NoError(t, err) + assert.NotNil(t, group) }, }, { @@ -565,11 +715,21 @@ func TestGroupAPIHandler(t *testing.T) { assert.Error(t, err) assert.IsType(t, &echo.HTTPError{}, err) assert.Equal(t, http.StatusNotFound, err.(*echo.HTTPError).Code) + + pub, ok := testingx.GetPublisherFromContext(ctx) + assert.Equal(t, true, ok) + assert.Len(t, pub.CalledWith(), 1) + + cw := pub.CalledWith()[0] + assert.Equal(t, testingx.TestPublisherMethodDelete, cw.Method) + assert.Equal(t, events.GroupTopic, cw.Topic) + assert.Equal(t, deleteGroupTestGroup.ID, cw.ResourceID) }, }, } - testingx.RunTests(ctxPermsAllow(context.Background()), t, tc, runFn) + ctx := testingx.NewTestPublisher().ContextWithPublisher(ctxPermsAllow(context.Background())) + testingx.RunTests(ctx, t, tc, runFn) }) } diff --git a/internal/testingx/pubsub.go b/internal/testingx/pubsub.go new file mode 100644 index 00000000..2e459420 --- /dev/null +++ b/internal/testingx/pubsub.go @@ -0,0 +1,96 @@ +package testingx + +import ( + "context" + + "go.infratographer.com/permissions-api/pkg/permissions" + "go.infratographer.com/x/events" + "go.infratographer.com/x/gidx" +) + +// TestPublisherMethod is a type for the methods of the TestPublisher +type TestPublisherMethod string + +const ( + // TestPublisherMethodCreate is the method name for CreateAuthRelationships + TestPublisherMethodCreate TestPublisherMethod = "CreateAuthRelationships" + // TestPublisherMethodDelete is the method name for DeleteAuthRelationships + TestPublisherMethodDelete TestPublisherMethod = "DeleteAuthRelationships" +) + +// TestPublisherCalledWith records the arguments passed to the TestPublisher +type TestPublisherCalledWith struct { + Method TestPublisherMethod + Topic string + ResourceID gidx.PrefixedID + Relations []events.AuthRelationshipRelation +} + +// TestPublisher is a test publisher implements the permissions.AuthRelationshipRequestHandler +type TestPublisher struct { + calledWiths []TestPublisherCalledWith + err error +} + +// TestPublisher implements permissions.AuthRelationshipRequestHandler +var _ permissions.AuthRelationshipRequestHandler = (*TestPublisher)(nil) + +// CalledWith returns the arguments passed to the TestPublisher +func (tp *TestPublisher) CalledWith() []TestPublisherCalledWith { + return tp.calledWiths +} + +// CreateAuthRelationships records the arguments passed to the TestPublisher +func (tp *TestPublisher) CreateAuthRelationships(_ context.Context, topic string, resourceID gidx.PrefixedID, relations ...events.AuthRelationshipRelation) error { + tp.calledWiths = append(tp.calledWiths, TestPublisherCalledWith{ + Method: TestPublisherMethodCreate, + Topic: topic, + ResourceID: resourceID, + Relations: relations, + }) + + return tp.err +} + +// DeleteAuthRelationships records the arguments passed to the TestPublisher +func (tp *TestPublisher) DeleteAuthRelationships(_ context.Context, topic string, resourceID gidx.PrefixedID, relations ...events.AuthRelationshipRelation) error { + tp.calledWiths = append(tp.calledWiths, TestPublisherCalledWith{ + Method: TestPublisherMethodDelete, + Topic: topic, + ResourceID: resourceID, + Relations: relations, + }) + + return tp.err +} + +// ContextWithPublisher injects the TestPublisher into the context +func (tp *TestPublisher) ContextWithPublisher(ctx context.Context) context.Context { + return context.WithValue(ctx, permissions.AuthRelationshipRequestHandlerCtxKey, tp) +} + +// GetPublisherFromContext returns the TestPublisher from the context +func GetPublisherFromContext(ctx context.Context) (p *TestPublisher, ok bool) { + p, ok = ctx.Value(permissions.AuthRelationshipRequestHandlerCtxKey).(*TestPublisher) + return +} + +type NewTestPublisherOption func(*TestPublisher) + +// TestPublisherWithError sets the error for the TestPublisher +func TestPublisherWithError(err error) NewTestPublisherOption { + return func(tp *TestPublisher) { + tp.err = err + } +} + +// NewTestPublisher returns a new TestPublisher +func NewTestPublisher(opts ...NewTestPublisherOption) *TestPublisher { + p := &TestPublisher{} + + for _, opt := range opts { + opt(p) + } + + return p +}