aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/alert_and_notification_logic.rb391
-rw-r--r--test/mauve_test_helper.rb104
-rw-r--r--test/mauve_time.rb38
-rw-r--r--test/notification.rb57
4 files changed, 590 insertions, 0 deletions
diff --git a/test/alert_and_notification_logic.rb b/test/alert_and_notification_logic.rb
new file mode 100644
index 0000000..19b2478
--- /dev/null
+++ b/test/alert_and_notification_logic.rb
@@ -0,0 +1,391 @@
+# Mauve server tests - alerts and notification logic. Define the basic workings
+# so that we know what should happen when we send sequences of alerts at
+# different times.
+#
+# These aren't really unit tests, just narrative specifications as to what
+# should happen under what stimuli. I suspect I will break these down into
+# smaller units if things break under otherwise difficult conditions.
+#
+
+$: << __FILE__.split("/")[0..-2].join("/")
+require 'test/unit'
+require 'mauve_test_helper'
+require 'mauve_time'
+
+class AlertAndNotificationLogic < Test::Unit::TestCase
+ include MauveTestHelper
+
+ def configuration_template
+ <<-TEMPLATE
+ # This is the head of all the configuration files. Filenames are relative
+ # to the cwd, which is assumed to be a fleeting test directory.
+
+ server {
+ ip "127.0.0.1"
+ port #{@port_alerts ||= 44444}
+ log_file ENV['TEST_LOG'] ? STDOUT : "#{dir}/log"
+ log_level 0
+ database "sqlite3:///#{dir}/mauve_test.db"
+ transmission_id_expire_time 600
+
+ # doesn't restart nicely at the moment
+ #web_interface {
+ # port #{@port_web ||= 44444}
+ #}
+ }
+
+ #
+ # All notifications are sent to files which we can open up and check during
+ # our tests. Network delivery is not tested in this script.
+ #
+
+ notification_method("xmpp") {
+ deliver_to_queue AlertAndNotificationLogic::Notifications
+ deliver_to_file "#{dir}/xmpp.txt"
+ disable_normal_delivery!
+
+ jid "mauveserv@chat.bytemark.co.uk"
+ password "foo"
+ }
+
+ notification_method("email") {
+ deliver_to_queue AlertAndNotificationLogic::Notifications
+ deliver_to_file "#{dir}/email.txt"
+ disable_normal_delivery!
+
+ # add in SMTP server, username, password etc.
+ # default to sending through localhost
+ from "matthew@bytemark.co.uk"
+ server "bytemail.bytemark.co.uk"
+ subject_prefix "[Bytemark alerts] "
+
+ }
+
+ notification_method("sms") {
+ provider "AQL"
+ deliver_to_queue AlertAndNotificationLogic::Notifications
+ deliver_to_file "#{dir}/sms.txt"
+ disable_normal_delivery!
+
+ username "x"
+ password "x"
+ from "01904890890"
+ max_messages_per_alert 3
+ }
+
+ # a person common to all our tests
+
+ person("joe_bloggs") {
+ urgent { sms("12345") }
+ normal { email("12345@joe_bloggs.email") }
+ low { xmpp("12345@joe_bloggs.xmpp") }
+ }
+
+ person("jimmy_junior") {
+ urgent { sms("66666") }
+ normal { email("jimmy@junior.email") }
+ low { email("jimmy@junior.email") }
+ }
+
+ alert_group {
+ includes { source == "rare-and-important" }
+ acknowledgement_time 60.minutes
+ level URGENT
+
+ notify("joe_bloggs") { every 10.minutes }
+ }
+
+ alert_group {
+ includes { source == "noisy-and-annoying" || alert_id == "whine" }
+ acknowledgement_time 24.hours
+ level LOW
+
+ notify("jimmy_junior") { every 2.hours }
+ notify("joe_bloggs") {
+ every 30.minutes
+ during {
+ unacknowledged 6.hours
+ }
+ }
+ }
+
+ alert_group {
+ includes { source == "can-wait-until-monday" }
+ level NORMAL
+
+ notify("jimmy_junior") {
+ every 30.minutes
+ during { days_in_week(1..5) && hours_in_day(9..5) }
+ }
+ notify("joe_bloggs") {
+ every 2.hours
+ during { days_in_week(1..5) && hours_in_day(9..5) }
+ }
+ }
+
+ # catch-all
+ alert_group {
+ acknowledgement_time 1.minute
+ level NORMAL
+
+ notify("joe_bloggs") { every 1.hour }
+ }
+ TEMPLATE
+ end
+
+ def setup
+ start_server(configuration_template)
+ end
+
+ def teardown
+ stop_server
+ # no tests should leave notifications on the stack
+ assert_no_notification
+ end
+
+ # Raise one alert, check representation in database, and that alert is
+ # received as expected.
+ #
+ def test_basic_fields_are_recognised
+ mauvesend("-o my_source -i alert1 -s \"alert1 summary\" -d \"alert1 detail\" -u \"alert1 subject\"")
+
+ assert_not_nil(alert = Alert.first)
+ assert_equal("my_source", alert.source)
+ assert_equal("alert1", alert.alert_id)
+ assert_equal("alert1 summary", alert.summary)
+ assert_equal("alert1 detail", alert.detail)
+ assert_equal("alert1 subject", alert.subject)
+ assert(alert.raised?)
+ assert(!alert.cleared?)
+ assert(!alert.acknowledged?)
+
+ with_next_notification do |destination, this_alert, other_alerts|
+ assert_equal("12345@joe_bloggs.email", destination)
+ assert_equal(Alert.first, this_alert)
+ assert_equal([Alert.first], other_alerts)
+ end
+
+ end
+
+ # Check that a simple automatic raise, acknowledge & auto-clear request
+ # work properly.
+ #
+ def test_auto_raise_and_clear
+ # Raise the alert, wait for it to be processed
+ mauvesend("-o my_source -i alert1 -s \"alert1 summary\" -d \"alert1 detail\" -u \"alert1 subject\" -r +5m -c +10m")
+
+ # Check internal state
+ #
+ assert(!Alert.first.raised?, "Auto-raising alert raised early")
+ assert(!Alert.first.cleared?, "Auto-clearing alert cleared early")
+ assert(!Alert.first.acknowledged?, "Alert acknowledged when I didn't expect it")
+
+ # We asked for it to be raised in 5 minutes, so no alert yet...
+ #
+ assert_no_notification
+
+ # Push forward to when the alert should be raised, check it has been
+ #
+ Time.advance(5.minutes)
+ assert(Alert.first.raised?, "#{Alert.first.inspect} should be raised by now")
+ assert(!Alert.first.cleared?, "#{Alert.first.inspect} should not be cleared")
+
+ # Check that we have a notification
+ #
+ with_next_notification do |destination, this_alert, other_alerts|
+ assert_equal("12345@joe_bloggs.email", destination)
+ assert_equal(Alert.first, this_alert)
+ assert_equal('raised', this_alert.update_type)
+ end
+
+ # Simulate manual acknowledgement
+ #
+ Alert.first.acknowledge!(Configuration.current.people["joe_bloggs"])
+ Timers.restart_and_then_wait_until_idle
+ assert(Alert.first.acknowledged?, "Acknowledgement didn't work")
+
+ # Check that the acknowledgement has caused a notification
+ #
+ with_next_notification do |destination, this_alert, other_alerts|
+ assert_equal("12345@joe_bloggs.email", destination)
+ assert_equal(Alert.first, this_alert)
+ assert_equal('acknowledged', this_alert.update_type, this_alert.inspect)
+ end
+ assert(Alert.first.acknowledged?)
+ assert(Alert.first.raised?)
+ assert(!Alert.first.cleared?)
+
+ # Now with the config set to un-acknowledge alerts after only 1 minute,
+ # try winding time on and check that this happens.
+ #
+ Time.advance(2.minutes)
+ with_next_notification do |destination, this_alert, other_alerts|
+ assert_equal("12345@joe_bloggs.email", destination)
+ assert_equal(Alert.first, this_alert)
+ assert_equal('raised', this_alert.update_type, this_alert.inspect)
+ end
+
+ # Check that auto-clearing works four minutes later
+ #
+ Time.advance(5.minutes)
+ assert(Alert.first.cleared?)
+ assert(!Alert.first.raised?)
+
+ # Finally check for a notification that auto-clearing has happened
+ #
+ with_next_notification do |destination, this_alert, other_alerts|
+ assert_equal("12345@joe_bloggs.email", destination)
+ assert_equal(Alert.first, this_alert)
+ assert_equal('cleared', this_alert.update_type, this_alert.inspect)
+ end
+
+ # And see that no further reminders are sent a while later
+ Time.advance(1.day)
+ assert_no_notification
+ end
+
+ def test_one_alert_changes_from_outside
+ # Raise our test alert, wait for it to be processed
+ mauvesend("-o my_source -i alert1 -s \"alert1 summary\" -d \"alert1 detail\" -u \"alert1 subject\"")
+
+ # Check internal representation, external notification
+ #
+ assert(Alert.first.raised?)
+ assert(!Alert.first.cleared?)
+ with_next_notification do |destination, this_alert, other_alerts|
+ assert_equal('raised', this_alert.update_type, this_alert.inspect)
+ end
+
+ # Check we get reminders every hour, and no more
+ #
+ 12.times do
+ Time.advance(1.hour)
+ with_next_notification do |destination, this_alert, other_alerts|
+ assert_equal('raised', this_alert.update_type, this_alert.inspect)
+ end
+ assert_no_notification
+ end
+
+ # Clear the alert, wait for it to be processed
+ mauvesend("-o my_source -i alert1 -c now")
+ assert(!Alert.first.raised?)
+ assert(Alert.first.cleared?)
+ with_next_notification do |destination, this_alert, other_alerts|
+ assert_equal('cleared', this_alert.update_type, this_alert.inspect)
+ end
+
+ # Check we can raise the same alert again
+ Time.advance(1.minute)
+ mauvesend("-o my_source -i alert1 -s \"alert1 summary\" -d \"alert1 detail\" -u \"alert1 subject\" -r now")
+ assert(Alert.first.raised?, Alert.first.inspect)
+ assert(!Alert.first.cleared?, Alert.first.inspect)
+ with_next_notification do |destination, this_alert, other_alerts|
+ assert_equal('raised', this_alert.update_type, this_alert.inspect)
+ end
+ end
+
+ def test_alert_groups
+ # check that this alert is reminded more often than normal
+ mauvesend("-o rare-and-important -i alert1 -s \"rare and important alert\"")
+ assert(Alert.first.raised?)
+ assert(!Alert.first.cleared?)
+
+ 10.times do
+ with_next_notification do |destination, this_alert, other_alerts|
+ assert_equal('raised', this_alert.update_type, this_alert.inspect)
+ assert_equal('12345', destination)
+ Time.advance(10.minutes)
+ end
+ end
+ discard_next_notification
+ end
+
+ def test_future_raising
+ mauvesend("-i heartbeat -c now -r +10m -s \"raise in the future\"")
+ assert(!Alert.first.raised?)
+ assert(Alert.first.cleared?)
+ assert_no_notification
+
+ # Check the future alert goes off
+ #
+ Time.advance(10.minutes)
+ assert(Alert.first.raised?)
+ assert(!Alert.first.cleared?)
+ with_next_notification do |destination, this_alert, other_alerts|
+ assert_equal('raised', this_alert.update_type, this_alert.inspect)
+ end
+
+ # Check that a repeat of the "heartbeat" update clears it, and we get
+ # a notification.
+ #
+ mauvesend("-i heartbeat -c now -r +10m -s \"raise in the future\"")
+ assert(!Alert.first.raised?)
+ assert(Alert.first.cleared?)
+ with_next_notification do |destination, this_alert, other_alerts|
+ assert_equal('cleared', this_alert.update_type, this_alert.inspect)
+ end
+
+ # Check that a re-send of the same clear alert doesn't send another
+ # notification
+ #
+ Time.advance(1.minute)
+ mauvesend("-i heartbeat -c now -r +10m -s \"raise in the future\"")
+ assert(!Alert.first.raised?)
+ assert(Alert.first.cleared?)
+ assert_no_notification
+
+ # Check that a skewed resend doesn't confuse it
+ #
+ mauvesend("-i heartbeat -c +1m -r +11m -s \"raise in the future\"")
+ assert(!Alert.first.raised?)
+ assert(Alert.first.cleared?)
+ Time.advance(1.minute)
+ assert(!Alert.first.raised?)
+ assert(Alert.first.cleared?)
+ assert_no_notification
+ end
+
+ # Make sure that using the "replace all flag" works as expected.
+ #
+ def test_replace_flag
+ mauvesend("-p")
+ #mauvesend("-p")
+ assert_no_notification
+
+ mauvesend("-i test1 -s\"\test1\"")
+ assert(Alert.first.raised?)
+ with_next_notification do |destination, this_alert, other_alerts|
+ assert_equal('raised', this_alert.update_type, this_alert.inspect)
+ end
+ assert_no_notification
+
+ mauvesend("-p")
+ #mauvesend("-p")
+ with_next_notification do |destination, this_alert, other_alerts|
+ assert_equal('cleared', this_alert.update_type, this_alert.inspect)
+ end
+ assert_no_notification
+ end
+
+ def test_earliest_date
+ alert = Alert.create!(
+ :alert_id => "test_id",
+ :source => "test1",
+ :subject => "test subject",
+ :summary => "test summary",
+ :raised_at => nil,
+ :will_raise_at => Time.now + 60,
+ :will_clear_at => Time.now + 120,
+ :update_type => "cleared",
+ :updated_at => Time.now
+ )
+ assert(alert)
+
+ assert(AlertEarliestDate.first.alert == alert)
+ end
+
+end
+
+
+
+
diff --git a/test/mauve_test_helper.rb b/test/mauve_test_helper.rb
new file mode 100644
index 0000000..8653bd5
--- /dev/null
+++ b/test/mauve_test_helper.rb
@@ -0,0 +1,104 @@
+require 'tmpdir'
+require 'thread'
+require 'timeout'
+require 'mauve/configuration'
+
+Thread.abort_on_exception = true
+
+module MauveTestHelper
+ include Mauve
+ Notifications = Queue.new
+
+ # Returns the base directory for temporary files for this test instance
+ #
+ def dir
+ if !@test_dir
+ now = ::Time.now
+ base=Dir.tmpdir+"/mauve_test"
+ Dir.mkdir(base) unless File.directory?(base)
+ base=base+"/#{$$}"
+ Dir.mkdir(base) unless File.directory?(base)
+ Dir.mkdir(@test_dir="#{base}/#{name}")
+ end
+ @test_dir
+ end
+
+ # Starts the Mauve server with a configuration supplied as a string.
+ #
+ def start_server(config)
+ @here = File.expand_path(__FILE__).split("/")[0..-2].join("/") + "/.."
+ File.open("#{dir}/config_file", "w") { |fh| fh.write(config) }
+ Notifications.clear
+
+ Configuration.current = ConfigurationBuilder.load("#{dir}/config_file")
+ Time.reset_to_midnight
+ @thread = Thread.new do
+ begin
+ Configuration.current.server.run
+ rescue Interrupt
+ Configuration.current.close
+ end
+ end
+ # avoids races if we try to shut down too quickly
+ Configuration.current.server.sleep_until_ready
+ Log.info "TEST RUN STARTED: #{name}"
+ end
+
+ # Stops the Mauve server, should reset it ready to start again within the
+ # same process.
+ #
+ def stop_server
+ @thread.raise(Interrupt.new)
+ @thread.join
+ end
+
+ # Send an alert to the server, return when the server process has definitely
+ # processed it (or die after 2s).
+ #
+ def mauvesend(cmd)
+ Configuration.current.server.sleep_until_ready
+ output = `TEST_TIME=#{Time.now.to_i} #{@here}/mauve_starter.rb #{@here}/bin/mauvesend -v 127.0.0.1:44444 #{cmd} 2>&1`
+ status = $?.exitstatus
+ raise "Exit #{status} from command: '#{output}'" unless status == 0
+ raise "mauvesend did not return an integer" unless output.to_i > 0
+ begin
+ timeout(2) { Configuration.current.server.sleep_until_transmission_id_received(output.to_i) }
+ rescue Timeout::Error
+ flunk("Did not receive transmission id '#{output}'")
+ end
+ end
+
+ # Assuming the test configuration contains a notification method with
+ # "deliver_to_queue TestClass::Notifications", this helper will return the next
+ # alert notification by that method as a triplet:
+ #
+ # [destination, alert, other_alerts]
+ #
+ # e.g. destination will be an email address, phone number or xmpp ID, just
+ # as in the configuration file. alert will be the subject of this
+ # alert, and other_alerts will be the other notifications that
+ # are relevant for this person at this time.
+ #
+ # The test will fail after 2s if no alert is received.
+ #
+ def with_next_notification
+ Timers.restart_and_then_wait_until_idle
+ flunk("Nothing on Notifications queue when I expected one") if Notifications.empty?
+ yield(*Notifications.pop)
+ end
+
+ def discard_next_notification
+ with_next_notification { }
+ end
+
+ # The reverse of next_alert, the test fails if an alert is received
+ # within 2s.
+ #
+ def assert_no_notification
+ Timers.restart_and_then_wait_until_idle
+ flunk("#{Notifications.pop.inspect} on Notifications queue when I expected nothing") unless
+ Notifications.empty?
+ true
+ end
+end
+
diff --git a/test/mauve_time.rb b/test/mauve_time.rb
new file mode 100644
index 0000000..d8c57a7
--- /dev/null
+++ b/test/mauve_time.rb
@@ -0,0 +1,38 @@
+require 'logger'
+require 'time'
+
+module Mauve
+ # A fake Time, which we use in testing. Time#now returns the same value every
+ # time, unless we call Time#advance which alters the value of 'now' by a
+ # given number of seconds. There is a simple pass-through for other methods.
+ #
+ class Time
+ class << self
+ def reset_to_midnight
+ @now = Time.parse("00:00")
+ Log.debug "Test time reset to #{@now}"
+ end
+
+ def now
+ reset_to_midnight unless @now
+ @now
+ end
+
+ def advance(seconds)
+ @now += seconds
+ Log.debug "Test time advanced by #{seconds} to #{@now}, kicking Mauve::Timers"
+ Timers.restart_and_then_wait_until_idle
+ @now
+ end
+
+ def at(*a)
+ ::Time.at(*a)
+ end
+
+ def parse(*a)
+ ::Time.parse(*a)
+ end
+ end
+ end
+end
+
diff --git a/test/notification.rb b/test/notification.rb
new file mode 100644
index 0000000..9cc3306
--- /dev/null
+++ b/test/notification.rb
@@ -0,0 +1,57 @@
+$: << "../lib/"
+
+require 'test/unit'
+require 'mauve/notification'
+
+# Test changes to notification things.
+class MauveNotificationTest < Test::Unit::TestCase
+
+ def test_x_in_list_of_y
+ mdr = Mauve::DuringRunner.new(Time.now)
+ [
+ [[0,1,3,4], 2, false],
+ [[0,2,4,6], 2, true],
+ [[0..1,3..6],2, false],
+ [[0..2, 4,5],2, true],
+ [[0,1..3], 2, true],
+ ].each do |y,x,result|
+ assert_equal(result, mdr.send(:x_in_list_of_y, x,y))
+ end
+ end
+
+ def test_hours_in_day
+ t = Time.gm(2010,1,2,3,4,5)
+ # => Sat Jan 02 03:04:05 UTC 2010
+ mdr = Mauve::DuringRunner.new(t)
+ [
+ [[0,1,3,4], true],
+ [[0,2,4,6], false],
+ [[[0,1,3],4], true],
+ [[[0,2,4],6], false],
+ [[0..1,3..6], true],
+ [[0..2, 4,5], false],
+ [[0,1..3], true],
+ [[4..12], false]
+ ].each do |hours, result|
+ assert_equal(result, mdr.send(:hours_in_day, hours))
+ end
+ end
+
+ def test_days_in_week
+ t = Time.gm(2010,1,2,3,4,5)
+ # => Sat Jan 02 03:04:05 UTC 2010
+ mdr = Mauve::DuringRunner.new(t)
+ [
+ [[0,1,3,4], false],
+ [[0,2,4,6], true],
+ [[[0,1,3],4], false],
+ [[[0,2,4],6], true],
+ [[0..1,3..6], true],
+ [[0..2, 4,5], false],
+ [[0,1..3], false],
+ [[4..6], true]
+ ].each do |days, result|
+ assert_equal(result, mdr.send(:days_in_week, days), "#{t.wday} in #{days.join(", ")}")
+ end
+ end
+end