From 50c0a5871862fd98c168a9c24c919f9ec04a94b5 Mon Sep 17 00:00:00 2001 From: Adam Kobor Date: Wed, 9 Sep 2020 17:25:37 +0200 Subject: [PATCH] SSL validation (#45) * Split up events and their related methods * Add a simple SSL validator * Rename Event to MonitorEvent * Initial SSL checker logic * Fix issues after rebase * Fix style issues * Add missing assertion for HandlersInfoSource test * Switch off communication logger in the test env * Fix test scope descriptions in UptimeCheckerTest * Add SSL_EVENT table to DatabaseCleaner * Integrate SSL checks into MonitorController * Fix error message string in SSLValidator * Integrate SSL checks into scheduler and basic handlers * Update API docs * Use dedicated emoji for SSLValidEvent * Improve existing tests * Integrate SSL checks into SlackEventHandler * Integrate SSL checks into TelegramEventHandler * Extract formatting from TextMessageEventHandler * Extract formatting from TextMessageEventHandler #2 * Restructure the whole text formatting logic * Integrate SSL checks into SMTPEventHandler * Update README * Expose SSL related info on MonitorDetailsDto * Add missing tests: UptimeEventRepository * Add missing tests for formatters * Update README --- README.md | 2 +- docs/api-doc/kuvasz-latest.yml | 29 ++ .../kuvaszuptime/kuvasz/DefaultSchema.java | 10 +- .../java/com/kuvaszuptime/kuvasz/Indexes.java | 5 + .../java/com/kuvaszuptime/kuvasz/Keys.java | 10 + .../com/kuvaszuptime/kuvasz/Sequences.java | 5 + .../java/com/kuvaszuptime/kuvasz/Tables.java | 6 + .../kuvaszuptime/kuvasz/enums/SslStatus.java | 51 +++ .../kuvaszuptime/kuvasz/tables/Monitor.java | 15 +- .../kuvaszuptime/kuvasz/tables/SslEvent.java | 190 +++++++++ .../kuvasz/tables/daos/MonitorDao.java | 14 + .../kuvasz/tables/daos/SslEventDao.java | 148 +++++++ .../kuvasz/tables/pojos/MonitorPojo.java | 26 +- .../kuvasz/tables/pojos/SslEventPojo.java | 235 +++++++++++ .../kuvasz/tables/records/MonitorRecord.java | 61 ++- .../kuvasz/tables/records/SslEventRecord.java | 369 ++++++++++++++++++ .../kuvasz/factories/EmailFactory.kt | 53 ++- .../kuvasz/handlers/DatabaseEventHandler.kt | 38 +- .../kuvasz/handlers/HandlersInfoSource.kt | 4 + .../kuvasz/handlers/LogEventHandler.kt | 39 +- .../kuvasz/handlers/RTCMessageEventHandler.kt | 77 ++++ .../kuvasz/handlers/SMTPEventHandler.kt | 27 +- .../kuvasz/handlers/SlackEventHandler.kt | 70 +--- .../kuvasz/handlers/TelegramEventHandler.kt | 76 +--- .../com/kuvaszuptime/kuvasz/models/Emoji.kt | 6 - .../com/kuvaszuptime/kuvasz/models/Event.kt | 127 ------ .../kuvasz/models/SSLValidation.kt | 11 + .../kuvasz/models/ScheduledCheck.kt | 2 +- .../models/{Error.kt => ServiceError.kt} | 0 .../kuvasz/models/dto/MonitorCreateDto.kt | 4 +- .../kuvasz/models/dto/MonitorDetailsDto.kt | 6 + .../kuvasz/models/dto/MonitorUpdateDto.kt | 3 +- .../kuvasz/models/events/MonitorEvent.kt | 181 +++++++++ .../kuvasz/models/events/StructuredMessage.kt | 41 ++ .../kuvasz/models/events/formatters/Emoji.kt | 27 ++ .../events/formatters/LogMessageFormatter.kt | 64 +++ .../formatters/PlainTextMessageFormatter.kt | 61 +++ .../formatters/RichTextMessageFormatter.kt | 63 +++ .../events/formatters/SlackTextFormatter.kt | 7 + .../formatters/TelegramTextFormatter.kt | 7 + .../events/formatters/TextMessageFormatter.kt | 11 + .../{ => handlers}/SlackWebhookMessage.kt | 2 +- .../{ => handlers}/TelegramAPIMessage.kt | 2 +- .../kuvasz/repositories/MonitorRepository.kt | 20 +- .../kuvasz/repositories/SSLEventRepository.kt | 58 +++ .../repositories/UptimeEventRepository.kt | 14 +- .../kuvasz/services/CheckScheduler.kt | 65 ++- .../kuvasz/services/DatabaseCleaner.kt | 6 +- .../kuvasz/services/EventDispatcher.kt | 28 +- .../kuvasz/services/MonitorCrudService.kt | 7 +- .../kuvasz/services/SSLChecker.kt | 64 +++ .../kuvasz/services/SSLValidator.kt | 53 +++ .../kuvasz/services/SlackWebhookService.kt | 7 +- .../kuvasz/services/TelegramAPIService.kt | 13 +- .../kuvasz/services/TextMessageService.kt | 8 + .../kuvasz/services/UptimeChecker.kt | 6 +- .../com/kuvaszuptime/kuvasz/util/Date+.kt | 17 +- ...V5__Add_ssl_monitoring_related_objects.sql | 23 ++ .../kuvasz/controllers/InfoEndpointTest.kt | 1 + .../controllers/MonitorControllerTest.kt | 81 +++- .../kuvaszuptime/kuvasz/events/EventTest.kt | 4 +- .../handlers/DatabaseEventHandlerTest.kt | 260 +++++++++++- .../kuvasz/handlers/SMTPEventHandlerTest.kt | 285 +++++++++++++- .../kuvasz/handlers/SlackEventHandlerTest.kt | 314 ++++++++++++--- .../handlers/TelegramEventHandlerTest.kt | 317 ++++++++++++--- .../com/kuvaszuptime/kuvasz/mocks/TestData.kt | 25 ++ .../formatters/LogMessageFormatterTest.kt | 218 +++++++++++ .../PlainTextMessageFormatterTest.kt | 203 ++++++++++ .../formatters/SlackTextFormatterTest.kt | 204 ++++++++++ .../formatters/TelegramTextFormatterTest.kt | 204 ++++++++++ .../repositories/UptimeEventRepositoryTest.kt | 58 +++ .../kuvasz/services/CheckSchedulerTest.kt | 42 +- .../kuvasz/services/DatabaseCleanerTest.kt | 51 +++ .../kuvasz/services/SSLCheckerTest.kt | 198 ++++++++++ .../kuvasz/services/SSLValidatorTest.kt | 44 +++ .../kuvasz/services/UptimeCheckerTest.kt | 10 +- .../com/kuvaszuptime/kuvasz/testutils/Rx.kt | 4 +- src/test/resources/logback-test.xml | 1 - 78 files changed, 4572 insertions(+), 526 deletions(-) create mode 100644 src/jooq/java/com/kuvaszuptime/kuvasz/enums/SslStatus.java create mode 100644 src/jooq/java/com/kuvaszuptime/kuvasz/tables/SslEvent.java create mode 100644 src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/SslEventDao.java create mode 100644 src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/SslEventPojo.java create mode 100644 src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/SslEventRecord.java create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/RTCMessageEventHandler.kt delete mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/models/Emoji.kt delete mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/models/Event.kt create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/models/SSLValidation.kt rename src/main/kotlin/com/kuvaszuptime/kuvasz/models/{Error.kt => ServiceError.kt} (100%) create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/MonitorEvent.kt create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/StructuredMessage.kt create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/Emoji.kt create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/LogMessageFormatter.kt create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/PlainTextMessageFormatter.kt create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/RichTextMessageFormatter.kt create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/SlackTextFormatter.kt create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/TelegramTextFormatter.kt create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/TextMessageFormatter.kt rename src/main/kotlin/com/kuvaszuptime/kuvasz/models/{ => handlers}/SlackWebhookMessage.kt (89%) rename src/main/kotlin/com/kuvaszuptime/kuvasz/models/{ => handlers}/TelegramAPIMessage.kt (84%) create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/SSLEventRepository.kt create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/services/SSLChecker.kt create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/services/SSLValidator.kt create mode 100644 src/main/kotlin/com/kuvaszuptime/kuvasz/services/TextMessageService.kt create mode 100644 src/main/resources/db/migration/V5__Add_ssl_monitoring_related_objects.sql create mode 100644 src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/LogMessageFormatterTest.kt create mode 100644 src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/PlainTextMessageFormatterTest.kt create mode 100644 src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/SlackTextFormatterTest.kt create mode 100644 src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/TelegramTextFormatterTest.kt create mode 100644 src/test/kotlin/com/kuvaszuptime/kuvasz/repositories/UptimeEventRepositoryTest.kt create mode 100644 src/test/kotlin/com/kuvaszuptime/kuvasz/services/SSLCheckerTest.kt create mode 100644 src/test/kotlin/com/kuvaszuptime/kuvasz/services/SSLValidatorTest.kt diff --git a/README.md b/README.md index 258f10b..2133904 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Kuvasz (pronounce as [ˈkuvɒs]) is an ancient hungarian breed of livestock & gu ### Features - Uptime & latency monitoring with a configurable interval +- SSL certification monitoring (once a day) - Email notifications through SMTP - Slack notifications through webhoooks - Telegram notifications through the Bot API @@ -29,7 +30,6 @@ Kuvasz (pronounce as [ˈkuvɒs]) is an ancient hungarian breed of livestock & gu ### Under development 🚧 -- SSL certification monitoring - Regular Lighthouse audits for your websites - Pagerduty, Opsgenie integration - Kuvasz Dashboard, a standalone GUI diff --git a/docs/api-doc/kuvasz-latest.yml b/docs/api-doc/kuvasz-latest.yml index 0d31012..a344f78 100644 --- a/docs/api-doc/kuvasz-latest.yml +++ b/docs/api-doc/kuvasz-latest.yml @@ -275,6 +275,8 @@ components: format: int32 enabled: type: boolean + sslCheckEnabled: + type: boolean createdAt: type: string format: date-time @@ -292,9 +294,22 @@ components: type: string format: date-time nullable: true + sslStatus: + $ref: '#/components/schemas/SslStatus' + sslStatusStartedAt: + type: string + format: date-time + nullable: true + lastSSLCheck: + type: string + format: date-time + nullable: true uptimeError: type: string nullable: true + sslError: + type: string + nullable: true averageLatencyInMs: type: integer format: int32 @@ -312,6 +327,12 @@ components: enum: - UP - DOWN + SslStatus: + type: string + enum: + - VALID + - INVALID + - WILL_EXPIRE ServiceError: type: object properties: @@ -344,6 +365,8 @@ components: updatedAt: type: string format: date-time + sslCheckEnabled: + type: boolean MonitorCreateDto: required: - name @@ -364,6 +387,9 @@ components: enabled: type: boolean nullable: true + sslCheckEnabled: + type: boolean + nullable: true MonitorUpdateDto: type: object properties: @@ -382,6 +408,9 @@ components: enabled: type: boolean nullable: true + sslCheckEnabled: + type: boolean + nullable: true securitySchemes: bearerAuth: type: http diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/DefaultSchema.java b/src/jooq/java/com/kuvaszuptime/kuvasz/DefaultSchema.java index 085edee..585310f 100644 --- a/src/jooq/java/com/kuvaszuptime/kuvasz/DefaultSchema.java +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/DefaultSchema.java @@ -6,6 +6,7 @@ import com.kuvaszuptime.kuvasz.tables.LatencyLog; import com.kuvaszuptime.kuvasz.tables.Monitor; +import com.kuvaszuptime.kuvasz.tables.SslEvent; import com.kuvaszuptime.kuvasz.tables.UptimeEvent; import java.util.Arrays; @@ -23,7 +24,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes" }) public class DefaultSchema extends SchemaImpl { - private static final long serialVersionUID = 63509414; + private static final long serialVersionUID = -839450692; /** * The reference instance of DEFAULT_SCHEMA @@ -40,6 +41,11 @@ public class DefaultSchema extends SchemaImpl { */ public final Monitor MONITOR = Monitor.MONITOR; + /** + * The table ssl_event. + */ + public final SslEvent SSL_EVENT = SslEvent.SSL_EVENT; + /** * The table uptime_event. */ @@ -63,6 +69,7 @@ public final List> getSequences() { return Arrays.>asList( Sequences.LATENCY_LOG_ID_SEQ, Sequences.MONITOR_ID_SEQ, + Sequences.SSL_EVENT_ID_SEQ, Sequences.UPTIME_EVENT_ID_SEQ); } @@ -71,6 +78,7 @@ public final List> getTables() { return Arrays.>asList( LatencyLog.LATENCY_LOG, Monitor.MONITOR, + SslEvent.SSL_EVENT, UptimeEvent.UPTIME_EVENT); } } diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/Indexes.java b/src/jooq/java/com/kuvaszuptime/kuvasz/Indexes.java index c76a736..ddc13fb 100644 --- a/src/jooq/java/com/kuvaszuptime/kuvasz/Indexes.java +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/Indexes.java @@ -5,6 +5,7 @@ import com.kuvaszuptime.kuvasz.tables.LatencyLog; +import com.kuvaszuptime.kuvasz.tables.SslEvent; import com.kuvaszuptime.kuvasz.tables.UptimeEvent; import org.jooq.Index; @@ -24,6 +25,8 @@ public class Indexes { public static final Index LATENCY_LOG_LATENCY_IDX = Indexes0.LATENCY_LOG_LATENCY_IDX; public static final Index LATENCY_LOG_MONITOR_IDX = Indexes0.LATENCY_LOG_MONITOR_IDX; + public static final Index SSL_EVENT_ENDED_AT_IDX = Indexes0.SSL_EVENT_ENDED_AT_IDX; + public static final Index SSL_EVENT_MONITOR_IDX = Indexes0.SSL_EVENT_MONITOR_IDX; public static final Index UPTIME_EVENT_ENDED_AT_IDX = Indexes0.UPTIME_EVENT_ENDED_AT_IDX; public static final Index UPTIME_EVENT_MONITOR_IDX = Indexes0.UPTIME_EVENT_MONITOR_IDX; @@ -34,6 +37,8 @@ public class Indexes { private static class Indexes0 { public static Index LATENCY_LOG_LATENCY_IDX = Internal.createIndex("latency_log_latency_idx", LatencyLog.LATENCY_LOG, new OrderField[] { LatencyLog.LATENCY_LOG.LATENCY }, false); public static Index LATENCY_LOG_MONITOR_IDX = Internal.createIndex("latency_log_monitor_idx", LatencyLog.LATENCY_LOG, new OrderField[] { LatencyLog.LATENCY_LOG.MONITOR_ID }, false); + public static Index SSL_EVENT_ENDED_AT_IDX = Internal.createIndex("ssl_event_ended_at_idx", SslEvent.SSL_EVENT, new OrderField[] { SslEvent.SSL_EVENT.ENDED_AT }, false); + public static Index SSL_EVENT_MONITOR_IDX = Internal.createIndex("ssl_event_monitor_idx", SslEvent.SSL_EVENT, new OrderField[] { SslEvent.SSL_EVENT.MONITOR_ID }, false); public static Index UPTIME_EVENT_ENDED_AT_IDX = Internal.createIndex("uptime_event_ended_at_idx", UptimeEvent.UPTIME_EVENT, new OrderField[] { UptimeEvent.UPTIME_EVENT.ENDED_AT }, false); public static Index UPTIME_EVENT_MONITOR_IDX = Internal.createIndex("uptime_event_monitor_idx", UptimeEvent.UPTIME_EVENT, new OrderField[] { UptimeEvent.UPTIME_EVENT.MONITOR_ID }, false); } diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/Keys.java b/src/jooq/java/com/kuvaszuptime/kuvasz/Keys.java index 0aa7a8e..3893437 100644 --- a/src/jooq/java/com/kuvaszuptime/kuvasz/Keys.java +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/Keys.java @@ -6,9 +6,11 @@ import com.kuvaszuptime.kuvasz.tables.LatencyLog; import com.kuvaszuptime.kuvasz.tables.Monitor; +import com.kuvaszuptime.kuvasz.tables.SslEvent; import com.kuvaszuptime.kuvasz.tables.UptimeEvent; import com.kuvaszuptime.kuvasz.tables.records.LatencyLogRecord; import com.kuvaszuptime.kuvasz.tables.records.MonitorRecord; +import com.kuvaszuptime.kuvasz.tables.records.SslEventRecord; import com.kuvaszuptime.kuvasz.tables.records.UptimeEventRecord; import org.jooq.ForeignKey; @@ -31,6 +33,7 @@ public class Keys { public static final Identity IDENTITY_LATENCY_LOG = Identities0.IDENTITY_LATENCY_LOG; public static final Identity IDENTITY_MONITOR = Identities0.IDENTITY_MONITOR; + public static final Identity IDENTITY_SSL_EVENT = Identities0.IDENTITY_SSL_EVENT; public static final Identity IDENTITY_UPTIME_EVENT = Identities0.IDENTITY_UPTIME_EVENT; // ------------------------------------------------------------------------- @@ -40,6 +43,8 @@ public class Keys { public static final UniqueKey LATENCY_LOG_PKEY = UniqueKeys0.LATENCY_LOG_PKEY; public static final UniqueKey MONITOR_PKEY = UniqueKeys0.MONITOR_PKEY; public static final UniqueKey UNIQUE_MONITOR_NAME = UniqueKeys0.UNIQUE_MONITOR_NAME; + public static final UniqueKey SSL_EVENT_PKEY = UniqueKeys0.SSL_EVENT_PKEY; + public static final UniqueKey SSL_EVENT_KEY = UniqueKeys0.SSL_EVENT_KEY; public static final UniqueKey UPTIME_EVENT_PKEY = UniqueKeys0.UPTIME_EVENT_PKEY; public static final UniqueKey UPTIME_EVENT_KEY = UniqueKeys0.UPTIME_EVENT_KEY; @@ -48,6 +53,7 @@ public class Keys { // ------------------------------------------------------------------------- public static final ForeignKey LATENCY_LOG__LATENCY_LOG_MONITOR_ID_FKEY = ForeignKeys0.LATENCY_LOG__LATENCY_LOG_MONITOR_ID_FKEY; + public static final ForeignKey SSL_EVENT__SSL_EVENT_MONITOR_ID_FKEY = ForeignKeys0.SSL_EVENT__SSL_EVENT_MONITOR_ID_FKEY; public static final ForeignKey UPTIME_EVENT__UPTIME_EVENT_MONITOR_ID_FKEY = ForeignKeys0.UPTIME_EVENT__UPTIME_EVENT_MONITOR_ID_FKEY; // ------------------------------------------------------------------------- @@ -57,6 +63,7 @@ public class Keys { private static class Identities0 { public static Identity IDENTITY_LATENCY_LOG = Internal.createIdentity(LatencyLog.LATENCY_LOG, LatencyLog.LATENCY_LOG.ID); public static Identity IDENTITY_MONITOR = Internal.createIdentity(Monitor.MONITOR, Monitor.MONITOR.ID); + public static Identity IDENTITY_SSL_EVENT = Internal.createIdentity(SslEvent.SSL_EVENT, SslEvent.SSL_EVENT.ID); public static Identity IDENTITY_UPTIME_EVENT = Internal.createIdentity(UptimeEvent.UPTIME_EVENT, UptimeEvent.UPTIME_EVENT.ID); } @@ -64,12 +71,15 @@ private static class UniqueKeys0 { public static final UniqueKey LATENCY_LOG_PKEY = Internal.createUniqueKey(LatencyLog.LATENCY_LOG, "latency_log_pkey", new TableField[] { LatencyLog.LATENCY_LOG.ID }, true); public static final UniqueKey MONITOR_PKEY = Internal.createUniqueKey(Monitor.MONITOR, "monitor_pkey", new TableField[] { Monitor.MONITOR.ID }, true); public static final UniqueKey UNIQUE_MONITOR_NAME = Internal.createUniqueKey(Monitor.MONITOR, "unique_monitor_name", new TableField[] { Monitor.MONITOR.NAME }, true); + public static final UniqueKey SSL_EVENT_PKEY = Internal.createUniqueKey(SslEvent.SSL_EVENT, "ssl_event_pkey", new TableField[] { SslEvent.SSL_EVENT.ID }, true); + public static final UniqueKey SSL_EVENT_KEY = Internal.createUniqueKey(SslEvent.SSL_EVENT, "ssl_event_key", new TableField[] { SslEvent.SSL_EVENT.MONITOR_ID, SslEvent.SSL_EVENT.STATUS, SslEvent.SSL_EVENT.ENDED_AT }, true); public static final UniqueKey UPTIME_EVENT_PKEY = Internal.createUniqueKey(UptimeEvent.UPTIME_EVENT, "uptime_event_pkey", new TableField[] { UptimeEvent.UPTIME_EVENT.ID }, true); public static final UniqueKey UPTIME_EVENT_KEY = Internal.createUniqueKey(UptimeEvent.UPTIME_EVENT, "uptime_event_key", new TableField[] { UptimeEvent.UPTIME_EVENT.MONITOR_ID, UptimeEvent.UPTIME_EVENT.STATUS, UptimeEvent.UPTIME_EVENT.ENDED_AT }, true); } private static class ForeignKeys0 { public static final ForeignKey LATENCY_LOG__LATENCY_LOG_MONITOR_ID_FKEY = Internal.createForeignKey(Keys.MONITOR_PKEY, LatencyLog.LATENCY_LOG, "latency_log_monitor_id_fkey", new TableField[] { LatencyLog.LATENCY_LOG.MONITOR_ID }, true); + public static final ForeignKey SSL_EVENT__SSL_EVENT_MONITOR_ID_FKEY = Internal.createForeignKey(Keys.MONITOR_PKEY, SslEvent.SSL_EVENT, "ssl_event_monitor_id_fkey", new TableField[] { SslEvent.SSL_EVENT.MONITOR_ID }, true); public static final ForeignKey UPTIME_EVENT__UPTIME_EVENT_MONITOR_ID_FKEY = Internal.createForeignKey(Keys.MONITOR_PKEY, UptimeEvent.UPTIME_EVENT, "uptime_event_monitor_id_fkey", new TableField[] { UptimeEvent.UPTIME_EVENT.MONITOR_ID }, true); } } diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/Sequences.java b/src/jooq/java/com/kuvaszuptime/kuvasz/Sequences.java index 45f0432..df920b2 100644 --- a/src/jooq/java/com/kuvaszuptime/kuvasz/Sequences.java +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/Sequences.java @@ -24,6 +24,11 @@ public class Sequences { */ public static final Sequence MONITOR_ID_SEQ = Internal.createSequence("monitor_id_seq", DefaultSchema.DEFAULT_SCHEMA, org.jooq.impl.SQLDataType.INTEGER.nullable(false), null, null, null, null, false, null); + /** + * The sequence ssl_event_id_seq + */ + public static final Sequence SSL_EVENT_ID_SEQ = Internal.createSequence("ssl_event_id_seq", DefaultSchema.DEFAULT_SCHEMA, org.jooq.impl.SQLDataType.INTEGER.nullable(false), null, null, null, null, false, null); + /** * The sequence uptime_event_id_seq */ diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/Tables.java b/src/jooq/java/com/kuvaszuptime/kuvasz/Tables.java index 6b112f5..086ba4a 100644 --- a/src/jooq/java/com/kuvaszuptime/kuvasz/Tables.java +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/Tables.java @@ -6,6 +6,7 @@ import com.kuvaszuptime.kuvasz.tables.LatencyLog; import com.kuvaszuptime.kuvasz.tables.Monitor; +import com.kuvaszuptime.kuvasz.tables.SslEvent; import com.kuvaszuptime.kuvasz.tables.UptimeEvent; @@ -25,6 +26,11 @@ public class Tables { */ public static final Monitor MONITOR = Monitor.MONITOR; + /** + * The table ssl_event. + */ + public static final SslEvent SSL_EVENT = SslEvent.SSL_EVENT; + /** * The table uptime_event. */ diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/enums/SslStatus.java b/src/jooq/java/com/kuvaszuptime/kuvasz/enums/SslStatus.java new file mode 100644 index 0000000..aee39d5 --- /dev/null +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/enums/SslStatus.java @@ -0,0 +1,51 @@ +/* + * This file is generated by jOOQ. + */ +package com.kuvaszuptime.kuvasz.enums; + + +import com.kuvaszuptime.kuvasz.DefaultSchema; + +import org.jooq.Catalog; +import org.jooq.EnumType; +import org.jooq.Schema; + + +/** + * This class is generated by jOOQ. + */ +@SuppressWarnings({ "all", "unchecked", "rawtypes" }) +public enum SslStatus implements EnumType { + + VALID("VALID"), + + INVALID("INVALID"), + + WILL_EXPIRE("WILL_EXPIRE"); + + private final String literal; + + private SslStatus(String literal) { + this.literal = literal; + } + + @Override + public Catalog getCatalog() { + return getSchema() == null ? null : getSchema().getCatalog(); + } + + @Override + public Schema getSchema() { + return DefaultSchema.DEFAULT_SCHEMA; + } + + @Override + public String getName() { + return "ssl_status"; + } + + @Override + public String getLiteral() { + return literal; + } +} diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/Monitor.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/Monitor.java index 7adfad5..2b34850 100644 --- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/Monitor.java +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/Monitor.java @@ -17,7 +17,7 @@ import org.jooq.Identity; import org.jooq.Name; import org.jooq.Record; -import org.jooq.Row7; +import org.jooq.Row8; import org.jooq.Schema; import org.jooq.Table; import org.jooq.TableField; @@ -33,7 +33,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes" }) public class Monitor extends TableImpl { - private static final long serialVersionUID = -1808562877; + private static final long serialVersionUID = 1271016748; /** * The reference instance of monitor @@ -83,6 +83,11 @@ public Class getRecordType() { */ public final TableField UPDATED_AT = createField(DSL.name("updated_at"), org.jooq.impl.SQLDataType.TIMESTAMPWITHTIMEZONE, this, ""); + /** + * The column monitor.ssl_check_enabled. + */ + public final TableField SSL_CHECK_ENABLED = createField(DSL.name("ssl_check_enabled"), org.jooq.impl.SQLDataType.BOOLEAN.nullable(false).defaultValue(org.jooq.impl.DSL.field("false", org.jooq.impl.SQLDataType.BOOLEAN)), this, ""); + /** * Create a monitor table reference */ @@ -163,11 +168,11 @@ public Monitor rename(Name name) { } // ------------------------------------------------------------------------- - // Row7 type methods + // Row8 type methods // ------------------------------------------------------------------------- @Override - public Row7 fieldsRow() { - return (Row7) super.fieldsRow(); + public Row8 fieldsRow() { + return (Row8) super.fieldsRow(); } } diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/SslEvent.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/SslEvent.java new file mode 100644 index 0000000..ca91bc4 --- /dev/null +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/SslEvent.java @@ -0,0 +1,190 @@ +/* + * This file is generated by jOOQ. + */ +package com.kuvaszuptime.kuvasz.tables; + + +import com.kuvaszuptime.kuvasz.DefaultSchema; +import com.kuvaszuptime.kuvasz.Indexes; +import com.kuvaszuptime.kuvasz.Keys; +import com.kuvaszuptime.kuvasz.enums.SslStatus; +import com.kuvaszuptime.kuvasz.tables.records.SslEventRecord; + +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.List; + +import org.jooq.Field; +import org.jooq.ForeignKey; +import org.jooq.Identity; +import org.jooq.Index; +import org.jooq.Name; +import org.jooq.Record; +import org.jooq.Row7; +import org.jooq.Schema; +import org.jooq.Table; +import org.jooq.TableField; +import org.jooq.TableOptions; +import org.jooq.UniqueKey; +import org.jooq.impl.DSL; +import org.jooq.impl.TableImpl; + + +/** + * This class is generated by jOOQ. + */ +@SuppressWarnings({ "all", "unchecked", "rawtypes" }) +public class SslEvent extends TableImpl { + + private static final long serialVersionUID = 2029223819; + + /** + * The reference instance of ssl_event + */ + public static final SslEvent SSL_EVENT = new SslEvent(); + + /** + * The class holding records for this type + */ + @Override + public Class getRecordType() { + return SslEventRecord.class; + } + + /** + * The column ssl_event.id. + */ + public final TableField ID = createField(DSL.name("id"), org.jooq.impl.SQLDataType.INTEGER.nullable(false).defaultValue(org.jooq.impl.DSL.field("nextval('kuvasz.ssl_event_id_seq'::regclass)", org.jooq.impl.SQLDataType.INTEGER)), this, ""); + + /** + * The column ssl_event.monitor_id. + */ + public final TableField MONITOR_ID = createField(DSL.name("monitor_id"), org.jooq.impl.SQLDataType.INTEGER.nullable(false), this, ""); + + /** + * The column ssl_event.status. Status of the event + */ + public final TableField STATUS = createField(DSL.name("status"), org.jooq.impl.SQLDataType.VARCHAR.nullable(false).asEnumDataType(com.kuvaszuptime.kuvasz.enums.SslStatus.class), this, "Status of the event"); + + /** + * The column ssl_event.error. + */ + public final TableField ERROR = createField(DSL.name("error"), org.jooq.impl.SQLDataType.CLOB, this, ""); + + /** + * The column ssl_event.started_at. The current event started at + */ + public final TableField STARTED_AT = createField(DSL.name("started_at"), org.jooq.impl.SQLDataType.TIMESTAMPWITHTIMEZONE.nullable(false).defaultValue(org.jooq.impl.DSL.field("now()", org.jooq.impl.SQLDataType.TIMESTAMPWITHTIMEZONE)), this, "The current event started at"); + + /** + * The column ssl_event.ended_at. The current event ended at + */ + public final TableField ENDED_AT = createField(DSL.name("ended_at"), org.jooq.impl.SQLDataType.TIMESTAMPWITHTIMEZONE, this, "The current event ended at"); + + /** + * The column ssl_event.updated_at. + */ + public final TableField UPDATED_AT = createField(DSL.name("updated_at"), org.jooq.impl.SQLDataType.TIMESTAMPWITHTIMEZONE.nullable(false), this, ""); + + /** + * Create a ssl_event table reference + */ + public SslEvent() { + this(DSL.name("ssl_event"), null); + } + + /** + * Create an aliased ssl_event table reference + */ + public SslEvent(String alias) { + this(DSL.name(alias), SSL_EVENT); + } + + /** + * Create an aliased ssl_event table reference + */ + public SslEvent(Name alias) { + this(alias, SSL_EVENT); + } + + private SslEvent(Name alias, Table aliased) { + this(alias, aliased, null); + } + + private SslEvent(Name alias, Table aliased, Field[] parameters) { + super(alias, null, aliased, parameters, DSL.comment(""), TableOptions.table()); + } + + public SslEvent(Table child, ForeignKey key) { + super(child, key, SSL_EVENT); + } + + @Override + public Schema getSchema() { + return DefaultSchema.DEFAULT_SCHEMA; + } + + @Override + public List getIndexes() { + return Arrays.asList(Indexes.SSL_EVENT_ENDED_AT_IDX, Indexes.SSL_EVENT_MONITOR_IDX); + } + + @Override + public Identity getIdentity() { + return Keys.IDENTITY_SSL_EVENT; + } + + @Override + public UniqueKey getPrimaryKey() { + return Keys.SSL_EVENT_PKEY; + } + + @Override + public List> getKeys() { + return Arrays.>asList(Keys.SSL_EVENT_PKEY, Keys.SSL_EVENT_KEY); + } + + @Override + public List> getReferences() { + return Arrays.>asList(Keys.SSL_EVENT__SSL_EVENT_MONITOR_ID_FKEY); + } + + public Monitor monitor() { + return new Monitor(this, Keys.SSL_EVENT__SSL_EVENT_MONITOR_ID_FKEY); + } + + @Override + public SslEvent as(String alias) { + return new SslEvent(DSL.name(alias), this); + } + + @Override + public SslEvent as(Name alias) { + return new SslEvent(alias, this); + } + + /** + * Rename this table + */ + @Override + public SslEvent rename(String name) { + return new SslEvent(DSL.name(name), null); + } + + /** + * Rename this table + */ + @Override + public SslEvent rename(Name name) { + return new SslEvent(name, null); + } + + // ------------------------------------------------------------------------- + // Row7 type methods + // ------------------------------------------------------------------------- + + @Override + public Row7 fieldsRow() { + return (Row7) super.fieldsRow(); + } +} diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/MonitorDao.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/MonitorDao.java index 29936a5..4066485 100644 --- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/MonitorDao.java +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/MonitorDao.java @@ -151,4 +151,18 @@ public List fetchRangeOfUpdatedAt(OffsetDateTime lowerInclusive, Of public List fetchByUpdatedAt(OffsetDateTime... values) { return fetch(Monitor.MONITOR.UPDATED_AT, values); } + + /** + * Fetch records that have ssl_check_enabled BETWEEN lowerInclusive AND upperInclusive + */ + public List fetchRangeOfSslCheckEnabled(Boolean lowerInclusive, Boolean upperInclusive) { + return fetchRange(Monitor.MONITOR.SSL_CHECK_ENABLED, lowerInclusive, upperInclusive); + } + + /** + * Fetch records that have ssl_check_enabled IN (values) + */ + public List fetchBySslCheckEnabled(Boolean... values) { + return fetch(Monitor.MONITOR.SSL_CHECK_ENABLED, values); + } } diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/SslEventDao.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/SslEventDao.java new file mode 100644 index 0000000..1388c8c --- /dev/null +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/SslEventDao.java @@ -0,0 +1,148 @@ +/* + * This file is generated by jOOQ. + */ +package com.kuvaszuptime.kuvasz.tables.daos; + + +import com.kuvaszuptime.kuvasz.enums.SslStatus; +import com.kuvaszuptime.kuvasz.tables.SslEvent; +import com.kuvaszuptime.kuvasz.tables.pojos.SslEventPojo; +import com.kuvaszuptime.kuvasz.tables.records.SslEventRecord; + +import java.time.OffsetDateTime; +import java.util.List; + +import org.jooq.Configuration; +import org.jooq.impl.DAOImpl; + + +/** + * This class is generated by jOOQ. + */ +@SuppressWarnings({ "all", "unchecked", "rawtypes" }) +public class SslEventDao extends DAOImpl { + + /** + * Create a new SslEventDao without any configuration + */ + public SslEventDao() { + super(SslEvent.SSL_EVENT, SslEventPojo.class); + } + + /** + * Create a new SslEventDao with an attached configuration + */ + public SslEventDao(Configuration configuration) { + super(SslEvent.SSL_EVENT, SslEventPojo.class, configuration); + } + + @Override + public Integer getId(SslEventPojo object) { + return object.getId(); + } + + /** + * Fetch records that have id BETWEEN lowerInclusive AND upperInclusive + */ + public List fetchRangeOfId(Integer lowerInclusive, Integer upperInclusive) { + return fetchRange(SslEvent.SSL_EVENT.ID, lowerInclusive, upperInclusive); + } + + /** + * Fetch records that have id IN (values) + */ + public List fetchById(Integer... values) { + return fetch(SslEvent.SSL_EVENT.ID, values); + } + + /** + * Fetch a unique record that has id = value + */ + public SslEventPojo fetchOneById(Integer value) { + return fetchOne(SslEvent.SSL_EVENT.ID, value); + } + + /** + * Fetch records that have monitor_id BETWEEN lowerInclusive AND upperInclusive + */ + public List fetchRangeOfMonitorId(Integer lowerInclusive, Integer upperInclusive) { + return fetchRange(SslEvent.SSL_EVENT.MONITOR_ID, lowerInclusive, upperInclusive); + } + + /** + * Fetch records that have monitor_id IN (values) + */ + public List fetchByMonitorId(Integer... values) { + return fetch(SslEvent.SSL_EVENT.MONITOR_ID, values); + } + + /** + * Fetch records that have status BETWEEN lowerInclusive AND upperInclusive + */ + public List fetchRangeOfStatus(SslStatus lowerInclusive, SslStatus upperInclusive) { + return fetchRange(SslEvent.SSL_EVENT.STATUS, lowerInclusive, upperInclusive); + } + + /** + * Fetch records that have status IN (values) + */ + public List fetchByStatus(SslStatus... values) { + return fetch(SslEvent.SSL_EVENT.STATUS, values); + } + + /** + * Fetch records that have error BETWEEN lowerInclusive AND upperInclusive + */ + public List fetchRangeOfError(String lowerInclusive, String upperInclusive) { + return fetchRange(SslEvent.SSL_EVENT.ERROR, lowerInclusive, upperInclusive); + } + + /** + * Fetch records that have error IN (values) + */ + public List fetchByError(String... values) { + return fetch(SslEvent.SSL_EVENT.ERROR, values); + } + + /** + * Fetch records that have started_at BETWEEN lowerInclusive AND upperInclusive + */ + public List fetchRangeOfStartedAt(OffsetDateTime lowerInclusive, OffsetDateTime upperInclusive) { + return fetchRange(SslEvent.SSL_EVENT.STARTED_AT, lowerInclusive, upperInclusive); + } + + /** + * Fetch records that have started_at IN (values) + */ + public List fetchByStartedAt(OffsetDateTime... values) { + return fetch(SslEvent.SSL_EVENT.STARTED_AT, values); + } + + /** + * Fetch records that have ended_at BETWEEN lowerInclusive AND upperInclusive + */ + public List fetchRangeOfEndedAt(OffsetDateTime lowerInclusive, OffsetDateTime upperInclusive) { + return fetchRange(SslEvent.SSL_EVENT.ENDED_AT, lowerInclusive, upperInclusive); + } + + /** + * Fetch records that have ended_at IN (values) + */ + public List fetchByEndedAt(OffsetDateTime... values) { + return fetch(SslEvent.SSL_EVENT.ENDED_AT, values); + } + + /** + * Fetch records that have updated_at BETWEEN lowerInclusive AND upperInclusive + */ + public List fetchRangeOfUpdatedAt(OffsetDateTime lowerInclusive, OffsetDateTime upperInclusive) { + return fetchRange(SslEvent.SSL_EVENT.UPDATED_AT, lowerInclusive, upperInclusive); + } + + /** + * Fetch records that have updated_at IN (values) + */ + public List fetchByUpdatedAt(OffsetDateTime... values) { + return fetch(SslEvent.SSL_EVENT.UPDATED_AT, values); + } +} diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/MonitorPojo.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/MonitorPojo.java index 2fb4cf4..5596f0c 100644 --- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/MonitorPojo.java +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/MonitorPojo.java @@ -29,7 +29,7 @@ }) public class MonitorPojo implements Serializable { - private static final long serialVersionUID = -1292240686; + private static final long serialVersionUID = -1878969857; private Integer id; private String name; @@ -38,6 +38,7 @@ public class MonitorPojo implements Serializable { private Boolean enabled; private OffsetDateTime createdAt; private OffsetDateTime updatedAt; + private Boolean sslCheckEnabled; public MonitorPojo() {} @@ -49,6 +50,7 @@ public MonitorPojo(MonitorPojo value) { this.enabled = value.enabled; this.createdAt = value.createdAt; this.updatedAt = value.updatedAt; + this.sslCheckEnabled = value.sslCheckEnabled; } public MonitorPojo( @@ -58,7 +60,8 @@ public MonitorPojo( Integer uptimeCheckInterval, Boolean enabled, OffsetDateTime createdAt, - OffsetDateTime updatedAt + OffsetDateTime updatedAt, + Boolean sslCheckEnabled ) { this.id = id; this.name = name; @@ -67,6 +70,7 @@ public MonitorPojo( this.enabled = enabled; this.createdAt = createdAt; this.updatedAt = updatedAt; + this.sslCheckEnabled = sslCheckEnabled; } @Id @@ -145,6 +149,16 @@ public MonitorPojo setUpdatedAt(OffsetDateTime updatedAt) { return this; } + @Column(name = "ssl_check_enabled", nullable = false) + public Boolean getSslCheckEnabled() { + return this.sslCheckEnabled; + } + + public MonitorPojo setSslCheckEnabled(Boolean sslCheckEnabled) { + this.sslCheckEnabled = sslCheckEnabled; + return this; + } + @Override public boolean equals(Object obj) { if (this == obj) @@ -196,6 +210,12 @@ else if (!createdAt.equals(other.createdAt)) } else if (!updatedAt.equals(other.updatedAt)) return false; + if (sslCheckEnabled == null) { + if (other.sslCheckEnabled != null) + return false; + } + else if (!sslCheckEnabled.equals(other.sslCheckEnabled)) + return false; return true; } @@ -210,6 +230,7 @@ public int hashCode() { result = prime * result + ((this.enabled == null) ? 0 : this.enabled.hashCode()); result = prime * result + ((this.createdAt == null) ? 0 : this.createdAt.hashCode()); result = prime * result + ((this.updatedAt == null) ? 0 : this.updatedAt.hashCode()); + result = prime * result + ((this.sslCheckEnabled == null) ? 0 : this.sslCheckEnabled.hashCode()); return result; } @@ -224,6 +245,7 @@ public String toString() { sb.append(", ").append(enabled); sb.append(", ").append(createdAt); sb.append(", ").append(updatedAt); + sb.append(", ").append(sslCheckEnabled); sb.append(")"); return sb.toString(); diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/SslEventPojo.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/SslEventPojo.java new file mode 100644 index 0000000..e3ee89e --- /dev/null +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/SslEventPojo.java @@ -0,0 +1,235 @@ +/* + * This file is generated by jOOQ. + */ +package com.kuvaszuptime.kuvasz.tables.pojos; + + +import com.kuvaszuptime.kuvasz.enums.SslStatus; + +import java.io.Serializable; +import java.time.OffsetDateTime; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotNull; + + +/** + * This class is generated by jOOQ. + */ +@SuppressWarnings({ "all", "unchecked", "rawtypes" }) +@Entity +@Table(name = "ssl_event", uniqueConstraints = { + @UniqueConstraint(name = "ssl_event_pkey", columnNames = {"id"}), + @UniqueConstraint(name = "ssl_event_key", columnNames = {"monitor_id", "status", "ended_at"}) +}, indexes = { + @Index(name = "ssl_event_ended_at_idx", columnList = "ended_at ASC"), + @Index(name = "ssl_event_monitor_idx", columnList = "monitor_id ASC") +}) +public class SslEventPojo implements Serializable { + + private static final long serialVersionUID = -231279334; + + private Integer id; + private Integer monitorId; + private SslStatus status; + private String error; + private OffsetDateTime startedAt; + private OffsetDateTime endedAt; + private OffsetDateTime updatedAt; + + public SslEventPojo() {} + + public SslEventPojo(SslEventPojo value) { + this.id = value.id; + this.monitorId = value.monitorId; + this.status = value.status; + this.error = value.error; + this.startedAt = value.startedAt; + this.endedAt = value.endedAt; + this.updatedAt = value.updatedAt; + } + + public SslEventPojo( + Integer id, + Integer monitorId, + SslStatus status, + String error, + OffsetDateTime startedAt, + OffsetDateTime endedAt, + OffsetDateTime updatedAt + ) { + this.id = id; + this.monitorId = monitorId; + this.status = status; + this.error = error; + this.startedAt = startedAt; + this.endedAt = endedAt; + this.updatedAt = updatedAt; + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false, precision = 32) + public Integer getId() { + return this.id; + } + + public SslEventPojo setId(Integer id) { + this.id = id; + return this; + } + + @Column(name = "monitor_id", nullable = false, precision = 32) + @NotNull + public Integer getMonitorId() { + return this.monitorId; + } + + public SslEventPojo setMonitorId(Integer monitorId) { + this.monitorId = monitorId; + return this; + } + + @Column(name = "status", nullable = false) + @NotNull + public SslStatus getStatus() { + return this.status; + } + + public SslEventPojo setStatus(SslStatus status) { + this.status = status; + return this; + } + + @Column(name = "error") + public String getError() { + return this.error; + } + + public SslEventPojo setError(String error) { + this.error = error; + return this; + } + + @Column(name = "started_at", nullable = false) + public OffsetDateTime getStartedAt() { + return this.startedAt; + } + + public SslEventPojo setStartedAt(OffsetDateTime startedAt) { + this.startedAt = startedAt; + return this; + } + + @Column(name = "ended_at") + public OffsetDateTime getEndedAt() { + return this.endedAt; + } + + public SslEventPojo setEndedAt(OffsetDateTime endedAt) { + this.endedAt = endedAt; + return this; + } + + @Column(name = "updated_at", nullable = false) + @NotNull + public OffsetDateTime getUpdatedAt() { + return this.updatedAt; + } + + public SslEventPojo setUpdatedAt(OffsetDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + final SslEventPojo other = (SslEventPojo) obj; + if (id == null) { + if (other.id != null) + return false; + } + else if (!id.equals(other.id)) + return false; + if (monitorId == null) { + if (other.monitorId != null) + return false; + } + else if (!monitorId.equals(other.monitorId)) + return false; + if (status == null) { + if (other.status != null) + return false; + } + else if (!status.equals(other.status)) + return false; + if (error == null) { + if (other.error != null) + return false; + } + else if (!error.equals(other.error)) + return false; + if (startedAt == null) { + if (other.startedAt != null) + return false; + } + else if (!startedAt.equals(other.startedAt)) + return false; + if (endedAt == null) { + if (other.endedAt != null) + return false; + } + else if (!endedAt.equals(other.endedAt)) + return false; + if (updatedAt == null) { + if (other.updatedAt != null) + return false; + } + else if (!updatedAt.equals(other.updatedAt)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.id == null) ? 0 : this.id.hashCode()); + result = prime * result + ((this.monitorId == null) ? 0 : this.monitorId.hashCode()); + result = prime * result + ((this.status == null) ? 0 : this.status.hashCode()); + result = prime * result + ((this.error == null) ? 0 : this.error.hashCode()); + result = prime * result + ((this.startedAt == null) ? 0 : this.startedAt.hashCode()); + result = prime * result + ((this.endedAt == null) ? 0 : this.endedAt.hashCode()); + result = prime * result + ((this.updatedAt == null) ? 0 : this.updatedAt.hashCode()); + return result; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("SslEventPojo ("); + + sb.append(id); + sb.append(", ").append(monitorId); + sb.append(", ").append(status); + sb.append(", ").append(error); + sb.append(", ").append(startedAt); + sb.append(", ").append(endedAt); + sb.append(", ").append(updatedAt); + + sb.append(")"); + return sb.toString(); + } +} diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/MonitorRecord.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/MonitorRecord.java index c035b7f..61fb8ec 100644 --- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/MonitorRecord.java +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/MonitorRecord.java @@ -20,8 +20,8 @@ import org.jooq.Field; import org.jooq.Record1; -import org.jooq.Record7; -import org.jooq.Row7; +import org.jooq.Record8; +import org.jooq.Row8; import org.jooq.impl.UpdatableRecordImpl; @@ -34,9 +34,9 @@ @UniqueConstraint(name = "monitor_pkey", columnNames = {"id"}), @UniqueConstraint(name = "unique_monitor_name", columnNames = {"name"}) }) -public class MonitorRecord extends UpdatableRecordImpl implements Record7 { +public class MonitorRecord extends UpdatableRecordImpl implements Record8 { - private static final long serialVersionUID = 1383018433; + private static final long serialVersionUID = 1165953385; /** * Setter for monitor.id. @@ -156,6 +156,22 @@ public OffsetDateTime getUpdatedAt() { return (OffsetDateTime) get(6); } + /** + * Setter for monitor.ssl_check_enabled. + */ + public MonitorRecord setSslCheckEnabled(Boolean value) { + set(7, value); + return this; + } + + /** + * Getter for monitor.ssl_check_enabled. + */ + @Column(name = "ssl_check_enabled", nullable = false) + public Boolean getSslCheckEnabled() { + return (Boolean) get(7); + } + // ------------------------------------------------------------------------- // Primary key information // ------------------------------------------------------------------------- @@ -166,17 +182,17 @@ public Record1 key() { } // ------------------------------------------------------------------------- - // Record7 type implementation + // Record8 type implementation // ------------------------------------------------------------------------- @Override - public Row7 fieldsRow() { - return (Row7) super.fieldsRow(); + public Row8 fieldsRow() { + return (Row8) super.fieldsRow(); } @Override - public Row7 valuesRow() { - return (Row7) super.valuesRow(); + public Row8 valuesRow() { + return (Row8) super.valuesRow(); } @Override @@ -214,6 +230,11 @@ public Field field7() { return Monitor.MONITOR.UPDATED_AT; } + @Override + public Field field8() { + return Monitor.MONITOR.SSL_CHECK_ENABLED; + } + @Override public Integer component1() { return getId(); @@ -249,6 +270,11 @@ public OffsetDateTime component7() { return getUpdatedAt(); } + @Override + public Boolean component8() { + return getSslCheckEnabled(); + } + @Override public Integer value1() { return getId(); @@ -284,6 +310,11 @@ public OffsetDateTime value7() { return getUpdatedAt(); } + @Override + public Boolean value8() { + return getSslCheckEnabled(); + } + @Override public MonitorRecord value1(Integer value) { setId(value); @@ -327,7 +358,13 @@ public MonitorRecord value7(OffsetDateTime value) { } @Override - public MonitorRecord values(Integer value1, String value2, String value3, Integer value4, Boolean value5, OffsetDateTime value6, OffsetDateTime value7) { + public MonitorRecord value8(Boolean value) { + setSslCheckEnabled(value); + return this; + } + + @Override + public MonitorRecord values(Integer value1, String value2, String value3, Integer value4, Boolean value5, OffsetDateTime value6, OffsetDateTime value7, Boolean value8) { value1(value1); value2(value2); value3(value3); @@ -335,6 +372,7 @@ public MonitorRecord values(Integer value1, String value2, String value3, Intege value5(value5); value6(value6); value7(value7); + value8(value8); return this; } @@ -352,7 +390,7 @@ public MonitorRecord() { /** * Create a detached, initialised MonitorRecord */ - public MonitorRecord(Integer id, String name, String url, Integer uptimeCheckInterval, Boolean enabled, OffsetDateTime createdAt, OffsetDateTime updatedAt) { + public MonitorRecord(Integer id, String name, String url, Integer uptimeCheckInterval, Boolean enabled, OffsetDateTime createdAt, OffsetDateTime updatedAt, Boolean sslCheckEnabled) { super(Monitor.MONITOR); set(0, id); @@ -362,5 +400,6 @@ public MonitorRecord(Integer id, String name, String url, Integer uptimeCheckInt set(4, enabled); set(5, createdAt); set(6, updatedAt); + set(7, sslCheckEnabled); } } diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/SslEventRecord.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/SslEventRecord.java new file mode 100644 index 0000000..02e4404 --- /dev/null +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/SslEventRecord.java @@ -0,0 +1,369 @@ +/* + * This file is generated by jOOQ. + */ +package com.kuvaszuptime.kuvasz.tables.records; + + +import com.kuvaszuptime.kuvasz.enums.SslStatus; +import com.kuvaszuptime.kuvasz.tables.SslEvent; + +import java.time.OffsetDateTime; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotNull; + +import org.jooq.Field; +import org.jooq.Record1; +import org.jooq.Record7; +import org.jooq.Row7; +import org.jooq.impl.UpdatableRecordImpl; + + +/** + * This class is generated by jOOQ. + */ +@SuppressWarnings({ "all", "unchecked", "rawtypes" }) +@Entity +@Table(name = "ssl_event", uniqueConstraints = { + @UniqueConstraint(name = "ssl_event_pkey", columnNames = {"id"}), + @UniqueConstraint(name = "ssl_event_key", columnNames = {"monitor_id", "status", "ended_at"}) +}, indexes = { + @Index(name = "ssl_event_ended_at_idx", columnList = "ended_at ASC"), + @Index(name = "ssl_event_monitor_idx", columnList = "monitor_id ASC") +}) +public class SslEventRecord extends UpdatableRecordImpl implements Record7 { + + private static final long serialVersionUID = -877029403; + + /** + * Setter for ssl_event.id. + */ + public SslEventRecord setId(Integer value) { + set(0, value); + return this; + } + + /** + * Getter for ssl_event.id. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false, precision = 32) + public Integer getId() { + return (Integer) get(0); + } + + /** + * Setter for ssl_event.monitor_id. + */ + public SslEventRecord setMonitorId(Integer value) { + set(1, value); + return this; + } + + /** + * Getter for ssl_event.monitor_id. + */ + @Column(name = "monitor_id", nullable = false, precision = 32) + @NotNull + public Integer getMonitorId() { + return (Integer) get(1); + } + + /** + * Setter for ssl_event.status. Status of the event + */ + public SslEventRecord setStatus(SslStatus value) { + set(2, value); + return this; + } + + /** + * Getter for ssl_event.status. Status of the event + */ + @Column(name = "status", nullable = false) + @NotNull + public SslStatus getStatus() { + return (SslStatus) get(2); + } + + /** + * Setter for ssl_event.error. + */ + public SslEventRecord setError(String value) { + set(3, value); + return this; + } + + /** + * Getter for ssl_event.error. + */ + @Column(name = "error") + public String getError() { + return (String) get(3); + } + + /** + * Setter for ssl_event.started_at. The current event started at + */ + public SslEventRecord setStartedAt(OffsetDateTime value) { + set(4, value); + return this; + } + + /** + * Getter for ssl_event.started_at. The current event started at + */ + @Column(name = "started_at", nullable = false) + public OffsetDateTime getStartedAt() { + return (OffsetDateTime) get(4); + } + + /** + * Setter for ssl_event.ended_at. The current event ended at + */ + public SslEventRecord setEndedAt(OffsetDateTime value) { + set(5, value); + return this; + } + + /** + * Getter for ssl_event.ended_at. The current event ended at + */ + @Column(name = "ended_at") + public OffsetDateTime getEndedAt() { + return (OffsetDateTime) get(5); + } + + /** + * Setter for ssl_event.updated_at. + */ + public SslEventRecord setUpdatedAt(OffsetDateTime value) { + set(6, value); + return this; + } + + /** + * Getter for ssl_event.updated_at. + */ + @Column(name = "updated_at", nullable = false) + @NotNull + public OffsetDateTime getUpdatedAt() { + return (OffsetDateTime) get(6); + } + + // ------------------------------------------------------------------------- + // Primary key information + // ------------------------------------------------------------------------- + + @Override + public Record1 key() { + return (Record1) super.key(); + } + + // ------------------------------------------------------------------------- + // Record7 type implementation + // ------------------------------------------------------------------------- + + @Override + public Row7 fieldsRow() { + return (Row7) super.fieldsRow(); + } + + @Override + public Row7 valuesRow() { + return (Row7) super.valuesRow(); + } + + @Override + public Field field1() { + return SslEvent.SSL_EVENT.ID; + } + + @Override + public Field field2() { + return SslEvent.SSL_EVENT.MONITOR_ID; + } + + @Override + public Field field3() { + return SslEvent.SSL_EVENT.STATUS; + } + + @Override + public Field field4() { + return SslEvent.SSL_EVENT.ERROR; + } + + @Override + public Field field5() { + return SslEvent.SSL_EVENT.STARTED_AT; + } + + @Override + public Field field6() { + return SslEvent.SSL_EVENT.ENDED_AT; + } + + @Override + public Field field7() { + return SslEvent.SSL_EVENT.UPDATED_AT; + } + + @Override + public Integer component1() { + return getId(); + } + + @Override + public Integer component2() { + return getMonitorId(); + } + + @Override + public SslStatus component3() { + return getStatus(); + } + + @Override + public String component4() { + return getError(); + } + + @Override + public OffsetDateTime component5() { + return getStartedAt(); + } + + @Override + public OffsetDateTime component6() { + return getEndedAt(); + } + + @Override + public OffsetDateTime component7() { + return getUpdatedAt(); + } + + @Override + public Integer value1() { + return getId(); + } + + @Override + public Integer value2() { + return getMonitorId(); + } + + @Override + public SslStatus value3() { + return getStatus(); + } + + @Override + public String value4() { + return getError(); + } + + @Override + public OffsetDateTime value5() { + return getStartedAt(); + } + + @Override + public OffsetDateTime value6() { + return getEndedAt(); + } + + @Override + public OffsetDateTime value7() { + return getUpdatedAt(); + } + + @Override + public SslEventRecord value1(Integer value) { + setId(value); + return this; + } + + @Override + public SslEventRecord value2(Integer value) { + setMonitorId(value); + return this; + } + + @Override + public SslEventRecord value3(SslStatus value) { + setStatus(value); + return this; + } + + @Override + public SslEventRecord value4(String value) { + setError(value); + return this; + } + + @Override + public SslEventRecord value5(OffsetDateTime value) { + setStartedAt(value); + return this; + } + + @Override + public SslEventRecord value6(OffsetDateTime value) { + setEndedAt(value); + return this; + } + + @Override + public SslEventRecord value7(OffsetDateTime value) { + setUpdatedAt(value); + return this; + } + + @Override + public SslEventRecord values(Integer value1, Integer value2, SslStatus value3, String value4, OffsetDateTime value5, OffsetDateTime value6, OffsetDateTime value7) { + value1(value1); + value2(value2); + value3(value3); + value4(value4); + value5(value5); + value6(value6); + value7(value7); + return this; + } + + // ------------------------------------------------------------------------- + // Constructors + // ------------------------------------------------------------------------- + + /** + * Create a detached SslEventRecord + */ + public SslEventRecord() { + super(SslEvent.SSL_EVENT); + } + + /** + * Create a detached, initialised SslEventRecord + */ + public SslEventRecord(Integer id, Integer monitorId, SslStatus status, String error, OffsetDateTime startedAt, OffsetDateTime endedAt, OffsetDateTime updatedAt) { + super(SslEvent.SSL_EVENT); + + set(0, id); + set(1, monitorId); + set(2, status); + set(3, error); + set(4, startedAt); + set(5, endedAt); + set(6, updatedAt); + } +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/factories/EmailFactory.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/factories/EmailFactory.kt index 7416fc7..6234a4d 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/factories/EmailFactory.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/factories/EmailFactory.kt @@ -1,47 +1,46 @@ package com.kuvaszuptime.kuvasz.factories import com.kuvaszuptime.kuvasz.config.handlers.EmailEventHandlerConfig -import com.kuvaszuptime.kuvasz.models.MonitorDownEvent -import com.kuvaszuptime.kuvasz.models.MonitorUpEvent -import com.kuvaszuptime.kuvasz.models.UptimeMonitorEvent -import com.kuvaszuptime.kuvasz.models.toEmoji -import com.kuvaszuptime.kuvasz.models.toStructuredMessage -import com.kuvaszuptime.kuvasz.models.toUptimeStatus +import com.kuvaszuptime.kuvasz.enums.SslStatus +import com.kuvaszuptime.kuvasz.models.events.SSLMonitorEvent +import com.kuvaszuptime.kuvasz.models.events.UptimeMonitorEvent +import com.kuvaszuptime.kuvasz.models.events.formatters.PlainTextMessageFormatter +import com.kuvaszuptime.kuvasz.models.events.formatters.getEmoji import org.simplejavamail.api.email.Email import org.simplejavamail.email.EmailBuilder class EmailFactory(private val config: EmailEventHandlerConfig) { - fun fromUptimeMonitorEvent(event: UptimeMonitorEvent): Email = + private val formatter = PlainTextMessageFormatter + + fun fromMonitorEvent(event: UptimeMonitorEvent): Email = + createEmailBase() + .withSubject(event.getSubject()) + .withPlainText(formatter.toFormattedMessage(event)) + .buildEmail() + + fun fromMonitorEvent(event: SSLMonitorEvent): Email = createEmailBase() .withSubject(event.getSubject()) - .withPlainText(event.toMessage()) + .withPlainText(formatter.toFormattedMessage(event)) .buildEmail() private fun UptimeMonitorEvent.getSubject(): String = - "[kuvasz-uptime] - ${toEmoji()} [${monitor.name}] ${monitor.url} is ${toUptimeStatus()}" + "[kuvasz-uptime] - ${getEmoji()} [${monitor.name}] ${monitor.url} is $uptimeStatus" + + private fun SSLMonitorEvent.getSubject(): String { + val statusString = when (sslStatus) { + SslStatus.VALID -> "has a VALID certificate" + SslStatus.INVALID -> "has an INVALID certificate" + SslStatus.WILL_EXPIRE -> "has a certificate that will expire soon" + } + + return "[kuvasz-uptime] - ${getEmoji()} [${monitor.name}] ${monitor.url} $statusString" + } private fun createEmailBase() = EmailBuilder .startingBlank() .to(config.to, config.to) .from(config.from, config.from) - - private fun UptimeMonitorEvent.toMessage() = - when (this) { - is MonitorUpEvent -> toStructuredMessage().let { details -> - listOfNotNull( - details.summary, - details.latency, - details.previousDownTime.orNull() - ) - } - is MonitorDownEvent -> toStructuredMessage().let { details -> - listOfNotNull( - details.summary, - details.error, - details.previousUpTime.orNull() - ) - } - }.joinToString("\n") } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandler.kt index c6d8e6b..36e109b 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandler.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandler.kt @@ -1,8 +1,9 @@ package com.kuvaszuptime.kuvasz.handlers -import com.kuvaszuptime.kuvasz.models.UptimeMonitorEvent -import com.kuvaszuptime.kuvasz.models.uptimeStatusNotEquals +import com.kuvaszuptime.kuvasz.models.events.SSLMonitorEvent +import com.kuvaszuptime.kuvasz.models.events.UptimeMonitorEvent import com.kuvaszuptime.kuvasz.repositories.LatencyLogRepository +import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository import com.kuvaszuptime.kuvasz.services.EventDispatcher import com.kuvaszuptime.kuvasz.util.transaction @@ -16,7 +17,8 @@ import javax.inject.Inject class DatabaseEventHandler @Inject constructor( private val eventDispatcher: EventDispatcher, private val uptimeEventRepository: UptimeEventRepository, - private val latencyLogRepository: LatencyLogRepository + private val latencyLogRepository: LatencyLogRepository, + private val sslEventRepository: SSLEventRepository ) { companion object { private val logger = LoggerFactory.getLogger(DatabaseEventHandler::class.java) @@ -37,13 +39,25 @@ class DatabaseEventHandler @Inject constructor( logger.debug("A MonitorDownEvent has been received for monitor with ID: ${event.monitor.id}") handleUptimeMonitorEvent(event) } + eventDispatcher.subscribeToSSLValidEvents { event -> + logger.debug("An SSLValidEvent has been received for monitor with ID: ${event.monitor.id}") + handleSSLMonitorEvent(event) + } + eventDispatcher.subscribeToSSLInvalidEvents { event -> + logger.debug("An SSLInvalidEvent has been received for monitor with ID: ${event.monitor.id}") + handleSSLMonitorEvent(event) + } + eventDispatcher.subscribeToSSLWillExpireEvents { event -> + logger.debug("An SSLWillExpireEvent has been received for monitor with ID: ${event.monitor.id}") + handleSSLMonitorEvent(event) + } } private fun handleUptimeMonitorEvent(currentEvent: UptimeMonitorEvent) { currentEvent.previousEvent.fold( { uptimeEventRepository.insertFromMonitorEvent(currentEvent) }, { previousEvent -> - if (currentEvent.uptimeStatusNotEquals(previousEvent)) { + if (currentEvent.statusNotEquals(previousEvent)) { uptimeEventRepository.transaction { uptimeEventRepository.endEventById(previousEvent.id, currentEvent.dispatchedAt) uptimeEventRepository.insertFromMonitorEvent(currentEvent) @@ -54,4 +68,20 @@ class DatabaseEventHandler @Inject constructor( } ) } + + private fun handleSSLMonitorEvent(currentEvent: SSLMonitorEvent) { + currentEvent.previousEvent.fold( + { sslEventRepository.insertFromMonitorEvent(currentEvent) }, + { previousEvent -> + if (currentEvent.statusNotEquals(previousEvent)) { + sslEventRepository.transaction { + sslEventRepository.endEventById(previousEvent.id, currentEvent.dispatchedAt) + sslEventRepository.insertFromMonitorEvent(currentEvent) + } + } else { + sslEventRepository.updateEventUpdatedAt(previousEvent.id, currentEvent.dispatchedAt) + } + } + ) + } } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/HandlersInfoSource.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/HandlersInfoSource.kt index 28a457d..cf8b619 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/HandlersInfoSource.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/HandlersInfoSource.kt @@ -28,6 +28,10 @@ class HandlersInfoSource @Inject constructor(private val environment: Environmen "slack-event-handler.enabled" to environment.getBooleanProp( "handler-config.slack-event-handler.enabled", false + ), + "telegram-event-handler.enabled" to environment.getBooleanProp( + "handler-config.telegram-event-handler.enabled", + false ) ) return MapPropertySource("handlers", mapOf("handlers" to handlerConfigs)) diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/LogEventHandler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/LogEventHandler.kt index f9fda1b..5878643 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/LogEventHandler.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/LogEventHandler.kt @@ -1,7 +1,9 @@ package com.kuvaszuptime.kuvasz.handlers -import com.kuvaszuptime.kuvasz.models.RedirectEvent -import com.kuvaszuptime.kuvasz.models.toPlainMessage +import com.kuvaszuptime.kuvasz.models.events.RedirectEvent +import com.kuvaszuptime.kuvasz.models.events.SSLMonitorEvent +import com.kuvaszuptime.kuvasz.models.events.UptimeMonitorEvent +import com.kuvaszuptime.kuvasz.models.events.formatters.LogMessageFormatter import com.kuvaszuptime.kuvasz.services.EventDispatcher import io.micronaut.context.annotation.Context import io.micronaut.context.annotation.Requires @@ -15,18 +17,41 @@ class LogEventHandler @Inject constructor(eventDispatcher: EventDispatcher) { private val logger = LoggerFactory.getLogger(LogEventHandler::class.java) } + private val formatter = LogMessageFormatter + init { eventDispatcher.subscribeToMonitorUpEvents { event -> - logger.info(event.toPlainMessage()) + event.handle() } eventDispatcher.subscribeToMonitorDownEvents { event -> - logger.error(event.toPlainMessage()) + event.handle() } eventDispatcher.subscribeToRedirectEvents { event -> - logger.warn(event.toLogMessage()) + event.handle() + } + eventDispatcher.subscribeToSSLValidEvents { event -> + event.handle() + } + eventDispatcher.subscribeToSSLInvalidEvents { event -> + event.handle() + } + eventDispatcher.subscribeToSSLWillExpireEvents { event -> + event.handle() } } - private fun RedirectEvent.toLogMessage() = - "Request to \"${monitor.name}\" (${monitor.url}) has been redirected" + private fun UptimeMonitorEvent.handle() { + val message = formatter.toFormattedMessage(this) + logger.info(message) + } + + private fun SSLMonitorEvent.handle() { + val message = formatter.toFormattedMessage(this) + logger.info(message) + } + + private fun RedirectEvent.handle() { + val message = formatter.toFormattedMessage(this) + logger.info(message) + } } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/RTCMessageEventHandler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/RTCMessageEventHandler.kt new file mode 100644 index 0000000..6f0db56 --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/RTCMessageEventHandler.kt @@ -0,0 +1,77 @@ +package com.kuvaszuptime.kuvasz.handlers + +import com.kuvaszuptime.kuvasz.models.events.formatters.RichTextMessageFormatter +import com.kuvaszuptime.kuvasz.models.events.SSLMonitorEvent +import com.kuvaszuptime.kuvasz.models.events.UptimeMonitorEvent +import com.kuvaszuptime.kuvasz.services.EventDispatcher +import com.kuvaszuptime.kuvasz.services.TextMessageService +import io.micronaut.http.HttpResponse +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn +import io.reactivex.Flowable +import io.reactivex.disposables.Disposable +import org.slf4j.Logger + +abstract class RTCMessageEventHandler( + private val eventDispatcher: EventDispatcher, + private val messageService: TextMessageService +) { + + internal abstract val logger: Logger + + internal abstract val formatter: RichTextMessageFormatter + + init { + subscribeToEvents() + } + + @ExecuteOn(TaskExecutors.IO) + internal fun subscribeToEvents() { + eventDispatcher.subscribeToMonitorUpEvents { event -> + logger.debug("A MonitorUpEvent has been received for monitor with ID: ${event.monitor.id}") + event.handle() + } + eventDispatcher.subscribeToMonitorDownEvents { event -> + logger.debug("A MonitorDownEvent has been received for monitor with ID: ${event.monitor.id}") + event.handle() + } + eventDispatcher.subscribeToSSLValidEvents { event -> + logger.debug("An SSLValidEvent has been received for monitor with ID: ${event.monitor.id}") + event.handle() + } + eventDispatcher.subscribeToSSLInvalidEvents { event -> + logger.debug("An SSLInvalidEvent has been received for monitor with ID: ${event.monitor.id}") + event.handle() + } + eventDispatcher.subscribeToSSLWillExpireEvents { event -> + logger.debug("An SSLWillExpireEvent has been received for monitor with ID: ${event.monitor.id}") + event.handle() + } + } + + private fun UptimeMonitorEvent.handle() = + this.runWhenStateChanges { event -> + val message = formatter.toFormattedMessage(event) + messageService.sendMessage(message).handleResponse() + } + + private fun SSLMonitorEvent.handle() = + this.runWhenStateChanges { event -> + val message = formatter.toFormattedMessage(event) + messageService.sendMessage(message).handleResponse() + } + + private fun Flowable>.handleResponse(): Disposable = + subscribe( + { + logger.debug("The message to your configured webhook has been successfully sent") + }, + { ex -> + if (ex is HttpClientResponseException) { + val responseBody = ex.response.getBody(String::class.java) + logger.error("The message cannot be sent to your configured webhook: $responseBody") + } + } + ) +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandler.kt index ae8bba9..836a387 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandler.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandler.kt @@ -2,7 +2,8 @@ package com.kuvaszuptime.kuvasz.handlers import com.kuvaszuptime.kuvasz.config.handlers.SMTPEventHandlerConfig import com.kuvaszuptime.kuvasz.factories.EmailFactory -import com.kuvaszuptime.kuvasz.models.runWhenStateChanges +import com.kuvaszuptime.kuvasz.models.events.SSLMonitorEvent +import com.kuvaszuptime.kuvasz.models.events.UptimeMonitorEvent import com.kuvaszuptime.kuvasz.services.EventDispatcher import com.kuvaszuptime.kuvasz.services.SMTPMailer import io.micronaut.context.annotation.Context @@ -30,11 +31,31 @@ class SMTPEventHandler @Inject constructor( private fun subscribeToEvents() { eventDispatcher.subscribeToMonitorUpEvents { event -> logger.debug("A MonitorUpEvent has been received for monitor with ID: ${event.monitor.id}") - event.runWhenStateChanges { smtpMailer.sendAsync(emailFactory.fromUptimeMonitorEvent(it)) } + event.handle() } eventDispatcher.subscribeToMonitorDownEvents { event -> logger.debug("A MonitorDownEvent has been received for monitor with ID: ${event.monitor.id}") - event.runWhenStateChanges { smtpMailer.sendAsync(emailFactory.fromUptimeMonitorEvent(it)) } + event.handle() } + eventDispatcher.subscribeToSSLValidEvents { event -> + logger.debug("An SSLValidEvent has been received for monitor with ID: ${event.monitor.id}") + event.handle() + } + eventDispatcher.subscribeToSSLInvalidEvents { event -> + logger.debug("An SSLInvalidEvent has been received for monitor with ID: ${event.monitor.id}") + event.handle() + } + eventDispatcher.subscribeToSSLWillExpireEvents { event -> + logger.debug("An SSLWillExpireEvent has been received for monitor with ID: ${event.monitor.id}") + event.handle() + } + } + + private fun UptimeMonitorEvent.handle() { + runWhenStateChanges { smtpMailer.sendAsync(emailFactory.fromMonitorEvent(it)) } + } + + private fun SSLMonitorEvent.handle() { + runWhenStateChanges { smtpMailer.sendAsync(emailFactory.fromMonitorEvent(it)) } } } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandler.kt index 35096aa..28a5f7d 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandler.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandler.kt @@ -1,79 +1,21 @@ package com.kuvaszuptime.kuvasz.handlers -import com.kuvaszuptime.kuvasz.models.MonitorDownEvent -import com.kuvaszuptime.kuvasz.models.MonitorUpEvent -import com.kuvaszuptime.kuvasz.models.SlackWebhookMessage -import com.kuvaszuptime.kuvasz.models.UptimeMonitorEvent -import com.kuvaszuptime.kuvasz.models.runWhenStateChanges -import com.kuvaszuptime.kuvasz.models.toEmoji -import com.kuvaszuptime.kuvasz.models.toStructuredMessage +import com.kuvaszuptime.kuvasz.models.events.formatters.SlackTextFormatter import com.kuvaszuptime.kuvasz.services.EventDispatcher import com.kuvaszuptime.kuvasz.services.SlackWebhookService import io.micronaut.context.annotation.Context import io.micronaut.context.annotation.Requires -import io.micronaut.http.HttpResponse -import io.micronaut.http.client.exceptions.HttpClientResponseException -import io.micronaut.scheduling.TaskExecutors -import io.micronaut.scheduling.annotation.ExecuteOn -import io.reactivex.Flowable import org.slf4j.LoggerFactory import javax.inject.Inject @Context @Requires(property = "handler-config.slack-event-handler.enabled", value = "true") class SlackEventHandler @Inject constructor( - private val slackWebhookService: SlackWebhookService, - private val eventDispatcher: EventDispatcher -) { - companion object { - private val logger = LoggerFactory.getLogger(SlackEventHandler::class.java) - } + slackWebhookService: SlackWebhookService, + eventDispatcher: EventDispatcher +) : RTCMessageEventHandler(eventDispatcher, slackWebhookService) { - init { - subscribeToEvents() - } + override val logger = LoggerFactory.getLogger(SlackEventHandler::class.java) - @ExecuteOn(TaskExecutors.IO) - private fun subscribeToEvents() { - eventDispatcher.subscribeToMonitorUpEvents { event -> - logger.debug("A MonitorUpEvent has been received for monitor with ID: ${event.monitor.id}") - event.runWhenStateChanges { slackWebhookService.sendMessage(it.toSlackMessage()).handleResponse() } - } - eventDispatcher.subscribeToMonitorDownEvents { event -> - logger.debug("A MonitorDownEvent has been received for monitor with ID: ${event.monitor.id}") - event.runWhenStateChanges { slackWebhookService.sendMessage(it.toSlackMessage()).handleResponse() } - } - } - - private fun UptimeMonitorEvent.toSlackMessage() = SlackWebhookMessage(text = "${toEmoji()} ${toMessage()}") - - private fun Flowable>.handleResponse() = - subscribe( - { - logger.debug("A Slack message to your configured webhook has been successfully sent") - }, - { ex -> - if (ex is HttpClientResponseException) { - val responseBody = ex.response.getBody(String::class.java) - logger.error("Slack message cannot be sent to your configured webhook: $responseBody") - } - } - ) - - private fun UptimeMonitorEvent.toMessage() = - when (this) { - is MonitorUpEvent -> toStructuredMessage().let { details -> - listOfNotNull( - "*${details.summary}*", - "_${details.latency}_", - details.previousDownTime.orNull() - ) - } - is MonitorDownEvent -> toStructuredMessage().let { details -> - listOfNotNull( - "*${details.summary}*", - details.previousUpTime.orNull() - ) - } - }.joinToString("\n") + override val formatter = SlackTextFormatter } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/TelegramEventHandler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/TelegramEventHandler.kt index bb97dcf..3dcec00 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/TelegramEventHandler.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/TelegramEventHandler.kt @@ -1,84 +1,20 @@ package com.kuvaszuptime.kuvasz.handlers -import com.kuvaszuptime.kuvasz.config.handlers.TelegramEventHandlerConfig -import com.kuvaszuptime.kuvasz.models.MonitorDownEvent -import com.kuvaszuptime.kuvasz.models.MonitorUpEvent -import com.kuvaszuptime.kuvasz.models.TelegramAPIMessage -import com.kuvaszuptime.kuvasz.models.UptimeMonitorEvent -import com.kuvaszuptime.kuvasz.models.runWhenStateChanges -import com.kuvaszuptime.kuvasz.models.toEmoji -import com.kuvaszuptime.kuvasz.models.toStructuredMessage +import com.kuvaszuptime.kuvasz.models.events.formatters.TelegramTextFormatter import com.kuvaszuptime.kuvasz.services.EventDispatcher import com.kuvaszuptime.kuvasz.services.TelegramAPIService import io.micronaut.context.annotation.Context import io.micronaut.context.annotation.Requires -import io.micronaut.http.HttpResponse -import io.micronaut.http.client.exceptions.HttpClientResponseException -import io.micronaut.scheduling.TaskExecutors -import io.micronaut.scheduling.annotation.ExecuteOn -import io.reactivex.Flowable import org.slf4j.LoggerFactory @Context @Requires(property = "handler-config.telegram-event-handler.enabled", value = "true") class TelegramEventHandler( - private val telegramAPIService: TelegramAPIService, - private val telegramEventHandlerConfig: TelegramEventHandlerConfig, - private val eventDispatcher: EventDispatcher -) { - companion object { - private val logger = LoggerFactory.getLogger(TelegramEventHandler::class.java) - } + telegramAPIService: TelegramAPIService, + eventDispatcher: EventDispatcher +) : RTCMessageEventHandler(eventDispatcher, telegramAPIService) { - init { - subscribeToEvents() - } + override val logger = LoggerFactory.getLogger(TelegramEventHandler::class.java) - @ExecuteOn(TaskExecutors.IO) - private fun subscribeToEvents() { - eventDispatcher.subscribeToMonitorUpEvents { event -> - logger.debug("A MonitorUpEvent has been received for monitor with ID: ${event.monitor.id}") - event.runWhenStateChanges { telegramAPIService.sendMessage(it.toTelegramMessage()).handleResponse() } - } - eventDispatcher.subscribeToMonitorDownEvents { event -> - logger.debug("A MonitorDownEvent has been received for monitor with ID: ${event.monitor.id}") - event.runWhenStateChanges { telegramAPIService.sendMessage(it.toTelegramMessage()).handleResponse() } - } - } - - private fun UptimeMonitorEvent.toTelegramMessage(): TelegramAPIMessage = - TelegramAPIMessage( - text = "${toEmoji()} ${toHTMLMessage()}", - chat_id = telegramEventHandlerConfig.chatId - ) - - private fun Flowable>.handleResponse() = - subscribe( - { - logger.debug("A Telegram message to your configured webhook has been successfully sent") - }, - { ex -> - if (ex is HttpClientResponseException) { - val responseBody = ex.response.getBody(String::class.java) - logger.error("Telegram message cannot be delivered due to an error: $responseBody") - } - } - ) - - private fun UptimeMonitorEvent.toHTMLMessage() = - when (this) { - is MonitorUpEvent -> toStructuredMessage().let { details -> - listOfNotNull( - "${details.summary}", - "${details.latency}", - details.previousDownTime.orNull() - ) - } - is MonitorDownEvent -> toStructuredMessage().let { details -> - listOfNotNull( - "${details.summary}", - details.previousUpTime.orNull() - ) - } - }.joinToString("\n") + override val formatter = TelegramTextFormatter } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/Emoji.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/Emoji.kt deleted file mode 100644 index 9300f7d..0000000 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/Emoji.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.kuvaszuptime.kuvasz.models - -object Emoji { - const val ALERT = "🚨" - const val CHECK_OK = "✅" -} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/Event.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/Event.kt deleted file mode 100644 index 99f6503..0000000 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/Event.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.kuvaszuptime.kuvasz.models - -import arrow.core.Option -import arrow.core.getOrElse -import arrow.core.toOption -import com.kuvaszuptime.kuvasz.enums.UptimeStatus -import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo -import com.kuvaszuptime.kuvasz.tables.pojos.UptimeEventPojo -import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp -import com.kuvaszuptime.kuvasz.util.toDurationString -import io.micronaut.http.HttpStatus -import java.net.URI -import kotlin.time.Duration -import kotlin.time.DurationUnit -import kotlin.time.toDuration - -sealed class Event { - val dispatchedAt = getCurrentTimestamp() -} - -sealed class UptimeMonitorEvent : Event() { - abstract val monitor: MonitorPojo - abstract val previousEvent: Option -} - -data class MonitorUpEvent( - override val monitor: MonitorPojo, - val status: HttpStatus, - val latency: Int, - override val previousEvent: Option -) : UptimeMonitorEvent() - -data class MonitorDownEvent( - override val monitor: MonitorPojo, - val status: HttpStatus?, - val error: Throwable, - override val previousEvent: Option -) : UptimeMonitorEvent() - -data class RedirectEvent( - val monitor: MonitorPojo, - val redirectLocation: URI -) : Event() - -data class StructuredUpMessage( - val summary: String, - val latency: String, - val previousDownTime: Option -) - -data class StructuredDownMessage( - val summary: String, - val error: String, - val previousUpTime: Option -) - -fun UptimeMonitorEvent.toUptimeStatus(): UptimeStatus = - when (this) { - is MonitorUpEvent -> UptimeStatus.UP - is MonitorDownEvent -> UptimeStatus.DOWN - } - -fun UptimeMonitorEvent.toEmoji(): String = - when (this) { - is MonitorUpEvent -> Emoji.CHECK_OK - is MonitorDownEvent -> Emoji.ALERT - } - -fun UptimeMonitorEvent.uptimeStatusEquals(previousEvent: UptimeEventPojo) = - toUptimeStatus() == previousEvent.status - -fun UptimeMonitorEvent.uptimeStatusNotEquals(previousEvent: UptimeEventPojo) = - !uptimeStatusEquals(previousEvent) - -fun UptimeMonitorEvent.getEndedEventDuration(): Option = - previousEvent.flatMap { previousEvent -> - Option.fromNullable( - if (uptimeStatusNotEquals(previousEvent)) { - val diff = dispatchedAt.toEpochSecond() - previousEvent.startedAt.toEpochSecond() - diff.toDuration(DurationUnit.SECONDS) - } else null - ) - } - -fun UptimeMonitorEvent.runWhenStateChanges(toRun: (UptimeMonitorEvent) -> Unit) { - return previousEvent.fold( - { toRun(this) }, - { previousEvent -> - if (uptimeStatusNotEquals(previousEvent)) { - toRun(this) - } - } - ) -} - -fun MonitorUpEvent.toPlainMessage(): String = - toStructuredMessage().let { details -> - listOfNotNull( - details.summary, - details.latency, - details.previousDownTime.orNull() - ).joinToString(". ") - } - -fun MonitorUpEvent.toStructuredMessage() = - StructuredUpMessage( - summary = "Your monitor \"${monitor.name}\" (${monitor.url}) is UP (${status.code})", - latency = "Latency: ${latency}ms", - previousDownTime = getEndedEventDuration().toDurationString().map { "Was down for $it" } - ) - -fun MonitorDownEvent.toPlainMessage(): String = - toStructuredMessage().let { details -> - listOfNotNull( - details.summary, - details.error, - details.previousUpTime.orNull() - ).joinToString(". ") - } - -fun MonitorDownEvent.toStructuredMessage() = - StructuredDownMessage( - summary = "Your monitor \"${monitor.name}\" (${monitor.url}) is DOWN" + - status.toOption().map { " (" + it.code + ")" }.getOrElse { "" }, - error = "Reason: ${error.message}", - previousUpTime = getEndedEventDuration().toDurationString().map { "Was up for $it" } - ) diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/SSLValidation.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/SSLValidation.kt new file mode 100644 index 0000000..34eb3e1 --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/SSLValidation.kt @@ -0,0 +1,11 @@ +package com.kuvaszuptime.kuvasz.models + +import java.time.OffsetDateTime + +data class SSLValidationError( + val message: String? +) + +data class CertificateInfo( + val validTo: OffsetDateTime +) diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/ScheduledCheck.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/ScheduledCheck.kt index 0651546..f541d52 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/ScheduledCheck.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/ScheduledCheck.kt @@ -9,5 +9,5 @@ data class ScheduledCheck( ) enum class CheckType { - UPTIME + UPTIME, SSL } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/Error.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/ServiceError.kt similarity index 100% rename from src/main/kotlin/com/kuvaszuptime/kuvasz/models/Error.kt rename to src/main/kotlin/com/kuvaszuptime/kuvasz/models/ServiceError.kt diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorCreateDto.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorCreateDto.kt index 4a91434..675eb6f 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorCreateDto.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorCreateDto.kt @@ -19,11 +19,13 @@ data class MonitorCreateDto( @get:NotNull @get:Min(MIN_UPTIME_CHECK_INTERVAL) val uptimeCheckInterval: Int, - val enabled: Boolean? = true + val enabled: Boolean? = true, + val sslCheckEnabled: Boolean? = false ) { fun toMonitorPojo(): MonitorPojo = MonitorPojo() .setName(name) .setUrl(url) .setEnabled(enabled) .setUptimeCheckInterval(uptimeCheckInterval) + .setSslCheckEnabled(sslCheckEnabled) } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorDetailsDto.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorDetailsDto.kt index 86eb3a2..781cfae 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorDetailsDto.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorDetailsDto.kt @@ -1,5 +1,6 @@ package com.kuvaszuptime.kuvasz.models.dto +import com.kuvaszuptime.kuvasz.enums.SslStatus import com.kuvaszuptime.kuvasz.enums.UptimeStatus import io.micronaut.core.annotation.Introspected import java.net.URI @@ -12,12 +13,17 @@ data class MonitorDetailsDto( val url: URI, val uptimeCheckInterval: Int, val enabled: Boolean, + val sslCheckEnabled: Boolean, val createdAt: OffsetDateTime, val updatedAt: OffsetDateTime?, val uptimeStatus: UptimeStatus?, val uptimeStatusStartedAt: OffsetDateTime?, val lastUptimeCheck: OffsetDateTime?, + val sslStatus: SslStatus?, + val sslStatusStartedAt: OffsetDateTime?, + val lastSSLCheck: OffsetDateTime?, val uptimeError: String?, + val sslError: String?, val averageLatencyInMs: Int?, val p95LatencyInMs: Int?, val p99LatencyInMs: Int? diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorUpdateDto.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorUpdateDto.kt index d533fa3..ad196e7 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorUpdateDto.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorUpdateDto.kt @@ -13,5 +13,6 @@ data class MonitorUpdateDto( val url: String?, @get:Min(MIN_UPTIME_CHECK_INTERVAL) val uptimeCheckInterval: Int?, - val enabled: Boolean? + val enabled: Boolean?, + val sslCheckEnabled: Boolean? ) diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/MonitorEvent.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/MonitorEvent.kt new file mode 100644 index 0000000..8e16a94 --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/MonitorEvent.kt @@ -0,0 +1,181 @@ +package com.kuvaszuptime.kuvasz.models.events + +import arrow.core.Option +import arrow.core.getOrElse +import arrow.core.toOption +import com.kuvaszuptime.kuvasz.enums.SslStatus +import com.kuvaszuptime.kuvasz.enums.UptimeStatus +import com.kuvaszuptime.kuvasz.models.CertificateInfo +import com.kuvaszuptime.kuvasz.models.SSLValidationError +import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo +import com.kuvaszuptime.kuvasz.tables.pojos.SslEventPojo +import com.kuvaszuptime.kuvasz.tables.pojos.UptimeEventPojo +import com.kuvaszuptime.kuvasz.util.diffToDuration +import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp +import com.kuvaszuptime.kuvasz.util.toDurationString +import io.micronaut.http.HttpStatus +import java.net.URI +import kotlin.time.Duration + +sealed class MonitorEvent { + abstract val monitor: MonitorPojo + + abstract fun toStructuredMessage(): StructuredMessage + + val dispatchedAt = getCurrentTimestamp() +} + +sealed class UptimeMonitorEvent : MonitorEvent() { + abstract val previousEvent: Option + + abstract val uptimeStatus: UptimeStatus + + fun statusNotEquals(previousEvent: UptimeEventPojo) = !statusEquals(previousEvent) + + fun getEndedEventDuration(): Option = + previousEvent.flatMap { previousEvent -> + Option.fromNullable( + if (statusNotEquals(previousEvent)) { + previousEvent.startedAt.diffToDuration(dispatchedAt) + } else null + ) + } + + fun runWhenStateChanges(toRun: (UptimeMonitorEvent) -> Unit) { + return previousEvent.fold( + { toRun(this) }, + { previousEvent -> + if (statusNotEquals(previousEvent)) { + toRun(this) + } + } + ) + } + + private fun statusEquals(previousEvent: UptimeEventPojo) = uptimeStatus == previousEvent.status +} + +data class MonitorUpEvent( + override val monitor: MonitorPojo, + val status: HttpStatus, + val latency: Int, + override val previousEvent: Option +) : UptimeMonitorEvent() { + + override val uptimeStatus = UptimeStatus.UP + + override fun toStructuredMessage() = + StructuredMonitorUpMessage( + summary = "Your monitor \"${monitor.name}\" (${monitor.url}) is UP (${status.code})", + latency = "Latency: ${latency}ms", + previousDownTime = getEndedEventDuration().toDurationString().map { "Was down for $it" } + ) +} + +data class MonitorDownEvent( + override val monitor: MonitorPojo, + val status: HttpStatus?, + val error: Throwable, + override val previousEvent: Option +) : UptimeMonitorEvent() { + + override val uptimeStatus = UptimeStatus.DOWN + + override fun toStructuredMessage() = + StructuredMonitorDownMessage( + summary = "Your monitor \"${monitor.name}\" (${monitor.url}) is DOWN" + + status.toOption().map { " (" + it.code + ")" }.getOrElse { "" }, + error = "Reason: ${error.message}", + previousUpTime = getEndedEventDuration().toDurationString().map { "Was up for $it" } + ) +} + +data class RedirectEvent( + override val monitor: MonitorPojo, + val redirectLocation: URI +) : MonitorEvent() { + + override fun toStructuredMessage() = StructuredRedirectMessage( + summary = "Request to \"${monitor.name}\" (${monitor.url}) has been redirected" + ) +} + +sealed class SSLMonitorEvent : MonitorEvent() { + abstract val previousEvent: Option + + abstract val sslStatus: SslStatus + + fun statusNotEquals(previousEvent: SslEventPojo) = !statusEquals(previousEvent) + + fun getEndedEventDuration(): Option = + previousEvent.flatMap { previousEvent -> + Option.fromNullable( + if (statusNotEquals(previousEvent)) { + previousEvent.startedAt.diffToDuration(dispatchedAt) + } else null + ) + } + + fun getPreviousStatusString(): String = previousEvent.map { it.status.name }.getOrElse { "" } + + fun runWhenStateChanges(toRun: (SSLMonitorEvent) -> Unit) { + return previousEvent.fold( + { toRun(this) }, + { previousEvent -> + if (statusNotEquals(previousEvent)) { + toRun(this) + } + } + ) + } + + private fun statusEquals(previousEvent: SslEventPojo) = sslStatus == previousEvent.status +} + +data class SSLValidEvent( + override val monitor: MonitorPojo, + val certInfo: CertificateInfo, + override val previousEvent: Option +) : SSLMonitorEvent() { + + override val sslStatus = SslStatus.VALID + + override fun toStructuredMessage() = + StructuredSSLValidMessage( + summary = "Your site \"${monitor.name}\" (${monitor.url}) has a VALID certificate", + previousInvalidEvent = getEndedEventDuration().toDurationString() + .map { "Was ${getPreviousStatusString()} for $it" } + ) +} + +data class SSLInvalidEvent( + override val monitor: MonitorPojo, + val error: SSLValidationError, + override val previousEvent: Option +) : SSLMonitorEvent() { + + override val sslStatus = SslStatus.INVALID + + override fun toStructuredMessage() = + StructuredSSLInvalidMessage( + summary = "Your site \"${monitor.name}\" (${monitor.url}) has an INVALID certificate", + error = "Reason: ${error.message}", + previousValidEvent = getEndedEventDuration().toDurationString() + .map { "Was ${getPreviousStatusString()} for $it" } + ) +} + +data class SSLWillExpireEvent( + override val monitor: MonitorPojo, + val certInfo: CertificateInfo, + override val previousEvent: Option +) : SSLMonitorEvent() { + + override val sslStatus = SslStatus.WILL_EXPIRE + + override fun toStructuredMessage() = + StructuredSSLWillExpireMessage( + summary = "Your SSL certificate for ${monitor.url} will expire soon", + validUntil = "Expiry date: ${certInfo.validTo}" + ) +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/StructuredMessage.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/StructuredMessage.kt new file mode 100644 index 0000000..e0bc8b3 --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/StructuredMessage.kt @@ -0,0 +1,41 @@ +package com.kuvaszuptime.kuvasz.models.events + +import arrow.core.Option + +sealed class StructuredMessage + +sealed class StructuredMonitorMessage : StructuredMessage() + +data class StructuredMonitorUpMessage( + val summary: String, + val latency: String, + val previousDownTime: Option +) : StructuredMonitorMessage() + +data class StructuredMonitorDownMessage( + val summary: String, + val error: String, + val previousUpTime: Option +) : StructuredMonitorMessage() + +data class StructuredRedirectMessage( + val summary: String +) : StructuredMessage() + +sealed class StructuredSSLMessage : StructuredMessage() + +data class StructuredSSLValidMessage( + val summary: String, + val previousInvalidEvent: Option +) : StructuredSSLMessage() + +data class StructuredSSLInvalidMessage( + val summary: String, + val error: String, + val previousValidEvent: Option +) : StructuredSSLMessage() + +data class StructuredSSLWillExpireMessage( + val summary: String, + val validUntil: String +) : StructuredSSLMessage() diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/Emoji.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/Emoji.kt new file mode 100644 index 0000000..572b1cc --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/Emoji.kt @@ -0,0 +1,27 @@ +package com.kuvaszuptime.kuvasz.models.events.formatters + +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.RedirectEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent + +object Emoji { + const val ALERT = "🚨" + const val CHECK_OK = "✅" + const val WARNING = "⚠️" + const val INFO = "ℹ️" + const val LOCK = "🔒️" +} + +fun MonitorEvent.getEmoji(): String = + when (this) { + is MonitorUpEvent -> Emoji.CHECK_OK + is MonitorDownEvent -> Emoji.ALERT + is RedirectEvent -> Emoji.INFO + is SSLValidEvent -> Emoji.LOCK + is SSLInvalidEvent -> Emoji.ALERT + is SSLWillExpireEvent -> Emoji.WARNING + } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/LogMessageFormatter.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/LogMessageFormatter.kt new file mode 100644 index 0000000..f150d1e --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/LogMessageFormatter.kt @@ -0,0 +1,64 @@ +package com.kuvaszuptime.kuvasz.models.events.formatters + +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.RedirectEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLMonitorEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent +import com.kuvaszuptime.kuvasz.models.events.UptimeMonitorEvent + +object LogMessageFormatter : TextMessageFormatter { + + override fun toFormattedMessage(event: UptimeMonitorEvent): String { + val messageParts: List = when (event) { + is MonitorUpEvent -> event.toStructuredMessage().let { details -> + listOfNotNull( + event.getEmoji() + " " + details.summary, + details.latency, + details.previousDownTime.orNull() + ) + } + is MonitorDownEvent -> event.toStructuredMessage().let { details -> + listOfNotNull( + event.getEmoji() + " " + details.summary, + details.error, + details.previousUpTime.orNull() + ) + } + } + + return messageParts.assemble() + } + + override fun toFormattedMessage(event: SSLMonitorEvent): String { + val messageParts: List = when (event) { + is SSLValidEvent -> event.toStructuredMessage().let { details -> + listOfNotNull( + event.getEmoji() + " " + details.summary, + details.previousInvalidEvent.orNull() + ) + } + is SSLWillExpireEvent -> event.toStructuredMessage().let { details -> + listOf( + event.getEmoji() + " " + details.summary, + details.validUntil + ) + } + is SSLInvalidEvent -> event.toStructuredMessage().let { details -> + listOfNotNull( + event.getEmoji() + " " + details.summary, + details.error, + details.previousValidEvent.orNull() + ) + } + } + + return messageParts.assemble() + } + + fun toFormattedMessage(event: RedirectEvent) = "${event.getEmoji()} ${event.toStructuredMessage().summary}" + + private fun List.assemble(): String = joinToString(". ") +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/PlainTextMessageFormatter.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/PlainTextMessageFormatter.kt new file mode 100644 index 0000000..99dea73 --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/PlainTextMessageFormatter.kt @@ -0,0 +1,61 @@ +package com.kuvaszuptime.kuvasz.models.events.formatters + +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLMonitorEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent +import com.kuvaszuptime.kuvasz.models.events.UptimeMonitorEvent + +object PlainTextMessageFormatter : TextMessageFormatter { + + override fun toFormattedMessage(event: UptimeMonitorEvent): String { + val messageParts: List = when (event) { + is MonitorUpEvent -> event.toStructuredMessage().let { details -> + listOfNotNull( + details.summary, + details.latency, + details.previousDownTime.orNull() + ) + } + is MonitorDownEvent -> event.toStructuredMessage().let { details -> + listOfNotNull( + details.summary, + details.error, + details.previousUpTime.orNull() + ) + } + } + + return messageParts.assemble() + } + + override fun toFormattedMessage(event: SSLMonitorEvent): String { + val messageParts: List = when (event) { + is SSLValidEvent -> event.toStructuredMessage().let { details -> + listOfNotNull( + details.summary, + details.previousInvalidEvent.orNull() + ) + } + is SSLWillExpireEvent -> event.toStructuredMessage().let { details -> + listOf( + details.summary, + details.validUntil + ) + } + is SSLInvalidEvent -> event.toStructuredMessage().let { details -> + listOfNotNull( + details.summary, + details.error, + details.previousValidEvent.orNull() + ) + } + } + + return messageParts.assemble() + } + + private fun List.assemble(): String = joinToString("\n") +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/RichTextMessageFormatter.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/RichTextMessageFormatter.kt new file mode 100644 index 0000000..dbeaefe --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/RichTextMessageFormatter.kt @@ -0,0 +1,63 @@ +package com.kuvaszuptime.kuvasz.models.events.formatters + +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLMonitorEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent +import com.kuvaszuptime.kuvasz.models.events.UptimeMonitorEvent + +abstract class RichTextMessageFormatter : TextMessageFormatter { + abstract fun bold(input: String): String + + abstract fun italic(input: String): String + + override fun toFormattedMessage(event: UptimeMonitorEvent): String { + val messageParts: List = when (event) { + is MonitorUpEvent -> event.toStructuredMessage().let { details -> + listOfNotNull( + event.getEmoji() + " " + bold(details.summary), + italic(details.latency), + details.previousDownTime.orNull() + ) + } + is MonitorDownEvent -> event.toStructuredMessage().let { details -> + listOfNotNull( + event.getEmoji() + " " + bold(details.summary), + details.previousUpTime.orNull() + ) + } + } + + return messageParts.assemble() + } + + override fun toFormattedMessage(event: SSLMonitorEvent): String { + val messageParts: List = when (event) { + is SSLValidEvent -> event.toStructuredMessage().let { details -> + listOfNotNull( + event.getEmoji() + " " + bold(details.summary), + details.previousInvalidEvent.orNull() + ) + } + is SSLWillExpireEvent -> event.toStructuredMessage().let { details -> + listOf( + event.getEmoji() + " " + bold(details.summary), + italic(details.validUntil) + ) + } + is SSLInvalidEvent -> event.toStructuredMessage().let { details -> + listOfNotNull( + event.getEmoji() + " " + bold(details.summary), + italic(details.error), + details.previousValidEvent.orNull() + ) + } + } + + return messageParts.assemble() + } + + private fun List.assemble(): String = joinToString("\n") +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/SlackTextFormatter.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/SlackTextFormatter.kt new file mode 100644 index 0000000..2f82ff7 --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/SlackTextFormatter.kt @@ -0,0 +1,7 @@ +package com.kuvaszuptime.kuvasz.models.events.formatters + +object SlackTextFormatter : RichTextMessageFormatter() { + override fun bold(input: String): String = "*$input*" + + override fun italic(input: String): String = "_${input}_" +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/TelegramTextFormatter.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/TelegramTextFormatter.kt new file mode 100644 index 0000000..e84095d --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/TelegramTextFormatter.kt @@ -0,0 +1,7 @@ +package com.kuvaszuptime.kuvasz.models.events.formatters + +object TelegramTextFormatter : RichTextMessageFormatter() { + override fun bold(input: String): String = "$input" + + override fun italic(input: String): String = "$input" +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/TextMessageFormatter.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/TextMessageFormatter.kt new file mode 100644 index 0000000..9621068 --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/TextMessageFormatter.kt @@ -0,0 +1,11 @@ +package com.kuvaszuptime.kuvasz.models.events.formatters + +import com.kuvaszuptime.kuvasz.models.events.SSLMonitorEvent +import com.kuvaszuptime.kuvasz.models.events.UptimeMonitorEvent + +interface TextMessageFormatter { + + fun toFormattedMessage(event: UptimeMonitorEvent): String + + fun toFormattedMessage(event: SSLMonitorEvent): String +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/SlackWebhookMessage.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/SlackWebhookMessage.kt similarity index 89% rename from src/main/kotlin/com/kuvaszuptime/kuvasz/models/SlackWebhookMessage.kt rename to src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/SlackWebhookMessage.kt index 916cb4b..36e4b66 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/SlackWebhookMessage.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/SlackWebhookMessage.kt @@ -1,4 +1,4 @@ -package com.kuvaszuptime.kuvasz.models +package com.kuvaszuptime.kuvasz.models.handlers import io.micronaut.core.annotation.Introspected import java.net.URI diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/TelegramAPIMessage.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/TelegramAPIMessage.kt similarity index 84% rename from src/main/kotlin/com/kuvaszuptime/kuvasz/models/TelegramAPIMessage.kt rename to src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/TelegramAPIMessage.kt index 68e10f2..b25b23f 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/TelegramAPIMessage.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/TelegramAPIMessage.kt @@ -1,4 +1,4 @@ -package com.kuvaszuptime.kuvasz.models +package com.kuvaszuptime.kuvasz.models.handlers import io.micronaut.core.annotation.Introspected diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/MonitorRepository.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/MonitorRepository.kt index 9719179..2c24b9b 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/MonitorRepository.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/MonitorRepository.kt @@ -9,6 +9,7 @@ import com.kuvaszuptime.kuvasz.models.PersistenceError import com.kuvaszuptime.kuvasz.models.dto.MonitorDetailsDto import com.kuvaszuptime.kuvasz.tables.LatencyLog.LATENCY_LOG import com.kuvaszuptime.kuvasz.tables.Monitor.MONITOR +import com.kuvaszuptime.kuvasz.tables.SslEvent.SSL_EVENT import com.kuvaszuptime.kuvasz.tables.UptimeEvent.UPTIME_EVENT import com.kuvaszuptime.kuvasz.tables.daos.MonitorDao import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo @@ -40,7 +41,11 @@ class MonitorRepository @Inject constructor(jooqConfig: Configuration) : Monitor UPTIME_EVENT.STATUS, UPTIME_EVENT.STARTED_AT, UPTIME_EVENT.UPDATED_AT, - UPTIME_EVENT.ERROR + UPTIME_EVENT.ERROR, + SSL_EVENT.STATUS, + SSL_EVENT.STARTED_AT, + SSL_EVENT.UPDATED_AT, + SSL_EVENT.ERROR ) .fetchInto(MonitorDetailsDto::class.java) @@ -52,7 +57,11 @@ class MonitorRepository @Inject constructor(jooqConfig: Configuration) : Monitor UPTIME_EVENT.STATUS, UPTIME_EVENT.STARTED_AT, UPTIME_EVENT.UPDATED_AT, - UPTIME_EVENT.ERROR + UPTIME_EVENT.ERROR, + SSL_EVENT.STATUS, + SSL_EVENT.STARTED_AT, + SSL_EVENT.UPDATED_AT, + SSL_EVENT.ERROR ) .fetchOneInto(MonitorDetailsDto::class.java) .toOption() @@ -80,6 +89,7 @@ class MonitorRepository @Inject constructor(jooqConfig: Configuration) : Monitor .set(MONITOR.URL, updatedPojo.url) .set(MONITOR.UPTIME_CHECK_INTERVAL, updatedPojo.uptimeCheckInterval) .set(MONITOR.ENABLED, updatedPojo.enabled) + .set(MONITOR.SSL_CHECK_ENABLED, updatedPojo.sslCheckEnabled) .set(MONITOR.UPDATED_AT, getCurrentTimestamp()) .where(MONITOR.ID.eq(updatedPojo.id)) .returning(MONITOR.asterisk()) @@ -98,18 +108,24 @@ class MonitorRepository @Inject constructor(jooqConfig: Configuration) : Monitor MONITOR.URL.`as`("url"), MONITOR.UPTIME_CHECK_INTERVAL.`as`("uptimeCheckInterval"), MONITOR.ENABLED.`as`("enabled"), + MONITOR.SSL_CHECK_ENABLED.`as`("sslCheckEnabled"), MONITOR.CREATED_AT.`as`("createdAt"), MONITOR.UPDATED_AT.`as`("updatedAt"), UPTIME_EVENT.STATUS.`as`("uptimeStatus"), UPTIME_EVENT.STARTED_AT.`as`("uptimeStatusStartedAt"), UPTIME_EVENT.UPDATED_AT.`as`("lastUptimeCheck"), + SSL_EVENT.STATUS.`as`("sslStatus"), + SSL_EVENT.STARTED_AT.`as`("sslStatusStartedAt"), + SSL_EVENT.UPDATED_AT.`as`("lastSSLCheck"), UPTIME_EVENT.ERROR.`as`("uptimeError"), + SSL_EVENT.ERROR.`as`("sslError"), round(avg(LATENCY_LOG.LATENCY), -1).`as`("averageLatencyInMs"), inline(null, SQLDataType.INTEGER).`as`("p95LatencyInMs"), inline(null, SQLDataType.INTEGER).`as`("p99LatencyInMs") ) .from(MONITOR) .leftJoin(UPTIME_EVENT).on(MONITOR.ID.eq(UPTIME_EVENT.MONITOR_ID).and(UPTIME_EVENT.ENDED_AT.isNull)) + .leftJoin(SSL_EVENT).on(MONITOR.ID.eq(SSL_EVENT.MONITOR_ID).and(SSL_EVENT.ENDED_AT.isNull)) .leftJoin(LATENCY_LOG).on(MONITOR.ID.eq(LATENCY_LOG.MONITOR_ID)) private fun DataAccessException.handle(): Either { diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/SSLEventRepository.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/SSLEventRepository.kt new file mode 100644 index 0000000..535b495 --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/SSLEventRepository.kt @@ -0,0 +1,58 @@ +package com.kuvaszuptime.kuvasz.repositories + +import arrow.core.toOption +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLMonitorEvent +import com.kuvaszuptime.kuvasz.tables.SslEvent.SSL_EVENT +import com.kuvaszuptime.kuvasz.tables.daos.SslEventDao +import com.kuvaszuptime.kuvasz.tables.pojos.SslEventPojo +import org.jooq.Configuration +import java.time.OffsetDateTime +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SSLEventRepository @Inject constructor(jooqConfig: Configuration) : SslEventDao(jooqConfig) { + private val dsl = jooqConfig.dsl() + + fun insertFromMonitorEvent(event: SSLMonitorEvent) { + val eventToInsert = SslEventPojo() + .setMonitorId(event.monitor.id) + .setStatus(event.sslStatus) + .setStartedAt(event.dispatchedAt) + .setUpdatedAt(event.dispatchedAt) + + if (event is SSLInvalidEvent) { + eventToInsert.error = event.error.message + } + + insert(eventToInsert) + } + + fun getPreviousEventByMonitorId(monitorId: Int) = + dsl.select(SSL_EVENT.asterisk()) + .from(SSL_EVENT) + .where(SSL_EVENT.MONITOR_ID.eq(monitorId)) + .and(SSL_EVENT.ENDED_AT.isNull) + .fetchOneInto(SslEventPojo::class.java) + .toOption() + + fun endEventById(eventId: Int, endedAt: OffsetDateTime) = + dsl.update(SSL_EVENT) + .set(SSL_EVENT.ENDED_AT, endedAt) + .set(SSL_EVENT.UPDATED_AT, endedAt) + .where(SSL_EVENT.ID.eq(eventId)) + .execute() + + fun deleteEventsBeforeDate(limit: OffsetDateTime) = + dsl.delete(SSL_EVENT) + .where(SSL_EVENT.ENDED_AT.isNotNull) + .and(SSL_EVENT.ENDED_AT.lessThan(limit)) + .execute() + + fun updateEventUpdatedAt(eventId: Int, updatedAt: OffsetDateTime) = + dsl.update(SSL_EVENT) + .set(SSL_EVENT.UPDATED_AT, updatedAt) + .where(SSL_EVENT.ID.eq(eventId)) + .execute() +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/UptimeEventRepository.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/UptimeEventRepository.kt index a2f8886..52293c9 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/UptimeEventRepository.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/UptimeEventRepository.kt @@ -1,9 +1,10 @@ package com.kuvaszuptime.kuvasz.repositories +import arrow.core.getOrElse import arrow.core.toOption -import com.kuvaszuptime.kuvasz.models.MonitorDownEvent -import com.kuvaszuptime.kuvasz.models.UptimeMonitorEvent -import com.kuvaszuptime.kuvasz.models.toUptimeStatus +import com.kuvaszuptime.kuvasz.enums.UptimeStatus +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.UptimeMonitorEvent import com.kuvaszuptime.kuvasz.tables.UptimeEvent.UPTIME_EVENT import com.kuvaszuptime.kuvasz.tables.daos.UptimeEventDao import com.kuvaszuptime.kuvasz.tables.pojos.UptimeEventPojo @@ -19,7 +20,7 @@ class UptimeEventRepository @Inject constructor(jooqConfig: Configuration) : Upt fun insertFromMonitorEvent(event: UptimeMonitorEvent) { val eventToInsert = UptimeEventPojo() .setMonitorId(event.monitor.id) - .setStatus(event.toUptimeStatus()) + .setStatus(event.uptimeStatus) .setStartedAt(event.dispatchedAt) .setUpdatedAt(event.dispatchedAt) @@ -56,4 +57,9 @@ class UptimeEventRepository @Inject constructor(jooqConfig: Configuration) : Upt .set(UPTIME_EVENT.UPDATED_AT, updatedAt) .where(UPTIME_EVENT.ID.eq(eventId)) .execute() + + fun isMonitorUp(monitorId: Int): Boolean = + getPreviousEventByMonitorId(monitorId) + .map { it.status == UptimeStatus.UP } + .getOrElse { false } } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/CheckScheduler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/CheckScheduler.kt index ba4d5c5..e258af0 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/CheckScheduler.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/CheckScheduler.kt @@ -1,6 +1,9 @@ package com.kuvaszuptime.kuvasz.services import arrow.core.Either +import arrow.core.Option +import arrow.core.Option.Companion.empty +import arrow.core.toOption import com.kuvaszuptime.kuvasz.models.CheckType import com.kuvaszuptime.kuvasz.models.ScheduledCheck import com.kuvaszuptime.kuvasz.models.SchedulingError @@ -12,6 +15,7 @@ import io.micronaut.context.annotation.Context import io.micronaut.scheduling.TaskExecutors import io.micronaut.scheduling.TaskScheduler import org.slf4j.LoggerFactory +import java.time.Duration import java.util.concurrent.ScheduledFuture import javax.annotation.PostConstruct import javax.inject.Inject @@ -21,10 +25,13 @@ import javax.inject.Named class CheckScheduler @Inject constructor( @Named(TaskExecutors.SCHEDULED) private val taskScheduler: TaskScheduler, private val monitorRepository: MonitorRepository, - private val uptimeChecker: UptimeChecker + private val uptimeChecker: UptimeChecker, + private val sslChecker: SSLChecker ) { companion object { + private const val SSL_CHECK_INITIAL_DELAY_MINUTES = 1L + private const val SSL_CHECK_PERIOD_DAYS = 1L private val logger = LoggerFactory.getLogger(CheckScheduler::class.java) } @@ -38,23 +45,42 @@ class CheckScheduler @Inject constructor( fun getScheduledChecks() = scheduledChecks - fun createChecksForMonitor(monitor: MonitorPojo): Either = - scheduleUptimeCheck(monitor).bimap( - { e -> - logger.error( - "Uptime check for \"${monitor.name}\" (${monitor.url}) cannot be set up: ${e.message}" - ) - SchedulingError(e.message) + fun createChecksForMonitor(monitor: MonitorPojo): Option { + fun Throwable.log(checkType: CheckType, monitor: MonitorPojo) { + logger.error("${checkType.name} check for \"${monitor.name}\" (${monitor.url}) cannot be set up: $message") + } + + fun ScheduledCheck.log(monitor: MonitorPojo) { + logger.info("${checkType.name} check for \"${monitor.name}\" (${monitor.url}) has been set up successfully") + } + + return scheduleUptimeCheck(monitor).fold( + { error -> + error.log(CheckType.UPTIME, monitor) + SchedulingError(error.message).toOption() }, - { scheduledTask -> - val scheduledCheck = - ScheduledCheck(checkType = CheckType.UPTIME, monitorId = monitor.id, task = scheduledTask) - scheduledChecks.add(scheduledCheck) - logger.info("Uptime check for \"${monitor.name}\" (${monitor.url}) has been set up successfully") + { scheduledUptimeTask -> + ScheduledCheck(checkType = CheckType.UPTIME, monitorId = monitor.id, task = scheduledUptimeTask) + .also { scheduledChecks.add(it) } + .also { it.log(monitor) } - scheduledCheck + if (monitor.sslCheckEnabled) { + scheduleSSLCheck(monitor).fold( + { error -> + error.log(CheckType.SSL, monitor) + SchedulingError(error.message).toOption() + }, + { scheduledSSLTask -> + ScheduledCheck(checkType = CheckType.SSL, monitorId = monitor.id, task = scheduledSSLTask) + .also { scheduledChecks.add(it) } + .also { it.log(monitor) } + } + ) + } + empty() } ) + } fun removeChecksOfMonitor(monitor: MonitorPojo) { scheduledChecks.forEach { check -> @@ -76,7 +102,7 @@ class CheckScheduler @Inject constructor( fun updateChecksForMonitor( existingMonitor: MonitorPojo, updatedMonitor: MonitorPojo - ): Either { + ): Option { removeChecksOfMonitor(existingMonitor) return createChecksForMonitor(updatedMonitor) } @@ -88,4 +114,13 @@ class CheckScheduler @Inject constructor( uptimeChecker.check(monitor) } } + + private fun scheduleSSLCheck(monitor: MonitorPojo): Either> = + Either.catchBlocking { + val initialDelay = Duration.ofMinutes(SSL_CHECK_INITIAL_DELAY_MINUTES) + val period = Duration.ofDays(SSL_CHECK_PERIOD_DAYS) + taskScheduler.scheduleAtFixedRate(initialDelay, period) { + sslChecker.check(monitor) + } + } } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/DatabaseCleaner.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/DatabaseCleaner.kt index 76c5968..567d837 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/DatabaseCleaner.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/DatabaseCleaner.kt @@ -2,6 +2,7 @@ package com.kuvaszuptime.kuvasz.services import com.kuvaszuptime.kuvasz.config.AppConfig import com.kuvaszuptime.kuvasz.repositories.LatencyLogRepository +import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp import io.micronaut.context.annotation.Requires @@ -15,7 +16,8 @@ import javax.inject.Singleton class DatabaseCleaner @Inject constructor( private val appConfig: AppConfig, private val uptimeEventRepository: UptimeEventRepository, - private val latencyLogRepository: LatencyLogRepository + private val latencyLogRepository: LatencyLogRepository, + private val sslEventRepository: SSLEventRepository ) { companion object { @@ -28,8 +30,10 @@ class DatabaseCleaner @Inject constructor( val limit = getCurrentTimestamp().minusDays(appConfig.dataRetentionDays.toLong()) val deletedUptimeEvents = uptimeEventRepository.deleteEventsBeforeDate(limit) val deletedLatencyLogs = latencyLogRepository.deleteLogsBeforeDate(limit) + val deletedSSLEvents = sslEventRepository.deleteEventsBeforeDate(limit) logger.info("$deletedUptimeEvents UPTIME_EVENT record has been deleted") logger.info("$deletedLatencyLogs LATENCY_LOG record has been deleted") + logger.info("$deletedSSLEvents SSL_EVENT record has been deleted") } } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/EventDispatcher.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/EventDispatcher.kt index e0b109d..707c277 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/EventDispatcher.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/EventDispatcher.kt @@ -1,9 +1,12 @@ package com.kuvaszuptime.kuvasz.services -import com.kuvaszuptime.kuvasz.models.Event -import com.kuvaszuptime.kuvasz.models.MonitorDownEvent -import com.kuvaszuptime.kuvasz.models.MonitorUpEvent -import com.kuvaszuptime.kuvasz.models.RedirectEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.RedirectEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent import io.reactivex.disposables.Disposable import io.reactivex.subjects.PublishSubject import javax.inject.Singleton @@ -14,12 +17,18 @@ class EventDispatcher { private val monitorUpEvents = PublishSubject.create() private val monitorDownEvents = PublishSubject.create() private val redirectEvents = PublishSubject.create() + private val sslValidEvents = PublishSubject.create() + private val sslWillExpireEvents = PublishSubject.create() + private val sslInvalidEvents = PublishSubject.create() - fun dispatch(event: Event) = + fun dispatch(event: MonitorEvent) = when (event) { is MonitorUpEvent -> monitorUpEvents.onNext(event) is MonitorDownEvent -> monitorDownEvents.onNext(event) is RedirectEvent -> redirectEvents.onNext(event) + is SSLValidEvent -> sslValidEvents.onNext(event) + is SSLInvalidEvent -> sslInvalidEvents.onNext(event) + is SSLWillExpireEvent -> sslWillExpireEvents.onNext(event) } fun subscribeToMonitorUpEvents(consumer: (MonitorUpEvent) -> Unit): Disposable = @@ -30,4 +39,13 @@ class EventDispatcher { fun subscribeToRedirectEvents(consumer: (RedirectEvent) -> Unit): Disposable = redirectEvents.subscribe(consumer) + + fun subscribeToSSLValidEvents(consumer: (SSLValidEvent) -> Unit): Disposable = + sslValidEvents.subscribe(consumer) + + fun subscribeToSSLInvalidEvents(consumer: (SSLInvalidEvent) -> Unit): Disposable = + sslInvalidEvents.subscribe(consumer) + + fun subscribeToSSLWillExpireEvents(consumer: (SSLWillExpireEvent) -> Unit): Disposable = + sslWillExpireEvents.subscribe(consumer) } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/MonitorCrudService.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/MonitorCrudService.kt index 6bd9df4..19949cc 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/MonitorCrudService.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/MonitorCrudService.kt @@ -47,7 +47,7 @@ class MonitorCrudService @Inject constructor( { persistenceError -> throw persistenceError }, { insertedMonitor -> if (insertedMonitor.enabled) { - checkScheduler.createChecksForMonitor(insertedMonitor).mapLeft { schedulingError -> + checkScheduler.createChecksForMonitor(insertedMonitor).map { schedulingError -> monitorRepository.deleteById(insertedMonitor.id) throw schedulingError } @@ -74,6 +74,7 @@ class MonitorCrudService @Inject constructor( url = monitorUpdateDto.url ?: existingMonitor.url uptimeCheckInterval = monitorUpdateDto.uptimeCheckInterval ?: existingMonitor.uptimeCheckInterval enabled = monitorUpdateDto.enabled ?: existingMonitor.enabled + sslCheckEnabled = monitorUpdateDto.sslCheckEnabled ?: existingMonitor.sslCheckEnabled } updatedMonitor.saveAndReschedule(existingMonitor) @@ -86,8 +87,8 @@ class MonitorCrudService @Inject constructor( { updatedMonitor -> if (updatedMonitor.enabled) { checkScheduler.updateChecksForMonitor(existingMonitor, updatedMonitor).fold( - { schedulingError -> throw schedulingError }, - { updatedMonitor } + { updatedMonitor }, + { schedulingError -> throw schedulingError } ) } else { checkScheduler.removeChecksOfMonitor(existingMonitor) diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SSLChecker.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SSLChecker.kt new file mode 100644 index 0000000..f0bbc24 --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SSLChecker.kt @@ -0,0 +1,64 @@ +package com.kuvaszuptime.kuvasz.services + +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent +import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository +import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository +import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo +import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn +import java.net.URL +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SSLChecker @Inject constructor( + private val sslValidator: SSLValidator, + private val uptimeEventRepository: UptimeEventRepository, + private val eventDispatcher: EventDispatcher, + private val sslEventRepository: SSLEventRepository +) { + + companion object { + private const val EXPIRY_THRESHOLD_DAYS = 30L + } + + @ExecuteOn(TaskExecutors.IO) + fun check(monitor: MonitorPojo) { + if (uptimeEventRepository.isMonitorUp(monitor.id)) { + val previousEvent = sslEventRepository.getPreviousEventByMonitorId(monitorId = monitor.id) + sslValidator.validate(URL(monitor.url)).fold( + { error -> + eventDispatcher.dispatch( + SSLInvalidEvent( + monitor = monitor, + error = error, + previousEvent = previousEvent + ) + ) + }, + { certInfo -> + if (certInfo.validTo.isBefore(getCurrentTimestamp().plusDays(EXPIRY_THRESHOLD_DAYS))) { + eventDispatcher.dispatch( + SSLWillExpireEvent( + monitor = monitor, + certInfo = certInfo, + previousEvent = previousEvent + ) + ) + } else { + eventDispatcher.dispatch( + SSLValidEvent( + monitor = monitor, + certInfo = certInfo, + previousEvent = previousEvent + ) + ) + } + } + ) + } + } +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SSLValidator.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SSLValidator.kt new file mode 100644 index 0000000..b555949 --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SSLValidator.kt @@ -0,0 +1,53 @@ +package com.kuvaszuptime.kuvasz.services + +import arrow.core.Either +import arrow.core.Option +import com.kuvaszuptime.kuvasz.models.CertificateInfo +import com.kuvaszuptime.kuvasz.models.SSLValidationError +import com.kuvaszuptime.kuvasz.util.toOffsetDateTime +import java.net.URL +import java.security.cert.Certificate +import java.security.cert.X509Certificate +import javax.inject.Singleton +import javax.net.ssl.HttpsURLConnection + +@Singleton +class SSLValidator { + + @Suppress("TooGenericExceptionCaught") + fun validate(url: URL): Either { + return try { + val conn = url.openConnection() as HttpsURLConnection + conn.connect() + + getCertificateForHost(url, conn.serverCertificates) + .map { cert -> + CertificateInfo(validTo = cert.notAfter.toOffsetDateTime()) + }.toEither { SSLValidationError("There were no matching CN for the given host") } + } catch (e: Throwable) { + Either.left(SSLValidationError(e.message)) + } + } + + private fun getCertificateForHost(url: URL, certs: Array): Option { + certs + .filterIsInstance() + .forEach { cert -> + if (cert.cnMatchesWithHost(url)) return Option.just(cert) + } + + return Option.empty() + } + + private fun X509Certificate.cnMatchesWithHost(url: URL): Boolean { + val cn = subjectDN.name.split(",").first().trimEnd().removePrefix("CN=") + + return if (cn.startsWith("*.")) { + val cnWithoutWildcard = cn.removePrefix("*.") + val subdomain = url.host.removeSuffix(cnWithoutWildcard) + val subdomainPattern = Regex("^(([A-Za-z0-9](?:[A-Za-z0-9\\-]{0,61}[A-Za-z0-9])?\\.)|(\\S{0}))\$") + + subdomain.matches(subdomainPattern) + } else cn == url.host + } +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SlackWebhookService.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SlackWebhookService.kt index 9bcc3c4..69463f8 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SlackWebhookService.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SlackWebhookService.kt @@ -1,7 +1,7 @@ package com.kuvaszuptime.kuvasz.services import com.kuvaszuptime.kuvasz.config.handlers.SlackEventHandlerConfig -import com.kuvaszuptime.kuvasz.models.SlackWebhookMessage +import com.kuvaszuptime.kuvasz.models.handlers.SlackWebhookMessage import io.micronaut.context.annotation.Requires import io.micronaut.context.event.ShutdownEvent import io.micronaut.core.type.Argument @@ -18,13 +18,14 @@ import javax.inject.Singleton class SlackWebhookService @Inject constructor( private val slackEventHandlerConfig: SlackEventHandlerConfig, private val httpClient: RxHttpClient -) { +) : TextMessageService { companion object { private const val RETRY_COUNT = 3L } - fun sendMessage(message: SlackWebhookMessage): Flowable> { + override fun sendMessage(content: String): Flowable> { + val message = SlackWebhookMessage(text = content) val request: HttpRequest = HttpRequest.POST(slackEventHandlerConfig.webhookUrl, message) return httpClient diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TelegramAPIService.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TelegramAPIService.kt index 3f7f2a9..ab259da 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TelegramAPIService.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TelegramAPIService.kt @@ -1,7 +1,7 @@ package com.kuvaszuptime.kuvasz.services import com.kuvaszuptime.kuvasz.config.handlers.TelegramEventHandlerConfig -import com.kuvaszuptime.kuvasz.models.TelegramAPIMessage +import com.kuvaszuptime.kuvasz.models.handlers.TelegramAPIMessage import io.micronaut.context.annotation.Requires import io.micronaut.context.event.ShutdownEvent import io.micronaut.core.type.Argument @@ -16,16 +16,17 @@ import javax.inject.Singleton @Singleton @Requires(property = "handler-config.telegram-event-handler.enabled", value = "true") class TelegramAPIService @Inject constructor( - telegramEventHandlerConfig: TelegramEventHandlerConfig, + private val telegramEventHandlerConfig: TelegramEventHandlerConfig, private val httpClient: RxHttpClient -) { - private val url = "https://api.telegram.org/bot" + telegramEventHandlerConfig.token + "/sendMessage" +) : TextMessageService { companion object { - private const val RETRY_COUNT = 3L + internal const val RETRY_COUNT = 3L } - fun sendMessage(message: TelegramAPIMessage): Flowable> { + override fun sendMessage(content: String): Flowable> { + val message = TelegramAPIMessage(chat_id = telegramEventHandlerConfig.chatId, text = content) + val url = "https://api.telegram.org/bot" + telegramEventHandlerConfig.token + "/sendMessage" val request: HttpRequest = HttpRequest.POST(url, message) return httpClient diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TextMessageService.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TextMessageService.kt new file mode 100644 index 0000000..32e796c --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TextMessageService.kt @@ -0,0 +1,8 @@ +package com.kuvaszuptime.kuvasz.services + +import io.micronaut.http.HttpResponse +import io.reactivex.Flowable + +interface TextMessageService { + fun sendMessage(content: String): Flowable> +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/UptimeChecker.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/UptimeChecker.kt index 8247adb..6432ab1 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/UptimeChecker.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/UptimeChecker.kt @@ -1,8 +1,8 @@ package com.kuvaszuptime.kuvasz.services -import com.kuvaszuptime.kuvasz.models.MonitorDownEvent -import com.kuvaszuptime.kuvasz.models.MonitorUpEvent -import com.kuvaszuptime.kuvasz.models.RedirectEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.RedirectEvent import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo import com.kuvaszuptime.kuvasz.util.RawHttpResponse diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/util/Date+.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/util/Date+.kt index 757b017..64eca6e 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/util/Date+.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/util/Date+.kt @@ -3,14 +3,25 @@ package com.kuvaszuptime.kuvasz.util import arrow.core.Option import java.time.OffsetDateTime import java.time.ZoneId +import java.time.ZoneOffset +import java.util.Date import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration fun getCurrentTimestamp(): OffsetDateTime = OffsetDateTime.now(ZoneId.of("UTC")) fun Option.toDurationString(): Option = map { duration -> - duration.toComponents { days, hours, minutes, seconds, _ -> - "$days day(s), $hours hour(s), $minutes minute(s), $seconds second(s)" - } + duration.toDurationString() +} + +fun Duration.toDurationString(): String = toComponents { days, hours, minutes, seconds, _ -> + "$days day(s), $hours hour(s), $minutes minute(s), $seconds second(s)" } fun Int.toDurationOfSeconds(): java.time.Duration = java.time.Duration.ofSeconds(toLong()) + +fun Date.toOffsetDateTime(): OffsetDateTime = toInstant().atOffset(ZoneOffset.UTC) + +fun OffsetDateTime.diffToDuration(endDateTime: OffsetDateTime): Duration = + (endDateTime.toEpochSecond() - this.toEpochSecond()).toDuration(DurationUnit.SECONDS) diff --git a/src/main/resources/db/migration/V5__Add_ssl_monitoring_related_objects.sql b/src/main/resources/db/migration/V5__Add_ssl_monitoring_related_objects.sql new file mode 100644 index 0000000..137025a --- /dev/null +++ b/src/main/resources/db/migration/V5__Add_ssl_monitoring_related_objects.sql @@ -0,0 +1,23 @@ +CREATE TYPE ssl_status AS ENUM ('VALID', 'INVALID', 'WILL_EXPIRE'); + +CREATE TABLE ssl_event +( + id SERIAL PRIMARY KEY, + monitor_id INTEGER NOT NULL REFERENCES monitor (id) ON DELETE CASCADE, + status ssl_status NOT NULL, + error TEXT, + started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + ended_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL, + CONSTRAINT "ssl_event_key" UNIQUE ("monitor_id", "status", "ended_at") +); + +COMMENT ON COLUMN ssl_event.status IS 'Status of the event'; +COMMENT ON COLUMN ssl_event.started_at IS 'The current event started at'; +COMMENT ON COLUMN ssl_event.ended_at IS 'The current event ended at'; + +CREATE INDEX IF NOT EXISTS "ssl_event_monitor_idx" ON "ssl_event" USING btree ("monitor_id" ASC NULLS LAST); +CREATE INDEX IF NOT EXISTS "ssl_event_ended_at_idx" ON "ssl_event" USING btree ("ended_at" ASC NULLS LAST); + +ALTER TABLE monitor + ADD COLUMN ssl_check_enabled BOOLEAN NOT NULL DEFAULT FAlSE; diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/InfoEndpointTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/InfoEndpointTest.kt index d7c6fff..58275e5 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/InfoEndpointTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/InfoEndpointTest.kt @@ -18,6 +18,7 @@ class InfoEndpointTest( response shouldContain "log-event-handler.enabled" response shouldContain "smtp-event-handler.enabled" response shouldContain "slack-event-handler.enabled" + response shouldContain "telegram-event-handler.enabled" } } } diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorControllerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorControllerTest.kt index 9b84e7a..2ec2074 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorControllerTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorControllerTest.kt @@ -3,13 +3,17 @@ package com.kuvaszuptime.kuvasz.controllers import arrow.core.Option import arrow.core.toOption import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec +import com.kuvaszuptime.kuvasz.enums.SslStatus import com.kuvaszuptime.kuvasz.enums.UptimeStatus import com.kuvaszuptime.kuvasz.mocks.createMonitor +import com.kuvaszuptime.kuvasz.mocks.createSSLEventRecord import com.kuvaszuptime.kuvasz.mocks.createUptimeEventRecord +import com.kuvaszuptime.kuvasz.models.CheckType import com.kuvaszuptime.kuvasz.models.dto.MonitorCreateDto import com.kuvaszuptime.kuvasz.models.dto.MonitorUpdateDto import com.kuvaszuptime.kuvasz.repositories.LatencyLogRepository import com.kuvaszuptime.kuvasz.repositories.MonitorRepository +import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository import com.kuvaszuptime.kuvasz.services.CheckScheduler import com.kuvaszuptime.kuvasz.testutils.shouldBe @@ -20,6 +24,7 @@ import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult import io.kotest.inspectors.forNone import io.kotest.inspectors.forOne +import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @@ -31,6 +36,7 @@ import io.micronaut.http.client.annotation.Client import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.test.annotation.MicronautTest +@Suppress("LongParameterList") @MicronautTest class MonitorControllerTest( @Client("/") private val client: RxHttpClient, @@ -38,6 +44,7 @@ class MonitorControllerTest( private val monitorRepository: MonitorRepository, private val latencyLogRepository: LatencyLogRepository, private val uptimeEventRepository: UptimeEventRepository, + private val sslEventRepository: SSLEventRepository, private val checkScheduler: CheckScheduler ) : DatabaseBehaviorSpec() { @@ -56,6 +63,12 @@ class MonitorControllerTest( status = UptimeStatus.UP, endedAt = null ) + createSSLEventRecord( + repository = sslEventRepository, + monitorId = monitor.id, + startedAt = now, + endedAt = null + ) val response = monitorClient.getMonitorsWithDetails(enabledOnly = null) then("it should return them") { @@ -65,12 +78,19 @@ class MonitorControllerTest( responseItem.name shouldBe monitor.name responseItem.url.toString() shouldBe monitor.url responseItem.enabled shouldBe monitor.enabled + responseItem.enabled shouldBe monitor.sslCheckEnabled responseItem.averageLatencyInMs shouldBe 800 responseItem.p95LatencyInMs shouldBe 1200 responseItem.p99LatencyInMs shouldBe 1200 responseItem.uptimeStatus shouldBe UptimeStatus.UP + responseItem.uptimeStatusStartedAt shouldBe now + responseItem.uptimeError shouldBe null responseItem.lastUptimeCheck shouldBe now responseItem.createdAt shouldBe monitor.createdAt + responseItem.sslStatus shouldBe SslStatus.VALID + responseItem.sslStatusStartedAt shouldBe now + responseItem.lastSSLCheck shouldBe now + responseItem.sslError shouldBe null } } @@ -85,8 +105,10 @@ class MonitorControllerTest( responseItem.name shouldBe enabledMonitor.name responseItem.url.toString() shouldBe enabledMonitor.url responseItem.enabled shouldBe enabledMonitor.enabled + responseItem.sslCheckEnabled shouldBe enabledMonitor.sslCheckEnabled responseItem.averageLatencyInMs shouldBe null responseItem.uptimeStatus shouldBe null + responseItem.sslStatus shouldBe null responseItem.createdAt shouldBe enabledMonitor.createdAt } } @@ -113,16 +135,28 @@ class MonitorControllerTest( status = UptimeStatus.UP, endedAt = null ) + createSSLEventRecord( + repository = sslEventRepository, + monitorId = monitor.id, + startedAt = now, + endedAt = null + ) + then("it should return it") { val response = monitorClient.getMonitorDetails(monitorId = monitor.id) response.id shouldBe monitor.id response.name shouldBe monitor.name response.url.toString() shouldBe monitor.url response.enabled shouldBe monitor.enabled + response.sslCheckEnabled shouldBe monitor.sslCheckEnabled response.averageLatencyInMs shouldBe 800 response.uptimeStatus shouldBe UptimeStatus.UP response.createdAt shouldBe monitor.createdAt response.lastUptimeCheck shouldBe now + response.sslStatus shouldBe SslStatus.VALID + response.sslStatusStartedAt shouldBe now + response.lastSSLCheck shouldBe now + response.sslError shouldBe null } } @@ -153,7 +187,8 @@ class MonitorControllerTest( monitorInDb.uptimeCheckInterval shouldBe createdMonitor.uptimeCheckInterval monitorInDb.enabled shouldBe createdMonitor.enabled monitorInDb.createdAt shouldBe createdMonitor.createdAt - checkScheduler.getScheduledChecks().forOne { it.monitorId shouldBe createdMonitor.id } + checkScheduler.getScheduledChecks().filter { it.monitorId == createdMonitor.id } + .forOne { it.checkType shouldBe CheckType.UPTIME } } } @@ -180,7 +215,8 @@ class MonitorControllerTest( secondResponse.status shouldBe HttpStatus.CONFLICT val monitorsInDb = monitorRepository.fetchByName(firstCreatedMonitor.name) monitorsInDb shouldHaveSize 1 - checkScheduler.getScheduledChecks().forOne { it.monitorId shouldBe firstCreatedMonitor.id } + checkScheduler.getScheduledChecks().filter { it.monitorId == firstCreatedMonitor.id } + .forOne { it.checkType shouldBe CheckType.UPTIME } } } @@ -263,29 +299,35 @@ class MonitorControllerTest( name = "test_monitor", url = "https://valid-url.com", uptimeCheckInterval = 6000, - enabled = true + enabled = true, + sslCheckEnabled = true ) val createdMonitor = monitorClient.createMonitor(createDto) - checkScheduler.getScheduledChecks().forOne { it.monitorId shouldBe createdMonitor.id } + val checks = checkScheduler.getScheduledChecks().filter { it.monitorId == createdMonitor.id } + checks.forOne { it.checkType shouldBe CheckType.UPTIME } + checks.forOne { it.checkType shouldBe CheckType.SSL } val updateDto = MonitorUpdateDto( name = "updated_test_monitor", url = "https://updated-url.com", uptimeCheckInterval = 5000, - enabled = false + enabled = false, + sslCheckEnabled = false ) - val updatedMonitor = monitorClient.updateMonitor(createdMonitor.id, updateDto) + monitorClient.updateMonitor(createdMonitor.id, updateDto) val monitorInDb = monitorRepository.findById(createdMonitor.id)!! then("it should update the monitor and remove the checks of it") { - monitorInDb.name shouldBe updatedMonitor.name - monitorInDb.url shouldBe updatedMonitor.url - monitorInDb.uptimeCheckInterval shouldBe updatedMonitor.uptimeCheckInterval - monitorInDb.enabled shouldBe updatedMonitor.enabled + monitorInDb.name shouldBe updateDto.name + monitorInDb.url shouldBe updateDto.url + monitorInDb.uptimeCheckInterval shouldBe updateDto.uptimeCheckInterval + monitorInDb.enabled shouldBe updateDto.enabled + monitorInDb.sslCheckEnabled shouldBe updateDto.sslCheckEnabled monitorInDb.createdAt shouldBe createdMonitor.createdAt monitorInDb.updatedAt shouldNotBe null - checkScheduler.getScheduledChecks().forNone { it.monitorId shouldBe createdMonitor.id } + val updatedChecks = checkScheduler.getScheduledChecks().filter { it.monitorId == createdMonitor.id } + updatedChecks.shouldBeEmpty() } } @@ -303,20 +345,24 @@ class MonitorControllerTest( name = null, url = null, uptimeCheckInterval = null, - enabled = true + enabled = true, + sslCheckEnabled = true ) - val updatedMonitor = monitorClient.updateMonitor(createdMonitor.id, updateDto) + monitorClient.updateMonitor(createdMonitor.id, updateDto) val monitorInDb = monitorRepository.findById(createdMonitor.id)!! then("it should update the monitor and create the checks of it") { monitorInDb.name shouldBe createdMonitor.name monitorInDb.url shouldBe createdMonitor.url monitorInDb.uptimeCheckInterval shouldBe createdMonitor.uptimeCheckInterval - monitorInDb.enabled shouldBe updatedMonitor.enabled + monitorInDb.enabled shouldBe updateDto.enabled + monitorInDb.sslCheckEnabled shouldBe updateDto.sslCheckEnabled monitorInDb.createdAt shouldBe createdMonitor.createdAt monitorInDb.updatedAt shouldNotBe null - checkScheduler.getScheduledChecks().forOne { it.monitorId shouldBe createdMonitor.id } + val checks = checkScheduler.getScheduledChecks().filter { it.monitorId == createdMonitor.id } + checks.forOne { it.checkType shouldBe CheckType.UPTIME } + checks.forOne { it.checkType shouldBe CheckType.SSL } } } @@ -338,7 +384,8 @@ class MonitorControllerTest( name = secondCreatedMonitor.name, url = null, uptimeCheckInterval = null, - enabled = true + enabled = null, + sslCheckEnabled = null ) val updateRequest = HttpRequest.PATCH("/monitors/${firstCreatedMonitor.id}", updateDto) @@ -354,7 +401,7 @@ class MonitorControllerTest( } `when`("it is called with a non existing monitor ID") { - val updateDto = MonitorUpdateDto(null, null, null, null) + val updateDto = MonitorUpdateDto(null, null, null, null, null) val updateRequest = HttpRequest.PATCH("/monitors/123232", updateDto) val response = shouldThrow { client.toBlocking().exchange(updateRequest) diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/events/EventTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/events/EventTest.kt index 4228f16..be12699 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/events/EventTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/events/EventTest.kt @@ -2,9 +2,7 @@ package com.kuvaszuptime.kuvasz.events import arrow.core.Option import com.kuvaszuptime.kuvasz.enums.UptimeStatus -import com.kuvaszuptime.kuvasz.models.MonitorUpEvent -import com.kuvaszuptime.kuvasz.models.getEndedEventDuration -import com.kuvaszuptime.kuvasz.models.runWhenStateChanges +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent import com.kuvaszuptime.kuvasz.tables.pojos.UptimeEventPojo import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandlerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandlerTest.kt index b79703d..22d5b1b 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandlerTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandlerTest.kt @@ -2,15 +2,23 @@ package com.kuvaszuptime.kuvasz.handlers import arrow.core.Option import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec +import com.kuvaszuptime.kuvasz.enums.SslStatus import com.kuvaszuptime.kuvasz.enums.UptimeStatus import com.kuvaszuptime.kuvasz.mocks.createMonitor -import com.kuvaszuptime.kuvasz.models.MonitorDownEvent -import com.kuvaszuptime.kuvasz.models.MonitorUpEvent +import com.kuvaszuptime.kuvasz.mocks.generateCertificateInfo +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.SSLValidationError +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent import com.kuvaszuptime.kuvasz.repositories.LatencyLogRepository import com.kuvaszuptime.kuvasz.repositories.MonitorRepository +import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository import com.kuvaszuptime.kuvasz.services.EventDispatcher import com.kuvaszuptime.kuvasz.tables.LatencyLog.LATENCY_LOG +import com.kuvaszuptime.kuvasz.tables.SslEvent.SSL_EVENT import com.kuvaszuptime.kuvasz.tables.UptimeEvent.UPTIME_EVENT import com.kuvaszuptime.kuvasz.testutils.shouldBe import io.kotest.core.test.TestCase @@ -28,15 +36,17 @@ import io.mockk.verifyOrder class DatabaseEventHandlerTest( uptimeEventRepository: UptimeEventRepository, latencyLogRepository: LatencyLogRepository, - monitorRepository: MonitorRepository + monitorRepository: MonitorRepository, + sslEventRepository: SSLEventRepository ) : DatabaseBehaviorSpec() { init { val eventDispatcher = EventDispatcher() - val uptimeEventRepositorySpy = spyk(uptimeEventRepository, recordPrivateCalls = true) - val latencyLogRepositorySpy = spyk(latencyLogRepository, recordPrivateCalls = true) - DatabaseEventHandler(eventDispatcher, uptimeEventRepositorySpy, latencyLogRepositorySpy) + val uptimeEventRepositorySpy = spyk(uptimeEventRepository) + val latencyLogRepositorySpy = spyk(latencyLogRepository) + val sslEventRepositorySpy = spyk(sslEventRepository) + DatabaseEventHandler(eventDispatcher, uptimeEventRepositorySpy, latencyLogRepositorySpy, sslEventRepositorySpy) - given("the DatabaseEventHandler") { + given("the DatabaseEventHandler - UPTIME events") { `when`("it receives a MonitorUpEvent and there is no previous event for the monitor") { val monitor = createMonitor(monitorRepository) val event = MonitorUpEvent( @@ -155,8 +165,8 @@ class DatabaseEventHandlerTest( verifyOrder { uptimeEventRepositorySpy.insertFromMonitorEvent(firstEvent) - uptimeEventRepository.endEventById(firstUptimeRecord.id, secondEvent.dispatchedAt) latencyLogRepositorySpy.insertLatencyForMonitor(monitor.id, secondEvent.latency) + uptimeEventRepositorySpy.endEventById(firstUptimeRecord.id, secondEvent.dispatchedAt) uptimeEventRepositorySpy.insertFromMonitorEvent(secondEvent) } @@ -196,7 +206,7 @@ class DatabaseEventHandlerTest( verifyOrder { latencyLogRepositorySpy.insertLatencyForMonitor(monitor.id, firstEvent.latency) uptimeEventRepositorySpy.insertFromMonitorEvent(firstEvent) - uptimeEventRepository.endEventById(firstUptimeRecord.id, secondEvent.dispatchedAt) + uptimeEventRepositorySpy.endEventById(firstUptimeRecord.id, secondEvent.dispatchedAt) uptimeEventRepositorySpy.insertFromMonitorEvent(secondEvent) } @@ -210,6 +220,238 @@ class DatabaseEventHandlerTest( } } } + + given("the DatabaseEventHandler - SSL events") { + `when`("it receives an SSLValidEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository) + val event = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + eventDispatcher.dispatch(event) + + then("it should insert a new SSLEvent record with status VALID") { + val expectedSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, event.monitor.id) + + verify(exactly = 1) { sslEventRepositorySpy.insertFromMonitorEvent(event) } + verify(exactly = 0) { sslEventRepositorySpy.endEventById(any(), any()) } + + expectedSSLRecord.status shouldBe SslStatus.VALID + expectedSSLRecord.startedAt shouldBe event.dispatchedAt + expectedSSLRecord.endedAt shouldBe null + expectedSSLRecord.updatedAt shouldBe event.dispatchedAt + } + } + + `when`("it receives an SSLInvalidEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository) + val event = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.empty(), + error = SSLValidationError("ssl error") + ) + eventDispatcher.dispatch(event) + + then("it should insert a new SSLEvent record with status INVALID") { + val expectedSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, event.monitor.id) + + verify(exactly = 1) { sslEventRepositorySpy.insertFromMonitorEvent(event) } + verify(exactly = 0) { sslEventRepositorySpy.endEventById(any(), any()) } + + expectedSSLRecord.status shouldBe SslStatus.INVALID + expectedSSLRecord.startedAt shouldBe event.dispatchedAt + expectedSSLRecord.endedAt shouldBe null + expectedSSLRecord.updatedAt shouldBe event.dispatchedAt + expectedSSLRecord.error shouldBe "ssl error" + } + } + + `when`("it receives an SSLValidEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + then("it should not insert a new SSLEvent record") { + val expectedSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + verify(exactly = 1) { sslEventRepositorySpy.insertFromMonitorEvent(firstEvent) } + verify(exactly = 0) { sslEventRepositorySpy.endEventById(any(), any()) } + + expectedSSLRecord.status shouldBe SslStatus.VALID + expectedSSLRecord.endedAt shouldBe null + expectedSSLRecord.updatedAt shouldBe secondEvent.dispatchedAt + } + } + + `when`("it receives an SSLValidEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.empty(), + error = SSLValidationError("ssl error") + ) + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + then("it should create a new SSLEvent record, and end the previous one") { + val sslRecords = sslEventRepository.fetchByMonitorId(monitor.id).sortedBy { it.startedAt } + + verifyOrder { + sslEventRepositorySpy.insertFromMonitorEvent(firstEvent) + sslEventRepositorySpy.endEventById(firstSSLRecord.id, secondEvent.dispatchedAt) + sslEventRepositorySpy.insertFromMonitorEvent(secondEvent) + } + + sslRecords[0].status shouldBe SslStatus.INVALID + sslRecords[0].endedAt shouldBe secondEvent.dispatchedAt + sslRecords[0].updatedAt shouldBe secondEvent.dispatchedAt + sslRecords[1].status shouldBe SslStatus.VALID + sslRecords[1].endedAt shouldBe null + sslRecords[1].updatedAt shouldBe secondEvent.dispatchedAt + } + } + + `when`("it receives an SSLInvalidEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.just(firstSSLRecord), + error = SSLValidationError("ssl error") + ) + eventDispatcher.dispatch(secondEvent) + + then("it should create a new SSLEvent record and end the previous one") { + val sslRecords = sslEventRepository.fetchByMonitorId(monitor.id).sortedBy { it.startedAt } + + verifyOrder { + sslEventRepositorySpy.insertFromMonitorEvent(firstEvent) + sslEventRepositorySpy.endEventById(firstSSLRecord.id, secondEvent.dispatchedAt) + sslEventRepositorySpy.insertFromMonitorEvent(secondEvent) + } + + sslRecords[0].status shouldBe SslStatus.VALID + sslRecords[0].endedAt shouldBe secondEvent.dispatchedAt + sslRecords[0].updatedAt shouldBe secondEvent.dispatchedAt + sslRecords[1].status shouldBe SslStatus.INVALID + sslRecords[1].endedAt shouldBe null + sslRecords[1].updatedAt shouldBe secondEvent.dispatchedAt + } + } + + `when`("it receives an SSLWillExpireEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository) + val event = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + eventDispatcher.dispatch(event) + + then("it should insert a new SSLEvent record with status WILL_EXPIRE") { + val expectedSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, event.monitor.id) + + verify(exactly = 1) { sslEventRepositorySpy.insertFromMonitorEvent(event) } + verify(exactly = 0) { sslEventRepositorySpy.endEventById(any(), any()) } + + expectedSSLRecord.status shouldBe SslStatus.WILL_EXPIRE + expectedSSLRecord.startedAt shouldBe event.dispatchedAt + expectedSSLRecord.endedAt shouldBe null + expectedSSLRecord.updatedAt shouldBe event.dispatchedAt + } + } + + `when`("it receives an SSLWillExpireEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + then("it should not insert a new SSLEvent record") { + val expectedSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + verify(exactly = 1) { sslEventRepositorySpy.insertFromMonitorEvent(firstEvent) } + verify(exactly = 0) { sslEventRepositorySpy.endEventById(any(), any()) } + + expectedSSLRecord.status shouldBe SslStatus.WILL_EXPIRE + expectedSSLRecord.endedAt shouldBe null + expectedSSLRecord.updatedAt shouldBe secondEvent.dispatchedAt + } + } + + `when`("it receives an SSLWillExpireEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + then("it should create a new SSLEvent record, and end the previous one") { + val sslRecords = sslEventRepository.fetchByMonitorId(monitor.id).sortedBy { it.startedAt } + + verifyOrder { + sslEventRepositorySpy.insertFromMonitorEvent(firstEvent) + sslEventRepositorySpy.endEventById(firstSSLRecord.id, secondEvent.dispatchedAt) + sslEventRepositorySpy.insertFromMonitorEvent(secondEvent) + } + + sslRecords[0].status shouldBe SslStatus.VALID + sslRecords[0].endedAt shouldBe secondEvent.dispatchedAt + sslRecords[0].updatedAt shouldBe secondEvent.dispatchedAt + sslRecords[1].status shouldBe SslStatus.WILL_EXPIRE + sslRecords[1].endedAt shouldBe null + sslRecords[1].updatedAt shouldBe secondEvent.dispatchedAt + } + } + } } override fun afterTest(testCase: TestCase, result: TestResult) { diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandlerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandlerTest.kt index 72fccb7..31df845 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandlerTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandlerTest.kt @@ -5,17 +5,25 @@ import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec import com.kuvaszuptime.kuvasz.config.handlers.SMTPEventHandlerConfig import com.kuvaszuptime.kuvasz.factories.EmailFactory import com.kuvaszuptime.kuvasz.mocks.createMonitor -import com.kuvaszuptime.kuvasz.models.MonitorDownEvent -import com.kuvaszuptime.kuvasz.models.MonitorUpEvent +import com.kuvaszuptime.kuvasz.mocks.generateCertificateInfo +import com.kuvaszuptime.kuvasz.models.SSLValidationError +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent import com.kuvaszuptime.kuvasz.repositories.MonitorRepository +import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository import com.kuvaszuptime.kuvasz.services.EventDispatcher import com.kuvaszuptime.kuvasz.services.SMTPMailer +import com.kuvaszuptime.kuvasz.tables.SslEvent import com.kuvaszuptime.kuvasz.tables.UptimeEvent.UPTIME_EVENT import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain import io.micronaut.context.annotation.Property import io.micronaut.http.HttpStatus import io.micronaut.test.annotation.MicronautTest @@ -24,6 +32,7 @@ import io.mockk.slot import io.mockk.spyk import io.mockk.verify import org.simplejavamail.api.email.Email +import java.time.OffsetDateTime @MicronautTest @Property(name = "handler-config.smtp-event-handler.enabled", value = "true") @@ -31,6 +40,7 @@ class SMTPEventHandlerTest( private val eventDispatcher: EventDispatcher, private val monitorRepository: MonitorRepository, private val uptimeEventRepository: UptimeEventRepository, + private val sslEventRepository: SSLEventRepository, smtpEventHandlerConfig: SMTPEventHandlerConfig, smtpMailer: SMTPMailer @@ -40,7 +50,7 @@ class SMTPEventHandlerTest( val mailerSpy = spyk(smtpMailer, recordPrivateCalls = true) SMTPEventHandler(smtpEventHandlerConfig, mailerSpy, eventDispatcher) - given("the SMTPEventHandler") { + given("the SMTPEventHandler - UPTIME events") { `when`("it receives a MonitorUpEvent and there is no previous event for the monitor") { val monitor = createMonitor(monitorRepository) val event = MonitorUpEvent( @@ -49,7 +59,7 @@ class SMTPEventHandlerTest( latency = 1000, previousEvent = Option.empty() ) - val expectedEmail = emailFactory.fromUptimeMonitorEvent(event) + val expectedEmail = emailFactory.fromMonitorEvent(event) eventDispatcher.dispatch(event) @@ -71,7 +81,7 @@ class SMTPEventHandlerTest( error = Throwable(), previousEvent = Option.empty() ) - val expectedEmail = emailFactory.fromUptimeMonitorEvent(event) + val expectedEmail = emailFactory.fromMonitorEvent(event) eventDispatcher.dispatch(event) @@ -95,7 +105,7 @@ class SMTPEventHandlerTest( ) eventDispatcher.dispatch(firstEvent) val firstUptimeRecord = uptimeEventRepository.fetchOne(UPTIME_EVENT.MONITOR_ID, monitor.id) - val expectedEmail = emailFactory.fromUptimeMonitorEvent(firstEvent) + val expectedEmail = emailFactory.fromMonitorEvent(firstEvent) val secondEvent = MonitorUpEvent( monitor = monitor, @@ -126,7 +136,7 @@ class SMTPEventHandlerTest( ) eventDispatcher.dispatch(firstEvent) val firstUptimeRecord = uptimeEventRepository.fetchOne(UPTIME_EVENT.MONITOR_ID, monitor.id) - val expectedEmail = emailFactory.fromUptimeMonitorEvent(firstEvent) + val expectedEmail = emailFactory.fromMonitorEvent(firstEvent) val secondEvent = MonitorDownEvent( monitor = monitor, @@ -166,8 +176,8 @@ class SMTPEventHandlerTest( ) eventDispatcher.dispatch(secondEvent) - val firstExpectedEmail = emailFactory.fromUptimeMonitorEvent(firstEvent) - val secondExpectedEmail = emailFactory.fromUptimeMonitorEvent(secondEvent) + val firstExpectedEmail = emailFactory.fromMonitorEvent(firstEvent) + val secondExpectedEmail = emailFactory.fromMonitorEvent(secondEvent) then("it should send two different emails about them") { val emailsSent = mutableListOf() @@ -202,8 +212,8 @@ class SMTPEventHandlerTest( ) eventDispatcher.dispatch(secondEvent) - val firstExpectedEmail = emailFactory.fromUptimeMonitorEvent(firstEvent) - val secondExpectedEmail = emailFactory.fromUptimeMonitorEvent(secondEvent) + val firstExpectedEmail = emailFactory.fromMonitorEvent(firstEvent) + val secondExpectedEmail = emailFactory.fromMonitorEvent(secondEvent) then("it should send two different emails about them") { val emailsSent = mutableListOf() @@ -219,6 +229,259 @@ class SMTPEventHandlerTest( } } } + + given("the SMTPEventHandler - SSL events") { + `when`("it receives an SSLValidEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository) + val event = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + val expectedEmail = emailFactory.fromMonitorEvent(event) + + eventDispatcher.dispatch(event) + + then("it should send an email about the event") { + val slot = slot() + + verify(exactly = 1) { mailerSpy.sendAsync(capture(slot)) } + slot.captured.plainText shouldBe expectedEmail.plainText + slot.captured.subject shouldContain "has a VALID" + slot.captured.subject shouldBe expectedEmail.subject + } + } + + `when`("it receives an SSLInvalidEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository) + val event = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.empty(), + error = SSLValidationError("ssl error") + ) + val expectedEmail = emailFactory.fromMonitorEvent(event) + + eventDispatcher.dispatch(event) + + then("it should send an email about the event") { + val slot = slot() + + verify(exactly = 1) { mailerSpy.sendAsync(capture(slot)) } + slot.captured.plainText shouldBe expectedEmail.plainText + slot.captured.subject shouldContain "has an INVALID" + slot.captured.subject shouldBe expectedEmail.subject + } + } + + `when`("it receives an SSLValidEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SslEvent.SSL_EVENT.MONITOR_ID, monitor.id) + val expectedEmail = emailFactory.fromMonitorEvent(firstEvent) + + val secondEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(validTo = OffsetDateTime.MAX), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + then("it should send only one email about them") { + val slot = slot() + + verify(exactly = 1) { mailerSpy.sendAsync(capture(slot)) } + slot.captured.plainText shouldBe expectedEmail.plainText + slot.captured.plainText shouldNotContain OffsetDateTime.MAX.toString() + slot.captured.subject shouldContain "has a VALID" + slot.captured.subject shouldBe expectedEmail.subject + } + } + + `when`("it receives an SSLInvalidEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.empty(), + error = SSLValidationError("ssl error1") + ) + eventDispatcher.dispatch(firstEvent) + + val expectedEmail = emailFactory.fromMonitorEvent(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SslEvent.SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.just(firstSSLRecord), + error = SSLValidationError("ssl error2") + ) + eventDispatcher.dispatch(secondEvent) + + then("it should send only one email about them") { + val slot = slot() + + verify(exactly = 1) { mailerSpy.sendAsync(capture(slot)) } + slot.captured.plainText shouldContain "ssl error1" + slot.captured.plainText shouldBe expectedEmail.plainText + slot.captured.subject shouldContain "has an INVALID" + slot.captured.subject shouldBe expectedEmail.subject + } + } + + `when`("it receives an SSLValidEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.empty(), + error = SSLValidationError("ssl error1") + ) + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SslEvent.SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + val firstExpectedEmail = emailFactory.fromMonitorEvent(firstEvent) + val secondExpectedEmail = emailFactory.fromMonitorEvent(secondEvent) + + then("it should send two different emails about them") { + val emailsSent = mutableListOf() + + verify(exactly = 2) { mailerSpy.sendAsync(capture(emailsSent)) } + emailsSent[0].plainText shouldBe firstExpectedEmail.plainText + emailsSent[0].subject shouldContain "has an INVALID" + emailsSent[0].subject shouldBe firstExpectedEmail.subject + emailsSent[1].plainText shouldBe secondExpectedEmail.plainText + emailsSent[1].subject shouldContain "has a VALID" + emailsSent[1].subject shouldBe secondExpectedEmail.subject + } + } + + `when`("it receives an SSLInvalidEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SslEvent.SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.just(firstSSLRecord), + error = SSLValidationError("ssl error") + ) + eventDispatcher.dispatch(secondEvent) + + val firstExpectedEmail = emailFactory.fromMonitorEvent(firstEvent) + val secondExpectedEmail = emailFactory.fromMonitorEvent(secondEvent) + + then("it should send two different emails about them") { + val emailsSent = mutableListOf() + + verify(exactly = 2) { mailerSpy.sendAsync(capture(emailsSent)) } + emailsSent[0].plainText shouldBe firstExpectedEmail.plainText + emailsSent[0].subject shouldContain "has a VALID" + emailsSent[0].subject shouldBe firstExpectedEmail.subject + emailsSent[1].plainText shouldBe secondExpectedEmail.plainText + emailsSent[1].subject shouldContain "has an INVALID" + emailsSent[1].subject shouldBe secondExpectedEmail.subject + } + } + + `when`("it receives an SSLWillExpireEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository) + val event = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + val expectedEmail = emailFactory.fromMonitorEvent(event) + + eventDispatcher.dispatch(event) + + then("it should send an email about the event") { + val slot = slot() + + verify(exactly = 1) { mailerSpy.sendAsync(capture(slot)) } + slot.captured.plainText shouldBe expectedEmail.plainText + slot.captured.subject shouldContain "will expire soon" + slot.captured.subject shouldBe expectedEmail.subject + } + } + + `when`("it receives an SSLWillExpireEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + eventDispatcher.dispatch(firstEvent) + + val expectedEmail = emailFactory.fromMonitorEvent(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SslEvent.SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(validTo = OffsetDateTime.MAX), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + then("it should send only one email about them") { + val slot = slot() + + verify(exactly = 1) { mailerSpy.sendAsync(capture(slot)) } + slot.captured.plainText shouldBe expectedEmail.plainText + slot.captured.plainText shouldNotContain OffsetDateTime.MAX.toString() + slot.captured.subject shouldContain "will expire soon" + slot.captured.subject shouldBe expectedEmail.subject + } + } + + `when`("it receives an SSLWillExpireEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SslEvent.SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + val firstExpectedEmail = emailFactory.fromMonitorEvent(firstEvent) + val secondExpectedEmail = emailFactory.fromMonitorEvent(secondEvent) + + then("it should send two different emails about them") { + val emailsSent = mutableListOf() + + verify(exactly = 2) { mailerSpy.sendAsync(capture(emailsSent)) } + emailsSent[0].plainText shouldBe firstExpectedEmail.plainText + emailsSent[0].subject shouldContain "has a VALID" + emailsSent[0].subject shouldBe firstExpectedEmail.subject + emailsSent[1].plainText shouldBe secondExpectedEmail.plainText + emailsSent[1].subject shouldContain "will expire soon" + emailsSent[1].subject shouldBe secondExpectedEmail.subject + } + } + } } override fun afterTest(testCase: TestCase, result: TestResult) { diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandlerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandlerTest.kt index 6f163d0..08c420f 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandlerTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandlerTest.kt @@ -4,18 +4,26 @@ import arrow.core.Option import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec import com.kuvaszuptime.kuvasz.config.handlers.SlackEventHandlerConfig import com.kuvaszuptime.kuvasz.mocks.createMonitor -import com.kuvaszuptime.kuvasz.models.MonitorDownEvent -import com.kuvaszuptime.kuvasz.models.MonitorUpEvent -import com.kuvaszuptime.kuvasz.models.SlackWebhookMessage +import com.kuvaszuptime.kuvasz.mocks.generateCertificateInfo +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.SSLValidationError +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent +import com.kuvaszuptime.kuvasz.models.handlers.SlackWebhookMessage import com.kuvaszuptime.kuvasz.repositories.MonitorRepository +import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository import com.kuvaszuptime.kuvasz.services.EventDispatcher import com.kuvaszuptime.kuvasz.services.SlackWebhookService +import com.kuvaszuptime.kuvasz.tables.SslEvent.SSL_EVENT import com.kuvaszuptime.kuvasz.tables.UptimeEvent.UPTIME_EVENT import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain import io.micronaut.core.type.Argument import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus @@ -29,13 +37,14 @@ import io.mockk.slot import io.mockk.spyk import io.mockk.verify import io.reactivex.Flowable +import java.time.OffsetDateTime @MicronautTest class SlackEventHandlerTest( private val eventDispatcher: EventDispatcher, private val monitorRepository: MonitorRepository, - private val uptimeEventRepository: UptimeEventRepository - + private val uptimeEventRepository: UptimeEventRepository, + private val sslEventRepository: SSLEventRepository ) : DatabaseBehaviorSpec() { private val mockHttpClient = mockk() @@ -45,7 +54,7 @@ class SlackEventHandlerTest( val webhookServiceSpy = spyk(slackWebhookService, recordPrivateCalls = true) SlackEventHandler(webhookServiceSpy, eventDispatcher) - given("the SlackEventHandler") { + given("the SlackEventHandler - UPTIME events") { `when`("it receives a MonitorUpEvent and there is no previous event for the monitor") { val monitor = createMonitor(monitorRepository) val event = MonitorUpEvent( @@ -54,15 +63,15 @@ class SlackEventHandlerTest( latency = 1000, previousEvent = Option.empty() ) - mockHttpResponse(HttpStatus.OK) + mockSuccessfulHttpResponse() eventDispatcher.dispatch(event) then("it should send a webhook message about the event") { - val slot = slot() + val slot = slot() verify(exactly = 1) { webhookServiceSpy.sendMessage(capture(slot)) } - slot.captured.text shouldContain "Your monitor \"testMonitor\" (http://irrelevant.com) is UP (200)" + slot.captured shouldContain "Your monitor \"${monitor.name}\" (${monitor.url}) is UP (200)" } } @@ -74,15 +83,15 @@ class SlackEventHandlerTest( error = Throwable(), previousEvent = Option.empty() ) - mockHttpResponse(HttpStatus.OK) + mockSuccessfulHttpResponse() eventDispatcher.dispatch(event) then("it should send a webhook message about the event") { - val slot = slot() + val slot = slot() verify(exactly = 1) { webhookServiceSpy.sendMessage(capture(slot)) } - slot.captured.text shouldContain "Your monitor \"testMonitor\" (http://irrelevant.com) is DOWN" + slot.captured shouldContain "Your monitor \"${monitor.name}\" (${monitor.url}) is DOWN" } } @@ -94,7 +103,7 @@ class SlackEventHandlerTest( latency = 1000, previousEvent = Option.empty() ) - mockHttpResponse(HttpStatus.OK) + mockSuccessfulHttpResponse() eventDispatcher.dispatch(firstEvent) val firstUptimeRecord = uptimeEventRepository.fetchOne(UPTIME_EVENT.MONITOR_ID, monitor.id) @@ -107,10 +116,10 @@ class SlackEventHandlerTest( eventDispatcher.dispatch(secondEvent) then("it should send only one notification about them") { - val slot = slot() + val slot = slot() verify(exactly = 1) { webhookServiceSpy.sendMessage(capture(slot)) } - slot.captured.text shouldContain "Latency: 1000ms" + slot.captured shouldContain "Latency: 1000ms" } } @@ -122,7 +131,7 @@ class SlackEventHandlerTest( error = Throwable("First error"), previousEvent = Option.empty() ) - mockHttpResponse(HttpStatus.OK) + mockSuccessfulHttpResponse() eventDispatcher.dispatch(firstEvent) val firstUptimeRecord = uptimeEventRepository.fetchOne(UPTIME_EVENT.MONITOR_ID, monitor.id) @@ -135,10 +144,10 @@ class SlackEventHandlerTest( eventDispatcher.dispatch(secondEvent) then("it should send only one notification about them") { - val slot = slot() + val slot = slot() verify(exactly = 1) { webhookServiceSpy.sendMessage(capture(slot)) } - slot.captured.text shouldContain "(500)" + slot.captured shouldContain "(500)" } } @@ -150,7 +159,7 @@ class SlackEventHandlerTest( previousEvent = Option.empty(), error = Throwable() ) - mockHttpResponse(HttpStatus.OK) + mockSuccessfulHttpResponse() eventDispatcher.dispatch(firstEvent) val firstUptimeRecord = uptimeEventRepository.fetchOne(UPTIME_EVENT.MONITOR_ID, monitor.id) @@ -163,12 +172,12 @@ class SlackEventHandlerTest( eventDispatcher.dispatch(secondEvent) then("it should send two different notifications about them") { - val notificationsSent = mutableListOf() + val notificationsSent = mutableListOf() verify(exactly = 2) { webhookServiceSpy.sendMessage(capture(notificationsSent)) } - notificationsSent[0].text shouldContain "is DOWN (500)" - notificationsSent[1].text shouldContain "Latency: 1000ms" - notificationsSent[1].text shouldContain "is UP (200)" + notificationsSent[0] shouldContain "is DOWN (500)" + notificationsSent[1] shouldContain "Latency: 1000ms" + notificationsSent[1] shouldContain "is UP (200)" } } @@ -180,7 +189,7 @@ class SlackEventHandlerTest( latency = 1000, previousEvent = Option.empty() ) - mockHttpResponse(HttpStatus.OK) + mockSuccessfulHttpResponse() eventDispatcher.dispatch(firstEvent) val firstUptimeRecord = uptimeEventRepository.fetchOne(UPTIME_EVENT.MONITOR_ID, monitor.id) @@ -193,15 +202,238 @@ class SlackEventHandlerTest( eventDispatcher.dispatch(secondEvent) then("it should send two different notifications about them") { - val notificationsSent = mutableListOf() + val notificationsSent = mutableListOf() + + verify(exactly = 2) { webhookServiceSpy.sendMessage(capture(notificationsSent)) } + notificationsSent[0] shouldContain "Latency: 1000ms" + notificationsSent[0] shouldContain "is UP (200)" + notificationsSent[1] shouldContain "is DOWN (500)" + } + } + } + + given("the SlackEventHandler - SSL events") { + `when`("it receives an SSLValidEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository) + val event = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + mockSuccessfulHttpResponse() + + eventDispatcher.dispatch(event) + + then("it should send a webhook message about the event") { + val slot = slot() + + verify(exactly = 1) { webhookServiceSpy.sendMessage(capture(slot)) } + slot.captured shouldContain + "Your site \"${monitor.name}\" (${monitor.url}) has a VALID certificate" + } + } + + `when`("it receives an SSLInvalidEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository) + val event = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.empty(), + error = SSLValidationError("ssl error") + ) + mockSuccessfulHttpResponse() + + eventDispatcher.dispatch(event) + + then("it should send a webhook message about the event") { + val slot = slot() + + verify(exactly = 1) { webhookServiceSpy.sendMessage(capture(slot)) } + slot.captured shouldContain + "Your site \"${monitor.name}\" (${monitor.url}) has an INVALID certificate" + } + } + + `when`("it receives an SSLValidEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + mockSuccessfulHttpResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(validTo = OffsetDateTime.MAX), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + then("it should send only one notification about them") { + val slot = slot() + + verify(exactly = 1) { webhookServiceSpy.sendMessage(capture(slot)) } + slot.captured shouldNotContain OffsetDateTime.MAX.toString() + } + } + + `when`("it receives an SSLInvalidEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.empty(), + error = SSLValidationError("ssl error1") + ) + mockSuccessfulHttpResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.just(firstSSLRecord), + error = SSLValidationError("ssl error2") + ) + eventDispatcher.dispatch(secondEvent) + + then("it should send only one notification about them") { + val slot = slot() + + verify(exactly = 1) { webhookServiceSpy.sendMessage(capture(slot)) } + slot.captured shouldContain "ssl error1" + } + } + + `when`("it receives an SSLValidEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.empty(), + error = SSLValidationError("ssl error1") + ) + mockSuccessfulHttpResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + then("it should send two different notifications about them") { + val notificationsSent = mutableListOf() verify(exactly = 2) { webhookServiceSpy.sendMessage(capture(notificationsSent)) } - notificationsSent[0].text shouldContain "Latency: 1000ms" - notificationsSent[0].text shouldContain "is UP (200)" - notificationsSent[1].text shouldContain "is DOWN (500)" + notificationsSent[0] shouldContain "has an INVALID certificate" + notificationsSent[1] shouldContain "has a VALID certificate" } } + `when`("it receives an SSLInvalidEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + mockSuccessfulHttpResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.just(firstSSLRecord), + error = SSLValidationError("ssl error") + ) + eventDispatcher.dispatch(secondEvent) + + then("it should send two different notifications about them") { + val notificationsSent = mutableListOf() + + verify(exactly = 2) { webhookServiceSpy.sendMessage(capture(notificationsSent)) } + notificationsSent[0] shouldContain "has a VALID certificate" + notificationsSent[1] shouldContain "has an INVALID certificate" + } + } + + `when`("it receives an SSLWillExpireEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository) + val event = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + mockSuccessfulHttpResponse() + + eventDispatcher.dispatch(event) + + then("it should send a webhook message about the event") { + val slot = slot() + + verify(exactly = 1) { webhookServiceSpy.sendMessage(capture(slot)) } + slot.captured shouldContain + "Your SSL certificate for ${monitor.url} will expire soon" + } + } + + `when`("it receives an SSLWillExpireEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + mockSuccessfulHttpResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(validTo = OffsetDateTime.MAX), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + then("it should send only one notification about them") { + val slot = slot() + + verify(exactly = 1) { webhookServiceSpy.sendMessage(capture(slot)) } + slot.captured shouldNotContain OffsetDateTime.MAX.toString() + } + } + + `when`("it receives an SSLWillExpireEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + mockSuccessfulHttpResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + then("it should send two different notifications about them") { + val notificationsSent = mutableListOf() + + verify(exactly = 2) { webhookServiceSpy.sendMessage(capture(notificationsSent)) } + notificationsSent[0] shouldContain "has a VALID certificate" + notificationsSent[1] shouldContain "Your SSL certificate for ${monitor.url} will expire soon" + } + } + } + + given("the SlackEventHandler - error handling logic") { `when`("it receives an event but an error happens when it calls the webhook") { val monitor = createMonitor(monitorRepository) val event = MonitorUpEvent( @@ -210,14 +442,14 @@ class SlackEventHandlerTest( latency = 1000, previousEvent = Option.empty() ) - mockHttpErrorResponse(HttpStatus.BAD_REQUEST, "bad_request") + mockHttpErrorResponse() then("it should send a webhook message about the event") { - val slot = slot() + val slot = slot() shouldNotThrowAny { eventDispatcher.dispatch(event) } verify(exactly = 1) { webhookServiceSpy.sendMessage(capture(slot)) } - slot.captured.text shouldContain "Your monitor \"testMonitor\" (http://irrelevant.com) is UP (200)" + slot.captured shouldContain "Your monitor \"${monitor.name}\" (${monitor.url}) is UP (200)" } } } @@ -228,27 +460,19 @@ class SlackEventHandlerTest( super.afterTest(testCase, result) } - private fun mockHttpResponse(status: HttpStatus, body: String = "") { + private fun mockSuccessfulHttpResponse() { every { - mockHttpClient.exchange( - any(), - Argument.STRING, - Argument.STRING - ) + mockHttpClient.exchange(any(), Argument.STRING, Argument.STRING) } returns Flowable.just( - HttpResponse.status(status).body(body) + HttpResponse.ok() ) } - private fun mockHttpErrorResponse(status: HttpStatus, body: String = "") { + private fun mockHttpErrorResponse() { every { - mockHttpClient.exchange( - any(), - Argument.STRING, - Argument.STRING - ) + mockHttpClient.exchange(any(), Argument.STRING, Argument.STRING) } returns Flowable.error( - HttpClientResponseException("error", HttpResponse.status(status).body(body)) + HttpClientResponseException("error", HttpResponse.badRequest("bad_request")) ) } } diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/TelegramEventHandlerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/TelegramEventHandlerTest.kt index a346c99..8a8e993 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/TelegramEventHandlerTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/TelegramEventHandlerTest.kt @@ -4,18 +4,26 @@ import arrow.core.Option import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec import com.kuvaszuptime.kuvasz.config.handlers.TelegramEventHandlerConfig import com.kuvaszuptime.kuvasz.mocks.createMonitor -import com.kuvaszuptime.kuvasz.models.MonitorDownEvent -import com.kuvaszuptime.kuvasz.models.MonitorUpEvent -import com.kuvaszuptime.kuvasz.models.TelegramAPIMessage +import com.kuvaszuptime.kuvasz.mocks.generateCertificateInfo +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.SSLValidationError +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent +import com.kuvaszuptime.kuvasz.models.handlers.SlackWebhookMessage import com.kuvaszuptime.kuvasz.repositories.MonitorRepository +import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository import com.kuvaszuptime.kuvasz.services.EventDispatcher import com.kuvaszuptime.kuvasz.services.TelegramAPIService +import com.kuvaszuptime.kuvasz.tables.SslEvent import com.kuvaszuptime.kuvasz.tables.UptimeEvent import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain import io.micronaut.core.type.Argument import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus @@ -29,12 +37,14 @@ import io.mockk.slot import io.mockk.spyk import io.mockk.verify import io.reactivex.Flowable +import java.time.OffsetDateTime @MicronautTest class TelegramEventHandlerTest( private val eventDispatcher: EventDispatcher, private val monitorRepository: MonitorRepository, - private val uptimeEventRepository: UptimeEventRepository + private val uptimeEventRepository: UptimeEventRepository, + private val sslEventRepository: SSLEventRepository ) : DatabaseBehaviorSpec() { private val mockHttpClient = mockk() @@ -45,7 +55,7 @@ class TelegramEventHandlerTest( } val telegramAPIService = TelegramAPIService(eventHandlerConfig, mockHttpClient) val apiServiceSpy = spyk(telegramAPIService, recordPrivateCalls = true) - TelegramEventHandler(apiServiceSpy, eventHandlerConfig, eventDispatcher) + TelegramEventHandler(apiServiceSpy, eventDispatcher) given("the TelegramEventHandler") { `when`("it receives a MonitorUpEvent and There is no previous event for the monitor") { @@ -56,15 +66,15 @@ class TelegramEventHandlerTest( latency = 1000, previousEvent = Option.empty() ) - mockHttpResponse(HttpStatus.OK) + mockSuccessfulHttpResponse() eventDispatcher.dispatch(event) then("it should send a message about the event") { - val slot = slot() + val slot = slot() verify(exactly = 1) { apiServiceSpy.sendMessage(capture(slot)) } - slot.captured.text shouldContain "Your monitor \"testMonitor\" (http://irrelevant.com) is UP (200)" + slot.captured shouldContain "Your monitor \"testMonitor\" (http://irrelevant.com) is UP (200)" } } @@ -76,15 +86,15 @@ class TelegramEventHandlerTest( error = Throwable(), previousEvent = Option.empty() ) - mockHttpResponse(HttpStatus.OK) + mockSuccessfulHttpResponse() eventDispatcher.dispatch(event) then("it should send a message about the event") { - val slot = slot() + val slot = slot() verify(exactly = 1) { apiServiceSpy.sendMessage(capture(slot)) } - slot.captured.text shouldContain "Your monitor \"testMonitor\" (http://irrelevant.com) is DOWN" + slot.captured shouldContain "Your monitor \"testMonitor\" (http://irrelevant.com) is DOWN" } } @@ -96,7 +106,7 @@ class TelegramEventHandlerTest( latency = 1000, previousEvent = Option.empty() ) - mockHttpResponse(HttpStatus.OK) + mockSuccessfulHttpResponse() eventDispatcher.dispatch(firstEvent) val firstUptimeRecord = uptimeEventRepository.fetchOne(UptimeEvent.UPTIME_EVENT.MONITOR_ID, monitor.id) @@ -109,10 +119,10 @@ class TelegramEventHandlerTest( eventDispatcher.dispatch(secondEvent) then("it should send only one notification about them") { - val slot = slot() + val slot = slot() verify(exactly = 1) { apiServiceSpy.sendMessage(capture(slot)) } - slot.captured.text shouldContain "Latency: 1000ms" + slot.captured shouldContain "Latency: 1000ms" } } @@ -124,7 +134,7 @@ class TelegramEventHandlerTest( error = Throwable("First error"), previousEvent = Option.empty() ) - mockHttpResponse(HttpStatus.OK) + mockSuccessfulHttpResponse() eventDispatcher.dispatch(firstEvent) val firstUptimeRecord = uptimeEventRepository.fetchOne(UptimeEvent.UPTIME_EVENT.MONITOR_ID, monitor.id) @@ -137,10 +147,10 @@ class TelegramEventHandlerTest( eventDispatcher.dispatch(secondEvent) then("it should send only one notification about them") { - val slot = slot() + val slot = slot() verify(exactly = 1) { apiServiceSpy.sendMessage(capture(slot)) } - slot.captured.text shouldContain "(500)" + slot.captured shouldContain "(500)" } } @@ -152,7 +162,7 @@ class TelegramEventHandlerTest( previousEvent = Option.empty(), error = Throwable() ) - mockHttpResponse(HttpStatus.OK) + mockSuccessfulHttpResponse() eventDispatcher.dispatch(firstEvent) val firstUptimeRecord = uptimeEventRepository.fetchOne(UptimeEvent.UPTIME_EVENT.MONITOR_ID, monitor.id) @@ -165,12 +175,12 @@ class TelegramEventHandlerTest( eventDispatcher.dispatch(secondEvent) then("it should send two different notifications about them") { - val notificationsSent = mutableListOf() + val notificationsSent = mutableListOf() verify(exactly = 2) { apiServiceSpy.sendMessage(capture(notificationsSent)) } - notificationsSent[0].text shouldContain "is DOWN (500)" - notificationsSent[1].text shouldContain "Latency: 1000ms" - notificationsSent[1].text shouldContain "is UP (200)" + notificationsSent[0] shouldContain "is DOWN (500)" + notificationsSent[1] shouldContain "Latency: 1000ms" + notificationsSent[1] shouldContain "is UP (200)" } } @@ -182,7 +192,7 @@ class TelegramEventHandlerTest( latency = 1000, previousEvent = Option.empty() ) - mockHttpResponse(HttpStatus.OK) + mockSuccessfulHttpResponse() eventDispatcher.dispatch(firstEvent) val firstUptimeRecord = uptimeEventRepository.fetchOne(UptimeEvent.UPTIME_EVENT.MONITOR_ID, monitor.id) @@ -195,16 +205,239 @@ class TelegramEventHandlerTest( eventDispatcher.dispatch(secondEvent) then("it should send two different notifications about them") { - val notificationsSent = mutableListOf() + val notificationsSent = mutableListOf() verify(exactly = 2) { apiServiceSpy.sendMessage(capture(notificationsSent)) } - notificationsSent[0].text shouldContain "Latency: 1000ms" - notificationsSent[0].text shouldContain "is UP (200)" - notificationsSent[1].text shouldContain "is DOWN (500)" + notificationsSent[0] shouldContain "Latency: 1000ms" + notificationsSent[0] shouldContain "is UP (200)" + notificationsSent[1] shouldContain "is DOWN (500)" } } + } + + given("the TelegramEventHandler - SSL events") { + `when`("it receives an SSLValidEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository) + val event = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + mockSuccessfulHttpResponse() + + eventDispatcher.dispatch(event) + + then("it should send a webhook message about the event") { + val slot = slot() + + verify(exactly = 1) { apiServiceSpy.sendMessage(capture(slot)) } + slot.captured shouldContain + "Your site \"${monitor.name}\" (${monitor.url}) has a VALID certificate" + } + } + + `when`("it receives an SSLInvalidEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository) + val event = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.empty(), + error = SSLValidationError("ssl error") + ) + mockSuccessfulHttpResponse() + + eventDispatcher.dispatch(event) + + then("it should send a webhook message about the event") { + val slot = slot() + + verify(exactly = 1) { apiServiceSpy.sendMessage(capture(slot)) } + slot.captured shouldContain + "Your site \"${monitor.name}\" (${monitor.url}) has an INVALID certificate" + } + } + + `when`("it receives an SSLValidEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + mockSuccessfulHttpResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SslEvent.SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(validTo = OffsetDateTime.MAX), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + then("it should send only one notification about them") { + val slot = slot() + + verify(exactly = 1) { apiServiceSpy.sendMessage(capture(slot)) } + slot.captured shouldNotContain OffsetDateTime.MAX.toString() + } + } + + `when`("it receives an SSLInvalidEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.empty(), + error = SSLValidationError("ssl error1") + ) + mockSuccessfulHttpResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SslEvent.SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.just(firstSSLRecord), + error = SSLValidationError("ssl error2") + ) + eventDispatcher.dispatch(secondEvent) - `when`("it receives an event but an error happens when it calls the API") { + then("it should send only one notification about them") { + val slot = slot() + + verify(exactly = 1) { apiServiceSpy.sendMessage(capture(slot)) } + slot.captured shouldContain "ssl error1" + } + } + + `when`("it receives an SSLValidEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.empty(), + error = SSLValidationError("ssl error1") + ) + mockSuccessfulHttpResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SslEvent.SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + then("it should send two different notifications about them") { + val notificationsSent = mutableListOf() + + verify(exactly = 2) { apiServiceSpy.sendMessage(capture(notificationsSent)) } + notificationsSent[0] shouldContain "has an INVALID certificate" + notificationsSent[1] shouldContain "has a VALID certificate" + } + } + + `when`("it receives an SSLInvalidEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + mockSuccessfulHttpResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SslEvent.SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = Option.just(firstSSLRecord), + error = SSLValidationError("ssl error") + ) + eventDispatcher.dispatch(secondEvent) + + then("it should send two different notifications about them") { + val notificationsSent = mutableListOf() + + verify(exactly = 2) { apiServiceSpy.sendMessage(capture(notificationsSent)) } + notificationsSent[0] shouldContain "has a VALID certificate" + notificationsSent[1] shouldContain "has an INVALID certificate" + } + } + + `when`("it receives an SSLWillExpireEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository) + val event = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + mockSuccessfulHttpResponse() + + eventDispatcher.dispatch(event) + + then("it should send a webhook message about the event") { + val slot = slot() + + verify(exactly = 1) { apiServiceSpy.sendMessage(capture(slot)) } + slot.captured shouldContain + "Your SSL certificate for ${monitor.url} will expire soon" + } + } + + `when`("it receives an SSLWillExpireEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + mockSuccessfulHttpResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SslEvent.SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(validTo = OffsetDateTime.MAX), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + then("it should send only one notification about them") { + val slot = slot() + + verify(exactly = 1) { apiServiceSpy.sendMessage(capture(slot)) } + slot.captured shouldNotContain OffsetDateTime.MAX.toString() + } + } + + `when`("it receives an SSLWillExpireEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository) + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.empty() + ) + mockSuccessfulHttpResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SslEvent.SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = Option.just(firstSSLRecord) + ) + eventDispatcher.dispatch(secondEvent) + + then("it should send two different notifications about them") { + val notificationsSent = mutableListOf() + + verify(exactly = 2) { apiServiceSpy.sendMessage(capture(notificationsSent)) } + notificationsSent[0] shouldContain "has a VALID certificate" + notificationsSent[1] shouldContain "Your SSL certificate for ${monitor.url} will expire soon" + } + } + } + + given("the TelegramEventHandler - error handling logic") { + `when`("it receives an event but an error happens when it calls the webhook") { val monitor = createMonitor(monitorRepository) val event = MonitorUpEvent( monitor = monitor, @@ -212,14 +445,14 @@ class TelegramEventHandlerTest( latency = 1000, previousEvent = Option.empty() ) - mockHttpErrorResponse(HttpStatus.BAD_REQUEST, "bad_request") + mockHttpErrorResponse() - then("it should send a message about the event") { - val slot = slot() + then("it should send a webhook message about the event") { + val slot = slot() shouldNotThrowAny { eventDispatcher.dispatch(event) } verify(exactly = 1) { apiServiceSpy.sendMessage(capture(slot)) } - slot.captured.text shouldContain "Your monitor \"testMonitor\" (http://irrelevant.com) is UP (200)" + slot.captured shouldContain "Your monitor \"${monitor.name}\" (${monitor.url}) is UP (200)" } } } @@ -230,27 +463,19 @@ class TelegramEventHandlerTest( super.afterTest(testCase, result) } - private fun mockHttpResponse(status: HttpStatus, body: String = "") { + private fun mockSuccessfulHttpResponse() { every { - mockHttpClient.exchange( - any(), - Argument.STRING, - Argument.STRING - ) + mockHttpClient.exchange(any(), Argument.STRING, Argument.STRING) } returns Flowable.just( - HttpResponse.status(status).body(body) + HttpResponse.ok() ) } - private fun mockHttpErrorResponse(status: HttpStatus, body: String = "") { + private fun mockHttpErrorResponse() { every { - mockHttpClient.exchange( - any(), - Argument.STRING, - Argument.STRING - ) + mockHttpClient.exchange(any(), Argument.STRING, Argument.STRING) } returns Flowable.error( - HttpClientResponseException("error", HttpResponse.status(status).body(body)) + HttpClientResponseException("error", HttpResponse.badRequest("bad_request")) ) } } diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/mocks/TestData.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/mocks/TestData.kt index 5ad0bfe..020e44d 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/mocks/TestData.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/mocks/TestData.kt @@ -1,9 +1,13 @@ package com.kuvaszuptime.kuvasz.mocks +import com.kuvaszuptime.kuvasz.enums.SslStatus import com.kuvaszuptime.kuvasz.enums.UptimeStatus +import com.kuvaszuptime.kuvasz.models.CertificateInfo import com.kuvaszuptime.kuvasz.repositories.MonitorRepository +import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo +import com.kuvaszuptime.kuvasz.tables.pojos.SslEventPojo import com.kuvaszuptime.kuvasz.tables.pojos.UptimeEventPojo import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp import java.time.OffsetDateTime @@ -13,6 +17,7 @@ fun createMonitor( repository: MonitorRepository, id: Int = 99999, enabled: Boolean = true, + sslCheckEnabled: Boolean = true, uptimeCheckInterval: Int = 30000, monitorName: String = "testMonitor", url: String = "http://irrelevant.com" @@ -23,6 +28,7 @@ fun createMonitor( .setUptimeCheckInterval(uptimeCheckInterval) .setUrl(url) .setEnabled(enabled) + .setSslCheckEnabled(sslCheckEnabled) .setCreatedAt(getCurrentTimestamp()) repository.insert(monitor) return monitor @@ -43,3 +49,22 @@ fun createUptimeEventRecord( .setUpdatedAt(endedAt ?: startedAt) .setEndedAt(endedAt) ) + +fun createSSLEventRecord( + repository: SSLEventRepository, + monitorId: Int, + status: SslStatus = SslStatus.VALID, + startedAt: OffsetDateTime, + endedAt: OffsetDateTime? +) = + repository.insert( + SslEventPojo() + .setMonitorId(monitorId) + .setStatus(status) + .setStartedAt(startedAt) + .setUpdatedAt(endedAt ?: startedAt) + .setEndedAt(endedAt) + ) + +fun generateCertificateInfo(validTo: OffsetDateTime = getCurrentTimestamp().plusDays(60)) = + CertificateInfo(validTo) diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/LogMessageFormatterTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/LogMessageFormatterTest.kt new file mode 100644 index 0000000..e7deff5 --- /dev/null +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/LogMessageFormatterTest.kt @@ -0,0 +1,218 @@ +package com.kuvaszuptime.kuvasz.models.events.formatters + +import arrow.core.Option +import com.kuvaszuptime.kuvasz.enums.SslStatus +import com.kuvaszuptime.kuvasz.enums.UptimeStatus +import com.kuvaszuptime.kuvasz.mocks.generateCertificateInfo +import com.kuvaszuptime.kuvasz.models.SSLValidationError +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.RedirectEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent +import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo +import com.kuvaszuptime.kuvasz.tables.pojos.SslEventPojo +import com.kuvaszuptime.kuvasz.tables.pojos.UptimeEventPojo +import com.kuvaszuptime.kuvasz.util.diffToDuration +import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp +import com.kuvaszuptime.kuvasz.util.toDurationString +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.micronaut.http.HttpStatus +import java.net.URI + +class LogMessageFormatterTest : BehaviorSpec( + { + val formatter = LogMessageFormatter + + val monitor = MonitorPojo() + .setId(1111) + .setName("test_monitor") + .setUrl("https://test.url") + + given("toFormattedMessage(event: UptimeMonitorEvent)") { + + `when`("it gets a MonitorUpEvent without a previousEvent") { + val event = MonitorUpEvent(monitor, HttpStatus.OK, 300, Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "✅ Your monitor \"test_monitor\" (https://test.url) is UP (200). Latency: 300ms" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorUpEvent with a previousEvent with the same status") { + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.UP) + val event = MonitorUpEvent(monitor, HttpStatus.OK, 300, Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedMessage = + "✅ Your monitor \"test_monitor\" (https://test.url) is UP (200). Latency: 300ms" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorUpEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.DOWN).setStartedAt(previousStartedAt) + val event = MonitorUpEvent(monitor, HttpStatus.OK, 300, Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "✅ Your monitor \"test_monitor\" (https://test.url) is UP (200). Latency: 300ms. " + + "Was down for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorDownEvent without a previousEvent") { + val event = MonitorDownEvent(monitor, HttpStatus.BAD_REQUEST, Throwable("uptime error"), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "🚨 Your monitor \"test_monitor\" (https://test.url) is DOWN (400). Reason: uptime error" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorDownEvent with a previousEvent with the same status") { + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.DOWN) + val event = MonitorDownEvent( + monitor, + HttpStatus.BAD_REQUEST, + Throwable("uptime error"), + Option.just(previousEvent) + ) + + then("it should return the correct message") { + val expectedMessage = + "🚨 Your monitor \"test_monitor\" (https://test.url) is DOWN (400). Reason: uptime error" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorDownEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.UP).setStartedAt(previousStartedAt) + val event = MonitorDownEvent( + monitor, + HttpStatus.BAD_REQUEST, + Throwable("uptime error"), + Option.just(previousEvent) + ) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "🚨 Your monitor \"test_monitor\" (https://test.url) is DOWN (400). Reason: uptime error. " + + "Was up for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + } + + given("toFormattedMessage(event: RedirectEvent)") { + + `when`("it gets a RedirectEvent") { + val event = RedirectEvent(monitor, URI("https://irrelevant.com")) + + then("it should return the correct message") { + val expectedMessage = + "ℹ️ Request to \"test_monitor\" (https://test.url) has been redirected" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + } + + given("toFormattedMessage(event: SSLMonitorEvent)") { + + `when`("it gets an SSLValidEvent without a previousEvent") { + val event = SSLValidEvent(monitor, generateCertificateInfo(), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "\uD83D\uDD12️ Your site \"test_monitor\" (https://test.url) has a VALID certificate" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLValidEvent with a previousEvent with the same status") { + val previousEvent = SslEventPojo().setStatus(SslStatus.VALID) + val event = SSLValidEvent(monitor, generateCertificateInfo(), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedMessage = + "\uD83D\uDD12️ Your site \"test_monitor\" (https://test.url) has a VALID certificate" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLValidEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = SslEventPojo().setStatus(SslStatus.INVALID).setStartedAt(previousStartedAt) + val event = SSLValidEvent(monitor, generateCertificateInfo(), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "\uD83D\uDD12️ Your site \"test_monitor\" (https://test.url) has a VALID certificate. " + + "Was INVALID for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLInvalidEvent without a previousEvent") { + val event = SSLInvalidEvent(monitor, SSLValidationError("ssl error"), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "🚨 Your site \"test_monitor\" (https://test.url) has an INVALID certificate. Reason: ssl error" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLInvalidEvent with a previousEvent with the same status") { + val previousEvent = SslEventPojo().setStatus(SslStatus.INVALID) + val event = SSLInvalidEvent(monitor, SSLValidationError("ssl error"), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedMessage = + "🚨 Your site \"test_monitor\" (https://test.url) has an INVALID certificate. Reason: ssl error" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLInvalidEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = SslEventPojo().setStatus(SslStatus.VALID).setStartedAt(previousStartedAt) + val event = SSLInvalidEvent(monitor, SSLValidationError("ssl error"), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "🚨 Your site \"test_monitor\" (https://test.url) has an INVALID certificate. " + + "Reason: ssl error. Was VALID for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLWillExpireEvent") { + val event = SSLWillExpireEvent(monitor, generateCertificateInfo(), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "⚠️ Your SSL certificate for https://test.url will expire soon. " + + "Expiry date: ${event.certInfo.validTo}" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + } + } +) diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/PlainTextMessageFormatterTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/PlainTextMessageFormatterTest.kt new file mode 100644 index 0000000..b12d9ec --- /dev/null +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/PlainTextMessageFormatterTest.kt @@ -0,0 +1,203 @@ +package com.kuvaszuptime.kuvasz.models.events.formatters + +import arrow.core.Option +import com.kuvaszuptime.kuvasz.enums.SslStatus +import com.kuvaszuptime.kuvasz.enums.UptimeStatus +import com.kuvaszuptime.kuvasz.mocks.generateCertificateInfo +import com.kuvaszuptime.kuvasz.models.SSLValidationError +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent +import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo +import com.kuvaszuptime.kuvasz.tables.pojos.SslEventPojo +import com.kuvaszuptime.kuvasz.tables.pojos.UptimeEventPojo +import com.kuvaszuptime.kuvasz.util.diffToDuration +import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp +import com.kuvaszuptime.kuvasz.util.toDurationString +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.micronaut.http.HttpStatus + +class PlainTextMessageFormatterTest : BehaviorSpec( + { + val formatter = PlainTextMessageFormatter + + val monitor = MonitorPojo() + .setId(1111) + .setName("test_monitor") + .setUrl("https://test.url") + + given("toFormattedMessage(event: UptimeMonitorEvent)") { + + `when`("it gets a MonitorUpEvent without a previousEvent") { + val event = MonitorUpEvent(monitor, HttpStatus.OK, 300, Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "Your monitor \"test_monitor\" (https://test.url) is UP (200)\nLatency: 300ms" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorUpEvent with a previousEvent with the same status") { + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.UP) + val event = MonitorUpEvent(monitor, HttpStatus.OK, 300, Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedMessage = + "Your monitor \"test_monitor\" (https://test.url) is UP (200)\nLatency: 300ms" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorUpEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.DOWN).setStartedAt(previousStartedAt) + val event = MonitorUpEvent(monitor, HttpStatus.OK, 300, Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "Your monitor \"test_monitor\" (https://test.url) is UP (200)\nLatency: 300ms\n" + + "Was down for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorDownEvent without a previousEvent") { + val event = MonitorDownEvent(monitor, HttpStatus.BAD_REQUEST, Throwable("uptime error"), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "Your monitor \"test_monitor\" (https://test.url) is DOWN (400)\nReason: uptime error" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorDownEvent with a previousEvent with the same status") { + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.DOWN) + val event = MonitorDownEvent( + monitor, + HttpStatus.BAD_REQUEST, + Throwable("uptime error"), + Option.just(previousEvent) + ) + + then("it should return the correct message") { + val expectedMessage = + "Your monitor \"test_monitor\" (https://test.url) is DOWN (400)\nReason: uptime error" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorDownEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.UP).setStartedAt(previousStartedAt) + val event = MonitorDownEvent( + monitor, + HttpStatus.BAD_REQUEST, + Throwable("uptime error"), + Option.just(previousEvent) + ) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "Your monitor \"test_monitor\" (https://test.url) is DOWN (400)\nReason: uptime error\n" + + "Was up for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + } + + given("toFormattedMessage(event: SSLMonitorEvent)") { + + `when`("it gets an SSLValidEvent without a previousEvent") { + val event = SSLValidEvent(monitor, generateCertificateInfo(), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "Your site \"test_monitor\" (https://test.url) has a VALID certificate" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLValidEvent with a previousEvent with the same status") { + val previousEvent = SslEventPojo().setStatus(SslStatus.VALID) + val event = SSLValidEvent(monitor, generateCertificateInfo(), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedMessage = + "Your site \"test_monitor\" (https://test.url) has a VALID certificate" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLValidEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = SslEventPojo().setStatus(SslStatus.INVALID).setStartedAt(previousStartedAt) + val event = SSLValidEvent(monitor, generateCertificateInfo(), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "Your site \"test_monitor\" (https://test.url) has a VALID certificate\n" + + "Was INVALID for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLInvalidEvent without a previousEvent") { + val event = SSLInvalidEvent(monitor, SSLValidationError("ssl error"), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "Your site \"test_monitor\" (https://test.url) has an INVALID certificate\nReason: ssl error" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLInvalidEvent with a previousEvent with the same status") { + val previousEvent = SslEventPojo().setStatus(SslStatus.INVALID) + val event = SSLInvalidEvent(monitor, SSLValidationError("ssl error"), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedMessage = + "Your site \"test_monitor\" (https://test.url) has an INVALID certificate\nReason: ssl error" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLInvalidEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = SslEventPojo().setStatus(SslStatus.VALID).setStartedAt(previousStartedAt) + val event = SSLInvalidEvent(monitor, SSLValidationError("ssl error"), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "Your site \"test_monitor\" (https://test.url) has an INVALID certificate\n" + + "Reason: ssl error\nWas VALID for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLWillExpireEvent") { + val event = SSLWillExpireEvent(monitor, generateCertificateInfo(), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "Your SSL certificate for https://test.url will expire soon\n" + + "Expiry date: ${event.certInfo.validTo}" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + } + } +) diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/SlackTextFormatterTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/SlackTextFormatterTest.kt new file mode 100644 index 0000000..7f1c399 --- /dev/null +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/SlackTextFormatterTest.kt @@ -0,0 +1,204 @@ +package com.kuvaszuptime.kuvasz.models.events.formatters + +import arrow.core.Option +import com.kuvaszuptime.kuvasz.enums.SslStatus +import com.kuvaszuptime.kuvasz.enums.UptimeStatus +import com.kuvaszuptime.kuvasz.mocks.generateCertificateInfo +import com.kuvaszuptime.kuvasz.models.SSLValidationError +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent +import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo +import com.kuvaszuptime.kuvasz.tables.pojos.SslEventPojo +import com.kuvaszuptime.kuvasz.tables.pojos.UptimeEventPojo +import com.kuvaszuptime.kuvasz.util.diffToDuration +import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp +import com.kuvaszuptime.kuvasz.util.toDurationString +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.micronaut.http.HttpStatus + +class SlackTextFormatterTest : BehaviorSpec( + { + val formatter = SlackTextFormatter + + val monitor = MonitorPojo() + .setId(1111) + .setName("test_monitor") + .setUrl("https://test.url") + + given("toFormattedMessage(event: UptimeMonitorEvent)") { + + `when`("it gets a MonitorUpEvent without a previousEvent") { + val event = MonitorUpEvent(monitor, HttpStatus.OK, 300, Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "✅ *Your monitor \"test_monitor\" (https://test.url) is UP (200)*\n_Latency: 300ms_" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorUpEvent with a previousEvent with the same status") { + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.UP) + val event = MonitorUpEvent(monitor, HttpStatus.OK, 300, Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedMessage = + "✅ *Your monitor \"test_monitor\" (https://test.url) is UP (200)*\n_Latency: 300ms_" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorUpEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.DOWN).setStartedAt(previousStartedAt) + val event = MonitorUpEvent(monitor, HttpStatus.OK, 300, Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "✅ *Your monitor \"test_monitor\" (https://test.url) is UP (200)*\n_Latency: 300ms_\n" + + "Was down for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorDownEvent without a previousEvent") { + val event = MonitorDownEvent(monitor, HttpStatus.BAD_REQUEST, Throwable("uptime error"), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "🚨 *Your monitor \"test_monitor\" (https://test.url) is DOWN (400)*" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorDownEvent with a previousEvent with the same status") { + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.DOWN) + val event = MonitorDownEvent( + monitor, + HttpStatus.BAD_REQUEST, + Throwable("uptime error"), + Option.just(previousEvent) + ) + + then("it should return the correct message") { + val expectedMessage = + "🚨 *Your monitor \"test_monitor\" (https://test.url) is DOWN (400)*" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorDownEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.UP).setStartedAt(previousStartedAt) + val event = MonitorDownEvent( + monitor, + HttpStatus.BAD_REQUEST, + Throwable("uptime error"), + Option.just(previousEvent) + ) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "🚨 *Your monitor \"test_monitor\" (https://test.url) is DOWN (400)*\n" + + "Was up for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + } + + given("toFormattedMessage(event: SSLMonitorEvent)") { + + `when`("it gets an SSLValidEvent without a previousEvent") { + val event = SSLValidEvent(monitor, generateCertificateInfo(), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "\uD83D\uDD12️ *Your site \"test_monitor\" (https://test.url) has a VALID certificate*" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLValidEvent with a previousEvent with the same status") { + val previousEvent = SslEventPojo().setStatus(SslStatus.VALID) + val event = SSLValidEvent(monitor, generateCertificateInfo(), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedMessage = + "\uD83D\uDD12️ *Your site \"test_monitor\" (https://test.url) has a VALID certificate*" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLValidEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = SslEventPojo().setStatus(SslStatus.INVALID).setStartedAt(previousStartedAt) + val event = SSLValidEvent(monitor, generateCertificateInfo(), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "\uD83D\uDD12️ *Your site \"test_monitor\" (https://test.url) has a VALID certificate*\n" + + "Was INVALID for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLInvalidEvent without a previousEvent") { + val event = SSLInvalidEvent(monitor, SSLValidationError("ssl error"), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "🚨 *Your site \"test_monitor\" (https://test.url) has an INVALID " + + "certificate*\n_Reason: ssl error_" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLInvalidEvent with a previousEvent with the same status") { + val previousEvent = SslEventPojo().setStatus(SslStatus.INVALID) + val event = SSLInvalidEvent(monitor, SSLValidationError("ssl error"), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedMessage = "🚨 *Your site \"test_monitor\" (https://test.url) has an INVALID " + + "certificate*\n_Reason: ssl error_" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLInvalidEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = SslEventPojo().setStatus(SslStatus.VALID).setStartedAt(previousStartedAt) + val event = SSLInvalidEvent(monitor, SSLValidationError("ssl error"), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "🚨 *Your site \"test_monitor\" (https://test.url) has an INVALID certificate*\n" + + "_Reason: ssl error_\nWas VALID for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLWillExpireEvent") { + val event = SSLWillExpireEvent(monitor, generateCertificateInfo(), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "⚠️ *Your SSL certificate for https://test.url will expire soon*\n" + + "_Expiry date: ${event.certInfo.validTo}_" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + } + } +) diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/TelegramTextFormatterTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/TelegramTextFormatterTest.kt new file mode 100644 index 0000000..0ddd8ef --- /dev/null +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/models/events/formatters/TelegramTextFormatterTest.kt @@ -0,0 +1,204 @@ +package com.kuvaszuptime.kuvasz.models.events.formatters + +import arrow.core.Option +import com.kuvaszuptime.kuvasz.enums.SslStatus +import com.kuvaszuptime.kuvasz.enums.UptimeStatus +import com.kuvaszuptime.kuvasz.mocks.generateCertificateInfo +import com.kuvaszuptime.kuvasz.models.SSLValidationError +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent +import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo +import com.kuvaszuptime.kuvasz.tables.pojos.SslEventPojo +import com.kuvaszuptime.kuvasz.tables.pojos.UptimeEventPojo +import com.kuvaszuptime.kuvasz.util.diffToDuration +import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp +import com.kuvaszuptime.kuvasz.util.toDurationString +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.micronaut.http.HttpStatus + +class TelegramTextFormatterTest : BehaviorSpec( + { + val formatter = TelegramTextFormatter + + val monitor = MonitorPojo() + .setId(1111) + .setName("test_monitor") + .setUrl("https://test.url") + + given("toFormattedMessage(event: UptimeMonitorEvent)") { + + `when`("it gets a MonitorUpEvent without a previousEvent") { + val event = MonitorUpEvent(monitor, HttpStatus.OK, 300, Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "✅ Your monitor \"test_monitor\" (https://test.url) is UP (200)\nLatency: 300ms" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorUpEvent with a previousEvent with the same status") { + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.UP) + val event = MonitorUpEvent(monitor, HttpStatus.OK, 300, Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedMessage = + "✅ Your monitor \"test_monitor\" (https://test.url) is UP (200)\nLatency: 300ms" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorUpEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.DOWN).setStartedAt(previousStartedAt) + val event = MonitorUpEvent(monitor, HttpStatus.OK, 300, Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "✅ Your monitor \"test_monitor\" (https://test.url) is UP (200)\nLatency: 300ms" + + "\nWas down for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorDownEvent without a previousEvent") { + val event = MonitorDownEvent(monitor, HttpStatus.BAD_REQUEST, Throwable("uptime error"), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "🚨 Your monitor \"test_monitor\" (https://test.url) is DOWN (400)" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorDownEvent with a previousEvent with the same status") { + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.DOWN) + val event = MonitorDownEvent( + monitor, + HttpStatus.BAD_REQUEST, + Throwable("uptime error"), + Option.just(previousEvent) + ) + + then("it should return the correct message") { + val expectedMessage = + "🚨 Your monitor \"test_monitor\" (https://test.url) is DOWN (400)" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets a MonitorDownEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = UptimeEventPojo().setStatus(UptimeStatus.UP).setStartedAt(previousStartedAt) + val event = MonitorDownEvent( + monitor, + HttpStatus.BAD_REQUEST, + Throwable("uptime error"), + Option.just(previousEvent) + ) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "🚨 Your monitor \"test_monitor\" (https://test.url) is DOWN (400)\n" + + "Was up for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + } + + given("toFormattedMessage(event: SSLMonitorEvent)") { + + `when`("it gets an SSLValidEvent without a previousEvent") { + val event = SSLValidEvent(monitor, generateCertificateInfo(), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "\uD83D\uDD12️ Your site \"test_monitor\" (https://test.url) has a VALID certificate" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLValidEvent with a previousEvent with the same status") { + val previousEvent = SslEventPojo().setStatus(SslStatus.VALID) + val event = SSLValidEvent(monitor, generateCertificateInfo(), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedMessage = + "\uD83D\uDD12️ Your site \"test_monitor\" (https://test.url) has a VALID certificate" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLValidEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = SslEventPojo().setStatus(SslStatus.INVALID).setStartedAt(previousStartedAt) + val event = SSLValidEvent(monitor, generateCertificateInfo(), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "\uD83D\uDD12️ Your site \"test_monitor\" (https://test.url) has a VALID certificate\n" + + "Was INVALID for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLInvalidEvent without a previousEvent") { + val event = SSLInvalidEvent(monitor, SSLValidationError("ssl error"), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "🚨 Your site \"test_monitor\" (https://test.url) has an INVALID " + + "certificate\nReason: ssl error" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLInvalidEvent with a previousEvent with the same status") { + val previousEvent = SslEventPojo().setStatus(SslStatus.INVALID) + val event = SSLInvalidEvent(monitor, SSLValidationError("ssl error"), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedMessage = "🚨 Your site \"test_monitor\" (https://test.url) has an INVALID " + + "certificate\nReason: ssl error" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLInvalidEvent with a previousEvent with different status") { + val previousStartedAt = getCurrentTimestamp().minusMinutes(30) + val previousEvent = SslEventPojo().setStatus(SslStatus.VALID).setStartedAt(previousStartedAt) + val event = SSLInvalidEvent(monitor, SSLValidationError("ssl error"), Option.just(previousEvent)) + + then("it should return the correct message") { + val expectedDurationString = + previousEvent.startedAt.diffToDuration(event.dispatchedAt).toDurationString() + val expectedMessage = + "🚨 Your site \"test_monitor\" (https://test.url) has an INVALID certificate\n" + + "Reason: ssl error\nWas VALID for $expectedDurationString" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + + `when`("it gets an SSLWillExpireEvent") { + val event = SSLWillExpireEvent(monitor, generateCertificateInfo(), Option.empty()) + + then("it should return the correct message") { + val expectedMessage = + "⚠️ Your SSL certificate for https://test.url will expire soon\n" + + "Expiry date: ${event.certInfo.validTo}" + formatter.toFormattedMessage(event) shouldBe expectedMessage + } + } + } + } +) diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/repositories/UptimeEventRepositoryTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/repositories/UptimeEventRepositoryTest.kt new file mode 100644 index 0000000..2c8a18d --- /dev/null +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/repositories/UptimeEventRepositoryTest.kt @@ -0,0 +1,58 @@ +package com.kuvaszuptime.kuvasz.repositories + +import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec +import com.kuvaszuptime.kuvasz.enums.UptimeStatus +import com.kuvaszuptime.kuvasz.mocks.createMonitor +import com.kuvaszuptime.kuvasz.mocks.createUptimeEventRecord +import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp +import io.kotest.matchers.shouldBe +import io.micronaut.test.annotation.MicronautTest + +@MicronautTest +class UptimeEventRepositoryTest( + private val monitorRepository: MonitorRepository, + private val uptimeEventRepository: UptimeEventRepository +) : DatabaseBehaviorSpec() { + + init { + given("isMonitorUp() method") { + `when`("the monitor is UP") { + val monitor = createMonitor(monitorRepository) + createUptimeEventRecord( + repository = uptimeEventRepository, + monitorId = monitor.id, + startedAt = getCurrentTimestamp(), + status = UptimeStatus.UP, + endedAt = null + ) + + then("it should return true") { + uptimeEventRepository.isMonitorUp(monitor.id) shouldBe true + } + } + + `when`("the monitor is DOWN") { + val monitor = createMonitor(monitorRepository) + createUptimeEventRecord( + repository = uptimeEventRepository, + monitorId = monitor.id, + startedAt = getCurrentTimestamp(), + status = UptimeStatus.DOWN, + endedAt = null + ) + + then("it should return false") { + uptimeEventRepository.isMonitorUp(monitor.id) shouldBe false + } + } + + `when`("there is no UPTIME_EVENT record") { + val monitor = createMonitor(monitorRepository) + + then("it should return false") { + uptimeEventRepository.isMonitorUp(monitor.id) shouldBe false + } + } + } + } +} diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/services/CheckSchedulerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/services/CheckSchedulerTest.kt index d61be06..ba72e52 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/services/CheckSchedulerTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/services/CheckSchedulerTest.kt @@ -4,17 +4,21 @@ import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec import com.kuvaszuptime.kuvasz.mocks.createMonitor import com.kuvaszuptime.kuvasz.models.CheckType import com.kuvaszuptime.kuvasz.repositories.MonitorRepository +import io.kotest.core.test.TestCase +import io.kotest.core.test.TestResult +import io.kotest.inspectors.forExactly import io.kotest.inspectors.forNone +import io.kotest.inspectors.forOne +import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe import io.micronaut.test.annotation.MicronautTest @MicronautTest class CheckSchedulerTest( private val checkScheduler: CheckScheduler, private val monitorRepository: MonitorRepository -) : DatabaseBehaviorSpec( - { +) : DatabaseBehaviorSpec() { + init { given("the CheckScheduler service") { `when`("there is an enabled monitor in the database and initialize has been called") { val monitor = createMonitor(monitorRepository) @@ -22,11 +26,11 @@ class CheckSchedulerTest( checkScheduler.initialize() then("it should schedule the check for it") { - val expectedCheck = checkScheduler.getScheduledChecks().find { it.monitorId == monitor.id } - expectedCheck shouldNotBe null - expectedCheck!!.checkType shouldBe CheckType.UPTIME - expectedCheck.task.isCancelled shouldBe false - expectedCheck.task.isDone shouldBe false + val expectedChecks = checkScheduler.getScheduledChecks().filter { it.monitorId == monitor.id } + expectedChecks shouldHaveSize 2 + expectedChecks.forOne { it.checkType shouldBe CheckType.UPTIME } + expectedChecks.forExactly(2) { it.task.isCancelled shouldBe false } + expectedChecks.forExactly(2) { it.task.isDone shouldBe false } } } @@ -49,6 +53,26 @@ class CheckSchedulerTest( checkScheduler.getScheduledChecks().forNone { it.monitorId shouldBe monitor.id } } } + + `when`( + "there is an enabled monitor in the database with disabled SSL checks" + + " and initialize has been called" + ) { + val monitor = createMonitor(monitorRepository, sslCheckEnabled = false) + + checkScheduler.initialize() + + then("it should schedule only the uptime check for it") { + val checks = checkScheduler.getScheduledChecks().filter { it.monitorId == monitor.id } + checks shouldHaveSize 1 + checks[0].checkType shouldBe CheckType.UPTIME + } + } } } -) + + override fun afterTest(testCase: TestCase, result: TestResult) { + checkScheduler.removeAllChecks() + super.afterTest(testCase, result) + } +} diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/services/DatabaseCleanerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/services/DatabaseCleanerTest.kt index 54a4377..0aab5a2 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/services/DatabaseCleanerTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/services/DatabaseCleanerTest.kt @@ -2,9 +2,11 @@ package com.kuvaszuptime.kuvasz.services import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec import com.kuvaszuptime.kuvasz.mocks.createMonitor +import com.kuvaszuptime.kuvasz.mocks.createSSLEventRecord import com.kuvaszuptime.kuvasz.mocks.createUptimeEventRecord import com.kuvaszuptime.kuvasz.repositories.LatencyLogRepository import com.kuvaszuptime.kuvasz.repositories.MonitorRepository +import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository import com.kuvaszuptime.kuvasz.tables.pojos.LatencyLogPojo import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp @@ -19,6 +21,7 @@ class DatabaseCleanerTest( private val uptimeEventRepository: UptimeEventRepository, private val latencyLogRepository: LatencyLogRepository, private val monitorRepository: MonitorRepository, + private val sslEventRepository: SSLEventRepository, private val databaseCleaner: DatabaseCleaner ) : DatabaseBehaviorSpec() { init { @@ -93,6 +96,54 @@ class DatabaseCleanerTest( latencyLogRecords shouldHaveSize 0 } } + + `when`("there is an SSL_EVENT record with an end date greater than retention limit") { + val monitor = createMonitor(monitorRepository) + createSSLEventRecord( + repository = sslEventRepository, + monitorId = monitor.id, + startedAt = getCurrentTimestamp().minusDays(1), + endedAt = getCurrentTimestamp() + ) + databaseCleaner.cleanObsoleteData() + + then("it should not delete it") { + val sslEventRecords = sslEventRepository.fetchByMonitorId(monitor.id) + sslEventRecords shouldHaveSize 1 + } + } + + `when`("there is an SSL_EVENT record without an end date") { + val monitor = createMonitor(monitorRepository) + createSSLEventRecord( + repository = sslEventRepository, + monitorId = monitor.id, + startedAt = getCurrentTimestamp().minusDays(20), + endedAt = null + ) + databaseCleaner.cleanObsoleteData() + + then("it should not delete it") { + val sslEventRecords = sslEventRepository.fetchByMonitorId(monitor.id) + sslEventRecords shouldHaveSize 1 + } + } + + `when`("there is an SSL_EVENT record with an end date less than retention limit") { + val monitor = createMonitor(monitorRepository) + createSSLEventRecord( + repository = sslEventRepository, + monitorId = monitor.id, + startedAt = getCurrentTimestamp().minusDays(20), + endedAt = getCurrentTimestamp().minusDays(8) + ) + databaseCleaner.cleanObsoleteData() + + then("it should delete it") { + val sslEventRecords = sslEventRepository.fetchByMonitorId(monitor.id) + sslEventRecords shouldHaveSize 0 + } + } } } diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/services/SSLCheckerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/services/SSLCheckerTest.kt new file mode 100644 index 0000000..ca7a4a4 --- /dev/null +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/services/SSLCheckerTest.kt @@ -0,0 +1,198 @@ +package com.kuvaszuptime.kuvasz.services + +import arrow.core.Either +import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec +import com.kuvaszuptime.kuvasz.enums.SslStatus +import com.kuvaszuptime.kuvasz.mocks.createMonitor +import com.kuvaszuptime.kuvasz.models.CertificateInfo +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.SSLValidationError +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent +import com.kuvaszuptime.kuvasz.repositories.MonitorRepository +import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository +import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository +import com.kuvaszuptime.kuvasz.testutils.shouldBe +import com.kuvaszuptime.kuvasz.testutils.toSubscriber +import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp +import io.kotest.core.test.TestCase +import io.kotest.core.test.TestResult +import io.kotest.matchers.comparables.shouldBeGreaterThan +import io.kotest.matchers.comparables.shouldBeLessThan +import io.kotest.matchers.shouldBe +import io.micronaut.test.annotation.MicronautTest +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import io.reactivex.subscribers.TestSubscriber +import java.time.OffsetDateTime + +@MicronautTest +class SSLCheckerTest( + private val monitorRepository: MonitorRepository, + sslEventRepository: SSLEventRepository +) : DatabaseBehaviorSpec() { + + private val sslValidator = mockk() + private val uptimeEventRepository = mockk() + + init { + val eventDispatcher = EventDispatcher() + val sslChecker = spyk( + SSLChecker( + sslValidator = sslValidator, + eventDispatcher = eventDispatcher, + sslEventRepository = sslEventRepository, + uptimeEventRepository = uptimeEventRepository + ) + ) + + given("the SSLChecker service") { + `when`("it checks a monitor that is DOWN") { + val monitor = createMonitor(monitorRepository) + mockIsMonitorUpResult(false) + + sslChecker.check(monitor) + + then("it should not run the SSL check") { + verify(exactly = 0) { sslValidator.validate(any()) } + } + } + + `when`("it checks a monitor with a valid certificate") { + val monitor = createMonitor(monitorRepository) + val subscriber = TestSubscriber() + eventDispatcher.subscribeToSSLValidEvents { it.toSubscriber(subscriber) } + mockValidationResult(SslStatus.VALID) + mockIsMonitorUpResult(true) + + sslChecker.check(monitor) + + then("it should dispatch an SSLValidEvent") { + val expectedEvent = subscriber.values().first() + + subscriber.valueCount() shouldBe 1 + expectedEvent.monitor.id shouldBe monitor.id + } + } + + `when`("it checks a monitor with an INVALID certificate") { + val monitor = createMonitor(monitorRepository) + val subscriber = TestSubscriber() + eventDispatcher.subscribeToSSLInvalidEvents { it.toSubscriber(subscriber) } + mockValidationResult(SslStatus.INVALID) + mockIsMonitorUpResult(true) + + sslChecker.check(monitor) + + then("it should dispatch an SSLInvalidEvent") { + val expectedEvent = subscriber.awaitCount(1).values().first() + + subscriber.valueCount() shouldBe 1 + expectedEvent.monitor.id shouldBe monitor.id + expectedEvent.error.message shouldBe "validation error" + } + } + + `when`("it checks a monitor that has an INVALID cert then it's VALID again") { + val monitor = createMonitor(monitorRepository) + val certValidSubscriber = TestSubscriber() + val certInvalidSubscriber = TestSubscriber() + eventDispatcher.subscribeToSSLValidEvents { it.toSubscriber(certValidSubscriber) } + eventDispatcher.subscribeToSSLInvalidEvents { it.toSubscriber(certInvalidSubscriber) } + mockValidationResult(SslStatus.INVALID) + mockIsMonitorUpResult(true) + + then("it should dispatch an SSLInvalid and an SSLValidEvent") { + sslChecker.check(monitor) + clearMocks(sslValidator) + mockValidationResult(SslStatus.VALID) + mockIsMonitorUpResult(true) + sslChecker.check(monitor) + + val expectedInvalidEvent = certInvalidSubscriber.values().first() + val expectedValidEvent = certValidSubscriber.values().first() + + certInvalidSubscriber.valueCount() shouldBe 1 + certValidSubscriber.valueCount() shouldBe 1 + expectedInvalidEvent.monitor.id shouldBe monitor.id + expectedValidEvent.monitor.id shouldBe monitor.id + expectedInvalidEvent.dispatchedAt shouldBeLessThan expectedValidEvent.dispatchedAt + } + } + + `when`("it checks a monitor that has a VALID cert but then it's INVALID again") { + val monitor = createMonitor(monitorRepository) + val certValidSubscriber = TestSubscriber() + val certInvalidSubscriber = TestSubscriber() + eventDispatcher.subscribeToSSLValidEvents { it.toSubscriber(certValidSubscriber) } + eventDispatcher.subscribeToSSLInvalidEvents { it.toSubscriber(certInvalidSubscriber) } + mockValidationResult(SslStatus.VALID) + mockIsMonitorUpResult(true) + + then("it should dispatch an SSLValid and then an SSLInvalidEvent") { + sslChecker.check(monitor) + clearMocks(sslValidator) + mockValidationResult(SslStatus.INVALID) + mockIsMonitorUpResult(true) + sslChecker.check(monitor) + + val expectedInvalidEvent = certInvalidSubscriber.values().first() + val expectedValidEvent = certValidSubscriber.values().first() + + certInvalidSubscriber.valueCount() shouldBe 1 + certValidSubscriber.valueCount() shouldBe 1 + expectedInvalidEvent.monitor.id shouldBe monitor.id + expectedValidEvent.monitor.id shouldBe monitor.id + expectedInvalidEvent.dispatchedAt shouldBeGreaterThan expectedValidEvent.dispatchedAt + } + } + + `when`("it checks a monitor that has a cert that expires soon") { + val monitor = createMonitor(monitorRepository) + val subscriber = TestSubscriber() + eventDispatcher.subscribeToSSLWillExpireEvents { it.toSubscriber(subscriber) } + val validTo = getCurrentTimestamp().minusDays(29) + mockValidationResult( + status = SslStatus.WILL_EXPIRE, + validTo = validTo + ) + mockIsMonitorUpResult(true) + + sslChecker.check(monitor) + + then("it should dispatch an SSLWillExpireEvent with the right expiration date") { + val expectedEvent = subscriber.values().first() + + subscriber.valueCount() shouldBe 1 + expectedEvent.monitor.id shouldBe monitor.id + expectedEvent.certInfo.validTo shouldBe validTo + } + } + } + } + + override fun afterTest(testCase: TestCase, result: TestResult) { + clearMocks(sslValidator) + super.afterTest(testCase, result) + } + + private fun mockValidationResult( + status: SslStatus, + validTo: OffsetDateTime = getCurrentTimestamp().plusDays(60) + ) { + val certInfo = CertificateInfo(validTo) + val mockResult: Either = when (status) { + SslStatus.VALID -> Either.right(certInfo) + SslStatus.WILL_EXPIRE -> Either.right(certInfo) + SslStatus.INVALID -> Either.left(SSLValidationError("validation error")) + } + every { sslValidator.validate(any()) } returns mockResult + } + + private fun mockIsMonitorUpResult(result: Boolean) { + every { uptimeEventRepository.isMonitorUp(any()) } returns result + } +} diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/services/SSLValidatorTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/services/SSLValidatorTest.kt new file mode 100644 index 0000000..8b19ee4 --- /dev/null +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/services/SSLValidatorTest.kt @@ -0,0 +1,44 @@ +package com.kuvaszuptime.kuvasz.services + +import io.kotest.core.spec.style.StringSpec +import io.kotest.data.forAll +import io.kotest.data.headers +import io.kotest.data.row +import io.kotest.data.table +import io.kotest.matchers.booleans.shouldBeTrue +import java.net.URL + +class SSLValidatorTest : StringSpec( + { + val validator = SSLValidator() + + "validate should return the right result" { + table( + headers("url", "isValid"), + row("https://sha256.badssl.com/", true), + row("https://sha384.badssl.com/", true), + row("https://sha512.badssl.com/", true), + row("https://1000-sans.badssl.com/", true), + row("https://10000-sans.badssl.com/", true), + row("https://ecc256.badssl.com/", true), + row("https://ecc384.badssl.com/", true), + row("https://rsa2048.badssl.com/", true), + row("https://rsa4096.badssl.com/", true), + row("https://rsa8192.badssl.com/", true), + row("https://extended-validation.badssl.com/", true), + + row("https://expired.badssl.com/", false), + row("https://wrong.host.badssl.com/", false), + row("https://self-signed.badssl.com/", false), + row("https://untrusted-root.badssl.com/", false), + row("https://no-common-name.badssl.com/", false), + row("https://no-subject.badssl.com/", false), + row("https://incomplete-chain.badssl.com/", false) + ).forAll { url, isValid -> + val result = validator.validate(URL(url)) + + (if (isValid) result.isRight() else result.isLeft()).shouldBeTrue() + } + } + } +) diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/services/UptimeCheckerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/services/UptimeCheckerTest.kt index f6035a4..741bbad 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/services/UptimeCheckerTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/services/UptimeCheckerTest.kt @@ -2,9 +2,9 @@ package com.kuvaszuptime.kuvasz.services import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec import com.kuvaszuptime.kuvasz.mocks.createMonitor -import com.kuvaszuptime.kuvasz.models.MonitorDownEvent -import com.kuvaszuptime.kuvasz.models.MonitorUpEvent -import com.kuvaszuptime.kuvasz.models.RedirectEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.RedirectEvent import com.kuvaszuptime.kuvasz.repositories.MonitorRepository import com.kuvaszuptime.kuvasz.testutils.toSubscriber import com.kuvaszuptime.kuvasz.util.toUri @@ -73,7 +73,7 @@ class UptimeCheckerTest( eventDispatcher.subscribeToMonitorDownEvents { it.toSubscriber(monitorDownSubscriber) } mockHttpResponse(uptimeCheckerSpy, HttpStatus.NOT_FOUND) - then("it should dispatch a MonitorDownEvent") { + then("it should dispatch a MonitorDownEvent and a MonitorUpEvent") { uptimeCheckerSpy.check(monitor) clearAllMocks() mockHttpResponse(uptimeCheckerSpy, HttpStatus.OK) @@ -98,7 +98,7 @@ class UptimeCheckerTest( eventDispatcher.subscribeToMonitorDownEvents { it.toSubscriber(monitorDownSubscriber) } mockHttpResponse(uptimeCheckerSpy, HttpStatus.OK) - then("it should dispatch a MonitorDownEvent") { + then("it should dispatch a MonitorUpEvent and a MonitorDownEvent") { uptimeCheckerSpy.check(monitor) clearAllMocks() mockHttpResponse(uptimeCheckerSpy, HttpStatus.NOT_FOUND) diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/testutils/Rx.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/testutils/Rx.kt index 0c74856..2910e74 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/testutils/Rx.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/testutils/Rx.kt @@ -1,6 +1,6 @@ package com.kuvaszuptime.kuvasz.testutils -import com.kuvaszuptime.kuvasz.models.Event +import com.kuvaszuptime.kuvasz.models.events.MonitorEvent import io.reactivex.subscribers.TestSubscriber -fun T.toSubscriber(testSubscriber: TestSubscriber) = testSubscriber.onNext(this) +fun T.toSubscriber(testSubscriber: TestSubscriber) = testSubscriber.onNext(this) diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 58905b9..11a1b13 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -12,7 +12,6 @@ -