Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
914 changes: 914 additions & 0 deletions internal/databasetesting/mock_crud.go

Large diffs are not rendered by default.

525 changes: 525 additions & 0 deletions internal/databasetesting/mock_dbclient.go

Large diffs are not rendered by default.

582 changes: 582 additions & 0 deletions internal/databasetesting/mock_dbclient_test.go

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions test-integration/frontend/frontend_crud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,44 @@ func TestFrontendCRUD(t *testing.T) {
})
}
}

// TestFrontendCRUDWithMock runs the frontend CRUD tests using a mock database.
// This test does not require a Cosmos DB emulator.
func TestFrontendCRUDWithMock(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()

allCRUDDirFS, err := fs.Sub(artifacts, "artifacts/FrontendCRUD")
require.NoError(t, err)

crudSuiteDirs := api.Must(fs.ReadDir(allCRUDDirFS, "."))
for _, crudSuiteDirEntry := range crudSuiteDirs {
crudSuiteDir := api.Must(fs.Sub(allCRUDDirFS, crudSuiteDirEntry.Name()))
t.Run(crudSuiteDirEntry.Name(), func(t *testing.T) {
testCRUDSuiteWithMock(
ctx,
t,
databasemutationhelpers.NothingCRUDSpecializer{},
crudSuiteDir)
})
}
}

// testCRUDSuiteWithMock runs a CRUD test suite using a mock database.
func testCRUDSuiteWithMock[InternalAPIType any](ctx context.Context, t *testing.T, specializer databasemutationhelpers.ResourceCRUDTestSpecializer[InternalAPIType], crudSuiteDir fs.FS) {
testDirs := api.Must(fs.ReadDir(crudSuiteDir, "."))
for _, testDirEntry := range testDirs {
testDir := api.Must(fs.Sub(crudSuiteDir, testDirEntry.Name()))

currTest, err := databasemutationhelpers.NewResourceMutationTest(
ctx,
specializer,
testDirEntry.Name(),
testDir,
)
require.NoError(t, err)

t.Run(testDirEntry.Name(), currTest.RunTestWithMock)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,56 @@ func (tt *ResourceMutationTest) RunTest(t *testing.T) {
})
require.NoError(t, err)

cosmosContainer := testInfo.CosmosResourcesContainer()
cosmosContentLoader := integrationutils.NewCosmosContentLoader(cosmosContainer)
stepInput := StepInput{
CosmosContainer: testInfo.CosmosResourcesContainer(),
CosmosContainer: cosmosContainer,
ContentLoader: cosmosContentLoader,
DocumentLister: cosmosContentLoader,
DBClient: testInfo.DBClient,
FrontendClient: testInfo.Get20240610ClientFactory,
FrontendURL: testInfo.FrontendURL,
ClusterServiceMockInfo: testInfo.ClusterServiceMock,
}
for _, step := range tt.steps {
t.Logf("Running step %s", step.StepID())
ctx := t.Context()
ctx = utils.ContextWithLogger(ctx, slogt.New(t, slogt.JSON()))

step.RunTest(ctx, t, stepInput)
}
}

