From b4a1f40e87ce86dbba55938e6dd4de0ab6c57dad Mon Sep 17 00:00:00 2001 From: Kai Date: Wed, 12 Jun 2024 13:21:57 +0200 Subject: [PATCH 01/10] test read games --- api/tests/gameController_test.go | 114 +++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 api/tests/gameController_test.go diff --git a/api/tests/gameController_test.go b/api/tests/gameController_test.go new file mode 100644 index 0000000..6ae1d48 --- /dev/null +++ b/api/tests/gameController_test.go @@ -0,0 +1,114 @@ +package tests + +import ( + "api/controllers" + "api/dtos" + "api/models" + "api/repositories" + "api/services" + "database/sql" + "encoding/json" + "fmt" + "github.com/DATA-DOG/go-sqlmock" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "io/ioutil" + "log" + "net/http/httptest" + "regexp" + "testing" +) + +func Test_Read_Should_Succeed(t *testing.T) { + //==============Prepare======================= + db, dbMock := databaseMock() + defer db.Close() + prepare_sql_mock_for_Test_Read_Should_Succeed(dbMock) + gameController := gameController(db) + + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Set("subject", "MockOwner") + + //Execute + gameController.GetAllGames(c) + + //Verify + if w.Code != 200 { + b, _ := ioutil.ReadAll(w.Body) + t.Error(w.Code, string(b)) + } + + games := []dtos.GetAllGamesResponseBody{} + + err := json.Unmarshal(w.Body.Bytes(), &games) + if err != nil { + t.Error(err) + } + if len(games) != 2 { + t.Error(fmt.Sprint("Expected 2 games, got ", len(games))) + } + + if games[0].ID == uuid.Nil { + t.Error(fmt.Sprint("Expected non-nil game ID, got ", games[0].ID)) + } + if games[1].ID == uuid.Nil { + t.Error(fmt.Sprint("Expected non-nil game ID, got ", games[1].ID)) + } + if len(games[0].Url) == 0 { + t.Error(fmt.Sprint("Expected non-empty game URL, got ", games[0].Url)) + } + if len(games[1].Url) == 0 { + t.Error(fmt.Sprint("Expected non-empty game URL, got ", games[1].Url)) + } + if len(games[0].Title) == 0 { + t.Error(fmt.Sprint("Expected non-empty game title, got ", games[0].Title)) + } + if len(games[1].Title) == 0 { + t.Error(fmt.Sprint("Expected non-empty game title, got ", games[1].Title)) + } +} + +func prepare_sql_mock_for_Test_Read_Should_Succeed(mock sqlmock.Sqlmock) { + gameA := models.Game{ + ID: uuid.New(), + Title: "AMockA", + StorageLocation: "AMockB", + Status: "AMockC", + Url: "AMockD", + Owner: "MockOwner", + } + + gameB := models.Game{ + ID: uuid.New(), + Title: "BMockA", + StorageLocation: "BMockB", + Status: "BMockC", + Url: "BMockD", + Owner: "MockOwner", + } + + mock.ExpectPrepare(regexp.QuoteMeta("SELECT * FROM games WHERE owner = ?")) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE owner = ?")). + WithArgs("MockOwner"). + WillReturnRows( + sqlmock.NewRows([]string{"ID", "Title", "StorageLocation", "Status", "Url", "Owner"}). + AddRow(gameA.ID, gameA.Title, gameA.StorageLocation, gameA.Status, gameA.Url, gameA.Owner). + AddRow(gameB.ID, gameB.Title, gameB.StorageLocation, gameB.Status, gameB.Url, gameB.Owner), + ) +} + +func databaseMock() (*sql.DB, sqlmock.Sqlmock) { + db, mock, err := sqlmock.New() + if err != nil { + log.Fatalf(err.Error()) + } + return db, mock +} + +func gameController(db *sql.DB) controllers.IGameController { + gamesRepository := repositories.GameRepository(db) + gamesService := services.GameService(gamesRepository) + return controllers.GameController(gamesService) +} From 1466e62fa3511d5f6368c9c7b1539c8c1e33008a Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 17 Jun 2024 19:06:57 +0200 Subject: [PATCH 02/10] Read-All test . --- api/tests/gameController_test.go | 103 ++++++++++++++----------------- api/tests/mocks/modelMock.go | 18 ++++++ 2 files changed, 64 insertions(+), 57 deletions(-) create mode 100644 api/tests/mocks/modelMock.go diff --git a/api/tests/gameController_test.go b/api/tests/gameController_test.go index 6ae1d48..229c4c0 100644 --- a/api/tests/gameController_test.go +++ b/api/tests/gameController_test.go @@ -6,12 +6,12 @@ import ( "api/models" "api/repositories" "api/services" + "api/tests/mocks" "database/sql" "encoding/json" "fmt" "github.com/DATA-DOG/go-sqlmock" "github.com/gin-gonic/gin" - "github.com/google/uuid" "io/ioutil" "log" "net/http/httptest" @@ -19,84 +19,73 @@ import ( "testing" ) -func Test_Read_Should_Succeed(t *testing.T) { - //==============Prepare======================= +func Test_Read_All_Should_Succeed(t *testing.T) { + //======================= PREPARE PREPARE PREPARE PREPARE ======================= + owner := "MockOwner" + // Create database mock db, dbMock := databaseMock() defer db.Close() - prepare_sql_mock_for_Test_Read_Should_Succeed(dbMock) + // Create Models + gameA := mocks.GameMock("A") + gameA.Owner = owner + gameB := mocks.GameMock("B") + gameB.Owner = owner + // Define queries + dbMock.ExpectPrepare(regexp.QuoteMeta("SELECT * FROM games WHERE owner = ?")) + dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE owner = ?")). + WithArgs(owner). + WillReturnRows( + sqlmock.NewRows([]string{"ID", "Title", "StorageLocation", "Status", "Url", "Owner"}). + AddRow(gameA.ID, gameA.Title, gameA.StorageLocation, gameA.Status, gameA.Url, gameA.Owner). + AddRow(gameB.ID, gameB.Title, gameB.StorageLocation, gameB.Status, gameB.Url, gameB.Owner), + ) + // Finally, create gameController gameController := gameController(db) - + // Prepare Gin gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) - c.Set("subject", "MockOwner") + c.Set("subject", owner) - //Execute + //======================= EXECUTE EXECUTE EXECUTE EXECUTE ======================= gameController.GetAllGames(c) - //Verify + //======================= VERIFY VERIFY VERIFY VERIFY ======================= + //Check HTTP response if w.Code != 200 { b, _ := ioutil.ReadAll(w.Body) t.Error(w.Code, string(b)) } - games := []dtos.GetAllGamesResponseBody{} - - err := json.Unmarshal(w.Body.Bytes(), &games) + //Check response body + var responseBody []dtos.GetAllGamesResponseBody + err := json.Unmarshal(w.Body.Bytes(), &responseBody) if err != nil { t.Error(err) } - if len(games) != 2 { - t.Error(fmt.Sprint("Expected 2 games, got ", len(games))) + if len(responseBody) != 2 { + t.Error(fmt.Sprint("Expected 2 games, got ", len(responseBody))) } - if games[0].ID == uuid.Nil { - t.Error(fmt.Sprint("Expected non-nil game ID, got ", games[0].ID)) - } - if games[1].ID == uuid.Nil { - t.Error(fmt.Sprint("Expected non-nil game ID, got ", games[1].ID)) - } - if len(games[0].Url) == 0 { - t.Error(fmt.Sprint("Expected non-empty game URL, got ", games[0].Url)) - } - if len(games[1].Url) == 0 { - t.Error(fmt.Sprint("Expected non-empty game URL, got ", games[1].Url)) - } - if len(games[0].Title) == 0 { - t.Error(fmt.Sprint("Expected non-empty game title, got ", games[0].Title)) - } - if len(games[1].Title) == 0 { - t.Error(fmt.Sprint("Expected non-empty game title, got ", games[1].Title)) - } + //Verify games + verifyDto(t, &responseBody[0], gameA) + verifyDto(t, &responseBody[1], gameB) + } -func prepare_sql_mock_for_Test_Read_Should_Succeed(mock sqlmock.Sqlmock) { - gameA := models.Game{ - ID: uuid.New(), - Title: "AMockA", - StorageLocation: "AMockB", - Status: "AMockC", - Url: "AMockD", - Owner: "MockOwner", +func verifyDto(t *testing.T, dto *dtos.GetAllGamesResponseBody, game *models.Game) { + if dto.Title != game.Title { + t.Error(fmt.Sprintf("Expected title %s, got %s", game.Title, dto.Title)) } - - gameB := models.Game{ - ID: uuid.New(), - Title: "BMockA", - StorageLocation: "BMockB", - Status: "BMockC", - Url: "BMockD", - Owner: "MockOwner", + if dto.Url != game.Url { + t.Error(fmt.Sprintf("Expected url %s, got %s", game.Url, dto.Url)) + } + if dto.ID != game.ID { + t.Error(fmt.Sprintf("Expected id %v, got %v", game.ID, dto.ID)) + } + if dto.Status != game.Status { + t.Error(fmt.Sprintf("Expected status %v, got %v", game.Status, dto.Status)) } - - mock.ExpectPrepare(regexp.QuoteMeta("SELECT * FROM games WHERE owner = ?")) - mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE owner = ?")). - WithArgs("MockOwner"). - WillReturnRows( - sqlmock.NewRows([]string{"ID", "Title", "StorageLocation", "Status", "Url", "Owner"}). - AddRow(gameA.ID, gameA.Title, gameA.StorageLocation, gameA.Status, gameA.Url, gameA.Owner). - AddRow(gameB.ID, gameB.Title, gameB.StorageLocation, gameB.Status, gameB.Url, gameB.Owner), - ) } func databaseMock() (*sql.DB, sqlmock.Sqlmock) { @@ -109,6 +98,6 @@ func databaseMock() (*sql.DB, sqlmock.Sqlmock) { func gameController(db *sql.DB) controllers.IGameController { gamesRepository := repositories.GameRepository(db) - gamesService := services.GameService(gamesRepository) + gamesService := services.GameService(gamesRepository, nil, nil) return controllers.GameController(gamesService) } diff --git a/api/tests/mocks/modelMock.go b/api/tests/mocks/modelMock.go new file mode 100644 index 0000000..526ffca --- /dev/null +++ b/api/tests/mocks/modelMock.go @@ -0,0 +1,18 @@ +package mocks + +import ( + "api/models" + "api/shared" + "github.com/google/uuid" +) + +func GameMock(identifier string) *models.Game { + return &models.Game{ + ID: uuid.New(), + Title: "Title_" + identifier, + StorageLocation: "Storage_Location_" + identifier, + Status: shared.Status_New, + Url: "Url_" + identifier, + Owner: "Owner_" + identifier, + } +} From 59f369d348605fde5422c264aa7bc9bb0b022bf4 Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 17 Jun 2024 19:12:50 +0200 Subject: [PATCH 03/10] find game by id test --- api/tests/gameController_test.go | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/api/tests/gameController_test.go b/api/tests/gameController_test.go index 229c4c0..ccca00e 100644 --- a/api/tests/gameController_test.go +++ b/api/tests/gameController_test.go @@ -19,6 +19,56 @@ import ( "testing" ) +func Test_Read_By_Id_Should_Succeed(t *testing.T) { + //======================= PREPARE PREPARE PREPARE PREPARE ======================= + owner := "MockOwner" + // Create database mock + db, dbMock := databaseMock() + defer db.Close() + // Create Models + game := mocks.GameMock("A") + game.Owner = owner + // Define queries + dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE ID = ?")). + WithArgs(game.ID).WillReturnRows( + sqlmock.NewRows([]string{"Id", "Title", "StorageLocation", "Status", "Url", "Owner"}). + AddRow(game.ID, game.Title, game.StorageLocation, game.Status, game.Url, game.Owner), + ) + + // Finally, create gameController + gameController := gameController(db) + // Prepare Gin + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Set("subject", owner) + + //======================= EXECUTE EXECUTE EXECUTE EXECUTE ======================= + c.Params = gin.Params{gin.Param{Key: "id", Value: game.ID.String()}} + gameController.GetGameById(c) + + //======================= VERIFY VERIFY VERIFY VERIFY ======================= + //Check HTTP response + if w.Code != 200 { + b, _ := ioutil.ReadAll(w.Body) + t.Error(w.Code, string(b)) + } + + //Check response body + var responseBody dtos.GetAllGamesResponseBody + err := json.Unmarshal(w.Body.Bytes(), &responseBody) + if err != nil { + t.Error(err) + } + if &responseBody == nil { + t.Error("response body is empty") + } + + //Verify games + verifyDto(t, &responseBody, game) + +} + func Test_Read_All_Should_Succeed(t *testing.T) { //======================= PREPARE PREPARE PREPARE PREPARE ======================= owner := "MockOwner" From d0bbc9b36dc46fab7280ad90fa3a25b349cc1126 Mon Sep 17 00:00:00 2001 From: Kai Date: Tue, 18 Jun 2024 10:07:48 +0200 Subject: [PATCH 04/10] prepared mocking interfaces --- api/apis/azureClient/azBlobClient.go | 116 +++++++++++++++++++++++++++ api/tests/mocks/azureMock.go | 66 +++++++++++++++ api/tests/mocks/k8sMock.go | 87 ++++++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 api/apis/azureClient/azBlobClient.go create mode 100644 api/tests/mocks/azureMock.go create mode 100644 api/tests/mocks/k8sMock.go diff --git a/api/apis/azureClient/azBlobClient.go b/api/apis/azureClient/azBlobClient.go new file mode 100644 index 0000000..1a76d45 --- /dev/null +++ b/api/apis/azureClient/azBlobClient.go @@ -0,0 +1,116 @@ +package azureClient + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/service" + "io" + "os" +) + +// azblob.client has no interface +// but we need one for mocking in the tests. +// So we have to define our own interface + +type IAzBlobClient interface { + CreateContainer(ctx context.Context, containerName string, o *azblob.CreateContainerOptions) (azblob.CreateContainerResponse, error) + DeleteContainer(ctx context.Context, containerName string, o *azblob.DeleteContainerOptions) (azblob.DeleteContainerResponse, error) + DeleteBlob(ctx context.Context, containerName string, blobName string, o *azblob.DeleteBlobOptions) (azblob.DeleteBlobResponse, error) + NewListBlobsFlatPager(containerName string, o *azblob.ListBlobsFlatOptions) *runtime.Pager[azblob.ListBlobsFlatResponse] + NewListContainersPager(o *azblob.ListContainersOptions) *runtime.Pager[azblob.ListContainersResponse] + UploadBuffer(ctx context.Context, containerName string, blobName string, buffer []byte, o *azblob.UploadBufferOptions) (azblob.UploadBufferResponse, error) + UploadFile(ctx context.Context, containerName string, blobName string, file *os.File, o *azblob.UploadFileOptions) (azblob.UploadFileResponse, error) + UploadStream(ctx context.Context, containerName string, blobName string, body io.Reader, o *azblob.UploadStreamOptions) (azblob.UploadStreamResponse, error) + DownloadBuffer(ctx context.Context, containerName string, blobName string, buffer []byte, o *azblob.DownloadBufferOptions) (int64, error) + DownloadFile(ctx context.Context, containerName string, blobName string, file *os.File, o *azblob.DownloadFileOptions) (int64, error) + DownloadStream(ctx context.Context, containerName string, blobName string, o *azblob.DownloadStreamOptions) (azblob.DownloadStreamResponse, error) +} + +type azBlobClient struct { + client *azblob.Client +} + +func AzBlobClient(azure *azblob.Client) IAzBlobClient { + return &azBlobClient{ + client: azure, + } +} + +// URL returns the URL endpoint used by the BlobClient object. +func (c *azBlobClient) URL() string { + return c.URL() +} + +// ServiceClient returns the embedded service client for this client. +func (c *azBlobClient) ServiceClient() *service.Client { + return c.client.ServiceClient() +} + +// CreateContainer is a lifecycle method to creates a new container under the specified account. +// If the container with the same name already exists, a ResourceExistsError will be raised. +// This method returns a client with which to interact with the newly created container. +func (c *azBlobClient) CreateContainer(ctx context.Context, containerName string, o *azblob.CreateContainerOptions) (azblob.CreateContainerResponse, error) { + return c.client.CreateContainer(ctx, containerName, o) +} + +// DeleteContainer is a lifecycle method that marks the specified container for deletion. +// The container and any blobs contained within it are later deleted during garbage collection. +// If the container is not found, a ResourceNotFoundError will be raised. +func (c *azBlobClient) DeleteContainer(ctx context.Context, containerName string, o *azblob.DeleteContainerOptions) (azblob.DeleteContainerResponse, error) { + return c.client.DeleteContainer(ctx, containerName, o) +} + +// DeleteBlob marks the specified blob or snapshot for deletion. The blob is later deleted during garbage collection. +// Note that deleting a blob also deletes all its snapshots. +// For more information, see https://docs.microsoft.com/rest/api/storageservices/delete-blob. +func (c *azBlobClient) DeleteBlob(ctx context.Context, containerName string, blobName string, o *azblob.DeleteBlobOptions) (azblob.DeleteBlobResponse, error) { + return c.client.DeleteBlob(ctx, containerName, blobName, o) +} + +// NewListBlobsFlatPager returns a pager for blobs starting from the specified Marker. Use an empty +// Marker to start enumeration from the beginning. Blob names are returned in lexicographic order. +// For more information, see https://docs.microsoft.com/rest/api/storageservices/list-blobs. +func (c *azBlobClient) NewListBlobsFlatPager(containerName string, o *azblob.ListBlobsFlatOptions) *runtime.Pager[azblob.ListBlobsFlatResponse] { + return c.client.NewListBlobsFlatPager(containerName, o) +} + +// NewListContainersPager operation returns a pager of the containers under the specified account. +// Use an empty Marker to start enumeration from the beginning. Container names are returned in lexicographic order. +// For more information, see https://docs.microsoft.com/rest/api/storageservices/list-containers2. +func (c *azBlobClient) NewListContainersPager(o *azblob.ListContainersOptions) *runtime.Pager[azblob.ListContainersResponse] { + return c.client.NewListContainersPager(o) +} + +// UploadBuffer uploads a buffer in blocks to a block blob. +func (c *azBlobClient) UploadBuffer(ctx context.Context, containerName string, blobName string, buffer []byte, o *azblob.UploadBufferOptions) (azblob.UploadBufferResponse, error) { + return c.client.UploadBuffer(ctx, containerName, blobName, buffer, o) +} + +// UploadFile uploads a file in blocks to a block blob. +func (c *azBlobClient) UploadFile(ctx context.Context, containerName string, blobName string, file *os.File, o *azblob.UploadFileOptions) (azblob.UploadFileResponse, error) { + return c.client.UploadFile(ctx, containerName, blobName, file, o) +} + +// UploadStream copies the file held in io.Reader to the Blob at blockBlobClient. +// A Context deadline or cancellation will cause this to error. +func (c *azBlobClient) UploadStream(ctx context.Context, containerName string, blobName string, body io.Reader, o *azblob.UploadStreamOptions) (azblob.UploadStreamResponse, error) { + return c.client.UploadStream(ctx, containerName, blobName, body, o) +} + +// DownloadBuffer downloads an Azure blob to a buffer with parallel. +func (c *azBlobClient) DownloadBuffer(ctx context.Context, containerName string, blobName string, buffer []byte, o *azblob.DownloadBufferOptions) (int64, error) { + return c.client.DownloadBuffer(ctx, containerName, blobName, buffer, o) +} + +// DownloadFile downloads an Azure blob to a local file. +// The file would be truncated if the size doesn't match. +func (c *azBlobClient) DownloadFile(ctx context.Context, containerName string, blobName string, file *os.File, o *azblob.DownloadFileOptions) (int64, error) { + return c.client.DownloadFile(ctx, containerName, blobName, file, o) +} + +// DownloadStream reads a range of bytes from a blob. The response also includes the blob's properties and metadata. +// For more information, see https://docs.microsoft.com/rest/api/storageservices/get-blob. +func (c *azBlobClient) DownloadStream(ctx context.Context, containerName string, blobName string, o *azblob.DownloadStreamOptions) (azblob.DownloadStreamResponse, error) { + return c.client.DownloadStream(ctx, containerName, blobName, o) +} diff --git a/api/tests/mocks/azureMock.go b/api/tests/mocks/azureMock.go new file mode 100644 index 0000000..df2daa5 --- /dev/null +++ b/api/tests/mocks/azureMock.go @@ -0,0 +1,66 @@ +package mocks + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" + "io" + "os" +) + +type AzureBlobClientMock struct{} + +func (AzureBlobClientMock) CreateContainer(ctx context.Context, containerName string, o *azblob.CreateContainerOptions) (azblob.CreateContainerResponse, error) { + //TODO implement me + panic("implement me") +} + +func (AzureBlobClientMock) DeleteContainer(ctx context.Context, containerName string, o *azblob.DeleteContainerOptions) (azblob.DeleteContainerResponse, error) { + //TODO implement me + panic("implement me") +} + +func (AzureBlobClientMock) DeleteBlob(ctx context.Context, containerName string, blobName string, o *azblob.DeleteBlobOptions) (azblob.DeleteBlobResponse, error) { + //TODO implement me + panic("implement me") +} + +func (AzureBlobClientMock) NewListBlobsFlatPager(containerName string, o *azblob.ListBlobsFlatOptions) *runtime.Pager[azblob.ListBlobsFlatResponse] { + //TODO implement me + panic("implement me") +} + +func (AzureBlobClientMock) NewListContainersPager(o *azblob.ListContainersOptions) *runtime.Pager[azblob.ListContainersResponse] { + //TODO implement me + panic("implement me") +} + +func (AzureBlobClientMock) UploadBuffer(ctx context.Context, containerName string, blobName string, buffer []byte, o *azblob.UploadBufferOptions) (azblob.UploadBufferResponse, error) { + //TODO implement me + panic("implement me") +} + +func (AzureBlobClientMock) UploadFile(ctx context.Context, containerName string, blobName string, file *os.File, o *azblob.UploadFileOptions) (azblob.UploadFileResponse, error) { + //TODO implement me + panic("implement me") +} + +func (AzureBlobClientMock) UploadStream(ctx context.Context, containerName string, blobName string, body io.Reader, o *azblob.UploadStreamOptions) (azblob.UploadStreamResponse, error) { + //TODO implement me + panic("implement me") +} + +func (AzureBlobClientMock) DownloadBuffer(ctx context.Context, containerName string, blobName string, buffer []byte, o *azblob.DownloadBufferOptions) (int64, error) { + //TODO implement me + panic("implement me") +} + +func (AzureBlobClientMock) DownloadFile(ctx context.Context, containerName string, blobName string, file *os.File, o *azblob.DownloadFileOptions) (int64, error) { + //TODO implement me + panic("implement me") +} + +func (AzureBlobClientMock) DownloadStream(ctx context.Context, containerName string, blobName string, o *azblob.DownloadStreamOptions) (azblob.DownloadStreamResponse, error) { + //TODO implement me + panic("implement me") +} diff --git a/api/tests/mocks/k8sMock.go b/api/tests/mocks/k8sMock.go new file mode 100644 index 0000000..d417080 --- /dev/null +++ b/api/tests/mocks/k8sMock.go @@ -0,0 +1,87 @@ +package mocks + +import ( + "context" + "github.com/stretchr/testify/mock" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type k8sMock struct { + mock *mock.Mock +} + +func K8sMock(mock *mock.Mock) *k8sMock { + return &k8sMock{mock: mock} +} + +func (s k8sMock) Mock() *mock.Mock { + return s.mock +} + +func (s k8sMock) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + s.mock.Called(ctx, key, obj) + return nil +} + +func (s k8sMock) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + s.mock.Called(ctx, list) + return nil +} + +func (s k8sMock) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + s.mock.Called(ctx, obj) + return nil +} + +func (s k8sMock) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { + s.mock.Called(ctx, obj) + return nil +} + +func (s k8sMock) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + s.mock.Called(ctx, obj) + return nil +} + +func (s k8sMock) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + s.mock.Called(ctx, obj, patch) + return nil +} + +func (s k8sMock) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { + s.mock.Called(ctx, obj) + return nil +} + +func (s k8sMock) Status() client.SubResourceWriter { + s.mock.Called() + return nil +} + +func (s k8sMock) SubResource(subResource string) client.SubResourceClient { + s.mock.Called(subResource) + return nil +} + +func (s k8sMock) Scheme() *runtime.Scheme { + s.mock.Called() + return nil +} + +func (s k8sMock) RESTMapper() meta.RESTMapper { + s.mock.Called() + return nil +} + +func (s k8sMock) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { + s.mock.Called(obj) + return schema.GroupVersionKind{}, nil +} + +func (s k8sMock) IsObjectNamespaced(obj runtime.Object) (bool, error) { + s.mock.Called(obj) + return true, nil +} From 819a25208cef0616fcef04153da764a3ae1cc468 Mon Sep 17 00:00:00 2001 From: Kai Date: Tue, 18 Jun 2024 10:15:40 +0200 Subject: [PATCH 05/10] find game and refresh test --- api/tests/gameController_test.go | 99 ++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 4 deletions(-) diff --git a/api/tests/gameController_test.go b/api/tests/gameController_test.go index ccca00e..d302496 100644 --- a/api/tests/gameController_test.go +++ b/api/tests/gameController_test.go @@ -1,17 +1,22 @@ package tests import ( + "api/apis" "api/controllers" "api/dtos" "api/models" "api/repositories" "api/services" + "api/shared" "api/tests/mocks" "database/sql" "encoding/json" "fmt" "github.com/DATA-DOG/go-sqlmock" "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/stretchr/testify/mock" + v1 "indiegamestream.com/indiegamestream/api/stream/v1" "io/ioutil" "log" "net/http/httptest" @@ -19,6 +24,92 @@ import ( "testing" ) +func Test_Read_By_Id_And_Refresh_Should_Succeed(t *testing.T) { + //======================= PREPARE PREPARE PREPARE PREPARE ======================= + owner := "MockOwner" + url := "fsdfsf-91f3975e-cdd5-4b9b-8f00-a86bb44d4b82.possum-climb.ts.net" + // Create Models + game := mocks.GameMock("A") + game.ID, _ = uuid.Parse("66c887ca-1f56-426e-ac0c-bc92fff8b798") + game.Owner = owner + game.Url = "" + game.Status = shared.Status_Installing + // Create fake k8s client + fakek8s := mocks.K8sMock(&mock.Mock{}) + k8sApi := apis.K8sService(fakek8s) + //Mock the calls to k8s + fakek8s.Mock(). + On("Get", + mock.AnythingOfType("context.backgroundCtx"), + mock.AnythingOfType("types.NamespacedName"), + mock.AnythingOfType("*v1.Game")). + Return(nil). + Run(func(args mock.Arguments) { + arg := args.Get(2).(*v1.Game) + arg.Status.URL = url + }) + + // Create database mock + db, dbMock := databaseMock() + defer db.Close() + // Define queries + dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE ID = ?")). + WithArgs(game.ID).WillReturnRows( + sqlmock.NewRows([]string{"Id", "Title", "StorageLocation", "Status", "Url", "Owner"}). + AddRow(game.ID, game.Title, game.StorageLocation, game.Status, game.Url, game.Owner), + ) + + dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE ID = ?")). + WithArgs(game.ID).WillReturnRows( + sqlmock.NewRows([]string{"Id", "Title", "StorageLocation", "Status", "Url", "Owner"}). + AddRow(game.ID, game.Title, game.StorageLocation, game.Status, game.Url, game.Owner), + ) + + dbMock.ExpectPrepare(regexp. + QuoteMeta("UPDATE games SET Title=?, StorageLocation=?, Status=?, Url=? WHERE ID = ?")) + + dbMock.ExpectExec(regexp. + QuoteMeta("UPDATE games SET Title=?, StorageLocation=?, Status=?, Url=? WHERE ID = ?")). + WithArgs(game.Title, game.StorageLocation, game.Status, url, game.ID). + WillReturnResult(sqlmock.NewResult(0, 1)) + + // Finally, create gameController + gameController := gameController(db, k8sApi, nil) + + // Prepare Gin + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Set("subject", owner) + + //======================= EXECUTE EXECUTE EXECUTE EXECUTE ======================= + c.Params = gin.Params{gin.Param{Key: "id", Value: game.ID.String()}} + gameController.GetGameById(c) + + //======================= VERIFY VERIFY VERIFY VERIFY ======================= + //Check HTTP response + if w.Code != 200 { + b, _ := ioutil.ReadAll(w.Body) + t.Error(w.Code, string(b)) + } + + //Check response body + var responseBody dtos.GetAllGamesResponseBody + err := json.Unmarshal(w.Body.Bytes(), &responseBody) + if err != nil { + t.Error(err) + } + if &responseBody == nil { + t.Error("response body is empty") + } + + //Verify games + game.Url = url // Game url should be set correctly + game.Status = shared.Status_Installed //Game Status should be set correctly + verifyDto(t, &responseBody, game) + +} + func Test_Read_By_Id_Should_Succeed(t *testing.T) { //======================= PREPARE PREPARE PREPARE PREPARE ======================= owner := "MockOwner" @@ -36,7 +127,7 @@ func Test_Read_By_Id_Should_Succeed(t *testing.T) { ) // Finally, create gameController - gameController := gameController(db) + gameController := gameController(db, nil, nil) // Prepare Gin gin.SetMode(gin.TestMode) w := httptest.NewRecorder() @@ -90,7 +181,7 @@ func Test_Read_All_Should_Succeed(t *testing.T) { AddRow(gameB.ID, gameB.Title, gameB.StorageLocation, gameB.Status, gameB.Url, gameB.Owner), ) // Finally, create gameController - gameController := gameController(db) + gameController := gameController(db, nil, nil) // Prepare Gin gin.SetMode(gin.TestMode) w := httptest.NewRecorder() @@ -146,8 +237,8 @@ func databaseMock() (*sql.DB, sqlmock.Sqlmock) { return db, mock } -func gameController(db *sql.DB) controllers.IGameController { +func gameController(db *sql.DB, k8s apis.IK8sApi, azure apis.IAzureApi) controllers.IGameController { gamesRepository := repositories.GameRepository(db) - gamesService := services.GameService(gamesRepository, nil, nil) + gamesService := services.GameService(gamesRepository, k8s, azure) return controllers.GameController(gamesService) } From ad861032501a531f1c9a39b0bb05f397d7f0d8c8 Mon Sep 17 00:00:00 2001 From: Kai Date: Tue, 18 Jun 2024 10:17:02 +0200 Subject: [PATCH 06/10] go mod tidy --- api/go.mod | 5 ++++- api/go.sum | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/api/go.mod b/api/go.mod index 5bc0578..25cfc01 100644 --- a/api/go.mod +++ b/api/go.mod @@ -7,6 +7,7 @@ toolchain go1.22.4 replace indiegamestream.com/indiegamestream => github.com/AustrianDataLAB/IndieGameStream/operator v0.0.0-20240602152718-e83f31e76ee0 require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5 v5.0.0 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 @@ -16,6 +17,7 @@ require ( github.com/go-sql-driver/mysql v1.8.1 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 + github.com/stretchr/testify v1.9.0 google.golang.org/api v0.183.0 indiegamestream.com/indiegamestream v0.0.0-00010101000000-000000000000 k8s.io/apimachinery v0.30.1 @@ -28,7 +30,6 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect cloud.google.com/go/compute/metadata v0.3.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/bytedance/sonic v1.11.6 // indirect @@ -72,7 +73,9 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect go.opencensus.io v0.24.0 // indirect diff --git a/api/go.sum b/api/go.sum index c86063b..b0ef769 100644 --- a/api/go.sum +++ b/api/go.sum @@ -191,6 +191,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= From 1c60db9bc8f81de57911edf7c7e988bf931803a4 Mon Sep 17 00:00:00 2001 From: Kai Date: Tue, 18 Jun 2024 10:22:40 +0200 Subject: [PATCH 07/10] dont fetch game url when we already have the game url --- api/services/gameService.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/services/gameService.go b/api/services/gameService.go index 71109e0..98a9372 100644 --- a/api/services/gameService.go +++ b/api/services/gameService.go @@ -40,7 +40,9 @@ func (g gameService) FindByID(id uuid.UUID) (*models.Game, error) { if err != nil { return nil, err } else { - g.updateGameUrl(game) + if game.Url == "" { + g.updateGameUrl(game) + } return game, nil } } From d4eb0d1969556217cb4fb898d036e488687dc29f Mon Sep 17 00:00:00 2001 From: Kai Date: Tue, 18 Jun 2024 10:23:27 +0200 Subject: [PATCH 08/10] remove unnecessary null check --- api/services/gameService.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/api/services/gameService.go b/api/services/gameService.go index 98a9372..d8d15c2 100644 --- a/api/services/gameService.go +++ b/api/services/gameService.go @@ -123,10 +123,6 @@ func (g gameService) Delete(id uuid.UUID) error { } func (g gameService) updateGameUrl(game *models.Game) { - if game == nil { - return - } - url, err := g.k8s.ReadGameUrl(game.ID) if err != nil { log.Println(fmt.Sprintf("Error reading game url: %s", err)) From 6efac5912eb0108de8bbb2df93cb85687598f093 Mon Sep 17 00:00:00 2001 From: Kai Date: Wed, 19 Jun 2024 21:21:17 +0200 Subject: [PATCH 09/10] added filename to integration tests --- api/tests/gameController_test.go | 24 ++++++++++++------------ api/tests/mocks/modelMock.go | 1 + 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/api/tests/gameController_test.go b/api/tests/gameController_test.go index d302496..fc11a4d 100644 --- a/api/tests/gameController_test.go +++ b/api/tests/gameController_test.go @@ -55,22 +55,22 @@ func Test_Read_By_Id_And_Refresh_Should_Succeed(t *testing.T) { // Define queries dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE ID = ?")). WithArgs(game.ID).WillReturnRows( - sqlmock.NewRows([]string{"Id", "Title", "StorageLocation", "Status", "Url", "Owner"}). - AddRow(game.ID, game.Title, game.StorageLocation, game.Status, game.Url, game.Owner), + sqlmock.NewRows([]string{"Id", "Title", "StorageLocation", "Status", "Url", "Owner", "FileName"}). + AddRow(game.ID, game.Title, game.StorageLocation, game.Status, game.Url, game.Owner, game.FileName), ) dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE ID = ?")). WithArgs(game.ID).WillReturnRows( - sqlmock.NewRows([]string{"Id", "Title", "StorageLocation", "Status", "Url", "Owner"}). - AddRow(game.ID, game.Title, game.StorageLocation, game.Status, game.Url, game.Owner), + sqlmock.NewRows([]string{"Id", "Title", "StorageLocation", "Status", "Url", "Owner", "FileName"}). + AddRow(game.ID, game.Title, game.StorageLocation, game.Status, game.Url, game.Owner, game.FileName), ) dbMock.ExpectPrepare(regexp. - QuoteMeta("UPDATE games SET Title=?, StorageLocation=?, Status=?, Url=? WHERE ID = ?")) + QuoteMeta("UPDATE games SET Title=?, StorageLocation=?, Status=?, Url=?, FileName=? WHERE ID = ?")) dbMock.ExpectExec(regexp. - QuoteMeta("UPDATE games SET Title=?, StorageLocation=?, Status=?, Url=? WHERE ID = ?")). - WithArgs(game.Title, game.StorageLocation, game.Status, url, game.ID). + QuoteMeta("UPDATE games SET Title=?, StorageLocation=?, Status=?, Url=?, FileName=? WHERE ID = ?")). + WithArgs(game.Title, game.StorageLocation, game.Status, url, game.FileName, game.ID). WillReturnResult(sqlmock.NewResult(0, 1)) // Finally, create gameController @@ -122,8 +122,8 @@ func Test_Read_By_Id_Should_Succeed(t *testing.T) { // Define queries dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE ID = ?")). WithArgs(game.ID).WillReturnRows( - sqlmock.NewRows([]string{"Id", "Title", "StorageLocation", "Status", "Url", "Owner"}). - AddRow(game.ID, game.Title, game.StorageLocation, game.Status, game.Url, game.Owner), + sqlmock.NewRows([]string{"Id", "Title", "StorageLocation", "Status", "Url", "Owner", "FileName"}). + AddRow(game.ID, game.Title, game.StorageLocation, game.Status, game.Url, game.Owner, game.FileName), ) // Finally, create gameController @@ -176,9 +176,9 @@ func Test_Read_All_Should_Succeed(t *testing.T) { dbMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM games WHERE owner = ?")). WithArgs(owner). WillReturnRows( - sqlmock.NewRows([]string{"ID", "Title", "StorageLocation", "Status", "Url", "Owner"}). - AddRow(gameA.ID, gameA.Title, gameA.StorageLocation, gameA.Status, gameA.Url, gameA.Owner). - AddRow(gameB.ID, gameB.Title, gameB.StorageLocation, gameB.Status, gameB.Url, gameB.Owner), + sqlmock.NewRows([]string{"ID", "Title", "StorageLocation", "Status", "Url", "Owner", "FileName"}). + AddRow(gameA.ID, gameA.Title, gameA.StorageLocation, gameA.Status, gameA.Url, gameA.Owner, gameA.FileName). + AddRow(gameB.ID, gameB.Title, gameB.StorageLocation, gameB.Status, gameB.Url, gameB.Owner, gameB.FileName), ) // Finally, create gameController gameController := gameController(db, nil, nil) diff --git a/api/tests/mocks/modelMock.go b/api/tests/mocks/modelMock.go index 526ffca..86f9b3c 100644 --- a/api/tests/mocks/modelMock.go +++ b/api/tests/mocks/modelMock.go @@ -14,5 +14,6 @@ func GameMock(identifier string) *models.Game { Status: shared.Status_New, Url: "Url_" + identifier, Owner: "Owner_" + identifier, + FileName: "File_" + identifier, } } From 288cc93c085485c0ce8822a8413743bd812173d6 Mon Sep 17 00:00:00 2001 From: Kai Date: Wed, 19 Jun 2024 21:31:02 +0200 Subject: [PATCH 10/10] fix issue in test refresh game --- api/tests/gameController_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tests/gameController_test.go b/api/tests/gameController_test.go index fc11a4d..26a1aeb 100644 --- a/api/tests/gameController_test.go +++ b/api/tests/gameController_test.go @@ -70,7 +70,7 @@ func Test_Read_By_Id_And_Refresh_Should_Succeed(t *testing.T) { dbMock.ExpectExec(regexp. QuoteMeta("UPDATE games SET Title=?, StorageLocation=?, Status=?, Url=?, FileName=? WHERE ID = ?")). - WithArgs(game.Title, game.StorageLocation, game.Status, url, game.FileName, game.ID). + WithArgs(game.Title, game.StorageLocation, shared.Status_Installed, url, game.FileName, game.ID). WillReturnResult(sqlmock.NewResult(0, 1)) // Finally, create gameController