Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
617cb89
Introduce the new endpoint to the Calinga.NET package
funkyDev1213 Apr 24, 2026
2af141a
revert changes to .net version
funkyDev1213 Apr 24, 2026
99b26c3
Remove Newtonsoft
funkyDev1213 Apr 24, 2026
b57ea6d
Remove remaining Newtonsoft dependencies
funkyDev1213 Apr 27, 2026
7c9f8ab
replace throwing LanguagesNotAvailableException with InvalidOperation…
funkyDev1213 Apr 28, 2026
890e84c
Handle null in body from Consumer API and add two tests covering it
funkyDev1213 Apr 28, 2026
86ee986
improve warmup cache test
funkyDev1213 Apr 28, 2026
67c2045
add KeysNotFoundException for when a list of keys is requested but th…
funkyDev1213 Apr 28, 2026
cc90297
add KeysNotFoundException file
funkyDev1213 Apr 28, 2026
7b4a712
Add tests to make sure that the Newtonsoft JSON caches still work in …
funkyDev1213 Apr 28, 2026
0e8512c
improve Nuget package page
funkyDev1213 Apr 28, 2026
46ea807
add gitattributes
funkyDev1213 Apr 28, 2026
27348b0
improve Nuget package's release notes
funkyDev1213 Apr 28, 2026
6af38b9
improve exception handling in fallback case
funkyDev1213 Apr 28, 2026
8dbf448
make Guard actually check what it says
funkyDev1213 Apr 28, 2026
3fc401b
fix typo
funkyDev1213 Apr 28, 2026
829de79
add more information to release notes
funkyDev1213 Apr 28, 2026
247ccf0
Update docs of 2.2.0
funkyDev1213 Apr 29, 2026
dda920a
add more release notes and fix guard
funkyDev1213 Apr 29, 2026
16eedaa
resolve PR comments
funkyDev1213 May 13, 2026
e0b5933
add Etag caching to Calinga.NET
funkyDev1213 May 15, 2026
475156c
fix pr not building
funkyDev1213 May 18, 2026
f598f80
fix failing tests
funkyDev1213 May 18, 2026
6290319
Merge pull request #49 from conplementAG/feature/etagCaching
funkyDev1213 May 20, 2026
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
488 changes: 476 additions & 12 deletions Calinga.NET.Tests/CalingaServiceTests.cs

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions Calinga.NET.Tests/CascadedCachingServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ public async Task GetTranslations_ShouldNotFail_WhenNoCacheHitInAnyLevel()
public async Task StoreTranslation_ShouldAddTranslationToAllLevels()
{
// Arrange
_firstLevelCachingService.Setup(x => x.StoreTranslationsAsync(It.IsAny<string>(), It.IsAny<IReadOnlyDictionary<string, string>>()));
_secondLevelCachingService.Setup(x => x.StoreTranslationsAsync(It.IsAny<string>(), It.IsAny<IReadOnlyDictionary<string, string>>()));
_firstLevelCachingService.Setup(x => x.StoreTranslationsAsync(It.IsAny<string>(), It.IsAny<IReadOnlyDictionary<string, string>>(), It.IsAny<string?>()));
_secondLevelCachingService.Setup(x => x.StoreTranslationsAsync(It.IsAny<string>(), It.IsAny<IReadOnlyDictionary<string, string>>(), It.IsAny<string?>()));

// Act
await _sut.StoreTranslationsAsync(TestData.Language_DE, TestData.Translations_De);

// Assert
_firstLevelCachingService.Verify(x => x.StoreTranslationsAsync(It.IsAny<string>(), It.IsAny<IReadOnlyDictionary<string, string>>()), Times.Once);
_secondLevelCachingService.Verify(x => x.StoreTranslationsAsync(It.IsAny<string>(), It.IsAny<IReadOnlyDictionary<string, string>>()), Times.Once);
_firstLevelCachingService.Verify(x => x.StoreTranslationsAsync(It.IsAny<string>(), It.IsAny<IReadOnlyDictionary<string, string>>(), It.IsAny<string?>()), Times.Once);
_secondLevelCachingService.Verify(x => x.StoreTranslationsAsync(It.IsAny<string>(), It.IsAny<IReadOnlyDictionary<string, string>>(), It.IsAny<string?>()), Times.Once);
}

