Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions Common/Notifications/Notification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,10 @@ public class NotificationEmail : Notification
public Dictionary<string, string> Headers { get; set; }

/// <summary>
/// Send to address:
/// Send to address. If null, the email is sent to all members in the project.
/// Always serialized, even when null: the cloud API requires the key to be present
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Include)]
public string Address { get; set; }

/// <summary>
Expand All @@ -138,20 +140,31 @@ public class NotificationEmail : Notification
/// <summary>
/// Default constructor for sending an email notification
/// </summary>
/// <param name="address">Address to send to, if null will default to users email. Will throw <see cref="ArgumentException"/> if invalid
/// <param name="address">Address to send to. If null, the email is sent to all members in the project.
/// Will throw <see cref="ArgumentException"/> if invalid
/// <see cref="Validate.EmailAddress"/></param>
/// <param name="subject">Subject of the email. Will set to <see cref="string.Empty"/> if null</param>
/// <param name="message">Message body of the email. Will set to <see cref="string.Empty"/> if null</param>
/// <param name="data">Data to attach to the email. Will set to <see cref="string.Empty"/> if null</param>
/// <param name="headers">Optional email headers to use</param>
public NotificationEmail(string address, string subject = "", string message = "", string data = "", Dictionary<string, string> headers = null)
{
if (!Validate.EmailAddress(address))
if (address == null)
{
throw new ArgumentException(Messages.NotificationEmail.InvalidEmailAddress(address));
// sent to all members in the project
Address = null;
}
else
{
address = address.Trim();
if (!Validate.EmailAddress(address))
{
throw new ArgumentException(Messages.NotificationEmail.InvalidEmailAddress(address));
}

Address = address;
}

Address = address;
Data = data ?? string.Empty;
Message = message ?? string.Empty;
Subject = subject ?? string.Empty;
Expand Down
5 changes: 4 additions & 1 deletion Common/Notifications/NotificationJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
var address = jObject.GetValue("Address", StringComparison.InvariantCultureIgnoreCase);
var headers= jObject.GetValue("Headers", StringComparison.InvariantCultureIgnoreCase);

return new NotificationEmail(address?.ToString(), token.ToString(), message?.ToString(), data?.ToString(), headers?.ToObject<Dictionary<string, string>>());
// a null address means the email is sent to all members in the project
var addressValue = address == null || address.Type == JTokenType.Null ? null : address.ToString();

return new NotificationEmail(addressValue, token.ToString(), message?.ToString(), data?.ToString(), headers?.ToObject<Dictionary<string, string>>());
}
else if (jObject.TryGetValue("Address", StringComparison.InvariantCultureIgnoreCase, out token))
{
Expand Down
6 changes: 4 additions & 2 deletions Common/Notifications/NotificationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ public NotificationManager(bool liveMode)

/// <summary>
/// Send an email to the address specified for live trading notifications.
/// If the address is null, the email is sent to all members in the project.
/// </summary>
/// <param name="subject">Subject of the email</param>
/// <param name="message">Message body, up to 10kb</param>
/// <param name="data">Data attachment (optional)</param>
/// <param name="address">Email address to send to, if null will default to users email</param>
/// <param name="address">Email address to send to. If null, the email is sent to all members in the project</param>
/// <param name="headers">Optional email headers to use</param>
public bool Email(string address, string subject, string message, string data, PyObject headers)
{
Expand All @@ -55,11 +56,12 @@ public bool Email(string address, string subject, string message, string data, P

/// <summary>
/// Send an email to the address specified for live trading notifications.
/// If the address is null, the email is sent to all members in the project.
/// </summary>
/// <param name="subject">Subject of the email</param>
/// <param name="message">Message body, up to 10kb</param>
/// <param name="data">Data attachment (optional)</param>
/// <param name="address">Email address to send to, if null will default to users email</param>
/// <param name="address">Email address to send to. If null, the email is sent to all members in the project</param>
/// <param name="headers">Optional email headers to use</param>
public bool Email(string address, string subject, string message, string data = "", Dictionary<string, string> headers = null)
{
Expand Down
95 changes: 95 additions & 0 deletions Tests/Common/Notifications/NotificationEmailLiveTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using QuantConnect.Configuration;
using QuantConnect.Notifications;
using QuantConnect.Tests.API;

namespace QuantConnect.Tests.Common.Notifications
{
/// <summary>
/// Live test: enqueues an email through Notify.Email with a null address and sends it
/// through the QuantConnect cloud API to verify it is emailed to all members in the project.
/// Requires "job-user-id", "api-access-token" and "notifications-endpoint" in the config.
/// </summary>
[TestFixture, Explicit("Sends a real email through the QuantConnect cloud API")]
public class NotificationEmailLiveTests
{
// Create a project on QC Cloud with more than one member and set its ID here:
// every member of the project should receive the test email
private const int ProjectId = 34485046;

[Test]
public void NotifyEmailWithNullAddress_SendsToAllProjectMembers()
{
// enqueue the email like an algorithm would: self.notify.email(None, ...)
var notify = new NotificationManager(liveMode: true);
Assert.IsTrue(notify.Email(null, $"Notify.Email All Members Test - Project {ProjectId}",
$"Sent by NotificationEmailLiveTests at {DateTime.UtcNow:u} with a null address."));

// dequeue and send, like the live result handler hands notifications to the messaging handler
Assert.IsTrue(notify.Messages.TryDequeue(out var notification));
var email = notification as NotificationEmail;
Assert.IsNotNull(email);
Assert.IsNull(email.Address);

Assert.IsTrue(Send(notification));
}

private static bool Send(Notification notification)
{
ApiTestBase.ReloadConfiguration();
var endpoint = Config.Get("notifications-endpoint");
if (string.IsNullOrEmpty(endpoint))
{
Assert.Ignore("The 'notifications-endpoint' configuration is not set");
}
var userId = Globals.UserId.ToStringInvariant();
var apiToken = Globals.UserToken;

var serialized = JsonConvert.SerializeObject(notification,
new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });
StringAssert.Contains("\"Address\":null", serialized);

var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant();
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes($"{apiToken}:{timestamp}")))
.ToLowerInvariant();

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Timestamp", timestamp);
client.DefaultRequestHeaders.Add("Authorization",
"Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{userId}:{hash}")));

using var response = client.PostAsync(endpoint,
new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "projectId", ProjectId.ToStringInvariant() },
{ "notification", serialized }
})).Result;
var body = response.Content.ReadAsStringAsync().Result;

