Skip to content
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: 108 additions & 0 deletions FlowableExternalWorkerClient.Tests/BpmnUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Xunit;

namespace FlowableExternalWorkerClient.Tests;

public static class BpmnUtils
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

public static async Task<string> DeployProcess(HttpClient client, string baseUrl, string filePath)
{
using var content = new MultipartFormDataContent();
var fileBytes = await File.ReadAllBytesAsync(filePath);
var fileContent = new ByteArrayContent(fileBytes);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
content.Add(fileContent, "file", Path.GetFileName(filePath));

var response = await client.PostAsync(baseUrl + "/process-api/repository/deployments", content);
Assert.Equal(System.Net.HttpStatusCode.Created, response.StatusCode);

var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
return json.GetProperty("id").GetString()!;
}

public static async Task DeleteDeployment(HttpClient client, string baseUrl, string deploymentId)
{
var response = await client.DeleteAsync(
baseUrl + "/process-api/repository/deployments/" + deploymentId);
Assert.Equal(System.Net.HttpStatusCode.NoContent, response.StatusCode);
}

public static async Task<string> GetProcessDefinitionId(HttpClient client, string baseUrl, string deploymentId)
{
var response = await client.GetAsync(
baseUrl + "/process-api/repository/process-definitions?deploymentId=" + deploymentId);
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);

var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
var data = json.GetProperty("data");
Assert.Equal(1, data.GetArrayLength());
return data[0].GetProperty("id").GetString()!;
}

public static async Task<string> StartProcess(HttpClient client, string baseUrl, string processDefinitionId)
{
var requestBody = new StringContent(
JsonSerializer.Serialize(new { processDefinitionId }, JsonOptions),
Encoding.UTF8,
"application/json"
);

var response = await client.PostAsync(
baseUrl + "/process-api/runtime/process-instances", requestBody);
Assert.Equal(System.Net.HttpStatusCode.Created, response.StatusCode);

var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
return json.GetProperty("id").GetString()!;
}

public static async Task TerminateProcess(HttpClient client, string baseUrl, string processInstanceId)
{
var response = await client.DeleteAsync(
baseUrl + "/process-api/runtime/process-instances/" + processInstanceId);
Assert.Equal(System.Net.HttpStatusCode.NoContent, response.StatusCode);
}

public static async Task<JsonElement?> GetProcessVariable(HttpClient client, string baseUrl,
string processInstanceId, string variableName)
{
var response = await client.GetAsync(
baseUrl + "/process-api/history/historic-variable-instances?processInstanceId=" +
processInstanceId + "&variableName=" + variableName);
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);

var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
var data = json.GetProperty("data");
if (data.GetArrayLength() == 1)
{
return data[0].GetProperty("variable");
}

return null;
}

public static async Task<List<string>> ExecutedActivityIds(HttpClient client, string baseUrl,
string processInstanceId)
{
var response = await client.GetAsync(
baseUrl + "/process-api/history/historic-activity-instances?processInstanceId=" + processInstanceId);
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);

var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
var data = json.GetProperty("data");
var activityIds = new List<string>();
foreach (var item in data.EnumerateArray())
{
activityIds.Add(item.GetProperty("activityId").GetString()!);
}

activityIds.Sort();
return activityIds;
}
}
168 changes: 168 additions & 0 deletions FlowableExternalWorkerClient.Tests/CassetteHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;

namespace FlowableExternalWorkerClient.Tests;

/// <summary>
/// A DelegatingHandler that records HTTP interactions to a JSON cassette file
/// and replays them in order on subsequent runs.
///
/// Mode: Auto - if cassette file exists, replays; otherwise records.
/// Replay is sequential (ordered), not matching-based, to correctly handle
/// repeated calls to the same endpoint with different responses.
/// </summary>
public class CassetteHandler : DelegatingHandler
{
private readonly string _cassettePath;
private readonly List<RecordedInteraction> _interactions;
private int _replayIndex;
private readonly bool _isReplaying;

public CassetteHandler(string cassettePath)
: base(new HttpClientHandler())
{
_cassettePath = cassettePath;
if (File.Exists(cassettePath))
{
var json = File.ReadAllText(cassettePath);
_interactions = JsonSerializer.Deserialize<List<RecordedInteraction>>(json,
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }) ?? new();
_isReplaying = true;
}
else
{
_interactions = new();
_isReplaying = false;
}
}

public bool IsReplaying => _isReplaying;

public HttpClient CreateHttpClient()
{
return new HttpClient(this, disposeHandler: false);
}

protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
if (_isReplaying)
{
return Replay(request);
}

return await Record(request, cancellationToken);
}

private HttpResponseMessage Replay(HttpRequestMessage request)
{
int index;
lock (_interactions)
{
index = _replayIndex++;
}

if (index >= _interactions.Count)
{
throw new InvalidOperationException(
$"Cassette '{Path.GetFileName(_cassettePath)}' has no more recorded interactions " +
$"(tried index {index}, total {_interactions.Count}). " +
$"Delete the cassette file to re-record.");
}

return _interactions[index].ToHttpResponseMessage();
}

private async Task<HttpResponseMessage> Record(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);

