Skip to content

Commit 7a7e6f9

Browse files
authored
Merge pull request #1225 from LeanerCloud/test/test-02-cli-purchase-path-tests
test(cmd): make CSV purchase-path tests assert real behavior
2 parents ca52ab7 + 59cbdd8 commit 7a7e6f9

3 files changed

Lines changed: 291 additions & 144 deletions

File tree

‎cmd/multi_service.go‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func effectiveDryRun(cfg Config) bool {
8383
// (2) score, display, confirm, and purchase.
8484
func runToolMultiService(ctx context.Context, cfg Config) {
8585
if cfg.CSVInput != "" {
86-
runToolFromCSV(ctx, cfg)
86+
runCSVPathOrFatal(ctx, cfg)
8787
return
8888
}
8989

@@ -272,8 +272,20 @@ func buildServiceStats(recs []common.Recommendation, results []common.PurchaseRe
272272
return stats
273273
}
274274

275+
// runCSVPathOrFatal runs the CSV purchase path and exits fatally on error.
276+
// It isolates the error-to-fatal glue from runToolMultiService so that path
277+
// stays under the cyclomatic-complexity budget while runToolFromCSV remains
278+
// unit-testable via its returned error.
279+
func runCSVPathOrFatal(ctx context.Context, cfg Config) {
280+
if err := runToolFromCSV(ctx, cfg); err != nil {
281+
log.Fatalf("%v", err)
282+
}
283+
}
284+
275285
// runToolFromCSV processes recommendations from a CSV input file.
276-
func runToolFromCSV(ctx context.Context, cfg Config) {
286+
// It returns an error instead of exiting so the orchestration glue is
287+
// unit-testable; the caller (runCSVPathOrFatal) turns errors fatal.
288+
func runToolFromCSV(ctx context.Context, cfg Config) error {
277289
isDryRun := effectiveDryRun(cfg)
278290
printRunMode(isDryRun)
279291

@@ -284,7 +296,7 @@ func runToolFromCSV(ctx context.Context, cfg Config) {
284296
// Read recommendations from CSV
285297
recs, err := loadRecommendationsFromCSV(cfg.CSVInput)
286298
if err != nil {
287-
log.Fatalf("Failed to read CSV file: %v", err)
299+
return fmt.Errorf("failed to read CSV file: %w", err)
288300
}
289301

290302
AppLogger.Printf("✅ Loaded %d recommendations from CSV\n", len(recs))
@@ -294,7 +306,7 @@ func runToolFromCSV(ctx context.Context, cfg Config) {
294306

295307
if len(recs) == 0 {
296308
AppLogger.Println("⚠️ No recommendations to process after filtering")
297-
return
309+
return nil
298310
}
299311

300312
// Load AWS configuration
@@ -305,7 +317,7 @@ func runToolFromCSV(ctx context.Context, cfg Config) {
305317
}
306318
awsCfg, err := awsconfig.LoadDefaultConfig(ctx, configOptions...)
307319
if err != nil {
308-
log.Fatalf("Failed to load AWS config: %v", err)
320+
return fmt.Errorf("failed to load AWS config: %w", err)
309321
}
310322

311323
// Create account alias cache for lookup
@@ -386,6 +398,7 @@ func runToolFromCSV(ctx context.Context, cfg Config) {
386398
// Print final summary using the post-dedup slice so counts match what was
387399
// actually processed, not the pre-dedup input passed into the outer loop.
388400
printMultiServiceSummary(allAdjustedRecs, allResults, serviceStats, isDryRun)
401+
return nil
389402
}
390403

391404
// filterAndAdjustRecommendations applies filters, coverage, count override, and instance limits to recommendations.

‎cmd/multi_service_coverage_test.go‎

Lines changed: 99 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import (
99

1010
"github.com/LeanerCloud/CUDly/pkg/common"
1111
"github.com/aws/aws-sdk-go-v2/aws"
12+
awsrds "github.com/aws/aws-sdk-go-v2/service/rds"
13+
rdstypes "github.com/aws/aws-sdk-go-v2/service/rds/types"
1214
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
1316
)
1417

1518
// ==================== Tests for coverage improvement ====================
@@ -18,72 +21,124 @@ import (
1821

1922
// ==================== Tests for queryMajorEngineVersions ====================
2023

21-
func TestQueryMajorEngineVersions_Success(t *testing.T) {
22-
// This test verifies the logic of queryMajorEngineVersions without mocking
23-
// Since it requires AWS credentials, we test the error cases and structure
24+
// engineKeyedRDSMajorVersionsStub implements RDSMajorVersionsClient with
25+
// canned per-engine responses and records which engines were queried, so
26+
// queryMajorEngineVersionsWithClient can be tested hermetically (no AWS
27+
// credentials, no network).
28+
type engineKeyedRDSMajorVersionsStub struct {
29+
versionsByEngine map[string][]rdstypes.DBMajorEngineVersion
30+
errByEngine map[string]error
31+
enginesQueried []string
32+
}
2433

25-
ctx := context.Background()
26-
origCfg := toolCfg
27-
defer func() { toolCfg = origCfg }()
34+
func (s *engineKeyedRDSMajorVersionsStub) DescribeDBMajorEngineVersions(
35+
_ context.Context,
36+
params *awsrds.DescribeDBMajorEngineVersionsInput,
37+
_ ...func(*awsrds.Options),
38+
) (*awsrds.DescribeDBMajorEngineVersionsOutput, error) {
39+
engine := aws.ToString(params.Engine)
40+
s.enginesQueried = append(s.enginesQueried, engine)
41+
if err := s.errByEngine[engine]; err != nil {
42+
return nil, err
43+
}
44+
return &awsrds.DescribeDBMajorEngineVersionsOutput{
45+
DBMajorEngineVersions: s.versionsByEngine[engine],
46+
}, nil
47+
}
2848

29-
// Test with empty profile (should work if AWS credentials are configured)
30-
toolCfg.Profile = ""
31-
toolCfg.ValidationProfile = ""
49+
// TestQueryMajorEngineVersionsWithClient_Success replaces a former
50+
// live-credential test: it stubs the RDS client and asserts the merged
51+
// engine:major-version map deterministically.
52+
func TestQueryMajorEngineVersionsWithClient_Success(t *testing.T) {
53+
stub := &engineKeyedRDSMajorVersionsStub{
54+
versionsByEngine: map[string][]rdstypes.DBMajorEngineVersion{
55+
"mysql": {rdsMajorVersion("mysql", "5.7"), rdsMajorVersion("mysql", "8.0")},
56+
"postgres": {rdsMajorVersion("postgres", "13")},
57+
},
58+
}
3259

33-
// This will attempt to load AWS config - may fail without credentials
34-
result, err := queryMajorEngineVersions(ctx, toolCfg)
60+
result, err := queryMajorEngineVersionsWithClient(context.Background(), stub)
61+
require.NoError(t, err)
62+
63+
assert.ElementsMatch(t,
64+
[]string{"mysql", "postgres", "aurora-mysql", "aurora-postgresql"},
65+
stub.enginesQueried,
66+
"must query every supported engine exactly once")
67+
68+
require.Len(t, result, 3)
69+
assert.Equal(t, "mysql", result["mysql:5.7"].Engine)
70+
assert.Equal(t, "5.7", result["mysql:5.7"].MajorEngineVersion)
71+
assert.Equal(t, "8.0", result["mysql:8.0"].MajorEngineVersion)
72+
assert.Equal(t, "13", result["postgres:13"].MajorEngineVersion)
73+
require.Len(t, result["postgres:13"].SupportedEngineLifecycles, 1,
74+
"lifecycle data must be carried through from the API response")
75+
}
3576

36-
// Either succeeds with valid credentials or fails gracefully
37-
if err != nil {
38-
assert.Error(t, err)
39-
assert.Contains(t, err.Error(), "failed to load AWS config")
40-
} else {
41-
// If it succeeds, verify the result structure
42-
assert.NotNil(t, result)
43-
// Result can be empty if no versions are found
77+
// TestQueryMajorEngineVersionsWithClient_EngineErrorContinues asserts the
78+
// warn-and-continue contract: one engine failing must not drop the results
79+
// of the others, and the overall call still succeeds.
80+
func TestQueryMajorEngineVersionsWithClient_EngineErrorContinues(t *testing.T) {
81+
stub := &engineKeyedRDSMajorVersionsStub{
82+
versionsByEngine: map[string][]rdstypes.DBMajorEngineVersion{
83+
"postgres": {rdsMajorVersion("postgres", "15")},
84+
},
85+
errByEngine: map[string]error{
86+
"mysql": errors.New("DescribeDBMajorEngineVersions throttled"),
87+
},
4488
}
89+
90+
result, err := queryMajorEngineVersionsWithClient(context.Background(), stub)
91+
require.NoError(t, err, "per-engine API failures are warn-and-continue")
92+
93+
assert.ElementsMatch(t,
94+
[]string{"mysql", "postgres", "aurora-mysql", "aurora-postgresql"},
95+
stub.enginesQueried,
96+
"a failing engine must not stop the remaining engine queries")
97+
require.Len(t, result, 1)
98+
assert.Equal(t, "15", result["postgres:15"].MajorEngineVersion)
4599
}
46100

47-
func TestQueryMajorEngineVersions_ProfileHandling(t *testing.T) {
101+
// TestQueryMajorEngineVersions_ProfileSelection asserts validation-profile
102+
// precedence deterministically: the configured profiles point at names
103+
// guaranteed not to exist in the (redirected, empty) shared config, so
104+
// config loading fails fast without any live AWS call and the error names
105+
// the profile the function actually selected.
106+
func TestQueryMajorEngineVersions_ProfileSelection(t *testing.T) {
48107
ctx := context.Background()
49-
origCfg := toolCfg
50-
defer func() { toolCfg = origCfg }()
51108

52109
tests := []struct {
53110
name string
54111
profile string
55112
validationProfile string
113+
wantProfileInErr string
56114
}{
57115
{
58-
name: "Uses validation profile if set",
59-
profile: "main-profile",
60-
validationProfile: "validation-profile",
116+
name: "validation profile takes precedence",
117+
profile: "cudly-test-main-profile-does-not-exist",
118+
validationProfile: "cudly-test-validation-profile-does-not-exist",
119+
wantProfileInErr: "cudly-test-validation-profile-does-not-exist",
61120
},
62121
{
63-
name: "Falls back to main profile",
64-
profile: "main-profile",
65-
validationProfile: "",
66-
},
67-
{
68-
name: "Empty profiles use default",
69-
profile: "",
70-
validationProfile: "",
122+
name: "falls back to main profile",
123+
profile: "cudly-test-main-profile-does-not-exist",
124+
wantProfileInErr: "cudly-test-main-profile-does-not-exist",
71125
},
72126
}
73127

74128
for _, tt := range tests {
75129
t.Run(tt.name, func(t *testing.T) {
76-
toolCfg.Profile = tt.profile
77-
toolCfg.ValidationProfile = tt.validationProfile
78-
79-
// This will attempt to load config - may fail without valid profiles
80-
_, err := queryMajorEngineVersions(ctx, toolCfg)
81-
82-
// We just verify it doesn't panic - actual AWS calls may fail
83-
// Error is acceptable for invalid profiles
84-
if err != nil {
85-
assert.Error(t, err)
86-
}
130+
t.Setenv("AWS_CONFIG_FILE", os.DevNull)
131+
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", os.DevNull)
132+
133+
_, err := queryMajorEngineVersions(ctx, Config{
134+
Profile: tt.profile,
135+
ValidationProfile: tt.validationProfile,
136+
})
137+
138+
require.Error(t, err)
139+
assert.Contains(t, err.Error(), "failed to load AWS config")
140+
assert.Contains(t, err.Error(), tt.wantProfileInErr,
141+
"error must name the selected profile")
87142
})
88143
}
89144
}

0 commit comments

Comments
 (0)