// RunTestWithMock runs the test using a mock database instead of Cosmos DB.
// This allows tests to run without requiring a Cosmos DB emulator.
func (tt *ResourceMutationTest) RunTestWithMock(t *testing.T) {
ctx := t.Context()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ctx = utils.ContextWithLogger(ctx, slogt.New(t, slogt.JSON()))

frontend, testInfo, err := integrationutils.NewFrontendFromMockDB(ctx, t)
require.NoError(t, err)
cleanupCtx := context.Background()
cleanupCtx = utils.ContextWithLogger(cleanupCtx, slogt.New(t, slogt.JSON()))
defer testInfo.Cleanup(cleanupCtx)
go frontend.Run(ctx, ctx.Done())

// wait for the server to be ready for testing
err = wait.PollUntilContextCancel(ctx, 1*time.Second, true, func(ctx context.Context) (bool, error) {
_, err := http.Get(testInfo.FrontendURL)
if err != nil {
t.Log(err)
return false, nil
}
return true, nil
})
require.NoError(t, err)

stepInput := StepInput{
CosmosContainer: nil, // No Cosmos container when using mock
ContentLoader: testInfo.ContentLoader,
DocumentLister: testInfo.DocumentLister,
DBClient: testInfo.DBClient,
FrontendClient: testInfo.Get20240610ClientFactory,
FrontendURL: testInfo.FrontendURL,
Expand Down Expand Up @@ -298,6 +346,8 @@ func readRawBytesInDir(dir fs.FS) ([][]byte, error) {

type StepInput struct {
CosmosContainer *azcosmos.ContainerClient
ContentLoader integrationutils.ContentLoader
DocumentLister integrationutils.DocumentLister
DBClient database.DBClient
FrontendClient func(subscriptionID string) *hcpsdk20240610preview.ClientFactory
FrontendURL string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,26 +53,35 @@ func (l *cosmosCompare) StepID() StepID {
}

func (l *cosmosCompare) RunTest(ctx context.Context, t *testing.T, stepInput StepInput) {
// Query all documents in the container
querySQL := "SELECT * FROM c"
queryOptions := &azcosmos.QueryOptions{
QueryParameters: []azcosmos.QueryParameter{},
}

queryPager := stepInput.CosmosContainer.NewQueryItemsPager(querySQL, azcosmos.PartitionKey{}, queryOptions)
var allActual []*database.TypedDocument
var err error

allActual := []*database.TypedDocument{}
for queryPager.More() {
queryResponse, err := queryPager.NextPage(ctx)
if stepInput.DocumentLister != nil {
// Use the DocumentLister interface (works with both Cosmos and mock)
allActual, err = stepInput.DocumentLister.ListAllDocuments(ctx)
require.NoError(t, err)
} else if stepInput.CosmosContainer != nil {
// Fallback to direct Cosmos querying
querySQL := "SELECT * FROM c"
queryOptions := &azcosmos.QueryOptions{
QueryParameters: []azcosmos.QueryParameter{},
}

for _, item := range queryResponse.Items {
// Parse the document to get its ID for filename
curr := &database.TypedDocument{}
err = json.Unmarshal(item, curr)
require.NoError(t, err)
allActual = append(allActual, curr)
queryPager := stepInput.CosmosContainer.NewQueryItemsPager(querySQL, azcosmos.PartitionKey{}, queryOptions)

for queryPager.More() {
queryResponse, queryErr := queryPager.NextPage(ctx)
require.NoError(t, queryErr)

for _, item := range queryResponse.Items {
curr := &database.TypedDocument{}
err = json.Unmarshal(item, curr)
require.NoError(t, err)
allActual = append(allActual, curr)
}
}
} else {
t.Fatal("neither DocumentLister nor CosmosContainer is set")
}

for _, currExpected := range l.expectedContent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,16 @@ func (l *listActiveOperationsStep) RunTest(ctx context.Context, t *testing.T, st
parentResourceID, err := azcorearm.ParseResourceID(l.key.ParentResourceID)
require.NoError(t, err)

operationsCRUD := database.NewOperationCRUD(stepInput.CosmosContainer, parentResourceID.SubscriptionID)
var operationsCRUD database.OperationCRUD
if stepInput.DBClient != nil {
// Use DBClient interface (works with both Cosmos and mock)
operationsCRUD = stepInput.DBClient.Operations(parentResourceID.SubscriptionID)
} else if stepInput.CosmosContainer != nil {
// Fallback to direct Cosmos container
operationsCRUD = database.NewOperationCRUD(stepInput.CosmosContainer, parentResourceID.SubscriptionID)
} else {
t.Fatal("neither DBClient nor CosmosContainer is set")
}
actualControllersIterator := operationsCRUD.ListActiveOperations(nil)
require.NoError(t, err)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,16 @@ func (l *loadCosmosStep) StepID() StepID {

func (l *loadCosmosStep) RunTest(ctx context.Context, t *testing.T, stepInput StepInput) {
for _, content := range l.contents {
err := integrationutils.LoadCosmosContent(ctx, stepInput.CosmosContainer, content)
var err error
if stepInput.ContentLoader != nil {
// Use the ContentLoader interface (works with both Cosmos and mock)
err = stepInput.ContentLoader.LoadContent(ctx, content)
} else if stepInput.CosmosContainer != nil {
// Fallback to direct Cosmos loading
err = integrationutils.LoadCosmosContent(ctx, stepInput.CosmosContainer, content)
} else {
t.Fatal("neither ContentLoader nor CosmosContainer is set")
}
require.NoError(t, err, "failed to load cosmos content: %v", string(content))
}
}
84 changes: 84 additions & 0 deletions test-integration/utils/integrationutils/content_loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2025 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package integrationutils

import (
"context"
"encoding/json"

"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"

"github.com/Azure/ARO-HCP/internal/database"
)

// ContentLoader is an interface for loading test content into a database.
// This abstraction allows tests to run against either a real Cosmos DB
// or a mock database implementation.
type ContentLoader interface {
// LoadContent loads a single JSON document into the database.
LoadContent(ctx context.Context, content []byte) error
}

// DocumentLister is an interface for listing all documents from a database.
// This is used by the cosmosCompare step to verify database contents.
type DocumentLister interface {
// ListAllDocuments returns all documents in the database.
ListAllDocuments(ctx context.Context) ([]*database.TypedDocument, error)
}

// CosmosContentLoader implements ContentLoader and DocumentLister using a real Cosmos DB container.
type CosmosContentLoader struct {
container *azcosmos.ContainerClient
}

// NewCosmosContentLoader creates a new CosmosContentLoader from a Cosmos container.
func NewCosmosContentLoader(container *azcosmos.ContainerClient) *CosmosContentLoader {
return &CosmosContentLoader{container: container}
}

// LoadContent loads a single JSON document into Cosmos DB.
func (c *CosmosContentLoader) LoadContent(ctx context.Context, content []byte) error {
return LoadCosmosContent(ctx, c.container, content)
}

// ListAllDocuments returns all documents in the Cosmos container.
func (c *CosmosContentLoader) ListAllDocuments(ctx context.Context) ([]*database.TypedDocument, error) {
querySQL := "SELECT * FROM c"
queryOptions := &azcosmos.QueryOptions{
QueryParameters: []azcosmos.QueryParameter{},
}

queryPager := c.container.NewQueryItemsPager(querySQL, azcosmos.PartitionKey{}, queryOptions)

var results []*database.TypedDocument
for queryPager.More() {
queryResponse, err := queryPager.NextPage(ctx)
if err != nil {
return nil, err
}

for _, item := range queryResponse.Items {
var doc database.TypedDocument
if err := json.Unmarshal(item, &doc); err != nil {
return nil, err
}
results = append(results, &doc)
}
}
return results, nil
}

var _ ContentLoader = &CosmosContentLoader{}
var _ DocumentLister = &CosmosContentLoader{}
Loading