Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78ad854f41
|
||
|
|
62e12e9ebe |
No files matched your search
+2
-1
@@ -5,9 +5,10 @@ All notable changes to this project are documented here. The format follows Keep
|
||||
## [0.1.0] - Unreleased
|
||||
|
||||
### Changed
|
||||
- Switch the optional qbit-email integration to the published `com.kingsrook.qbits.email.api` package in `0.1.0-RC.3` (fixes #13). Preserve MARKETING messages, idempotency keys, execution links and outcome mapping; use typed readiness failures so missing providers or brand postal addresses remain send-time rejections. Upgrade `qbit-build-parent` to `2.1.1` and remove all per-repository guardrail allow-lists.
|
||||
- Resolve architecture finding F5 (#9): depend on `qbit-assistant-provider-api` and `qbit-assistant-provider-openai` `0.1.0-RC.11`, with no full assistant dependency, including in tests.
|
||||
- Replace the local OpenAI-compatible implementation with the published provider and document `ProviderSettings`, environment-key resolution and provider-name migration. Agent tests use a local scripted double.
|
||||
- Upgrade QQQ to `4.1.0-RC.6` and `qbit-build-parent` to `2.1.0`; enable architecture enforcement and strict UI API routes with explicit provider API/artifact allowances.
|
||||
- Upgrade QQQ to `4.1.0-RC.6`; enable architecture enforcement and strict UI API routes.
|
||||
|
||||
### Added
|
||||
- Product-contingent possible values for editable campaign, claim, finding, metric and outcome-event references.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
> Marketing for QQQ applications: products and their verified evidence, campaigns, content with revision history, exact human approval, controlled execution, honest measurement, and a marketing agent that prepares work but never publishes.
|
||||
|
||||
**Status:** 0.1.0, not yet published.
|
||||
- Built against QQQ `4.1.0-RC.6` with `qbit-build-parent` `2.1.0`.
|
||||
- Built against QQQ `4.1.0-RC.6` with `qbit-build-parent` `2.1.1`.
|
||||
- Hosted in QRun-IO/Voyage next to qbit-crm, on a local branch.
|
||||
- The real integrations are implemented but unconfigured. See [what is configured](#what-is-implemented-what-is-not-configured).
|
||||
|
||||
@@ -107,7 +107,7 @@ When the optional [qbit-email](https://github.com/QRun-IO/qbit-email) qbit is pr
|
||||
- Idempotency key `marketing:<content id>:<approval id>:<recipient key>`; the linked record is the `marketing_execution` row. A repeat is a duplicate, never a second send.
|
||||
- Accepted means queued in the qbit-email outbox. A suppressed recipient is skipped, not failed (the result says how many); a refusal by qbit-email is a rejected execution with its reason.
|
||||
- Needs qbit-email's `publicBaseUrl`, the `QQQ_EMAIL_UNSUBSCRIBE_KEY` secret, an enabled default provider and a default brand with a postal address. Without the first two the transport reports *Not configured* and says which is missing.
|
||||
- qbit-email is an optional dependency (`qbit-email.version`, a SNAPSHOT until its first RC) and needs QQQ `4.1.0-RC.5`.
|
||||
- qbit-email is an optional dependency pinned to `0.1.0-RC.3` from Maven Central (`qbit-email.version`). The integration uses only its published `com.kingsrook.qbits.email.api` package for installation checks, readiness, sends and lookups, with the parent's default guardrail allow-lists.
|
||||
|
||||
## What is implemented, what is not configured
|
||||
|
||||
|
||||
@@ -26,19 +26,19 @@
|
||||
<qbit-assistant.version>0.1.0-RC.11</qbit-assistant.version>
|
||||
<qbit.guardrails.enforce>true</qbit.guardrails.enforce>
|
||||
<qbit.guardrails.strictUiApiRoutes>true</qbit.guardrails.strictUiApiRoutes>
|
||||
<!-- 2.1.0 matches package prefixes, not class names; provider-api owns ModelPricing. -->
|
||||
<!-- TEMPORARY: qbit-email internals until it publishes an API (QRun-IO/qbit-marketing#13). -->
|
||||
<qbit.guardrails.allowedApiPackages>com.kingsrook.qbits.objectstorage.api,com.kingsrook.qbits.assistant.provider,com.kingsrook.qbits.assistant.engine,com.kingsrook.qbits.email</qbit.guardrails.allowedApiPackages>
|
||||
<qbit.guardrails.allowedQbitArtifacts>com.kingsrook.qbits:qbit-object-storage,com.kingsrook.qbits:*-api,com.kingsrook.qbits:qbit-assistant-provider-openai,com.kingsrook.qbits:qbit-email</qbit.guardrails.allowedQbitArtifacts>
|
||||
<!-- qbit-email from Maven Central -->
|
||||
<qbit-email.version>0.1.0-RC.1</qbit-email.version>
|
||||
<!-- Assistant provider SPI only (not email): these replace the parent lists, so they repeat -->
|
||||
<!-- the parent defaults (object storage, qbit-email's api, *-api) plus the assistant entries. -->
|
||||
<qbit.guardrails.allowedApiPackages>com.kingsrook.qbits.objectstorage.api,com.kingsrook.qbits.email.api,com.kingsrook.qbits.assistant.provider,com.kingsrook.qbits.assistant.engine</qbit.guardrails.allowedApiPackages>
|
||||
<qbit.guardrails.allowedQbitArtifacts>com.kingsrook.qbits:qbit-object-storage,com.kingsrook.qbits:qbit-email,com.kingsrook.qbits:*-api,com.kingsrook.qbits:qbit-assistant-provider-openai</qbit.guardrails.allowedQbitArtifacts>
|
||||
<qbit-email.version>0.1.0-RC.3</qbit-email.version>
|
||||
<h2.version>2.3.232</h2.version>
|
||||
</properties>
|
||||
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>2.1.0</version>
|
||||
<version>2.1.1</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
|
||||
@@ -168,7 +168,7 @@ public class MarketingRuntime implements QSupplementalInstanceMetaData
|
||||
{
|
||||
try
|
||||
{
|
||||
Class.forName("com.kingsrook.qbits.email.outbox.EmailService");
|
||||
Class.forName("com.kingsrook.qbits.email.api.EmailApi");
|
||||
return (true);
|
||||
}
|
||||
catch(ClassNotFoundException | LinkageError e)
|
||||
|
||||
@@ -20,23 +20,19 @@ package com.kingsrook.qbits.marketing.email;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.email.EmailQBitConfig;
|
||||
import com.kingsrook.qbits.email.EmailRuntime;
|
||||
import com.kingsrook.qbits.email.metadata.EmailTables;
|
||||
import com.kingsrook.qbits.email.outbox.EmailCategory;
|
||||
import com.kingsrook.qbits.email.outbox.EmailRequest;
|
||||
import com.kingsrook.qbits.email.outbox.EmailSendResult;
|
||||
import com.kingsrook.qbits.email.outbox.EmailService;
|
||||
import com.kingsrook.qbits.email.support.SystemWrites;
|
||||
import com.kingsrook.qbits.email.api.EmailApi;
|
||||
import com.kingsrook.qbits.email.api.EmailApiCategory;
|
||||
import com.kingsrook.qbits.email.api.EmailApiMessage;
|
||||
import com.kingsrook.qbits.email.api.EmailApiReadiness;
|
||||
import com.kingsrook.qbits.email.api.EmailApiReadinessCheck;
|
||||
import com.kingsrook.qbits.email.api.EmailApiRequest;
|
||||
import com.kingsrook.qbits.email.api.EmailApiResult;
|
||||
import com.kingsrook.qbits.marketing.model.MarketingExecution;
|
||||
import com.kingsrook.qbits.marketing.model.enums.ChannelType;
|
||||
import com.kingsrook.qbits.marketing.transport.MarketingTransport;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
@@ -88,8 +84,8 @@ public class QbitEmailTransport implements MarketingTransport
|
||||
{
|
||||
try
|
||||
{
|
||||
Class.forName("com.kingsrook.qbits.email.outbox.EmailService");
|
||||
return (EmailRuntime.config(qInstance) != null);
|
||||
Class.forName("com.kingsrook.qbits.email.api.EmailApi");
|
||||
return (EmailApi.isInstalled(qInstance));
|
||||
}
|
||||
catch(ClassNotFoundException | LinkageError e)
|
||||
{
|
||||
@@ -119,28 +115,30 @@ public class QbitEmailTransport implements MarketingTransport
|
||||
@Override
|
||||
public TransportStatus getStatus()
|
||||
{
|
||||
EmailQBitConfig emailConfig = isInstalled(QContext.getQInstance()) ? EmailRuntime.config() : null;
|
||||
if(emailConfig == null)
|
||||
if(!isInstalled(QContext.getQInstance()))
|
||||
{
|
||||
return (new TransportStatus(TransportStatus.State.NOT_CONFIGURED, "The email qbit (qbit-email) is not installed in this application."));
|
||||
}
|
||||
if(!StringUtils.hasContent(emailConfig.getPublicBaseUrl()))
|
||||
{
|
||||
return (new TransportStatus(TransportStatus.State.NOT_CONFIGURED, "Marketing email needs the email qbit's publicBaseUrl (the unsubscribe link base)."));
|
||||
}
|
||||
|
||||
String key = null;
|
||||
try
|
||||
{
|
||||
key = emailConfig.getSecretResolver().apply(UNSUBSCRIBE_KEY_NAME);
|
||||
EmailApiReadiness readiness = EmailApi.readiness(QContext.getQInstance(), EmailApiCategory.MARKETING);
|
||||
if(readiness.failed(EmailApiReadinessCheck.NOT_INSTALLED))
|
||||
{
|
||||
return (new TransportStatus(TransportStatus.State.NOT_CONFIGURED, "The email qbit (qbit-email) is not installed in this application."));
|
||||
}
|
||||
if(readiness.failed(EmailApiReadinessCheck.PUBLIC_BASE_URL))
|
||||
{
|
||||
return (new TransportStatus(TransportStatus.State.NOT_CONFIGURED, "Marketing email needs the email qbit's publicBaseUrl (the unsubscribe link base)."));
|
||||
}
|
||||
if(readiness.failed(EmailApiReadinessCheck.UNSUBSCRIBE_KEY))
|
||||
{
|
||||
return (new TransportStatus(TransportStatus.State.NOT_CONFIGURED, "Marketing email needs the " + UNSUBSCRIBE_KEY_NAME + " secret (it signs the unsubscribe links)."));
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
catch(QException e)
|
||||
{
|
||||
LOG.warn("Could not resolve the unsubscribe key", logPair("error", e.getClass().getSimpleName()));
|
||||
}
|
||||
if(!StringUtils.hasContent(key))
|
||||
{
|
||||
return (new TransportStatus(TransportStatus.State.NOT_CONFIGURED, "Marketing email needs the " + UNSUBSCRIBE_KEY_NAME + " secret (it signs the unsubscribe links)."));
|
||||
LOG.warn("Could not check email qbit readiness", logPair("error", e.getClass().getSimpleName()));
|
||||
return (new TransportStatus(TransportStatus.State.CONFIGURED, "Could not check the email qbit's configuration; retry when its configuration can be read."));
|
||||
}
|
||||
return (new TransportStatus(TransportStatus.State.OPERATIONAL, "Queues through the email qbit's outbox with its unsubscribe link and suppression list."));
|
||||
}
|
||||
@@ -183,7 +181,7 @@ public class QbitEmailTransport implements MarketingTransport
|
||||
|
||||
try
|
||||
{
|
||||
EmailSendResult result = EmailService.send(emailRequest(request, recipient));
|
||||
EmailApiResult result = EmailApi.send(emailRequest(request, recipient));
|
||||
switch(result.status())
|
||||
{
|
||||
case QUEUED -> queued++;
|
||||
@@ -227,8 +225,8 @@ public class QbitEmailTransport implements MarketingTransport
|
||||
{
|
||||
try
|
||||
{
|
||||
String prefix = "marketing:" + stripPrefix(idempotencyKey) + ":";
|
||||
List<QRecord> rows = SystemWrites.query(EmailTables.MESSAGE, new QQueryFilter(new QFilterCriteria("idempotencyKey", QCriteriaOperator.STARTS_WITH, prefix)).withLimit(1));
|
||||
String prefix = "marketing:" + stripPrefix(idempotencyKey) + ":";
|
||||
List<EmailApiMessage> rows = EmailApi.findMessagesByIdempotencyKeyPrefix(prefix, 1);
|
||||
if(rows.isEmpty())
|
||||
{
|
||||
return (new TransportLookup(TransportLookup.State.NOT_FOUND, null));
|
||||
@@ -247,7 +245,7 @@ public class QbitEmailTransport implements MarketingTransport
|
||||
/*******************************************************************************
|
||||
** One MARKETING message to one recipient.
|
||||
*******************************************************************************/
|
||||
private EmailRequest emailRequest(TransportRequest request, Recipient recipient)
|
||||
private EmailApiRequest emailRequest(TransportRequest request, Recipient recipient)
|
||||
{
|
||||
String body = request.body() == null ? "" : request.body();
|
||||
if(StringUtils.hasContent(request.destinationUrl()) && request.destinationUrl().matches("(?i)https?://\\S+"))
|
||||
@@ -256,11 +254,11 @@ public class QbitEmailTransport implements MarketingTransport
|
||||
body = body + "\n\n[" + label + "](" + request.destinationUrl() + ")";
|
||||
}
|
||||
|
||||
return (new EmailRequest()
|
||||
return (new EmailApiRequest()
|
||||
.withTo(recipient.address())
|
||||
.withSubject(request.title() == null ? "" : request.title().replaceAll("[\\r\\n]+", " ").trim())
|
||||
.withMarkdownBody(body)
|
||||
.withCategory(EmailCategory.MARKETING)
|
||||
.withCategory(EmailApiCategory.MARKETING)
|
||||
.withIdempotencyKey(baseKey(request) + ":" + recipient.key())
|
||||
.withLinkedRecord(MarketingExecution.TABLE_NAME, request.executionId())
|
||||
.withSender("marketing", "execution"));
|
||||
|
||||
@@ -21,9 +21,9 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.email.EmailQBitConfig;
|
||||
import com.kingsrook.qbits.email.EmailQBitProducer;
|
||||
import com.kingsrook.qbits.email.metadata.EmailTables;
|
||||
import com.kingsrook.qbits.email.permissions.EmailPermissions;
|
||||
import com.kingsrook.qbits.email.support.SystemWrites;
|
||||
import com.kingsrook.qbits.email.api.EmailApi;
|
||||
import com.kingsrook.qbits.email.api.EmailApiCategory;
|
||||
import com.kingsrook.qbits.email.api.EmailApiMessage;
|
||||
import com.kingsrook.qbits.marketing.MarketingQBitConfig;
|
||||
import com.kingsrook.qbits.marketing.MarketingQBitProducer;
|
||||
import com.kingsrook.qbits.marketing.MarketingRuntime;
|
||||
@@ -34,10 +34,13 @@ import com.kingsrook.qbits.marketing.transport.MarketingTransport.Recipient;
|
||||
import com.kingsrook.qbits.marketing.transport.MarketingTransport.TransportRequest;
|
||||
import com.kingsrook.qbits.marketing.transport.MarketingTransport.TransportResult;
|
||||
import com.kingsrook.qbits.marketing.transport.QqqMessagingEmailTransport;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
@@ -90,7 +93,7 @@ class QbitEmailTransportTest
|
||||
new QInstanceValidator().validate(qInstance);
|
||||
|
||||
List<String> permissions = new ArrayList<>(MarketingPermissions.ALL);
|
||||
permissions.addAll(EmailPermissions.ALL);
|
||||
permissions.addAll(List.of("email.view", "email.send", "email.manage", "email.admin", "email.hasAccess"));
|
||||
QContext.init(qInstance, new QSession().withPermissions(permissions));
|
||||
return (qInstance);
|
||||
}
|
||||
@@ -103,7 +106,7 @@ class QbitEmailTransportTest
|
||||
private void provider() throws QException
|
||||
{
|
||||
brand();
|
||||
SystemWrites.insertOne(EmailTables.PROVIDER, new QRecord().withValue("name", "relay").withValue("label", "Relay").withValue("kind", "SMTP").withValue("host", "127.0.0.1")
|
||||
insert("email_provider", new QRecord().withValue("name", "relay").withValue("label", "Relay").withValue("kind", "SMTP").withValue("host", "127.0.0.1")
|
||||
.withValue("port", 2525).withValue("security", "NONE").withValue("fromAddress", "sender@example.com").withValue("enabled", true).withValue("isDefault", true));
|
||||
}
|
||||
|
||||
@@ -114,12 +117,23 @@ class QbitEmailTransportTest
|
||||
*******************************************************************************/
|
||||
private void brand() throws QException
|
||||
{
|
||||
SystemWrites.insertOne(EmailTables.BRAND, new QRecord().withValue("name", "default").withValue("label", "Example").withValue("isDefault", true)
|
||||
insert("email_brand", new QRecord().withValue("name", "default").withValue("label", "Example").withValue("isDefault", true)
|
||||
.withValue("postalAddress", "1 Main St, Springfield"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Configure the email qbit's editable tables through ordinary QQQ actions.
|
||||
*******************************************************************************/
|
||||
private void insert(String table, QRecord record) throws QException
|
||||
{
|
||||
var output = new InsertAction().execute(new InsertInput(table).withRecords(List.of(record)));
|
||||
assertThat(output.getRecords()).hasSize(1).allSatisfy(row -> assertThat(row.getErrors()).isNullOrEmpty());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@@ -151,22 +165,32 @@ class QbitEmailTransportTest
|
||||
assertThat(result.outcome()).as(result.message()).isEqualTo(TransportResult.Outcome.ACCEPTED);
|
||||
assertThat(result.providerStatus()).isEqualTo("queued");
|
||||
|
||||
List<QRecord> rows = SystemWrites.query(EmailTables.MESSAGE, new QQueryFilter());
|
||||
List<EmailApiMessage> rows = EmailApi.findMessagesByIdempotencyKeyPrefix("marketing:3:4:", 10);
|
||||
assertThat(rows).hasSize(2);
|
||||
assertThat(rows).extracting(r -> r.getValueString("idempotencyKey")).containsExactlyInAnyOrder("marketing:3:4:crm-contact:1", "marketing:3:4:crm-contact:2");
|
||||
assertThat(rows).extracting(EmailApiMessage::idempotencyKey).containsExactlyInAnyOrder("marketing:3:4:crm-contact:1", "marketing:3:4:crm-contact:2");
|
||||
assertThat(rows).allSatisfy(r ->
|
||||
{
|
||||
assertThat(r.getValueString("category")).isEqualTo("MARKETING");
|
||||
assertThat(r.category()).isEqualTo(EmailApiCategory.MARKETING);
|
||||
assertThat(r.linkedTableName()).isEqualTo("marketing_execution");
|
||||
assertThat(r.linkedRecordId()).isEqualTo("7");
|
||||
assertThat(r.templateName()).isNull();
|
||||
assertThat(r.status()).isEqualTo("QUEUED");
|
||||
});
|
||||
|
||||
// The public API intentionally omits content; inspect it as an email admin.
|
||||
List<QRecord> messages = new QueryAction().execute(new QueryInput("email_message")
|
||||
.withShouldFetchHeavyFields(true).withShouldOmitHiddenFields(false)).getRecords();
|
||||
assertThat(messages).hasSize(2).allSatisfy(r ->
|
||||
{
|
||||
assertThat(r.getValueString("subject")).isEqualTo("Big news");
|
||||
assertThat(r.getValueString("linkedTableName")).isEqualTo("marketing_execution");
|
||||
assertThat(r.getValueString("linkedRecordId")).isEqualTo("7");
|
||||
assertThat(r.getValueString("senderQbit")).isEqualTo("marketing");
|
||||
assertThat(r.getValueString("senderProcess")).isEqualTo("execution");
|
||||
assertThat(r.getValueString("htmlBody")).contains("https://example.org/try").contains("nsubscribe");
|
||||
assertThat(r.getValueString("toAddresses")).doesNotContain(",");
|
||||
});
|
||||
|
||||
assertThat(transport.send(request).outcome()).isEqualTo(TransportResult.Outcome.DUPLICATE);
|
||||
assertThat(SystemWrites.query(EmailTables.MESSAGE, new QQueryFilter())).hasSize(2);
|
||||
assertThat(EmailApi.findMessagesByIdempotencyKeyPrefix("marketing:3:4:", 10)).hasSize(2);
|
||||
|
||||
assertThat(transport.lookup("marketing-execution:3:4").state()).isEqualTo(MarketingTransport.TransportLookup.State.FOUND_ACCEPTED);
|
||||
assertThat(transport.lookup("marketing-execution:9:9").state()).isEqualTo(MarketingTransport.TransportLookup.State.NOT_FOUND);
|
||||
@@ -183,7 +207,7 @@ class QbitEmailTransportTest
|
||||
{
|
||||
instance(true, "https://app.example.org", "k", new MarketingQBitConfig());
|
||||
provider();
|
||||
SystemWrites.insertOne(EmailTables.SUPPRESSION, new QRecord().withValue("address", "gone@example.org").withValue("reason", "UNSUBSCRIBED").withValue("scope", "MARKETING"));
|
||||
insert("email_suppression", new QRecord().withValue("address", "gone@example.org").withValue("reason", "UNSUBSCRIBED").withValue("scope", "MARKETING"));
|
||||
|
||||
MarketingTransport transport = new QbitEmailTransport();
|
||||
TransportResult result = transport.send(request("marketing-execution:1:1", new Recipient("crm-contact:9", "gone@example.org")));
|
||||
@@ -216,6 +240,7 @@ class QbitEmailTransportTest
|
||||
assertThat(new QbitEmailTransport().getStatus().detail()).contains("publicBaseUrl");
|
||||
|
||||
instance(true, "https://app.example.org", "k", new MarketingQBitConfig());
|
||||
assertThat(transport.getStatus().state()).isEqualTo(MarketingTransport.TransportStatus.State.OPERATIONAL);
|
||||
TransportResult refused = new QbitEmailTransport().send(request("marketing-execution:5:5", new Recipient("a", "a@example.org")));
|
||||
assertThat(refused.outcome()).isEqualTo(TransportResult.Outcome.REJECTED);
|
||||
assertThat(refused.message()).contains("refused the message");
|
||||
@@ -227,6 +252,25 @@ class QbitEmailTransportTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A missing postal address is a send-time refusal, not NOT_CONFIGURED.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testMissingBrandIsRefusedAtSend() throws Exception
|
||||
{
|
||||
instance(true, "https://app.example.org", "k", new MarketingQBitConfig());
|
||||
insert("email_provider", new QRecord().withValue("name", "relay").withValue("label", "Relay").withValue("kind", "SMTP").withValue("host", "127.0.0.1")
|
||||
.withValue("port", 2525).withValue("security", "NONE").withValue("fromAddress", "sender@example.com").withValue("enabled", true).withValue("isDefault", true));
|
||||
|
||||
QbitEmailTransport transport = new QbitEmailTransport();
|
||||
assertThat(transport.getStatus().state()).isEqualTo(MarketingTransport.TransportStatus.State.OPERATIONAL);
|
||||
TransportResult result = transport.send(request("marketing-execution:8:8", new Recipient("a", "a@example.org")));
|
||||
assertThat(result.outcome()).isEqualTo(TransportResult.Outcome.REJECTED);
|
||||
assertThat(result.message()).contains("postal address");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Without the email qbit in the instance the name does not resolve to the
|
||||
** transport, and a host-registered transport always wins.
|
||||
|
||||
Reference in new issue
Block a user