Skip to content

Commit 9af640a

Browse files
authored
fix: ParseObject Relations not working (#407)
1 parent 9573619 commit 9af640a

18 files changed

+396
-64
lines changed

Parse.Tests/EncoderTests.cs

-7
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

+346-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,84 @@
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+
77+
}
78+
79+
[TestCleanup]
80+
public void TearDown() => (Client.Services as ServiceHub).Reset();
81+
1182
[TestMethod]
1283
public void TestRelationQuery()
1384
{
@@ -24,4 +95,278 @@ public void TestRelationQuery()
2495

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

Parse/Abstractions/Infrastructure/IJsonConvertible.cs

+2-1
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

+1-1
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)