// Buffer the response body so both recording and caller can use it
var bodyBytes = response.Content != null
? await response.Content.ReadAsByteArrayAsync(cancellationToken)
: Array.Empty<byte>();

var interaction = new RecordedInteraction
{
Method = request.Method.Method,
Uri = request.RequestUri?.ToString() ?? "",
StatusCode = (int)response.StatusCode,
ResponseBody = Convert.ToBase64String(bodyBytes),
ResponseContentType = response.Content?.Headers.ContentType?.ToString()
};

foreach (var header in response.Headers)
{
interaction.ResponseHeaders[header.Key] = header.Value.ToArray();
}

lock (_interactions)
{
_interactions.Add(interaction);
}

// Return a new response with buffered body so the caller can read it
var newResponse = new HttpResponseMessage(response.StatusCode);
newResponse.Content = new ByteArrayContent(bodyBytes);
if (response.Content?.Headers.ContentType != null)
{
newResponse.Content.Headers.ContentType = response.Content.Headers.ContentType;
}

foreach (var header in response.Headers)
{
newResponse.Headers.TryAddWithoutValidation(header.Key, header.Value);
}

newResponse.RequestMessage = request;
return newResponse;
}

public void Save()
{
if (!_isReplaying)
{
Directory.CreateDirectory(Path.GetDirectoryName(_cassettePath)!);
var json = JsonSerializer.Serialize(_interactions, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
File.WriteAllText(_cassettePath, json);
}
}
}

public class RecordedInteraction
{
public string Method { get; set; } = "";
public string Uri { get; set; } = "";
public int StatusCode { get; set; }
public string? ResponseBody { get; set; }
public string? ResponseContentType { get; set; }
public Dictionary<string, string[]> ResponseHeaders { get; set; } = new();

public HttpResponseMessage ToHttpResponseMessage()
{
var response = new HttpResponseMessage((HttpStatusCode)StatusCode);

if (ResponseBody != null)
{
var bytes = Convert.FromBase64String(ResponseBody);
response.Content = new ByteArrayContent(bytes);
if (ResponseContentType != null)
{
response.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(ResponseContentType);
}
}

foreach (var header in ResponseHeaders)
{
response.Headers.TryAddWithoutValidation(header.Key, header.Value);
}

return response;
}
}
89 changes: 89 additions & 0 deletions FlowableExternalWorkerClient.Tests/CmmnUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Xunit;

namespace FlowableExternalWorkerClient.Tests;

public static class CmmnUtils
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

public static async Task<string> DeployCase(HttpClient client, string baseUrl, string filePath)
{
using var content = new MultipartFormDataContent();
var fileBytes = await File.ReadAllBytesAsync(filePath);
var fileContent = new ByteArrayContent(fileBytes);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
content.Add(fileContent, "file", Path.GetFileName(filePath));

var response = await client.PostAsync(baseUrl + "/cmmn-api/cmmn-repository/deployments", content);
Assert.Equal(System.Net.HttpStatusCode.Created, response.StatusCode);

var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
return json.GetProperty("id").GetString()!;
}

public static async Task DeleteCaseDeployment(HttpClient client, string baseUrl, string deploymentId)
{
var response = await client.DeleteAsync(
baseUrl + "/cmmn-api/cmmn-repository/deployments/" + deploymentId);
Assert.Equal(System.Net.HttpStatusCode.NoContent, response.StatusCode);
}

public static async Task<string> GetCaseDefinitionId(HttpClient client, string baseUrl, string deploymentId)
{
var response = await client.GetAsync(
baseUrl + "/cmmn-api/cmmn-repository/case-definitions?deploymentId=" + deploymentId);
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);

var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
var data = json.GetProperty("data");
Assert.Equal(1, data.GetArrayLength());
return data[0].GetProperty("id").GetString()!;
}

public static async Task<string> StartCase(HttpClient client, string baseUrl, string caseDefinitionId)
{
var requestBody = new StringContent(
JsonSerializer.Serialize(new { caseDefinitionId }, JsonOptions),
Encoding.UTF8,
"application/json"
);

var response = await client.PostAsync(
baseUrl + "/cmmn-api/cmmn-runtime/case-instances", requestBody);
Assert.Equal(System.Net.HttpStatusCode.Created, response.StatusCode);

var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
return json.GetProperty("id").GetString()!;
}

public static async Task TerminateCase(HttpClient client, string baseUrl, string caseInstanceId)
{
var response = await client.DeleteAsync(
baseUrl + "/cmmn-api/cmmn-runtime/case-instances/" + caseInstanceId);
Assert.Equal(System.Net.HttpStatusCode.NoContent, response.StatusCode);
}

public static async Task<JsonElement?> GetCaseVariable(HttpClient client, string baseUrl,
string caseInstanceId, string variableName)
{
var response = await client.GetAsync(
baseUrl + "/cmmn-api/cmmn-history/historic-variable-instances?caseInstanceId=" +
caseInstanceId + "&variableName=" + variableName);
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);

var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
var data = json.GetProperty("data");
if (data.GetArrayLength() == 1)
{
return data[0].GetProperty("variable");
}

return null;
}
}
Loading