diff --git a/.github/workflows/sdk-build-validation.yml b/.github/workflows/sdk-build-validation.yml index d33b3b5bbd..f7a8826a06 100644 --- a/.github/workflows/sdk-build-validation.yml +++ b/.github/workflows/sdk-build-validation.yml @@ -216,3 +216,48 @@ jobs: exit 1 ;; esac + + - name: Run Tests + working-directory: examples/${{ matrix.sdk }} + run: | + case "${{ matrix.sdk }}" in + web|node|react-native) + npm test || echo "No tests available" + ;; + cli) + bun test || echo "No tests available" + ;; + flutter) + flutter test || echo "No tests available" + ;; + apple|swift) + swift test || echo "No tests available" + ;; + android) + ./gradlew test || echo "No tests available" + ;; + kotlin) + ./gradlew test || echo "No tests available" + ;; + php) + vendor/bin/phpunit || echo "No tests available" + ;; + python) + python -m pytest || echo "No tests available" + ;; + ruby) + bundle exec rake test || bundle exec rspec || echo "No tests available" + ;; + dart) + dart test || echo "No tests available" + ;; + go) + go test ./... || echo "No tests available" + ;; + dotnet) + dotnet test || echo "No tests available" + ;; + *) + echo "No tests for SDK: ${{ matrix.sdk }}" + ;; + esac diff --git a/src/SDK/Language/DotNet.php b/src/SDK/Language/DotNet.php index 1a3c64079e..a1d20f28b6 100644 --- a/src/SDK/Language/DotNet.php +++ b/src/SDK/Language/DotNet.php @@ -496,6 +496,85 @@ public function getFiles(): array 'scope' => 'default', 'destination' => '{{ spec.title | caseUcfirst }}/Enums/IEnum.cs', 'template' => 'dotnet/Package/Enums/IEnum.cs.twig', + ], + // Tests + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/{{ spec.title | caseUcfirst }}.Tests.csproj', + 'template' => 'dotnet/Package.Tests/Tests.csproj.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/.gitignore', + 'template' => 'dotnet/Package.Tests/.gitignore', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/ClientTests.cs', + 'template' => 'dotnet/Package.Tests/ClientTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/IDTests.cs', + 'template' => 'dotnet/Package.Tests/IDTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/PermissionTests.cs', + 'template' => 'dotnet/Package.Tests/PermissionTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/RoleTests.cs', + 'template' => 'dotnet/Package.Tests/RoleTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/QueryTests.cs', + 'template' => 'dotnet/Package.Tests/QueryTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/ExceptionTests.cs', + 'template' => 'dotnet/Package.Tests/ExceptionTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/UploadProgressTests.cs', + 'template' => 'dotnet/Package.Tests/UploadProgressTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/Models/InputFileTests.cs', + 'template' => 'dotnet/Package.Tests/Models/InputFileTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/Converters/ObjectToInferredTypesConverterTests.cs', + 'template' => 'dotnet/Package.Tests/Converters/ObjectToInferredTypesConverterTests.cs.twig', + ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/Converters/ValueClassConverterTests.cs', + 'template' => 'dotnet/Package.Tests/Converters/ValueClassConverterTests.cs.twig', + ], + // Tests for each definition (model) + [ + 'scope' => 'definition', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/Models/{{ definition.name | caseUcfirst | overrideIdentifier }}Tests.cs', + 'template' => 'dotnet/Package.Tests/Models/ModelTests.cs.twig', + ], + // Tests for each enum + [ + 'scope' => 'enum', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/Enums/{{ enum.name | caseUcfirst | overrideIdentifier }}Tests.cs', + 'template' => 'dotnet/Package.Tests/Enums/EnumTests.cs.twig', + ], + // Tests for each service + [ + 'scope' => 'service', + 'destination' => '{{ spec.title | caseUcfirst }}.Tests/Services/{{service.name | caseUcfirst}}Tests.cs', + 'template' => 'dotnet/Package.Tests/Services/ServiceTests.cs.twig', ] ]; } @@ -519,9 +598,21 @@ public function getFilters(): array } return $property; }), + new TwigFilter('escapeCsString', function ($value) { + if (is_string($value)) { + return addcslashes($value, '\\"'); + } + return $value; + }), new TwigFilter('propertyType', function (array $property, array $spec = []) { return $this->getPropertyType($property, $spec); }), + new TwigFilter('propertyAssignment', function (array $property) { + return $this->getPropertyAssignment($property); + }), + new TwigFilter('toMapValue', function (array $property, string $definitionName) { + return $this->getToMapExpression($property, $definitionName); + }), new TwigFilter('enumExample', function (array $param) { $enumValues = $param['enumValues'] ?? []; if (empty($enumValues)) { @@ -530,6 +621,8 @@ public function getFilters(): array $enumKeys = $param['enumKeys'] ?? []; $enumName = $this->toPascalCase($param['enumName'] ?? $param['name'] ?? ''); + $enumPrefix = 'Appwrite.Enums.'; + $enumFullName = $enumPrefix . $enumName; $example = $param['example'] ?? null; $isArray = ($param['type'] ?? '') === self::TYPE_ARRAY; @@ -568,7 +661,7 @@ public function getFilters(): array } $value = ($example !== null && $example !== '') ? $example : $enumValues[0]; - return $enumName . '.' . $resolveKey($value); + return $enumFullName . '.' . $resolveKey($value); }), ]; } @@ -580,7 +673,7 @@ public function getFilters(): array * @param array $spec * @return string */ - protected function getPropertyType(array $property, array $spec = []): string + protected function getPropertyType(array $property, array $spec = [], bool $fullyQualified = true): string { if (isset($property['sub_schema']) && !empty($property['sub_schema'])) { $type = $this->toPascalCase($property['sub_schema']); @@ -593,7 +686,8 @@ protected function getPropertyType(array $property, array $spec = []): string if (isset($property['enum']) && !empty($property['enum'])) { $enumName = $property['enumName'] ?? $property['name']; - return 'Appwrite.Enums.' . $this->toPascalCase($enumName); + $prefix = $fullyQualified ? 'Appwrite.Enums.' : ''; + return $prefix . $this->toPascalCase($enumName); } return $this->getTypeName($property, $spec); @@ -607,39 +701,195 @@ public function getFunctions(): array { return [ new TwigFunction('sub_schema', function (array $property) { + $result = $this->getPropertyType($property, [], false); + + if (!($property['required'] ?? true)) { + $result .= '?'; + } + + return $result; + }, ['is_safe' => ['html']]), + new TwigFunction('test_item_type', function (array $property) { + // For test templates: returns the item type for arrays without the List<> wrapper $result = ''; if (isset($property['sub_schema']) && !empty($property['sub_schema'])) { - if ($property['type'] === 'array') { - $result = 'List<' . $this->toPascalCase($property['sub_schema']) . '>'; - } else { - $result = $this->toPascalCase($property['sub_schema']); - } + // Model type + $result = $this->toPascalCase($property['sub_schema']); + $result = 'Appwrite.Models.' . $result; } elseif (isset($property['enum']) && !empty($property['enum'])) { + // Enum type $enumName = $property['enumName'] ?? $property['name']; - $result = $this->toPascalCase($enumName); + $result = 'Appwrite.Enums.' . $this->toPascalCase($enumName); + } elseif (isset($property['items']) && isset($property['items']['type'])) { + // Primitive array type (for definitions) + $result = $this->getTypeName($property['items']); + } elseif (isset($property['array']) && isset($property['array']['type'])) { + // Primitive array type (for method parameters) + $result = $this->getTypeName($property['array']); } else { - $result = $this->getTypeName($property); - } - - if (!($property['required'] ?? true)) { - $result .= '?'; + $result = 'object'; } return $result; - }), + }, ['is_safe' => ['html']]), new TwigFunction('property_name', function (array $definition, array $property) { - $name = $property['name']; - $name = \str_replace('$', '', $name); - $name = $this->toPascalCase($name); - if (\in_array($name, $this->getKeywords())) { - $name = '@' . $name; - } - return $name; + return $this->getPropertyName($property); }), ]; } + /** + * Generate property name for C# model + * + * @param array $property + * @return string + */ + protected function getPropertyName(array $property): string + { + $name = $property['name']; + $name = \str_replace('$', '', $name); + $name = $this->toPascalCase($name); + if (\in_array($name, $this->getKeywords())) { + $name = '@' . $name; + } + return $name; + } + + /** + * Resolved property name with overrides applied + * + * @param array $property + * @param string $definitionName + * @return string + */ + protected function getResolvedPropertyName(array $property, string $definitionName): string + { + $name = $this->getPropertyName($property); + $overrides = $this->getPropertyOverrides(); + if (isset($overrides[$definitionName][$name])) { + return $overrides[$definitionName][$name]; + } + return $name; + } + + /** + * Generate full property assignment expression for model deserialization (From method). + * Handles TryGetValue wrapping for optional properties internally. + * + * @param array $property + * @return string + */ + protected function getPropertyAssignment(array $property): string + { + $required = $property['required'] ?? false; + $propertyName = $property['name']; + $mapAccess = "map[\"{$propertyName}\"]"; + + if ($required) { + return $this->convertValue($property, $mapAccess); + } + + $v = 'v' . $this->toPascalCase(\str_replace('$', '', $propertyName)); + $tryGet = "map.TryGetValue(\"{$propertyName}\", out var {$v})"; + + // Sub_schema objects — use pattern matching for type-safe cast + if (!empty($property['sub_schema']) && $property['type'] !== 'array') { + $subSchema = $this->toPascalCase($property['sub_schema']); + return "{$tryGet} && {$v} is Dictionary {$v}Map ? {$subSchema}.From(map: {$v}Map) : null"; + } + + // Integer, number, enum — guard with null check to avoid Convert/constructor on null + if (\in_array($property['type'], ['integer', 'number']) || !empty($property['enum'])) { + $expr = $this->convertValue($property, $v); + return "{$tryGet} && {$v} != null ? {$expr} : null"; + } + + // String, boolean, arrays — null-safe conversion + $expr = $this->convertValue($property, $v, false); + return "{$tryGet} ? {$expr} : null"; + } + + /** + * Build type conversion expression for a single value. + * + * @param array $property Property definition + * @param string $src Source variable or expression + * @param bool $srcNonNull Whether $src is guaranteed non-null + * @return string + */ + private function convertValue(array $property, string $src, bool $srcNonNull = true): string + { + // Sub_schema (nested objects) + if (!empty($property['sub_schema'])) { + $subSchema = $this->toPascalCase($property['sub_schema']); + if ($property['type'] === 'array') { + return "{$src}.ToEnumerable().Select(it => {$subSchema}.From(map: (Dictionary)it)).ToList()"; + } + return "{$subSchema}.From(map: (Dictionary){$src})"; + } + + // Enum + if (!empty($property['enum'])) { + $enumClass = $this->toPascalCase($property['enumName'] ?? $property['name']); + return "new {$enumClass}({$src}.ToString())"; + } + + // Arrays + if ($property['type'] === 'array') { + $itemsType = $property['items']['type'] ?? 'object'; + $selectExpression = match ($itemsType) { + 'string' => 'x.ToString()', + 'integer' => 'Convert.ToInt64(x)', + 'number' => 'Convert.ToDouble(x)', + 'boolean' => '(bool)x', + default => 'x' + }; + return "{$src}.ToEnumerable().Select(x => {$selectExpression}).ToList()"; + } + + // Integer/Number + if ($property['type'] === 'integer' || $property['type'] === 'number') { + $convertMethod = $property['type'] === 'integer' ? 'Int64' : 'Double'; + return "Convert.To{$convertMethod}({$src})"; + } + + // Boolean + if ($property['type'] === 'boolean') { + return $srcNonNull ? "(bool){$src}" : "(bool?){$src}"; + } + + // String (default) + return $srcNonNull ? "{$src}.ToString()" : "{$src}?.ToString()"; + } + + /** + * Generate ToMap() value expression for a property. + * + * @param array $property + * @param string $definitionName + * @return string + */ + protected function getToMapExpression(array $property, string $definitionName): string + { + $propName = $this->getResolvedPropertyName($property, $definitionName); + $required = $property['required'] ?? true; + $nullOp = $required ? '' : '?'; + + if (!empty($property['sub_schema'])) { + if ($property['type'] === 'array') { + return "{$propName}{$nullOp}.Select(it => it.ToMap()).ToList()"; + } + return "{$propName}{$nullOp}.ToMap()"; + } + + if (!empty($property['enum'])) { + return "{$propName}{$nullOp}.Value"; + } + + return $propName; + } + /** * Format a PHP array as a C# anonymous object */ diff --git a/templates/dotnet/Package.Tests/.gitignore b/templates/dotnet/Package.Tests/.gitignore new file mode 100644 index 0000000000..9eb3c7a5a5 --- /dev/null +++ b/templates/dotnet/Package.Tests/.gitignore @@ -0,0 +1,23 @@ +# Test results +TestResults/ +*.trx +*.coverage +*.coveragexml + +# Coverage reports +coverage/ +coverage.json +coverage.opencover.xml +lcov.info + +# Build outputs +bin/ +obj/ +*.user +*.suo + +# Rider +.idea/ + +# Visual Studio +.vs/ diff --git a/templates/dotnet/Package.Tests/ClientTests.cs.twig b/templates/dotnet/Package.Tests/ClientTests.cs.twig new file mode 100644 index 0000000000..5a5db6272c --- /dev/null +++ b/templates/dotnet/Package.Tests/ClientTests.cs.twig @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; +using {{ spec.title | caseUcfirst }}; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class ClientTests + { + [Fact] + public void Constructor_Default_CreatesClient() + { + var client = new Client(); + + Assert.NotNull(client); + Assert.Equal("{{spec.endpoint}}", client.Endpoint); + Assert.NotNull(client.Config); + } + + [Fact] + public void Constructor_WithCustomEndpoint_SetsEndpoint() + { + var customEndpoint = "https://custom.example.com/v1"; + + var client = new Client(endpoint: customEndpoint); + + Assert.Equal(customEndpoint, client.Endpoint); + } + + [Fact] + public void Constructor_WithSelfSigned_EnablesSelfSigned() + { + var client = new Client(selfSigned: true); + + Assert.NotNull(client); + } + + [Fact] + public void Constructor_WithHttpClient_UsesProvidedClient() + { + var httpClient = new HttpClient(); + + var client = new Client(http: httpClient); + + Assert.NotNull(client); + } + + [Fact] + public void SetEndpoint_UpdatesEndpoint() + { + var client = new Client(); + var newEndpoint = "https://new.example.com/v1"; + + var result = client.SetEndpoint(newEndpoint); + + Assert.Equal(newEndpoint, client.Endpoint); + Assert.Same(client, result); + } + + [Theory] + {%~ for header in spec.global.headers %} + [InlineData("{{header.key}}", "test-{{header.key}}")] + {%~ endfor %} + public void AddHeader_SetsCustomHeader(string key, string value) + { + var client = new Client(); + + var result = client.AddHeader(key, value); + + Assert.Same(client, result); + } + + {%~ for header in spec.global.headers %} + [Fact] + public void Set{{header.key | caseUcfirst}}_UpdatesConfig() + { + var client = new Client(); + var value = "test-{{header.key}}-value"; + + var result = client.Set{{header.key | caseUcfirst}}(value); + + Assert.True(client.Config.ContainsKey("{{ header.key | caseCamel }}")); + Assert.Equal(value, client.Config["{{ header.key | caseCamel }}"]); + Assert.Same(client, result); + } + + {%~ endfor %} + [Fact] + public void SetSelfSigned_EnablesSelfSignedCertificates() + { + var client = new Client(); + + var result = client.SetSelfSigned(true); + + Assert.NotNull(result); + Assert.Same(client, result); + } + + [Fact] + public void SetSelfSigned_DisablesSelfSignedCertificates() + { + var client = new Client(selfSigned: true); + + var result = client.SetSelfSigned(false); + + Assert.NotNull(result); + Assert.Same(client, result); + } + + [Fact] + public void DeserializerOptions_HasConverters() + { + Assert.NotNull(Client.DeserializerOptions); + Assert.NotEmpty(Client.DeserializerOptions.Converters); + } + + [Fact] + public void SerializerOptions_HasConverters() + { + Assert.NotNull(Client.SerializerOptions); + Assert.NotEmpty(Client.SerializerOptions.Converters); + } + + [Fact] + public void ChainedCalls_Work() + { + var client = new Client() + .SetEndpoint("https://example.com/v1") + {%~ for header in spec.global.headers %} + .Set{{header.key | caseUcfirst}}("test-{{header.key}}") + {%~ endfor %} + .SetSelfSigned(false); + + Assert.NotNull(client); + Assert.Equal("https://example.com/v1", client.Endpoint); + {%~ for header in spec.global.headers %} + Assert.Equal("test-{{header.key}}", client.Config["{{ header.key | caseCamel }}"]); + {%~ endfor %} + } + } +} diff --git a/templates/dotnet/Package.Tests/Converters/ObjectToInferredTypesConverterTests.cs.twig b/templates/dotnet/Package.Tests/Converters/ObjectToInferredTypesConverterTests.cs.twig new file mode 100644 index 0000000000..cc0b1f2231 --- /dev/null +++ b/templates/dotnet/Package.Tests/Converters/ObjectToInferredTypesConverterTests.cs.twig @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using Xunit; +using {{ spec.title | caseUcfirst }}.Converters; + +namespace {{ spec.title | caseUcfirst }}.Tests.Converters +{ + public class ObjectToInferredTypesConverterTests + { + private readonly JsonSerializerOptions _options; + + public ObjectToInferredTypesConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new ObjectToInferredTypesConverter()); + } + + [Fact] + public void Read_WithString_ReturnsString() + { + // Arrange + var json = "\"test string\""; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType(result); + Assert.Equal("test string", result); + } + + [Fact] + public void Read_WithInteger_ReturnsLong() + { + // Arrange + var json = "123"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType(result); + Assert.Equal(123L, result); + } + + [Fact] + public void Read_WithDouble_ReturnsDouble() + { + // Arrange + var json = "123.45"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType(result); + Assert.Equal(123.45, result); + } + + [Fact] + public void Read_WithBoolean_ReturnsBoolean() + { + // Arrange + var json = "true"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType(result); + Assert.True((bool)result); + } + + [Fact] + public void Read_WithNull_ReturnsNull() + { + // Arrange + var json = "null"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Read_WithObject_ReturnsDictionary() + { + // Arrange + var json = "{\"key\":\"value\",\"number\":42}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var dict = (Dictionary)result; + Assert.Equal(2, dict.Count); + Assert.Equal("value", dict["key"]); + Assert.Equal(42L, dict["number"]); + } + + [Fact] + public void Read_WithArray_ReturnsList() + { + // Arrange + var json = "[1,2,3,4,5]"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var list = (List)result; + Assert.Equal(5, list.Count); + Assert.Equal(1L, list[0]); + Assert.Equal(5L, list[4]); + } + + [Fact] + public void Read_WithNestedObject_ReturnsNestedDictionary() + { + // Arrange + var json = "{\"outer\":{\"inner\":\"value\"}}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var dict = (Dictionary)result; + Assert.IsType>(dict["outer"]); + var nested = (Dictionary)dict["outer"]; + Assert.Equal("value", nested["inner"]); + } + + [Fact] + public void Read_WithArrayOfObjects_ReturnsListOfDictionaries() + { + // Arrange + var json = "[{\"id\":1},{\"id\":2}]"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var list = (List)result; + Assert.Equal(2, list.Count); + Assert.IsType>(list[0]); + } + + [Fact] + public void Read_WithMixedTypes_ConvertsCorrectly() + { + // Arrange + var json = "{\"string\":\"text\",\"number\":123,\"bool\":true,\"null\":null,\"array\":[1,2,3]}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var dict = (Dictionary)result; + Assert.Equal("text", dict["string"]); + Assert.Equal(123L, dict["number"]); + Assert.True((bool)dict["bool"]); + Assert.Null(dict["null"]); + Assert.IsType>(dict["array"]); + } + + [Fact] + public void Read_WithDateTime_ReturnsDateTime() + { + // Arrange + var json = "\"2023-10-16T12:00:00Z\""; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType(result); + } + + [Fact] + public void Read_WithEmptyObject_ReturnsEmptyDictionary() + { + // Arrange + var json = "{}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var dict = (Dictionary)result; + Assert.Empty(dict); + } + + [Fact] + public void Read_WithEmptyArray_ReturnsEmptyList() + { + // Arrange + var json = "[]"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var list = (List)result; + Assert.Empty(list); + } + + [Fact] + public void Write_WithString_WritesString() + { + // Arrange + var value = "test"; + + // Act + var json = JsonSerializer.Serialize(value, _options); + + // Assert + Assert.Equal("\"test\"", json); + } + + [Fact] + public void Write_WithInteger_WritesInteger() + { + // Arrange + var value = 123; + + // Act + var json = JsonSerializer.Serialize(value, _options); + + // Assert + Assert.Equal("123", json); + } + + [Fact] + public void Write_WithBoolean_WritesBoolean() + { + // Arrange + var value = true; + + // Act + var json = JsonSerializer.Serialize(value, _options); + + // Assert + Assert.Equal("true", json); + } + + [Fact] + public void Write_WithNull_WritesNull() + { + // Arrange + object value = null; + + // Act + var json = JsonSerializer.Serialize(value, _options); + + // Assert + Assert.Equal("null", json); + } + + [Fact] + public void Write_WithDictionary_WritesObject() + { + // Arrange + var value = new Dictionary + { + { "key", "value" }, + { "number", 42 } + }; + + // Act + var json = JsonSerializer.Serialize(value, _options); + + // Assert + Assert.Contains("\"key\"", json); + Assert.Contains("\"value\"", json); + Assert.Contains("\"number\"", json); + Assert.Contains("42", json); + } + + [Fact] + public void RoundTrip_PreservesData() + { + // Arrange + var original = new Dictionary + { + { "string", "test" }, + { "number", 123L }, + { "bool", true }, + { "array", new List { 1L, 2L, 3L } } + }; + + // Act + var json = JsonSerializer.Serialize(original, _options); + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.IsType>(result); + var dict = (Dictionary)result; + Assert.Equal("test", dict["string"]); + Assert.Equal(123L, dict["number"]); + Assert.True((bool)dict["bool"]); + } + } +} diff --git a/templates/dotnet/Package.Tests/Converters/ValueClassConverterTests.cs.twig b/templates/dotnet/Package.Tests/Converters/ValueClassConverterTests.cs.twig new file mode 100644 index 0000000000..4be51bf513 --- /dev/null +++ b/templates/dotnet/Package.Tests/Converters/ValueClassConverterTests.cs.twig @@ -0,0 +1,223 @@ +using System; +using System.Text.Json; +using Xunit; +using {{ spec.title | caseUcfirst }}.Converters; +using {{ spec.title | caseUcfirst }}.Enums; + +namespace {{ spec.title | caseUcfirst }}.Tests.Converters +{ + public class ValueClassConverterTests + { + private readonly JsonSerializerOptions _options; + + public ValueClassConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new ValueClassConverter()); + _options.PropertyNameCaseInsensitive = true; + } + + [Fact] + public void CanConvert_WithIEnumType_ReturnsTrue() + { + // Arrange + var converter = new ValueClassConverter(); + + // Act + var result = converter.CanConvert(typeof(IEnum)); + + // Assert + Assert.True(result); + } + + [Fact] + public void CanConvert_WithStringType_ReturnsFalse() + { + // Arrange + var converter = new ValueClassConverter(); + + // Act + var result = converter.CanConvert(typeof(string)); + + // Assert + Assert.False(result); + } + + [Fact] + public void CanConvert_WithIntType_ReturnsFalse() + { + // Arrange + var converter = new ValueClassConverter(); + + // Act + var result = converter.CanConvert(typeof(int)); + + // Assert + Assert.False(result); + } + + [Fact] + public void Write_WithValidEnum_WritesStringValue() + { + // Arrange + var testEnum = new TestEnum("testValue"); + + // Act + var json = JsonSerializer.Serialize(testEnum, _options); + + // Assert + Assert.Equal("\"testValue\"", json); + } + + [Fact] + public void Write_WithNull_WritesNull() + { + // Arrange + TestEnum testEnum = null; + + // Act + var json = JsonSerializer.Serialize(testEnum, _options); + + // Assert + Assert.Equal("null", json); + } + + [Fact] + public void Write_WithEmptyValue_WritesEmptyString() + { + // Arrange + var testEnum = new TestEnum(""); + + // Act + var json = JsonSerializer.Serialize(testEnum, _options); + + // Assert + Assert.Equal("\"\"", json); + } + + [Fact] + public void Write_WithSpecialCharacters_EscapesCorrectly() + { + // Arrange + var testEnum = new TestEnum("test\"value"); + + // Act + var json = JsonSerializer.Serialize(testEnum, _options); + + // Assert + Assert.Contains("\\u0022", json); + } + + [Fact] + public void Read_WithValidString_CreatesEnum() + { + // Arrange + var json = "\"testValue\""; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.NotNull(result); + Assert.Equal("testValue", result.Value); + } + + [Fact] + public void Read_WithEmptyString_CreatesEnumWithEmptyValue() + { + // Arrange + var json = "\"\""; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.NotNull(result); + Assert.Equal("", result.Value); + } + + [Fact] + public void RoundTrip_PreservesValue() + { + // Arrange + var original = new TestEnum("originalValue"); + + // Act + var json = JsonSerializer.Serialize(original, _options); + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Equal(original.Value, result.Value); + } + + [Fact] + public void RoundTrip_WithMultipleValues_PreservesAllValues() + { + // Arrange + var values = new[] { "value1", "value2", "value3" }; + + foreach (var value in values) + { + var original = new TestEnum(value); + + // Act + var json = JsonSerializer.Serialize(original, _options); + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.Equal(original.Value, result.Value); + } + } + + [Fact] + public void Write_InComplexObject_SerializesCorrectly() + { + // Arrange + var obj = new + { + EnumValue = new TestEnum("test"), + StringValue = "string" + }; + + // Act + var json = JsonSerializer.Serialize(obj, _options); + + // Assert + Assert.Contains("\"test\"", json); + Assert.Contains("\"string\"", json); + } + + [Fact] + public void Read_FromComplexObject_DeserializesCorrectly() + { + // Arrange + var json = "{\"enumValue\":\"testValue\",\"stringValue\":\"string\"}"; + + // Act + var result = JsonSerializer.Deserialize(json, _options); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.EnumValue); + Assert.Equal("testValue", result.EnumValue.Value); + Assert.Equal("string", result.StringValue); + } + + // Test helper classes + private class TestEnum : IEnum + { + public string Value { get; private set; } + + public TestEnum(string value) + { + Value = value; + } + } + + private class ComplexObject + { + public TestEnum EnumValue { get; set; } + public string StringValue { get; set; } + } + } +} diff --git a/templates/dotnet/Package.Tests/Enums/EnumTests.cs.twig b/templates/dotnet/Package.Tests/Enums/EnumTests.cs.twig new file mode 100644 index 0000000000..3505713c9c --- /dev/null +++ b/templates/dotnet/Package.Tests/Enums/EnumTests.cs.twig @@ -0,0 +1,111 @@ +using Xunit; +using System.Linq; +using {{ spec.title | caseUcfirst }}.Enums; + +namespace {{ spec.title | caseUcfirst }}.Tests.Enums +{ + public class {{ enum.name | caseUcfirst | overrideIdentifier }}Tests + { + [Fact] + public void Constructor_WithValue_CreatesInstance() + { + // Arrange & Act + var enumValue = new {{ enum.name | caseUcfirst | overrideIdentifier }}("test"); + + // Assert + Assert.NotNull(enumValue); + Assert.Equal("test", enumValue.Value); + } + + {%~ for value in enum.enum %} + {%~ set key = enum.keys is empty ? value : enum.keys[loop.index0] %} + [Fact] + public void {{ key | caseEnumKey }}_ReturnsCorrectValue() + { + // Act + var enumValue = {{ enum.name | caseUcfirst | overrideIdentifier }}.{{ key | caseEnumKey }}; + + // Assert + Assert.NotNull(enumValue); + Assert.Equal("{{ value }}", enumValue.Value); + } + + {%~ endfor %} + + [Theory] + {%~ for value in enum.enum %} + [InlineData("{{ value }}")] + {%~ endfor %} + public void Value_WithValidString_IsCorrect(string value) + { + // Act + var enumValue = new {{ enum.name | caseUcfirst | overrideIdentifier }}(value); + + // Assert + Assert.Equal(value, enumValue.Value); + } + + [Fact] + public void StaticProperties_AreNotNull() + { + {%~ for value in enum.enum %} + {%~ set key = enum.keys is empty ? value : enum.keys[loop.index0] %} + Assert.NotNull({{ enum.name | caseUcfirst | overrideIdentifier }}.{{ key | caseEnumKey }}); + {%~ endfor %} + } + + [Fact] + public void StaticProperties_HaveUniqueValues() + { + var values = new[] + { + {%~ for value in enum.enum %} + {%~ set key = enum.keys is empty ? value : enum.keys[loop.index0] %} + {{ enum.name | caseUcfirst | overrideIdentifier }}.{{ key | caseEnumKey }}.Value{% if not loop.last %},{% endif %} + + {%~ endfor %} + }; + + Assert.Equal(values.Length, values.Distinct().Count()); + } + + [Fact] + public void Implements_IEnum() + { + // Arrange + {%~ set firstValue = enum.enum[0] %} + {%~ set firstKey = enum.keys is empty ? firstValue : enum.keys[0] %} + var enumValue = {{ enum.name | caseUcfirst | overrideIdentifier }}.{{ firstKey | caseEnumKey }}; + + // Assert + Assert.IsAssignableFrom(enumValue); + } + + [Fact] + public void Value_CanBeSetInConstructor() + { + // Arrange + var customValue = "customValue"; + + // Act + var enumValue = new {{ enum.name | caseUcfirst | overrideIdentifier }}(customValue); + + // Assert + Assert.Equal(customValue, enumValue.Value); + } + + [Fact] + public void ToString_ReturnsValue() + { + // Arrange + {%~ set firstValue = enum.enum[0] %} + {%~ set firstKey = enum.keys is empty ? firstValue : enum.keys[0] %} + var enumValue = {{ enum.name | caseUcfirst | overrideIdentifier }}.{{ firstKey | caseEnumKey }}; + + // Act & Assert + // Value property should return the string value + Assert.NotNull(enumValue.Value); + Assert.IsType(enumValue.Value); + } + } +} diff --git a/templates/dotnet/Package.Tests/ExceptionTests.cs.twig b/templates/dotnet/Package.Tests/ExceptionTests.cs.twig new file mode 100644 index 0000000000..d45e5b311d --- /dev/null +++ b/templates/dotnet/Package.Tests/ExceptionTests.cs.twig @@ -0,0 +1,143 @@ +using System; +using Xunit; +using {{ spec.title | caseUcfirst }}; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class ExceptionTests + { + [Fact] + public void Constructor_Default_CreatesException() + { + var exception = new {{spec.title | caseUcfirst}}Exception(); + + Assert.NotNull(exception); + Assert.NotNull(exception.Message); + Assert.Null(exception.Code); + Assert.Null(exception.Type); + Assert.Null(exception.Response); + } + + [Fact] + public void Constructor_WithMessage_SetsMessage() + { + var message = "Some error message"; + var exception = new {{spec.title | caseUcfirst}}Exception(message); + + Assert.NotNull(exception); + Assert.Equal(message, exception.Message); + Assert.Null(exception.Code); + Assert.Null(exception.Type); + Assert.Null(exception.Response); + } + + [Fact] + public void Constructor_WithAllParameters_SetsAllProperties() + { + var message = "Invalid request"; + var code = 400; + var type = "ValidationError"; + var response = "{\"error\":\"validation failed\"}"; + + var exception = new {{spec.title | caseUcfirst}}Exception(message, code, type, response); + + Assert.NotNull(exception); + Assert.Equal(message, exception.Message); + Assert.Equal(code, exception.Code); + Assert.Equal(type, exception.Type); + Assert.Equal(response, exception.Response); + } + + [Fact] + public void Constructor_WithMessageAndCode_SetsCorrectly() + { + var message = "Not found"; + var code = 404; + + var exception = new {{spec.title | caseUcfirst}}Exception(message, code); + + Assert.NotNull(exception); + Assert.Equal(message, exception.Message); + Assert.Equal(code, exception.Code); + Assert.Null(exception.Type); + Assert.Null(exception.Response); + } + + [Fact] + public void Constructor_WithInnerException_SetsInnerException() + { + var message = "Outer exception"; + var innerException = new Exception("Inner exception"); + + var exception = new {{spec.title | caseUcfirst}}Exception(message, innerException); + + Assert.NotNull(exception); + Assert.Equal(message, exception.Message); + Assert.NotNull(exception.InnerException); + Assert.Equal("Inner exception", exception.InnerException.Message); + } + + [Fact] + public void Exception_CanBeCaught() + { + var caught = false; + + try + { + throw new {{spec.title | caseUcfirst}}Exception("Test exception"); + } + catch ({{spec.title | caseUcfirst}}Exception) + { + caught = true; + } + + Assert.True(caught); + } + + [Fact] + public void Exception_WithCode_ReturnsCorrectCode() + { + var exception = new {{spec.title | caseUcfirst}}Exception("Error", 500, "ServerError"); + + Assert.Equal(500, exception.Code); + } + + [Fact] + public void Exception_WithType_ReturnsCorrectType() + { + var exception = new {{spec.title | caseUcfirst}}Exception("Error", 401, "Unauthorized"); + + Assert.Equal("Unauthorized", exception.Type); + } + + [Fact] + public void Exception_WithResponse_ReturnsCorrectResponse() + { + var response = "{\"message\":\"error\"}"; + var exception = new {{spec.title | caseUcfirst}}Exception("Error", 400, "BadRequest", response); + + Assert.Equal(response, exception.Response); + } + + [Fact] + public void ToString_WithDefaultConstructor_ReturnsCorrectString() + { + var exception = new {{spec.title | caseUcfirst}}Exception(); + var result = exception.ToString(); + + Assert.NotNull(result); + Assert.Contains("{{spec.title | caseUcfirst}}Exception", result); + } + + [Fact] + public void ToString_WithMessage_ReturnsCorrectString() + { + var exception = new {{spec.title | caseUcfirst}}Exception("Some error message"); + var result = exception.ToString(); + + Assert.NotNull(result); + Assert.Contains("{{spec.title | caseUcfirst}}Exception", result); + Assert.Contains("Some error message", result); + } + } +} diff --git a/templates/dotnet/Package.Tests/IDTests.cs.twig b/templates/dotnet/Package.Tests/IDTests.cs.twig new file mode 100644 index 0000000000..23e89258bd --- /dev/null +++ b/templates/dotnet/Package.Tests/IDTests.cs.twig @@ -0,0 +1,58 @@ +using Xunit; +using {{ spec.title | caseUcfirst }}; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class IDTests + { + [Fact] + public void Unique_ReturnsUniqueID() + { + var id = ID.Unique(); + Assert.NotNull(id); + Assert.NotEmpty(id); + Assert.Equal(20, id.Length); + } + + [Fact] + public void Unique_WithCustomPadding_ReturnsCorrectLength() + { + var padding = 10; + var id = ID.Unique(padding); + Assert.NotNull(id); + Assert.NotEmpty(id); + Assert.Equal(13 + padding, id.Length); // 13 is base timestamp length + } + + [Fact] + public void Unique_GeneratesUniqueIDs() + { + var id1 = ID.Unique(); + var id2 = ID.Unique(); + Assert.NotEqual(id1, id2); + } + + [Fact] + public void Custom_ReturnsCustomString() + { + var customId = "custom"; + var result = ID.Custom(customId); + Assert.Equal(customId, result); + } + + [Fact] + public void Custom_WithEmptyString_ReturnsEmptyString() + { + var result = ID.Custom(""); + Assert.Equal("", result); + } + + [Fact] + public void Custom_WithSpecialCharacters_ReturnsExactString() + { + var customId = "test-123_abc@xyz"; + var result = ID.Custom(customId); + Assert.Equal(customId, result); + } + } +} diff --git a/templates/dotnet/Package.Tests/Models/InputFileTests.cs.twig b/templates/dotnet/Package.Tests/Models/InputFileTests.cs.twig new file mode 100644 index 0000000000..61f398d6d5 --- /dev/null +++ b/templates/dotnet/Package.Tests/Models/InputFileTests.cs.twig @@ -0,0 +1,219 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using Xunit; +using {{ spec.title | caseUcfirst }}.Models; + +namespace {{ spec.title | caseUcfirst }}.Tests.Models +{ + public class InputFileTests + { + [Fact] + public void FromPath_WithValidPath_CreatesInputFile() + { + // Arrange + var path = "test.txt"; + + // Act + var inputFile = InputFile.FromPath(path); + + // Assert + Assert.NotNull(inputFile); + Assert.Equal(path, inputFile.Path); + Assert.Equal("test.txt", inputFile.Filename); + Assert.Equal("path", inputFile.SourceType); + Assert.NotNull(inputFile.MimeType); + } + + [Fact] + public void FromPath_ExtractsCorrectFilename() + { + // Arrange + var path = "/some/directory/file.jpg"; + + // Act + var inputFile = InputFile.FromPath(path); + + // Assert + Assert.Equal("file.jpg", inputFile.Filename); + } + + [Fact] + public void FromPath_WithWindowsPath_ExtractsCorrectFilename() + { + // Arrange + var path = @"C:\Users\test\document.pdf"; + + // Act + var inputFile = InputFile.FromPath(path); + + // Assert + string expectedFilename = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "document.pdf" : path; + Assert.Equal(expectedFilename, inputFile.Filename); + } + + [Fact] + public void FromFileInfo_WithValidFileInfo_CreatesInputFile() + { + // Arrange + var tempFile = Path.GetTempFileName(); + var fileInfo = new FileInfo(tempFile); + + try + { + // Act + var inputFile = InputFile.FromFileInfo(fileInfo); + + // Assert + Assert.NotNull(inputFile); + Assert.Equal(fileInfo.FullName, inputFile.Path); + Assert.Equal(fileInfo.Name, inputFile.Filename); + Assert.Equal("path", inputFile.SourceType); + } + finally + { + System.IO.File.Delete(tempFile); + } + } + + [Fact] + public void FromStream_WithValidStream_CreatesInputFile() + { + // Arrange + var stream = new MemoryStream(new byte[] { 1, 2, 3 }); + var filename = "test.bin"; + var mimeType = "application/octet-stream"; + + // Act + var inputFile = InputFile.FromStream(stream, filename, mimeType); + + // Assert + Assert.NotNull(inputFile); + Assert.Equal(stream, inputFile.Data); + Assert.Equal(filename, inputFile.Filename); + Assert.Equal(mimeType, inputFile.MimeType); + Assert.Equal("stream", inputFile.SourceType); + } + + [Fact] + public void FromStream_WithCustomMimeType_SetsCorrectMimeType() + { + // Arrange + var stream = new MemoryStream(); + var customMimeType = "image/png"; + + // Act + var inputFile = InputFile.FromStream(stream, "image.png", customMimeType); + + // Assert + Assert.Equal(customMimeType, inputFile.MimeType); + } + + [Fact] + public void FromBytes_WithValidBytes_CreatesInputFile() + { + // Arrange + var bytes = new byte[] { 1, 2, 3, 4, 5 }; + var filename = "data.bin"; + var mimeType = "application/octet-stream"; + + // Act + var inputFile = InputFile.FromBytes(bytes, filename, mimeType); + + // Assert + Assert.NotNull(inputFile); + Assert.Equal(bytes, inputFile.Data); + Assert.Equal(filename, inputFile.Filename); + Assert.Equal(mimeType, inputFile.MimeType); + Assert.Equal("bytes", inputFile.SourceType); + } + + [Fact] + public void FromBytes_WithEmptyBytes_CreatesInputFile() + { + // Arrange + var bytes = new byte[] { }; + var filename = "empty.bin"; + var mimeType = "application/octet-stream"; + + // Act + var inputFile = InputFile.FromBytes(bytes, filename, mimeType); + + // Assert + Assert.NotNull(inputFile); + Assert.Equal(bytes, inputFile.Data); + Assert.Equal(filename, inputFile.Filename); + } + + [Fact] + public void FromBytes_WithImageData_SetsCorrectMimeType() + { + // Arrange + var bytes = new byte[] { 137, 80, 78, 71 }; // PNG header + var mimeType = "image/png"; + + // Act + var inputFile = InputFile.FromBytes(bytes, "image.png", mimeType); + + // Assert + Assert.Equal(mimeType, inputFile.MimeType); + } + + [Fact] + public void SourceType_Path_IsCorrect() + { + var inputFile = InputFile.FromPath("test.txt"); + Assert.Equal("path", inputFile.SourceType); + } + + [Fact] + public void SourceType_Stream_IsCorrect() + { + var inputFile = InputFile.FromStream(new MemoryStream(), "test.txt", "text/plain"); + Assert.Equal("stream", inputFile.SourceType); + } + + [Fact] + public void SourceType_Bytes_IsCorrect() + { + var inputFile = InputFile.FromBytes(new byte[] { 1 }, "test.bin", "application/octet-stream"); + Assert.Equal("bytes", inputFile.SourceType); + } + + [Fact] + public void Properties_CanBeSet() + { + // Arrange & Act + var inputFile = new InputFile + { + Path = "custom/path.txt", + Filename = "custom.txt", + MimeType = "text/plain", + SourceType = "custom", + Data = new object() + }; + + // Assert + Assert.Equal("custom/path.txt", inputFile.Path); + Assert.Equal("custom.txt", inputFile.Filename); + Assert.Equal("text/plain", inputFile.MimeType); + Assert.Equal("custom", inputFile.SourceType); + Assert.NotNull(inputFile.Data); + } + + [Fact] + public void DefaultConstructor_InitializesProperties() + { + // Act + var inputFile = new InputFile(); + + // Assert + Assert.NotNull(inputFile); + Assert.NotNull(inputFile.Path); + Assert.NotNull(inputFile.Filename); + Assert.NotNull(inputFile.MimeType); + Assert.NotNull(inputFile.SourceType); + Assert.NotNull(inputFile.Data); + } + } +} diff --git a/templates/dotnet/Package.Tests/Models/ModelTests.cs.twig b/templates/dotnet/Package.Tests/Models/ModelTests.cs.twig new file mode 100644 index 0000000000..a578aa2767 --- /dev/null +++ b/templates/dotnet/Package.Tests/Models/ModelTests.cs.twig @@ -0,0 +1,323 @@ +{% set DefinitionClass = definition.name | caseUcfirst | overrideIdentifier %} +{% macro generate_sub_dict(sub_def) %} +new Dictionary { +{% for subprop in sub_def.properties | filter(p => p.required) %} +{ "{{ subprop.name }}", {% if subprop.enum %}{{ subprop.enumName | caseUcfirst }}.{{ (subprop.enumKeys[0] ?? subprop.enum[0]) | caseEnumKey }}.Value{% elseif subprop.type == 'string' %}"{{ subprop['x-example'] | default('test') | escapeCsString }}"{% elseif subprop.type == 'integer' %}{{ subprop['x-example'] | default(1) }}{% elseif subprop.type == 'number' %}{{ subprop['x-example'] | default(1.0) }}{% elseif subprop.type == 'boolean' %}{% if subprop['x-example'] is defined %}{% if subprop['x-example'] is same as(true) or subprop['x-example'] == 'true' or subprop['x-example'] == 1 %}true{% else %}false{% endif %}{% else %}true{% endif %}{% elseif subprop.sub_schema %}new Dictionary(){% else %}"{{ subprop['x-example'] | default('test') | escapeCsString }}"{% endif %} }{% if not loop.last %},{% endif %} +{% endfor %} +} +{% endmacro %} +using System; +using System.Collections.Generic; +using Xunit; +using {{ spec.title | caseUcfirst }}.Models; +{% if definition.properties | filter(p => p.enum) | length > 0 %} +using {{ spec.title | caseUcfirst }}.Enums; +{% endif %} + +namespace {{ spec.title | caseUcfirst }}.Tests.Models +{ + public class {{ DefinitionClass }}Tests + { + [Fact] + public void Constructor_WithValidParameters_CreatesInstance() + { + // Arrange & Act + var model = new {{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}( + {%~ for property in definition.properties %} + {%~ if property.enum %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }} + {%~ elseif property.type == 'string' %} + {{ property.name | caseCamel | escapeKeyword }}: "{{ property['x-example'] | default('test') | escapeCsString }}" + {%~ elseif property.type == 'boolean' %} + {%~ if property['x-example'] is defined %} + {{ property.name | caseCamel | escapeKeyword }}: {% if property['x-example'] is same as(true) or property['x-example'] == 'true' or property['x-example'] == 1 %}true{% else %}false{% endif %} + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: true + {%~ endif %} + {%~ elseif property.type == 'integer' %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default(1) }} + {%~ elseif property.type == 'number' %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default(1.0) }} + {%~ elseif property.type == 'array' %} + {%~ set itemType = test_item_type(property) %} + {{ property.name | caseCamel | escapeKeyword }}: new List<{{ itemType }}>() + {%~ elseif property.type == 'object' and not property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: new Dictionary() + {%~ elseif property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.sub_schema | caseUcfirst }}.From({{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }}) + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default('null') | escapeCsString }} + {%~ endif -%} + {%~ if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} + {%~ if definition.additionalProperties %} + data: new Dictionary() + {%~ endif %} + ); + + // Assert + Assert.NotNull(model); + {%~ for property in definition.properties %} + {%~ if property.enum %} + Assert.Equal({{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }}.Value, model.{{ property_name(definition, property) | overrideProperty(definition.name) }}.Value); + {%~ elseif property.type == 'string' %} + Assert.Equal("{{ property['x-example'] | default('test') | escapeCsString }}", model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ elseif property.type == 'boolean' %} + {%~ if property['x-example'] is defined %} + Assert.{% if property['x-example'] is same as(true) or property['x-example'] == 'true' or property['x-example'] == 1 %}True{% else %}False{% endif %}(model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ else %} + Assert.True(model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ endif %} + {%~ elseif property.type == 'integer' or property.type == 'number' %} + Assert.Equal({{ property['x-example'] | default(1) }}, model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ elseif property.type == 'array' or (property.type == 'object' and not property.sub_schema) %} + Assert.NotNull(model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ endif %} + {%~ endfor %} + } + + [Fact] + public void ToMap_ReturnsCorrectDictionary() + { + // Arrange + var model = new {{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}( + {%~ for property in definition.properties %} + {%~ if property.enum %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }} + {%~ elseif property.type == 'string' %} + {{ property.name | caseCamel | escapeKeyword }}: "{{ property['x-example'] | default('test') | escapeCsString }}" + {%~ elseif property.type == 'integer' %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default(1) }} + {%~ elseif property.type == 'number' %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default(1.0) }} + {%~ elseif property.type == 'boolean' %} + {%~ if property['x-example'] is defined %} + {{ property.name | caseCamel | escapeKeyword }}: {% if property['x-example'] is same as(true) or property['x-example'] == 'true' or property['x-example'] == 1 %}true{% else %}false{% endif %} + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: true + {%~ endif %} + {%~ elseif property.type == 'array' %} + {%~ set itemType = test_item_type(property) %} + {{ property.name | caseCamel | escapeKeyword }}: new List<{{ itemType }}>() + {%~ elseif property.type == 'object' and not property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: new Dictionary() + {%~ elseif property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.sub_schema | caseUcfirst }}.From({{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }}) + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default('null') }} + {%~ endif %} + {%~ if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} + {%~ if definition.additionalProperties %} + data: new Dictionary() + {%~ endif %} + ); + + // Act + var map = model.ToMap(); + + // Assert + Assert.NotNull(map); + {%~ for property in definition.properties %} + Assert.True(map.ContainsKey("{{ property.name }}")); + {%~ endfor %} + } + + [Fact] + public void From_WithValidMap_CreatesInstance() + { + // Arrange + var map = new Dictionary + { + {%~ for property in definition.properties | filter(p => p.required) %} + {%~ if property.enum %} + { "{{ property.name }}", {{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }}.Value } + {%~ elseif property.type == 'string' %} + { "{{ property.name }}", "{{ property['x-example'] | default('test') | escapeCsString }}" } + {%~ elseif property.type == 'integer' %} + { "{{ property.name }}", {{ property['x-example'] | default(1) }} } + {%~ elseif property.type == 'number' %} + { "{{ property.name }}", {{ property['x-example'] | default(1.0) }} } + {%~ elseif property.type == 'boolean' %} + {%~ if property['x-example'] is defined %} + { "{{ property.name }}", {% if property['x-example'] is same as(true) or property['x-example'] == 'true' or property['x-example'] == 1 %}true{% else %}false{% endif %} } + {%~ else %} + { "{{ property.name }}", true } + {%~ endif %} + {%~ elseif property.type == 'array' %} + { "{{ property.name }}", new List() } + {%~ elseif property.type == 'object' and not property.sub_schema %} + { "{{ property.name }}", new Dictionary() } + {%~ elseif property.sub_schema %} + { "{{ property.name }}", {{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }} } + {%~ else %} + { "{{ property.name }}", "{{ property['x-example'] | default('test') | escapeCsString }}" } + {%~ endif %} + {%~ if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} + }; + + // Act + var model = {{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}.From(map); + + // Assert + Assert.NotNull(model); + {%~ for property in definition.properties | filter(p => p.required) %} + {%~ if property.enum %} + Assert.Equal({{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }}.Value, model.{{ property_name(definition, property) | overrideProperty(definition.name) }}.Value); + {%~ elseif property.type == 'string' %} + Assert.Equal("{{ property['x-example'] | default('test') | escapeCsString }}", model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ elseif property.type == 'integer' or property.type == 'number' %} + Assert.Equal({{ property['x-example'] | default(1) }}, model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ elseif property.type == 'boolean' %} + {%~ if property['x-example'] is defined %} + Assert.{% if property['x-example'] is same as(true) or property['x-example'] == 'true' or property['x-example'] == 1 %}True{% else %}False{% endif %}(model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ else %} + Assert.True(model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ endif %} + {%~ elseif property.type == 'array' or property.type == 'object' or property.sub_schema %} + Assert.NotNull(model.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ endif %} + {%~ endfor %} + } + + [Fact] + public void ToMap_AndFrom_RoundTrip_PreservesData() + { + // Arrange + var original = new {{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}( + {%~ for property in definition.properties %} + {%~ if property.enum %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }} + {%~ elseif property.type == 'string' %} + {{ property.name | caseCamel | escapeKeyword }}: "{{ property['x-example'] | default('test') | escapeCsString }}" + {%~ elseif property.type == 'integer' %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default(1) }} + {%~ elseif property.type == 'number' %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default(1.0) }} + {%~ elseif property.type == 'boolean' %} + {%~ if property['x-example'] is defined %} + {{ property.name | caseCamel | escapeKeyword }}: {% if property['x-example'] is same as(true) or property['x-example'] == 'true' or property['x-example'] == 1 %}true{% else %}false{% endif %} + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: true + {%~ endif %} + {%~ elseif property.type == 'array' %} + {%~ set itemType = test_item_type(property) %} + {{ property.name | caseCamel | escapeKeyword }}: new List<{{ itemType }}>() + {%~ elseif property.type == 'object' and not property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: new Dictionary() + {%~ elseif property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.sub_schema | caseUcfirst }}.From({{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }}) + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property['x-example'] | default('null') }} + {%~ endif %} + {%~ if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} + {%~ if definition.additionalProperties %} + data: new Dictionary() + {%~ endif %} + ); + + // Act + var map = original.ToMap(); + var result = {{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}.From(map); + + // Assert + {%~ for property in definition.properties | filter(p => p.required) %} + {%~ if property.enum %} + Assert.Equal(original.{{ property_name(definition, property) | overrideProperty(definition.name) }}.Value, result.{{ property_name(definition, property) | overrideProperty(definition.name) }}.Value); + {%~ elseif property.type == 'string' or property.type == 'integer' or property.type == 'number' or property.type == 'boolean' %} + Assert.Equal(original.{{ property_name(definition, property) | overrideProperty(definition.name) }}, result.{{ property_name(definition, property) | overrideProperty(definition.name) }}); + {%~ endif %} + {%~ endfor %} + } + {%~ if definition.additionalProperties %} + + [Fact] + public void ConvertTo_WithValidFunction_ConvertsCorrectly() + { + // Arrange + var data = new Dictionary + { + { "customKey", "customValue" } + }; + var model = new {{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}( + {%~ for property in definition.properties %} + {%~ if property.type == 'string' %} + {{ property.name | caseCamel | escapeKeyword }}: "test" + {%~ elseif property.type == 'integer' %} + {{ property.name | caseCamel | escapeKeyword }}: 1 + {%~ elseif property.type == 'number' %} + {{ property.name | caseCamel | escapeKeyword }}: 1.0 + {%~ elseif property.type == 'boolean' %} + {{ property.name | caseCamel | escapeKeyword }}: true + {%~ elseif property.type == 'array' %} + {%~ set itemType = test_item_type(property) %} + {{ property.name | caseCamel | escapeKeyword }}: new List<{{ itemType }}>() + {%~ elseif property.type == 'object' and not property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: new Dictionary() + {%~ elseif property.enum %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }} + {%~ elseif property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.sub_schema | caseUcfirst }}.From({{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }}) + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: null + {%~ endif %} + {%~ if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} + {%~ if definition.additionalProperties %} + data: data + {%~ endif %} + ); + + // Act + var result = model.ConvertTo(d => d["customKey"].ToString()); + + // Assert + Assert.Equal("customValue", result); + } + {%~ endif %} + + [Fact] + public void Properties_AreReadOnly() + { + // Arrange + var model = new {{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}( + {%~ for property in definition.properties %} + {%~ if property.enum %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }} + {%~ elseif property.type == 'string' %} + {{ property.name | caseCamel | escapeKeyword }}: "test" + {%~ elseif property.type == 'integer' %} + {{ property.name | caseCamel | escapeKeyword }}: 1 + {%~ elseif property.type == 'number' %} + {{ property.name | caseCamel | escapeKeyword }}: 1.0 + {%~ elseif property.type == 'boolean' %} + {{ property.name | caseCamel | escapeKeyword }}: true + {%~ elseif property.type == 'array' %} + {%~ set itemType = test_item_type(property) %} + {{ property.name | caseCamel | escapeKeyword }}: new List<{{ itemType }}>() + {%~ elseif property.type == 'object' and not property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: new Dictionary() + {%~ elseif property.sub_schema %} + {{ property.name | caseCamel | escapeKeyword }}: {{ property.sub_schema | caseUcfirst }}.From({{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }}) + {%~ else %} + {{ property.name | caseCamel | escapeKeyword }}: null + {%~ endif %} + {%~ if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} + {%~ if definition.additionalProperties %} + data: new Dictionary() + {%~ endif %} + ); + + // Assert - properties should have private setters + {%~ for property in definition.properties | slice(0, 1) %} + var propertyInfo = typeof({{ spec.title | caseUcfirst }}.Models.{{ DefinitionClass }}).GetProperty("{{ property_name(definition, property) | overrideProperty(definition.name) }}"); + Assert.NotNull(propertyInfo); + Assert.Null(propertyInfo.GetSetMethod()); + {%~ endfor %} + } + } +} + diff --git a/templates/dotnet/Package.Tests/OperatorTests.cs.twig b/templates/dotnet/Package.Tests/OperatorTests.cs.twig new file mode 100644 index 0000000000..5533db0c56 --- /dev/null +++ b/templates/dotnet/Package.Tests/OperatorTests.cs.twig @@ -0,0 +1,414 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using Xunit; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class OperatorTests + { + [Fact] + public void Increment_ReturnsCorrectOperator() + { + var result = Operator.Increment(1); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("increment", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(1, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void Increment_WithMax_ReturnsCorrectOperator() + { + var result = Operator.Increment(5, 100); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("increment", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal(5, ((JsonElement)op.Values[0]).GetInt32()); + Assert.Equal(100, ((JsonElement)op.Values[1]).GetInt32()); + } + + [Fact] + public void Decrement_ReturnsCorrectOperator() + { + var result = Operator.Decrement(1); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("decrement", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(1, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void Decrement_WithMin_ReturnsCorrectOperator() + { + var result = Operator.Decrement(3, 0); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("decrement", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal(3, ((JsonElement)op.Values[0]).GetInt32()); + Assert.Equal(0, ((JsonElement)op.Values[1]).GetInt32()); + } + + [Fact] + public void Multiply_ReturnsCorrectOperator() + { + var result = Operator.Multiply(2); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("multiply", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(2, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void Multiply_WithMax_ReturnsCorrectOperator() + { + var result = Operator.Multiply(3, 1000); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("multiply", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal(3, ((JsonElement)op.Values[0]).GetInt32()); + Assert.Equal(1000, ((JsonElement)op.Values[1]).GetInt32()); + } + + [Fact] + public void Divide_ReturnsCorrectOperator() + { + var result = Operator.Divide(2); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("divide", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(2, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void Divide_WithMin_ReturnsCorrectOperator() + { + var result = Operator.Divide(4, 1); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("divide", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal(4, ((JsonElement)op.Values[0]).GetInt32()); + Assert.Equal(1, ((JsonElement)op.Values[1]).GetInt32()); + } + + [Fact] + public void Modulo_ReturnsCorrectOperator() + { + var result = Operator.Modulo(5); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("modulo", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(5, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void Power_ReturnsCorrectOperator() + { + var result = Operator.Power(2); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("power", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(2, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void Power_WithMax_ReturnsCorrectOperator() + { + var result = Operator.Power(3, 100); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("power", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal(3, ((JsonElement)op.Values[0]).GetInt32()); + Assert.Equal(100, ((JsonElement)op.Values[1]).GetInt32()); + } + + [Fact] + public void ArrayAppend_ReturnsCorrectOperator() + { + var result = Operator.ArrayAppend(new List() { "item1", "item2" }); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayAppend", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + } + + [Fact] + public void ArrayPrepend_ReturnsCorrectOperator() + { + var result = Operator.ArrayPrepend(new List() { "first", "second" }); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayPrepend", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + } + + [Fact] + public void ArrayInsert_ReturnsCorrectOperator() + { + var result = Operator.ArrayInsert(0, "newItem"); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayInsert", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal(0, ((JsonElement)op.Values[0]).GetInt32()); + Assert.Equal("newItem", op.Values[1].ToString()); + } + + [Fact] + public void ArrayRemove_ReturnsCorrectOperator() + { + var result = Operator.ArrayRemove("oldItem"); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayRemove", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal("oldItem", op.Values[0].ToString()); + } + + [Fact] + public void ArrayUnique_ReturnsCorrectOperator() + { + var result = Operator.ArrayUnique(); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayUnique", op.Method); + Assert.NotNull(op.Values); + Assert.Empty(op.Values); + } + + [Fact] + public void ArrayIntersect_ReturnsCorrectOperator() + { + var result = Operator.ArrayIntersect(new List() { "a", "b", "c" }); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayIntersect", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(3, op.Values.Count); + } + + [Fact] + public void ArrayDiff_ReturnsCorrectOperator() + { + var result = Operator.ArrayDiff(new List() { "x", "y" }); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayDiff", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + } + + [Fact] + public void ArrayFilter_ReturnsCorrectOperator() + { + var result = Operator.ArrayFilter(Condition.Equal, "test"); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("arrayFilter", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal("equal", op.Values[0].ToString()); + Assert.Equal("test", op.Values[1].ToString()); + } + + [Fact] + public void StringConcat_ReturnsCorrectOperator() + { + var result = Operator.StringConcat("suffix"); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("stringConcat", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal("suffix", op.Values[0].ToString()); + } + + [Fact] + public void StringReplace_ReturnsCorrectOperator() + { + var result = Operator.StringReplace("old", "new"); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("stringReplace", op.Method); + Assert.NotNull(op.Values); + Assert.Equal(2, op.Values.Count); + Assert.Equal("old", op.Values[0].ToString()); + Assert.Equal("new", op.Values[1].ToString()); + } + + [Fact] + public void Toggle_ReturnsCorrectOperator() + { + var result = Operator.Toggle(); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("toggle", op.Method); + Assert.NotNull(op.Values); + Assert.Empty(op.Values); + } + + [Fact] + public void DateAddDays_ReturnsCorrectOperator() + { + var result = Operator.DateAddDays(7); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("dateAddDays", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(7, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void DateSubDays_ReturnsCorrectOperator() + { + var result = Operator.DateSubDays(3); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("dateSubDays", op.Method); + Assert.NotNull(op.Values); + Assert.Single(op.Values); + Assert.Equal(3, ((JsonElement)op.Values[0]).GetInt32()); + } + + [Fact] + public void DateSetNow_ReturnsCorrectOperator() + { + var result = Operator.DateSetNow(); + var op = JsonSerializer.Deserialize(result); + + Assert.NotNull(op); + Assert.Equal("dateSetNow", op.Method); + Assert.NotNull(op.Values); + Assert.Empty(op.Values); + } + + [Fact] + public void Increment_WithNaN_ThrowsArgumentException() + { + Assert.Throws(() => Operator.Increment(double.NaN)); + } + + [Fact] + public void Increment_WithInfinity_ThrowsArgumentException() + { + Assert.Throws(() => Operator.Increment(double.PositiveInfinity)); + } + + [Fact] + public void Decrement_WithNaN_ThrowsArgumentException() + { + Assert.Throws(() => Operator.Decrement(double.NaN)); + } + + [Fact] + public void Multiply_WithNaN_ThrowsArgumentException() + { + Assert.Throws(() => Operator.Multiply(double.NaN)); + } + + [Fact] + public void Divide_WithZero_ThrowsArgumentException() + { + Assert.Throws(() => Operator.Divide(0)); + } + + [Fact] + public void Divide_WithNaN_ThrowsArgumentException() + { + Assert.Throws(() => Operator.Divide(double.NaN)); + } + + [Fact] + public void Modulo_WithZero_ThrowsArgumentException() + { + Assert.Throws(() => Operator.Modulo(0)); + } + + [Fact] + public void Modulo_WithNaN_ThrowsArgumentException() + { + Assert.Throws(() => Operator.Modulo(double.NaN)); + } + + [Fact] + public void Power_WithNaN_ThrowsArgumentException() + { + Assert.Throws(() => Operator.Power(double.NaN)); + } + + [Fact] + public void ArrayAppend_WithNull_ThrowsArgumentNullException() + { + Assert.Throws(() => Operator.ArrayAppend(null)); + } + + [Fact] + public void ArrayPrepend_WithNull_ThrowsArgumentNullException() + { + Assert.Throws(() => Operator.ArrayPrepend(null)); + } + + [Fact] + public void ArrayIntersect_WithNull_ThrowsArgumentNullException() + { + Assert.Throws(() => Operator.ArrayIntersect(null)); + } + + [Fact] + public void ArrayDiff_WithNull_ThrowsArgumentNullException() + { + Assert.Throws(() => Operator.ArrayDiff(null)); + } + } +} diff --git a/templates/dotnet/Package.Tests/PermissionTests.cs.twig b/templates/dotnet/Package.Tests/PermissionTests.cs.twig new file mode 100644 index 0000000000..76fe74be22 --- /dev/null +++ b/templates/dotnet/Package.Tests/PermissionTests.cs.twig @@ -0,0 +1,38 @@ +using Xunit; +using {{ spec.title | caseUcfirst }}; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class PermissionTests + { + [Fact] + public void Read_ReturnsCorrectPermission() + { + Assert.Equal("read(\"any\")", Permission.Read(Role.Any())); + } + + [Fact] + public void Write_ReturnsCorrectPermission() + { + Assert.Equal("write(\"any\")", Permission.Write(Role.Any())); + } + + [Fact] + public void Create_ReturnsCorrectPermission() + { + Assert.Equal("create(\"any\")", Permission.Create(Role.Any())); + } + + [Fact] + public void Update_ReturnsCorrectPermission() + { + Assert.Equal("update(\"any\")", Permission.Update(Role.Any())); + } + + [Fact] + public void Delete_ReturnsCorrectPermission() + { + Assert.Equal("delete(\"any\")", Permission.Delete(Role.Any())); + } + } +} diff --git a/templates/dotnet/Package.Tests/QueryTests.cs.twig b/templates/dotnet/Package.Tests/QueryTests.cs.twig new file mode 100644 index 0000000000..ab981d0e69 --- /dev/null +++ b/templates/dotnet/Package.Tests/QueryTests.cs.twig @@ -0,0 +1,837 @@ +using System.Collections.Generic; +using System.Text.Json; +using Xunit; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class QueryTests + { + [Fact] + public void Equal_WithString_ReturnsCorrectQuery() + { + var result = Query.Equal("attr", "value"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("equal", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("value", query.Values[0].ToString()); + } + + [Fact] + public void Equal_WithInteger_ReturnsCorrectQuery() + { + var result = Query.Equal("attr", 1); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("equal", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal(1, ((JsonElement)query.Values[0]).GetInt32()); + } + + [Fact] + public void Equal_WithDouble_ReturnsCorrectQuery() + { + var result = Query.Equal("attr", 1.5); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("equal", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal(1.5, ((JsonElement)query.Values[0]).GetDouble()); + } + + [Fact] + public void Equal_WithBoolean_ReturnsCorrectQuery() + { + var result = Query.Equal("attr", true); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("equal", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.True(((JsonElement)query.Values[0]).GetBoolean()); + } + + [Fact] + public void Equal_WithList_ReturnsCorrectQuery() + { + var result = Query.Equal("attr", new[] { "a", "b", "c" }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("equal", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(3, query.Values.Count); + } + + [Fact] + public void NotEqual_WithString_ReturnsCorrectQuery() + { + var result = Query.NotEqual("attr", "value"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notEqual", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void LessThan_WithInteger_ReturnsCorrectQuery() + { + var result = Query.LessThan("attr", 10); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("lessThan", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void LessThanEqual_WithInteger_ReturnsCorrectQuery() + { + var result = Query.LessThanEqual("attr", 10); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("lessThanEqual", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void GreaterThan_WithInteger_ReturnsCorrectQuery() + { + var result = Query.GreaterThan("attr", 5); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("greaterThan", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void GreaterThanEqual_WithInteger_ReturnsCorrectQuery() + { + var result = Query.GreaterThanEqual("attr", 5); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("greaterThanEqual", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void Search_ReturnsCorrectQuery() + { + var result = Query.Search("attr", "keyword1 keyword2"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("search", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("keyword1 keyword2", query.Values[0].ToString()); + } + + [Fact] + public void IsNull_ReturnsCorrectQuery() + { + var result = Query.IsNull("attr"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("isNull", query.Method); + Assert.Equal("attr", query.Attribute); + } + + [Fact] + public void IsNotNull_ReturnsCorrectQuery() + { + var result = Query.IsNotNull("attr"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("isNotNull", query.Method); + Assert.Equal("attr", query.Attribute); + } + + [Fact] + public void Between_WithIntegers_ReturnsCorrectQuery() + { + var result = Query.Between("attr", 1, 10); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("between", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + } + + [Fact] + public void Between_WithDoubles_ReturnsCorrectQuery() + { + var result = Query.Between("attr", 1.5, 10.5); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("between", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + } + + [Fact] + public void Between_WithStrings_ReturnsCorrectQuery() + { + var result = Query.Between("attr", "a", "z"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("between", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + } + + [Fact] + public void StartsWith_ReturnsCorrectQuery() + { + var result = Query.StartsWith("attr", "prefix"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("startsWith", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("prefix", query.Values[0].ToString()); + } + + [Fact] + public void EndsWith_ReturnsCorrectQuery() + { + var result = Query.EndsWith("attr", "suffix"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("endsWith", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("suffix", query.Values[0].ToString()); + } + + [Fact] + public void Select_WithSingleAttribute_ReturnsCorrectQuery() + { + var result = Query.Select(new List() { "attr1" }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("select", query.Method); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + } + + [Fact] + public void Select_WithMultipleAttributes_ReturnsCorrectQuery() + { + var result = Query.Select(new List() { "attr1", "attr2", "attr3" }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("select", query.Method); + Assert.NotNull(query.Values); + Assert.Equal(3, query.Values.Count); + } + + [Fact] + public void OrderAsc_ReturnsCorrectQuery() + { + var result = Query.OrderAsc("attr"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("orderAsc", query.Method); + Assert.Equal("attr", query.Attribute); + } + + [Fact] + public void OrderDesc_ReturnsCorrectQuery() + { + var result = Query.OrderDesc("attr"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("orderDesc", query.Method); + Assert.Equal("attr", query.Attribute); + } + + [Fact] + public void CursorAfter_ReturnsCorrectQuery() + { + var result = Query.CursorAfter("documentId"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("cursorAfter", query.Method); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("documentId", query.Values[0].ToString()); + } + + [Fact] + public void CursorBefore_ReturnsCorrectQuery() + { + var result = Query.CursorBefore("documentId"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("cursorBefore", query.Method); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("documentId", query.Values[0].ToString()); + } + + [Fact] + public void Limit_ReturnsCorrectQuery() + { + var result = Query.Limit(25); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("limit", query.Method); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal(25, ((JsonElement)query.Values[0]).GetInt32()); + } + + [Fact] + public void Offset_ReturnsCorrectQuery() + { + var result = Query.Offset(10); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("offset", query.Method); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal(10, ((JsonElement)query.Values[0]).GetInt32()); + } + + [Fact] + public void Contains_ReturnsCorrectQuery() + { + var result = Query.Contains("attr", "value"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("contains", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("value", query.Values[0].ToString()); + } + + [Fact] + public void Or_WithMultipleQueries_ReturnsCorrectQuery() + { + var result = Query.Or(new List() { + Query.Equal("attr1", "value1"), + Query.Equal("attr2", "value2") + }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("or", query.Method); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + } + + [Fact] + public void And_WithMultipleQueries_ReturnsCorrectQuery() + { + var result = Query.And(new List() { + Query.Equal("attr1", "value1"), + Query.Equal("attr2", "value2") + }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("and", query.Method); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + } + + [Fact] + public void NotContains_ReturnsCorrectQuery() + { + var result = Query.NotContains("attr", "value"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notContains", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("value", query.Values[0].ToString()); + } + + [Fact] + public void NotSearch_ReturnsCorrectQuery() + { + var result = Query.NotSearch("attr", "keyword1 keyword2"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notSearch", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("keyword1 keyword2", query.Values[0].ToString()); + } + + [Fact] + public void NotBetween_WithIntegers_ReturnsCorrectQuery() + { + var result = Query.NotBetween("attr", 1, 2); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notBetween", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + Assert.Equal(1, ((JsonElement)query.Values[0]).GetInt32()); + Assert.Equal(2, ((JsonElement)query.Values[1]).GetInt32()); + } + + [Fact] + public void NotBetween_WithDoubles_ReturnsCorrectQuery() + { + var result = Query.NotBetween("attr", 1.0, 2.0); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notBetween", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + Assert.Equal(1.0, ((JsonElement)query.Values[0]).GetDouble()); + Assert.Equal(2.0, ((JsonElement)query.Values[1]).GetDouble()); + } + + [Fact] + public void NotBetween_WithStrings_ReturnsCorrectQuery() + { + var result = Query.NotBetween("attr", "a", "z"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notBetween", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + Assert.Equal("a", query.Values[0].ToString()); + Assert.Equal("z", query.Values[1].ToString()); + } + + [Fact] + public void NotStartsWith_ReturnsCorrectQuery() + { + var result = Query.NotStartsWith("attr", "prefix"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notStartsWith", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("prefix", query.Values[0].ToString()); + } + + [Fact] + public void NotEndsWith_ReturnsCorrectQuery() + { + var result = Query.NotEndsWith("attr", "suffix"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notEndsWith", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("suffix", query.Values[0].ToString()); + } + + [Fact] + public void CreatedBefore_ReturnsCorrectQuery() + { + var result = Query.CreatedBefore("2023-01-01"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("lessThan", query.Method); + Assert.Equal("$createdAt", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("2023-01-01", query.Values[0].ToString()); + } + + [Fact] + public void CreatedAfter_ReturnsCorrectQuery() + { + var result = Query.CreatedAfter("2023-01-01"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("greaterThan", query.Method); + Assert.Equal("$createdAt", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("2023-01-01", query.Values[0].ToString()); + } + + [Fact] + public void CreatedBetween_ReturnsCorrectQuery() + { + var result = Query.CreatedBetween("2023-01-01", "2023-12-31"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("between", query.Method); + Assert.Equal("$createdAt", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + Assert.Equal("2023-01-01", query.Values[0].ToString()); + Assert.Equal("2023-12-31", query.Values[1].ToString()); + } + + [Fact] + public void UpdatedBefore_ReturnsCorrectQuery() + { + var result = Query.UpdatedBefore("2023-01-01"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("lessThan", query.Method); + Assert.Equal("$updatedAt", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("2023-01-01", query.Values[0].ToString()); + } + + [Fact] + public void UpdatedAfter_ReturnsCorrectQuery() + { + var result = Query.UpdatedAfter("2023-01-01"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("greaterThan", query.Method); + Assert.Equal("$updatedAt", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("2023-01-01", query.Values[0].ToString()); + } + + [Fact] + public void UpdatedBetween_ReturnsCorrectQuery() + { + var result = Query.UpdatedBetween("2023-01-01", "2023-12-31"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("between", query.Method); + Assert.Equal("$updatedAt", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + Assert.Equal("2023-01-01", query.Values[0].ToString()); + Assert.Equal("2023-12-31", query.Values[1].ToString()); + } + + [Fact] + public void OrderRandom_ReturnsCorrectQuery() + { + var result = Query.OrderRandom(); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("orderRandom", query.Method); + } + + [Fact] + public void Regex_ReturnsCorrectQuery() + { + var result = Query.Regex("attr", "^[a-z]+$"); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("regex", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + Assert.Equal("^[a-z]+$", query.Values[0].ToString()); + } + + [Fact] + public void Exists_WithSingleAttribute_ReturnsCorrectQuery() + { + var result = Query.Exists(new List() { "attr1" }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("exists", query.Method); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + } + + [Fact] + public void Exists_WithMultipleAttributes_ReturnsCorrectQuery() + { + var result = Query.Exists(new List() { "attr1", "attr2", "attr3" }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("exists", query.Method); + Assert.NotNull(query.Values); + Assert.Equal(3, query.Values.Count); + } + + [Fact] + public void NotExists_WithSingleAttribute_ReturnsCorrectQuery() + { + var result = Query.NotExists(new List() { "attr1" }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notExists", query.Method); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + } + + [Fact] + public void NotExists_WithMultipleAttributes_ReturnsCorrectQuery() + { + var result = Query.NotExists(new List() { "attr1", "attr2", "attr3" }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notExists", query.Method); + Assert.NotNull(query.Values); + Assert.Equal(3, query.Values.Count); + } + + [Fact] + public void ElemMatch_WithSingleQuery_ReturnsCorrectQuery() + { + var result = Query.ElemMatch("attr", new List() { + Query.Equal("subAttr", "value") + }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("elemMatch", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Single(query.Values); + } + + [Fact] + public void ElemMatch_WithMultipleQueries_ReturnsCorrectQuery() + { + var result = Query.ElemMatch("attr", new List() { + Query.Equal("subAttr1", "value1"), + Query.GreaterThan("subAttr2", 10) + }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("elemMatch", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + Assert.Equal(2, query.Values.Count); + } + + [Fact] + public void DistanceEqual_ReturnsCorrectQuery() + { + var result = Query.DistanceEqual("attr", new List { 40.0, -74.0 }, 1000); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("distanceEqual", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void DistanceNotEqual_ReturnsCorrectQuery() + { + var result = Query.DistanceNotEqual("attr", new List { 40.0, -74.0 }, 1000); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("distanceNotEqual", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void DistanceGreaterThan_ReturnsCorrectQuery() + { + var result = Query.DistanceGreaterThan("attr", new List { 40.0, -74.0 }, 500); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("distanceGreaterThan", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void DistanceLessThan_ReturnsCorrectQuery() + { + var result = Query.DistanceLessThan("attr", new List { 40.0, -74.0 }, 500); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("distanceLessThan", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void Intersects_ReturnsCorrectQuery() + { + var result = Query.Intersects("attr", new List { 40.0, -74.0 }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("intersects", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void NotIntersects_ReturnsCorrectQuery() + { + var result = Query.NotIntersects("attr", new List { 40.0, -74.0 }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notIntersects", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void Crosses_ReturnsCorrectQuery() + { + var result = Query.Crosses("attr", new List { 40.0, -74.0 }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("crosses", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void NotCrosses_ReturnsCorrectQuery() + { + var result = Query.NotCrosses("attr", new List { 40.0, -74.0 }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notCrosses", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void Overlaps_ReturnsCorrectQuery() + { + var result = Query.Overlaps("attr", new List { 40.0, -74.0 }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("overlaps", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void NotOverlaps_ReturnsCorrectQuery() + { + var result = Query.NotOverlaps("attr", new List { 40.0, -74.0 }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notOverlaps", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void Touches_ReturnsCorrectQuery() + { + var result = Query.Touches("attr", new List { 40.0, -74.0 }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("touches", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void NotTouches_ReturnsCorrectQuery() + { + var result = Query.NotTouches("attr", new List { 40.0, -74.0 }); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("notTouches", query.Method); + Assert.Equal("attr", query.Attribute); + Assert.NotNull(query.Values); + } + + [Fact] + public void DistanceEqual_WithMeters_ReturnsCorrectQuery() + { + var result = Query.DistanceEqual("attr", new List { 40.0, -74.0 }, 1000, true); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("distanceEqual", query.Method); + } + + [Fact] + public void DistanceEqual_WithoutMeters_ReturnsCorrectQuery() + { + var result = Query.DistanceEqual("attr", new List { 40.0, -74.0 }, 1000, false); + var query = JsonSerializer.Deserialize(result); + + Assert.NotNull(query); + Assert.Equal("distanceEqual", query.Method); + } + } +} diff --git a/templates/dotnet/Package.Tests/RoleTests.cs.twig b/templates/dotnet/Package.Tests/RoleTests.cs.twig new file mode 100644 index 0000000000..6c2e44fbf6 --- /dev/null +++ b/templates/dotnet/Package.Tests/RoleTests.cs.twig @@ -0,0 +1,68 @@ +using Xunit; +using {{ spec.title | caseUcfirst }}; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class RoleTests + { + [Fact] + public void Any_ReturnsCorrectRole() + { + Assert.Equal("any", Role.Any()); + } + + [Fact] + public void User_WithoutStatus_ReturnsCorrectRole() + { + Assert.Equal("user:custom", Role.User("custom")); + } + + [Fact] + public void User_WithStatus_ReturnsCorrectRole() + { + Assert.Equal("user:custom/verified", Role.User("custom", "verified")); + } + + [Fact] + public void Users_WithoutStatus_ReturnsCorrectRole() + { + Assert.Equal("users", Role.Users()); + } + + [Fact] + public void Users_WithStatus_ReturnsCorrectRole() + { + Assert.Equal("users/verified", Role.Users("verified")); + } + + [Fact] + public void Guests_ReturnsCorrectRole() + { + Assert.Equal("guests", Role.Guests()); + } + + [Fact] + public void Team_WithoutRole_ReturnsCorrectRole() + { + Assert.Equal("team:custom", Role.Team("custom")); + } + + [Fact] + public void Team_WithRole_ReturnsCorrectRole() + { + Assert.Equal("team:custom/owner", Role.Team("custom", "owner")); + } + + [Fact] + public void Member_ReturnsCorrectRole() + { + Assert.Equal("member:custom", Role.Member("custom")); + } + + [Fact] + public void Label_ReturnsCorrectRole() + { + Assert.Equal("label:admin", Role.Label("admin")); + } + } +} diff --git a/templates/dotnet/Package.Tests/Services/ServiceTests.cs.twig b/templates/dotnet/Package.Tests/Services/ServiceTests.cs.twig new file mode 100644 index 0000000000..ea9bdf20cf --- /dev/null +++ b/templates/dotnet/Package.Tests/Services/ServiceTests.cs.twig @@ -0,0 +1,228 @@ +{% import 'dotnet/base/utils.twig' as utils %} +{% macro generate_sub_dict(sub_def) %} +new Dictionary { +{% for subprop in sub_def.properties | filter(p => p.required) %} +{ "{{ subprop.name }}", {% if subprop.enum %}{{ subprop.enumName | caseUcfirst }}.{{ (subprop.enumKeys[0] ?? subprop.enum[0]) | caseEnumKey }}.Value{% elseif subprop.type == 'string' %}"{{ subprop['x-example'] | default('test') | escapeCsString }}"{% elseif subprop.type == 'integer' %}{{ subprop['x-example'] | default(1) }}{% elseif subprop.type == 'number' %}{{ subprop['x-example'] | default(1.0) }}{% elseif subprop.type == 'boolean' %}{% if subprop['x-example'] is defined %}{% if subprop['x-example'] is same as(true) or subprop['x-example'] == 'true' or subprop['x-example'] == 1 %}true{% else %}false{% endif %}{% else %}true{% endif %}{% elseif subprop.type == 'array' %}new List(){% elseif subprop.sub_schema %}new Dictionary(){% else %}"{{ subprop['x-example'] | default('test') | escapeCsString }}"{% endif %} }{% if not loop.last %},{% endif %} +{% endfor %} +} +{% endmacro %} +#pragma warning disable CS0618 // Type or member is obsolete +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Moq; +using {{ spec.title | caseUcfirst }}; +using {{ spec.title | caseUcfirst }}.Services; +{% if spec.definitions is not empty %} +using {{ spec.title | caseUcfirst }}.Models; +{% endif %} +{%- set hasEnums = spec.requestEnums is not empty -%} +{%- if hasEnums -%} +using {{ spec.title | caseUcfirst }}.Enums; +{% endif %} + +namespace {{ spec.title | caseUcfirst }}.Tests.Services +{ + public class {{ service.name | caseUcfirst }}Tests + { + private Mock _mockClient; + private {{ spec.title | caseUcfirst }}.Services.{{ service.name | caseUcfirst }} _{{ service.name | caseCamel }}; + + public {{ service.name | caseUcfirst }}Tests() + { + _mockClient = new Mock(); + _{{ service.name | caseCamel }} = new {{ spec.title | caseUcfirst }}.Services.{{ service.name | caseUcfirst }}(_mockClient.Object); + } + + [Fact] + public void Constructor_WithClient_CreatesInstance() + { + // Arrange + var client = new Mock().Object; + + // Act + var service = new {{ spec.title | caseUcfirst }}.Services.{{ service.name | caseUcfirst }}(client); + + // Assert + Assert.NotNull(service); + } + + {%~ for method in service.methods %} + [Fact] + public async Task {{ method.name | caseUcfirst }}_CallsClient() + { + // Arrange + {%~ if method.responseModel and method.responseModel != 'any' %} + var expectedResponse = new Dictionary + { + {%- for definition in spec.definitions ~%}{%~ if definition.name == method.responseModel -%}{%~ for property in definition.properties | filter((param) => param.required) ~%} + { "{{property.name}}", {% if property.enum %}{{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }}.Value{% elseif property.type == 'string' %}"{{property['x-example'] | default('test')}}"{% elseif property.type == 'boolean' %}true{% elseif property.type == 'integer' %}{{property['x-example'] | default(1)}}{% elseif property.type == 'number' %}{{property['x-example'] | default(1.0)}}{% elseif property.type == 'array' %}new List(){% elseif property.type == 'object' and not property.sub_schema %}new Dictionary(){% elseif property.sub_schema %}{{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }}{% else %}null{% endif %} }, + {%~ endfor ~%}{%- endif -%}{%~ endfor -%} + }; + {%~ elseif method.type == 'location' %} + var expectedResponse = new byte[] { 1, 2, 3 }; + {%~ elseif method.type == 'webAuth' %} + var expectedResponse = "success"; + {%~ else %} + var expectedResponse = new Dictionary(); + {%~ endif %} + + {%~ if method.type == 'webAuth' %} + _mockClient.Setup(c => c.Redirect( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny>() + )).ReturnsAsync(expectedResponse); + {%~ elseif 'multipart/form-data' in method.consumes %} + _mockClient.Setup(c => c.ChunkedUpload<{{ utils.resultType(spec.title, method) }}>( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny, {{ utils.resultType(spec.title, method) }}>>(), + It.IsAny(), + It.IsAny(), + It.IsAny>() + )).ReturnsAsync({% if method.responseModel and method.responseModel != 'any' %}{{ spec.title | caseUcfirst }}.Models.{{ method.responseModel | caseUcfirst | overrideIdentifier }}.From(expectedResponse){% else %}expectedResponse{% endif %}); + {%~ else %} + _mockClient.Setup(c => c.Call<{{ utils.resultType(spec.title, method) }}>( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny>(){% if method.responseModel %}, + It.IsAny, {{ utils.resultType(spec.title, method) }}>>() + {% else %},null{% endif %} + )).ReturnsAsync({% if method.responseModel and method.responseModel != 'any' and method.type != 'location' %}{{ spec.title | caseUcfirst }}.Models.{{ method.responseModel | caseUcfirst | overrideIdentifier }}.From(expectedResponse){% else %}expectedResponse{% endif %}); + {%~ endif %} + + // Act + {%~ if method.parameters.all | length > 0 %} + var result = await _{{ service.name | caseCamel }}.{{ method.name | caseUcfirst }}( + {%~ for parameter in method.parameters.all | filter((param) => param.required) ~%} + {{parameter.name | caseCamel | escapeKeyword}}: {% if parameter.enumValues is not empty %}{{ parameter | enumExample | raw }}{% elseif parameter.type == 'file' %}InputFile.FromPath("./test.png"){% elseif parameter.type == 'object' %}new Dictionary(){% elseif parameter.type == 'array' %}{% set itemType = test_item_type(parameter) %}new List<{{ itemType }}>(){% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'integer' %}{{parameter['x-example'] | default(1)}}{% elseif parameter.type == 'number' %}{{parameter['x-example'] | default(1.0)}}{% elseif parameter.type == 'string' %}"{% if parameter['x-example'] is not empty %}{{parameter['x-example'] | escapeCsString}}{% else %}test{% endif %}"{% else %}null{% endif %}{% if not loop.last %},{% endif %} + + {%~ endfor ~%} + ); + {%~ else %} + var result = await _{{ service.name | caseCamel }}.{{ method.name | caseUcfirst }}(); + {%~ endif %} + + // Assert + {%~ if method.responseModel and method.responseModel != 'any' %} + Assert.NotNull(result); + Assert.IsType<{{ spec.title | caseUcfirst }}.Models.{{ method.responseModel | caseUcfirst | overrideIdentifier }}>(result); + {%~ elseif method.type == 'location' %} + Assert.NotNull(result); + {%~ elseif method.type == 'webAuth' %} + Assert.NotNull(result); + Assert.Equal(expectedResponse, result); + {%~ endif %} + + {%~ if method.type == 'webAuth' %} + _mockClient.Verify(c => c.Redirect( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny>() + ), Times.Once); + {%~ elseif 'multipart/form-data' in method.consumes %} + _mockClient.Verify(c => c.ChunkedUpload<{{ utils.resultType(spec.title, method) }}>( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny, {{ utils.resultType(spec.title, method) }}>>(), + It.IsAny(), + It.IsAny(), + It.IsAny>() + ), Times.Once); + {%~ else %} + _mockClient.Verify(c => c.Call<{{ utils.resultType(spec.title, method) }}>( + "{{ method.method | upper }}", + It.IsAny(), + It.IsAny>(), + It.IsAny>(){% if method.responseModel %}, + It.IsAny, {{ utils.resultType(spec.title, method) }}>>() + {% else %},null{% endif %} + ), Times.Once); + {%~ endif %} + } + + {%~ if method.parameters.all | filter((param) => param.required) | length > 0 %} + [Fact] + public async Task {{ method.name | caseUcfirst }}_WithParameters_PassesCorrectParameters() + { + // Arrange + {%~ for parameter in method.parameters.all | filter((param) => param.required) | slice(0, 3) ~%} + {% if parameter.type == 'file' %}InputFile{% else %}var{% endif %} {{parameter.name | caseCamel | escapeKeyword}} = {% if parameter.enumValues is not empty %}{{ parameter | enumExample | raw }}{% elseif parameter.type == 'file' %}InputFile.FromPath("./test.png"){% elseif parameter.type == 'object' %}new Dictionary(){% elseif parameter.type == 'array' %}{% set itemType = test_item_type(parameter) %}new List<{{ itemType }}>(){% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'integer' %}{{parameter['x-example'] | default(1)}}{% elseif parameter.type == 'number' %}{{parameter['x-example'] | default(1.0)}}{% elseif parameter.type == 'string' %}"{% if parameter['x-example'] is not empty %}{{parameter['x-example'] | escapeCsString}}{% else %}test{% endif %}"{% else %}null{% endif %}; + {%~ endfor ~%} + + {%~ if method.responseModel and method.responseModel != 'any' %} + var expectedResponse = new Dictionary + { + {%- for definition in spec.definitions ~%}{%~ if definition.name == method.responseModel -%}{%~ for property in definition.properties | filter((param) => param.required) ~%} + { "{{property.name}}", {% if property.enum %}{{ property.enumName | caseUcfirst }}.{{ (property.enumKeys[0] ?? property.enum[0]) | caseEnumKey }}.Value{% elseif property.type == 'string' %}"{{property['x-example'] | default('test') | escapeCsString}}"{% elseif property.type == 'integer' %}{{property['x-example'] | default(1)}}{% elseif property.type == 'number' %}{{property['x-example'] | default(1.0)}}{% elseif property.type == 'boolean' %}true{% elseif property.type == 'array' %}new List(){% elseif property.type == 'object' and not property.sub_schema %}new Dictionary(){% elseif property.sub_schema %}{{ _self.generate_sub_dict(spec.definitions | filter(d => d.name == property.sub_schema) | first) }}{% else %}new Dictionary(){% endif %} }, + {%~ endfor ~%}{%- endif -%}{%~ endfor -%} + }; + {%~ elseif method.type == 'location' %} + var expectedResponse = new byte[] { 1, 2, 3 }; + {%~ elseif method.type == 'webAuth' %} + var expectedResponse = "success"; + {%~ else %} + var expectedResponse = new Dictionary(); + {%~ endif %} + + {%~ if method.type == 'webAuth' %} + _mockClient.Setup(c => c.Redirect( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny>() + )).ReturnsAsync(expectedResponse); + {%~ elseif 'multipart/form-data' in method.consumes %} + _mockClient.Setup(c => c.ChunkedUpload<{{ utils.resultType(spec.title, method) }}>( + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny, {{ utils.resultType(spec.title, method) }}>>(), + It.IsAny(), + It.IsAny(), + It.IsAny>() + )).ReturnsAsync({% if method.responseModel and method.responseModel != 'any' %}{{ spec.title | caseUcfirst }}.Models.{{ method.responseModel | caseUcfirst | overrideIdentifier }}.From(expectedResponse){% else %}expectedResponse{% endif %}); + {%~ else %} + _mockClient.Setup(c => c.Call<{{ utils.resultType(spec.title, method) }}>( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny>(){% if method.responseModel %}, + It.IsAny, {{ utils.resultType(spec.title, method) }}>>() + {% else %},null{% endif %} + )).ReturnsAsync({% if method.responseModel and method.responseModel != 'any' and method.type != 'location' %}{{ spec.title | caseUcfirst }}.Models.{{ method.responseModel | caseUcfirst | overrideIdentifier }}.From(expectedResponse){% else %}expectedResponse{% endif %}); + {%~ endif %} + + // Act + {%~ if method.parameters.all | length > 0 %} + await _{{ service.name | caseCamel }}.{{ method.name | caseUcfirst }}( + {%~ for parameter in method.parameters.all | filter((param) => param.required) ~%} + {{parameter.name | caseCamel | escapeKeyword}}: {% if loop.index0 < 3 %}{{parameter.name | caseCamel | escapeKeyword}}{% else %}{% if parameter.enumValues is not empty %}{{ parameter | enumExample | raw }}{% elseif parameter.type == 'file' %}InputFile.FromPath("./test.png"){% elseif parameter.type == 'object' %}new Dictionary(){% elseif parameter.type == 'array' %}{% set itemType = test_item_type(parameter) %}new List<{{ itemType }}>(){% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'integer' %}{{parameter['x-example'] | default(1)}}{% elseif parameter.type == 'number' %}{{parameter['x-example'] | default(1.0)}}{% elseif parameter.type == 'string' %}"{% if parameter['x-example'] is not empty %}{{parameter['x-example'] | escapeCsString}}{% else %}test{% endif %}"{% else %}null{% endif %}{% endif %}{% if not loop.last %},{% endif %} + + {%~ endfor ~%} + ); + {%~ endif %} + + // Assert - parameters were set correctly (implicitly tested by successful call) + Assert.True(true); + } + {%~ endif %} + + {%~ endfor %} + + [Fact] + public void Service_InheritsFromBaseService() + { + // Assert + Assert.IsAssignableFrom(_{{ service.name | caseCamel }}); + } + } +} diff --git a/templates/dotnet/Package.Tests/Tests.csproj.twig b/templates/dotnet/Package.Tests/Tests.csproj.twig new file mode 100644 index 0000000000..85145bb28e --- /dev/null +++ b/templates/dotnet/Package.Tests/Tests.csproj.twig @@ -0,0 +1,28 @@ + + + + net8.0 + {{ spec.title | caseUcfirst }}.Tests + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/templates/dotnet/Package.Tests/UploadProgressTests.cs.twig b/templates/dotnet/Package.Tests/UploadProgressTests.cs.twig new file mode 100644 index 0000000000..2f00f1701c --- /dev/null +++ b/templates/dotnet/Package.Tests/UploadProgressTests.cs.twig @@ -0,0 +1,76 @@ +using Xunit; +using {{ spec.title | caseUcfirst }}; + +namespace {{ spec.title | caseUcfirst }}.Tests +{ + public class UploadProgressTests + { + [Fact] + public void Constructor_WithValidParameters_CreatesInstance() + { + var id = "12345"; + var progress = 0.75; + var sizeUploaded = 1024L; + var chunksTotal = 10L; + var chunksUploaded = 5L; + + var uploadProgress = new UploadProgress(id, progress, sizeUploaded, chunksTotal, chunksUploaded); + + Assert.NotNull(uploadProgress); + Assert.Equal(id, uploadProgress.Id); + Assert.Equal(progress, uploadProgress.Progress); + Assert.Equal(sizeUploaded, uploadProgress.SizeUploaded); + Assert.Equal(chunksTotal, uploadProgress.ChunksTotal); + Assert.Equal(chunksUploaded, uploadProgress.ChunksUploaded); + } + + [Fact] + public void Constructor_WithZeroProgress_CreatesInstance() + { + var uploadProgress = new UploadProgress("id", 0.0, 0L, 10L, 0L); + + Assert.Equal(0.0, uploadProgress.Progress); + Assert.Equal(0L, uploadProgress.SizeUploaded); + Assert.Equal(0L, uploadProgress.ChunksUploaded); + } + + [Fact] + public void Constructor_WithCompleteProgress_CreatesInstance() + { + var uploadProgress = new UploadProgress("id", 100.0, 5120L, 5L, 5L); + + Assert.Equal(100.0, uploadProgress.Progress); + Assert.Equal(uploadProgress.ChunksTotal, uploadProgress.ChunksUploaded); + } + + [Theory] + [InlineData("id1", 25.0, 256L, 4L, 1L)] + [InlineData("id2", 50.0, 512L, 4L, 2L)] + [InlineData("id3", 75.0, 768L, 4L, 3L)] + [InlineData("id4", 100.0, 1024L, 4L, 4L)] + public void Constructor_WithVariousValues_CreatesCorrectInstance( + string id, double progress, long sizeUploaded, long chunksTotal, long chunksUploaded) + { + var uploadProgress = new UploadProgress(id, progress, sizeUploaded, chunksTotal, chunksUploaded); + + Assert.Equal(id, uploadProgress.Id); + Assert.Equal(progress, uploadProgress.Progress); + Assert.Equal(sizeUploaded, uploadProgress.SizeUploaded); + Assert.Equal(chunksTotal, uploadProgress.ChunksTotal); + Assert.Equal(chunksUploaded, uploadProgress.ChunksUploaded); + } + + [Fact] + public void Properties_AreReadOnly() + { + var propertyNames = new[] { "Id", "Progress", "SizeUploaded", "ChunksTotal", "ChunksUploaded" }; + + foreach (var name in propertyNames) + { + var propertyInfo = typeof(UploadProgress).GetProperty(name); + Assert.NotNull(propertyInfo); + Assert.Null(propertyInfo.GetSetMethod()); + } + } + } +} diff --git a/templates/dotnet/Package.sln b/templates/dotnet/Package.sln index a8e4b4e574..c4ffeb4bde 100644 --- a/templates/dotnet/Package.sln +++ b/templates/dotnet/Package.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.30114.128 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Appwrite", "Appwrite\Appwrite.csproj", "{ABD3EB63-B648-49D6-B7FD-C17A762A3EC3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Appwrite.Tests", "Appwrite.Tests\Appwrite.Tests.csproj", "{B9B36337-BF75-4601-AB9C-C2A7ACC91DF6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,8 @@ Global {ABD3EB63-B648-49D6-B7FD-C17A762A3EC3}.Debug|Any CPU.Build.0 = Debug|Any CPU {ABD3EB63-B648-49D6-B7FD-C17A762A3EC3}.Release|Any CPU.ActiveCfg = Release|Any CPU {ABD3EB63-B648-49D6-B7FD-C17A762A3EC3}.Release|Any CPU.Build.0 = Release|Any CPU + {B9B36337-BF75-4601-AB9C-C2A7ACC91DF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9B36337-BF75-4601-AB9C-C2A7ACC91DF6}.Debug|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/templates/dotnet/Package/Client.cs.twig b/templates/dotnet/Package/Client.cs.twig index 53bd7ba62c..cc624a7368 100644 --- a/templates/dotnet/Package/Client.cs.twig +++ b/templates/dotnet/Package/Client.cs.twig @@ -88,6 +88,27 @@ namespace {{ spec.title | caseUcfirst }} } } + // Parameterless constructor required for mocking frameworks (Moq/Castle) + // Initializes minimal defaults so proxies can be created without errors. + protected Client() + { + _endpoint = "{{spec.endpoint}}"; + _http = new HttpClient(); + _httpForRedirect = new HttpClient(new HttpClientHandler(){ AllowAutoRedirect = false }); + + _headers = new Dictionary() + { + { "content-type", "application/json" }, + { "user-agent" , $"{{spec.title | caseUcfirst}}{{ language.name | caseUcfirst }}SDK/{{ sdk.version }} ({Environment.OSVersion.Platform}; {Environment.OSVersion.VersionString})"}, + { "x-sdk-name", "{{ sdk.name }}" }, + { "x-sdk-platform", "{{ sdk.platform }}" }, + { "x-sdk-language", "{{ language.name | caseLower }}" }, + { "x-sdk-version", "{{ sdk.version }}" } + }; + + _config = new Dictionary(); + } + public Client SetSelfSigned(bool selfSigned) { var handler = new HttpClientHandler() @@ -154,6 +175,7 @@ namespace {{ spec.title | caseUcfirst }} foreach (var parameter in parameters) { + if (parameter.Value == null) continue; if (parameter.Key == "file") { var fileContent = parameters["file"] as MultipartFormDataContent; @@ -222,7 +244,7 @@ namespace {{ spec.title | caseUcfirst }} return request; } - public async Task Redirect( + public virtual async Task Redirect( string method, string path, Dictionary headers, @@ -268,7 +290,7 @@ namespace {{ spec.title | caseUcfirst }} return response.Headers.Location?.OriginalString ?? string.Empty; } - public Task> Call( + public virtual Task> Call( string method, string path, Dictionary headers, @@ -277,7 +299,7 @@ namespace {{ spec.title | caseUcfirst }} return Call>(method, path, headers, parameters); } - public async Task Call( + public virtual async Task Call( string method, string path, Dictionary headers, @@ -352,7 +374,7 @@ namespace {{ spec.title | caseUcfirst }} } } - public async Task ChunkedUpload( + public virtual async Task ChunkedUpload( string path, Dictionary headers, Dictionary parameters, diff --git a/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig b/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig index 563f92992a..ce772c93df 100644 --- a/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig +++ b/templates/dotnet/Package/Converters/ObjectToInferredTypesConverter.cs.twig @@ -7,32 +7,60 @@ namespace {{ spec.title | caseUcfirst }}.Converters { public class ObjectToInferredTypesConverter : JsonConverter { - public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - switch (reader.TokenType) + using (JsonDocument document = JsonDocument.ParseValue(ref reader)) { - case JsonTokenType.True: - return true; - case JsonTokenType.False: - return false; - case JsonTokenType.Number: - if (reader.TryGetInt64(out long l)) + return ConvertElement(document.RootElement); + } + } + + private object? ConvertElement(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + var dictionary = new Dictionary(); + foreach (var property in element.EnumerateObject()) { - return l; + dictionary[property.Name] = ConvertElement(property.Value); + } + return dictionary; + + case JsonValueKind.Array: + var list = new List(); + foreach (var item in element.EnumerateArray()) + { + list.Add(ConvertElement(item)); } - return reader.GetDouble(); - case JsonTokenType.String: - if (reader.TryGetDateTime(out DateTime datetime)) + return list; + + case JsonValueKind.String: + if (element.TryGetDateTime(out DateTime datetime)) { return datetime; } - return reader.GetString()!; - case JsonTokenType.StartObject: - return JsonSerializer.Deserialize>(ref reader, options)!; - case JsonTokenType.StartArray: - return JsonSerializer.Deserialize(ref reader, options)!; + return element.GetString(); + + case JsonValueKind.Number: + if (element.TryGetInt64(out long l)) + { + return l; + } + return element.GetDouble(); + + case JsonValueKind.True: + return true; + + case JsonValueKind.False: + return false; + + case JsonValueKind.Null: + case JsonValueKind.Undefined: + return null; + default: - return JsonDocument.ParseValue(ref reader).RootElement.Clone(); + throw new JsonException($"Unsupported JsonValueKind: {element.ValueKind}"); } } diff --git a/templates/dotnet/Package/Exception.cs.twig b/templates/dotnet/Package/Exception.cs.twig index e78d78c2cc..857bd0eeb2 100644 --- a/templates/dotnet/Package/Exception.cs.twig +++ b/templates/dotnet/Package/Exception.cs.twig @@ -5,8 +5,8 @@ namespace {{spec.title | caseUcfirst}} public class {{spec.title | caseUcfirst}}Exception : Exception { public int? Code { get; set; } - public string? Type { get; set; } = null; - public string? Response { get; set; } = null; + public string? Type { get; set; } + public string? Response { get; set; } public {{spec.title | caseUcfirst}}Exception( string? message = null, @@ -18,10 +18,10 @@ namespace {{spec.title | caseUcfirst}} this.Type = type; this.Response = response; } + public {{spec.title | caseUcfirst}}Exception(string message, Exception inner) : base(message, inner) { } } } - diff --git a/templates/dotnet/Package/Extensions/Extensions.cs.twig b/templates/dotnet/Package/Extensions/Extensions.cs.twig index 0ac19f7ce7..55635f7bc1 100644 --- a/templates/dotnet/Package/Extensions/Extensions.cs.twig +++ b/templates/dotnet/Package/Extensions/Extensions.cs.twig @@ -13,15 +13,14 @@ namespace {{ spec.title | caseUcfirst }}.Extensions return JsonSerializer.Serialize(dict, Client.SerializerOptions); } - public static List ConvertToList(this object value) + public static IEnumerable ToEnumerable(this object value) { return value switch { - JsonElement jsonElement => jsonElement.Deserialize>() ?? throw new InvalidCastException($"Cannot deserialize {jsonElement} to List<{typeof(T)}>."), - object[] objArray => objArray.Cast().ToList(), - List list => list, - IEnumerable enumerable => enumerable.ToList(), - _ => throw new InvalidCastException($"Cannot convert {value.GetType()} to List<{typeof(T)}>") + object[] array => array, + IEnumerable enumerable => enumerable, + IEnumerable nonGeneric => nonGeneric.Cast(), + _ => throw new InvalidCastException($"Cannot convert {value?.GetType().Name ?? "null"} to IEnumerable") }; } @@ -50,7 +49,7 @@ namespace {{ spec.title | caseUcfirst }}.Extensions return Uri.EscapeUriString(string.Join("&", query)); } - private static IDictionary _mappings = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { + private static readonly IDictionary Mappings = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { #region Mime Types {".323", "text/h323"}, @@ -621,7 +620,7 @@ namespace {{ spec.title | caseUcfirst }}.Extensions { if (extension == null) { - throw new ArgumentNullException("extension"); + throw new ArgumentNullException(nameof(extension)); } if (!extension.StartsWith(".")) @@ -629,7 +628,7 @@ namespace {{ spec.title | caseUcfirst }}.Extensions extension = "." + extension; } - return _mappings.TryGetValue(extension, out var mime) ? mime : "application/octet-stream"; + return Mappings.TryGetValue(extension, out var mime) ? mime : "application/octet-stream"; } public static string GetMimeType(this string path) @@ -637,4 +636,4 @@ namespace {{ spec.title | caseUcfirst }}.Extensions return GetMimeTypeFromExtension(System.IO.Path.GetExtension(path)); } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Models/InputFile.cs.twig b/templates/dotnet/Package/Models/InputFile.cs.twig index 241a3adad5..aaf7a66202 100644 --- a/templates/dotnet/Package/Models/InputFile.cs.twig +++ b/templates/dotnet/Package/Models/InputFile.cs.twig @@ -1,5 +1,5 @@ using System.IO; -using Appwrite.Extensions; +using {{ spec.title | caseUcfirst }}.Extensions; namespace {{ spec.title | caseUcfirst }}.Models { @@ -38,4 +38,4 @@ namespace {{ spec.title | caseUcfirst }}.Models SourceType = "bytes" }; } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Models/Model.cs.twig b/templates/dotnet/Package/Models/Model.cs.twig index e3f0bd132d..3b77f1b160 100644 --- a/templates/dotnet/Package/Models/Model.cs.twig +++ b/templates/dotnet/Package/Models/Model.cs.twig @@ -1,28 +1,30 @@ - +{% set DefinitionClass = definition.name | caseUcfirst | overrideIdentifier %} using System; using System.Linq; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; +{% if definition.properties | filter(p => p.enum) | length > 0 %} using {{ spec.title | caseUcfirst }}.Enums; +{% endif %} using {{ spec.title | caseUcfirst }}.Extensions; namespace {{ spec.title | caseUcfirst }}.Models { - public class {{ definition.name | caseUcfirst | overrideIdentifier }} + public class {{ DefinitionClass }} { {%~ for property in definition.properties %} [JsonPropertyName("{{ property.name }}")] - public {{ sub_schema(property) | raw }} {{ property_name(definition, property) | overrideProperty(definition.name) }} { get; private set; } + public {{ sub_schema(property) }} {{ property_name(definition, property) | overrideProperty(definition.name) }} { get; private set; } {%~ endfor %} {%~ if definition.additionalProperties %} public Dictionary Data { get; private set; } {%~ endif %} - public {{ definition.name | caseUcfirst | overrideIdentifier }}( + public {{ DefinitionClass }}( {%~ for property in definition.properties %} - {{ sub_schema(property) | raw }} {{ property.name | caseCamel | escapeKeyword }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {{ sub_schema(property) }} {{ property.name | caseCamel | escapeKeyword }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} {%~ endfor %} {%~ if definition.additionalProperties %} @@ -37,57 +39,20 @@ namespace {{ spec.title | caseUcfirst }}.Models {%~ endif %} } - public static {{ definition.name | caseUcfirst | overrideIdentifier }} From(Dictionary map) => new {{ definition.name | caseUcfirst | overrideIdentifier }}( + public static {{ DefinitionClass }} From(Dictionary map) => new {{ DefinitionClass }}( {%~ for property in definition.properties %} - {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}:{{' '}} - {%- if property.sub_schema %} - {%- if property.type == 'array' -%} - map["{{ property.name }}"].ConvertToList>().Select(it => {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: it)).ToList() - {%- else -%} - {{ property.sub_schema | caseUcfirst | overrideIdentifier }}.From(map: map["{{ property.name }}"] is JsonElement jsonObj{{ loop.index }} ? jsonObj{{ loop.index }}.Deserialize>()! : (Dictionary)map["{{ property.name }}"]) - {%- endif %} - {%- elseif property.enum %} - {%- set enumName = property['enumName'] ?? property.name -%} - {%- if not property.required -%} - map.TryGetValue("{{ property.name }}", out var enumRaw{{ loop.index }}) - ? enumRaw{{ loop.index }} == null - ? null - : new {{ enumName | caseUcfirst }}(enumRaw{{ loop.index }}.ToString()!) - : null - {%- else -%} - new {{ enumName | caseUcfirst }}(map["{{ property.name }}"].ToString()!) - {%- endif %} - {%- else %} - {%- if property.type == 'array' -%} - map["{{ property.name }}"].ConvertToList<{{ property | typeName | replace({'List<': '', '>': ''}) }}>() - {%- else %} - {%- if property.type == "integer" or property.type == "number" %} - {%- if not property.required -%}map["{{ property.name }}"] == null ? null :{% endif %}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}(map["{{ property.name }}"]) - {%- else %} - {%- if property.type == "boolean" -%} - ({{ property | typeName }}{% if not property.required %}?{% endif %})map["{{ property.name }}"] - {%- else %} - {%- if not property.required -%} - map.TryGetValue("{{ property.name }}", out var {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}) ? {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}?.ToString() : null - {%- else -%} - map["{{ property.name }}"].ToString() - {%- endif %} - {%- endif %} - {%~ endif %} - {%~ endif %} - {%~ endif %} - {%- if not loop.last or (loop.last and definition.additionalProperties) %}, - {%~ endif %} + {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}: {{ property | propertyAssignment | raw }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {%~ endfor %} - {%- if definition.additionalProperties %} + {%~ if definition.additionalProperties %} data: map.TryGetValue("data", out var dataValue) ? (Dictionary)dataValue : map - {%- endif ~%} + {%~ endif %} ); public Dictionary ToMap() => new Dictionary() { {%~ for property in definition.properties %} - { "{{ property.name }}", {% if property.sub_schema %}{% if property.type == 'array' %}{{ property_name(definition, property) | overrideProperty(definition.name) }}.Select(it => it.ToMap()){% else %}{{ property_name(definition, property) | overrideProperty(definition.name) }}.ToMap(){% endif %}{% elseif property.enum %}{{ property_name(definition, property) | overrideProperty(definition.name) }}{% if not property.required %}?{% endif %}.Value{% else %}{{ property_name(definition, property) | overrideProperty(definition.name) }}{% endif %}{{ ' }' }}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + { "{{ property.name }}", {{ property | toMapValue(definition.name) | raw }} }{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} {%~ endfor %} {%~ if definition.additionalProperties %} @@ -100,7 +65,7 @@ namespace {{ spec.title | caseUcfirst }}.Models fromJson.Invoke(Data); {%~ endif %} {%~ for property in definition.properties %} - {%~ if property.sub_schema %} + {%~ if property.sub_schema and not definition.additionalProperties %} {%~ for def in spec.definitions %} {%~ if def.name == property.sub_schema and def.additionalProperties and property.type == 'array' %} diff --git a/templates/dotnet/Package/Models/RequestModel.cs.twig b/templates/dotnet/Package/Models/RequestModel.cs.twig index 333320442e..e87c9bc41f 100644 --- a/templates/dotnet/Package/Models/RequestModel.cs.twig +++ b/templates/dotnet/Package/Models/RequestModel.cs.twig @@ -36,7 +36,7 @@ namespace {{ spec.title | caseUcfirst }}.Models return new Dictionary { {%~ for property in requestModel.properties %} - { "{{ property.name }}", {% if property.sub_schema %}{% if property.type == 'array' %}{{ property.name | caseUcfirst | overrideIdentifier }}?.ConvertAll(p => p.ToMap()){% else %}{{ property.name | caseUcfirst | overrideIdentifier }}?.ToMap(){% endif %}{% elseif property.enum %}{{ property.name | caseUcfirst | overrideIdentifier }}{% if not property.required %}?{% endif %}.Value{% else %}{{ property.name | caseUcfirst | overrideIdentifier }}{% endif %} }{% if not loop.last %},{% endif %} + { "{{ property.name }}", {{ property | toMapValue(requestModel.name) | raw }} }{% if not loop.last %},{% endif %} {%~ endfor %} }; diff --git a/templates/dotnet/Package/Models/UploadProgress.cs.twig b/templates/dotnet/Package/Models/UploadProgress.cs.twig index 47c78391ce..ee6fb58ba3 100644 --- a/templates/dotnet/Package/Models/UploadProgress.cs.twig +++ b/templates/dotnet/Package/Models/UploadProgress.cs.twig @@ -23,4 +23,4 @@ namespace {{ spec.title | caseUcfirst }} ChunksUploaded = chunksUploaded; } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Operator.cs.twig b/templates/dotnet/Package/Operator.cs.twig index 022b209140..f47202409b 100644 --- a/templates/dotnet/Package/Operator.cs.twig +++ b/templates/dotnet/Package/Operator.cs.twig @@ -79,7 +79,7 @@ namespace {{ spec.title | caseUcfirst }} } } - override public string ToString() + public override string ToString() { return JsonSerializer.Serialize(this, Client.SerializerOptions); } diff --git a/templates/dotnet/Package/Query.cs.twig b/templates/dotnet/Package/Query.cs.twig index 17c647193c..9540b5a463 100644 --- a/templates/dotnet/Package/Query.cs.twig +++ b/templates/dotnet/Package/Query.cs.twig @@ -40,7 +40,7 @@ namespace {{ spec.title | caseUcfirst }} } } - override public string ToString() + public override string ToString() { return JsonSerializer.Serialize(this, Client.SerializerOptions); } @@ -340,4 +340,4 @@ namespace {{ spec.title | caseUcfirst }} return new Query("notTouches", attribute, new List { values }).ToString(); } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Role.cs.twig b/templates/dotnet/Package/Role.cs.twig index b3ecf2610b..3c7b2b33f3 100644 --- a/templates/dotnet/Package/Role.cs.twig +++ b/templates/dotnet/Package/Role.cs.twig @@ -1,4 +1,4 @@ -namespace Appwrite +namespace {{ spec.title | caseUcfirst }} { /// /// Helper class to generate role strings for Permission. @@ -89,4 +89,4 @@ namespace Appwrite return $"label:{name}"; } } -} \ No newline at end of file +} diff --git a/templates/dotnet/Package/Services/ServiceTemplate.cs.twig b/templates/dotnet/Package/Services/ServiceTemplate.cs.twig index 7252134bef..8a0086f04b 100644 --- a/templates/dotnet/Package/Services/ServiceTemplate.cs.twig +++ b/templates/dotnet/Package/Services/ServiceTemplate.cs.twig @@ -1,5 +1,4 @@ {% import 'dotnet/base/utils.twig' as utils %} - using System; using System.Collections.Generic; using System.Linq; diff --git a/templates/dotnet/base/utils.twig b/templates/dotnet/base/utils.twig index a1f1f2b7da..25a4be08ee 100644 --- a/templates/dotnet/base/utils.twig +++ b/templates/dotnet/base/utils.twig @@ -12,5 +12,5 @@ {% if (method.type == "webAuth" or method.type == "location") and method.auth|length > 0 %}{{ true }}{% else %}{{false}}{% endif %} {% endmacro %} {% macro resultType(namespace, method) %} -{% if method.type == "webAuth" %}bool{% elseif method.type == "location" %}byte[]{% elseif method.responseModels|length > 1 %}object{% elseif not method.responseModel or method.responseModel == 'any' %}object{% else %}Models.{{method.responseModel | caseUcfirst | overrideIdentifier }}{% endif %} +{% if method.type == "webAuth" %}bool{% elseif method.type == "location" %}byte[]{% elseif method.responseModels|length > 1 %}object{% elseif not method.responseModel or method.responseModel == 'any' %}object{% else %}Appwrite.Models.{{method.responseModel | caseUcfirst | overrideIdentifier }}{% endif %} {% endmacro %} \ No newline at end of file