diff --git a/CHANGELOG.md b/CHANGELOG.md index 4790972..723dc59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project are documented here. The format follows Keep ### Fixed +- Purge actions warn when providers are NONE or hostnames are OFF, and report requests as recorded but not sent. The home explains recorded-only purges and that this version does not run origin header checks. - Tag rule `{prefix.field}` placeholders now resolve the joined table from the `Id` field's possible-value source, so prefixed tables (`cms_blog`) work; a missing joined table is logged instead of silently skipped. ## [0.1.0] - Unreleased diff --git a/README.md b/README.md index 814712d..0754c09 100644 --- a/README.md +++ b/README.md @@ -97,3 +97,18 @@ KoF22 receiver and exact changes required. ## License Apache License 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE). + +### Before a CDN is live + +With no enabled CDN provider (including providers of type `NONE`), purges are +recorded only, not sent. A real provider also needs a `LIVE` or `LOCKED` hostname +with purging enabled. Purge From CDN, Purge Everything, and Retry Purge show a +Review warning and a recorded-only Result when sending is unavailable. Requests +for enabled providers remain visible with `SKIPPED_NONE_PROVIDER` or +`SKIPPED_STAGE_OFF`; with no enabled provider, no new request row is created. +The home Connection and Health cards explain the unconfigured state, and purge +activity labels recorded-only requests when disconnected. + +This version does not run origin header checks. The Origin headers card says +nothing has been checked until the host application writes results to the +standard `cdn_origin_check` table; those results continue to appear normally. diff --git a/src/main/java/com/kingsrook/qbits/cdn/app/CdnHomeData.java b/src/main/java/com/kingsrook/qbits/cdn/app/CdnHomeData.java index 7e73f38..787ca9d 100644 --- a/src/main/java/com/kingsrook/qbits/cdn/app/CdnHomeData.java +++ b/src/main/java/com/kingsrook/qbits/cdn/app/CdnHomeData.java @@ -61,14 +61,14 @@ public record CdnHomeData(List providers, List hostn ** A provider row as shown. *******************************************************************************/ public record ProviderView(Integer id, String name, String type, String distributionId, String dnsTarget, String originHost, - String lastHealthStatus, String lastHealthMessage, Instant lastSuccessfulTest, Long credentialAgeDays) + String lastHealthStatus, String lastHealthMessage, Instant lastSuccessfulTest, Long credentialAgeDays, Boolean enabled) { /******************************************************************************* ** Whether this is a real CDN (not the NONE placeholder). *******************************************************************************/ public boolean isConnected() { - return (!CdnProviderType.NONE.name().equals(type)); + return (Boolean.TRUE.equals(enabled) && !CdnProviderType.NONE.name().equals(type)); } } @@ -119,7 +119,7 @@ public record CdnHomeData(List providers, List hostn LocalDate rotated = ValueUtils.getValueAsLocalDate(row.getValue("credentialRotatedDate")); providers.add(new ProviderView(id, row.getValueString("name"), row.getValueString("providerType"), row.getValueString("zoneOrDistributionId"), row.getValueString("dnsTarget"), row.getValueString("originHost"), row.getValueString("lastHealthStatus"), row.getValueString("lastHealthMessage"), - ValueUtils.getValueAsInstant(row.getValue("lastSuccessfulTestDate")), rotated == null ? null : ChronoUnit.DAYS.between(rotated, today))); + ValueUtils.getValueAsInstant(row.getValue("lastSuccessfulTestDate")), rotated == null ? null : ChronoUnit.DAYS.between(rotated, today), row.getValueBoolean("enabled"))); if(CdnProviderType.CLOUDFRONT.name().equals(row.getValueString("providerType"))) { PathPlannerSettings settings = PathPlannerSettings.from(CdnPurgeDispatcher.configOf(row)); diff --git a/src/main/java/com/kingsrook/qbits/cdn/app/CdnHomeView.java b/src/main/java/com/kingsrook/qbits/cdn/app/CdnHomeView.java index 0457162..83cd0af 100644 --- a/src/main/java/com/kingsrook/qbits/cdn/app/CdnHomeView.java +++ b/src/main/java/com/kingsrook/qbits/cdn/app/CdnHomeView.java @@ -80,7 +80,7 @@ public final class CdnHomeView String text = "All CDN checks are OK."; if("NOT_CONNECTED".equals(state)) { - text = "No CDN connected yet."; + text = "No CDN provider configured: purges are recorded only."; } else if("ATTENTION".equals(state)) { @@ -99,7 +99,7 @@ public final class CdnHomeView open(html, "Connection"); if(!data.isConnected()) { - empty(html, "No CDN connected yet. The CloudFront distribution is created in the cutover window."); + empty(html, "No CDN provider configured: purges are recorded only. The CloudFront distribution is created in the cutover window."); } for(CdnHomeData.ProviderView provider : data.providers()) { @@ -182,6 +182,7 @@ public final class CdnHomeView Instant created = ValueUtils.getValueAsInstant(row.getValue("createDate")); html.append("
  • #") .append(esc(String.valueOf(row.getValue("id")))).append(" ").append(esc(row.getValueString("status"))) + .append(data.isConnected() ? "" : " - recorded only (not sent)") .append(created == null ? "" : ", " + WHEN.format(created)).append("
  • "); } html.append(""); @@ -224,7 +225,7 @@ public final class CdnHomeView open(html, "Origin headers"); if(data.originChecks().isEmpty()) { - empty(html, "No origin header check has run yet."); + empty(html, "Origin header checks are not run by this version; nothing has been checked."); } for(QRecord check : data.originChecks()) { diff --git a/src/main/java/com/kingsrook/qbits/cdn/processes/CdnProcesses.java b/src/main/java/com/kingsrook/qbits/cdn/processes/CdnProcesses.java index 5c5baab..420cb8d 100644 --- a/src/main/java/com/kingsrook/qbits/cdn/processes/CdnProcesses.java +++ b/src/main/java/com/kingsrook/qbits/cdn/processes/CdnProcesses.java @@ -167,6 +167,56 @@ public final class CdnProcesses + /******************************************************************************* + ** Summary counts for requests that cannot be sent, shared by all actions. + *******************************************************************************/ + private static class UnsentSummary + { + private final ProcessSummaryLine absent = line(Status.WARNING, + "record has no enabled CDN provider", "records have no enabled CDN provider", + "record had no enabled CDN provider (no purge request recorded)", "records had no enabled CDN provider (no purge requests recorded)"); + private final ProcessSummaryLine none = line(Status.WARNING, + "No CDN provider configured: purges are recorded only, not sent.", "No CDN provider configured: purges are recorded only, not sent.", + "purge request was recorded (not sent: no CDN provider)", "purge requests were recorded (not sent: no CDN provider)"); + private final ProcessSummaryLine off = line(Status.WARNING, + "CDN hostnames are not live: purges are recorded only, not sent.", "CDN hostnames are not live: purges are recorded only, not sent.", + "purge request was recorded (not sent: CDN hostnames are not live)", "purge requests were recorded (not sent: CDN hostnames are not live)"); + + + + /******************************************************************************* + ** Count a skipped request; returns whether it cannot be sent. + *******************************************************************************/ + boolean add(CdnPurgeStatus status) + { + if(status == CdnPurgeStatus.SKIPPED_NONE_PROVIDER) + { + none.incrementCount(1); + return (true); + } + if(status == CdnPurgeStatus.SKIPPED_STAGE_OFF) + { + off.incrementCount(1); + return (true); + } + return (false); + } + + + + /******************************************************************************* + ** Include only reasons encountered by this action. + *******************************************************************************/ + void append(ArrayList lines) + { + absent.addSelfToListIfAnyCount(lines); + none.addSelfToListIfAnyCount(lines); + off.addSelfToListIfAnyCount(lines); + } + } + + + /******************************************************************************* ** Scheduled: send due purges and poll in-progress ones. *******************************************************************************/ @@ -189,6 +239,7 @@ public final class CdnProcesses *******************************************************************************/ public static class PurgeRecordsTransform extends AbstractTransformStep implements ProcessSummaryProviderInterface { + private final UnsentSummary unsent = new UnsentSummary(); private final ProcessSummaryLine records = line(Status.OK, "record will be purged", "records will be purged", "record was purged", "records were purged"); private final ProcessSummaryLine noRule = line(Status.INFO, "record has no CDN tag rule (no change)", "records have no CDN tag rule (no change)", @@ -207,6 +258,7 @@ public final class CdnProcesses String table = input.getValueString(StreamedETLWithFrontendProcess.FIELD_SOURCE_TABLE); List rules = rules(table); TagRuleEngine engine = new TagRuleEngine(); + CdnPurgeStatus sending = CdnPurgeService.sendingStatus(null); for(QRecord record : input.getRecords()) { List tags = new ArrayList<>(); @@ -226,7 +278,10 @@ public final class CdnProcesses recordPaths.forEach(path -> paths.add(path.path())); record.setValue("cdnTags", String.join("\n", tags)); record.setValue("cdnPaths", com.kingsrook.qbits.cdn.purge.PurgeText.paths(recordPaths)); - records.incrementCountAndAddPrimaryKey(record.getValue("id")); + if(!unsent.add(sending)) + { + records.incrementCountAndAddPrimaryKey(record.getValue("id")); + } output.getRecords().add(record); } } @@ -249,6 +304,7 @@ public final class CdnProcesses lines.add(pathLine); } noRule.addSelfToListIfAnyCount(lines); + unsent.append(lines); return (lines); } } @@ -256,10 +312,11 @@ public final class CdnProcesses /******************************************************************************* - ** Purge From CDN, execute: one purge request (sent at once, hubs included). + ** Purge From CDN, execute: record requests with hubs included. *******************************************************************************/ public static class PurgeRecordsLoad extends AbstractLoadStep implements ProcessSummaryProviderInterface { + private final UnsentSummary unsent = new UnsentSummary(); private final ProcessSummaryLine requested = line(Status.OK, "purge request will be recorded", "purge requests will be recorded", "purge request was recorded (sent within seconds)", "purge requests were recorded (sent within seconds)"); @@ -277,7 +334,17 @@ public final class CdnProcesses List rows = CdnPurgeService.enqueue(new CdnPurgeService.Request(CdnPurgeKind.TAG.name(), com.kingsrook.qbits.cdn.purge.PurgeText.tags(record.getValueString("cdnTags")), com.kingsrook.qbits.cdn.purge.PurgeText.paths(record.getValueString("cdnPaths")), true, CdnPurgeSource.RECORD_ACTION, table, record.getValue("id"))); - requested.incrementCount(rows.size()); + if(rows.isEmpty()) + { + unsent.absent.incrementCount(1); + } + for(QRecord row : rows) + { + if(!unsent.add(CdnPurgeStatus.valueOf(row.getValueString("status")))) + { + requested.incrementCount(1); + } + } output.getRecords().add(record); } } @@ -296,6 +363,7 @@ public final class CdnProcesses { transform.noRule.addSelfToListIfAnyCount(lines); } + unsent.append(lines); return (lines); } } @@ -307,6 +375,7 @@ public final class CdnProcesses *******************************************************************************/ public static class RetryTransform extends AbstractTransformStep implements ProcessSummaryProviderInterface { + private final UnsentSummary unsent = new UnsentSummary(); private final ProcessSummaryLine retry = line(Status.OK, "purge request will be sent again", "purge requests will be sent again", "purge request was queued again", "purge requests were queued again"); private final ProcessSummaryLine skip = line(Status.INFO, "purge request has not failed (no change)", "purge requests have not failed (no change)", @@ -325,7 +394,10 @@ public final class CdnProcesses String status = record.getValueString("status"); if(CdnPurgeStatus.FAILED.name().equals(status) || CdnPurgeStatus.DEAD.name().equals(status) || CdnPurgeStatus.DEFERRED_BUDGET.name().equals(status)) { - retry.incrementCountAndAddPrimaryKey(record.getValue("id")); + if(!unsent.add(CdnPurgeService.sendingStatus(record.getValue("cdnProviderId")))) + { + retry.incrementCountAndAddPrimaryKey(record.getValue("id")); + } output.getRecords().add(record); } else @@ -346,6 +418,7 @@ public final class CdnProcesses ArrayList lines = new ArrayList<>(); retry.addSelfToListIfAnyCount(lines); skip.addSelfToListIfAnyCount(lines); + unsent.append(lines); return (lines); } } @@ -353,10 +426,16 @@ public final class CdnProcesses /******************************************************************************* - ** Retry Purge, execute: back to PENDING (sent on the next dispatcher pass). + ** Retry Purge, execute: queue live providers or record why sending is skipped. *******************************************************************************/ - public static class RetryLoad extends AbstractLoadStep + public static class RetryLoad extends AbstractLoadStep implements ProcessSummaryProviderInterface { + private final UnsentSummary unsent = new UnsentSummary(); + private final ProcessSummaryLine requested = line(Status.OK, "purge request will be recorded", "purge requests will be recorded", + "purge request was queued again", "purge requests were queued again"); + + + /******************************************************************************* ** Reset the rows. *******************************************************************************/ @@ -366,7 +445,12 @@ public final class CdnProcesses List updates = new ArrayList<>(); for(QRecord record : input.getRecords()) { - updates.add(new QRecord().withValue("id", record.getValue("id")).withValue("status", CdnPurgeStatus.PENDING.name()) + CdnPurgeStatus status = CdnPurgeService.sendingStatus(record.getValue("cdnProviderId")); + if(!unsent.add(status)) + { + requested.incrementCount(1); + } + updates.add(new QRecord().withValue("id", record.getValue("id")).withValue("status", status.name()) .withValue("source", CdnPurgeSource.RETRY.name()).withValue("attempts", 0).withValue("nextAttemptDate", null) .withValue("errorMessage", null).withValue("completedDate", null).withValue("modifyDate", CdnRuntime.clock().instant())); } @@ -375,6 +459,24 @@ public final class CdnProcesses output.getRecords().addAll(new UpdateAction().execute(new UpdateInput(CdnTables.PURGE_REQUEST).withRecords(updates)).getRecords()); } } + + + + /******************************************************************************* + ** Result lines from the requests recorded during execution. + *******************************************************************************/ + @Override + public ArrayList getProcessSummary(RunBackendStepOutput output, boolean isForResultScreen) + { + ArrayList lines = new ArrayList<>(); + requested.addSelfToListIfAnyCount(lines); + if(getTransformStep() instanceof RetryTransform transform) + { + transform.skip.addSelfToListIfAnyCount(lines); + } + unsent.append(lines); + return (lines); + } } @@ -384,6 +486,7 @@ public final class CdnProcesses *******************************************************************************/ public static class PurgeHostnameTransform extends AbstractTransformStep implements ProcessSummaryProviderInterface { + private final UnsentSummary unsent = new UnsentSummary(); private final ProcessSummaryLine providers = line(Status.WARNING, "CDN distribution will be purged completely (every hostname on it)", "CDN distributions will be purged completely (every hostname on them)", "CDN distribution purge was recorded", "CDN distribution purges were recorded"); @@ -402,7 +505,10 @@ public final class CdnProcesses Serializable providerId = record.getValue("cdnProviderId"); if(providerId != null && seen.add(providerId)) { - providers.incrementCountAndAddPrimaryKey(record.getValue("id")); + if(!unsent.add(CdnPurgeService.sendingStatus(providerId))) + { + providers.incrementCountAndAddPrimaryKey(record.getValue("id")); + } output.getRecords().add(record); } } @@ -418,6 +524,7 @@ public final class CdnProcesses { ArrayList lines = new ArrayList<>(); providers.addSelfToListIfAnyCount(lines); + unsent.append(lines); return (lines); } } @@ -427,8 +534,14 @@ public final class CdnProcesses /******************************************************************************* ** Purge Everything, execute. *******************************************************************************/ - public static class PurgeHostnameLoad extends AbstractLoadStep + public static class PurgeHostnameLoad extends AbstractLoadStep implements ProcessSummaryProviderInterface { + private final UnsentSummary unsent = new UnsentSummary(); + private final ProcessSummaryLine requested = line(Status.OK, "purge request will be recorded", "purge requests will be recorded", + "CDN distribution purge was recorded", "CDN distribution purges were recorded"); + + + /******************************************************************************* ** Record an ALL purge for each provider. *******************************************************************************/ @@ -437,11 +550,36 @@ public final class CdnProcesses { for(QRecord record : input.getRecords()) { - CdnPurgeService.enqueueFor(record.getValue("cdnProviderId"), new CdnPurgeService.Request(CdnPurgeKind.ALL.name(), List.of(), List.of(), true, + List rows = CdnPurgeService.enqueueFor(record.getValue("cdnProviderId"), new CdnPurgeService.Request(CdnPurgeKind.ALL.name(), List.of(), List.of(), true, CdnPurgeSource.MANUAL, CdnTables.HOSTNAME, record.getValue("id"))); + if(rows.isEmpty()) + { + unsent.absent.incrementCount(1); + } + for(QRecord row : rows) + { + if(!unsent.add(CdnPurgeStatus.valueOf(row.getValueString("status")))) + { + requested.incrementCount(1); + } + } output.getRecords().add(record); } } + + + + /******************************************************************************* + ** Result lines from the requests recorded during execution. + *******************************************************************************/ + @Override + public ArrayList getProcessSummary(RunBackendStepOutput output, boolean isForResultScreen) + { + ArrayList lines = new ArrayList<>(); + requested.addSelfToListIfAnyCount(lines); + unsent.append(lines); + return (lines); + } } diff --git a/src/main/java/com/kingsrook/qbits/cdn/purge/CdnPurgeService.java b/src/main/java/com/kingsrook/qbits/cdn/purge/CdnPurgeService.java index f052e9e..de822bf 100644 --- a/src/main/java/com/kingsrook/qbits/cdn/purge/CdnPurgeService.java +++ b/src/main/java/com/kingsrook/qbits/cdn/purge/CdnPurgeService.java @@ -120,6 +120,36 @@ public final class CdnPurgeService + /******************************************************************************* + ** Sending eligibility for one provider, or all enabled providers when null. + ** A pending status means at least one provider can send; otherwise the + ** status gives the same reason used when recording a new request. + *******************************************************************************/ + public static CdnPurgeStatus sendingStatus(Serializable providerId) throws QException + { + QQueryFilter filter = new QQueryFilter(new QFilterCriteria("enabled", QCriteriaOperator.EQUALS, true)); + if(providerId != null) + { + filter.addCriteria(new QFilterCriteria("id", QCriteriaOperator.EQUALS, providerId)); + } + CdnPurgeStatus status = CdnPurgeStatus.SKIPPED_NONE_PROVIDER; + for(QRecord provider : new QueryAction().execute(new QueryInput(CdnTables.PROVIDER).withFilter(filter)).getRecords()) + { + CdnPurgeStatus candidate = initialStatus(provider); + if(candidate == CdnPurgeStatus.PENDING) + { + return (candidate); + } + if(candidate == CdnPurgeStatus.SKIPPED_STAGE_OFF) + { + status = candidate; + } + } + return (status); + } + + + /******************************************************************************* ** Status of a new request for a provider. *******************************************************************************/ diff --git a/src/test/java/com/kingsrook/qbits/cdn/app/CdnHomeTest.java b/src/test/java/com/kingsrook/qbits/cdn/app/CdnHomeTest.java index 461f807..e7ade8a 100644 --- a/src/test/java/com/kingsrook/qbits/cdn/app/CdnHomeTest.java +++ b/src/test/java/com/kingsrook/qbits/cdn/app/CdnHomeTest.java @@ -58,10 +58,10 @@ class CdnHomeTest { CdnTestSupport.setUp(); String html = render(CdnWidgets.HOME_WIDGET); - assertThat(html).contains("No CDN connected yet.").contains("No edge statistics yet.").contains("No purges recorded yet.") - .contains("No path-billed provider connected yet.").contains("No origin header check has run yet.").contains("Nothing to compare yet.") + assertThat(html).contains("No CDN provider configured: purges are recorded only.").contains("No edge statistics yet.").contains("No purges recorded yet.") + .contains("No path-billed provider connected yet.").contains("Origin header checks are not run by this version; nothing has been checked.").contains("Nothing to compare yet.") .contains("minmax(min(100%,22rem),1fr)"); - assertThat(render(CdnWidgets.HEALTH_CARD_WIDGET)).contains("No CDN connected yet.").contains("href=\"/cdnAdmin\""); + assertThat(render(CdnWidgets.HEALTH_CARD_WIDGET)).contains("No CDN provider configured: purges are recorded only.").contains("href=\"/cdnAdmin\""); } @@ -99,7 +99,7 @@ class CdnHomeTest String html = CdnHomeView.render(data, "/cdnAdmin"); assertThat(html).contains("d1.cloudfront.net").contains(age + " days (rotate every 90) - rotation due").contains("1,500").contains("91.5%") .contains(" reviewLines = (List) review.getValues().get(StreamedETLWithFrontendProcess.FIELD_VALIDATION_SUMMARY); + for(ProcessSummaryLineInterface reviewLine : reviewLines) + { + ProcessSummaryLine summary = (ProcessSummaryLine) reviewLine; + summary.pickMessage(false); + if(summary.getMessage().contains("purges are recorded only")) + { + assertEquals(com.kingsrook.qqq.backend.core.model.actions.processes.Status.WARNING, summary.getStatus()); + } + } + RunProcessInput go = new RunProcessInput(); go.setProcessName(process); go.setProcessUUID(review.getProcessUUID()); @@ -137,6 +147,7 @@ class CdnProcessesTest new QRecord().withValue("cdnProviderId", 1).withValue("status", "SUCCEEDED")); List> lines = run(CdnProcesses.RETRY_PURGE, rows.get(0).getValue("id") + "," + rows.get(1).getValue("id")); assertThat(lines.get(0)).containsExactly("1 purge request will be sent again", "1 purge request has not failed (no change)"); + assertThat(lines.get(1)).contains("1 purge request was queued again"); QRecord retried = purges().get(0); assertEquals("PENDING", retried.getValueString("status")); assertEquals(0, retried.getValueInteger("attempts")); @@ -155,9 +166,99 @@ class CdnProcessesTest CdnTestSupport.insert(CdnTables.PROVIDER, new QRecord().withValue("name", "other").withValue("providerType", "CLOUDFRONT").withValue("enabled", true)); List> lines = run(CdnProcesses.PURGE_HOSTNAME, "1"); assertThat(lines.get(0)).containsExactly("1 CDN distribution will be purged completely (every hostname on it)"); + assertThat(lines.get(1)).containsExactly("1 CDN distribution purge was recorded"); List rows = purges(); assertThat(rows).hasSize(1); assertEquals("ALL", rows.get(0).getValueString("kind")); assertEquals(1, rows.get(0).getValueInteger("cdnProviderId")); } + + + + /******************************************************************************* + ** Every action reports NONE and OFF honestly, including plural results. + *******************************************************************************/ + @Test + void recordedOnlySummaries() throws Exception + { + for(boolean none : List.of(true, false)) + { + for(String process : List.of(CdnProcesses.PURGE_PREFIX + "post", CdnProcesses.PURGE_HOSTNAME, CdnProcesses.RETRY_PURGE)) + { + setUp(); + String table = none ? CdnTables.PROVIDER : CdnTables.HOSTNAME; + new com.kingsrook.qqq.backend.core.actions.tables.UpdateAction().execute( + new com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput(table) + .withRecord(new QRecord().withValue("id", 1).withValue(none ? "providerType" : "stage", none ? "NONE" : "OFF"))); + String ids = "1"; + if(process.equals(CdnProcesses.PURGE_PREFIX + "post")) + { + CdnTestSupport.insert("post", new QRecord().withValue("slug", "one"), new QRecord().withValue("slug", "two")); + ids = "1,2"; + } + else if(process.equals(CdnProcesses.RETRY_PURGE)) + { + CdnTestSupport.insert(CdnTables.PURGE_REQUEST, + new QRecord().withValue("cdnProviderId", 1).withValue("status", "FAILED"), + new QRecord().withValue("cdnProviderId", 1).withValue("status", "DEAD")); + ids = "1,2"; + } + List> lines = run(process, ids); + String warning = none ? "No CDN provider configured" : "CDN hostnames are not live"; + assertThat(lines.get(0)).anyMatch(line -> line.contains(warning + ": purges are recorded only, not sent.")); + assertThat(lines.get(0)).noneMatch(line -> line.contains("will be purged") || line.contains("will be sent again")); + String reason = none ? "no CDN provider" : "CDN hostnames are not live"; + assertThat(lines.get(1)).containsExactly(process.equals(CdnProcesses.PURGE_HOSTNAME) + ? "1 purge request was recorded (not sent: " + reason + ")" + : "2 purge requests were recorded (not sent: " + reason + ")"); + assertThat(purges()).allMatch(row -> (none ? "SKIPPED_NONE_PROVIDER" : "SKIPPED_STAGE_OFF").equals(row.getValueString("status"))); + } + } + } + + + + /******************************************************************************* + ** Eligibility considers enabled providers and their purge-enabled hosts. + *******************************************************************************/ + @Test + void sendingEligibility() throws Exception + { + CdnTestSupport.setUp(); + assertEquals(com.kingsrook.qbits.cdn.model.CdnPurgeStatus.SKIPPED_NONE_PROVIDER, + com.kingsrook.qbits.cdn.purge.CdnPurgeService.sendingStatus(null)); + CdnTestSupport.liveProvider(CdnProviderType.NONE, null); + Integer live = CdnTestSupport.liveProvider(CdnProviderType.CLOUDFRONT, null); + assertEquals(com.kingsrook.qbits.cdn.model.CdnPurgeStatus.PENDING, + com.kingsrook.qbits.cdn.purge.CdnPurgeService.sendingStatus(null)); + new com.kingsrook.qqq.backend.core.actions.tables.UpdateAction().execute( + new com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput(CdnTables.HOSTNAME) + .withRecord(new QRecord().withValue("id", 2).withValue("purgeEnabled", false))); + assertEquals(com.kingsrook.qbits.cdn.model.CdnPurgeStatus.SKIPPED_STAGE_OFF, + com.kingsrook.qbits.cdn.purge.CdnPurgeService.sendingStatus(live)); + new com.kingsrook.qqq.backend.core.actions.tables.UpdateAction().execute( + new com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput(CdnTables.PROVIDER) + .withRecord(new QRecord().withValue("id", live).withValue("enabled", false))); + assertEquals(com.kingsrook.qbits.cdn.model.CdnPurgeStatus.SKIPPED_NONE_PROVIDER, + com.kingsrook.qbits.cdn.purge.CdnPurgeService.sendingStatus(null)); + } + + + + + /******************************************************************************* + ** Without an enabled provider, do not claim a request row was recorded. + *******************************************************************************/ + @Test + void absentProvider() throws Exception + { + CdnTestSupport.setUp(); + CdnTestSupport.postRule(); + CdnTestSupport.insert("post", new QRecord().withValue("slug", "one")); + List> lines = run(CdnProcesses.PURGE_PREFIX + "post", "1"); + assertThat(lines.get(0)).contains("1 No CDN provider configured: purges are recorded only, not sent."); + assertThat(lines.get(1)).containsExactly("1 record had no enabled CDN provider (no purge request recorded)"); + assertThat(purges()).isEmpty(); + } + }