Skip to content

Commit ad37f3e

Browse files
committed
This Aims at fixing all issues with Relations.
- Changed the return time of `ConvertToJSON` to return "object" as we now use that one method to pass both primitive and complex datatypes like Dictionnary for Relations. - Implemented ConvertToJSON in ParseRelationOperations which now fixes Relations completely (Before, the related object would save but its pointer was never referenced)
1 parent 9573619 commit ad37f3e

18 files changed

+430
-87
lines changed

Parse.Tests/EncoderTests.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,6 @@ public void TestEncodeBytes()
7676
Assert.AreEqual(Convert.ToBase64String(new byte[] { 1, 2, 3, 4 }), value["base64"]);
7777
}
7878

79-
[TestMethod]
80-
public void TestEncodeParseObjectWithNoObjectsEncoder()
81-
{
82-
ParseObject obj = new ParseObject("Corgi");
83-
84-
Assert.ThrowsException<ArgumentException>(() => NoObjectsEncoder.Instance.Encode(obj, Client));
85-
}
8679

8780
[TestMethod]
8881
public void TestEncodeParseObjectWithPointerOrLocalIdEncoder()

Parse.Tests/RelationTests.cs

Lines changed: 349 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,85 @@
1+
using System;
12
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Linq;
5+
using System.Threading.Tasks;
26
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
using Moq;
8+
using Parse.Abstractions.Infrastructure.Control;
9+
using Parse.Abstractions.Infrastructure;
310
using Parse.Abstractions.Internal;
11+
using Parse.Abstractions.Platform.Objects;
412
using Parse.Infrastructure;
13+
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
14+
using Parse.Platform.Objects;
15+
using System.Threading;
16+
using Parse.Abstractions.Platform.Users;
517

618
namespace Parse.Tests;
719

