Skip to content

Commit 1b952a7

Browse files
committed
Fix IssueStatus xml deserialization
1 parent ddacd56 commit 1b952a7

File tree

6 files changed

+273
-10
lines changed

6 files changed

+273
-10
lines changed

src/redmine-net-api/Types/Issue.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public sealed class Issue :
5959
/// Gets or sets the status.Possible values: open, closed, * to get open and closed issues, status id
6060
/// </summary>
6161
/// <value>The status.</value>
62-
public IdentifiableName Status { get; set; }
62+
public IssueStatus Status { get; set; }
6363

6464
/// <summary>
6565
/// Gets or sets the priority.
@@ -307,7 +307,7 @@ public override void ReadXml(XmlReader reader)
307307
case RedmineKeys.RELATIONS: Relations = reader.ReadElementContentAsCollection<IssueRelation>(); break;
308308
case RedmineKeys.SPENT_HOURS: SpentHours = reader.ReadElementContentAsNullableFloat(); break;
309309
case RedmineKeys.START_DATE: StartDate = reader.ReadElementContentAsNullableDateTime(); break;
310-
case RedmineKeys.STATUS: Status = new IdentifiableName(reader); break;
310+
case RedmineKeys.STATUS: Status = new IssueStatus(reader); break;
311311
case RedmineKeys.SUBJECT: Subject = reader.ReadElementContentAsString(); break;
312312
case RedmineKeys.TOTAL_ESTIMATED_HOURS: TotalEstimatedHours = reader.ReadElementContentAsNullableFloat(); break;
313313
case RedmineKeys.TOTAL_SPENT_HOURS: TotalSpentHours = reader.ReadElementContentAsNullableFloat(); break;
@@ -406,7 +406,7 @@ public override void ReadJson(JsonReader reader)
406406
case RedmineKeys.RELATIONS: Relations = reader.ReadAsCollection<IssueRelation>(); break;
407407
case RedmineKeys.SPENT_HOURS: SpentHours = (float?)reader.ReadAsDouble(); break;
408408
case RedmineKeys.START_DATE: StartDate = reader.ReadAsDateTime(); break;
409-
case RedmineKeys.STATUS: Status = new IdentifiableName(reader); break;
409+
case RedmineKeys.STATUS: Status = new IssueStatus(reader); break;
410410
case RedmineKeys.SUBJECT: Subject = reader.ReadAsString(); break;
411411
case RedmineKeys.TOTAL_ESTIMATED_HOURS: TotalEstimatedHours = (float?)reader.ReadAsDouble(); break;
412412
case RedmineKeys.TOTAL_SPENT_HOURS: TotalSpentHours = (float?)reader.ReadAsDouble(); break;