[TestMethod]
Expand All @@ -110,7 +110,7 @@ public async Task GetTranslations_ShouldBackfillFirstLevel_WhenSecondLevelHasDat
.ReturnsAsync(CacheResponse.Empty);
_secondLevelCachingService.Setup(x => x.GetTranslations(TestData.Language_DE, false))
.ReturnsAsync(TestData.Cache_Translations_De);
_firstLevelCachingService.Setup(x => x.StoreTranslationsAsync(TestData.Language_DE, It.IsAny<IReadOnlyDictionary<string, string>>()))
_firstLevelCachingService.Setup(x => x.StoreTranslationsAsync(TestData.Language_DE, It.IsAny<IReadOnlyDictionary<string, string>>(), It.IsAny<string?>()))
.Returns(Task.CompletedTask);

// Act
Expand All @@ -120,7 +120,7 @@ public async Task GetTranslations_ShouldBackfillFirstLevel_WhenSecondLevelHasDat
actual.Result.Should().BeEquivalentTo(TestData.Translations_De);
_firstLevelCachingService.Verify(
x => x.StoreTranslationsAsync(TestData.Language_DE, It.Is<IReadOnlyDictionary<string, string>>(
dict => dict.Count == TestData.Translations_De.Count)),
dict => dict.Count == TestData.Translations_De.Count), It.IsAny<string?>()),
Times.Once,
"First level cache should be backfilled when second level has data");
}
Expand All @@ -138,7 +138,7 @@ public async Task GetTranslations_ShouldNotBackfill_WhenFirstLevelHasData()
// Assert
actual.Result.Should().BeEquivalentTo(TestData.Translations_De);
_firstLevelCachingService.Verify(
x => x.StoreTranslationsAsync(It.IsAny<string>(), It.IsAny<IReadOnlyDictionary<string, string>>()),
x => x.StoreTranslationsAsync(It.IsAny<string>(), It.IsAny<IReadOnlyDictionary<string, string>>(), It.IsAny<string?>()),
Times.Never,
"No backfill should occur when first level already has data");
_secondLevelCachingService.Verify(
Expand Down
284 changes: 142 additions & 142 deletions Calinga.NET.Tests/Context/TestContext.cs
Original file line number Diff line number Diff line change
@@ -1,143 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Moq;
using Moq.Protected;
using Newtonsoft.Json;
using Calinga.NET.Caching;
using Calinga.NET.Infrastructure;
namespace Calinga.NET.Tests.Context
{
public class TestContext
{
private readonly Dictionary<string, TranslationsRepository> _repositories = new Dictionary<string, TranslationsRepository>();
public CalingaServiceSettings Settings { get; }
public Exception LastException { get; private set; }
public object LastResult { get; private set; }
private ICalingaService _service;
public ICalingaService Service => _service ??= BuildCalingaService();
public TranslationsRepository this[string repository] => _repositories[repository];
public TestContext()
{
Settings = new CalingaServiceSettings
{
CacheDirectory = AppDomain.CurrentDomain.BaseDirectory ?? string.Empty,
Organization = Guid.NewGuid().ToString(),
Team = Guid.NewGuid().ToString(),
Project = Guid.NewGuid().ToString()
};
_repositories.Add("Calinga", new TranslationsRepository());
_repositories.Add("Cache", new TranslationsRepository());
}
public async Task Try<T>(Func<Task<T>> action)
{
try
{
LastResult = await action().ConfigureAwait(false);
LastException = null;
}
catch (Exception e)
{
LastException = e;
}
}
private ICalingaService BuildCalingaService()
{
var httpClient = BuildHttpClientMock();
var fileService = BuildFileCachingServiceMock();
var cachingService = new CascadedCachingService(new InMemoryCachingService(new DateTimeService(), Settings), fileService.Object);
var consumerHttpClient = new ConsumerHttpClient(Settings, httpClient);
return new CalingaService(cachingService, consumerHttpClient, Settings);
}
private Mock<ICachingService> BuildFileCachingServiceMock()
{
var fileService = new Mock<ICachingService>();
fileService.Setup(x => x.GetTranslations(It.IsAny<string>(), It.IsAny<bool>())).ReturnsAsync(
(string languageName, bool isDraft) =>
{
if (
!this["Cache"].Organizations.ContainsKey(Settings.Organization) ||
!this["Cache"].Organizations[Settings.Organization]
.ContainsKey(Settings.Team) ||
!this["Cache"].Organizations[Settings.Organization][
this.Settings.Team].ContainsKey(Settings.Project))
{
return CacheResponse.Empty;
}
return new CacheResponse(this["Cache"].Organizations[Settings.Organization][
Settings.Team][
Settings.Project][languageName], true);
});
fileService.Setup(f => f.ClearCache()).Callback(() =>
{
this["Cache"].Organizations.Clear();
});
return fileService;
}
private HttpClient BuildHttpClientMock()
{
var messageHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
messageHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()).ReturnsAsync(
(HttpRequestMessage request, CancellationToken _) =>
{
if (request.Method == HttpMethod.Get)
{
var segments = request.RequestUri.Segments.Select(s => s.Trim('/'))
.Select(HttpUtility.UrlDecode).ToArray();
var organizationName = segments[2];
var teamName = segments[3];
var projectName = segments[4];
var languageName = segments[6];
try
{
var translations =
this["Calinga"].Organizations
[organizationName][teamName][projectName][languageName];
return new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(JsonConvert.SerializeObject(translations))
};
}
catch (Exception)
{
return new HttpResponseMessage
{
StatusCode = HttpStatusCode.NotFound,
Content = new StringContent("not found")
};
}
}
return new HttpResponseMessage
{ StatusCode = HttpStatusCode.NotImplemented, Content = new StringContent("{}") };
});
var httpClient = new HttpClient(messageHandler.Object);
return httpClient;
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;

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

using Calinga.NET.Caching;
using Calinga.NET.Infrastructure;

namespace Calinga.NET.Tests.Context
{
public class TestContext
{
private readonly Dictionary<string, TranslationsRepository> _repositories = new Dictionary<string, TranslationsRepository>();

public CalingaServiceSettings Settings { get; }
public Exception LastException { get; private set; }
public object LastResult { get; private set; }
private ICalingaService _service;

public ICalingaService Service => _service ??= BuildCalingaService();

public TranslationsRepository this[string repository] => _repositories[repository];

public TestContext()
{
Settings = new CalingaServiceSettings
{
CacheDirectory = AppDomain.CurrentDomain.BaseDirectory ?? string.Empty,
Organization = Guid.NewGuid().ToString(),
Team = Guid.NewGuid().ToString(),
Project = Guid.NewGuid().ToString()
};

_repositories.Add("Calinga", new TranslationsRepository());
_repositories.Add("Cache", new TranslationsRepository());
}

public async Task Try<T>(Func<Task<T>> action)
{
try
{
LastResult = await action().ConfigureAwait(false);
LastException = null;
}
catch (Exception e)
{
LastException = e;
}
}

private ICalingaService BuildCalingaService()
{
var httpClient = BuildHttpClientMock();

var fileService = BuildFileCachingServiceMock();

var cachingService = new CascadedCachingService(new InMemoryCachingService(new DateTimeService(), Settings), fileService.Object);
var consumerHttpClient = new ConsumerHttpClient(Settings, httpClient);

return new CalingaService(cachingService, consumerHttpClient, Settings);
}

private Mock<ICachingService> BuildFileCachingServiceMock()
{
var fileService = new Mock<ICachingService>();
fileService.Setup(x => x.GetTranslations(It.IsAny<string>(), It.IsAny<bool>())).ReturnsAsync(
(string languageName, bool isDraft) =>
{
if (
!this["Cache"].Organizations.ContainsKey(Settings.Organization) ||
!this["Cache"].Organizations[Settings.Organization]
.ContainsKey(Settings.Team) ||
!this["Cache"].Organizations[Settings.Organization][
this.Settings.Team].ContainsKey(Settings.Project))
{
return CacheResponse.Empty;
}

return new CacheResponse(this["Cache"].Organizations[Settings.Organization][
Settings.Team][
Settings.Project][languageName], true);
});

fileService.Setup(f => f.ClearCache()).Callback(() =>
{
this["Cache"].Organizations.Clear();
});
return fileService;
}

private HttpClient BuildHttpClientMock()
{
var messageHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
messageHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()).ReturnsAsync(
(HttpRequestMessage request, CancellationToken _) =>
{
if (request.Method == HttpMethod.Get)
{
var segments = request.RequestUri.Segments.Select(s => s.Trim('/'))
.Select(HttpUtility.UrlDecode).ToArray();
var organizationName = segments[2];
var teamName = segments[3];
var projectName = segments[4];
var languageName = segments[6];
try
{
var translations =
this["Calinga"].Organizations
[organizationName][teamName][projectName][languageName];
return new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(JsonSerializer.Serialize(translations))
};
}
catch (Exception)
{
return new HttpResponseMessage
{
StatusCode = HttpStatusCode.NotFound,
Content = new StringContent("not found")
};
}
}

return new HttpResponseMessage
{ StatusCode = HttpStatusCode.NotImplemented, Content = new StringContent("{}") };
});
var httpClient = new HttpClient(messageHandler.Object);
return httpClient;
}
}
}
Loading