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

Middle or low level APIs for executing operations. #125

Merged
merged 8 commits into from
Jan 3, 2024
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
108 changes: 102 additions & 6 deletions src/SenseNet.Client.IntegrationTests/ContentTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
using System.Net.WebSockets;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using SenseNet.Extensions.DependencyInjection;
using System.Threading.Channels;
using IdentityModel.Client;
using Newtonsoft.Json;
using SenseNet.Client.Security;

namespace SenseNet.Client.IntegrationTests;

Expand Down Expand Up @@ -600,4 +597,103 @@ await GetRepositoryCollection(
await repository.DeleteContentAsync(contentId, true, cancel).ConfigureAwait(false);
Assert.IsFalse(await repository.IsContentExistsAsync(path, cancel).ConfigureAwait(false));
}

/* ================================================================================================== OPERATIONS */

[TestMethod]
public async Task IT_Op_CallFunction()
{
var repository = await GetRepositoryCollection()
.GetRepositoryAsync("local", CancellationToken.None).ConfigureAwait(false);

// ACT
var request = new OperationRequest() {ContentId = 2, OperationName = "GetPermissions"};
var getPermissionsResponse = await repository.InvokeFunctionAsync<GetPermissionsResponse>(request, CancellationToken.None);

// ASSERT
Assert.AreEqual(2, getPermissionsResponse.Id);
Assert.AreEqual("/Root", getPermissionsResponse.Path);
Assert.AreEqual(true, getPermissionsResponse.Inherits);
Assert.IsNotNull(getPermissionsResponse.Entries);
Assert.IsTrue(getPermissionsResponse.Entries.Length > 2);
Assert.AreEqual("allow", getPermissionsResponse.Entries[0].Permissions.See.Value);
}
[TestMethod]
public async Task IT_Op_ExecuteAction()
{
var repository = await GetRepositoryCollection()
.GetRepositoryAsync("local", CancellationToken.None).ConfigureAwait(false);
var cancel = new CancellationTokenSource().Token;

var content = repository.CreateContent("/Root/Content", "SystemFolder", Guid.NewGuid().ToString());
await content.SaveAsync(cancel);
Assert.AreNotEqual(0, content.Id);
Assert.IsTrue(await repository.IsContentExistsAsync(content.Path, cancel));

// ACT
var postData = new {permanent = true};
var request = new OperationRequest() { ContentId = content.Id, OperationName = "Delete", PostData = postData};
await repository.InvokeActionAsync(request, CancellationToken.None);

// ASSERT
Assert.IsFalse(await repository.IsContentExistsAsync(content.Path, cancel));
}
[TestMethod]
public async Task IT_Op_ProcessOperationResponse()
{
var repository = await GetRepositoryCollection()
.GetRepositoryAsync("local", CancellationToken.None).ConfigureAwait(false);

var request = new OperationRequest
{
ContentId = 2,
OperationName = "GetPermissions"
};

// ACT
string? response = null;
await repository.ProcessOperationResponseAsync(request, HttpMethod.Get,
(r) => { response = r; }, CancellationToken.None);

// ASSERT
var getPermissionsResponse = JsonConvert.DeserializeObject<GetPermissionsResponse>(response);
Assert.AreEqual(2, getPermissionsResponse.Id);
Assert.AreEqual("/Root", getPermissionsResponse.Path);
Assert.AreEqual(true, getPermissionsResponse.Inherits);
Assert.IsNotNull(getPermissionsResponse.Entries);
Assert.IsTrue(getPermissionsResponse.Entries.Length > 2);
Assert.AreEqual("allow", getPermissionsResponse.Entries[0].Permissions.See.Value);
}
[TestMethod]
public async Task IT_Op_ProcessOperationResponse_Error()
{
var repository = await GetRepositoryCollection()
.GetRepositoryAsync("local", CancellationToken.None).ConfigureAwait(false);

var request = new OperationRequest()
{
ContentId = 2,
OperationName = "TestOperation"
};

// ACT
var isResponseProcessorCalled = false;
Exception? exception = null;
try
{
await repository.ProcessOperationResponseAsync(request, HttpMethod.Get,
(r) => { isResponseProcessorCalled = true; }, CancellationToken.None);
Assert.Fail("ClientException was not thrown.");
}
catch (ClientException e)
{
exception = e;
}

// ASSERT
Assert.IsFalse(isResponseProcessorCalled);
Assert.IsTrue(exception.Message.Contains("Operation not found"));
Assert.IsTrue(exception.Message.Contains("TestOperation"));
}

}
88 changes: 88 additions & 0 deletions src/SenseNet.Client.Tests/UnitTests/ODataRequestTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1162,4 +1162,92 @@ public void UploadRequest_ParentIdAndPath_PathIgnored()
}

