mirror of
https://github.com/ElectronNET/Electron.NET.git
synced 2026-09-21 22:45:20 +00:00
* fix: correct ReleaseNotes deserialization for AutoUpdater (fixes #1039) - TypeScript normalize() now maps string releaseNotes to { note } objects and also handles arrays of strings (both cases were broken in PR #1041) - Added ReleaseNotesConverter (JsonConverter<ReleaseNoteInfo[]>) as defensive C# layer that handles all shapes: null, string, string[], object[] - Added [JsonConverter] attribute on UpdateInfo.ReleaseNotes - Added unit tests (no Electron required) covering all four input shapes * chore: sync generated autoUpdater.js and .map with updated TypeScript source * refactor: address Copilot PR review comments - Fix namespace: ElectronNET.Converter -> ElectronNET.API.Converter - Replace JsonDocument.ParseValue() with JsonSerializer.Deserialize<ReleaseNoteInfo>() for cleaner, allocation-free object array parsing - Fix Write(): empty ReleaseNoteInfo[] now serializes as [] instead of null - Use Array.Empty<ReleaseNoteInfo>() in UpdateInfo default initializer - Add tests: Serialize_WithEmptyReleaseNotes and Serialize_WithNullReleaseNotes
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
namespace ElectronNET.API.Entities
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using ElectronNET.API.Converter;
|
||||
|
||||
namespace ElectronNET.API.Entities
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
@@ -24,7 +28,8 @@
|
||||
/// <summary>
|
||||
/// Gets or sets the release notes.
|
||||
/// </summary>
|
||||
public ReleaseNoteInfo[] ReleaseNotes { get; set; } = new ReleaseNoteInfo[0];
|
||||
[JsonConverter(typeof(ReleaseNotesConverter))]
|
||||
public ReleaseNoteInfo[] ReleaseNotes { get; set; } = Array.Empty<ReleaseNoteInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the release date.
|
||||
|
||||
91
src/ElectronNET.API/Converter/ReleaseNotesConverter.cs
Normal file
91
src/ElectronNET.API/Converter/ReleaseNotesConverter.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using ElectronNET.API.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ElectronNET.API.Converter;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the polymorphic shape of releaseNotes coming from electron-builder.
|
||||
/// Depending on the updater.fullChangelog setting, electron-builder sends:
|
||||
/// - null → when there are no notes
|
||||
/// - "some string" → plain string (FullChangelog = false, default)
|
||||
/// - ["note A", "note B"] → array of strings (after broken normalize in older TS)
|
||||
/// - [{ version, note }, ...] → array of objects (FullChangelog = true)
|
||||
/// All forms are normalised to ReleaseNoteInfo[] so the C# model stays clean.
|
||||
/// See: https://github.com/ElectronNET/Electron.NET/issues/1039
|
||||
/// </summary>
|
||||
public class ReleaseNotesConverter : JsonConverter<ReleaseNoteInfo[]>
|
||||
{
|
||||
// Ensure the converter is called even when the JSON token is null,
|
||||
// so we can return an empty array instead of null.
|
||||
public override bool HandleNull => true;
|
||||
|
||||
public override ReleaseNoteInfo[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonTokenType.Null:
|
||||
return Array.Empty<ReleaseNoteInfo>();
|
||||
|
||||
case JsonTokenType.String:
|
||||
// Plain string: "Some release notes"
|
||||
return new[] { new ReleaseNoteInfo { Note = reader.GetString() } };
|
||||
|
||||
case JsonTokenType.StartArray:
|
||||
var list = new List<ReleaseNoteInfo>();
|
||||
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
// Array of strings: ["Note A", "Note B"]
|
||||
list.Add(new ReleaseNoteInfo { Note = reader.GetString() });
|
||||
}
|
||||
else if (reader.TokenType == JsonTokenType.StartObject)
|
||||
{
|
||||
// Array of objects: [{ "version": "1.0", "note": "..." }]
|
||||
var entry = JsonSerializer.Deserialize<ReleaseNoteInfo>(ref reader, options) ?? new ReleaseNoteInfo();
|
||||
list.Add(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Skip();
|
||||
}
|
||||
}
|
||||
return list.ToArray();
|
||||
|
||||
default:
|
||||
throw new JsonException($"Unexpected token {reader.TokenType} when reading releaseNotes.");
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ReleaseNoteInfo[] value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.Length == 0)
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
writer.WriteEndArray();
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteStartArray();
|
||||
foreach (var item in value)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
if (item.Version is not null)
|
||||
{
|
||||
writer.WriteString("version", item.Version);
|
||||
}
|
||||
writer.WriteString("note", item.Note);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,10 @@ const electron_updater_1 = require("electron-updater");
|
||||
let electronSocket;
|
||||
function normalize(updateInfo) {
|
||||
if (typeof updateInfo?.releaseNotes === "string") {
|
||||
updateInfo.releaseNotes = [updateInfo.releaseNotes];
|
||||
updateInfo.releaseNotes = [{ note: updateInfo.releaseNotes }];
|
||||
}
|
||||
else if (Array.isArray(updateInfo?.releaseNotes)) {
|
||||
updateInfo.releaseNotes = updateInfo.releaseNotes.map((entry) => typeof entry === "string" ? { note: entry } : entry);
|
||||
}
|
||||
}
|
||||
module.exports = (socket) => {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,7 +5,11 @@ let electronSocket: Socket;
|
||||
|
||||
function normalize(updateInfo) {
|
||||
if (typeof updateInfo?.releaseNotes === "string") {
|
||||
updateInfo.releaseNotes = [updateInfo.releaseNotes];
|
||||
updateInfo.releaseNotes = [{ note: updateInfo.releaseNotes }];
|
||||
} else if (Array.isArray(updateInfo?.releaseNotes)) {
|
||||
updateInfo.releaseNotes = updateInfo.releaseNotes.map((entry) =>
|
||||
typeof entry === "string" ? { note: entry } : entry,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
namespace ElectronNET.IntegrationTests.Tests
|
||||
{
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ElectronNET.API.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for UpdateInfo JSON deserialization.
|
||||
/// Tests the fix for issue #1039: releaseNotes arrives as string or string[] from electron-builder
|
||||
/// when FullChangelog is false (default), but the C# model expects ReleaseNoteInfo[].
|
||||
/// No Electron runtime is required for these tests.
|
||||
/// </summary>
|
||||
public class UpdateInfoSerializationTests
|
||||
{
|
||||
// camelCase + ignore null — mirrors ElectronJson.Options used in production
|
||||
private static readonly JsonSerializerOptions Options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
// electron-builder sends a plain string when FullChangelog = false (default)
|
||||
[Fact]
|
||||
public void Deserialize_WithStringReleaseNotes_ShouldConvertToSingleEntry()
|
||||
{
|
||||
var json = """{"version":"1.2.3","releaseNotes":"Some release notes"}""";
|
||||
|
||||
var result = JsonSerializer.Deserialize<UpdateInfo>(json, Options);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.ReleaseNotes.Should().HaveCount(1);
|
||||
result.ReleaseNotes[0].Note.Should().Be("Some release notes");
|
||||
}
|
||||
|
||||
// After the (incorrect) TypeScript normalize: string → ["string"] which is an array of strings,
|
||||
// not an array of ReleaseNoteInfo objects. The C# model must handle this too.
|
||||
[Fact]
|
||||
public void Deserialize_WithArrayOfStringReleaseNotes_ShouldConvertToEntries()
|
||||
{
|
||||
var json = """{"version":"1.2.3","releaseNotes":["Note A","Note B"]}""";
|
||||
|
||||
var result = JsonSerializer.Deserialize<UpdateInfo>(json, Options);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.ReleaseNotes.Should().HaveCount(2);
|
||||
result.ReleaseNotes[0].Note.Should().Be("Note A");
|
||||
result.ReleaseNotes[1].Note.Should().Be("Note B");
|
||||
}
|
||||
|
||||
// When FullChangelog = true, electron-builder sends proper objects; this must keep working.
|
||||
[Fact]
|
||||
public void Deserialize_WithProperReleaseNoteObjects_ShouldDeserializeNormally()
|
||||
{
|
||||
var json = """{"version":"1.2.3","releaseNotes":[{"version":"1.2.3","note":"Proper note"}]}""";
|
||||
|
||||
var result = JsonSerializer.Deserialize<UpdateInfo>(json, Options);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.ReleaseNotes.Should().HaveCount(1);
|
||||
result.ReleaseNotes[0].Version.Should().Be("1.2.3");
|
||||
result.ReleaseNotes[0].Note.Should().Be("Proper note");
|
||||
}
|
||||
|
||||
// Null releaseNotes should result in an empty array (matching the default value).
|
||||
[Fact]
|
||||
public void Deserialize_WithNullReleaseNotes_ShouldReturnEmptyArray()
|
||||
{
|
||||
var json = """{"version":"1.2.3","releaseNotes":null}""";
|
||||
|
||||
var result = JsonSerializer.Deserialize<UpdateInfo>(json, Options);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.ReleaseNotes.Should().NotBeNull();
|
||||
result.ReleaseNotes.Should().BeEmpty();
|
||||
}
|
||||
|
||||
// Absent releaseNotes field should keep the default empty array.
|
||||
[Fact]
|
||||
public void Deserialize_WithMissingReleaseNotes_ShouldReturnEmptyArray()
|
||||
{
|
||||
var json = """{"version":"1.2.3"}""";
|
||||
|
||||
var result = JsonSerializer.Deserialize<UpdateInfo>(json, Options);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.ReleaseNotes.Should().NotBeNull();
|
||||
result.ReleaseNotes.Should().BeEmpty();
|
||||
}
|
||||
|
||||
// Empty array must serialize as [] not null, so round-trips and downstream
|
||||
// consumers don't receive unexpected null for a non-null array value.
|
||||
[Fact]
|
||||
public void Serialize_WithEmptyReleaseNotes_ShouldProduceEmptyArray()
|
||||
{
|
||||
var updateInfo = new UpdateInfo { Version = "1.2.3", ReleaseNotes = Array.Empty<ReleaseNoteInfo>() };
|
||||
|
||||
var json = JsonSerializer.Serialize(updateInfo, Options);
|
||||
|
||||
json.Should().Contain("\"releaseNotes\":[]");
|
||||
}
|
||||
|
||||
// Null value: with DefaultIgnoreCondition.WhenWritingNull the property is
|
||||
// omitted entirely at the serializer level (before Write() is called).
|
||||
[Fact]
|
||||
public void Serialize_WithNullReleaseNotes_ShouldOmitProperty()
|
||||
{
|
||||
var updateInfo = new UpdateInfo { Version = "1.2.3", ReleaseNotes = null };
|
||||
|
||||
var json = JsonSerializer.Serialize(updateInfo, Options);
|
||||
|
||||
json.Should().NotContain("releaseNotes");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user