Skip to content

Commit c249436

Browse files
Update config validation logic for entities (#3306)
## Why make this change? Closes #3267 ## What is this change? Alters the validation logic in the following way. Is top-level config with data-source-files? (we call this a `Root` config file) ├── YES │ ├── Has datasource? → ValidateEntityPresence (same rules as non-root) │ ├── No datasource but has entities/autoentities? → ERROR │ └── No datasource, no entities → VALID (children provide everything) │ └── For each child → ValidateNonRootConfig(child, filename) │ └── NO (standalone or child config) ├── No datasource? → ERROR: "data source is required" └── Has datasource → ValidateEntityPresence Note: A top-level config file without any children data-source files is NOT considered a root. And an intermediary config file, ie: is a child, that also has child configs is NOT a root. Only a top-level config with children configs is a Root. #### ValidateEntityPresence Count resolved autoentities from AutoentityResolutionCounts total = manual entities + resolved autoentities total == 0? → ERROR: "No entities found" total > 0 but autoentities discovered nothing? → WARN: "Autoentities configured but none discovered" No double messaging. If total is 0, only the error is recorded, not the warning. ## How was this tested? ### Truth table — top-level config Variables (`1` = present / non-empty, `0` = absent / empty): - **DSF** — `data-source-files` present - **DS** — `data-source` present - **E** — manual `entities` count > 0 - **AE** — `autoentities` count > 0 (presence, *not* resolved count) Path is determined by `IsRootConfig = (DSF == 1) && !IsChildConfig`. | # | DSF | DS | E | AE | AE resolved | Path | Expected | Test | |---|:---:|:--:|:-:|:--:|:-----------:|------|----------|------| | 1 | 0 | 0 | 0 | 0 | — | Non-root | **Error**: "data source is required" | `TestNonRootWithNoDataSourceProducesError` | | 2 | 0 | 0 | 0 | 1 | — | Non-root | **Error**: "data source is required" | _covered by #1 — DS check fires first_ | | 3 | 0 | 0 | 1 | 0 | — | Non-root | **Error**: "data source is required" | _covered by #1_ | | 4 | 0 | 0 | 1 | 1 | — | Non-root | **Error**: "data source is required" | _covered by #1_ | | 5 | 0 | 1 | 0 | 0 | — | Non-root | **Error**: "No entities found" | `TestNonRootWithDataSourceAndNoEntitiesProducesError` | | 6a | 0 | 1 | 0 | 1 | 0 | Non-root | **Error**: "No entities found" | `TestNonRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` | | 6b | 0 | 1 | 0 | 1 | >0 | Non-root | **Valid** | `TestNonRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` | | 7 | 0 | 1 | 1 | 0 | — | Non-root | **Valid** | `TestNonRootWithDataSourceAndEntitiesIsValid` | | 8a | 0 | 1 | 1 | 1 | 0 | Non-root | **Valid** + **Warn** | `TestNonRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` | | 8b | 0 | 1 | 1 | 1 | >0 | Non-root | **Valid** | _covered by #7 / #6b combined_ | | 9 | 1 | 0 | 0 | 0 | — | Root | **Valid** (children carry the load) | `TestRootWithNoDataSourceAndNoEntitiesIsValid`, `TestRootConfigWithNoDataSourceAndNoEntitiesParses` | | 10 | 1 | 0 | 0 | 1 | — | Root | **Error**: "must not define entities or autoentities" | `TestRootWithNoDataSourceButAutoentitiesProducesError` | | 11 | 1 | 0 | 1 | 0 | — | Root | **Error**: "must not define entities" | `TestRootWithNoDataSourceButEntitiesProducesError` | | 12 | 1 | 0 | 1 | 1 | — | Root | **Error** | _covered by #11_ | | 13 | 1 | 1 | 0 | 0 | — | Root (with own DS) | **Error**: "No entities found" | `TestRootWithDataSourceAndNoEntitiesProducesError` | | 14a | 1 | 1 | 0 | 1 | 0 | Root (with own DS) | **Error**: "No entities found" | `TestRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` | | 14b | 1 | 1 | 0 | 1 | >0 | Root (with own DS) | **Valid** | `TestRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` | | 15 | 1 | 1 | 1 | 0 | — | Root (with own DS) | **Valid** | `TestRootWithDataSourceAndEntitiesIsValid` | | 16a | 1 | 1 | 1 | 1 | 0 | Root (with own DS) | **Valid** + **Warn** | `TestRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` | | 16b | 1 | 1 | 1 | 1 | >0 | Root (with own DS) | **Valid** | _covered by #15 / #14b combined_ | ### Truth table — child config (validated when iterating `root.ChildConfigs`) Children are always treated as non-root regardless of their own `data-source-files`. | # | DS | E | AE | AE resolved | Expected | Test | |---|:--:|:-:|:--:|:-----------:|----------|------| | C1 | 0 | 0 | 0 | — | **Error** naming the child file: "data source is required" | `TestChildWithNoDataSourceProducesNamedError` | | C2 | 0 | * | * | — | **Error** naming the child file: "data source is required" | _covered by C1_ | | C3 | 1 | 0 | 0 | — | **Error** naming the child file: "No entities found" | `TestChildWithDataSourceAndNoEntitiesProducesNamedError` | | C4a | 1 | 0 | 1 | 0 | **Error** naming the child file: "No entities found" | `TestChildWithDataSourceAndAutoentitiesResolvingZeroProducesNamedError` | | C4b | 1 | 0 | 1 | >0 | **Valid** | _covered by C5 (resolved entities behave the same as manual entities)_ | | C5 | 1 | 1 | 0 | — | **Valid** | _implicitly via `TestRootWithDataSourceAndEntitiesIsValid` setup_ | | C6a | 1 | 1 | 1 | 0 | **Valid** + **Warn** naming the child file | `TestChildWithEntitiesAndAutoentitiesResolvingZeroLogsNamedWarning` | | C6b | 1 | 1 | 1 | >0 | **Valid** | _covered by C5_ | ### Other scenarios | Scenario | Expected | Test | |----------|----------|------| | Connection-string error gates entity validation (no entity error fires when DB unreachable) | `IsConfigValid == false` due to connection error only | `TestValidateNonRootZeroEntitiesWithInvalidConnectionString` | | Config with no entities parses cleanly (constructor no longer throws) and `IsConfigValid` returns false without throwing | parse OK, validate fails | `TestValidateConfigWithNoEntitiesProducesCleanError` _(modified)_ | | Root parses successfully without a data source | parse OK, `IsRootConfig == true` | `TestRootConfigWithNoDataSourceAndNoEntitiesParses` | | Non-root with DS and no entities parses successfully | parse OK, `IsRootConfig == false` | `TestNonRootConfigWithDataSourceAndNoEntitiesParses` | | Autoentities present but resolve to nothing — must not crash, must not double-message with "No entities found" | no crash; only "No entities found" if total = 0 | `ValidateAutoentitiesConfiguration` _(modified to `isValidateOnly: true`)_ | New tests: `TestRootConfigWithNoDataSourceAndNoEntitiesParses` Root config (has data-source-files) without datasource parses OK `TestNonRootConfigWithDataSourceAndNoEntitiesParses` Non-root config with datasource + no entities parses OK (validation catches it later) `TestNonRootWithDataSourceAndNoEntitiesProducesError` Calls ValidateDataSourceAndEntityPresence directly, error recorded `TestNonRootWithNoDataSourceProducesError` No datasource, error with "data source is required" `TestNonRootWithDataSourceAndEntitiesIsValid` Datasource + entities, no errors `TestRootWithNoDataSourceAndNoEntitiesIsValid` Root with child, no own datasource, valid `TestRootWithNoDataSourceButEntitiesProducesError` Root with entities but no datasource, error `TestRootWithDataSourceAndEntitiesIsValid` Root with own datasource + entities, valid `TestChildWithDataSourceAndNoEntitiesProducesNamedError` Child with no entities, error names the child file `TestChildWithNoDataSourceProducesNamedError` Child with no datasource, error names the child file `TestNonRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` Non-root with only autoentities that resolve to 0 `TestNonRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` Non-root with only autoentities resolving > 0 entities `TestNonRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` Non-root with manual entities + autoentities resolving 0 `TestRootWithNoDataSourceButAutoentitiesProducesError` Root with no datasource but autoentities defined `TestRootWithDataSourceAndNoEntitiesProducesError` Root with own datasource and zero entities/autoentities `TestRootWithDataSourceAndAutoentitiesResolvingZeroProducesError` Root with own datasource and autoentities resolving 0 `TestRootWithDataSourceAndAutoentitiesResolvingEntitiesIsValid` Root with own datasource and autoentities resolving > 0 `TestRootWithEntitiesAndAutoentitiesResolvingZeroLogsWarning` Root with own datasource, manual entities, and autoentities resolving 0 `TestChildWithDataSourceAndAutoentitiesResolvingZeroProducesNamedError` Child with autoentities-only resolving 0 `TestChildWithEntitiesAndAutoentitiesResolvingZeroLogsNamedWarning` Child with manual entities + autoentities resolving 0 Modified tests: `TestValidateConfigWithNoEntitiesProducesCleanError` Replaced main's version (expected parse failure) with ours: parse succeeds, IsConfigValid returns false `ValidateAutoentitiesConfiguration` Changed to isValidateOnly: true, asserts no crashes instead of zero errors --------- Co-authored-by: Anusha Kolan <anushakolan10@gmail.com>
1 parent ba3fd40 commit c249436

18 files changed

Lines changed: 948 additions & 109 deletions

src/Cli.Tests/ConfigureOptionsTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,7 @@ public void TestDatabaseTypeUpdate(string dbType)
811811
string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE);
812812
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config));
813813
Assert.IsNotNull(config.Runtime);
814-
Assert.AreEqual(config.DataSource.DatabaseType, Enum.Parse<DatabaseType>(dbType, ignoreCase: true));
814+
Assert.AreEqual(config.DataSource!.DatabaseType, Enum.Parse<DatabaseType>(dbType, ignoreCase: true));
815815
}
816816