820
[TestClass]
921
public class RelationTests
1022
{
23+
[ParseClassName("TestObject")]
24+
private class TestObject : ParseObject { }
25+
26+
[ParseClassName("Friend")]
27+
private class Friend : ParseObject { }
28+
29+
private ParseClient Client { get; set; }
30+
31+
[TestInitialize]
32+
public void SetUp()
33+
{
34+
// Initialize the client and ensure the instance is set
35+
Client = new ParseClient(new ServerConnectionData { Test = true });
36+
Client.Publicize();
37+
38+
// Register the test classes
39+
Client.RegisterSubclass(typeof(TestObject));
40+
Client.RegisterSubclass(typeof(Friend));
41+
Client.RegisterSubclass(typeof(ParseUser));
42+
Client.RegisterSubclass(typeof(ParseSession));
43+
Client.RegisterSubclass(typeof(ParseUser));
44+
45+
// **--- Mocking Setup ---**
46+
var hub = new MutableServiceHub(); // Use MutableServiceHub for mocking
47+
var mockUserController = new Mock<IParseUserController>();
48+
var mockObjectController = new Mock<IParseObjectController>();
49+
50+
// **Mock SignUpAsync for ParseUser:**
51+
mockUserController
52+
.Setup(controller => controller.SignUpAsync(
53+
It.IsAny<IObjectState>(),
54+
It.IsAny<IDictionary<string, IParseFieldOperation>>(),
55+
It.IsAny<IServiceHub>(),
56+
It.IsAny<CancellationToken>()))
57+
.ReturnsAsync(new MutableObjectState { ObjectId = "some0neTol4v4" }); // Predefined ObjectId for User
58+
59+
// **Mock SaveAsync for ParseObject (Friend objects):**
60+
int objectSaveCounter = 1; // Counter for Friend ObjectIds
61+
mockObjectController
62+
.Setup(controller => controller.SaveAsync(
63+
It.IsAny<IObjectState>(),
64+
It.IsAny<IDictionary<string, IParseFieldOperation>>(),
65+
It.IsAny<string>(),
66+
It.IsAny<IServiceHub>(),
67+
It.IsAny<CancellationToken>()))
68+
.ReturnsAsync(() => // Use a lambda to generate different ObjectIds for each Friend
69+
{
70+
return new MutableObjectState { ObjectId = $"mockFriendObjectId{objectSaveCounter++}" };
71+
});
72+
73+
// **Inject Mocks into ServiceHub:**
74+
hub.UserController = mockUserController.Object;
75+
hub.ObjectController = mockObjectController.Object;
76+
//(Client.Services as ServiceHub)..ReplaceWith(hub); // Replace the Client's ServiceHub with the MutableServiceHub
77+
78+
}
79+
80+
[TestCleanup]
81+
public void TearDown() => (Client.Services as ServiceHub).Reset();
82+
1183
[TestMethod]
1284
public void TestRelationQuery()
1385
{
@@ -24,4 +96,280 @@ public void TestRelationQuery()
2496

2597
Assert.AreEqual("child", encoded["redirectClassNameForKey"]);
2698
}
27-
}
99+
100+
[TestMethod]
101+
[Description("Tests AddRelationToUserAsync throws exception when user is null")] // Mock difficulty: 1
102+
public async Task AddRelationToUserAsync_ThrowsException_WhenUserIsNull()
103+
{
104+
105+
var relatedObjects = new List<ParseObject>
106+
{
107+
new ParseObject("Friend", Client.Services) { ["name"] = "Friend1" }
108+
};
109+
110+
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => UserManagement.AddRelationToUserAsync(null, "friends", relatedObjects));
111+
112+
}
113+
[TestMethod]
114+
[Description("Tests AddRelationToUserAsync throws exception when relationfield is null")] // Mock difficulty: 1
115+
public async Task AddRelationToUserAsync_ThrowsException_WhenRelationFieldIsNull()
116+
{
117+
var user = new ParseUser() { Username = "TestUser", Password = "TestPass", Services = Client.Services };
118+
await user.SignUpAsync();
119+
var relatedObjects = new List<ParseObject>
120+
{
121+
new ParseObject("Friend", Client.Services) { ["name"] = "Friend1" }
122+
};
123+
await Assert.ThrowsExceptionAsync<ArgumentException>(() => UserManagement.AddRelationToUserAsync(user, null, relatedObjects));
124+
}
125+
126+
[TestMethod]
127+
[Description("Tests UpdateUserRelationAsync throws exception when user is null")] // Mock difficulty: 1
128+
public async Task UpdateUserRelationAsync_ThrowsException_WhenUserIsNull()
129+
{
130+
var relatedObjectsToAdd = new List<ParseObject>
131+
{
132+
new ParseObject("Friend", Client.Services) { ["name"] = "Friend1" }
133+
};
134+
var relatedObjectsToRemove = new List<ParseObject>
135+
{
136+
new ParseObject("Friend", Client.Services) { ["name"] = "Friend2" }
137+
};
138+
139+
140+
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => UserManagement.UpdateUserRelationAsync(null, "friends", relatedObjectsToAdd, relatedObjectsToRemove));
141+
}
142+
[TestMethod]
143+
[Description("Tests UpdateUserRelationAsync throws exception when relationfield is null")] // Mock difficulty: 1
144+
public async Task UpdateUserRelationAsync_ThrowsException_WhenRelationFieldIsNull()
145+
{
146+
var user = new ParseUser() { Username = "TestUser", Password = "TestPass", Services = Client.Services };
147+
await user.SignUpAsync();
148+
149+
var relatedObjectsToAdd = new List<ParseObject>
150+
{
151+
new ParseObject("Friend", Client.Services) { ["name"] = "Friend1" }
152+
};
153+
var relatedObjectsToRemove = new List<ParseObject>
154+
{
155+
new ParseObject("Friend", Client.Services) { ["name"] = "Friend2" }
156+
};
157+
158+
159+
await Assert.ThrowsExceptionAsync<ArgumentException>(() => UserManagement.UpdateUserRelationAsync(user, null, relatedObjectsToAdd, relatedObjectsToRemove));
160+
}
161+
[TestMethod]
162+
[Description("Tests DeleteUserRelationAsync throws exception when user is null")] // Mock difficulty: 1
163+
public async Task DeleteUserRelationAsync_ThrowsException_WhenUserIsNull()
164+
{
165+
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => UserManagement.DeleteUserRelationAsync(null, "friends"));
166+
}
167+
[TestMethod]
168+
[Description("Tests DeleteUserRelationAsync throws exception when relationfield is null")] // Mock difficulty: 1
169+
public async Task DeleteUserRelationAsync_ThrowsException_WhenRelationFieldIsNull()
170+
{
171+
var user = new ParseUser() { Username = "TestUser", Password = "TestPass", Services = Client.Services };
172+
await user.SignUpAsync();
173+
174+
await Assert.ThrowsExceptionAsync<ArgumentException>(() => UserManagement.DeleteUserRelationAsync(user, null));
175+
}
176+
[TestMethod]
177+
[Description("Tests GetUserRelationsAsync throws exception when user is null")] // Mock difficulty: 1
178+
public async Task GetUserRelationsAsync_ThrowsException_WhenUserIsNull()
179+
{
180+
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => UserManagement.GetUserRelationsAsync(null, "friends"));
181+
}
182+
[TestMethod]
183+
[Description("Tests GetUserRelationsAsync throws exception when relationfield is null")] // Mock difficulty: 1
184+
public async Task GetUserRelationsAsync_ThrowsException_WhenRelationFieldIsNull()
185+
{
186+
var user = new ParseUser() { Username = "TestUser", Password = "TestPass", Services = Client.Services };
187+
await user.SignUpAsync();
188+
189+
await Assert.ThrowsExceptionAsync<ArgumentException>(() => UserManagement.GetUserRelationsAsync(user, null));
190+
}
191+
192+
193+
194+
[TestMethod]
195+
[Description("Tests that AddRelationToUserAsync throws when a related object is unsaved")]
196+
public async Task AddRelationToUserAsync_ThrowsException_WhenRelatedObjectIsUnsaved()
197+
{
198+
// Arrange: Create and sign up a test user.
199+
var user = new ParseUser() { Username = "TestUser", Password = "TestPass", Services = Client.Services };
200+
await user.SignUpAsync();
201+
202+
// Create an unsaved Friend object (do NOT call SaveAsync).
203+
var unsavedFriend = new ParseObject("Friend", Client.Services) { ["name"] = "UnsavedFriend" };
204+
var relatedObjects = new List<ParseObject> { unsavedFriend };
205+
206+
// Act & Assert: Expect an exception when trying to add an unsaved object.
207+
await Assert.ThrowsExceptionAsync<ArgumentException>(() =>
208+
UserManagement.AddRelationToUserAsync(user, "friends", relatedObjects));
209+
}
210+
211+
212+
213+
}
214+
215+
public static class UserManagement
216+
{
217+
public static async Task AddRelationToUserAsync(ParseUser user, string relationField, IList<ParseObject> relatedObjects)
218+
{
219+
if (user == null)
220+
{
221+
throw new ArgumentNullException(nameof(user), "User must not be null.");
222+
}
223+
224+
if (string.IsNullOrEmpty(relationField))
225+
{
226+
throw new ArgumentException("Relation field must not be null or empty.", nameof(relationField));
227+
}
228+
229+
if (relatedObjects == null || relatedObjects.Count == 0)
230+
{
231+
Debug.WriteLine("No objects provided to add to the relation.");
232+
return;
233+
}
234+
235+
var relation = user.GetRelation<ParseObject>(relationField);
236+
237+
foreach (var obj in relatedObjects)
238+
{
239+
relation.Add(obj);
240+
}
241+
242+
await user.SaveAsync();
243+
Debug.WriteLine($"Added {relatedObjects.Count} objects to the '{relationField}' relation for user '{user.Username}'.");
244+
}
245+
public static async Task UpdateUserRelationAsync(ParseUser user, string relationField, IList<ParseObject> toAdd, IList<ParseObject> toRemove)
246+
{
247+
if (user == null)
248+
{
249+
throw new ArgumentNullException(nameof(user), "User must not be null.");
250+
}
251+
252+
if (string.IsNullOrEmpty(relationField))
253+
{
254+
throw new ArgumentException("Relation field must not be null or empty.", nameof(relationField));
255+
}
256+
257+
var relation = user.GetRelation<ParseObject>(relationField);
258+
259+
// Add objects to the relation
260+
if (toAdd != null && toAdd.Count > 0)
261+
{
262+
foreach (var obj in toAdd)
263+
{
264+
relation.Add(obj);
265+
}
266+
Debug.WriteLine($"Added {toAdd.Count} objects to the '{relationField}' relation.");
267+
}
268+
269+
// Remove objects from the relation
270+
if (toRemove != null && toRemove.Count > 0)
271+
{
272+
273+
foreach (var obj in toRemove)
274+
{
275+
relation.Remove(obj);
276+
}
277+
Debug.WriteLine($"Removed {toRemove.Count} objects from the '{relationField}' relation.");
278+
}
279+
280+
await user.SaveAsync();
281+
}
282+
public static async Task DeleteUserRelationAsync(ParseUser user, string relationField)
283+
{
284+
if (user == null)
285+
{
286+
throw new ArgumentNullException(nameof(user), "User must not be null.");
287+
}
288+
289+
if (string.IsNullOrEmpty(relationField))
290+
{
291+
throw new ArgumentException("Relation field must not be null or empty.", nameof(relationField));
292+
}
293+
294+
var relation = user.GetRelation<ParseObject>(relationField);
295+
var relatedObjects = await relation.Query.FindAsync();
296+
297+
298+
foreach (var obj in relatedObjects)
299+
{
300+
relation.Remove(obj);
301+
}
302+
303+
await user.SaveAsync();
304+
Debug.WriteLine($"Removed all objects from the '{relationField}' relation for user '{user.Username}'.");
305+
}
306+
public static async Task ManageUserRelationsAsync(ParseClient client)
307+
{
308+
// Get the current user
309+
var user = await ParseClient.Instance.GetCurrentUser();
310+
311+
if (user == null)
312+
{
313+
Debug.WriteLine("No user is currently logged in.");
314+
return;
315+
}
316+
317+
const string relationField = "friends"; // Example relation field name
318+
319+
// Create related objects to add
320+
var relatedObjectsToAdd = new List<ParseObject>
321+
{
322+
new ParseObject("Friend", client.Services) { ["name"] = "Alice" },
323+
new ParseObject("Friend", client.Services) { ["name"] = "Bob" }
324+
};
325+
326+
// Save related objects to the server before adding to the relation
327+
foreach (var obj in relatedObjectsToAdd)
328+
{
329+
await obj.SaveAsync();
330+
}
331+
332+
// Add objects to the relation
333+
await AddRelationToUserAsync(user, relationField, relatedObjectsToAdd);
334+
335+
// Query the relation
336+
var relatedObjects = await GetUserRelationsAsync(user, relationField);
337+
338+
// Update the relation (add and remove objects)
339+
var relatedObjectsToRemove = new List<ParseObject> { relatedObjects[0] }; // Remove the first related object
340+
var newObjectsToAdd = new List<ParseObject>
341+
{
342+
new ParseObject("Friend", client.Services) { ["name"] = "Charlie" }
343+
};
344+
345+
foreach (var obj in newObjectsToAdd)
346+
{
347+
await obj.SaveAsync();
348+
}
349+
350+
await UpdateUserRelationAsync(user, relationField, newObjectsToAdd, relatedObjectsToRemove);
351+
352+
// Delete the relation
353+
// await DeleteUserRelationAsync(user, relationField);
354+
}
355+
public static async Task<IList<ParseObject>> GetUserRelationsAsync(ParseUser user, string relationField)
356+
{
357+
if (user == null)
358+
{
359+
throw new ArgumentNullException(nameof(user), "User must not be null.");
360+
}
361+
362+
if (string.IsNullOrEmpty(relationField))
363+
{
364+
throw new ArgumentException("Relation field must not be null or empty.", nameof(relationField));
365+
}
366+
367+
var relation = user.GetRelation<ParseObject>(relationField);
368+
369+
var results = await relation.Query.FindAsync();
370+
Debug.WriteLine($"Retrieved {results.Count()} objects from the '{relationField}' relation for user '{user.Username}'.");
371+
return results.ToList();
372+
}
373+
374+
}
375+

Parse/Abstractions/Infrastructure/IJsonConvertible.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ public interface IJsonConvertible
1111
/// Converts the object to a data structure that can be converted to JSON.
1212
/// </summary>
1313
/// <returns>An object to be JSONified.</returns>
14-
IDictionary<string, object> ConvertToJSON(IServiceHub serviceHub=default);
14+
15+
object ConvertToJSON(IServiceHub serviceHub=default);
1516
}

Parse/Infrastructure/Control/ParseAddOperation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public object Apply(object oldValue, string key)
4949
return result;
5050
}
5151

52-
public IDictionary<string, object> ConvertToJSON(IServiceHub serviceHub = default)
52+
public object ConvertToJSON(IServiceHub serviceHub = default)
5353
{
5454
// Convert the data into JSON-compatible structures
5555
var encodedObjects = Data.Select(EncodeForParse).ToList();

0 commit comments

Comments
 (0)