Skip to content

Commit 26a937b

Browse files
authored
add tests similar to the tests in the Python external worker repository (#3)
* add tests similar to the tests in the Python repository * remove static wait and switch to thread subscription
1 parent fc98836 commit 26a937b

31 files changed

+4979
-5
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using System.Net.Http.Headers;
2+
using System.Text;
3+
using System.Text.Json;
4+
using Xunit;
5+
6+
namespace FlowableExternalWorkerClient.Tests;
7+
8+
public static class BpmnUtils
9+
{
10+
private static readonly JsonSerializerOptions JsonOptions = new()
11+
{
12+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
13+
};
14+
15+
public static async Task<string> DeployProcess(HttpClient client, string baseUrl, string filePath)
16+
{
17+
using var content = new MultipartFormDataContent();
18+
var fileBytes = await File.ReadAllBytesAsync(filePath);
19+
var fileContent = new ByteArrayContent(fileBytes);
20+
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
21+
content.Add(fileContent, "file", Path.GetFileName(filePath));
22+
23+
var response = await client.PostAsync(baseUrl + "/process-api/repository/deployments", content);
24+
Assert.Equal(System.Net.HttpStatusCode.Created, response.StatusCode);
25+
26+
var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
27+
return json.GetProperty("id").GetString()!;
28+
}
29+
30+
public static async Task DeleteDeployment(HttpClient client, string baseUrl, string deploymentId)
31+
{
32+
var response = await client.DeleteAsync(
33+
baseUrl + "/process-api/repository/deployments/" + deploymentId);
34+
Assert.Equal(System.Net.HttpStatusCode.NoContent, response.StatusCode);
35+
}
36+
37+
public static async Task<string> GetProcessDefinitionId(HttpClient client, string baseUrl, string deploymentId)
38+
{
39+
var response = await client.GetAsync(
40+
baseUrl + "/process-api/repository/process-definitions?deploymentId=" + deploymentId);
41+
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
42+
43+
var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
44+
var data = json.GetProperty("data");
45+
Assert.Equal(1, data.GetArrayLength());
46+
return data[0].GetProperty("id").GetString()!;
47+
}
48+
49+
public static async Task<string> StartProcess(HttpClient client, string baseUrl, string processDefinitionId)
50+
{
51+
var requestBody = new StringContent(
52+
JsonSerializer.Serialize(new { processDefinitionId }, JsonOptions),
53+
Encoding.UTF8,
54+
"application/json"
55+
);
56+
57+
var response = await client.PostAsync(
58+
baseUrl + "/process-api/runtime/process-instances", requestBody);
59+
Assert.Equal(System.Net.HttpStatusCode.Created, response.StatusCode);
60+
61+
var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
62+
return json.GetProperty("id").GetString()!;
63+
}
64+
65+
public static async Task TerminateProcess(HttpClient client, string baseUrl, string processInstanceId)
66+
{
67+
var response = await client.DeleteAsync(
68+
baseUrl + "/process-api/runtime/process-instances/" + processInstanceId);
69+
Assert.Equal(System.Net.HttpStatusCode.NoContent, response.StatusCode);
70+
}
71+
72+
public static async Task<JsonElement?> GetProcessVariable(HttpClient client, string baseUrl,
73+
string processInstanceId, string variableName)
74+
{
75+
var response = await client.GetAsync(
76+
baseUrl + "/process-api/history/historic-variable-instances?processInstanceId=" +
77+
processInstanceId + "&variableName=" + variableName);
78+
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
79+
80+
var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
81+
var data = json.GetProperty("data");
82+
if (data.GetArrayLength() == 1)
83+
{
84+
return data[0].GetProperty("variable");
85+
}
86+
87+
return null;
88+
}
89+
90+
public static async Task<List<string>> ExecutedActivityIds(HttpClient client, string baseUrl,
91+
string processInstanceId)
92+
{
93+
var response = await client.GetAsync(
94+
baseUrl + "/process-api/history/historic-activity-instances?processInstanceId=" + processInstanceId);
95+
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
96+
97+
var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
98+
var data = json.GetProperty("data");
99+
var activityIds = new List<string>();
100+
foreach (var item in data.EnumerateArray())
101+
{
102+
activityIds.Add(item.GetProperty("activityId").GetString()!);
103+
}
104+
105+
activityIds.Sort();
106+
return activityIds;
107+
}
108+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
using System.Net;
2+
using System.Net.Http.Headers;
3+
using System.Text.Json;
4+
5+
namespace FlowableExternalWorkerClient.Tests;
6+
7+
/// <summary>
8+
/// A DelegatingHandler that records HTTP interactions to a JSON cassette file
9+
/// and replays them in order on subsequent runs.
10+
///
11+
/// Mode: Auto - if cassette file exists, replays; otherwise records.
12+
/// Replay is sequential (ordered), not matching-based, to correctly handle
13+
/// repeated calls to the same endpoint with different responses.
14+
/// </summary>
15+
public class CassetteHandler : DelegatingHandler
16+
{
17+
private readonly string _cassettePath;
18+
private readonly List<RecordedInteraction> _interactions;
19+
private int _replayIndex;
20+
private readonly bool _isReplaying;
21+
22+
public CassetteHandler(string cassettePath)
23+
: base(new HttpClientHandler())
24+
{
25+
_cassettePath = cassettePath;
26+
if (File.Exists(cassettePath))
27+
{
28+
var json = File.ReadAllText(cassettePath);
29+
_interactions = JsonSerializer.Deserialize<List<RecordedInteraction>>(json,
30+
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }) ?? new();
31+
_isReplaying = true;
32+
}
33+
else
34+
{
35+
_interactions = new();
36+
_isReplaying = false;
37+
}
38+
}
39+
40+
public bool IsReplaying => _isReplaying;
41+
42+
public HttpClient CreateHttpClient()
43+
{
44+
return new HttpClient(this, disposeHandler: false);
45+
}
46+
47+
protected override async Task<HttpResponseMessage> SendAsync(
48+
HttpRequestMessage request, CancellationToken cancellationToken)
49+
{
50+
if (_isReplaying)
51+
{
52+
return Replay(request);
53+
}
54+
55+
return await Record(request, cancellationToken);
56+
}
57+
58+
private HttpResponseMessage Replay(HttpRequestMessage request)
59+
{
60+
int index;
61+
lock (_interactions)
62+
{
63+
index = _replayIndex++;
64+
}
65+
66+
if (index >= _interactions.Count)
67+
{
68+
throw new InvalidOperationException(
69+
$"Cassette '{Path.GetFileName(_cassettePath)}' has no more recorded interactions " +
70+
$"(tried index {index}, total {_interactions.Count}). " +
71+
$"Delete the cassette file to re-record.");
72+
}
73+
74+
return _interactions[index].ToHttpResponseMessage();
75+
}
76+
77+
private async Task<HttpResponseMessage> Record(
78+
HttpRequestMessage request, CancellationToken cancellationToken)
79+
{
80+
var response = await base.SendAsync(request, cancellationToken);
81+
82+
// Buffer the response body so both recording and caller can use it
83+
var bodyBytes = response.Content != null
84+
? await response.Content.ReadAsByteArrayAsync(cancellationToken)
85+
: Array.Empty<byte>();
86+
87+
var interaction = new RecordedInteraction
88+
{
89+
Method = request.Method.Method,
90+
Uri = request.RequestUri?.ToString() ?? "",
91+
StatusCode = (int)response.StatusCode,
92+
ResponseBody = Convert.ToBase64String(bodyBytes),
93+
ResponseContentType = response.Content?.Headers.ContentType?.ToString()
94+
};
95+
96+
foreach (var header in response.Headers)
97+
{
98+
interaction.ResponseHeaders[header.Key] = header.Value.ToArray();
99+
}
100+
101+
lock (_interactions)
102+
{
103+
_interactions.Add(interaction);
104+
}
105+
106+
// Return a new response with buffered body so the caller can read it
107+
var newResponse = new HttpResponseMessage(response.StatusCode);
108+
newResponse.Content = new ByteArrayContent(bodyBytes);
109+
if (response.Content?.Headers.ContentType != null)
110+
{
111+
newResponse.Content.Headers.ContentType = response.Content.Headers.ContentType;
112+
}
113+
114+
foreach (var header in response.Headers)
115+
{
116+
newResponse.Headers.TryAddWithoutValidation(header.Key, header.Value);
117+
}
118+
119+
newResponse.RequestMessage = request;
120+
return newResponse;
121+
}
122+
123+
public void Save()
124+
{
125+
if (!_isReplaying)
126+
{
127+
Directory.CreateDirectory(Path.GetDirectoryName(_cassettePath)!);
128+
var json = JsonSerializer.Serialize(_interactions, new JsonSerializerOptions
129+
{
130+
WriteIndented = true,
131+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
132+
});
133+
File.WriteAllText(_cassettePath, json);
134+
}
135+
}
136+
}
137+
138+
public class RecordedInteraction
139+
{
140+
public string Method { get; set; } = "";
141+
public string Uri { get; set; } = "";
142+
public int StatusCode { get; set; }
143+
public string? ResponseBody { get; set; }
144+
public string? ResponseContentType { get; set; }
145+
public Dictionary<string, string[]> ResponseHeaders { get; set; } = new();
146+
147+
public HttpResponseMessage ToHttpResponseMessage()
148+
{
149+
var response = new HttpResponseMessage((HttpStatusCode)StatusCode);
150+
151+
if (ResponseBody != null)
152+
{
153+
var bytes = Convert.FromBase64String(ResponseBody);
154+
response.Content = new ByteArrayContent(bytes);
155+
if (ResponseContentType != null)
156+
{
157+
response.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(ResponseContentType);
158+
}
159+
}
160+
161+
foreach (var header in ResponseHeaders)
162+
{
163+
response.Headers.TryAddWithoutValidation(header.Key, header.Value);
164+
}
165+
166+
return response;
167+
}
168+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using System.Net.Http.Headers;
2+
using System.Text;
3+
using System.Text.Json;
4+
using Xunit;
5+
6+
namespace FlowableExternalWorkerClient.Tests;
7+
8+
public static class CmmnUtils
9+
{
10+
private static readonly JsonSerializerOptions JsonOptions = new()
11+
{
12+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
13+
};
14+
15+
public static async Task<string> DeployCase(HttpClient client, string baseUrl, string filePath)
16+
{
17+
using var content = new MultipartFormDataContent();
18+
var fileBytes = await File.ReadAllBytesAsync(filePath);
19+
var fileContent = new ByteArrayContent(fileBytes);
20+
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
21+
content.Add(fileContent, "file", Path.GetFileName(filePath));
22+
23+
var response = await client.PostAsync(baseUrl + "/cmmn-api/cmmn-repository/deployments", content);
24+
Assert.Equal(System.Net.HttpStatusCode.Created, response.StatusCode);
25+
26+
var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
27+
return json.GetProperty("id").GetString()!;
28+
}
29+
30+
public static async Task DeleteCaseDeployment(HttpClient client, string baseUrl, string deploymentId)
31+
{
32+
var response = await client.DeleteAsync(
33+
baseUrl + "/cmmn-api/cmmn-repository/deployments/" + deploymentId);
34+
Assert.Equal(System.Net.HttpStatusCode.NoContent, response.StatusCode);
35+
}
36+
37+
public static async Task<string> GetCaseDefinitionId(HttpClient client, string baseUrl, string deploymentId)
38+
{
39+
var response = await client.GetAsync(
40+
baseUrl + "/cmmn-api/cmmn-repository/case-definitions?deploymentId=" + deploymentId);
41+
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
42+
43+
var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
44+
var data = json.GetProperty("data");
45+
Assert.Equal(1, data.GetArrayLength());
46+
return data[0].GetProperty("id").GetString()!;
47+
}
48+
49+
public static async Task<string> StartCase(HttpClient client, string baseUrl, string caseDefinitionId)
50+
{
51+
var requestBody = new StringContent(
52+
JsonSerializer.Serialize(new { caseDefinitionId }, JsonOptions),
53+
Encoding.UTF8,
54+
"application/json"
55+
);
56+
57+
var response = await client.PostAsync(
58+
baseUrl + "/cmmn-api/cmmn-runtime/case-instances", requestBody);
59+
Assert.Equal(System.Net.HttpStatusCode.Created, response.StatusCode);
60+
61+
var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
62+
return json.GetProperty("id").GetString()!;
63+
}
64+
65+
public static async Task TerminateCase(HttpClient client, string baseUrl, string caseInstanceId)
66+
{
67+
var response = await client.DeleteAsync(
68+
baseUrl + "/cmmn-api/cmmn-runtime/case-instances/" + caseInstanceId);
69+
Assert.Equal(System.Net.HttpStatusCode.NoContent, response.StatusCode);
70+
}
71+
72+
public static async Task<JsonElement?> GetCaseVariable(HttpClient client, string baseUrl,
73+
string caseInstanceId, string variableName)
74+
{
75+
var response = await client.GetAsync(
76+
baseUrl + "/cmmn-api/cmmn-history/historic-variable-instances?caseInstanceId=" +
77+
caseInstanceId + "&variableName=" + variableName);
78+
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
79+
80+
var json = await JsonSerializer.DeserializeAsync<JsonElement>(await response.Content.ReadAsStreamAsync());
81+
var data = json.GetProperty("data");
82+
if (data.GetArrayLength() == 1)
83+
{
84+
return data[0].GetProperty("variable");
85+
}
86+
87+
return null;
88+
}
89+
}

0 commit comments

Comments
 (0)