#endregion

[TestMethod]
public void ActionRequest_Id()
{
var request = new OperationRequest
{
ContentId = 42,
OperationName = "Operation1",
};
Assert.AreEqual($"{_baseUri}/OData.svc/content(42)/Operation1?metadata=no",
request.ToODataRequest(null).ToString());
}
[TestMethod]
public void ActionRequest_IdAndPathAndParameters()
{
var request = new OperationRequest
{
ContentId = 42,
Path = "/Root/Content",
OperationName = "Operation1",
};
request.Parameters.Add("param1", "value1");
request.Parameters.Add("param2", "value2");
Assert.AreEqual($"{_baseUri}/OData.svc/content(42)/Operation1?metadata=no&param1=value1&param2=value2",
request.ToODataRequest(null).ToString());
}
[TestMethod]
public void ActionRequest_Path()
{
var request = new OperationRequest
{
Path = "/Root/Content",
OperationName = "Operation1",
};
Assert.AreEqual($"{_baseUri}/OData.svc/Root('Content')/Operation1?metadata=no",
request.ToODataRequest(null).ToString());
}
[TestMethod]
public void ActionRequest_PostData()
{
var request = new OperationRequest
{
Path = "/Root/Content",
OperationName = "Operation1",
PostData = new {A = "x", B = -13, C = true}
};
var oDataRequest = request.ToODataRequest(null);
Assert.AreSame(request.PostData, oDataRequest.PostData);
}
[TestMethod]
public void ActionRequest_NoContent()
{
Exception? exception = null;
try
{
var request = new OperationRequest { OperationName = "Operation1" };
var _ = request.ToODataRequest(null).ToString();
Assert.Fail("The expected exception was not thrown.");
}
catch (InvalidOperationException ex)
{
exception = ex;
}

Assert.AreEqual(
"Invalid request properties: Path must be provided.",
exception.Message);
}
[TestMethod]
public void ActionRequest_NoOperation()
{
Exception? exception = null;
try
{
var request = new OperationRequest { Path = "/Root/Content" };
var _ = request.ToODataRequest(null).ToString();
Assert.Fail("The expected exception was not thrown.");
}
catch (InvalidOperationException ex)
{
exception = ex;
}

Assert.AreEqual(
"Invalid request properties: OperationName must be provided.",
exception.Message);
}

}
2 changes: 1 addition & 1 deletion src/SenseNet.Client/ClientException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public class ErrorMessage
/// <summary>
/// Human readable error message.
/// </summary>
[JsonProperty(PropertyName = "value", Order = 1)]
[JsonProperty(PropertyName = "value", Order = 2)]
public string Value { get; internal set; }
}
/// <summary>
Expand Down
5 changes: 3 additions & 2 deletions src/SenseNet.Client/JsonHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ public static string Serialize(object obj)
/// <returns>The deserialized object from the JSON string.</returns>
public static T Deserialize<T>(string json)
{
if (IsErrorResponse(json, out var exception))
throw exception;
if(typeof(T) != typeof(ErrorResponse))
if (IsErrorResponse(json, out var exception))
throw exception;
return JsonConvert.DeserializeObject<T>(json, JsonHelper.JsonSerializerSettings);
}
/// <summary>
Expand Down
54 changes: 54 additions & 0 deletions src/SenseNet.Client/Repository/IRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -484,8 +484,62 @@ Task<string> GetResponseStringAsync(Uri uri, HttpMethod method, string postData,
/// <returns>A Task that represents the asynchronous operation.</returns>
Task DownloadAsync(DownloadRequest request, Func<Stream, StreamProperties, Task> responseProcessor, CancellationToken cancel);

/// <summary>
/// Calls a server function by the provided <paramref name="request"/>
/// and returns the response converted to the desired object.
/// </summary>
/// <typeparam name="T">Can be any existing class or struct.</typeparam>
/// <param name="request">The <see cref="OperationRequest"/> instance.</param>
/// <param name="cancel">The token to monitor for cancellation requests.</param>
/// <returns>A Task that represents the asynchronous operation and wraps the response object.</returns>
/// <exception cref="ClientException">Thrown if the <paramref name="request"/> is invalid
/// or not the requested operation is not an OData function.
/// Also thrown if the server returns an error object.</exception>
Task<T> InvokeFunctionAsync<T>(OperationRequest request, CancellationToken cancel);
/// <summary>
/// Executes a server action by the provided <paramref name="request"/>.
/// </summary>
/// <param name="request">The <see cref="OperationRequest"/> instance.</param>
/// <param name="cancel">The token to monitor for cancellation requests.</param>
/// <returns>A task that represents an asynchronous operation.</returns>
/// <exception cref="ClientException">Thrown if the <paramref name="request"/> is invalid
/// or not the requested operation is not an OData action.
/// Also thrown if the server returns an error object.</exception>
Task InvokeActionAsync(OperationRequest request, CancellationToken cancel);
/// <summary>
/// Executes a server action by the provided <paramref name="request"/>.
/// and returns the response converted to the desired object.
/// </summary>
/// <typeparam name="T">Can be any existing class or struct.</typeparam>
/// <param name="request">The <see cref="OperationRequest"/> instance.</param>
/// <param name="cancel">The token to monitor for cancellation requests.</param>
/// <returns>A Task that represents the asynchronous operation and wraps the response object.</returns>
/// <exception cref="ClientException">Thrown if the <paramref name="request"/> is invalid
/// or not the requested operation is not an OData action.
/// Also thrown if the server returns an error object.</exception>
Task<T> InvokeActionAsync<T>(OperationRequest request, CancellationToken cancel);

/* ============================================================================ LOW LEVEL API */

/// <summary>
/// Executes a server operation by the provided <paramref name="request"/>.
/// </summary>
/// The operation can be an OData action (POST) or function (GET).
/// See type of operations and parameters on the documentation pages: https://docs.sensenet.com
/// The response can be processed with the <paramref name="responseProcessor"/> callback.
/// The callback is called with a string parameter containing the raw response.
/// <remarks>
/// </remarks>
/// <param name="request">The <see cref="OperationRequest"/> instance.</param>
/// <param name="method">The HTTP method. Can be GET or POST.</param>
/// <param name="responseProcessor">Callback for processing the response.</param>
/// <param name="cancel">The token to monitor for cancellation requests.</param>
/// <returns>A task that represents an asynchronous operation.</returns>
/// <exception cref="ClientException">Thrown if the <paramref name="request"/> or <paramref name="method"/>
/// is invalid or not matched. Also thrown if the server returns an error object.</exception>
Task ProcessOperationResponseAsync(OperationRequest request, HttpMethod method,
Action<string> responseProcessor, CancellationToken cancel);

/// <summary>
/// Sends the specified HTTP request and passes the response to the <paramref name="responseProcessor"/> callback.
/// </summary>
Expand Down
31 changes: 31 additions & 0 deletions src/SenseNet.Client/Repository/OperationRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;

// ReSharper disable once CheckNamespace
namespace SenseNet.Client;

public class OperationRequest : LoadCollectionRequest
{
public int ContentId { get; set; }
public string OperationName { get; set; }
public object PostData { get; set; }

protected override void AddProperties(ODataRequest oDataRequest)
{
// Avoid InvalidOperationException
if (ContentId > 0 && string.IsNullOrEmpty(Path))
Path = "/Root";

if (string.IsNullOrEmpty(OperationName))
throw new InvalidOperationException("Invalid request properties: OperationName must be provided.");

base.AddProperties(oDataRequest);

oDataRequest.ContentId = ContentId;
oDataRequest.ActionName = OperationName;
oDataRequest.PostData = PostData;

// Set back the "false" value because operation can be called on a single content.
oDataRequest.IsCollectionRequest = false;

}
}
Loading