diff --git a/Tests/IsapiDeleteSuccessTests.cs b/Tests/IsapiDeleteSuccessTests.cs new file mode 100644 index 0000000..0527260 --- /dev/null +++ b/Tests/IsapiDeleteSuccessTests.cs @@ -0,0 +1,89 @@ +using Xunit; + +namespace HikvisionAttendanceService.Tests; + +/// +/// Covers delete-success parsing rules used by USER_DELETE confirmation +/// (regex-based mirror so the test project does not need System.Web.Extensions). +/// +public class IsapiDeleteSuccessTests +{ + [Theory] + [InlineData("{\"statusCode\":1,\"statusString\":\"OK\",\"subStatusCode\":\"ok\"}", 200, true)] + [InlineData("{\n \"statusCode\": 1,\n \"statusString\": \"OK\",\n \"subStatusCode\": \"ok\"\n}", 200, true)] + [InlineData("{\"statusCode\":\"1\",\"statusString\":\"OK\",\"subStatusCode\":\"ok\"}", 200, true)] + [InlineData("{ \"statusCode\" : 1 , \"statusString\" : \"OK\" }", 200, true)] + [InlineData("{\"statusCode\":1,\"subStatusCode\":\"ok\"}", 200, true)] + [InlineData("{\"statusCode\":1,\"statusString\":\"OK\"}", 200, true)] + [InlineData("{\"statusCode\":1}", 200, false)] + [InlineData("{\"statusCode\":4,\"statusString\":\"Invalid Operation\",\"subStatusCode\":\"employeeNoNotExist\"}", 200, false)] + [InlineData("{\"statusCode\":1,\"statusString\":\"OK\",\"subStatusCode\":\"ok\"}", 400, false)] + public void Confirmed_delete_success_rules(string body, int httpStatus, bool expected) + { + Assert.Equal(expected, IsConfirmed(body, httpStatus)); + } + + [Fact] + public void Failure_reason_maps_employee_not_found() + { + Assert.Equal( + "Employee was not found on the device", + Describe(200, 4, "Invalid Operation", "employeeNoNotExist")); + } + + private static bool IsConfirmed(string? body, int httpStatus) + { + if (httpStatus < 200 || httpStatus >= 300) + return false; + if (!TryParse(body, out var statusCode, out var statusString, out var subStatusCode)) + return false; + if (statusCode != 1) + return false; + return string.Equals(statusString, "OK", System.StringComparison.OrdinalIgnoreCase) + || string.Equals(subStatusCode, "ok", System.StringComparison.OrdinalIgnoreCase); + } + + private static bool TryParse(string? body, out int statusCode, out string statusString, out string subStatusCode) + { + statusCode = 0; + statusString = ""; + subStatusCode = ""; + if (string.IsNullOrWhiteSpace(body)) + return false; + + var mCode = System.Text.RegularExpressions.Regex.Match(body, + "\"statusCode\"\\s*:\\s*\"?(?-?\\d+)\"?", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + if (mCode.Success) + int.TryParse(mCode.Groups["v"].Value, out statusCode); + + var mStr = System.Text.RegularExpressions.Regex.Match(body, + "\"statusString\"\\s*:\\s*\"(?[^\"]*)\"", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + if (mStr.Success) statusString = mStr.Groups["v"].Value; + + var mSub = System.Text.RegularExpressions.Regex.Match(body, + "\"subStatusCode\"\\s*:\\s*\"(?[^\"]*)\"", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + if (mSub.Success) subStatusCode = mSub.Groups["v"].Value; + + return statusCode != 0 || statusString.Length > 0 || subStatusCode.Length > 0; + } + + private static string Describe(int httpStatus, int statusCode, string statusString, string subStatusCode) + { + if (httpStatus < 200 || httpStatus >= 300) + return "HTTP " + httpStatus; + var sub = (subStatusCode ?? "").Trim(); + if (sub.IndexOf("notExist", System.StringComparison.OrdinalIgnoreCase) >= 0 || + sub.IndexOf("notFound", System.StringComparison.OrdinalIgnoreCase) >= 0 || + sub.Equals("employeeNoNotExist", System.StringComparison.OrdinalIgnoreCase)) + return "Employee was not found on the device"; + if (!string.IsNullOrWhiteSpace(statusString) && + !string.Equals(statusString, "OK", System.StringComparison.OrdinalIgnoreCase)) + return statusString.Trim(); + if (statusCode != 0 && statusCode != 1) + return "Hikvision statusCode=" + statusCode; + return "device delete rejected"; + } +}