Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Default: Git decides per-file based on heuristics, but normalises text on commit.
* text=auto

# Pin LF across the board — matches the existing state of every file in the
# repo today, including the .sln. Prevents CRLF drift when contributors edit
# from Windows without an EOL-aware editor.
*.cs text eol=lf
*.csproj text eol=lf
*.sln text eol=lf
*.json text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.xml text eol=lf
*.props text eol=lf
*.targets text eol=lf
*.editorconfig text eol=lf
*.gitattributes text eol=lf

# Binary — never touch.
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.snk binary
*.nupkg binary
*.dll binary
*.exe binary
*.pdb binary
8 changes: 4 additions & 4 deletions Calinga.NET.Tests/Calinga.NET.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
</PackageReference>
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="RichardSzalay.MockHttp" Version="6.0.0" />
<PackageReference Include="SpecFlow" Version="3.9.22" />
<PackageReference Include="SpecFlow.MsTest" Version="3.9.22" />
<PackageReference Include="SpecFlow.Tools.MsBuild.Generation" Version="3.9.22" />
<PackageReference Include="SpecFlow" Version="3.9.22" />
<PackageReference Include="SpecFlow.MsTest" Version="3.9.22" />
<PackageReference Include="SpecFlow.Tools.MsBuild.Generation" Version="3.9.22" />
</ItemGroup>

<ItemGroup>
Expand All @@ -29,7 +29,7 @@
<Target Name="IncludeCucumberMessagesSpecs" BeforeTargets="BeforeUpdateFeatureFilesInProject" Condition="$(DesignTimeBuild) != 'true' OR '$(BuildingProject)' == 'true'">
<Copy SourceFiles="@(FeatureFiles)" DestinationFolder="Specs/%(RecursiveDir)" />
<ItemGroup>
<None Include="Specs/**/*.feature" />
<None Include="Specs/**/*.feature" />
</ItemGroup>
</Target>

Expand Down
313 changes: 309 additions & 4 deletions Calinga.NET.Tests/CalingaServiceTests.cs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Calinga.NET.Tests/Context/TestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

using Moq;
using Moq.Protected;
using Newtonsoft.Json;
using System.Text.Json;

using Calinga.NET.Caching;
using Calinga.NET.Infrastructure;
Expand Down Expand Up @@ -120,7 +120,7 @@ private HttpClient BuildHttpClientMock()
return new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(JsonConvert.SerializeObject(translations))
Content = new StringContent(JsonSerializer.Serialize(translations))
};
}
catch (Exception)
Expand Down
74 changes: 62 additions & 12 deletions Calinga.NET.Tests/FileCachingServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
using Calinga.NET.Infrastructure.Exceptions;
using FluentAssertions;
using Moq;
using Newtonsoft.Json;
using System.Text.Json;

