Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ParseObject Relations not working #407

Merged
merged 4 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions Parse.Tests/EncoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,6 @@ public void TestEncodeBytes()
Assert.AreEqual(Convert.ToBase64String(new byte[] { 1, 2, 3, 4 }), value["base64"]);
}

[TestMethod]
public void TestEncodeParseObjectWithNoObjectsEncoder()
{
ParseObject obj = new ParseObject("Corgi");

Assert.ThrowsException<ArgumentException>(() => NoObjectsEncoder.Instance.Encode(obj, Client));
}

[TestMethod]
public void TestEncodeParseObjectWithPointerOrLocalIdEncoder()
Expand Down
347 changes: 346 additions & 1 deletion Parse.Tests/RelationTests.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,84 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Parse.Abstractions.Infrastructure.Control;
using Parse.Abstractions.Infrastructure;
using Parse.Abstractions.Internal;
using Parse.Abstractions.Platform.Objects;
using Parse.Infrastructure;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Parse.Platform.Objects;
using System.Threading;
using Parse.Abstractions.Platform.Users;

namespace Parse.Tests;

[TestClass]
public class RelationTests
{
[ParseClassName("TestObject")]
private class TestObject : ParseObject { }

[ParseClassName("Friend")]
private class Friend : ParseObject { }

private ParseClient Client { get; set; }

[TestInitialize]
public void SetUp()
{
// Initialize the client and ensure the instance is set
Client = new ParseClient(new ServerConnectionData { Test = true });
Client.Publicize();

// Register the test classes
Client.RegisterSubclass(typeof(TestObject));
Client.RegisterSubclass(typeof(Friend));
Client.RegisterSubclass(typeof(ParseUser));
Client.RegisterSubclass(typeof(ParseSession));
Client.RegisterSubclass(typeof(ParseUser));

// **--- Mocking Setup ---**
var hub = new MutableServiceHub(); // Use MutableServiceHub for mocking
var mockUserController = new Mock<IParseUserController>();
var mockObjectController = new Mock<IParseObjectController>();

// **Mock SignUpAsync for ParseUser:**
mockUserController
.Setup(controller => controller.SignUpAsync(
It.IsAny<IObjectState>(),
It.IsAny<IDictionary<string, IParseFieldOperation>>(),
It.IsAny<IServiceHub>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new MutableObjectState { ObjectId = "some0neTol4v4" }); // Predefined ObjectId for User

// **Mock SaveAsync for ParseObject (Friend objects):**
int objectSaveCounter = 1; // Counter for Friend ObjectIds
mockObjectController
.Setup(controller => controller.SaveAsync(
It.IsAny<IObjectState>(),
It.IsAny<IDictionary<string, IParseFieldOperation>>(),
It.IsAny<string>(),
It.IsAny<IServiceHub>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(() => // Use a lambda to generate different ObjectIds for each Friend
{
return new MutableObjectState { ObjectId = $"mockFriendObjectId{objectSaveCounter++}" };
});

// **Inject Mocks into ServiceHub:**
hub.UserController = mockUserController.Object;
hub.ObjectController = mockObjectController.Object;

}

[TestCleanup]
public void TearDown() => (Client.Services as ServiceHub).Reset();

[TestMethod]
public void TestRelationQuery()
{
Expand All @@ -24,4 +95,278 @@ public void TestRelationQuery()

Assert.AreEqual("child", encoded["redirectClassNameForKey"]);
}
}

[TestMethod]
[Description("Tests AddRelationToUserAsync throws exception when user is null")] // Mock difficulty: 1
public async Task AddRelationToUserAsync_ThrowsException_WhenUserIsNull()
{

var relatedObjects = new List<ParseObject>
{
new ParseObject("Friend", Client.Services) { ["name"] = "Friend1" }
};

await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => UserManagement.AddRelationToUserAsync(null, "friends", relatedObjects));

}
[TestMethod]
[Description("Tests AddRelationToUserAsync throws exception when relationfield is null")] // Mock difficulty: 1
public async Task AddRelationToUserAsync_ThrowsException_WhenRelationFieldIsNull()
{
var user = new ParseUser() { Username = "TestUser", Password = "TestPass", Services = Client.Services };
await user.SignUpAsync();
var relatedObjects = new List<ParseObject>
{
new ParseObject("Friend", Client.Services) { ["name"] = "Friend1" }
};
await Assert.ThrowsExceptionAsync<ArgumentException>(() => UserManagement.AddRelationToUserAsync(user, null, relatedObjects));
}

[TestMethod]
[Description("Tests UpdateUserRelationAsync throws exception when user is null")] // Mock difficulty: 1
public async Task UpdateUserRelationAsync_ThrowsException_WhenUserIsNull()
{
var relatedObjectsToAdd = new List<ParseObject>
{
new ParseObject("Friend", Client.Services) { ["name"] = "Friend1" }
};
var relatedObjectsToRemove = new List<ParseObject>
{
new ParseObject("Friend", Client.Services) { ["name"] = "Friend2" }
};


await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => UserManagement.UpdateUserRelationAsync(null, "friends", relatedObjectsToAdd, relatedObjectsToRemove));
}
[TestMethod]
[Description("Tests UpdateUserRelationAsync throws exception when relationfield is null")] // Mock difficulty: 1
public async Task UpdateUserRelationAsync_ThrowsException_WhenRelationFieldIsNull()
{
var user = new ParseUser() { Username = "TestUser", Password = "TestPass", Services = Client.Services };
await user.SignUpAsync();

var relatedObjectsToAdd = new List<ParseObject>
{
new ParseObject("Friend", Client.Services) { ["name"] = "Friend1" }
};
var relatedObjectsToRemove = new List<ParseObject>
{
new ParseObject("Friend", Client.Services) { ["name"] = "Friend2" }
};


await Assert.ThrowsExceptionAsync<ArgumentException>(() => UserManagement.UpdateUserRelationAsync(user, null, relatedObjectsToAdd, relatedObjectsToRemove));
}
[TestMethod]
[Description("Tests DeleteUserRelationAsync throws exception when user is null")] // Mock difficulty: 1
public async Task DeleteUserRelationAsync_ThrowsException_WhenUserIsNull()
{
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => UserManagement.DeleteUserRelationAsync(null, "friends"));
}
[TestMethod]
[Description("Tests DeleteUserRelationAsync throws exception when relationfield is null")] // Mock difficulty: 1
public async Task DeleteUserRelationAsync_ThrowsException_WhenRelationFieldIsNull()
{
var user = new ParseUser() { Username = "TestUser", Password = "TestPass", Services = Client.Services };
await user.SignUpAsync();

await Assert.ThrowsExceptionAsync<ArgumentException>(() => UserManagement.DeleteUserRelationAsync(user, null));
}
[TestMethod]
[Description("Tests GetUserRelationsAsync throws exception when user is null")] // Mock difficulty: 1
public async Task GetUserRelationsAsync_ThrowsException_WhenUserIsNull()
{
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => UserManagement.GetUserRelationsAsync(null, "friends"));
}
[TestMethod]
[Description("Tests GetUserRelationsAsync throws exception when relationfield is null")] // Mock difficulty: 1
public async Task GetUserRelationsAsync_ThrowsException_WhenRelationFieldIsNull()
{
var user = new ParseUser() { Username = "TestUser", Password = "TestPass", Services = Client.Services };
await user.SignUpAsync();

await Assert.ThrowsExceptionAsync<ArgumentException>(() => UserManagement.GetUserRelationsAsync(user, null));
}



