Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
291a404
refactor: extract LambdaAPI interface from concrete *lambda.Client
tooky Apr 3, 2026
9a6eca0
test: add LambdaAPI contract test suite against real AWS
tooky Apr 3, 2026
cd4e54a
feat: add FakeLambdaClient that passes the LambdaAPI contract
tooky Apr 3, 2026
96e6043
test: add fake-backed unit tests for Lambda filtering and pagination
tooky Apr 3, 2026
1f1784d
test: add fake-backed unit tests for Lambda orchestration
tooky Apr 3, 2026
93b13dc
test: trim Lambda integration tests to focused smoke tests
tooky Apr 3, 2026
d07be83
chore: mark all Lambda fakes & contract slices complete in TODO
tooky Apr 3, 2026
7eac7cd
feat: inject FakeLambdaClient into command tests via factory
tooky Apr 3, 2026
f744999
chore: mark Slice 7 complete in TODO
tooky Apr 3, 2026
2ff108a
docs: add TODO checklist for remaining cloud provider fakes
tooky Apr 3, 2026
4bd004f
Apply suggestions from code review
jumboduck Apr 20, 2026
828e726
test: skip pagination contract test when account has only 1 Lambda fu…
jumboduck Apr 20, 2026
1f03351
Remove latest_activity from repo list and get commands (#770)
jumboduck Apr 9, 2026
7b2f406
chore(deps): bump the go-dependencies group across 1 directory with 1…
dependabot[bot] Apr 9, 2026
5840ef9
chore(deps): bump actions/download-artifact from 7 to 8 (#765)
dependabot[bot] Apr 9, 2026
e8af159
chore(deps): bump go.opentelemetry.io/otel/sdk from 1.41.0 to 1.43.0 …
dependabot[bot] Apr 9, 2026
dc055d3
chore(deps): bump golang.org/x/term in the go-dependencies group (#772)
dependabot[bot] Apr 9, 2026
022a757
chore(deps): bump github.com/aws/aws-sdk-go-v2/service/ecs (#773)
dependabot[bot] Apr 11, 2026
eac9b4e
chore(deps): bump step-security/harden-runner (#774)
dependabot[bot] Apr 11, 2026
a0506e8
chore(deps): bump github.com/mattn/go-shellwords (#775)
dependabot[bot] Apr 14, 2026
3825c45
fix: skip legacy_ref self-references in checklinks plugin (#778)
AlexKantor87 Apr 15, 2026
04db728
feat(k8s-reporter): add extraVolumes, extraVolumeMounts, extraEnvVars…
AlexKantor87 Apr 15, 2026
5b5b979
Remove dead Pipedrive chat widget from docs footer (#783)
AlexKantor87 Apr 15, 2026
106b2ae
5348 sonar qube pr problem (#780)
ToreMerkely Apr 15, 2026
200bf53
feat: use appVersion as a default CLI version (#785)
mbevc1 Apr 15, 2026
9d55770
Enable retrieving sonarqube scan results for PRs (#784)
FayeSGW Apr 15, 2026
87d43ac
Update helm docs (#786)
ci-signed-commit-bot[bot] Apr 15, 2026
3a857a6
Guard against lightweight tags in release workflow (#787)
AlexKantor87 Apr 16, 2026
ee994de
docs: clarify OpenShift runAsUser must be set to null, not omitted (#…
AlexKantor87 Apr 16, 2026
9e3ba82
Sonar qube pr test now uses the lastest sonar-qube-pr scan and not a …
ToreMerkely Apr 16, 2026
f506975
feat: check for latest version (#781)
mbevc1 Apr 16, 2026
4caec2f
chore: helm-docs GHA and use more modern version (#793)
mbevc1 Apr 16, 2026
e395714
chore: re-trigger helm-docs (#795)
mbevc1 Apr 16, 2026
c97b03e
Update helm docs (#796)
ci-signed-commit-bot[bot] Apr 16, 2026
a480ac2
4808 fix attestation name check (#794)
FayeSGW Apr 17, 2026
bebcccd
Make slack failure webhook trigger on main instead of master (#797)
FayeSGW Apr 17, 2026
29ebc42
chore(deps): bump github.com/moby/spdystream from 0.5.0 to 0.5.1 (#798)
dependabot[bot] Apr 17, 2026
293155b
chore(deps): bump the go-dependencies group with 6 updates (#800)
dependabot[bot] Apr 17, 2026
f4a72c5
chore(deps): bump step-security/harden-runner (#801)
dependabot[bot] Apr 17, 2026
cc362f9
fix: remove duplicate/orphaned line in FakeLambdaClient.ListFunctions
jumboduck Apr 20, 2026
31a4507
Merge branch 'main' into introduce-lambda-fakes-and-contracts
jumboduck Apr 21, 2026
2b926ef
address review comments: add error-type comment and rename skipOrSetC…
jumboduck Apr 21, 2026
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
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ test_integration_restart_server: test_setup_restart_server
test_integration_single: test_setup
@export KOSLI_TESTS=true $(FAKE_CI_ENV) && $(GOTESTSUM) -- -p=1 ./... -run "${TARGET}"

test_smoke_aws: ## Run AWS contract and smoke tests against real AWS (requires AWS creds)
@echo "Running AWS contract and smoke tests against real AWS..."
@echo "Requires AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to be set"
@$(GOTESTSUM) -- -v -p=1 -run "LambdaContract_RealAWS|AWSTestSuite/TestGetLambdaPackageData|AWSTestSuite/TestGetEcsTasksData|AWSTestSuite/TestGetS3Data" ./internal/aws/


test_docs: deps vet ensure_network test_setup ## Test docs
./bin/test_docs_cmds.sh docs.kosli.com/content/use_cases/simulating_a_devops_system/_index.md
Expand Down
60 changes: 60 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,63 @@
- [x] Slice 2: Add `--params` flag across all three commands
- [x] Slice 3: Show params in `--show-input` output
- [x] Slice 4: Update help text and examples

## Fakes & contract tests for cloud provider integrations (#758)

### Lambda (done — this PR)

- [x] Slice 1: Define `LambdaAPI` interface and refactor signatures
- [x] Slice 2: Contract test suite against real AWS
- [x] Slice 3: Build `FakeLambdaClient` that passes the contract
- [x] Slice 4: Fake-backed unit tests for filtering and pagination
- [x] Slice 5: Fake-backed unit tests for orchestration
- [x] Slice 6: Trim existing integration tests
- [x] Slice 7: Package-level factory + fake-backed command tests

### ECS (next)

- [ ] Define `ECSAPI` interface (`ListClusters`, `DescribeClusters`, `ListServices`, `ListTasks`, `DescribeTasks`) and refactor signatures
- [ ] Contract test suite against real AWS (env-gated)
- [ ] Build `FakeECSClient` that passes the contract (nested pagination: clusters → services → tasks)
- [ ] Fake-backed unit tests for filtering (cluster names, service names, regex, exclude patterns)
- [ ] Fake-backed unit tests for orchestration (concurrent cluster/service/task fetching, error propagation)
- [ ] `NewECSClientFunc` factory + inject fake into `snapshotECS_test.go` command tests
- [ ] Trim existing ECS integration tests to smoke tests
- [ ] Add ECS to `make test_smoke_aws`

### S3

- [ ] Define `S3API` interface (decide: fake at paginator level or raw `ListObjectsV2` level)
- [ ] Contract test suite against real AWS (env-gated)
- [ ] Build `FakeS3Client` that passes the contract
- [ ] Fake-backed unit tests for path include/exclude filtering and digest computation
- [ ] `NewS3ClientFunc` factory + inject fake into `snapshotS3_test.go` command tests
- [ ] Trim existing S3 integration tests to smoke tests
- [ ] Add S3 to `make test_smoke_aws`

### Azure Apps

- [ ] Define interfaces for ARM AppService + Azure Container Registry clients
- [ ] Contract test suite against real Azure (env-gated)
- [ ] Build fakes that pass the contracts
- [ ] Fake-backed unit tests for app listing, image fingerprinting, error propagation
- [ ] Factory + inject fakes into `snapshotAzureApps_test.go` command tests
- [ ] Trim existing Azure integration tests to smoke tests

### Docker

- [ ] Define `DockerAPI` interface (Pull, Push, Tag, Remove, Run, container operations)
- [ ] Contract test suite against real Docker daemon
- [ ] Build `FakeDockerClient` that passes the contract
- [ ] Fake-backed unit tests
- [ ] Factory + inject fake into `snapshotDocker_test.go` command tests
- [ ] Trim existing Docker integration tests to smoke tests

### Kubernetes

- [ ] Define interface for Kubernetes clientset operations (pod listing, namespace listing)
- [ ] Contract test suite against real cluster (KIND, env-gated)
- [ ] Build fake that passes the contract (semaphore pattern, namespace filtering)
- [ ] Fake-backed unit tests for filtering, large-scale concurrency, error propagation
- [ ] Factory + inject fake into `snapshotK8S_test.go` command tests
- [ ] Trim existing Kube integration tests to smoke tests
95 changes: 47 additions & 48 deletions cmd/kosli/snapshotLambda_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import (
"fmt"
"testing"

"github.com/kosli-dev/cli/internal/testHelpers"
"github.com/aws/aws-sdk-go-v2/service/lambda/types"
"github.com/kosli-dev/cli/internal/aws"
"github.com/stretchr/testify/suite"
)

Expand All @@ -19,10 +20,6 @@ type SnapshotLambdaTestSuite struct {
imageFunctionName string
}

type snapshotLambdaTestConfig struct {
requireAuthToBeSet bool
}

func (suite *SnapshotLambdaTestSuite) SetupTest() {
suite.envName = "snapshot-lambda-env"
suite.zipFunctionName = "cli-tests"
Expand All @@ -35,67 +32,75 @@ func (suite *SnapshotLambdaTestSuite) SetupTest() {
suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)

CreateEnv(global.Org, suite.envName, "lambda", suite.T())

// Inject fake Lambda client so tests run without AWS credentials.
// The fake is seeded with two functions matching the names used in test cases.
zipCodeSha256 := "Mh48OOkSYuXHLfS9QF6bF3tvTXUOGvC3jKLiuF1vkbQ="
imageCodeSha256 := "e908950659e56bb886acbb0ecf9b8f38bf6e0382ede71095e166269ee4db601e"
lastModified := "2024-01-15T10:30:00.000+0000"
zipName := suite.zipFunctionName
imageName := suite.imageFunctionName
aws.NewLambdaClientFunc = func(_ *aws.AWSStaticCreds) (aws.LambdaAPI, error) {
return &aws.FakeLambdaClient{
Functions: []types.FunctionConfiguration{
{
FunctionName: &zipName,
CodeSha256: &zipCodeSha256,
LastModified: &lastModified,
PackageType: types.PackageTypeZip,
},
{
FunctionName: &imageName,
CodeSha256: &imageCodeSha256,
LastModified: &lastModified,
PackageType: types.PackageTypeImage,
},
},
}, nil
}
}

func (suite *SnapshotLambdaTestSuite) TearDownTest() {
aws.ResetLambdaClientFactory()
Comment thread
jumboduck marked this conversation as resolved.
}

func (suite *SnapshotLambdaTestSuite) TestSnapshotLambdaCmd() {
tests := []cmdTestCase{
{
name: "snapshot lambda works with deprecated --function-name for Zip package type",
cmd: fmt.Sprintf(`snapshot lambda %s %s --function-name %s`, suite.envName, suite.defaultKosliArguments, suite.zipFunctionName),
additionalConfig: snapshotLambdaTestConfig{
requireAuthToBeSet: true,
},
name: "snapshot lambda works with deprecated --function-name for Zip package type",
cmd: fmt.Sprintf(`snapshot lambda %s %s --function-name %s`, suite.envName, suite.defaultKosliArguments, suite.zipFunctionName),
golden: fmt.Sprintf("Flag --function-name has been deprecated, use --function-names instead\n1 lambda functions were reported to environment %s\n", suite.envName),
},
{
name: "snapshot lambda works with --function-names for Zip package type",
cmd: fmt.Sprintf(`snapshot lambda %s %s --function-names %s`, suite.envName, suite.defaultKosliArguments, suite.zipFunctionName),
additionalConfig: snapshotLambdaTestConfig{
requireAuthToBeSet: true,
},
name: "snapshot lambda works with --function-names for Zip package type",
cmd: fmt.Sprintf(`snapshot lambda %s %s --function-names %s`, suite.envName, suite.defaultKosliArguments, suite.zipFunctionName),
golden: fmt.Sprintf("1 lambda functions were reported to environment %s\n", suite.envName),
},
{
name: "snapshot lambda works with --function-names taking a list of functions",
cmd: fmt.Sprintf(`snapshot lambda %s %s --function-names %s,%s`, suite.envName, suite.defaultKosliArguments, suite.zipFunctionName, suite.imageFunctionName),
additionalConfig: snapshotLambdaTestConfig{
requireAuthToBeSet: true,
},
name: "snapshot lambda works with --function-names taking a list of functions",
cmd: fmt.Sprintf(`snapshot lambda %s %s --function-names %s,%s`, suite.envName, suite.defaultKosliArguments, suite.zipFunctionName, suite.imageFunctionName),
golden: fmt.Sprintf("2 lambda functions were reported to environment %s\n", suite.envName),
},
{
name: "snapshot lambda works with --function-names for Image package type",
cmd: fmt.Sprintf(`snapshot lambda %s %s --function-names %s`, suite.envName, suite.defaultKosliArguments, suite.imageFunctionName),
additionalConfig: snapshotLambdaTestConfig{
requireAuthToBeSet: true,
},
name: "snapshot lambda works with --function-names for Image package type",
cmd: fmt.Sprintf(`snapshot lambda %s %s --function-names %s`, suite.envName, suite.defaultKosliArguments, suite.imageFunctionName),
golden: fmt.Sprintf("1 lambda functions were reported to environment %s\n", suite.envName),
},
{
name: "snapshot lambda works with --function-names and deprecated --function-version which is ignored",
cmd: fmt.Sprintf(`snapshot lambda %s %s --function-names %s --function-version 317`, suite.envName, suite.defaultKosliArguments, suite.zipFunctionName),
additionalConfig: snapshotLambdaTestConfig{
requireAuthToBeSet: true,
},
name: "snapshot lambda works with --function-names and deprecated --function-version which is ignored",
cmd: fmt.Sprintf(`snapshot lambda %s %s --function-names %s --function-version 317`, suite.envName, suite.defaultKosliArguments, suite.zipFunctionName),
golden: fmt.Sprintf("Flag --function-version has been deprecated, --function-version is no longer supported. It will be removed in a future release.\n1 lambda functions were reported to environment %s\n", suite.envName),
},
{
wantError: false,
name: "snapshot lambda without --function-names will report all lambdas in the AWS account",
cmd: fmt.Sprintf(`snapshot lambda %s %s`, suite.envName, suite.defaultKosliArguments),
additionalConfig: snapshotLambdaTestConfig{
requireAuthToBeSet: true,
},
name: "snapshot lambda without --function-names will report all lambdas",
cmd: fmt.Sprintf(`snapshot lambda %s %s`, suite.envName, suite.defaultKosliArguments),
goldenRegex: fmt.Sprintf("[0-9]+ lambda functions were reported to environment %s\n", suite.envName),
},
{
wantError: true,
name: "snapshot lambda fails when both of --function-name and --function-names are set",
cmd: fmt.Sprintf(`snapshot lambda %s --function-name foo --function-names foo %s`, suite.envName, suite.defaultKosliArguments),
additionalConfig: snapshotLambdaTestConfig{
requireAuthToBeSet: true,
},
golden: "Flag --function-name has been deprecated, use --function-names instead\nError: only one of --function-name, --function-names, --exclude is allowed\n",
golden: "Flag --function-name has been deprecated, use --function-names instead\nError: only one of --function-name, --function-names, --exclude is allowed\n",
},
{
wantError: true,
Expand All @@ -122,19 +127,13 @@ func (suite *SnapshotLambdaTestSuite) TestSnapshotLambdaCmd() {
golden: "Error: only one of --function-name, --function-names, --exclude-regex is allowed\n",
},
{
name: "snapshot lambda works if both --exclude and --exclude-regex are set",
cmd: fmt.Sprintf(`snapshot lambda %s %s --exclude %s --exclude-regex function1`, suite.envName, suite.defaultKosliArguments, suite.zipFunctionName),
additionalConfig: snapshotLambdaTestConfig{
requireAuthToBeSet: true,
},
name: "snapshot lambda works if both --exclude and --exclude-regex are set",
cmd: fmt.Sprintf(`snapshot lambda %s %s --exclude %s --exclude-regex function1`, suite.envName, suite.defaultKosliArguments, suite.zipFunctionName),
goldenRegex: fmt.Sprintf("[0-9]+ lambda functions were reported to environment %s\n", suite.envName),
},
}

for _, t := range tests {
if t.additionalConfig != nil && t.additionalConfig.(snapshotLambdaTestConfig).requireAuthToBeSet {
testHelpers.SkipIfEnvVarUnset(suite.T(), []string{"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"})
}
runTestCmd(suite.T(), []cmdTestCase{t})
}
}
Expand Down
36 changes: 31 additions & 5 deletions internal/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,27 @@ func (staticCreds *AWSStaticCreds) NewLambdaClient() (*lambda.Client, error) {
return lambda.NewFromConfig(cfg), nil
}

// LambdaAPI is the interface for AWS Lambda operations used by this package.
// The real *lambda.Client satisfies this implicitly.
type LambdaAPI interface {
ListFunctions(ctx context.Context, params *lambda.ListFunctionsInput, optFns ...func(*lambda.Options)) (*lambda.ListFunctionsOutput, error)
GetFunctionConfiguration(ctx context.Context, params *lambda.GetFunctionConfigurationInput, optFns ...func(*lambda.Options)) (*lambda.GetFunctionConfigurationOutput, error)
}

// defaultNewLambdaClient creates a real Lambda client from credentials.
func defaultNewLambdaClient(creds *AWSStaticCreds) (LambdaAPI, error) {
return creds.NewLambdaClient()
}

// NewLambdaClientFunc is the factory used by GetLambdaPackageData to create a
// LambdaAPI client. Tests can replace this to inject a FakeLambdaClient.
var NewLambdaClientFunc = defaultNewLambdaClient
Comment thread
jumboduck marked this conversation as resolved.
Comment thread
jumboduck marked this conversation as resolved.

// ResetLambdaClientFactory restores the default (real AWS) client factory.
func ResetLambdaClientFactory() {
NewLambdaClientFunc = defaultNewLambdaClient
}
Comment thread
jumboduck marked this conversation as resolved.

// NewECSClient returns a new ECS API client
func (staticCreds *AWSStaticCreds) NewECSClient() (*ecs.Client, error) {
cfg, err := staticCreds.NewAWSConfigFromEnvOrFlags()
Expand All @@ -149,7 +170,7 @@ func (staticCreds *AWSStaticCreds) NewECSClient() (*ecs.Client, error) {
}

// getFilteredLambdaFuncs fetches a filtered set of lambda functions recursively (50 at a time) and returns a list of FunctionConfiguration
func getFilteredLambdaFuncs(client *lambda.Client, nextMarker *string, allFunctions *[]types.FunctionConfiguration,
func getFilteredLambdaFuncs(client LambdaAPI, nextMarker *string, allFunctions *[]types.FunctionConfiguration,
filter *filters.ResourceFilterOptions) (*[]types.FunctionConfiguration, error) {
params := &lambda.ListFunctionsInput{}
if nextMarker != nil {
Expand Down Expand Up @@ -187,11 +208,16 @@ func getFilteredLambdaFuncs(client *lambda.Client, nextMarker *string, allFuncti

// GetLambdaPackageData returns a digest and metadata of a Lambda function package
func (staticCreds *AWSStaticCreds) GetLambdaPackageData(filter *filters.ResourceFilterOptions) ([]*LambdaData, error) {
lambdaData := []*LambdaData{}
client, err := staticCreds.NewLambdaClient()
client, err := NewLambdaClientFunc(staticCreds)
if err != nil {
return lambdaData, err
return []*LambdaData{}, err
}
return getLambdaPackageDataFromClient(client, filter)
}

// getLambdaPackageDataFromClient fetches Lambda function data using the provided LambdaAPI client.
func getLambdaPackageDataFromClient(client LambdaAPI, filter *filters.ResourceFilterOptions) ([]*LambdaData, error) {
lambdaData := []*LambdaData{}

filteredFunctions, err := getFilteredLambdaFuncs(client, nil, &[]types.FunctionConfiguration{}, filter)
if err != nil {
Expand Down Expand Up @@ -247,7 +273,7 @@ func (staticCreds *AWSStaticCreds) GetLambdaPackageData(filter *filters.Resource
}

// getAndProcessOneLambdaFunc get a lambda function by its name and return a LambdaData object from it
func getAndProcessOneLambdaFunc(client *lambda.Client, functionName string) (*LambdaData, error) {
func getAndProcessOneLambdaFunc(client LambdaAPI, functionName string) (*LambdaData, error) {
params := &lambda.GetFunctionConfigurationInput{
FunctionName: aws.String(functionName),
}
Expand Down
Loading
Loading