namespace Calinga.NET.Tests
{
Expand Down Expand Up @@ -48,7 +48,7 @@ public async Task StoreTranslationsAsync_CreatesFileWithValidJson()
var tempFilePath = Path.Combine(_settings.CacheDirectory, _settings.Organization, _settings.Team, _settings.Project, "EN.json.temp");
_fileSystem.Setup(fs => fs.CreateDirectory(It.IsAny<string>()));
_fileSystem.Setup(fs => fs.WriteAllTextAsync(tempFilePath, It.IsAny<string>())).Returns(Task.CompletedTask);
_fileSystem.Setup(fs => fs.ReadAllTextAsync(tempFilePath)).ReturnsAsync(JsonConvert.SerializeObject(translations));
_fileSystem.Setup(fs => fs.ReadAllTextAsync(tempFilePath)).ReturnsAsync(JsonSerializer.Serialize(translations));
_fileSystem.Setup(fs => fs.FileExists(path)).Returns(false);
_fileSystem.Setup(fs => fs.ReplaceFile(tempFilePath, path, null));

Expand Down Expand Up @@ -105,7 +105,7 @@ public async Task StoreTranslationsAsync_OverwritesExistingFile()
var prevFilePath = Path.Combine(_settings.CacheDirectory, _settings.Organization, _settings.Team, _settings.Project, "EN.json.prev");
_fileSystem.Setup(fs => fs.CreateDirectory(It.IsAny<string>()));
_fileSystem.Setup(fs => fs.WriteAllTextAsync(tempFilePath, It.IsAny<string>())).Returns(Task.CompletedTask);
_fileSystem.Setup(fs => fs.ReadAllTextAsync(tempFilePath)).ReturnsAsync(JsonConvert.SerializeObject(translations));
_fileSystem.Setup(fs => fs.ReadAllTextAsync(tempFilePath)).ReturnsAsync(JsonSerializer.Serialize(translations));
_fileSystem.Setup(fs => fs.FileExists(path)).Returns(true);
_fileSystem.Setup(fs => fs.ReplaceFile(tempFilePath, path, null));

Expand All @@ -128,7 +128,7 @@ public async Task StoreTranslationsAsync_HandlesEmptyTranslations()
var tempFilePath = Path.Combine(_settings.CacheDirectory, _settings.Organization, _settings.Team, _settings.Project, "EN.json.temp");
_fileSystem.Setup(fs => fs.CreateDirectory(It.IsAny<string>()));
_fileSystem.Setup(fs => fs.WriteAllTextAsync(tempFilePath, It.IsAny<string>())).Returns(Task.CompletedTask);
_fileSystem.Setup(fs => fs.ReadAllTextAsync(tempFilePath)).ReturnsAsync(JsonConvert.SerializeObject(translations));
_fileSystem.Setup(fs => fs.ReadAllTextAsync(tempFilePath)).ReturnsAsync(JsonSerializer.Serialize(translations));
_fileSystem.Setup(fs => fs.FileExists(path)).Returns(false);
_fileSystem.Setup(fs => fs.ReplaceFile(tempFilePath, path, null));

Expand Down Expand Up @@ -165,7 +165,7 @@ public async Task GetTranslations_FileExists_ReturnsValidTranslations()
var path = Path.Combine(_settings.CacheDirectory, _settings.Organization, _settings.Team, _settings.Project, "EN.json");
var translations = new Dictionary<string, string> { { "key1", "value1" } };
_fileSystem.Setup(fs => fs.FileExists(path)).Returns(true);
_fileSystem.Setup(fs => fs.ReadAllTextAsync(path)).ReturnsAsync(JsonConvert.SerializeObject(translations));
_fileSystem.Setup(fs => fs.ReadAllTextAsync(path)).ReturnsAsync(JsonSerializer.Serialize(translations));

// Act
var result = await _service.GetTranslations(language, false);
Expand Down Expand Up @@ -210,7 +210,7 @@ public async Task GetLanguages_FileExists_ReturnsValidLanguages()
var path = Path.Combine(_settings.CacheDirectory, _settings.Organization, _settings.Team, _settings.Project, "Languages.json");
var languages = new List<Language> { new Language { Name = "en" } };
_fileSystem.Setup(fs => fs.FileExists(path)).Returns(true);
_fileSystem.Setup(fs => fs.ReadAllTextAsync(path)).ReturnsAsync(JsonConvert.SerializeObject(languages));
_fileSystem.Setup(fs => fs.ReadAllTextAsync(path)).ReturnsAsync(JsonSerializer.Serialize(languages));

// Act
var result = await _service.GetLanguages();
Expand Down Expand Up @@ -272,7 +272,7 @@ public async Task StoreLanguagesAsync_CreatesFileWithValidJson()
"Languages.json.temp");
_fileSystem.Setup(fs => fs.CreateDirectory(It.IsAny<string>()));
_fileSystem.Setup(fs => fs.WriteAllTextAsync(tempFilePath, It.IsAny<string>())).Returns(Task.CompletedTask);
_fileSystem.Setup(fs => fs.ReadAllTextAsync(tempFilePath)).ReturnsAsync(JsonConvert.SerializeObject(languages));
_fileSystem.Setup(fs => fs.ReadAllTextAsync(tempFilePath)).ReturnsAsync(JsonSerializer.Serialize(languages));
_fileSystem.Setup(fs => fs.FileExists(path)).Returns(false);
_fileSystem.Setup(fs => fs.ReplaceFile(tempFilePath, path, null));

Expand Down Expand Up @@ -363,7 +363,7 @@ public async Task GetTranslations_InvalidJson_ThrowsExceptionAndLogsWarning()
_fileSystem.Setup(fs => fs.ReadAllTextAsync(path)).ReturnsAsync("{invalid json}");

// Act & Assert
await Assert.ThrowsExceptionAsync<Newtonsoft.Json.JsonReaderException>(() => _service.GetTranslations(language, false));
await Assert.ThrowsExceptionAsync<JsonException>(() => _service.GetTranslations(language, false));
}

[TestMethod]
Expand All @@ -375,7 +375,7 @@ public async Task GetLanguages_InvalidJson_ThrowsExceptionAndLogsWarning()
_fileSystem.Setup(fs => fs.ReadAllTextAsync(path)).ReturnsAsync("{invalid json}");

// Act & Assert
await Assert.ThrowsExceptionAsync<Newtonsoft.Json.JsonReaderException>(() => _service.GetLanguages());
await Assert.ThrowsExceptionAsync<JsonException>(() => _service.GetLanguages());
}

[TestMethod]
Expand Down Expand Up @@ -432,7 +432,7 @@ public async Task StoreTranslationsAsync_LogsInfoOnSuccess()
var tempFilePath = Path.Combine(_settings.CacheDirectory, _settings.Organization, _settings.Team, _settings.Project, "EN.json.temp");
_fileSystem.Setup(fs => fs.CreateDirectory(It.IsAny<string>()));
_fileSystem.Setup(fs => fs.WriteAllTextAsync(tempFilePath, It.IsAny<string>())).Returns(Task.CompletedTask);
_fileSystem.Setup(fs => fs.ReadAllTextAsync(tempFilePath)).ReturnsAsync(JsonConvert.SerializeObject(translations));
_fileSystem.Setup(fs => fs.ReadAllTextAsync(tempFilePath)).ReturnsAsync(JsonSerializer.Serialize(translations));
_fileSystem.Setup(fs => fs.FileExists(path)).Returns(false);
_fileSystem.Setup(fs => fs.ReplaceFile(tempFilePath, path, null));

