Merge pull request 'release/1.0.8-rc7' (#47) from release/1.0.8-rc7 into main
All checks were successful
Dotnet build and test / build (push) Successful in 2m0s

Reviewed-on: #47
This commit is contained in:
Paul Schneider 2026-08-29 19:36:08 +01:00
commit 01e7ad715d
26 changed files with 199 additions and 31 deletions

View file

@ -44,4 +44,8 @@ jobs:
- name: Test
run: |
echo "🚀 Lancement des tests..."
cd /src/_src && dotnet test --verbosity normal && echo "✅ Success !" || echo "❌ Fail ($?)!"
cd /src/_src && dotnet test \
--verbosity normal \
--filter="Category!=Platform-Android" \
--logger "xunit;LogFileName=test-results.xml" \
&& echo "✅ Success !" || echo "❌ Fail ($?)!"

View file

@ -26,7 +26,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Test
run: dotnet test --no-build --verbosity normal
run: dotnet test --no-build --verbosity normal --filter="Category!=Platform-Android"
# 4. Build et Push de l'image de production finale
- name: Build and push production image
uses: docker/build-push-action@v7

View file

@ -12,7 +12,7 @@
### Fixed
* [Yavsc.Org][TODO] La forme de l'email de l'utilisateur est maintenant validée avant l'envoi du formulaire d'enregistrement
* [Yavsc.Org] (Ticket #45) La forme de l'email de l'utilisateur est maintenant validée avant l'envoi du formulaire d'enregistrement
## [1.0.8-rc6] - unstable

View file

@ -11,7 +11,7 @@
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>

View file

@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>

View file

@ -7,7 +7,7 @@
<Nullable>enable</Nullable>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<PropertyGroup>

View file

@ -12,6 +12,7 @@ namespace PostIt.Tests;
/// Skip conditions: the package is not installed on the connected device,
/// or no device is connected via adb.
/// </summary>
[Trait("Category", "Platform-Android")]
public class AndroidAppLaunchTests
{
private const string PackageName = "fr.pschneider.postit";
@ -23,8 +24,8 @@ public class AndroidAppLaunchTests
_output = output;
}
// https://twosixtech.com/blog/integrating-docker-and-adb/
// FIXME ala hosted shared resource adb server - [Fact]
// TODO https://twosixtech.com/blog/integrating-docker-and-adb/
[Fact]
public void PostIt_starts_and_draws_a_first_frame_on_the_emulator()
{
if (!IsPackageInstalledOnAnyDevice())

View file

@ -79,7 +79,7 @@ public class MainPageSaveTests
// whose Title is exactly what the user typed. The bug
// fails this assertion with Title == string.Empty.
Assert.NotEmpty(recorder.Calls);
var (method, path, body) = recorder.FirstCall;
var (method, path, body) = recorder.Calls[1];
Assert.Equal(HttpMethod.Post, method);
Assert.Equal("blogspot", path);
var sent = Assert.IsType<BlogPostDto>(body);

View file

@ -8,7 +8,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>

View file

@ -55,6 +55,21 @@ public class PostItViewModelTests
Assert.Equal("Hello", posts[0].Title);
}
[Fact]
public async Task TogglePublishCommand_uses_the_current_checked_state_without_inverting_it()
{
var api = new RecordingPublishApi();
var blog = new BlogApiClient(api, "http://localhost/");
var viewModel = new MainViewModel(blog);
viewModel.SelectedPost = new BlogPostDto { Id = 42, IsPublished = false };
await viewModel.SetPublishStateAsync(true);
Assert.True(api.LastPublishValue);
Assert.True(viewModel.DraftIsPublished);
}
/// <summary>Test fake that always throws if the API is invoked.</summary>
private sealed class ThrowingYavscApiClient : YavscApiClient
{
@ -103,4 +118,34 @@ public class PostItViewModelTests
return Task.FromResult(default(T)!);
}
}
private sealed class RecordingPublishApi : IYavscApiClient
{
public bool LastPublishValue { get; private set; }
public HttpClient Http { get; } = new();
public Task<T> CallAsync<T>(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
{
if (method == HttpMethod.Put && path.Contains("/publish", StringComparison.OrdinalIgnoreCase))
{
var publish = body?.GetType().GetProperty("publish")?.GetValue(body) is bool value && value;
LastPublishValue = publish;
}
return Task.FromResult(default(T)!);
}
public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
{
if (method == HttpMethod.Put && path.Contains("/publish", StringComparison.OrdinalIgnoreCase))
{
var publish = body?.GetType().GetProperty("publish")?.GetValue(body) is bool value && value;
LastPublishValue = publish;
}
return Task.CompletedTask;
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
}

View file

@ -5,7 +5,7 @@
<LangVersion>latest</LangVersion>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>

View file

@ -190,8 +190,7 @@ public partial class MainViewModel : ViewModelBase
/// overload; the dedicated endpoint keeps the wire
/// contract clean.</para>
/// </summary>
[RelayCommand]
internal async Task TogglePublishAsync()
public async Task SetPublishStateAsync(bool publish)
{
if (SelectedPost is null || SelectedPost.Id == 0)
{
@ -201,20 +200,29 @@ public partial class MainViewModel : ViewModelBase
await ExecuteAsync(async () =>
{
var desired = !DraftIsPublished;
await BlogClient!.SetPublishAsync(SelectedPost.Id, desired);
DraftIsPublished = desired;
// The checkbox updates DraftIsPublished before the command is
// executed. Using the current bound value avoids the
// double-toggle bug in which the UI has already flipped the
// state and the command flips it again.
await BlogClient!.SetPublishAsync(SelectedPost.Id, publish);
DraftIsPublished = publish;
// Mirror into the selected post so a subsequent
// RefreshPostsAsync() doesn't blow away the
// locally flipped state until the round-trip
// re-hydrates it.
SelectedPost.IsPublished = desired;
StatusMessage = desired
SelectedPost.IsPublished = publish;
StatusMessage = publish
? $"Billet {SelectedPost.Id} publié."
: $"Billet {SelectedPost.Id} remis en brouillon.";
});
}
[RelayCommand]
internal async Task TogglePublishAsync()
{
await SetPublishStateAsync(DraftIsPublished);
}
/// <summary>
/// DEV ONLY: open the signature capture page. The production
/// entry point is a SignalR push from Yavsc.Org ("devis

View file

@ -12,8 +12,8 @@ namespace Yavsc.ViewModels.Account
public string UserName { get; set; }
[Required()]
[StringLength( maximumLength:102, MinimumLength = 5)]
// [EmailAddress]
[StringLength(maximumLength: 102, MinimumLength = 5)]
[EmailAddress(ErrorMessage = "L'adresse e-mail n'est pas valide.")]
[Display(Name = "Email", Description = "E-Mail")]
public string Email { get; set; }

View file

@ -11,7 +11,7 @@
<LangVersion>latest</LangVersion>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
</Project>

View file

@ -17,7 +17,7 @@
<Library>true</Library>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>

View file

@ -7,7 +7,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>

View file

@ -9,7 +9,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>

View file

@ -8,7 +8,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>

View file

@ -1,8 +1,19 @@
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using MailKit.Net.Smtp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using MimeKit;
using Yavsc.Interface;
using Yavsc.Interfaces;
using Yavsc.Models.Relationship;
using Yavsc.Org.Tests.Fakes;
using Yavsc.Services;
using Yavsc.Settings;
using Yavsc.ViewModels.Account;
namespace Yavsc.Org.Tests
{
@ -56,5 +67,96 @@ namespace Yavsc.Org.Tests
Assert.Equal(_serverFixture.SiteSettings.Owner.EMail, client.LastSentMessage?.To.Mailboxes.First().Address);
}
[Fact]
public void RegisterModel_rejects_invalid_email_format()
{
var model = new RegisterModel
{
UserName = "alice",
Email = "this is not an email",
Password = "Password123!",
ConfirmPassword = "Password123!"
};
var results = new List<ValidationResult>();
var valid = Validator.TryValidateObject(
model,
new ValidationContext(model),
results,
validateAllProperties: true);
Assert.False(valid);
Assert.Contains(results, r => r.MemberNames.Contains(nameof(RegisterModel.Email)));
}
[Fact]
public async Task SendEmailAsync_ignores_smtp_recipient_rejection()
{
var sender = new MailSender(
Options.Create(new SiteSettings
{
Title = "Test",
Authority = "example.com",
Owner = new StaticContact { Name = "Test Owner", EMail = "owner@example.com" }
}),
Options.Create(new SmtpSettings
{
Host = "smtp.test.local",
Port = 465,
UserName = "test-user",
Password = "secret"
}),
NullLoggerFactory.Instance,
new TestStringLocalizer(),
new RejectingSmtpClientFactory());
var result = await sender.SendEmailAsync(
"Alice",
"contact@pschneider.fr",
"Welcome",
"hello");
Assert.Equal(string.Empty, result);
}
private sealed class RejectingSmtpClientFactory : ISmtpClientFactory
{
public Yavsc.Interfaces.ISmtpClient CreateClient() => new RejectingSmtpClient();
}
private sealed class RejectingSmtpClient : Yavsc.Interfaces.ISmtpClient
{
public int Timeout { get; set; }
public void Connect(string host, int port, MailKit.Security.SecureSocketOptions options) { }
public void Authenticate(string userName, string password) { }
public Task SendAsync(MimeMessage message, CancellationToken cancellationToken = default)
{
throw new SmtpCommandException(
SmtpErrorCode.RecipientNotAccepted,
SmtpStatusCode.MailboxUnavailable,
"Recipient address rejected: User unknown in local recipient table");
}
public void Disconnect(bool quit) { }
public void Dispose() { }
}
private sealed class TestStringLocalizer : IStringLocalizer<MailSender>
{
public LocalizedString this[string name] => new(name, name);
public LocalizedString this[string name, params object[] arguments] => new(name, string.Format(CultureInfo.InvariantCulture, name, arguments));
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
=> Enumerable.Empty<LocalizedString>();
public LocalizedString GetString(string name)
=> new(name, name);
public LocalizedString GetString(string name, params object[] arguments)
=> new(name, string.Format(CultureInfo.InvariantCulture, name, arguments));
public IStringLocalizer WithCulture(CultureInfo culture)
=> this;
}
}
}

View file

@ -11,7 +11,7 @@
<RunSettingsFilePath>$(MSBuildProjectDirectory)\test.runsettings</RunSettingsFilePath>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>

View file

@ -564,6 +564,8 @@ IHtmlLocalizerFactory htmlLocalizerFactory,
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterModel model)
{
model.Email = model.Email?.Trim();
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.UserName, Email = model.Email };

View file

@ -9,7 +9,7 @@
<RepositoryUrl>https://github.com/pazof/yavsc</RepositoryUrl>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>

View file

@ -1,3 +1,4 @@
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -114,7 +115,7 @@ namespace Yavsc.Services
msg.MessageId = MimeKit.Utils.MimeUtils.GenerateMessageId(
siteSettings.Authority
);
using ISmtpClient sc = _smtpClientFactory.CreateClient();
using Yavsc.Interfaces.ISmtpClient sc = _smtpClientFactory.CreateClient();
{
sc.Timeout = 30000;
sc.Connect(
@ -139,6 +140,11 @@ namespace Yavsc.Services
logger.LogError(ex, "Refusing to send email because the recipient or sender address is malformed. To={To}, From={From}", email, siteSettings.Owner.EMail);
return string.Empty;
}
catch (SmtpCommandException ex)
{
logger.LogError(ex, "SMTP rejected the recipient or sender address. To={To}, Subject={Subject}, Status={Status}, Error={Error}", email, subject, ex.StatusCode, ex.Message);
return string.Empty;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to send email. To={To}, Subject={Subject}", email, subject);

View file

@ -9,7 +9,7 @@
<Library>true</Library>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>

View file

@ -14,7 +14,7 @@
-->
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>

View file

@ -7,7 +7,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<InformationalVersion>1.1.0-beta.1+172.Branch.release-1.0.8-rc6.Sha.c5941e04ced8547b1a25e8c43eb24ddef608b428</InformationalVersion>
<InformationalVersion>1.1.0-beta.1+177.Branch.release-1.0.8-rc7.Sha.1b237fa5404368bc891ac6c599fc3920bbf83c1c</InformationalVersion>
<Version>1.1.0-beta.1</Version>
</PropertyGroup>
<ItemGroup>