public void Cache_hit_and_expire()
        {
            Plug            deki         = Plug.New("http://mock/deki");
            AutoMockPlug    autoMock     = MockPlug.Register(deki.Uri);
            Action          expire       = null;
            PageChangeCache cache        = new PageChangeCache(deki, (key, trigger) => expire = trigger);
            DateTime        timestamp    = DateTime.UtcNow;
            XUri            pageUri      = deki.Uri.At("pages", "10").With("redirects", "0");
            XDoc            pageResponse = new XDoc("page")
                                           .Elem("uri.ui", "http://foo.com/@api/deki/pages/10")
                                           .Elem("title", "foo")
                                           .Elem("path", "foo/bar");
            XUri feedUri = deki.Uri
                           .At("pages", "10", "feed")
                           .With("redirects", "0")
                           .With("format", "raw")
                           .With("since", timestamp.Subtract(TimeSpan.FromSeconds(10)).ToString("yyyyMMddHHmmss"));
            XDoc changeResponse = new XDoc("table")
                                  .Start("change")
                                  .Elem("rc_summary", "Two edits")
                                  .Elem("rc_comment", "edit 1")
                                  .Elem("rc_comment", "edit 2")
                                  .End();

            _log.Debug("first get");
            autoMock.Expect("GET", pageUri, (XDoc)null, DreamMessage.Ok(pageResponse));
            autoMock.Expect("GET", feedUri, (XDoc)null, DreamMessage.Ok(changeResponse));
            Assert.IsNotNull(Coroutine.Invoke(cache.GetPageData, (uint)10, "foo", timestamp, CultureInfo.InvariantCulture, (string)null, new Result <PageChangeData>()).Wait());
            Assert.IsTrue(autoMock.WaitAndVerify(10.Seconds()));

            _log.Debug("second get, cache hit");
            autoMock.Reset();
            Assert.IsNotNull(Coroutine.Invoke(cache.GetPageData, (uint)10, "foo", timestamp, CultureInfo.InvariantCulture, (string)null, new Result <PageChangeData>()).Wait());
            Assert.IsTrue(autoMock.WaitAndVerify(2.Seconds()));

            _log.Debug("third get, cache miss");
            autoMock.Reset();
            autoMock.Expect("GET", pageUri, (XDoc)null, DreamMessage.Ok(pageResponse));
            autoMock.Expect("GET", feedUri, (XDoc)null, DreamMessage.Ok(changeResponse));
            expire();
            Assert.IsNotNull(Coroutine.Invoke(cache.GetPageData, (uint)10, "foo", timestamp, CultureInfo.InvariantCulture, (string)null, new Result <PageChangeData>()).Wait());
            Assert.IsTrue(autoMock.WaitAndVerify(10.Seconds()));
        }
        public void Get_page_data_in_user_timezone()
        {
            Plug            deki         = Plug.New("http://mock/deki");
            AutoMockPlug    autoMock     = MockPlug.Register(deki.Uri);
            PageChangeCache cache        = new PageChangeCache(deki, (key, trigger) => { });
            DateTime        timestamp    = DateTime.Parse("2009/02/01 10:10:00");
            XUri            pageUri      = deki.Uri.At("pages", "10").With("redirects", "0");
            XDoc            pageResponse = new XDoc("page")
                                           .Elem("uri.ui", "http://foo.com/@api/deki/pages/10")
                                           .Elem("title", "foo")
                                           .Elem("path", "foo/bar");
            XUri feedUri = deki.Uri
                           .At("pages", "10", "feed")
                           .With("redirects", "0")
                           .With("format", "raw")
                           .With("since", timestamp.Subtract(TimeSpan.FromSeconds(10)).ToString("yyyyMMddHHmmss"));
            XDoc changeResponse = new XDoc("table")
                                  .Start("change")
                                  .Elem("rc_summary", "Two edits")
                                  .Elem("rc_comment", "edit 1")
                                  .Elem("rc_comment", "edit 2")
                                  .Elem("rc_timestamp", "20090201101000")
                                  .End();

            _log.Debug("first get");
            autoMock.Expect("GET", pageUri, (XDoc)null, DreamMessage.Ok(pageResponse));
            autoMock.Expect("GET", feedUri, (XDoc)null, DreamMessage.Ok(changeResponse));
            PageChangeData data = Coroutine.Invoke(cache.GetPageData, (uint)10, "foo", timestamp, CultureInfo.InvariantCulture, "-09:00", new Result <PageChangeData>()).Wait();

            Assert.IsTrue(autoMock.WaitAndVerify(TimeSpan.FromSeconds(10)));

            //string plainbody = "foo\r\n[ http://foo.com/@api/deki/pages/10 ]\r\n\r\n - edit 1 (Sun, 01 Feb 2009 01:10:00 -09:00)\r\n   [ http://foo.com/@api/deki/pages/10?revision ]\r\n\r\n";
            XDoc htmlBody = XDocFactory.From("<html><p><b><a href=\"http://foo.com/@api/deki/pages/10\">foo</a></b> ( Last edited by <a href=\"http://foo.com/User%3A\" /> )<br /><small><a href=\"http://foo.com/@api/deki/pages/10\">http://foo.com/@api/deki/pages/10</a></small><br /><small><a href=\"http://foo.com/index.php?title=Special%3APageAlerts&amp;id=10\">Unsubscribe</a></small></p><p><ol><li>edit 1 ( <a href=\"http://foo.com/@api/deki/pages/10?revision\">Sun, 01 Feb 2009 01:10:00 -09:00</a> by <a href=\"http://foo.com/User%3A\" /> )</li></ol></p><br /></html>", MimeType.TEXT_XML);

            Assert.AreEqual(htmlBody.ToString(), data.HtmlBody.ToString());
        }
        //--- Methods ---
        protected override Yield Start(XDoc config, IContainer container, Result result)
        {
            yield return(Coroutine.Invoke(base.Start, config, new Result()));

            // set up plug for phpscript that will handle the notifications
            _emailer = Plug.New(config["uri.emailer"].AsUri);

            // set up plug deki, so we can validate users
            _deki = Plug.New(config["uri.deki"].AsUri);

            // get the apikey, which we will need as a subscription auth token for subscriptions not done on behalf of a user
            _apikey = config["apikey"].AsText;
            _cache  = new PageChangeCache(_deki.With("apikey", _apikey), TimeSpan.FromSeconds(config["page-cache-ttl"].AsInt ?? 2));

            if (!container.IsRegistered <IPageSubscriptionInstance>())
            {
                var builder = new ContainerBuilder();
                builder.Register <PageSubscriptionInstance>().As <IPageSubscriptionInstance>().FactoryScoped();
                builder.Build(container);
            }

            // TODO (arnec): this should be hitting the API to retrieve resources

            // resource manager for email template
            var resourcePath = Config["resources-path"].AsText;

            if (!string.IsNullOrEmpty(resourcePath))
            {
                _resourceManager = new PlainTextResourceManager(Environment.ExpandEnvironmentVariables(resourcePath));
            }
            else
            {
                // creating a test resource manager
                _log.WarnFormat("'resource-path' was not defined in Config, using a test resource manager for email templating");
                var testSet = new TestResourceSet {
                    { "Notification.Page.email-subject", "Page Modified" },
                    { "Notification.Page.email-header", "The following pages have changed:" }
                };
                _resourceManager = new PlainTextResourceManager(testSet);
            }

            // set up subscription for pubsub
            var subscriptionSet = new XDoc("subscription-set")
                                  .Elem("uri.owner", Self.Uri.AsServerUri().ToString())
                                  .Start("subscription")
                                  .Elem("channel", "event://*/deki/users/*")
                                  .Add(DreamCookie.NewSetCookie("service-key", InternalAccessKey, Self.Uri).AsSetCookieDocument)
                                  .Start("recipient")
                                  .Attr("authtoken", _apikey)
                                  .Elem("uri", Self.Uri.AsServerUri().At("updateuser").ToString())
                                  .End()
                                  .End()
                                  .Start("subscription")
                                  .Elem("channel", "event://*/deki/pages/create")
                                  .Elem("channel", "event://*/deki/pages/update")
                                  .Elem("channel", "event://*/deki/pages/delete")
                                  .Elem("channel", "event://*/deki/pages/revert")
                                  .Elem("channel", "event://*/deki/pages/move")
                                  .Elem("channel", "event://*/deki/pages/tags/update")
                                  .Elem("channel", "event://*/deki/pages/dependentschanged/comments/create")
                                  .Elem("channel", "event://*/deki/pages/dependentschanged/comments/update")
                                  .Elem("channel", "event://*/deki/pages/dependentschanged/comments/delete")
                                  .Elem("channel", "event://*/deki/pages/dependentschanged/files/create")
                                  .Elem("channel", "event://*/deki/pages/dependentschanged/files/update")
                                  .Elem("channel", "event://*/deki/pages/dependentschanged/files/delete")
                                  .Elem("channel", "event://*/deki/pages/dependentschanged/files/properties/*")
                                  .Elem("channel", "event://*/deki/pages/dependentschanged/files/restore")
                                  .Add(DreamCookie.NewSetCookie("service-key", InternalAccessKey, Self.Uri).AsSetCookieDocument)
                                  .Start("recipient")
                                  .Attr("authtoken", _apikey)
                                  .Elem("uri", Self.Uri.AsServerUri().At("notify").ToString())
                                  .End()
                                  .End();
            Result <DreamMessage> subscribe;

            yield return(subscribe = PubSub.At("subscribers").PostAsync(subscriptionSet));

            string accessKey = subscribe.Value.ToDocument()["access-key"].AsText;
            XUri   location  = subscribe.Value.Headers.Location;

            Cookies.Update(DreamCookie.NewSetCookie("access-key", accessKey, location), null);
            _subscriptionLocation = Plug.New(location.AsLocalUri().WithoutQuery());
            _log.DebugFormat("set up initial subscription location at {0}", _subscriptionLocation.Uri);

            // set up notification accumulator queue
            TimeSpan accumulationMinutes = TimeSpan.FromSeconds(config["accumulation-time"].AsInt ?? 10 * 60);

            _log.DebugFormat("Initializing queue with {0:0.00} minute accumulation", accumulationMinutes.TotalMinutes);
            _notificationQueue = new NotificationDelayQueue(accumulationMinutes, SendEmail);
            result.Return();
        }