Skip to content

Commit e90cddb

Browse files
committed
Adds additional unit tests to validate against setting invalid values
1 parent 5771884 commit e90cddb

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
@@ -1047,3 +1047,279 @@ func Test_ProjectService_ChangeServiceHost(t *testing.T) {
10471047
"persisted host should be 'appservice'")
10481048
}
10491049

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

0 commit comments

Comments
 (0)