Skip to content

Commit 7d12edc

Browse files
committed
Adds additional unit tests to validate against setting invalid values
1 parent 7e76087 commit 7d12edc

File tree

1 file changed

+276
-0
lines changed

1 file changed

+276
-0
lines changed

cli/azd/internal/grpcserver/project_service_test.go

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,3 +1094,279 @@ func Test_ProjectService_ChangeServiceHost(t *testing.T) {
10941094
"persisted host should be 'appservice'")
10951095
}
10961096

1097+
// Test_ProjectService_TypeValidation_InvalidChangesNotPersisted tests that invalid type changes
1098+
// fail validation and are not persisted to disk.
1099+
func Test_ProjectService_TypeValidation_InvalidChangesNotPersisted(t *testing.T) {
1100+
mockContext := mocks.NewMockContext(context.Background())
1101+
temp := t.TempDir()
1102+
1103+
azdContext := azdcontext.NewAzdContextWithDirectory(temp)
1104+
1105+
// Create initial project with a service
1106+
projectConfig := &project.ProjectConfig{
1107+
Name: "test-project",
1108+
Services: map[string]*project.ServiceConfig{
1109+
"web": {
1110+
Name: "web",
1111+
RelativePath: "./src/web",
1112+
Host: project.ContainerAppTarget,
1113+
Language: project.ServiceLanguageDotNet,
1114+
},
1115+
},
1116+
}
1117+
1118+
err := project.Save(*mockContext.Context, projectConfig, azdContext.ProjectPath())
1119+
require.NoError(t, err)
1120+
1121+
loadedConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1122+
require.NoError(t, err)
1123+
1124+
// Setup lazy dependencies
1125+
lazyAzdContext := lazy.From(azdContext)
1126+
fileConfigManager := config.NewFileConfigManager(config.NewManager())
1127+
localDataStore := environment.NewLocalFileDataStore(azdContext, fileConfigManager)
1128+
envManager, err := environment.NewManager(
1129+
mockContext.Container,
1130+
azdContext,
1131+
mockContext.Console,
1132+
localDataStore,
1133+
nil,
1134+
)
1135+
require.NoError(t, err)
1136+
lazyEnvManager := lazy.From(envManager)
1137+
lazyProjectConfig := lazy.From(loadedConfig)
1138+
1139+
service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig)
1140+
1141+
t.Run("Project_SetInfraToInt_ShouldFailAndNotPersist", func(t *testing.T) {
1142+
// Try to set "infra" (which should be an object) to an integer
1143+
intValue, err := structpb.NewValue(123)
1144+
require.NoError(t, err)
1145+
1146+
_, err = service.SetConfigValue(*mockContext.Context, &azdext.SetProjectConfigValueRequest{
1147+
Path: "infra",
1148+
Value: intValue,
1149+
})
1150+
1151+
// This should fail because "infra" expects a provisioning.Options struct, not an int
1152+
require.Error(t, err, "setting infra to int should fail validation")
1153+
1154+
// Verify the change was NOT persisted to disk
1155+
reloadedConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1156+
require.NoError(t, err)
1157+
require.NotNil(t, reloadedConfig.Infra, "infra should still be valid object")
1158+
require.Empty(t, reloadedConfig.Infra.Provider, "infra.provider should be empty (default)")
1159+
})
1160+
1161+
t.Run("Project_SetInfraProviderToObject_ShouldFailAndNotPersist", func(t *testing.T) {
1162+
// Try to set "infra.provider" (which should be a string) to an object
1163+
objectValue, err := structpb.NewStruct(map[string]interface{}{
1164+
"nested": "value",
1165+
})
1166+
require.NoError(t, err)
1167+
1168+
_, err = service.SetConfigValue(*mockContext.Context, &azdext.SetProjectConfigValueRequest{
1169+
Path: "infra.provider",
1170+
Value: structpb.NewStructValue(objectValue),
1171+
})
1172+
1173+
// This should fail because "infra.provider" expects a string, not an object
1174+
require.Error(t, err, "setting infra.provider to object should fail validation")
1175+
1176+
// Verify the change was NOT persisted to disk
1177+
reloadedConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1178+
require.NoError(t, err)
1179+
require.Empty(t, reloadedConfig.Infra.Provider, "infra.provider should still be empty")
1180+
})
1181+
1182+
t.Run("Project_SetInfraProviderToInt_FailsDuringSet", func(t *testing.T) {
1183+
// Try to set "infra.provider" to an int instead of a string
1184+
invalidProvider, err := structpb.NewValue(999)
1185+
require.NoError(t, err)
1186+
1187+
_, err = service.SetConfigValue(*mockContext.Context, &azdext.SetProjectConfigValueRequest{
1188+
Path: "infra.provider",
1189+
Value: invalidProvider,
1190+
})
1191+
1192+
// SetConfigValue calls reloadAndCacheProjectConfig which calls project.Load
1193+
// project.Load fails because "999" is not a valid provider
1194+
require.Error(t, err)
1195+
require.Contains(t, err.Error(), "unsupported IaC provider '999'")
1196+
1197+
// Verify the change was NOT persisted to disk (should still be valid)
1198+
reloadedConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1199+
require.NoError(t, err)
1200+
require.Empty(t, reloadedConfig.Infra.Provider)
1201+
})
1202+
1203+
t.Run("Service_SetHostToInt_CoercesToString", func(t *testing.T) {
1204+
// Save the current state
1205+
originalConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1206+
require.NoError(t, err)
1207+
originalHost := originalConfig.Services["web"].Host
1208+
1209+
// Try to set "host" to an integer instead of a string
1210+
invalidValue, err := structpb.NewValue(789)
1211+
require.NoError(t, err)
1212+
1213+
_, err = service.SetServiceConfigValue(*mockContext.Context, &azdext.SetServiceConfigValueRequest{
1214+
ServiceName: "web",
1215+
Path: "host",
1216+
Value: invalidValue,
1217+
})
1218+
1219+
// This succeeds at the config level (YAML allows numbers)
1220+
require.NoError(t, err)
1221+
1222+
// YAML coerces 789 to string "789", which is then treated as a custom host value
1223+
// (project.Load doesn't fail on unknown host types, it treats them as custom)
1224+
reloadedConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1225+
require.NoError(t, err)
1226+
require.Equal(t, project.ServiceTargetKind("789"), reloadedConfig.Services["web"].Host)
1227+
1228+
// Restore the original valid configuration
1229+
err = project.Save(*mockContext.Context, originalConfig, azdContext.ProjectPath())
1230+
require.NoError(t, err)
1231+
1232+
// Verify restoration succeeded
1233+
restoredConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1234+
require.NoError(t, err)
1235+
require.Equal(t, originalHost, restoredConfig.Services["web"].Host)
1236+
})
1237+
1238+
t.Run("Service_SetLanguageToArray_ShouldFailAndNotPersist", func(t *testing.T) {
1239+
// Get current language value
1240+
originalConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1241+
require.NoError(t, err)
1242+
originalLanguage := originalConfig.Services["web"].Language
1243+
1244+
// Try to set "language" to an array
1245+
arrayValue, err := structpb.NewList([]interface{}{"go", "python"})
1246+
require.NoError(t, err)
1247+
1248+
_, err = service.SetServiceConfigValue(*mockContext.Context, &azdext.SetServiceConfigValueRequest{
1249+
ServiceName: "web",
1250+
Path: "language",
1251+
Value: structpb.NewListValue(arrayValue),
1252+
})
1253+
1254+
// This should fail because "language" expects a string, not an array
1255+
require.Error(t, err, "setting language to array should fail validation")
1256+
1257+
// Verify the change was NOT persisted to disk
1258+
reloadedConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1259+
require.NoError(t, err)
1260+
require.Equal(t, originalLanguage, reloadedConfig.Services["web"].Language,
1261+
"language should still have original value")
1262+
})
1263+
1264+
t.Run("Service_SetDockerToInvalidStructure_ShouldSucceedButFailOnReload", func(t *testing.T) {
1265+
// Save the current state
1266+
originalConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1267+
require.NoError(t, err)
1268+
1269+
// Try to set "docker.path" to an int instead of a string
1270+
invalidPath, err := structpb.NewValue(123)
1271+
require.NoError(t, err)
1272+
1273+
_, err = service.SetServiceConfigValue(*mockContext.Context, &azdext.SetServiceConfigValueRequest{
1274+
ServiceName: "web",
1275+
Path: "docker.path",
1276+
Value: invalidPath,
1277+
})
1278+
1279+
// This succeeds at the config level (YAML allows numbers)
1280+
require.NoError(t, err, "setting docker.path to int succeeds at config level")
1281+
1282+
// When we reload, YAML will coerce 123 to string "123", which is technically valid
1283+
// but semantically wrong (not a valid file path)
1284+
reloadedConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1285+
require.NoError(t, err, "parsing succeeds because YAML coerces int to string")
1286+
require.Equal(t, "123", reloadedConfig.Services["web"].Docker.Path, "path is coerced to string '123'")
1287+
1288+
// Restore the original valid configuration
1289+
err = project.Save(*mockContext.Context, originalConfig, azdContext.ProjectPath())
1290+
require.NoError(t, err)
1291+
1292+
// Verify restoration succeeded
1293+
restoredConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1294+
require.NoError(t, err)
1295+
require.Empty(t, restoredConfig.Services["web"].Docker.Path)
1296+
})
1297+
}
1298+
1299+
// Test_ProjectService_TypeValidation_CoercedValues tests YAML type coercion behavior
1300+
func Test_ProjectService_TypeValidation_CoercedValues(t *testing.T) {
1301+
mockContext := mocks.NewMockContext(context.Background())
1302+
temp := t.TempDir()
1303+
1304+
azdContext := azdcontext.NewAzdContextWithDirectory(temp)
1305+
1306+
// Create initial project
1307+
projectConfig := &project.ProjectConfig{
1308+
Name: "test-project",
1309+
}
1310+
1311+
err := project.Save(*mockContext.Context, projectConfig, azdContext.ProjectPath())
1312+
require.NoError(t, err)
1313+
1314+
loadedConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1315+
require.NoError(t, err)
1316+
1317+
// Setup lazy dependencies
1318+
lazyAzdContext := lazy.From(azdContext)
1319+
fileConfigManager := config.NewFileConfigManager(config.NewManager())
1320+
localDataStore := environment.NewLocalFileDataStore(azdContext, fileConfigManager)
1321+
envManager, err := environment.NewManager(
1322+
mockContext.Container,
1323+
azdContext,
1324+
mockContext.Console,
1325+
localDataStore,
1326+
nil,
1327+
)
1328+
require.NoError(t, err)
1329+
lazyEnvManager := lazy.From(envManager)
1330+
lazyProjectConfig := lazy.From(loadedConfig)
1331+
1332+
service := NewProjectService(lazyAzdContext, lazyEnvManager, lazyProjectConfig)
1333+
1334+
t.Run("SetNameToInt_GetsCoercedToString", func(t *testing.T) {
1335+
// Try to set "name" (which should be a string) to an integer
1336+
intValue, err := structpb.NewValue(456)
1337+
require.NoError(t, err)
1338+
1339+
_, err = service.SetConfigValue(*mockContext.Context, &azdext.SetProjectConfigValueRequest{
1340+
Path: "name",
1341+
Value: intValue,
1342+
})
1343+
1344+
// YAML will coerce the int to a string, so this succeeds
1345+
require.NoError(t, err, "YAML coerces int to string, so this succeeds")
1346+
1347+
// When loaded as ProjectConfig, it gets coerced to string "456"
1348+
reloadedConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1349+
require.NoError(t, err)
1350+
require.Equal(t, "456", reloadedConfig.Name, "YAML unmarshals int as string '456'")
1351+
})
1352+
1353+
t.Run("SetNameToBool_GetsCoercedToString", func(t *testing.T) {
1354+
// Try to set "name" to a boolean
1355+
boolValue, err := structpb.NewValue(true)
1356+
require.NoError(t, err)
1357+
1358+
_, err = service.SetConfigValue(*mockContext.Context, &azdext.SetProjectConfigValueRequest{
1359+
Path: "name",
1360+
Value: boolValue,
1361+
})
1362+
1363+
// YAML will coerce bool to string
1364+
require.NoError(t, err, "YAML coerces bool to string")
1365+
1366+
// When loaded as ProjectConfig, it gets coerced to string "true"
1367+
reloadedConfig, err := project.Load(*mockContext.Context, azdContext.ProjectPath())
1368+
require.NoError(t, err)
1369+
require.Equal(t, "true", reloadedConfig.Name, "YAML unmarshals bool as string 'true'")
1370+
})
1371+
}
1372+

0 commit comments

Comments
 (0)