[TestMethod]
[Description("Tests that AddRelationToUserAsync throws when a related object is unsaved")]
public async Task AddRelationToUserAsync_ThrowsException_WhenRelatedObjectIsUnsaved()
{
// Arrange: Create and sign up a test user.
var user = new ParseUser() { Username = "TestUser", Password = "TestPass", Services = Client.Services };
await user.SignUpAsync();

// Create an unsaved Friend object (do NOT call SaveAsync).
var unsavedFriend = new ParseObject("Friend", Client.Services) { ["name"] = "UnsavedFriend" };
var relatedObjects = new List<ParseObject> { unsavedFriend };

// Act & Assert: Expect an exception when trying to add an unsaved object.
await Assert.ThrowsExceptionAsync<ArgumentException>(() =>
UserManagement.AddRelationToUserAsync(user, "friends", relatedObjects));
}



}

public static class UserManagement
{
public static async Task AddRelationToUserAsync(ParseUser user, string relationField, IList<ParseObject> relatedObjects)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user), "User must not be null.");
}

if (string.IsNullOrEmpty(relationField))
{
throw new ArgumentException("Relation field must not be null or empty.", nameof(relationField));
}

if (relatedObjects == null || relatedObjects.Count == 0)
{
Debug.WriteLine("No objects provided to add to the relation.");
return;
}