Expand Down Expand Up @@ -526,7 +526,7 @@ public async Task StoreTranslationsAsync_ReplaceFileThrowsIOException_DeletesTem
var tempFilePath = Path.Combine(_settings.CacheDirectory, _settings.Organization, _settings.Team, _settings.Project, "EN.json.temp");
_fileSystem.Setup(fs => fs.CreateDirectory(It.IsAny<string>()));
_fileSystem.Setup(fs => fs.WriteAllTextAsync(tempFilePath, It.IsAny<string>())).Returns(Task.CompletedTask);
_fileSystem.Setup(fs => fs.ReadAllTextAsync(tempFilePath)).ReturnsAsync(JsonConvert.SerializeObject(translations));
_fileSystem.Setup(fs => fs.ReadAllTextAsync(tempFilePath)).ReturnsAsync(JsonSerializer.Serialize(translations));
_fileSystem.Setup(fs => fs.FileExists(path)).Returns(false);
_fileSystem.Setup(fs => fs.ReplaceFile(tempFilePath, path, null)).Throws<IOException>();
_fileSystem.Setup(fs => fs.FileExists(tempFilePath)).Returns(true);
Expand All @@ -548,7 +548,7 @@ public async Task StoreLanguagesAsync_ReplaceFileThrowsIOException_DeletesTempFi
var tempFilePath = Path.Combine(_settings.CacheDirectory, _settings.Organization, _settings.Team, _settings.Project, "Languages.json.temp");
_fileSystem.Setup(fs => fs.CreateDirectory(It.IsAny<string>()));
_fileSystem.Setup(fs => fs.WriteAllTextAsync(tempFilePath, It.IsAny<string>())).Returns(Task.CompletedTask);
_fileSystem.Setup(fs => fs.ReadAllTextAsync(tempFilePath)).ReturnsAsync(JsonConvert.SerializeObject(languages));
_fileSystem.Setup(fs => fs.ReadAllTextAsync(tempFilePath)).ReturnsAsync(JsonSerializer.Serialize(languages));
_fileSystem.Setup(fs => fs.FileExists(path)).Returns(false);
_fileSystem.Setup(fs => fs.ReplaceFile(tempFilePath, path, null)).Throws<IOException>();
_fileSystem.Setup(fs => fs.FileExists(tempFilePath)).Returns(true);
Expand Down Expand Up @@ -697,5 +697,55 @@ public async Task StoreLanguagesAsync_AndStoreTranslationsAsync_ShouldNotThrow_W
}

#endregion

#region Newtonsoft → System.Text.Json compatibility

[TestMethod]
public async Task GetTranslations_ReadsNewtonsoftEraFile_Successfully()
{
// Arrange — exact byte shape Newtonsoft 13 produced for Dictionary<string, string>:
// compact, double-quoted keys/values, no whitespace, no BOM. Pinning a literal here
// (instead of round-tripping through System.Text.Json) is the whole point — proves
// existing on-disk caches written by 2.1.x are still readable after the JSON-library
// swap in 2.2.0.
const string newtonsoftEraJson = "{\"key1\":\"value1\",\"key2\":\"value2\"}";
var language = "EN";
var path = Path.Combine(_settings.CacheDirectory, _settings.Organization, _settings.Team, _settings.Project, "EN.json");
_fileSystem.Setup(fs => fs.FileExists(path)).Returns(true);
_fileSystem.Setup(fs => fs.ReadAllTextAsync(path)).ReturnsAsync(newtonsoftEraJson);

// Act
var result = await _service.GetTranslations(language, false);

// Assert
result.FoundInCache.Should().BeTrue();
result.Result.Should().HaveCount(2);
result.Result["key1"].Should().Be("value1");
result.Result["key2"].Should().Be("value2");
}

[TestMethod]
public async Task GetLanguages_ReadsNewtonsoftEraFile_Successfully()
{
// Arrange — Newtonsoft 13 default for List<Language>: PascalCase property names,
// no whitespace, double-quoted strings, JSON booleans lowercase. Same rationale as
// the translations test — pin a literal so any future serializer-options change
// (e.g. JsonNamingPolicy.CamelCase) surfaces as a failing test, not a broken cache.
const string newtonsoftEraJson = "[{\"Name\":\"en\",\"IsReference\":true},{\"Name\":\"de\",\"IsReference\":false}]";
var path = Path.Combine(_settings.CacheDirectory, _settings.Organization, _settings.Team, _settings.Project, "Languages.json");
_fileSystem.Setup(fs => fs.FileExists(path)).Returns(true);
_fileSystem.Setup(fs => fs.ReadAllTextAsync(path)).ReturnsAsync(newtonsoftEraJson);

// Act
var result = await _service.GetLanguages();

// Assert
result.FoundInCache.Should().BeTrue();
result.Result.Should().HaveCount(2);
result.Result.Should().ContainSingle(l => l.Name == "en" && l.IsReference);
result.Result.Should().ContainSingle(l => l.Name == "de" && !l.IsReference);
}

#endregion
}
}
Loading