817817
/// <summary>
@@ -841,7 +841,7 @@ public void TestDatabaseTypeUpdateCosmosDB_NoSQLToMSSQL()
841841
string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE);
842842
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config));
843843
Assert.IsNotNull(config.Runtime);
844-
Assert.AreEqual(config.DataSource.DatabaseType, DatabaseType.MSSQL);
844+
Assert.AreEqual(config.DataSource!.DatabaseType, DatabaseType.MSSQL);
845845
Assert.AreEqual(config.DataSource.Options!.GetValueOrDefault("set-session-context", false), true);
846846
Assert.IsFalse(config.DataSource.Options!.ContainsKey("database"));
847847
Assert.IsFalse(config.DataSource.Options!.ContainsKey("container"));
@@ -877,7 +877,7 @@ public void TestDatabaseTypeUpdateMSSQLToCosmosDB_NoSQL()
877877
string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE);
878878
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config));
879879
Assert.IsNotNull(config.Runtime);
880-
Assert.AreEqual(config.DataSource.DatabaseType, DatabaseType.CosmosDB_NoSQL);
880+
Assert.AreEqual(config.DataSource!.DatabaseType, DatabaseType.CosmosDB_NoSQL);
881881
Assert.AreEqual(config.DataSource.Options!.GetValueOrDefault("database"), "testdb");
882882
Assert.AreEqual(config.DataSource.Options!.GetValueOrDefault("container"), "testcontainer");
883883
Assert.AreEqual(config.DataSource.Options!.GetValueOrDefault("schema"), "testschema.gql");

