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 @@
-