Compare commits
2
Commits
8a0ebeeb1e
...
a45ca014ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a45ca014ca | ||
|
|
c812d94d99 |
@@ -110,6 +110,20 @@ public class DocumentMetadataBuilderTests
|
|||||||
Assert.Equal(guid.ToString(), metadata["file_guid"]);
|
Assert.Equal(guid.ToString(), metadata["file_guid"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Zahlungserinnerung für Müller", "Zahlungserinnerung f%C3%BCr M%C3%BCller")]
|
||||||
|
[InlineData("Rechnung\r\nKunde", "Rechnung%0D%0AKunde")]
|
||||||
|
public void Build_NonAsciiOrControlCharacters_PercentEncodesUtf8ForHttpHeaders(
|
||||||
|
string value, string expected)
|
||||||
|
{
|
||||||
|
var row = new Dictionary<string, object?> { ["DocumentName"] = value };
|
||||||
|
|
||||||
|
var metadata = DocumentMetadataBuilder.Build(row, new[] { "DocumentName" });
|
||||||
|
|
||||||
|
Assert.Equal(expected, metadata["DocumentName"]);
|
||||||
|
Assert.All(metadata["DocumentName"], c => Assert.InRange(c, '\u0020', '\u007e'));
|
||||||
|
}
|
||||||
|
|
||||||
// ── Field-list edge cases ────────────────────────────────────────────────
|
// ── Field-list edge cases ────────────────────────────────────────────────
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("")]
|
[InlineData("")]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.Metrics;
|
using System.Diagnostics.Metrics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -48,6 +49,12 @@ public class ProcessWebComServiceTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class ThrowingHandler : HttpMessageHandler
|
||||||
|
{
|
||||||
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
=> throw new HttpRequestException("simulated transport failure");
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class StubHttpClientFactory : IHttpClientFactory
|
private sealed class StubHttpClientFactory : IHttpClientFactory
|
||||||
{
|
{
|
||||||
private readonly HttpMessageHandler _handler;
|
private readonly HttpMessageHandler _handler;
|
||||||
@@ -55,6 +62,41 @@ public class ProcessWebComServiceTests
|
|||||||
public HttpClient CreateClient(string name) => new(_handler, disposeHandler: false);
|
public HttpClient CreateClient(string name) => new(_handler, disposeHandler: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Captures what would be written to <c>fds__logEmail</c> without hitting a database, by
|
||||||
|
/// overriding the audit-log write. Lets tests assert that every send path is logged and that
|
||||||
|
/// failures carry the service response / exception detail.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class CapturingComService : ProcessWebComService
|
||||||
|
{
|
||||||
|
public readonly List<(bool success, List<string> log)> Entries = new();
|
||||||
|
|
||||||
|
public CapturingComService(
|
||||||
|
IOptions<ProcessWebComSettings> settings,
|
||||||
|
IOptions<FuchsEmailSettings> emailSettings,
|
||||||
|
IHttpClientFactory httpClientFactory)
|
||||||
|
: base(NullLogger<ProcessWebComService>.Instance, intranet: null!, settings, emailSettings, httpClientFactory)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task WriteAuditLogAsync(string reference, string guid, string config,
|
||||||
|
DateTime sent, bool success, IEnumerable<string> errors)
|
||||||
|
{
|
||||||
|
Entries.Add((success, errors.ToList()));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CapturingComService CreateCapturing(HttpMessageHandler handler, bool enabled = true, string? overrideRecipient = null)
|
||||||
|
{
|
||||||
|
var settings = Options.Create(new ProcessWebComSettings
|
||||||
|
{
|
||||||
|
Enabled = enabled, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "tok"
|
||||||
|
});
|
||||||
|
var emailSettings = Options.Create(new FuchsEmailSettings { OverrideRecipient = overrideRecipient });
|
||||||
|
return new CapturingComService(settings, emailSettings, new StubHttpClientFactory(handler));
|
||||||
|
}
|
||||||
|
|
||||||
private static ProcessWebComService CreateService(StubHandler handler, bool enabled = true, string? overrideRecipient = null)
|
private static ProcessWebComService CreateService(StubHandler handler, bool enabled = true, string? overrideRecipient = null)
|
||||||
{
|
{
|
||||||
var settings = Options.Create(new ProcessWebComSettings
|
var settings = Options.Create(new ProcessWebComSettings
|
||||||
@@ -129,6 +171,102 @@ public class ProcessWebComServiceTests
|
|||||||
Assert.Equal(0, handler.CallCount);
|
Assert.Equal(0, handler.CallCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Audit logging: every attempt is logged; failures carry the service response ─
|
||||||
|
[Fact]
|
||||||
|
public async Task SendEmailAsync_Success_LogsSuccessfulAttempt()
|
||||||
|
{
|
||||||
|
var svc = CreateCapturing(new StubHandler(HttpStatusCode.OK));
|
||||||
|
|
||||||
|
await svc.SendEmailAsync("inv_log_ok", "S", "<p>x</p>", "kunde@example.de", "Kunde");
|
||||||
|
|
||||||
|
var entry = Assert.Single(svc.Entries);
|
||||||
|
Assert.True(entry.success);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendEmailAsync_ApiError_LogsFailureIncludingServiceResponse()
|
||||||
|
{
|
||||||
|
var svc = CreateCapturing(new StubHandler(HttpStatusCode.InternalServerError, "mailer rejected: quota exceeded"));
|
||||||
|
|
||||||
|
bool result = await svc.SendEmailAsync("inv_log_api", "S", "<p>x</p>", "kunde@example.de", "Kunde");
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
var entry = Assert.Single(svc.Entries);
|
||||||
|
Assert.False(entry.success);
|
||||||
|
Assert.Contains(entry.log, l => l.Contains("mailer rejected: quota exceeded")); // raw service response
|
||||||
|
Assert.Contains(entry.log, l => l.Contains("500")); // HTTP status
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendEmailAsync_TransportException_LogsFailureIncludingExceptionDetail()
|
||||||
|
{
|
||||||
|
var svc = CreateCapturing(new ThrowingHandler());
|
||||||
|
|
||||||
|
bool result = await svc.SendEmailAsync("inv_log_ex", "S", "<p>x</p>", "kunde@example.de", "Kunde");
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
var entry = Assert.Single(svc.Entries);
|
||||||
|
Assert.False(entry.success);
|
||||||
|
Assert.Contains(entry.log, l => l.Contains("Exception while sending"));
|
||||||
|
Assert.Contains(entry.log, l => l.Contains("simulated transport failure"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("not-an-email")]
|
||||||
|
[InlineData("")]
|
||||||
|
public async Task SendEmailAsync_InvalidEmail_StillLogsFailedAttempt(string badEmail)
|
||||||
|
{
|
||||||
|
var svc = CreateCapturing(new StubHandler(HttpStatusCode.OK));
|
||||||
|
|
||||||
|
bool result = await svc.SendEmailAsync("inv_log_bad", "S", "<p>x</p>", badEmail, "Kunde");
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
var entry = Assert.Single(svc.Entries);
|
||||||
|
Assert.False(entry.success);
|
||||||
|
Assert.Contains(entry.log, l => l.Contains("Invalid recipient email address"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendEmailAsync_Disabled_LogsFailedAttempt()
|
||||||
|
{
|
||||||
|
var svc = CreateCapturing(new StubHandler(HttpStatusCode.OK), enabled: false);
|
||||||
|
|
||||||
|
await svc.SendEmailAsync("inv_log_dis", "S", "<p>x</p>", "kunde@example.de", "Kunde");
|
||||||
|
|
||||||
|
var entry = Assert.Single(svc.Entries);
|
||||||
|
Assert.False(entry.success);
|
||||||
|
Assert.Contains(entry.log, l => l.ToLowerInvariant().Contains("disabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Audit-log parameters honour the NOT NULL columns of fds__emaillog ──────
|
||||||
|
[Fact]
|
||||||
|
public void BuildAuditLogParameters_FailedAttempt_NeverPassesNullForNotNullColumns()
|
||||||
|
{
|
||||||
|
// fds__emaillog.config and .DateSent are NOT NULL; a failed/unset send must not send NULL
|
||||||
|
// (regression: it previously did, so every audit write failed and no email was ever logged).
|
||||||
|
var pl = ProcessWebComService.BuildAuditLogParameters(
|
||||||
|
"admin_test", guid: "", config: "", sent: default, success: false, errors: new[] { "err" });
|
||||||
|
|
||||||
|
object? config = pl.Single(p => p.ParameterName == "@config").Value;
|
||||||
|
object? dateSent = pl.Single(p => p.ParameterName == "@DateSent").Value;
|
||||||
|
|
||||||
|
Assert.Equal("", config);
|
||||||
|
Assert.NotEqual(DBNull.Value, config);
|
||||||
|
Assert.IsType<DateTime>(dateSent);
|
||||||
|
Assert.NotEqual((object)DBNull.Value, dateSent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildAuditLogParameters_SuccessfulSend_KeepsProvidedSentTimestamp()
|
||||||
|
{
|
||||||
|
var when = new DateTime(2026, 7, 16, 12, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
var pl = ProcessWebComService.BuildAuditLogParameters(
|
||||||
|
"inv_1", guid: "g1", config: "", sent: when, success: true, errors: Array.Empty<string>());
|
||||||
|
|
||||||
|
Assert.Equal(when, pl.Single(p => p.ParameterName == "@DateSent").Value);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Dev/test recipient override safety net ─────────────────────────────────
|
// ── Dev/test recipient override safety net ─────────────────────────────────
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SendEmailAsync_OverrideRecipientSet_RedirectsToOverrideAddress()
|
public async Task SendEmailAsync_OverrideRecipientSet_RedirectsToOverrideAddress()
|
||||||
|
|||||||
@@ -85,8 +85,10 @@ public partial class IntranetController
|
|||||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mirror OCORE's getJSONResult exactly (application/json; charset=utf-8) so the response
|
||||||
|
// parses identically to every other endpoint the frontend's postXT talks to.
|
||||||
private ContentResult AdminJson(object payload) =>
|
private ContentResult AdminJson(object payload) =>
|
||||||
Content(JsonConvert.SerializeObject(payload, CamelCaseJson), "application/json");
|
Content(JsonConvert.SerializeObject(payload, CamelCaseJson), "application/json; charset=utf-8");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the calling user's <c>fds_sys</c> module authorization level via the
|
/// Resolves the calling user's <c>fds_sys</c> module authorization level via the
|
||||||
|
|||||||
+2
-2
@@ -33,8 +33,8 @@
|
|||||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
||||||
<PackageReference Include="MailKit" Version="4.17.0" />
|
<PackageReference Include="MailKit" Version="4.17.0" />
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.16.0" />
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.16.0" />
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.16.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.16.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.16.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.16.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.16.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.16.0" />
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public static class DocumentMetadataBuilder
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(field)) continue;
|
if (string.IsNullOrWhiteSpace(field)) continue;
|
||||||
if (!TryGetValue(row, field, out object? value)) continue;
|
if (!TryGetValue(row, field, out object? value)) continue;
|
||||||
metadata[field] = Stringify(value);
|
metadata[field] = ToAsciiHeaderValue(Stringify(value));
|
||||||
}
|
}
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
@@ -55,4 +55,24 @@ public static class DocumentMetadataBuilder
|
|||||||
DateTime dt => dt.ToString("O"),
|
DateTime dt => dt.ToString("O"),
|
||||||
_ => value.ToString() ?? ""
|
_ => value.ToString() ?? ""
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Azure Blob metadata is transported in HTTP headers, whose values must contain ASCII
|
||||||
|
/// characters only. Preserve readable, printable ASCII and percent-encode every other
|
||||||
|
/// UTF-8 byte so titles and document names containing umlauts cannot break the upload.
|
||||||
|
/// </summary>
|
||||||
|
private static string ToAsciiHeaderValue(string value)
|
||||||
|
{
|
||||||
|
if (value.All(c => c is >= ' ' and <= '~')) return value;
|
||||||
|
|
||||||
|
var result = new System.Text.StringBuilder(value.Length);
|
||||||
|
foreach (byte b in System.Text.Encoding.UTF8.GetBytes(value))
|
||||||
|
{
|
||||||
|
if (b is >= 0x20 and <= 0x7E)
|
||||||
|
result.Append((char)b);
|
||||||
|
else
|
||||||
|
result.Append('%').Append(b.ToString("X2"));
|
||||||
|
}
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ public class ProcessWebComService : IComService
|
|||||||
_logger.LogWarning("SendEmailAsync: invalid email address '{Email}' for ref {Reference}", email, reference);
|
_logger.LogWarning("SendEmailAsync: invalid email address '{Email}' for ref {Reference}", email, reference);
|
||||||
FuchsTelemetry.EmailsFailed.Add(1, new KeyValuePair<string, object?>("reason", "invalid-email"));
|
FuchsTelemetry.EmailsFailed.Add(1, new KeyValuePair<string, object?>("reason", "invalid-email"));
|
||||||
act?.SetStatus(ActivityStatusCode.Error, "invalid-email");
|
act?.SetStatus(ActivityStatusCode.Error, "invalid-email");
|
||||||
|
// Every attempt is audit-logged, including this early rejection (previously it returned unlogged).
|
||||||
|
await WriteAuditLogAsync(reference, "", "", default, false,
|
||||||
|
[$"Invalid recipient email address: '{email}'"]);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +119,7 @@ public class ProcessWebComService : IComService
|
|||||||
_logger.LogDebug("SendEmailAsync ref={Reference} to={Email} attachments={Count}",
|
_logger.LogDebug("SendEmailAsync ref={Reference} to={Email} attachments={Count}",
|
||||||
reference, email, attachmentPayload.Length);
|
reference, email, attachmentPayload.Length);
|
||||||
|
|
||||||
var (ok, responseBody) = await PostToApiAsync(reference, "e", communication);
|
var (ok, status, responseBody) = await PostToApiAsync(reference, "e", communication);
|
||||||
if (ok)
|
if (ok)
|
||||||
{
|
{
|
||||||
success = true;
|
success = true;
|
||||||
@@ -124,13 +127,17 @@ public class ProcessWebComService : IComService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
errors.Add($"API error: {responseBody}");
|
// Persist the mailer's raw response so an unsuccessful send is diagnosable from the log.
|
||||||
_logger.LogWarning("SendEmailAsync API error for {Reference}: {Body}", reference, responseBody);
|
errors.Add($"API error {(int)status} ({status}): {responseBody}");
|
||||||
|
_logger.LogWarning("SendEmailAsync API error for {Reference}: {Status} {Body}",
|
||||||
|
reference, (int)status, responseBody);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
errors.Add("Beim Versenden ist ein Fehler aufgetreten.");
|
// Capture the exception detail in the audit log (not just a generic message) so a
|
||||||
|
// transport/serialization failure is diagnosable after the fact.
|
||||||
|
errors.Add($"Exception while sending: {ex.GetType().Name}: {ex.Message}");
|
||||||
act?.SetStatus(ActivityStatusCode.Error, ex.Message);
|
act?.SetStatus(ActivityStatusCode.Error, ex.Message);
|
||||||
_logger.LogError(ex, "SendEmailAsync failed for {Reference}", reference);
|
_logger.LogError(ex, "SendEmailAsync failed for {Reference}", reference);
|
||||||
}
|
}
|
||||||
@@ -171,7 +178,7 @@ public class ProcessWebComService : IComService
|
|||||||
body = message
|
body = message
|
||||||
};
|
};
|
||||||
|
|
||||||
var (ok, responseBody) = await PostToApiAsync(mobile, "s", communication);
|
var (ok, status, responseBody) = await PostToApiAsync(mobile, "s", communication);
|
||||||
if (ok)
|
if (ok)
|
||||||
{
|
{
|
||||||
FuchsTelemetry.SmsSent.Add(1);
|
FuchsTelemetry.SmsSent.Add(1);
|
||||||
@@ -179,7 +186,7 @@ public class ProcessWebComService : IComService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogWarning("SendSmsAsync API error for {Mobile}: {Body}", mobile, responseBody);
|
_logger.LogWarning("SendSmsAsync API error for {Mobile}: {Status} {Body}", mobile, (int)status, responseBody);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -191,7 +198,7 @@ public class ProcessWebComService : IComService
|
|||||||
|
|
||||||
// ── Private helpers ────────────────────────────────────────────────────────
|
// ── Private helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async Task<(bool ok, string body)> PostToApiAsync(string reference, string comtype, object communication)
|
private async Task<(bool ok, System.Net.HttpStatusCode status, string body)> PostToApiAsync(string reference, string comtype, object communication)
|
||||||
{
|
{
|
||||||
var client = _httpClientFactory.CreateClient("ProcessWebMailer");
|
var client = _httpClientFactory.CreateClient("ProcessWebMailer");
|
||||||
|
|
||||||
@@ -214,23 +221,21 @@ public class ProcessWebComService : IComService
|
|||||||
|
|
||||||
var response = await client.PostAsync($"{_settings.BaseUrl}/mailer/push_com", content);
|
var response = await client.PostAsync($"{_settings.BaseUrl}/mailer/push_com", content);
|
||||||
string responseBody = await response.Content.ReadAsStringAsync();
|
string responseBody = await response.Content.ReadAsStringAsync();
|
||||||
return (response.IsSuccessStatusCode, responseBody);
|
return (response.IsSuccessStatusCode, response.StatusCode, responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task WriteAuditLogAsync(string reference, string guid, string config,
|
/// <summary>
|
||||||
|
/// Persists one communication attempt to <c>[dbo].[fds__logEmail]</c>. Called on every path —
|
||||||
|
/// success, API error (with the mailer's raw response in <paramref name="errors"/>), exceptions,
|
||||||
|
/// invalid address and the disabled-service case — so the email log is always complete. Wrapped
|
||||||
|
/// in try/catch so a logging failure never breaks (or masks) the send itself. Overridable for tests.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual async Task WriteAuditLogAsync(string reference, string guid, string config,
|
||||||
DateTime sent, bool success, IEnumerable<string> errors)
|
DateTime sent, bool success, IEnumerable<string> errors)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var pl = new List<SqlParameter>
|
var pl = BuildAuditLogParameters(reference, guid, config, sent, success, errors);
|
||||||
{
|
|
||||||
SQL_VarChar("@Ref", reference),
|
|
||||||
SQL_VarChar("@guid", guid),
|
|
||||||
SQL_DateTime("@DateSent", sent == default ? DBNull.Value : (object)sent),
|
|
||||||
SQL_NVarChar("@config", config, dbNull_IfEmpty: true),
|
|
||||||
SQL_Bit("@success", success),
|
|
||||||
SQL_NVarChar("@log", JsonConvert.SerializeObject(errors.ToList()))
|
|
||||||
};
|
|
||||||
await setSQLValue_async(
|
await setSQLValue_async(
|
||||||
"EXECUTE [dbo].[fds__logEmail] @Ref, @guid, @DateSent, @config, @success, @log;",
|
"EXECUTE [dbo].[fds__logEmail] @Ref, @guid, @DateSent, @config, @success, @log;",
|
||||||
_intranet.Intranet__SQLConnectionString, pl,
|
_intranet.Intranet__SQLConnectionString, pl,
|
||||||
@@ -242,6 +247,28 @@ public class ProcessWebComService : IComService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the parameter list for <c>[dbo].[fds__logEmail]</c>. <c>fds__emaillog</c> declares
|
||||||
|
/// <c>config</c> and <c>DateSent</c> as NOT NULL, so neither may be sent as NULL:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><c>@config</c> is stored as an empty string (never NULL) when unset.</item>
|
||||||
|
/// <item><c>@DateSent</c> falls back to the current (attempt) time when the send did not
|
||||||
|
/// complete — the <c>success</c> flag distinguishes a real send-time from a failed attempt.</item>
|
||||||
|
/// </list>
|
||||||
|
/// Pure/side-effect-free so the NOT NULL contract can be unit-tested without a database.
|
||||||
|
/// </summary>
|
||||||
|
internal static List<SqlParameter> BuildAuditLogParameters(string reference, string guid,
|
||||||
|
string config, DateTime sent, bool success, IEnumerable<string> errors) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
SQL_VarChar("@Ref", reference),
|
||||||
|
SQL_VarChar("@guid", guid ?? ""),
|
||||||
|
SQL_DateTime("@DateSent", sent == default ? DateTime.UtcNow : sent),
|
||||||
|
SQL_NVarChar("@config", config ?? ""),
|
||||||
|
SQL_Bit("@success", success),
|
||||||
|
SQL_NVarChar("@log", JsonConvert.SerializeObject(errors.ToList()))
|
||||||
|
};
|
||||||
|
|
||||||
private string BuildSignature()
|
private string BuildSignature()
|
||||||
{
|
{
|
||||||
string sigPath = Path.Combine(AppContext.BaseDirectory,
|
string sigPath = Path.Combine(AppContext.BaseDirectory,
|
||||||
|
|||||||
@@ -230,18 +230,13 @@ let $adm = {
|
|||||||
button: $adt.testmail_btn,
|
button: $adt.testmail_btn,
|
||||||
size: [420, 560],
|
size: [420, 560],
|
||||||
addcontent: $$.dc('adm_note').text($adt.tm_hint),
|
addcontent: $$.dc('adm_note').text($adt.tm_hint),
|
||||||
submit: function (e) {
|
url: $ocms.url('admin/testmail'),
|
||||||
let c = $(this).ldng(1);
|
typedvalues: true,
|
||||||
let qs = c.serializeObject(true, { typedvalues: true });
|
/* Runs inside jQuery's success dispatch — it must never throw, or postXT's
|
||||||
$ocms.postXT({
|
`complete` (which clears the page loading indicator) is skipped. */
|
||||||
url: $ocms.url('admin/testmail'), data: qs, timeout: 60000, success: function (r) {
|
success: function (r) {
|
||||||
let res = r.result || {};
|
let res = (r && r.result) || {};
|
||||||
alert(res.message || (res.sent ? $adt.tm_sent : $adt.tm_failed));
|
alert(res.message || (res.sent === true ? $adt.tm_sent : $adt.tm_failed));
|
||||||
c.trigger('modal_close');
|
|
||||||
}, error: function () {
|
|
||||||
alert($adt.tm_failed);
|
|
||||||
}, complete: function () { c.ldng(0); }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,18 +315,13 @@ let $adm = {
|
|||||||
button: $adt.testmail_btn,
|
button: $adt.testmail_btn,
|
||||||
size: [420, 560],
|
size: [420, 560],
|
||||||
addcontent: $$.dc('adm_note').text($adt.tm_hint),
|
addcontent: $$.dc('adm_note').text($adt.tm_hint),
|
||||||
submit: function (e) {
|
url: $ocms.url('admin/testmail'),
|
||||||
let c = $(this).ldng(1);
|
typedvalues: true,
|
||||||
let qs = c.serializeObject(true, { typedvalues: true });
|
/* Runs inside jQuery's success dispatch — it must never throw, or postXT's
|
||||||
$ocms.postXT({
|
`complete` (which clears the page loading indicator) is skipped. */
|
||||||
url: $ocms.url('admin/testmail'), data: qs, timeout: 60000, success: function (r) {
|
success: function (r) {
|
||||||
let res = r.result || {};
|
let res = (r && r.result) || {};
|
||||||
alert(res.message || (res.sent ? $adt.tm_sent : $adt.tm_failed));
|
alert(res.message || (res.sent === true ? $adt.tm_sent : $adt.tm_failed));
|
||||||
c.trigger('modal_close');
|
|
||||||
}, error: function () {
|
|
||||||
alert($adt.tm_failed);
|
|
||||||
}, complete: function () { c.ldng(0); }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
Submodule OCORE updated: bea48b312a...91eb660610
+1
-1
Submodule OCORE_web updated: 70c5874352...552ceca7bf
Reference in New Issue
Block a user