src/redmine-net-api/Types/IssueStatus.cs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,44 @@ namespace Redmine.Net.Api.Types
3030
/// </summary>
3131
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
3232
[XmlRoot(RedmineKeys.ISSUE_STATUS)]
33-
public sealed class IssueStatus : IdentifiableName, IEquatable<IssueStatus>
33+
public sealed class IssueStatus : IdentifiableName, IEquatable<IssueStatus>, ICloneable<IssueStatus>
3434
{
35+
public IssueStatus()
36+
{
37+
38+
}
39+
40+
/// <summary>
41+
///
42+
/// </summary>
43+
internal IssueStatus(int id, string name, bool isDefault = false, bool isClosed = false)
44+
{
45+
Id = id;
46+
Name = name;
47+
IsClosed = isClosed;
48+
IsDefault = isDefault;
49+
}
50+
51+
internal IssueStatus(XmlReader reader)
52+
{
53+
Initialize(reader);
54+
}
55+
56+
internal IssueStatus(JsonReader reader)
57+
{
58+
Initialize(reader);
59+
}
60+
61+
private void Initialize(XmlReader reader)
62+
{
63+
ReadXml(reader);
64+
}
65+
66+
private void Initialize(JsonReader reader)
67+
{
68+
ReadJson(reader);
69+
}
70+
3571
#region Properties
3672
/// <summary>
3773
/// Gets or sets a value indicating whether IssueStatus is default.
@@ -55,6 +91,16 @@ public sealed class IssueStatus : IdentifiableName, IEquatable<IssueStatus>
5591
/// <param name="reader"></param>
5692
public override void ReadXml(XmlReader reader)
5793
{
94+
if (reader.HasAttributes && reader.Name == "status")
95+
{
96+
Id = reader.ReadAttributeAsInt(RedmineKeys.ID);
97+
IsClosed = reader.ReadAttributeAsBoolean(RedmineKeys.IS_CLOSED);
98+
IsDefault = reader.ReadAttributeAsBoolean(RedmineKeys.IS_DEFAULT);
99+
Name = reader.GetAttribute(RedmineKeys.NAME);
100+
reader.Read();
101+
return;
102+
}
103+
58104
reader.Read();
59105
while (!reader.EOF)
60106
{
@@ -121,6 +167,22 @@ public bool Equals(IssueStatus other)
121167
&& IsDefault == other.IsDefault;
122168
}
123169

170+
/// <summary>
171+
///
172+
/// </summary>
173+
/// <param name="resetId"></param>
174+
/// <returns></returns>
175+
public new IssueStatus Clone(bool resetId)
176+
{
177+
return new IssueStatus
178+
{
179+
Id = Id,
180+
Name = Name,
181+
IsClosed = IsClosed,
182+
IsDefault = IsDefault
183+
};
184+
}
185+
124186
/// <summary>
125187
///
126188
/// </summary>

tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ private static Issue CreateSampleIssue()
9494
Id = 1,
9595
Project = new IdentifiableName(100, "Test Project"),
9696
Tracker = new IdentifiableName(200, "Bug"),
97-
Status = new IdentifiableName(300, "New"),
97+
Status = new IssueStatus(300, "New"),
9898
Priority = new IdentifiableName(400, "Normal"),
9999
Author = new IdentifiableName(500, "John Doe"),
100100
Subject = "Test Issue",

tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ private static Issue CreateSampleIssue()
8888
Id = 1,
8989
Project = new IdentifiableName { Id = 100, Name = "Test Project" },
9090
Tracker = new IdentifiableName { Id = 1, Name = "Bug" },
91-
Status = new IdentifiableName { Id = 1, Name = "New" },
91+
Status = new IssueStatus { Id = 1, Name = "New" },
9292
Priority = new IdentifiableName { Id = 1, Name = "Normal" },
9393
Author = new IdentifiableName { Id = 1, Name = "John Doe" },
9494
Subject = "Test Issue",
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System;
2+
using System.Linq;
3+
using Padi.DotNet.RedmineAPI.Tests.Infrastructure;
4+
using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures;
5+
using Xunit;
6+
7+
namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json;
8+
9+
[Collection(Constants.JsonRedmineSerializerCollection)]
10+
public class IssuesTests(JsonSerializerFixture fixture)
11+
{
12+
[Fact]
13+
public void Should_Deserialize_Issue_With_Watchers()
14+
{
15+
const string input = """
16+
{
17+
"issue": {
18+
"id": 5,
19+
"project": {
20+
"id": 1,
21+
"name": "Project-Test"
22+
},
23+
"tracker": {
24+
"id": 1,
25+
"name": "Bug"
26+
},
27+
"status": {
28+
"id": 1,
29+
"name": "New",
30+
"is_closed": true
31+
},
32+
"priority": {
33+
"id": 2,
34+
"name": "Normal"
35+
},
36+
"author": {
37+
"id": 90,
38+
"name": "Admin User"
39+
},
40+
"fixed_version": {
41+
"id": 2,
42+
"name": "version2"
43+
},
44+
"subject": "#380",
45+
"description": "",
46+
"start_date": "2025-04-28",
47+
"due_date": null,
48+
"done_ratio": 0,
49+
"is_private": false,
50+
"estimated_hours": null,
51+
"total_estimated_hours": null,
52+
"spent_hours": 0.0,
53+
"total_spent_hours": 0.0,
54+
"created_on": "2025-04-28T17:58:42Z",
55+
"updated_on": "2025-04-28T17:58:42Z",
56+
"closed_on": null,
57+
"watchers": [
58+
{
59+
"id": 91,
60+
"name": "Normal User"
61+
},
62+
{
63+
"id": 90,
64+
"name": "Admin User"
65+
}
66+
]
67+
}
68+
}
69+
""";
70+
71+
var output = fixture.Serializer.Deserialize<Redmine.Net.Api.Types.Issue>(input);
72+
73+
Assert.NotNull(output);
74+
Assert.Equal(5, output.Id);
75+
Assert.Equal("Project-Test", output.Project.Name);
76+
Assert.Equal(1, output.Project.Id);
77+
Assert.Equal("Bug", output.Tracker.Name);
78+
Assert.Equal(1, output.Tracker.Id);
79+
Assert.Equal("New", output.Status.Name);
80+
Assert.Equal(1, output.Status.Id);
81+
Assert.True(output.Status.IsClosed);
82+
Assert.False(output.Status.IsDefault);
83+
Assert.Equal(2, output.FixedVersion.Id);
84+
Assert.Equal("version2", output.FixedVersion.Name);
85+
Assert.Equal(new DateTime(2025, 4, 28), output.StartDate);
86+
Assert.Null(output.DueDate);
87+
Assert.Equal(0, output.DoneRatio);
88+
Assert.Null(output.EstimatedHours);
89+
Assert.Null(output.TotalEstimatedHours);
90+
91+
var watchers = output.Watchers.ToList();
92+
Assert.Equal(2, watchers.Count);
93+
94+
Assert.Equal(91, watchers[0].Id);
95+
Assert.Equal("Normal User", watchers[0].Name);
96+
Assert.Equal(90, watchers[1].Id);
97+
Assert.Equal("Admin User", watchers[1].Name);
98+
}
99+
}

tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,8 @@ This is not to be confused with another useful proposed feature that
117117
</issue>
118118
<issue>
119119
<id>4325</id>
120-
<journals type="array">
121-
</journals>
122-
<changesets type="array">
123-
</changesets>
120+
<journals type="array"/>
121+
<changesets type="array"/>
124122
<custom_fields type="array">
125123
<custom_field name="Affected version" id="1">
126124
<value>1.0.1</value>
@@ -134,6 +132,48 @@ This is not to be confused with another useful proposed feature that
134132
""";
135133

136134
var output = fixture.Serializer.DeserializeToPagedResults<Redmine.Net.Api.Types.Issue>(input);
135+
136+
Assert.NotNull(output);
137+
Assert.Equal(2, output.TotalItems);
138+
139+
var issues = output.Items.ToList();
140+
Assert.Equal(4326, issues[0].Id);
141+
Assert.Equal("Redmine", issues[0].Project.Name);
142+
Assert.Equal(1, issues[0].Project.Id);
143+
Assert.Equal("Feature", issues[0].Tracker.Name);
144+
Assert.Equal(2, issues[0].Tracker.Id);
145+
Assert.Equal("New", issues[0].Status.Name);
146+
Assert.Equal(1, issues[0].Status.Id);
147+
Assert.Equal("Normal", issues[0].Priority.Name);
148+
Assert.Equal(4, issues[0].Priority.Id);
149+
Assert.Equal("John Smith", issues[0].Author.Name);
150+
Assert.Equal(10106, issues[0].Author.Id);
151+
Assert.Equal("Email notifications", issues[0].Category.Name);
152+
Assert.Equal(9, issues[0].Category.Id);
153+
Assert.Contains("Aggregate Multiple Issue Changes for Email Notifications", issues[0].Subject);
154+
Assert.Contains("This is not to be confused with another useful proposed feature", issues[0].Description);
155+
Assert.Equal(new DateTime(2009, 12, 3), issues[0].StartDate);
156+
Assert.Null(issues[0].DueDate);
157+
Assert.Equal(0, issues[0].DoneRatio);
158+
Assert.Null(issues[0].EstimatedHours);
159+
160+
Assert.NotNull(issues[0].CustomFields);
161+
var issueCustomFields = issues[0].CustomFields.ToList();
162+
Assert.Equal(4, issueCustomFields.Count);
163+
164+
Assert.Equal(2,issueCustomFields[0].Id);
165+
Assert.Equal("Duplicate",issueCustomFields[0].Values[0].Info);
166+
Assert.False(issueCustomFields[0].Multiple);
167+
Assert.Equal("Resolution",issueCustomFields[0].Name);
168+
169+
Assert.NotNull(issues[1].CustomFields);
170+
issueCustomFields = issues[1].CustomFields.ToList();
171+
Assert.Equal(2, issueCustomFields.Count);
172+
173+
Assert.Equal(1,issueCustomFields[0].Id);
174+
Assert.Equal("1.0.1",issueCustomFields[0].Values[0].Info);
175+
Assert.False(issueCustomFields[0].Multiple);
176+
Assert.Equal("Affected version",issueCustomFields[0].Name);
137177
}
138178

139179
[Fact]
@@ -200,5 +240,67 @@ public void Should_Deserialize_Issue_With_Journals()
200240
Assert.Equal("8", details[0].NewValue);
201241

202242
}
243+
244+
[Fact]
245+
public void Should_Deserialize_Issue_With_Watchers()
246+
{
247+
const string input = """
248+
<?xml version="1.0" encoding="UTF-8"?>
249+
<issue>
250+
<id>5</id>
251+
<project id="1" name="Project-Test"/>
252+
<tracker id="1" name="Bug"/>
253+
<status id="1" name="New" is_closed="true"/>
254+
<priority id="2" name="Normal"/>
255+
<author id="90" name="Admin User"/>
256+
<fixed_version id="2" name="version2"/>
257+
<subject>#380</subject>
258+
<description></description>
259+
<start_date>2025-04-28</start_date>
260+
<due_date/>
261+
<done_ratio>0</done_ratio>
262+
<is_private>false</is_private>
263+
<estimated_hours/>
264+
<total_estimated_hours/>
265+
<spent_hours>0.0</spent_hours>
266+
<total_spent_hours>0.0</total_spent_hours>
267+
<created_on>2025-04-28T17:58:42Z</created_on>
268+
<updated_on>2025-04-28T17:58:42Z</updated_on>
269+
<closed_on/>
270+
<watchers type="array">
271+
<user id="91" name="Normal User"/>
272+
<user id="90" name="Admin User"/>
273+
</watchers>
274+
</issue>
275+
""";
276+
277+
var output = fixture.Serializer.Deserialize<Redmine.Net.Api.Types.Issue>(input);
278+
279+
Assert.NotNull(output);
280+
Assert.Equal(5, output.Id);
281+
Assert.Equal("Project-Test", output.Project.Name);
282+
Assert.Equal(1, output.Project.Id);
283+
Assert.Equal("Bug", output.Tracker.Name);
284+
Assert.Equal(1, output.Tracker.Id);
285+
Assert.Equal("New", output.Status.Name);
286+
Assert.Equal(1, output.Status.Id);
287+
Assert.True(output.Status.IsClosed);
288+
Assert.False(output.Status.IsDefault);
289+
Assert.Equal(2, output.FixedVersion.Id);
290+
Assert.Equal("version2", output.FixedVersion.Name);
291+
Assert.Equal(new DateTime(2025, 4, 28), output.StartDate);
292+
Assert.Null(output.DueDate);
293+
Assert.Equal(0, output.DoneRatio);
294+
Assert.Null(output.EstimatedHours);
295+
Assert.Null(output.TotalEstimatedHours);
296+
297+
var watchers = output.Watchers.ToList();
298+
Assert.Equal(2, watchers.Count);
299+
300+
Assert.Equal(91, watchers[0].Id);
301+
Assert.Equal("Normal User", watchers[0].Name);
302+
Assert.Equal(90, watchers[1].Id);
303+
Assert.Equal("Admin User", watchers[1].Name);
304+
}
203305
}
204306

0 commit comments

Comments
 (0)