diff --git a/apps/db-adapter/internal/api/admin_test.go b/apps/db-adapter/internal/api/admin_test.go new file mode 100644 index 0000000..3118c0b --- /dev/null +++ b/apps/db-adapter/internal/api/admin_test.go @@ -0,0 +1,172 @@ +package api + +import ( + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + memgraphMock "github.com/vcscsvcscs/GenerationsHeritage/apps/db-adapter/internal/memgraph/mock" + "github.com/vcscsvcscs/GenerationsHeritage/apps/db-adapter/pkg/api" +) + +func TestCreateAdminRelationship(t *testing.T) { + gin.SetMode(gin.TestMode) + + t.Run("Successful case", func(t *testing.T) { + mockSession := new(memgraphMock.SessionWithContext) + mockDriver := new(memgraphMock.DriverWithContext) + mockDriver.On("NewSession", mock.Anything, mock.Anything).Return(mockSession) + mockSession.On("ExecuteRead", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + mockSession.On("ExecuteWrite", mock.Anything, mock.Anything, mock.Anything).Return(map[string]any{"result": "success"}, nil) + mockSession.On("Close", mock.Anything).Return(nil) + + srv := &server{ + db: mockDriver, + dbOpTimeout: 5 * time.Second, + } + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + c.Request = httptest.NewRequest(http.MethodPost, "/admin", nil) + params := api.CreateAdminRelationshipParams{XUserID: *api.IntPtr(1)} + + srv.CreateAdminRelationship(c, 1, 2, params) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "success") + }) + + t.Run("Unauthorized case", func(t *testing.T) { + mockSession := new(memgraphMock.SessionWithContext) + mockDriver := new(memgraphMock.DriverWithContext) + mockDriver.On("NewSession", mock.Anything, mock.Anything).Return(mockSession) + mockSession.On("ExecuteRead", mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("unauthorized")) + mockSession.On("Close", mock.Anything).Return(nil) + + srv := &server{ + db: mockDriver, + dbOpTimeout: 5 * time.Second, + } + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + c.Request = httptest.NewRequest(http.MethodPost, "/admin", nil) + params := api.CreateAdminRelationshipParams{XUserID: *api.IntPtr(3)} + + srv.CreateAdminRelationship(c, 1, 2, params) + + assert.Equal(t, http.StatusUnauthorized, w.Code) + assert.Contains(t, w.Body.String(), "unauthorized") + }) + + t.Run("Internal server error case", func(t *testing.T) { + mockSession := new(memgraphMock.SessionWithContext) + mockDriver := new(memgraphMock.DriverWithContext) + mockDriver.On("NewSession", mock.Anything, mock.Anything).Return(mockSession) + mockSession.On("ExecuteRead", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + mockSession.On("ExecuteWrite", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("db error")) + mockSession.On("Close", mock.Anything).Return(nil) + + srv := &server{ + db: mockDriver, + dbOpTimeout: 5 * time.Second, + } + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + c.Request = httptest.NewRequest(http.MethodPost, "/admin", nil) + params := api.CreateAdminRelationshipParams{XUserID: 1} + + srv.CreateAdminRelationship(c, 1, 2, params) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "db error") + }) +} + +func TestDeleteAdminRelationship(t *testing.T) { + gin.SetMode(gin.TestMode) + + t.Run("Successful case", func(t *testing.T) { + mockSession := new(memgraphMock.SessionWithContext) + mockDriver := new(memgraphMock.DriverWithContext) + mockDriver.On("NewSession", mock.Anything, mock.Anything).Return(mockSession) + mockSession.On("ExecuteRead", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + mockSession.On("ExecuteWrite", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + mockSession.On("Close", mock.Anything).Return(nil) + + srv := &server{ + db: mockDriver, + dbOpTimeout: 5 * time.Second, + } + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + c.Request = httptest.NewRequest(http.MethodDelete, "/admin", nil) + params := api.DeleteAdminRelationshipParams{XUserID: 2} + + srv.DeleteAdminRelationship(c, 1, 2, params) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "admin relationship was deleted") + }) + + t.Run("Unauthorized case", func(t *testing.T) { + mockSession := new(memgraphMock.SessionWithContext) + mockDriver := new(memgraphMock.DriverWithContext) + mockDriver.On("NewSession", mock.Anything, mock.Anything).Return(mockSession) + mockSession.On("ExecuteRead", mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("unauthorized")) + mockSession.On("Close", mock.Anything).Return(nil) + + srv := &server{ + db: mockDriver, + dbOpTimeout: 5 * time.Second, + } + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + c.Request = httptest.NewRequest(http.MethodDelete, "/admin", nil) + params := api.DeleteAdminRelationshipParams{XUserID: 3} + + srv.DeleteAdminRelationship(c, 1, 2, params) + + assert.Equal(t, http.StatusUnauthorized, w.Code) + assert.Contains(t, w.Body.String(), "unauthorized") + }) + + t.Run("Internal server error case", func(t *testing.T) { + mockSession := new(memgraphMock.SessionWithContext) + mockDriver := new(memgraphMock.DriverWithContext) + mockDriver.On("NewSession", mock.Anything, mock.Anything).Return(mockSession) + mockSession.On("ExecuteRead", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + mockSession.On("ExecuteWrite", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("db error")) + mockSession.On("Close", mock.Anything).Return(nil) + + srv := &server{ + db: mockDriver, + dbOpTimeout: 5 * time.Second, + } + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + c.Request = httptest.NewRequest(http.MethodDelete, "/admin", nil) + params := api.DeleteAdminRelationshipParams{XUserID: 2} + + srv.DeleteAdminRelationship(c, 1, 2, params) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "db error") + }) +} diff --git a/apps/db-adapter/internal/api/auth/admin_operations.go b/apps/db-adapter/internal/api/auth/admin_operations.go index 481090c..420e3cf 100644 --- a/apps/db-adapter/internal/api/auth/admin_operations.go +++ b/apps/db-adapter/internal/api/auth/admin_operations.go @@ -22,6 +22,6 @@ func CouldManagePersonUnknownAdmin(ctx context.Context, session neo4j.SessionWit return nil } - _, err := session.ExecuteRead(ctx, memgraph.GetAdminRelationship(ctx, userId, XUserID), nil) + _, err := session.ExecuteRead(ctx, memgraph.GetAdminRelationship(ctx, userId, XUserID)) return err } diff --git a/apps/db-adapter/internal/api/auth/read_operations_test.go b/apps/db-adapter/internal/api/auth/read_operations_test.go new file mode 100644 index 0000000..d96d84c --- /dev/null +++ b/apps/db-adapter/internal/api/auth/read_operations_test.go @@ -0,0 +1,97 @@ +package auth + +import ( + "fmt" + "sync" + "testing" + + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + memgraphMock "github.com/vcscsvcscs/GenerationsHeritage/apps/db-adapter/internal/memgraph/mock" + "github.com/vcscsvcscs/GenerationsHeritage/apps/db-adapter/pkg/api" +) + +func TestCouldSeePersonsProfile(t *testing.T) { + ctx := t.Context() + + t.Run("User can manage person", func(t *testing.T) { + mockResult := new(memgraphMock.Result) + mockResult.On("Single", mock.Anything).Return(&neo4j.Record{}, nil) + mockSession := new(memgraphMock.SessionWithContext) + mockSession.On("ExecuteRead", mock.Anything, mock.Anything, mock.Anything).Return(mockResult, nil).Once() + err := CouldSeePersonsProfile(ctx, mockSession, 1, 2) + require.NoError(t, err) + }) + + t.Run("User cannot manage person but is in family tree", func(t *testing.T) { + mockResult := new(memgraphMock.Result) + mockResult.On("Single", mock.Anything).Return(nil, fmt.Errorf("no permission")) + mockSession := &memgraphMock.SessionWithContext{ + ReturnOnce: &sync.Once{}, + } + mockSession.On("ExecuteRead", mock.Anything, mock.Anything, mock.Anything).Return(mockResult, nil, map[string]any{ + "people": []api.OptimizedPersonNode{ + {Id: api.IntPtr(1)}, + }, + }, nil) + + err := CouldSeePersonsProfile(ctx, mockSession, 1, 3) + require.NoError(t, err) + mockSession.AssertExpectations(t) + }) + + t.Run("User not in family tree", func(t *testing.T) { + mockSession := memgraphMock.SessionWithContext{ + ReturnOnce: &sync.Once{}, + } + mockSession.On("ExecuteRead", mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("no permission"), map[string]any{ + "people": []api.OptimizedPersonNode{ + {Id: api.IntPtr(4)}, + }, + }, nil) + + err := CouldSeePersonsProfile(ctx, &mockSession, 1, 3) + require.Error(t, err) + require.EqualError(t, err, "user 3 does not have permission to see user 1") + mockSession.AssertExpectations(t) + }) + + t.Run("Error during ExecuteRead", func(t *testing.T) { + mockSession := &memgraphMock.SessionWithContext{ + ReturnOnce: &sync.Once{}, + } + mockSession.On("ExecuteRead", mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("read error"), nil, fmt.Errorf("read error")) + + err := CouldSeePersonsProfile(ctx, mockSession, 1, 3) + require.Error(t, err) + require.EqualError(t, err, "read error") + mockSession.AssertExpectations(t) + }) + + t.Run("Invalid result format from ExecuteRead", func(t *testing.T) { + mockSession := &memgraphMock.SessionWithContext{ + ReturnOnce: &sync.Once{}, + } + mockSession.On("ExecuteRead", mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("invalid"), "invalid", nil).Once() + + err := CouldSeePersonsProfile(ctx, mockSession, 1, 3) + require.Error(t, err) + require.EqualError(t, err, "could not convert result to map[string]any") + mockSession.AssertExpectations(t) + }) + + t.Run("Invalid people format in result", func(t *testing.T) { + mockSession := &memgraphMock.SessionWithContext{ + ReturnOnce: &sync.Once{}, + } + mockSession.On("ExecuteRead", mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("invalid"), map[string]any{ + "people": "invalid", + }, nil) + + err := CouldSeePersonsProfile(ctx, mockSession, 1, 3) + require.Error(t, err) + require.EqualError(t, err, "could not convert people to []api.PersonProperties") + mockSession.AssertExpectations(t) + }) +} diff --git a/apps/db-adapter/internal/memgraph/mock/session.go b/apps/db-adapter/internal/memgraph/mock/session.go index f3ddc61..eb64d6f 100644 --- a/apps/db-adapter/internal/memgraph/mock/session.go +++ b/apps/db-adapter/internal/memgraph/mock/session.go @@ -2,6 +2,7 @@ package mock import ( "context" + "sync" "github.com/neo4j/neo4j-go-driver/v5/neo4j" "github.com/stretchr/testify/mock" @@ -9,6 +10,7 @@ import ( type SessionWithContext struct { neo4j.SessionWithContext + ReturnOnce *sync.Once mock.Mock } @@ -43,11 +45,25 @@ func (m *SessionWithContext) BeginTransaction(ctx context.Context, configurers . func (m *SessionWithContext) ExecuteRead(ctx context.Context, work neo4j.ManagedTransactionWork, configurers ...func(*neo4j.TransactionConfig)) (any, error) { args := m.Called(ctx, work, configurers) - if args.Get(0) != nil { - return args.Get(0), args.Error(1) - } + if len(args) > 2 && args.Get(2) != nil { + returnValue1, returnValue2 := args.Get(2), args.Error(3) - return nil, args.Error(1) + m.ReturnOnce.Do(func() { + if args.Get(0) != nil { + returnValue1, returnValue2 = args.Get(0), args.Error(1) + } + + returnValue1, returnValue2 = nil, args.Error(1) + }) + + return returnValue1, returnValue2 + } else { + if args.Get(0) != nil { + return args.Get(0), args.Error(1) + } + + return nil, args.Error(1) + } } func (m *SessionWithContext) ExecuteWrite(ctx context.Context, work neo4j.ManagedTransactionWork, configurers ...func(*neo4j.TransactionConfig)) (any, error) {