src/Cli.Tests/EndToEndTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public Task TestInitForCosmosDBNoSql()
6868

6969
Assert.IsNotNull(runtimeConfig);
7070
Assert.IsTrue(runtimeConfig.AllowIntrospection);
71-
Assert.AreEqual(DatabaseType.CosmosDB_NoSQL, runtimeConfig.DataSource.DatabaseType);
71+
Assert.AreEqual(DatabaseType.CosmosDB_NoSQL, runtimeConfig.DataSource!.DatabaseType);
7272
CosmosDbNoSQLDataSourceOptions? cosmosDataSourceOptions = runtimeConfig.DataSource.GetTypedOptions<CosmosDbNoSQLDataSourceOptions>();
7373
Assert.IsNotNull(cosmosDataSourceOptions);
7474
Assert.AreEqual("graphqldb", cosmosDataSourceOptions.Database);
@@ -96,7 +96,7 @@ public void TestInitForCosmosDBPostgreSql()
9696
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig));
9797

9898
Assert.IsNotNull(runtimeConfig);
99-
Assert.AreEqual(DatabaseType.CosmosDB_PostgreSQL, runtimeConfig.DataSource.DatabaseType);
99+
Assert.AreEqual(DatabaseType.CosmosDB_PostgreSQL, runtimeConfig.DataSource!.DatabaseType);
100100
Assert.IsNotNull(runtimeConfig.Runtime);
101101
Assert.IsNotNull(runtimeConfig.Runtime.Rest);
102102
Assert.AreEqual("/rest-api", runtimeConfig.Runtime.Rest.Path);
@@ -127,7 +127,7 @@ public void TestInitializingRestAndGraphQLGlobalSettings()
127127
out RuntimeConfig? runtimeConfig,
128128
replacementSettings: replacementSettings));
129129