var relation = user.GetRelation<ParseObject>(relationField);

foreach (var obj in relatedObjects)
{
relation.Add(obj);
}

await user.SaveAsync();
Debug.WriteLine($"Added {relatedObjects.Count} objects to the '{relationField}' relation for user '{user.Username}'.");
}
public static async Task UpdateUserRelationAsync(ParseUser user, string relationField, IList<ParseObject> toAdd, IList<ParseObject> toRemove)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user), "User must not be null.");
}

if (string.IsNullOrEmpty(relationField))
{
throw new ArgumentException("Relation field must not be null or empty.", nameof(relationField));
}

var relation = user.GetRelation<ParseObject>(relationField);

// Add objects to the relation
if (toAdd != null && toAdd.Count > 0)
{
foreach (var obj in toAdd)
{
relation.Add(obj);
}
Debug.WriteLine($"Added {toAdd.Count} objects to the '{relationField}' relation.");
}

// Remove objects from the relation
if (toRemove != null && toRemove.Count > 0)
{

foreach (var obj in toRemove)
{
relation.Remove(obj);
}
Debug.WriteLine($"Removed {toRemove.Count} objects from the '{relationField}' relation.");
}

await user.SaveAsync();
}
public static async Task DeleteUserRelationAsync(ParseUser user, string relationField)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user), "User must not be null.");
}

if (string.IsNullOrEmpty(relationField))
{
throw new ArgumentException("Relation field must not be null or empty.", nameof(relationField));
}

var relation = user.GetRelation<ParseObject>(relationField);
var relatedObjects = await relation.Query.FindAsync();


foreach (var obj in relatedObjects)
{
relation.Remove(obj);
}

await user.SaveAsync();
Debug.WriteLine($"Removed all objects from the '{relationField}' relation for user '{user.Username}'.");
}
public static async Task ManageUserRelationsAsync(ParseClient client)
{
// Get the current user
var user = await ParseClient.Instance.GetCurrentUser();

if (user == null)
{
Debug.WriteLine("No user is currently logged in.");
return;
}

const string relationField = "friends"; // Example relation field name

// Create related objects to add
var relatedObjectsToAdd = new List<ParseObject>
{
new ParseObject("Friend", client.Services) { ["name"] = "Alice" },
new ParseObject("Friend", client.Services) { ["name"] = "Bob" }
};

// Save related objects to the server before adding to the relation
foreach (var obj in relatedObjectsToAdd)
{
await obj.SaveAsync();
}

// Add objects to the relation
await AddRelationToUserAsync(user, relationField, relatedObjectsToAdd);

// Query the relation
var relatedObjects = await GetUserRelationsAsync(user, relationField);

// Update the relation (add and remove objects)
var relatedObjectsToRemove = new List<ParseObject> { relatedObjects[0] }; // Remove the first related object
var newObjectsToAdd = new List<ParseObject>
{
new ParseObject("Friend", client.Services) { ["name"] = "Charlie" }
};

foreach (var obj in newObjectsToAdd)
{
await obj.SaveAsync();
}

await UpdateUserRelationAsync(user, relationField, newObjectsToAdd, relatedObjectsToRemove);

}
public static async Task<IList<ParseObject>> GetUserRelationsAsync(ParseUser user, string relationField)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user), "User must not be null.");
}

if (string.IsNullOrEmpty(relationField))
{
throw new ArgumentException("Relation field must not be null or empty.", nameof(relationField));
}

var relation = user.GetRelation<ParseObject>(relationField);

var results = await relation.Query.FindAsync();
Debug.WriteLine($"Retrieved {results.Count()} objects from the '{relationField}' relation for user '{user.Username}'.");
return results.ToList();
}

}

3 changes: 2 additions & 1 deletion Parse/Abstractions/Infrastructure/IJsonConvertible.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ public interface IJsonConvertible
/// Converts the object to a data structure that can be converted to JSON.
/// </summary>
/// <returns>An object to be JSONified.</returns>
IDictionary<string, object> ConvertToJSON(IServiceHub serviceHub=default);

object ConvertToJSON(IServiceHub serviceHub=default);
}
2 changes: 1 addition & 1 deletion Parse/Infrastructure/Control/ParseAddOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public object Apply(object oldValue, string key)
return result;
}

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