Assert.IsTrue(response.IsSuccessStatusCode, body);
return JObject.Parse(body).Value<bool>("success");
}
}
}
24 changes: 24 additions & 0 deletions Tests/Common/Notifications/NotificationEmailTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ public void Constructor_SetsNullMessage_ToEmptyString()
Assert.AreEqual(string.Empty, email.Message);
}

[Test]
public void Constructor_SetsNullAddress_ToNull_ToSendToAllProjectMembers()
{
// a null address means the email will be sent to all members in the project
var email = new NotificationEmail(null, "subject", "message", "data");
Assert.IsNull(email.Address);
}

[TestCase("")]
[TestCase(" ")]
public void Constructor_ThrowsArgumentException_WhenAddressIsEmpty(string address)
{
// only a null address is sent to all members in the project
Assert.Throws<ArgumentException>(() => new NotificationEmail(address, "subject", "message", "data"));
}

[TestCase("jones@proseware.com,david.jones@proseware.com")]
[TestCase("jones@proseware.com, david.jones@proseware.com")]
public void Constructor_ThrowsArgumentException_WhenAddressIsCommaSeparated(string address)
{
// multiple addresses are not supported
Assert.Throws<ArgumentException>(() => new NotificationEmail(address, "subject", "message", "data"));
}

[Test]
[TestCase("js@contoso.中国", true)]
[TestCase("j@proseware.com9", true)]
Expand Down
18 changes: 18 additions & 0 deletions Tests/Common/Notifications/NotificationJsonConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ public void EmailRoundTrip(bool nullFields)
Assert.AreEqual(expected.Headers, result.Headers);
}

[Test]
public void EmailWithoutAddressRoundTrip()
{
// a null address means the email will be sent to all members in the project
var expected = new NotificationEmail(null, "subjectP", "message", "dataContent");

// the cloud API requires the Address key to be present, null means all members in the project
var serialized = JsonConvert.SerializeObject(expected);
StringAssert.Contains("\"Address\":null", serialized);

var result = (NotificationEmail)JsonConvert.DeserializeObject<Notification>(serialized);

Assert.IsNull(result.Address);
Assert.AreEqual(expected.Subject, result.Subject);
Assert.AreEqual(expected.Data, result.Data);
Assert.AreEqual(expected.Message, result.Message);
}

[TestCase(true)]
[TestCase(false)]
public void SmsRoundTrip(bool nullFields)
Expand Down
39 changes: 39 additions & 0 deletions Tests/Common/Notifications/NotificationManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ public void Email_AddsNotificationEmail_ToMessages_WhenLiveModeIsTrue()
}
}

[Test]
public void Email_WithoutAddress_AddsNotificationEmail_ForAllProjectMembers()
{
Assert.AreEqual(_liveMode, _notify.Email(null, "subject", "message", "data"));
Assert.AreEqual(_liveMode ? 1 : 0, _notify.Messages.Count);
if (_liveMode)
{
var email = _notify.Messages.Single() as NotificationEmail;
Assert.IsNotNull(email);
Assert.IsNull(email.Address);
}
}

[Test]
public void Sms_AddsNotificationSms_ToMessages_WhenLiveModeIsTrue()
{
Expand Down Expand Up @@ -106,6 +119,32 @@ public void FtpAddsNotificationToMessagesWhenLiveModeIsTrueFromStringContents()
}
}

[Test]
public void PythonEmailWithNoneAddress_SendsToAllProjectMembers()
{
using (Py.GIL())
{
var test = PyModule.FromString("testModule",
@"
from AlgorithmImports import *

def email_none(notifier):
return notifier.email(None, 'subject', 'message', 'data')");

dynamic function = test.GetAttr("email_none");
bool result = function(_notify);

Assert.AreEqual(_liveMode, result);
Assert.AreEqual(_liveMode ? 1 : 0, _notify.Messages.Count);
if (_liveMode)
{
var email = _notify.Messages.Single() as NotificationEmail;
Assert.IsNotNull(email);
Assert.IsNull(email.Address);
}
}
}

[TestCase("email")]
[TestCase("web")]
public void PythonOverloads(string notificationType)
Expand Down
Loading