130-
SqlConnectionStringBuilder builder = new(runtimeConfig.DataSource.ConnectionString);
130+
SqlConnectionStringBuilder builder = new(runtimeConfig.DataSource!.ConnectionString);
131131
Assert.AreEqual(ProductInfo.GetDataApiBuilderUserAgent(), builder.ApplicationName);
132132

133133
Assert.IsNotNull(runtimeConfig);
@@ -208,7 +208,7 @@ public void TestEnablingMultipleCreateOperation(CliBool isMultipleCreateEnabled,
208208
replacementSettings: replacementSettings));
209209

210210
Assert.IsNotNull(runtimeConfig);
211-
Assert.AreEqual(expectedDbType, runtimeConfig.DataSource.DatabaseType);
211+
Assert.AreEqual(expectedDbType, runtimeConfig.DataSource!.DatabaseType);
212212
Assert.IsNotNull(runtimeConfig.Runtime);
213213
Assert.IsNotNull(runtimeConfig.Runtime.GraphQL);
214214
if (runtimeConfig.DataSource.DatabaseType is DatabaseType.MSSQL && isMultipleCreateEnabled is not CliBool.None)
@@ -247,7 +247,7 @@ public void TestAddEntity()
247247

248248
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? addRuntimeConfig));
249249
Assert.IsNotNull(addRuntimeConfig);
250-
Assert.AreEqual(TEST_ENV_CONN_STRING, addRuntimeConfig.DataSource.ConnectionString);
250+
Assert.AreEqual(TEST_ENV_CONN_STRING, addRuntimeConfig.DataSource!.ConnectionString);
251251
Assert.AreEqual(1, addRuntimeConfig.Entities.Count()); // 1 new entity added
252252
Assert.IsTrue(addRuntimeConfig.Entities.ContainsKey("todo"));
253253
Entity entity = addRuntimeConfig.Entities["todo"];

src/Cli.Tests/ModuleInitializer.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ public static void Init()
119119
VerifierSettings.IgnoreMember<DataSource>(dataSource => dataSource.DatabaseTypeNotSupportedMessage);
120120
// Ignore DefaultDataSourceName as that's not serialized in our config file.
121121
VerifierSettings.IgnoreMember<RuntimeConfig>(config => config.DefaultDataSourceName);
122+
// Ignore IsRootConfig as that's a computed property for validation, not serialized.
123+
VerifierSettings.IgnoreMember<RuntimeConfig>(config => config.IsRootConfig);
124+
// Ignore IsChildConfig as that's a runtime flag for validation, not serialized.
125+
VerifierSettings.IgnoreMember<RuntimeConfig>(config => config.IsChildConfig);
126+
// Ignore AutoentityResolutionCounts as that's populated at runtime during metadata initialization.
127+
VerifierSettings.IgnoreMember<RuntimeConfig>(config => config.AutoentityResolutionCounts);
128+
// Ignore ChildConfigs as that's populated at runtime during child config loading.
129+
VerifierSettings.IgnoreMember<RuntimeConfig>(config => config.ChildConfigs);
122130
// Ignore MaxResponseSizeMB as as that's unimportant from a test standpoint.
123131
VerifierSettings.IgnoreMember<HostOptions>(options => options.MaxResponseSizeMB);
124132
// Ignore UserProvidedMaxResponseSizeMB as that's not serialized in our config file.

src/Cli.Tests/UserDelegatedAuthRuntimeParsingTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public void TestRuntimeCanParseUserDelegatedAuthConfig()
5050
// Assert
5151
Assert.IsTrue(success);
5252
Assert.IsNotNull(config);
53-
Assert.IsNotNull(config.DataSource.UserDelegatedAuth);
53+
Assert.IsNotNull(config.DataSource!.UserDelegatedAuth);
5454
Assert.IsTrue(config.DataSource.UserDelegatedAuth.Enabled);
5555
Assert.AreEqual("https://database.windows.net", config.DataSource.UserDelegatedAuth.DatabaseAudience);
5656
}
@@ -95,7 +95,7 @@ public void TestRuntimeCanParseConfigWithoutUserDelegatedAuth()
9595
// Assert
9696
Assert.IsTrue(success);
9797
Assert.IsNotNull(config);
98-
Assert.IsNull(config.DataSource.UserDelegatedAuth);
98+
Assert.IsNull(config.DataSource!.UserDelegatedAuth);
9999
}
100100
}
101101
}

0 commit comments

Comments
 (0)