From 442bd60b74f81eafeeb395fcefe747e9f7938415 Mon Sep 17 00:00:00 2001 From: cocoa-dev004 <66989461+cocoa-dev004@users.noreply.github.com> Date: Wed, 8 Jun 2022 14:04:31 +0900 Subject: [PATCH 1/5] Add event log function --- .../Covid19Radar.Android.csproj | 1 + .../Covid19Radar.Android/MainApplication.cs | 19 +- .../EventLogSubmissionBackgroundService.cs | 107 +++++++ Covid19Radar/Covid19Radar.iOS/AppDelegate.cs | 21 +- .../Covid19Radar.iOS/Covid19Radar.iOS.csproj | 1 + Covid19Radar/Covid19Radar.iOS/Info.plist | 1 + .../EventLogSubmissionBackgroundService.cs | 105 +++++++ .../Covid19Radar/Common/AppConstants.cs | 10 + .../Common/DeviceVerifierUtils.cs | 6 +- .../Covid19Radar/Model/V1EventLogRequest.cs | 42 +-- .../Repository/EventLogRepository.cs | 292 ++++++++++++++++++ .../Repository/SendEventLogStateRepository.cs | 158 ++++++++++ .../Repository/UserDataRepository.cs | 36 --- .../AbsEventLogSubmissionBackgroundService.cs | 11 + .../Covid19Radar/Services/EventLogService.cs | 218 ++++++------- .../Services/EventLogServiceNop.cs | 35 +-- .../Services/ExposureDetectionService.cs | 131 ++------ .../Covid19Radar/Services/HttpDataService.cs | 23 ++ .../Services/HttpDataServiceMock.cs | 8 + .../Covid19Radar/Services/IHttpDataService.cs | 2 + .../Services/ILocalPathService.cs | 4 + ...entlog_submission_parameter1-cleartext.txt | 2 +- .../Services/ExposureDetectionServiceTests.cs | 14 +- .../Services/HttpDataServiceTests.cs | 87 +++++- 24 files changed, 989 insertions(+), 345 deletions(-) create mode 100644 Covid19Radar/Covid19Radar.Android/Services/EventLogSubmissionBackgroundService.cs create mode 100644 Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs create mode 100644 Covid19Radar/Covid19Radar/Repository/EventLogRepository.cs create mode 100644 Covid19Radar/Covid19Radar/Repository/SendEventLogStateRepository.cs create mode 100644 Covid19Radar/Covid19Radar/Services/AbsEventLogSubmissionBackgroundService.cs diff --git a/Covid19Radar/Covid19Radar.Android/Covid19Radar.Android.csproj b/Covid19Radar/Covid19Radar.Android/Covid19Radar.Android.csproj index 8c2e65477..2cba60edc 100644 --- a/Covid19Radar/Covid19Radar.Android/Covid19Radar.Android.csproj +++ b/Covid19Radar/Covid19Radar.Android/Covid19Radar.Android.csproj @@ -191,6 +191,7 @@ + diff --git a/Covid19Radar/Covid19Radar.Android/MainApplication.cs b/Covid19Radar/Covid19Radar.Android/MainApplication.cs index 32202a76b..0c357e822 100644 --- a/Covid19Radar/Covid19Radar.Android/MainApplication.cs +++ b/Covid19Radar/Covid19Radar.Android/MainApplication.cs @@ -45,6 +45,9 @@ private Lazy _exposureDetectionService private Lazy _exposureDetectionBackgroundService = new Lazy(() => ContainerLocator.Current.Resolve()); + private readonly Lazy _eventLogSubmissionBackgroundService + = new Lazy(() => ContainerLocator.Current.Resolve()); + private Lazy _loggerService = new Lazy(() => ContainerLocator.Current.Resolve()); @@ -79,13 +82,26 @@ public override void OnCreate() SetupENClient(exposureNotificationApiService.Client); } + ScheduleBackgroundTasks(); + } + + private void ScheduleBackgroundTasks() + { try { _exposureDetectionBackgroundService.Value.Schedule(); } catch (Exception exception) { - _loggerService.Value.Exception("failed to Scheduling", exception); + _loggerService.Value.Exception("failed to Scheduling ExposureDetectionBackgroundService", exception); + } + try + { + _eventLogSubmissionBackgroundService.Value.Schedule(); + } + catch (Exception exception) + { + _loggerService.Value.Exception("failed to Scheduling EventLogSubmissionBackgroundService", exception); } } @@ -121,6 +137,7 @@ private void RegisterPlatformTypes(IContainer container) container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); + container.Register(Reuse.Singleton); } public Task GetExposureConfigurationAsync() diff --git a/Covid19Radar/Covid19Radar.Android/Services/EventLogSubmissionBackgroundService.cs b/Covid19Radar/Covid19Radar.Android/Services/EventLogSubmissionBackgroundService.cs new file mode 100644 index 000000000..1c5172226 --- /dev/null +++ b/Covid19Radar/Covid19Radar.Android/Services/EventLogSubmissionBackgroundService.cs @@ -0,0 +1,107 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using Android.Content; +using Android.Runtime; +using AndroidX.Work; +using Covid19Radar.Common; +using Covid19Radar.Services; +using Covid19Radar.Services.Logs; +using Java.Util.Concurrent; +using Prism.Ioc; +using Xamarin.Essentials; + +namespace Covid19Radar.Droid.Services +{ + public class EventLogSubmissionBackgroundService : AbsEventLogSubmissionBackgroundService + { + private const string CURRENT_WORK_NAME = "eventlog_submission_worker_20220112"; + + private const long INTERVAL_IN_HOURS = 24; + private const long BACKOFF_DELAY_IN_MINUTES = 60; + + private readonly ILoggerService _loggerService; + + public EventLogSubmissionBackgroundService( + ILoggerService loggerService + ) : base() + { + _loggerService = loggerService; + } + + public override void Schedule() + { + _loggerService.StartMethod(); + + WorkManager workManager = WorkManager.GetInstance(Platform.AppContext); + + PeriodicWorkRequest periodicWorkRequest = CreatePeriodicWorkRequest(); + workManager.EnqueueUniquePeriodicWork( + CURRENT_WORK_NAME, + ExistingPeriodicWorkPolicy.Replace, + periodicWorkRequest + ); + + _loggerService.EndMethod(); + } + + private static PeriodicWorkRequest CreatePeriodicWorkRequest() + { + var workRequestBuilder = new PeriodicWorkRequest.Builder( + typeof(BackgroundWorker), + INTERVAL_IN_HOURS, TimeUnit.Hours + ) + .SetConstraints(new Constraints.Builder() + .SetRequiresBatteryNotLow(true) + .SetRequiredNetworkType(NetworkType.Connected) + .Build()) + .SetBackoffCriteria(BackoffPolicy.Linear, BACKOFF_DELAY_IN_MINUTES, TimeUnit.Minutes); + return workRequestBuilder.Build(); + } + + [Preserve] + public class BackgroundWorker : Worker + { + private Lazy _loggerService => new Lazy(() => ContainerLocator.Current.Resolve()); + private Lazy _eventLogService => new Lazy(() => ContainerLocator.Current.Resolve()); + + public BackgroundWorker(Context context, WorkerParameters workerParams) : base(context, workerParams) + { + // do nothing + } + + public override Result DoWork() + { + var loggerService = _loggerService.Value; + var eventLogService = _eventLogService.Value; + + loggerService.StartMethod(); + + try + { + eventLogService.SendAllAsync(AppConstants.EventLogMaxRequestSizeInBytes, AppConstants.EventLogMaxRetry); + return Result.InvokeSuccess(); + } + catch (Exception exception) + { + loggerService.Exception("Exception occurred, SendAllAsync", exception); + return Result.InvokeFailure(); + } + finally + { + loggerService.EndMethod(); + } + } + + public override void OnStopped() + { + base.OnStopped(); + + _loggerService.Value.Warning("OnStopped"); + } + } + } +} + diff --git a/Covid19Radar/Covid19Radar.iOS/AppDelegate.cs b/Covid19Radar/Covid19Radar.iOS/AppDelegate.cs index 62c0f7ba7..c4fabdbb5 100644 --- a/Covid19Radar/Covid19Radar.iOS/AppDelegate.cs +++ b/Covid19Radar/Covid19Radar.iOS/AppDelegate.cs @@ -41,6 +41,9 @@ private Lazy _exposureNotificationClient private Lazy _exposureDetectionBackgroundService = new Lazy(() => ContainerLocator.Current.Resolve()); + private readonly Lazy _eventLogSubmissionBackgroundService + = new Lazy(() => ContainerLocator.Current.Resolve()); + private Lazy _exposureDetectionService = new Lazy(() => ContainerLocator.Current.Resolve()); @@ -109,6 +112,13 @@ public override bool FinishedLaunching(UIApplication app, NSDictionary launchOpt UIApplication.SharedApplication.SetMinimumBackgroundFetchInterval(UIApplication.BackgroundFetchIntervalMinimum); + ScheduleBackgroundTask(); + + return base.FinishedLaunching(app, launchOptions); + } + + private void ScheduleBackgroundTask() + { try { _exposureDetectionBackgroundService.Value.Schedule(); @@ -117,8 +127,14 @@ public override bool FinishedLaunching(UIApplication app, NSDictionary launchOpt { _loggerService.Value.Exception("failed to Scheduling", exception); } - - return base.FinishedLaunching(app, launchOptions); + try + { + _eventLogSubmissionBackgroundService.Value.Schedule(); + } + catch (Exception exception) + { + _loggerService.Value.Exception("failed to Scheduling EventLogSubmissionBackgroundService", exception); + } } private bool IsUniversalLinks(NSDictionary launchOptions) @@ -257,6 +273,7 @@ private void RegisterPlatformTypes(IContainer container) container.Register(Reuse.Singleton); #endif container.Register(Reuse.Singleton); + container.Register(Reuse.Singleton); } public Task GetExposureConfigurationAsync() diff --git a/Covid19Radar/Covid19Radar.iOS/Covid19Radar.iOS.csproj b/Covid19Radar/Covid19Radar.iOS/Covid19Radar.iOS.csproj index e3ac0a6c3..013036399 100644 --- a/Covid19Radar/Covid19Radar.iOS/Covid19Radar.iOS.csproj +++ b/Covid19Radar/Covid19Radar.iOS/Covid19Radar.iOS.csproj @@ -270,6 +270,7 @@ + diff --git a/Covid19Radar/Covid19Radar.iOS/Info.plist b/Covid19Radar/Covid19Radar.iOS/Info.plist index cfd01d1dc..751350d06 100644 --- a/Covid19Radar/Covid19Radar.iOS/Info.plist +++ b/Covid19Radar/Covid19Radar.iOS/Info.plist @@ -55,6 +55,7 @@ APP_PACKAGE_NAME.exposure-notification APP_PACKAGE_NAME.delete-old-logs + APP_PACKAGE_NAME.eventlog-submission UIAppFonts diff --git a/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs b/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs new file mode 100644 index 000000000..b0c15bbe5 --- /dev/null +++ b/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs @@ -0,0 +1,105 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Threading; +using System.Threading.Tasks; +using BackgroundTasks; +using Covid19Radar.Common; +using Covid19Radar.Services; +using Covid19Radar.Services.Logs; +using Foundation; +using Xamarin.Essentials; + +namespace Covid19Radar.iOS.Services +{ + public class EventLogSubmissionBackgroundService : AbsEventLogSubmissionBackgroundService + { + private static readonly string IDENTIFIER = AppInfo.PackageName + ".eventlog-submission"; + + private const double ONE_DAY_IN_SECONDS = 1 * 24 * 60 * 60; + + private readonly IEventLogService _eventLogService; + private readonly ILoggerService _loggerService; + + public EventLogSubmissionBackgroundService( + IEventLogService eventLogService, + ILoggerService loggerService + ) : base() + { + _eventLogService = eventLogService; + _loggerService = loggerService; + } + + public override void Schedule() + { + _ = BGTaskScheduler.Shared.Register(IDENTIFIER, null, task => + { + HandleSendLogAsync((BGAppRefreshTask)task); + }); + + ScheduleSendEventLog(); + + _loggerService.EndMethod(); + } + + private void HandleSendLogAsync(BGAppRefreshTask task) + { + _loggerService.StartMethod(); + + ScheduleSendEventLog(); + + var cancellationTokenSource = new CancellationTokenSource(); + task.ExpirationHandler = cancellationTokenSource.Cancel; + + _ = Task.Run(async () => + { + try + { + await _eventLogService.SendAllAsync(AppConstants.EventLogMaxRequestSizeInBytes, AppConstants.EventLogMaxRetry); + task.SetTaskCompleted(true); + } + catch (OperationCanceledException exception) + { + _loggerService.Exception($"Background task canceled.", exception); + task.SetTaskCompleted(false); + } + catch (Exception exception) + { + _loggerService.Exception($"Exception", exception); + task.SetTaskCompleted(false); + } + finally + { + cancellationTokenSource.Dispose(); + _loggerService.EndMethod(); + } + }); + } + + private void ScheduleSendEventLog() + { + _loggerService.StartMethod(); + + var bgTaskRequest = new BGProcessingTaskRequest(IDENTIFIER) + { + EarliestBeginDate = NSDate.FromTimeIntervalSinceNow(ONE_DAY_IN_SECONDS), + RequiresNetworkConnectivity = true + }; + + _loggerService.Info($"request.EarliestBeginDate: {bgTaskRequest.EarliestBeginDate}"); + + _ = BGTaskScheduler.Shared.Submit(bgTaskRequest, out var error); + if (error != null) + { + NSErrorException exception = new NSErrorException(error); + _loggerService.Exception("BGTaskScheduler submit failed.", exception); + throw exception; + } + + _loggerService.EndMethod(); + } + } +} + diff --git a/Covid19Radar/Covid19Radar/Common/AppConstants.cs b/Covid19Radar/Covid19Radar/Common/AppConstants.cs index 467ac036b..abd34d71d 100644 --- a/Covid19Radar/Covid19Radar/Common/AppConstants.cs +++ b/Covid19Radar/Covid19Radar/Common/AppConstants.cs @@ -81,6 +81,16 @@ public static readonly DateTime COCOA_FIRST_RELEASE_DATE /// public const int DelayForRegistrationErrorMillis = 5000; + /// + /// Maximum size of event log content to be sent. + /// + public const long EventLogMaxRequestSizeInBytes = 8 * 1024 * 1024; // 8 MiB + + /// + /// Number of retries to send event log. + /// + public const int EventLogMaxRetry = 4; + #region Other Private Methods private static TimeZoneInfo JstTimeZoneInfo() diff --git a/Covid19Radar/Covid19Radar/Common/DeviceVerifierUtils.cs b/Covid19Radar/Covid19Radar/Common/DeviceVerifierUtils.cs index 94259db64..bbea05756 100644 --- a/Covid19Radar/Covid19Radar/Common/DeviceVerifierUtils.cs +++ b/Covid19Radar/Covid19Radar/Common/DeviceVerifierUtils.cs @@ -44,12 +44,12 @@ static string GetRegionString(IEnumerable regions) => public static string GetNonceClearTextV3(V1EventLogRequest eventLogRequest) { - return string.Join("|", eventLogRequest.AppPackageName, GetListClearText(eventLogRequest.EventLogs)); + return string.Join("|", eventLogRequest.IdempotencyKey, eventLogRequest.AppPackageName, GetListClearText(eventLogRequest.EventLogs)); - static string GetListClearText(V1EventLogRequest.EventLog[] eventLogs) + static string GetListClearText(IList eventLogs) => string.Join(",", eventLogs.Select(log => GetClearText(log))); - static string GetClearText(V1EventLogRequest.EventLog log) + static string GetClearText(EventLog log) => string.Join(".", log.HasConsent, log.Epoch, log.Type, log.Subtype, log.Content); } diff --git a/Covid19Radar/Covid19Radar/Model/V1EventLogRequest.cs b/Covid19Radar/Covid19Radar/Model/V1EventLogRequest.cs index d319a258d..09becf837 100644 --- a/Covid19Radar/Covid19Radar/Model/V1EventLogRequest.cs +++ b/Covid19Radar/Covid19Radar/Model/V1EventLogRequest.cs @@ -1,7 +1,9 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. + using System; +using System.Collections.Generic; using Covid19Radar.Common; using Newtonsoft.Json; @@ -22,32 +24,32 @@ public class V1EventLogRequest public string DeviceVerificationPayload; [JsonProperty("event_logs")] - public EventLog[] EventLogs; - - public string ToJsonString() => JsonConvert.SerializeObject(this); + public List EventLogs; + } - public class EventLog - { - [JsonProperty("has_consent")] - public bool HasConsent; + public class EventLog + { + [JsonProperty("has_consent")] + public bool HasConsent; - [JsonProperty("epoch")] - public long Epoch; + [JsonProperty("epoch")] + public long Epoch; - [JsonProperty("type")] - public string Type; + [JsonProperty("type")] + public string Type; - [JsonProperty("subtype")] - public string Subtype; + [JsonProperty("subtype")] + public string Subtype; - [JsonProperty("content")] - public string Content; + [JsonProperty("content")] + public string Content; - [JsonIgnore] - public string Timestamp - { - get => DateTime.UnixEpoch.AddSeconds(Epoch).ToString(AppConstants.FORMAT_TIMESTAMP); - } + [JsonIgnore] + public string Timestamp + { + get => DateTime.UnixEpoch.AddSeconds(Epoch).ToString(AppConstants.FORMAT_TIMESTAMP); } + + internal string GetEventType() => $"{Type}-{Subtype}"; } } diff --git a/Covid19Radar/Covid19Radar/Repository/EventLogRepository.cs b/Covid19Radar/Covid19Radar/Repository/EventLogRepository.cs new file mode 100644 index 000000000..2fec2688f --- /dev/null +++ b/Covid19Radar/Covid19Radar/Repository/EventLogRepository.cs @@ -0,0 +1,292 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Covid19Radar.Common; +using Covid19Radar.Model; +using Covid19Radar.Services; +using Covid19Radar.Services.Logs; +using Newtonsoft.Json; + +namespace Covid19Radar.Repository +{ + public interface IEventLogRepository + { + public Task AddAsync( + EventLog eventLog, + long maxSize = AppConstants.EventLogMaxRequestSizeInBytes + ); + + public Task> GetLogsAsync( + long maxSize = AppConstants.EventLogMaxRequestSizeInBytes + ); + + public Task RemoveAsync(EventLog eventLog); + + public Task AddEventNotifiedAsync( + long maxSize = AppConstants.EventLogMaxRequestSizeInBytes + ); + } + + public class EventLogRepository : IEventLogRepository + { + private const string LOG_EXTENSION = ".log"; + + private readonly ISendEventLogStateRepository _sendEventLogStateRepository; + private readonly IDateTimeUtility _dateTimeUtility; + private readonly ILoggerService _loggerService; + + private readonly string _basePath; + + public EventLogRepository( + ISendEventLogStateRepository sendEventLogStateRepository, + IDateTimeUtility dateTimeUtility, + ILocalPathService localPathService, + ILoggerService loggerService + ) + { + _sendEventLogStateRepository = sendEventLogStateRepository; + _dateTimeUtility = dateTimeUtility; + _basePath = localPathService.EventLogDirPath; + _loggerService = loggerService; + } + + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + + private string GetFileName(EventLog eventLog) + { + var clearText = string.Join(".", eventLog.Epoch, eventLog.Type, eventLog.Subtype, eventLog.Content); + + using var sha = SHA256.Create(); + var textBytes = Encoding.UTF8.GetBytes(clearText); + string hash = Convert.ToBase64String(sha.ComputeHash(textBytes)); + + return $"{hash}{LOG_EXTENSION}"; + } + + public async Task AddAsync(EventLog eventLog, long maxSize = AppConstants.EventLogMaxRequestSizeInBytes) + { + _loggerService.StartMethod(); + + await _semaphore.WaitAsync(); + + try + { + return await AddAsyncInternal(eventLog, maxSize); + } + finally + { + _semaphore.Release(); + + _loggerService.EndMethod(); + } + } + + private async Task AddAsyncInternal(EventLog eventLog, long maxSize) + { + _loggerService.StartMethod(); + + try + { + string fileName = GetFileName(eventLog); + string filePath = Path.Combine(_basePath, fileName); + + if (File.Exists(filePath)) + { + _loggerService.Info($"{filePath} already exist."); + return false; + } + + var serializedJson = JsonConvert.SerializeObject(eventLog); + + // Check log size. + long size = Encoding.UTF8.GetByteCount(serializedJson); + if (size > maxSize) + { + _loggerService.Info($"Log size {size} exceed maxSize( {maxSize} bytes."); + return false; + } + + await File.WriteAllTextAsync(filePath, serializedJson); + + return true; + } + finally + { + _loggerService.EndMethod(); + } + } + + public async Task> GetLogsAsync(long maxSize = AppConstants.EventLogMaxRequestSizeInBytes) + { + _loggerService.StartMethod(); + + await _semaphore.WaitAsync(); + + try + { + return await GetLogsAsyncInternal(maxSize); + } + finally + { + _semaphore.Release(); + + _loggerService.EndMethod(); + } + } + + private async Task> GetLogsAsyncInternal(long maxSize) + { + _loggerService.StartMethod(); + + try + { + long currentSize = 0; + var resultList = new List(); + + string[] files = Directory.GetFiles(_basePath) + .Where(file => file.EndsWith(LOG_EXTENSION)) + .ToArray(); + if (files.Length == 0) + { + _loggerService.Info("No log found."); + return resultList; + } + + List filePathList = files.Select(file => Path.Combine(_basePath, file)).ToList(); + foreach (var path in filePathList) + { + string content = await File.ReadAllTextAsync(path); + + // Check result size. + long size = Encoding.UTF8.GetByteCount(content); + long expectSize = currentSize + size; + if (expectSize > maxSize) + { + _loggerService.Info($"Log {path} size will exceed maxSize( {maxSize} bytes."); + continue; + } + + try + { + EventLog eventLog = JsonConvert.DeserializeObject(content); + resultList.Add(eventLog); + } + catch (JsonReaderException exception) + { + _loggerService.Exception($"Serialize failed {path} will be removed.", exception); + File.Delete(path); + } + } + + return resultList; + } + finally + { + _loggerService.EndMethod(); + } + + } + + public async Task RemoveAsync(EventLog eventLog) + { + _loggerService.StartMethod(); + + await _semaphore.WaitAsync(); + + try + { + return RemoveAsyncInternal(eventLog); + } + finally + { + _semaphore.Release(); + + _loggerService.EndMethod(); + } + } + + private bool RemoveAsyncInternal(EventLog eventLog) + { + _loggerService.StartMethod(); + + try + { + string fileName = GetFileName(eventLog); + string filePath = Path.Combine(_basePath, fileName); + + if (!File.Exists(filePath)) + { + _loggerService.Info($"{filePath} not found."); + return false; + } + + File.Delete(filePath); + + _loggerService.Info($"{filePath} is deleted."); + + return true; + } + finally + { + _loggerService.EndMethod(); + } + } + + public async Task AddEventNotifiedAsync(long maxSize = AppConstants.EventLogMaxRequestSizeInBytes) + { + _loggerService.StartMethod(); + + await _semaphore.WaitAsync(); + + try + { + await AddEventNotifiedAsyncInternal(maxSize); + } + finally + { + _semaphore.Release(); + + _loggerService.EndMethod(); + } + } + + private async Task AddEventNotifiedAsyncInternal(long maxSize) + { + bool hasConsent = _sendEventLogStateRepository.GetSendEventLogState( + EventType.ExposureNotified + ) == SendEventLogState.Enable; + + var content = new EventContentExposureNotified() + { + NotifiedTimeInMillis = _dateTimeUtility.UtcNow.Ticks + }; + + var eventLog = new EventLog() + { + HasConsent = hasConsent, + Epoch = _dateTimeUtility.UtcNow.ToUnixEpoch(), + Type = EventType.ExposureNotified.Type, + Subtype = EventType.ExposureNotified.SubType, + Content = content.ToJsonString(), + }; + await AddAsync(eventLog, maxSize); + } + } + + public class EventContentExposureNotified + { + [JsonProperty("notified_time_in_millis")] + public long NotifiedTimeInMillis; + + public string ToJsonString() => JsonConvert.SerializeObject(this, Formatting.Indented); + } +} \ No newline at end of file diff --git a/Covid19Radar/Covid19Radar/Repository/SendEventLogStateRepository.cs b/Covid19Radar/Covid19Radar/Repository/SendEventLogStateRepository.cs new file mode 100644 index 000000000..a6550ec60 --- /dev/null +++ b/Covid19Radar/Covid19Radar/Repository/SendEventLogStateRepository.cs @@ -0,0 +1,158 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; +using System.Linq; +using Covid19Radar.Common; +using Covid19Radar.Services; +using Covid19Radar.Services.Logs; +using Newtonsoft.Json; + +namespace Covid19Radar.Repository +{ + public enum SendEventLogState + { + NotSet = 0, + Disable = -1, + Enable = 1 + } + + public class EventType + { + public static readonly EventType ExposureNotified = new EventType("ExposureNotification", "ExposureNotified"); + public static readonly EventType ExposureData = new EventType("ExposureNotification", "ExposureData"); + + public static readonly EventType[] All = new EventType[] { + ExposureNotified, + ExposureData, + }; + + public string Type { get; } + public string SubType { get; } + + public EventType(string type, string subType) + { + Type = type; + SubType = subType; + } + + public override string ToString() + { + return $"{Type}-{SubType}"; + } + } + + public interface ISendEventLogStateRepository + { + void SetSendEventLogState(EventType eventType, SendEventLogState state); + + SendEventLogState GetSendEventLogState(EventType eventType); + + public static bool IsExistNotSetEventType(ISendEventLogStateRepository sendEventLogStateRepository) + => EventType.All + .Select(eventType => sendEventLogStateRepository.GetSendEventLogState(eventType)) + .Any(state => state == SendEventLogState.NotSet); + } + + public class SendEventLogStateRepository : ISendEventLogStateRepository + { + private const string EMPTY_DICT = "{}"; + + private readonly IPreferencesService _preferencesService; + private readonly ILoggerService _loggerService; + + public SendEventLogStateRepository( + IPreferencesService preferencesService, + ILoggerService loggerService + ) + { + _preferencesService = preferencesService; + _loggerService = loggerService; + } + + public SendEventLogState GetSendEventLogState(EventType eventType) + { + string stateString = EMPTY_DICT; + + try + { + _loggerService.StartMethod(); + + if (!_preferencesService.ContainsKey(PreferenceKey.SendEventLogState)) + { + return SendEventLogState.NotSet; + } + + stateString = _preferencesService.GetStringValue( + PreferenceKey.SendEventLogState, + EMPTY_DICT + ); + + IDictionary stateDict + = JsonConvert.DeserializeObject>(stateString); + + if (!stateDict.ContainsKey(eventType.ToString())) + { + return SendEventLogState.NotSet; + } + + int value = stateDict[eventType.ToString()]; + return (SendEventLogState)Enum.ToObject(typeof(SendEventLogState), value); + } + catch (JsonReaderException exception) + { + _preferencesService.SetStringValue(PreferenceKey.SendEventLogState, EMPTY_DICT); + + _loggerService.Exception($"JsonSerializationException {stateString}", exception); + _loggerService.Warning($"Preference-key {PreferenceKey.SendEventLogState} has been initialized."); + } + finally + { + _loggerService.EndMethod(); + } + + return SendEventLogState.NotSet; + } + + public void SetSendEventLogState(EventType eventType, SendEventLogState state) + { + try + { + _loggerService.StartMethod(); + + string stateString = EMPTY_DICT; + IDictionary stateDict = new Dictionary(); + + try + { + if (_preferencesService.ContainsKey(PreferenceKey.SendEventLogState)) + { + stateString = _preferencesService.GetStringValue( + PreferenceKey.SendEventLogState, + EMPTY_DICT + ); + + stateDict = JsonConvert.DeserializeObject>(stateString); + } + } + catch (JsonReaderException exception) + { + _loggerService.Exception($"JsonSerializationException {stateString}", exception); + } + + stateDict[eventType.ToString()] = (int)state; + + string newStateString = JsonConvert.SerializeObject(stateDict); + _preferencesService.SetStringValue(PreferenceKey.SendEventLogState, newStateString); + + } + finally + { + _loggerService.EndMethod(); + + } + } + } +} diff --git a/Covid19Radar/Covid19Radar/Repository/UserDataRepository.cs b/Covid19Radar/Covid19Radar/Repository/UserDataRepository.cs index 5727de4eb..a34c3d0c8 100644 --- a/Covid19Radar/Covid19Radar/Repository/UserDataRepository.cs +++ b/Covid19Radar/Covid19Radar/Repository/UserDataRepository.cs @@ -43,16 +43,6 @@ public interface IUserDataRepository Task GetLastProcessDiagnosisKeyTimestampAsync(string region); Task SetLastProcessDiagnosisKeyTimestampAsync(string region, long timestamp); Task RemoveLastProcessDiagnosisKeyTimestampAsync(); - - void SetSendEventLogState(SendEventLogState sendEventLogState); - SendEventLogState GetSendEventLogState(); - } - - public enum SendEventLogState - { - NotSet = 0, - Disable = -1, - Enable = 1 } public class UserDataRepository : IUserDataRepository @@ -271,31 +261,5 @@ public void RemoveAllExposureNotificationStatus() _preferencesService.RemoveValue(PreferenceKey.LastConfirmedDateTimeEpoch); _loggerService.EndMethod(); } - - public void SetSendEventLogState(SendEventLogState sendEventLogState) - { - _loggerService.StartMethod(); - - _preferencesService.SetIntValue(PreferenceKey.SendEventLogState, (int)sendEventLogState); - - _loggerService.EndMethod(); - } - - public SendEventLogState GetSendEventLogState() - { - _loggerService.StartMethod(); - try - { - int state = _preferencesService.GetIntValue( - PreferenceKey.SendEventLogState, - (int)SendEventLogState.NotSet - ); - return (SendEventLogState)state; - } - finally - { - _loggerService.EndMethod(); - } - } } } diff --git a/Covid19Radar/Covid19Radar/Services/AbsEventLogSubmissionBackgroundService.cs b/Covid19Radar/Covid19Radar/Services/AbsEventLogSubmissionBackgroundService.cs new file mode 100644 index 000000000..07a31c74b --- /dev/null +++ b/Covid19Radar/Covid19Radar/Services/AbsEventLogSubmissionBackgroundService.cs @@ -0,0 +1,11 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace Covid19Radar.Services +{ + public abstract class AbsEventLogSubmissionBackgroundService : IBackgroundService + { + public abstract void Schedule(); + } +} diff --git a/Covid19Radar/Covid19Radar/Services/EventLogService.cs b/Covid19Radar/Covid19Radar/Services/EventLogService.cs index 3ffc88874..6ee0beefe 100644 --- a/Covid19Radar/Covid19Radar/Services/EventLogService.cs +++ b/Covid19Radar/Covid19Radar/Services/EventLogService.cs @@ -4,199 +4,160 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Net; using System.Net.Http; using System.Text; +using System.Threading; using System.Threading.Tasks; -using Chino; -using Covid19Radar.Common; using Covid19Radar.Model; using Covid19Radar.Repository; using Covid19Radar.Services.Logs; -using Newtonsoft.Json; namespace Covid19Radar.Services { public interface IEventLogService { - public Task SendExposureDataAsync( - string idempotencyKey, - ExposureConfiguration exposureConfiguration, - string deviceModel, - string enVersion, - ExposureSummary exposureSummary, - IList exposureInformation - ); - - public Task SendExposureDataAsync( - string idempotencyKey, - ExposureConfiguration exposureConfiguration, - string deviceModel, - string enVersion, - IList dailySummaries, - IList exposureWindows - ); - - public Task SendExposureDataAsync( - string idempotencyKey, - ExposureConfiguration exposureConfiguration, - string deviceModel, - string enVersion - ); + public Task> SendAllAsync(long maxSize, int maxRetry); } -#if EVENT_LOG_ENABLED public class EventLogService : IEventLogService { - private readonly IUserDataRepository _userDataRepository; + private readonly ISendEventLogStateRepository _sendEventLogStateRepository; + private readonly IEventLogRepository _eventLogRepository; private readonly IServerConfigurationRepository _serverConfigurationRepository; private readonly IEssentialsService _essentialsService; private readonly IDeviceVerifier _deviceVerifier; - private readonly IDateTimeUtility _dateTimeUtility; - + private readonly IHttpDataService _httpDataService; private readonly ILoggerService _loggerService; - private readonly HttpClient _httpClient; + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); public EventLogService( - IUserDataRepository userDataRepository, + ISendEventLogStateRepository sendEventLogStateRepository, + IEventLogRepository eventLogRepository, IServerConfigurationRepository serverConfigurationRepository, IEssentialsService essentialsService, IDeviceVerifier deviceVerifier, - IDateTimeUtility dateTimeUtility, - IHttpClientService httpClientService, + IHttpDataService httpDataService, ILoggerService loggerService ) { - _userDataRepository = userDataRepository; + _sendEventLogStateRepository = sendEventLogStateRepository; + _eventLogRepository = eventLogRepository; _serverConfigurationRepository = serverConfigurationRepository; _essentialsService = essentialsService; _deviceVerifier = deviceVerifier; - _dateTimeUtility = dateTimeUtility; + _httpDataService = httpDataService; _loggerService = loggerService; - - _httpClient = httpClientService.Create(); } - public async Task SendExposureDataAsync( - string idempotencyKey, - ExposureConfiguration exposureConfiguration, - string deviceModel, - string enVersion, - ExposureSummary exposureSummary, - IList exposureInformation - ) + public async Task> SendAllAsync(long maxSize, int maxRetry) { - var data = new ExposureData(exposureConfiguration, - exposureSummary, exposureInformation - ) - { - Device = deviceModel, - EnVersion = enVersion, - }; - - await SendExposureDataAsync(idempotencyKey, data); - } + await _semaphore.WaitAsync(); - public async Task SendExposureDataAsync( - string idempotencyKey, - ExposureConfiguration exposureConfiguration, - string deviceModel, - string enVersion, - IList dailySummaries, - IList exposureWindows - ) - { - var data = new ExposureData(exposureConfiguration, - dailySummaries, exposureWindows - ) + try { - Device = deviceModel, - EnVersion = enVersion, - }; - - await SendExposureDataAsync(idempotencyKey, data); + return await SendAllInternalAsync(maxSize, maxRetry); + } + finally + { + _semaphore.Release(); + } } - public async Task SendExposureDataAsync( - string idempotencyKey, - ExposureConfiguration exposureConfiguration, - string deviceModel, - string enVersion - ) + private async Task> SendAllInternalAsync(long maxSize, int maxRetry) { - var data = new ExposureData( - exposureConfiguration - ) + _loggerService.StartMethod(); + + try { - Device = deviceModel, - EnVersion = enVersion, - }; + List eventLogList = await _eventLogRepository.GetLogsAsync(maxSize); + if (eventLogList.Count == 0) + { + _loggerService.Info($"No Event-logs found."); + return new List(); + } - await SendExposureDataAsync(idempotencyKey, data); - } + IDictionary eventStateDict = new Dictionary(); + foreach (var eventType in EventType.All) + { + eventStateDict[eventType.ToString()] = _sendEventLogStateRepository.GetSendEventLogState(eventType); + } + foreach (var eventLog in eventLogList) + { + eventLog.HasConsent = eventStateDict[eventLog.GetEventType()] == SendEventLogState.Enable; + } - private async Task SendExposureDataAsync( - string idempotencyKey, - ExposureData exposureData - ) - { - _loggerService.StartMethod(); + string idempotencyKey = Guid.NewGuid().ToString(); - SendEventLogState sendEventLogState = _userDataRepository.GetSendEventLogState(); - bool isEnabled = sendEventLogState == SendEventLogState.Enable; + for (var retryCount = 0; retryCount < maxRetry; retryCount++) + { + try + { + List sentEventLogList = await SendAsync( + idempotencyKey, + eventLogList + .Where(eventLog => eventLog.HasConsent) + .ToList() + ); + _loggerService.Info($"Send complete."); + + // TODO Error handling?? + + _loggerService.Info($"Clean up..."); + foreach (var eventLog in eventLogList) + { + await _eventLogRepository.RemoveAsync(eventLog); + } + _loggerService.Info($"Done."); + + return sentEventLogList; + } + catch (Exception exception) + { + _loggerService.Exception("Exception occurred, SendAsync", exception); + } + } - if (!isEnabled) + return new List(); + } + finally { - _loggerService.Debug($"Send event-log function is not enabled."); _loggerService.EndMethod(); - return; } + } - await _serverConfigurationRepository.LoadAsync(); - - string exposureDataCollectServerEndpoint = _serverConfigurationRepository.EventLogApiEndpoint; - _loggerService.Debug($"exposureDataCollectServerEndpoint: {exposureDataCollectServerEndpoint}"); + private async Task> SendAsync(string idempotencyKey, List eventLogList) + { + _loggerService.StartMethod(); try { - var contentJson = exposureData.ToJsonString(); - - var eventLog = new V1EventLogRequest.EventLog() { - HasConsent = isEnabled, - Epoch = _dateTimeUtility.UtcNow.ToUnixEpoch(), - Type = "ExposureData", - Subtype = "Debug", - Content = contentJson, - }; - var eventLogs = new[] { eventLog }; - var request = new V1EventLogRequest() { IdempotencyKey = idempotencyKey, Platform = _essentialsService.Platform, AppPackageName = _essentialsService.AppPackageName, - EventLogs = eventLogs, + EventLogs = eventLogList, }; request.DeviceVerificationPayload = await _deviceVerifier.VerifyAsync(request); - var requestJson = request.ToJsonString(); - - var httpContent = new StringContent(requestJson, Encoding.UTF8, "application/json"); + ApiResponse response = await _httpDataService.PutEventLog(request); + _loggerService.Info($"PutEventLog() StatusCode:{response.StatusCode}"); - Uri uri = new Uri(exposureDataCollectServerEndpoint); - - HttpResponseMessage response = await _httpClient.PutAsync(uri, httpContent); - if (response.IsSuccessStatusCode) - { - var responseJson = await response.Content.ReadAsStringAsync(); - _loggerService.Debug($"{responseJson}"); - } - else + if (response.StatusCode == (int)HttpStatusCode.Created) { - _loggerService.Info($"UploadExposureDataAsync {response.StatusCode}"); + _loggerService.Info("Send event log succeeded"); + _loggerService.Debug($"response: {response.Result}"); + return eventLogList; } + + // TODO Error Handling?? + + return new List(); } finally { @@ -204,5 +165,4 @@ ExposureData exposureData } } } -#endif } diff --git a/Covid19Radar/Covid19Radar/Services/EventLogServiceNop.cs b/Covid19Radar/Covid19Radar/Services/EventLogServiceNop.cs index eccf1b9d7..6a3dac8df 100644 --- a/Covid19Radar/Covid19Radar/Services/EventLogServiceNop.cs +++ b/Covid19Radar/Covid19Radar/Services/EventLogServiceNop.cs @@ -4,44 +4,19 @@ using System.Collections.Generic; using System.Threading.Tasks; -using Chino; +using Covid19Radar.Model; namespace Covid19Radar.Services { public class EventLogServiceNop : IEventLogService { - public Task SendExposureDataAsync( - string idempotencyKey, - ExposureConfiguration exposureConfiguration, - string deviceModel, string enVersion, - ExposureSummary exposureSummary, - IList exposureInformation + public Task> SendAllAsync( + long maxSize, + int maxRetry ) { // do nothing - return Task.CompletedTask; - } - - public Task SendExposureDataAsync( - string idempotencyKey, - ExposureConfiguration exposureConfiguration, - string deviceModel, string enVersion, - IList dailySummaries, IList exposureWindows - ) - { - // do nothing - return Task.CompletedTask; - } - - public Task SendExposureDataAsync( - string idempotencyKey, - ExposureConfiguration exposureConfiguration, - string deviceModel, - string enVersion - ) - { - // do nothing - return Task.CompletedTask; + return Task.FromResult(new List()); } } } diff --git a/Covid19Radar/Covid19Radar/Services/ExposureDetectionService.cs b/Covid19Radar/Covid19Radar/Services/ExposureDetectionService.cs index 64ba9ffaa..4a6bfbbc8 100644 --- a/Covid19Radar/Covid19Radar/Services/ExposureDetectionService.cs +++ b/Covid19Radar/Covid19Radar/Services/ExposureDetectionService.cs @@ -29,7 +29,8 @@ public interface IExposureDetectionService public class ExposureDetectionService : IExposureDetectionService { private readonly ILoggerService _loggerService; - private readonly IUserDataRepository _userDataRepository; + private readonly ISendEventLogStateRepository _sendEventLogStateRepository; + private readonly IExposureDataRepository _exposureDataRepository; private readonly ILocalNotificationService _localNotificationService; @@ -38,38 +39,31 @@ public class ExposureDetectionService : IExposureDetectionService private readonly IExposureConfigurationRepository _exposureConfigurationRepository; - private readonly IEventLogService _eventLogService; - - private readonly IDebugExposureDataCollectServer _exposureDataCollectServer; + private readonly IEventLogRepository _eventLogRepository; private readonly IDateTimeUtility _dateTimeUtility; - private readonly IDeviceInfoUtility _deviceInfoUtility; public ExposureDetectionService ( ILoggerService loggerService, - IUserDataRepository userDataRepository, + ISendEventLogStateRepository sendEventLogStateRepository, IExposureDataRepository exposureDataRepository, ILocalNotificationService localNotificationService, IExposureRiskCalculationConfigurationRepository exposureRiskCalculationConfigurationRepository, IExposureRiskCalculationService exposureRiskCalculationService, IExposureConfigurationRepository exposureConfigurationRepository, - IEventLogService eventLogService, - IDebugExposureDataCollectServer exposureDataCollectServer, - IDateTimeUtility dateTimeUtility, - IDeviceInfoUtility deviceInfoUtility + IEventLogRepository eventLogRepository, + IDateTimeUtility dateTimeUtility ) { _loggerService = loggerService; - _userDataRepository = userDataRepository; + _sendEventLogStateRepository = sendEventLogStateRepository; _exposureDataRepository = exposureDataRepository; _localNotificationService = localNotificationService; _exposureRiskCalculationService = exposureRiskCalculationService; _exposureRiskCalculationConfigurationRepository = exposureRiskCalculationConfigurationRepository; _exposureConfigurationRepository = exposureConfigurationRepository; - _eventLogService = eventLogService; - _exposureDataCollectServer = exposureDataCollectServer; + _eventLogRepository = eventLogRepository; _dateTimeUtility = dateTimeUtility; - _deviceInfoUtility = deviceInfoUtility; } public void DiagnosisKeysDataMappingApplied() @@ -128,40 +122,18 @@ long expectOldestDateMillisSinceEpoch if (isHighRiskExposureDetected) { _ = _localNotificationService.ShowExposureNotificationAsync(); - } - else - { - _loggerService.Info($"DailySummary: {dailySummaries.Count}, but no high-risk exposure detected"); - } - try - { - await _exposureDataCollectServer.UploadExposureDataAsync( - exposureConfiguration, - _deviceInfoUtility.Model, - enVersionStr, - newDailySummaries, newExposureWindows - ); - } - catch (Exception e) - { - _loggerService.Exception("UploadExposureDataAsync", e); - } + bool enableSendEventExposureNotificationNotified = _sendEventLogStateRepository + .GetSendEventLogState(EventType.ExposureNotified) == SendEventLogState.Enable; - string idempotencyKey = Guid.NewGuid().ToString(); - try - { - await _eventLogService.SendExposureDataAsync( - idempotencyKey, - exposureConfiguration, - _deviceInfoUtility.Model, - enVersionStr, - newDailySummaries, newExposureWindows - ); + if (enableSendEventExposureNotificationNotified) + { + await _eventLogRepository.AddEventNotifiedAsync(); + } } - catch (Exception e) + else { - _loggerService.Exception("SendExposureDataAsync", e); + _loggerService.Info($"DailySummary: {dailySummaries.Count}, but no high-risk exposure detected"); } } @@ -182,76 +154,25 @@ public async Task ExposureDetectedAsync(ExposureConfiguration exposureConfigurat if (isNewExposureDetected) { _ = _localNotificationService.ShowExposureNotificationAsync(); - } - else - { - _loggerService.Info($"MatchedKeyCount: {exposureSummary.MatchedKeyCount}, but no new exposure detected"); - } - try - { - await _exposureDataCollectServer.UploadExposureDataAsync( - exposureConfiguration, - _deviceInfoUtility.Model, - enVersionStr, - exposureSummary, exposureInformations - ); - } - catch (Exception e) - { - _loggerService.Exception("UploadExposureDataAsync", e); - } + bool enableSendEventExposureNotificationNotified = _sendEventLogStateRepository + .GetSendEventLogState(EventType.ExposureNotified) == SendEventLogState.Enable; - string idempotencyKey = Guid.NewGuid().ToString(); - try - { - await _eventLogService.SendExposureDataAsync( - idempotencyKey, - exposureConfiguration, - _deviceInfoUtility.Model, - enVersionStr, - exposureSummary, exposureInformations - ); + if (enableSendEventExposureNotificationNotified) + { + await _eventLogRepository.AddEventNotifiedAsync(); + } } - catch (Exception e) + else { - _loggerService.Exception("SendExposureDataAsync", e); + _loggerService.Info($"MatchedKeyCount: {exposureSummary.MatchedKeyCount}, but no new exposure detected"); } } - public async Task ExposureNotDetectedAsync(ExposureConfiguration exposureConfiguration, long enVersion) + public Task ExposureNotDetectedAsync(ExposureConfiguration exposureConfiguration, long enVersion) { _loggerService.Info("ExposureNotDetected"); - - var enVersionStr = enVersion.ToString(); - - try - { - await _exposureDataCollectServer.UploadExposureDataAsync( - exposureConfiguration, - _deviceInfoUtility.Model, - enVersionStr - ); - } - catch (Exception e) - { - _loggerService.Exception("UploadExposureDataAsync", e); - } - - string idempotencyKey = Guid.NewGuid().ToString(); - try - { - await _eventLogService.SendExposureDataAsync( - idempotencyKey, - exposureConfiguration, - _deviceInfoUtility.Model, - enVersionStr - ); - } - catch (Exception e) - { - _loggerService.Exception("SendExposureDataAsync", e); - } + return Task.CompletedTask; } } } diff --git a/Covid19Radar/Covid19Radar/Services/HttpDataService.cs b/Covid19Radar/Covid19Radar/Services/HttpDataService.cs index ce5308f1c..b2915769a 100644 --- a/Covid19Radar/Covid19Radar/Services/HttpDataService.cs +++ b/Covid19Radar/Covid19Radar/Services/HttpDataService.cs @@ -145,6 +145,29 @@ public async Task> GetLogStorageSas() return new ApiResponse(statusCode, logStorageSas); } + // PUT /api/v*/event_log + public async Task> PutEventLog(V1EventLogRequest request) + { + loggerService.StartMethod(); + + try + { + await serverConfigurationRepository.LoadAsync(); + + string url = serverConfigurationRepository.EventLogApiEndpoint; + var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); + + HttpResponseMessage response = await apiClient.PutAsync(url, content); + string responseContent = await response.Content.ReadAsStringAsync(); + + return new ApiResponse((int)response.StatusCode, responseContent); + } + finally + { + loggerService.EndMethod(); + } + } + private async Task PutAsync(string url, HttpContent body) { var result = await httpClient.PutAsync(url, body); diff --git a/Covid19Radar/Covid19Radar/Services/HttpDataServiceMock.cs b/Covid19Radar/Covid19Radar/Services/HttpDataServiceMock.cs index cacb6abfc..1bc7a61c7 100644 --- a/Covid19Radar/Covid19Radar/Services/HttpDataServiceMock.cs +++ b/Covid19Radar/Covid19Radar/Services/HttpDataServiceMock.cs @@ -144,6 +144,14 @@ public Task> GetLogStorageSas() return new ApiResponse((int)HttpStatusCode.OK, new LogStorageSas { SasToken = "sv=2012-02-12&se=2015-07-08T00%3A12%3A08Z&sr=c&sp=wl&sig=t%2BbzU9%2B7ry4okULN9S0wst%2F8MCUhTjrHyV9rDNLSe8g%3Dsss" }); }); } + + public Task> PutEventLog(V1EventLogRequest request) + { + return Task.Factory.StartNew(() => + { + return new ApiResponse((int)HttpStatusCode.Created, ""); + }); + } } public class MockCommonUtils diff --git a/Covid19Radar/Covid19Radar/Services/IHttpDataService.cs b/Covid19Radar/Covid19Radar/Services/IHttpDataService.cs index 4a6d72205..369de292f 100644 --- a/Covid19Radar/Covid19Radar/Services/IHttpDataService.cs +++ b/Covid19Radar/Covid19Radar/Services/IHttpDataService.cs @@ -15,5 +15,7 @@ public interface IHttpDataService Task PutSelfExposureKeysAsync(DiagnosisSubmissionParameter request); Task> GetLogStorageSas(); + + Task> PutEventLog(V1EventLogRequest request); } } diff --git a/Covid19Radar/Covid19Radar/Services/ILocalPathService.cs b/Covid19Radar/Covid19Radar/Services/ILocalPathService.cs index 487c67588..9d4ec567e 100644 --- a/Covid19Radar/Covid19Radar/Services/ILocalPathService.cs +++ b/Covid19Radar/Covid19Radar/Services/ILocalPathService.cs @@ -14,6 +14,7 @@ public interface ILocalPathService private const string CURRENT_EXPOSURE_RISK_CALCULATION_CONFIGURATION_FILENAME = "exposure_risk_calculation_configuration_current.json"; private const string EXPOSURE_DATA_FILENAME = "exposure_data.json"; + private const string EVENT_LOG_QUEUE_DIR = "event_log_queue"; string ExposureConfigurationDirPath => Path.Combine(FileSystem.AppDataDirectory, EXPOSURE_CONFIGURATION_DIR); @@ -32,5 +33,8 @@ public interface ILocalPathService string LogsDirPath { get; } string LogUploadingTmpPath { get; } string LogUploadingPublicPath { get; } + + string EventLogDirPath => + Path.Combine(FileSystem.AppDataDirectory, EVENT_LOG_QUEUE_DIR); } } diff --git a/Covid19Radar/Tests/Covid19Radar.UnitTests/Files/eventlog_submission_parameter1-cleartext.txt b/Covid19Radar/Tests/Covid19Radar.UnitTests/Files/eventlog_submission_parameter1-cleartext.txt index 4a6f145e0..3745f79cd 100644 --- a/Covid19Radar/Tests/Covid19Radar.UnitTests/Files/eventlog_submission_parameter1-cleartext.txt +++ b/Covid19Radar/Tests/Covid19Radar.UnitTests/Files/eventlog_submission_parameter1-cleartext.txt @@ -1 +1 @@ -jp.go.mhlw.covid19radar|True.1640256881.ExposureNotification.ExposureData.Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullam,True.1640257311.ExposureNotification.ExposureData.Aenean sit amet dui blandit, faucibus neque sollicitudin, sollicitudin nisi. Vivamus mattis felis vitae lorem ultrices interdum. Nam massa orci, hendrerit in mauris at, ultricies eleifend tortor. Etiam neque dolor, tincidunt a scelerisque id, commodo at purus. Mauris ac ultrices quam. Aliquam elementum diam venenatis orci tempor finibus. Donec ultricies interdum tortor, vitae imperdiet orci porta ac.,True.1640258212.ExposureNotification.ExposureData.Etiam venenatis condimentum maximus. Morbi eu consectetur erat. Nam in tellus velit. Cras accumsan mattis lectus quis vulputate. Fusce bibendum odio risus, eget hendrerit enim volutpat nec. Praesent sagittis, metus et ullamcorper interdum, lacus dolor consectetur quam, ut euismod elit erat quis nisi. Phasellus ac condimentum lorem. Etiam suscipit, odio at elementum malesuada, nisl metus posuere lorem, non sollicitudin quam justo ac enim. Vestibulum finibus, sapien et dignissim commodo, felis quam tincidunt odio, ac luctus tortor nibh ut nunc. Phasellus lobortis posuere rutrum. Donec dictum nec quam ac cursus. Aliquam erat volutpat. In vitae ullamcorper ipsum. Maecenas vestibulum nibh purus. Aliquam erat volutpat. \ No newline at end of file +E09585E4-A6A9-4E4A-84C3-0FB8B23B2498|jp.go.mhlw.covid19radar|True.1640256881.ExposureNotification.ExposureData.Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullam,True.1640257311.ExposureNotification.ExposureData.Aenean sit amet dui blandit, faucibus neque sollicitudin, sollicitudin nisi. Vivamus mattis felis vitae lorem ultrices interdum. Nam massa orci, hendrerit in mauris at, ultricies eleifend tortor. Etiam neque dolor, tincidunt a scelerisque id, commodo at purus. Mauris ac ultrices quam. Aliquam elementum diam venenatis orci tempor finibus. Donec ultricies interdum tortor, vitae imperdiet orci porta ac.,True.1640258212.ExposureNotification.ExposureData.Etiam venenatis condimentum maximus. Morbi eu consectetur erat. Nam in tellus velit. Cras accumsan mattis lectus quis vulputate. Fusce bibendum odio risus, eget hendrerit enim volutpat nec. Praesent sagittis, metus et ullamcorper interdum, lacus dolor consectetur quam, ut euismod elit erat quis nisi. Phasellus ac condimentum lorem. Etiam suscipit, odio at elementum malesuada, nisl metus posuere lorem, non sollicitudin quam justo ac enim. Vestibulum finibus, sapien et dignissim commodo, felis quam tincidunt odio, ac luctus tortor nibh ut nunc. Phasellus lobortis posuere rutrum. Donec dictum nec quam ac cursus. Aliquam erat volutpat. In vitae ullamcorper ipsum. Maecenas vestibulum nibh purus. Aliquam erat volutpat. \ No newline at end of file diff --git a/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/ExposureDetectionServiceTests.cs b/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/ExposureDetectionServiceTests.cs index e25fb5b7e..3d49c3dc4 100644 --- a/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/ExposureDetectionServiceTests.cs +++ b/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/ExposureDetectionServiceTests.cs @@ -23,15 +23,16 @@ public class ExposureDetectionServiceTests: IDisposable private readonly MockRepository mockRepository; private readonly Mock loggerService; + private readonly Mock sendEventLogStateRepository; private readonly Mock localNotificationService; private readonly Mock exposureDataCollectServer; private readonly Mock exposureRiskCalculationConfigurationRepository; private readonly Mock exposureRiskCalculationService; private readonly Mock eventLogService; private readonly Mock dateTimeUtility; + private readonly Mock eventLogRepository; private readonly Mock deviceInfoUtility; - private readonly Mock clientService; private readonly Mock localPathService; private readonly Mock preferencesService; @@ -45,6 +46,7 @@ public ExposureDetectionServiceTests() { mockRepository = new MockRepository(MockBehavior.Default); loggerService = mockRepository.Create(); + sendEventLogStateRepository = mockRepository.Create(); localNotificationService = mockRepository.Create(); exposureDataCollectServer = mockRepository.Create(); exposureRiskCalculationConfigurationRepository = mockRepository.Create(); @@ -57,9 +59,9 @@ public ExposureDetectionServiceTests() secureStorageService = mockRepository.Create(); serverConfigurationRepository = mockRepository.Create(); dateTimeUtility = mockRepository.Create(); + eventLogRepository = mockRepository.Create(); deviceInfoUtility = mockRepository.Create(); - localPathService.Setup(x => x.ExposureConfigurationDirPath).Returns($"{Path.GetTempPath()}/cocoa/"); } @@ -101,16 +103,14 @@ private ExposureDetectionService CreateService() return new ExposureDetectionService( loggerService.Object, - userDataRepository, + sendEventLogStateRepository.Object, exposureDataRepository, localNotificationService.Object, exposureRiskCalculationConfigurationRepository.Object, exposureRiskCalculationService.Object, exposureConfigurationRepository, - eventLogService.Object, - exposureDataCollectServer.Object, - dateTimeUtility.Object, - deviceInfoUtility.Object + eventLogRepository.Object, + dateTimeUtility.Object ); } #endregion diff --git a/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/HttpDataServiceTests.cs b/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/HttpDataServiceTests.cs index 03ac6a495..fa6afee51 100644 --- a/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/HttpDataServiceTests.cs +++ b/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/HttpDataServiceTests.cs @@ -26,7 +26,8 @@ public class HttpDataServiceTests private readonly MockRepository mockRepository; private readonly Mock mockLoggerService; private readonly Mock mockHttpClientService; - private readonly Mock mockServerConfigurationRepository; + + private readonly ReleaseServerConfigurationRepository _serverConfigurationRepository; #endregion @@ -37,7 +38,7 @@ public HttpDataServiceTests() mockRepository = new MockRepository(MockBehavior.Default); mockLoggerService = mockRepository.Create(); mockHttpClientService = mockRepository.Create(); - mockServerConfigurationRepository = mockRepository.Create(); + _serverConfigurationRepository = new ReleaseServerConfigurationRepository(); } #endregion @@ -49,7 +50,7 @@ private IHttpDataService CreateService() return new HttpDataService( mockLoggerService.Object, mockHttpClientService.Object, - mockServerConfigurationRepository.Object + _serverConfigurationRepository ); } @@ -77,8 +78,6 @@ public async Task PostRegisterUserAsyncTestsAsync_Success() })); mockHttpClientService.Setup(x => x.Create()).Returns(mockHttpClient); - mockServerConfigurationRepository.Setup(x => x.UserRegisterApiEndpoint) - .Returns(IServerConfigurationRepository.CombineAsUrl(AppSettings.Instance.ApiUrlBase, "api/register")); var unitUnderTest = CreateService(); @@ -106,8 +105,6 @@ public async Task PostRegisterUserAsyncTestsAsync_Failure() })); mockHttpClientService.Setup(x => x.Create()).Returns(mockHttpClient); - mockServerConfigurationRepository.Setup(x => x.UserRegisterApiEndpoint) - .Returns(IServerConfigurationRepository.CombineAsUrl(AppSettings.Instance.ApiUrlBase, "api/register")); var unitUnderTest = CreateService(); @@ -135,8 +132,6 @@ public void PostRegisterUserAsyncTestsAsync_Exception() })); mockHttpClientService.Setup(x => x.Create()).Returns(mockHttpClient); - mockServerConfigurationRepository.Setup(x => x.UserRegisterApiEndpoint) - .Returns(IServerConfigurationRepository.CombineAsUrl(AppSettings.Instance.ApiUrlBase, "api/register")); var unitUnderTest = CreateService(); @@ -173,8 +168,6 @@ public async Task PutSelfExposureKeysAsync_Success() })); mockHttpClientService.Setup(x => x.Create()).Returns(mockHttpClient); - mockServerConfigurationRepository.Setup(x => x.DiagnosisKeyRegisterApiUrls) - .Returns(new List() { IServerConfigurationRepository.CombineAsUrl(AppSettings.Instance.ApiUrlBase, "api/v3/diagnosis") }); var unitUnderTest = CreateService(); @@ -231,6 +224,78 @@ public async Task PutSelfExposureKeysAsync_Success() #endregion + #region PutEventLogTests + + [Fact] + public async Task PutEventLogTests_Success() + { + HttpContent requestContent = null; + var mockHttpClient = new HttpClient(new MockHttpHandler((r, c) => + { + var absoluteUri = r.RequestUri.AbsoluteUri; + if (absoluteUri.EndsWith("/api/v1/event_log")) + { + requestContent = r.Content; + var response = new HttpResponseMessage(HttpStatusCode.Created); + response.Content = new StringContent(""); + return response; + } + return new HttpResponseMessage(HttpStatusCode.NotFound); + })); + + mockHttpClientService.Setup(x => x.Create()).Returns(mockHttpClient); + + var unitUnderTest = CreateService(); + + var request = new V1EventLogRequest() + { + IdempotencyKey = "05A6A158-E216-4599-B99E-3708D360FF2F", + Platform = "platform", + AppPackageName = "app-package-name", + DeviceVerificationPayload = "device-verification-payload", + EventLogs = new List() + { + new EventLog() + { + HasConsent = true, + Epoch = 1000, + Type = "type", + Subtype = "subtype", + Content = "content" + } + } + }; + + var result = await unitUnderTest.PutEventLog(request); + + Assert.Equal((int)HttpStatusCode.Created, result.StatusCode); + Assert.NotNull(requestContent); + + var stringContent = await requestContent.ReadAsStringAsync(); + Assert.NotEmpty(stringContent); + + var jsonContent = JsonConvert.DeserializeObject(stringContent) as JObject; + Assert.NotNull(jsonContent); + + Assert.Equal("05A6A158-E216-4599-B99E-3708D360FF2F", jsonContent["idempotency_key"].Value()); + Assert.Equal("platform", jsonContent["platform"].Value()); + Assert.Equal("app-package-name", jsonContent["appPackageName"].Value()); + Assert.Equal("device-verification-payload", jsonContent["deviceVerificationPayload"].Value()); + + var eventLogs = jsonContent["event_logs"] as JArray; + Assert.Single(eventLogs); + + Assert.True(eventLogs[0]["has_consent"].Value()); + Assert.Equal(1000, eventLogs[0]["epoch"].Value()); + Assert.Equal("type", eventLogs[0]["type"].Value()); + Assert.Equal("subtype", eventLogs[0]["subtype"].Value()); + Assert.Equal("content", eventLogs[0]["content"].Value()); + + Assert.Null(mockHttpClient.DefaultRequestHeaders.Authorization); + } + + #endregion + #endregion } } From 4839cab7a1a7318d503f485b009a7d6bf380773b Mon Sep 17 00:00:00 2001 From: cocoa-dev004 <66989461+cocoa-dev004@users.noreply.github.com> Date: Tue, 14 Jun 2022 11:19:48 +0900 Subject: [PATCH 2/5] Fixed error handling etc --- .../EventLogSubmissionBackgroundService.cs | 5 +- .../EventLogSubmissionBackgroundService.cs | 10 +- Covid19Radar/Covid19Radar/App.xaml.cs | 6 +- .../Covid19Radar/Common/AppConstants.cs | 5 + .../Repository/EventLogRepository.cs | 39 +-- .../Covid19Radar/Services/EventLogService.cs | 78 +++-- .../Services/EventLogServiceNop.cs | 5 +- .../SendEventLogStateRepositoryTests.cs | 196 +++++++++++ .../Services/EventLogServiceTests.cs | 325 ++++++++++++++++++ 9 files changed, 598 insertions(+), 71 deletions(-) create mode 100644 Covid19Radar/Tests/Covid19Radar.UnitTests/Repository/SendEventLogStateRepositoryTests.cs create mode 100644 Covid19Radar/Tests/Covid19Radar.UnitTests/Services/EventLogServiceTests.cs diff --git a/Covid19Radar/Covid19Radar.Android/Services/EventLogSubmissionBackgroundService.cs b/Covid19Radar/Covid19Radar.Android/Services/EventLogSubmissionBackgroundService.cs index 1c5172226..2c276e8fa 100644 --- a/Covid19Radar/Covid19Radar.Android/Services/EventLogSubmissionBackgroundService.cs +++ b/Covid19Radar/Covid19Radar.Android/Services/EventLogSubmissionBackgroundService.cs @@ -81,7 +81,10 @@ public override Result DoWork() try { - eventLogService.SendAllAsync(AppConstants.EventLogMaxRequestSizeInBytes, AppConstants.EventLogMaxRetry); + eventLogService.SendAllAsync( + AppConstants.EventLogMaxRequestSizeInBytes, + AppConstants.EventLogMaxRetry, + AppConstants.EventLogRetryInternval); return Result.InvokeSuccess(); } catch (Exception exception) diff --git a/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs b/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs index b0c15bbe5..3b77a5fec 100644 --- a/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs +++ b/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs @@ -55,9 +55,13 @@ private void HandleSendLogAsync(BGAppRefreshTask task) _ = Task.Run(async () => { + _loggerService.Info("HandleSendLogAsync() Task.Run() start"); try { - await _eventLogService.SendAllAsync(AppConstants.EventLogMaxRequestSizeInBytes, AppConstants.EventLogMaxRetry); + await _eventLogService.SendAllAsync( + AppConstants.EventLogMaxRequestSizeInBytes, + AppConstants.EventLogMaxRetry, + AppConstants.EventLogRetryInternval); task.SetTaskCompleted(true); } catch (OperationCanceledException exception) @@ -73,9 +77,11 @@ private void HandleSendLogAsync(BGAppRefreshTask task) finally { cancellationTokenSource.Dispose(); - _loggerService.EndMethod(); + _loggerService.Info("HandleSendLogAsync() Task.Run() end"); } }); + + _loggerService.EndMethod(); } private void ScheduleSendEventLog() diff --git a/Covid19Radar/Covid19Radar/App.xaml.cs b/Covid19Radar/Covid19Radar/App.xaml.cs index 2d11bdd2d..5653063f3 100644 --- a/Covid19Radar/Covid19Radar/App.xaml.cs +++ b/Covid19Radar/Covid19Radar/App.xaml.cs @@ -220,11 +220,9 @@ private static void RegisterCommonTypes(IContainer container) container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); -#if EVENT_LOG_ENABLED + container.Register(Reuse.Singleton); + container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); -#else - container.Register(Reuse.Singleton); -#endif // Utilities container.Register(Reuse.Singleton); diff --git a/Covid19Radar/Covid19Radar/Common/AppConstants.cs b/Covid19Radar/Covid19Radar/Common/AppConstants.cs index abd34d71d..c8f094ffc 100644 --- a/Covid19Radar/Covid19Radar/Common/AppConstants.cs +++ b/Covid19Radar/Covid19Radar/Common/AppConstants.cs @@ -91,6 +91,11 @@ public static readonly DateTime COCOA_FIRST_RELEASE_DATE /// public const int EventLogMaxRetry = 4; + /// + /// Retry interval for sending event logs. + /// + public const int EventLogRetryInternval = 1000; + #region Other Private Methods private static TimeZoneInfo JstTimeZoneInfo() diff --git a/Covid19Radar/Covid19Radar/Repository/EventLogRepository.cs b/Covid19Radar/Covid19Radar/Repository/EventLogRepository.cs index 2fec2688f..21b4f5ebe 100644 --- a/Covid19Radar/Covid19Radar/Repository/EventLogRepository.cs +++ b/Covid19Radar/Covid19Radar/Repository/EventLogRepository.cs @@ -20,11 +20,6 @@ namespace Covid19Radar.Repository { public interface IEventLogRepository { - public Task AddAsync( - EventLog eventLog, - long maxSize = AppConstants.EventLogMaxRequestSizeInBytes - ); - public Task> GetLogsAsync( long maxSize = AppConstants.EventLogMaxRequestSizeInBytes ); @@ -72,24 +67,6 @@ private string GetFileName(EventLog eventLog) return $"{hash}{LOG_EXTENSION}"; } - public async Task AddAsync(EventLog eventLog, long maxSize = AppConstants.EventLogMaxRequestSizeInBytes) - { - _loggerService.StartMethod(); - - await _semaphore.WaitAsync(); - - try - { - return await AddAsyncInternal(eventLog, maxSize); - } - finally - { - _semaphore.Release(); - - _loggerService.EndMethod(); - } - } - private async Task AddAsyncInternal(EventLog eventLog, long maxSize) { _loggerService.StartMethod(); @@ -111,18 +88,30 @@ private async Task AddAsyncInternal(EventLog eventLog, long maxSize) long size = Encoding.UTF8.GetByteCount(serializedJson); if (size > maxSize) { - _loggerService.Info($"Log size {size} exceed maxSize( {maxSize} bytes."); + _loggerService.Info($"Log size {size} exceed maxSize {maxSize} bytes."); return false; } + string directoryName = Path.GetDirectoryName(filePath); + if (!Directory.Exists(directoryName)) + { + Directory.CreateDirectory(directoryName); + _loggerService.Info($"Directory created. directoryName:{directoryName}"); + } + await File.WriteAllTextAsync(filePath, serializedJson); return true; } + catch (Exception ex) + { + _loggerService.Exception("Write event log failure.", ex); + } finally { _loggerService.EndMethod(); } + return false; } public async Task> GetLogsAsync(long maxSize = AppConstants.EventLogMaxRequestSizeInBytes) @@ -278,7 +267,7 @@ private async Task AddEventNotifiedAsyncInternal(long maxSize) Subtype = EventType.ExposureNotified.SubType, Content = content.ToJsonString(), }; - await AddAsync(eventLog, maxSize); + await AddAsyncInternal(eventLog, maxSize); } } diff --git a/Covid19Radar/Covid19Radar/Services/EventLogService.cs b/Covid19Radar/Covid19Radar/Services/EventLogService.cs index 6ee0beefe..2f34bd355 100644 --- a/Covid19Radar/Covid19Radar/Services/EventLogService.cs +++ b/Covid19Radar/Covid19Radar/Services/EventLogService.cs @@ -6,8 +6,6 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; using Covid19Radar.Model; @@ -18,14 +16,13 @@ namespace Covid19Radar.Services { public interface IEventLogService { - public Task> SendAllAsync(long maxSize, int maxRetry); + public Task SendAllAsync(long maxSize, int maxRetry, int retryInterval); } public class EventLogService : IEventLogService { private readonly ISendEventLogStateRepository _sendEventLogStateRepository; private readonly IEventLogRepository _eventLogRepository; - private readonly IServerConfigurationRepository _serverConfigurationRepository; private readonly IEssentialsService _essentialsService; private readonly IDeviceVerifier _deviceVerifier; private readonly IHttpDataService _httpDataService; @@ -36,7 +33,6 @@ public class EventLogService : IEventLogService public EventLogService( ISendEventLogStateRepository sendEventLogStateRepository, IEventLogRepository eventLogRepository, - IServerConfigurationRepository serverConfigurationRepository, IEssentialsService essentialsService, IDeviceVerifier deviceVerifier, IHttpDataService httpDataService, @@ -45,20 +41,19 @@ ILoggerService loggerService { _sendEventLogStateRepository = sendEventLogStateRepository; _eventLogRepository = eventLogRepository; - _serverConfigurationRepository = serverConfigurationRepository; _essentialsService = essentialsService; _deviceVerifier = deviceVerifier; _httpDataService = httpDataService; _loggerService = loggerService; } - public async Task> SendAllAsync(long maxSize, int maxRetry) + public async Task SendAllAsync(long maxSize, int maxRetry, int retryInterval) { await _semaphore.WaitAsync(); try { - return await SendAllInternalAsync(maxSize, maxRetry); + await SendAllInternalAsync(maxSize, maxRetry, retryInterval); } finally { @@ -66,7 +61,7 @@ public async Task> SendAllAsync(long maxSize, int maxRetry) } } - private async Task> SendAllInternalAsync(long maxSize, int maxRetry) + private async Task SendAllInternalAsync(long maxSize, int maxRetry, int retryInterval) { _loggerService.StartMethod(); @@ -75,8 +70,8 @@ private async Task> SendAllInternalAsync(long maxSize, int maxRet List eventLogList = await _eventLogRepository.GetLogsAsync(maxSize); if (eventLogList.Count == 0) { - _loggerService.Info($"No Event-logs found."); - return new List(); + _loggerService.Info($"Event-logs not found."); + return; } IDictionary eventStateDict = new Dictionary(); @@ -87,41 +82,52 @@ private async Task> SendAllInternalAsync(long maxSize, int maxRet foreach (var eventLog in eventLogList) { - eventLog.HasConsent = eventStateDict[eventLog.GetEventType()] == SendEventLogState.Enable; + if (eventStateDict[eventLog.GetEventType()] != SendEventLogState.Enable) + { + eventLog.HasConsent = false; + } } string idempotencyKey = Guid.NewGuid().ToString(); + List agreedEventLogList + = eventLogList.Where(eventLog => eventLog.HasConsent).ToList(); - for (var retryCount = 0; retryCount < maxRetry; retryCount++) + if (agreedEventLogList.Count == 0) { - try - { - List sentEventLogList = await SendAsync( - idempotencyKey, - eventLogList - .Where(eventLog => eventLog.HasConsent) - .ToList() - ); - _loggerService.Info($"Send complete."); + _loggerService.Info($"Agreed event-logs not found."); + return; + } - // TODO Error handling?? + int tries = 0; + while (true) + { + bool isSuccess = await SendAsync(idempotencyKey, agreedEventLogList); + + if (isSuccess) + { + _loggerService.Info($"Event log send successful."); _loggerService.Info($"Clean up..."); foreach (var eventLog in eventLogList) { await _eventLogRepository.RemoveAsync(eventLog); } - _loggerService.Info($"Done."); - return sentEventLogList; + break; } - catch (Exception exception) + else if (tries >= maxRetry) { - _loggerService.Exception("Exception occurred, SendAsync", exception); + _loggerService.Error("Event log send failed all."); + break; } + + _loggerService.Warning($"Event log send failed. tries:{tries + 1}"); + await Task.Delay(retryInterval); + + tries++; } - return new List(); + _loggerService.Info($"Done."); } finally { @@ -129,7 +135,7 @@ private async Task> SendAllInternalAsync(long maxSize, int maxRet } } - private async Task> SendAsync(string idempotencyKey, List eventLogList) + private async Task SendAsync(string idempotencyKey, List eventLogList) { _loggerService.StartMethod(); @@ -151,18 +157,20 @@ private async Task> SendAsync(string idempotencyKey, List(); + } + catch (Exception ex) + { + _loggerService.Exception("Exception occurred, SendAsync", ex); } finally { _loggerService.EndMethod(); } + + _loggerService.Error("Send event log failure"); + return false; } } } diff --git a/Covid19Radar/Covid19Radar/Services/EventLogServiceNop.cs b/Covid19Radar/Covid19Radar/Services/EventLogServiceNop.cs index 6a3dac8df..8fea52571 100644 --- a/Covid19Radar/Covid19Radar/Services/EventLogServiceNop.cs +++ b/Covid19Radar/Covid19Radar/Services/EventLogServiceNop.cs @@ -10,10 +10,7 @@ namespace Covid19Radar.Services { public class EventLogServiceNop : IEventLogService { - public Task> SendAllAsync( - long maxSize, - int maxRetry - ) + public Task SendAllAsync(long maxSize, int maxRetry, int retryInterval) { // do nothing return Task.FromResult(new List()); diff --git a/Covid19Radar/Tests/Covid19Radar.UnitTests/Repository/SendEventLogStateRepositoryTests.cs b/Covid19Radar/Tests/Covid19Radar.UnitTests/Repository/SendEventLogStateRepositoryTests.cs new file mode 100644 index 000000000..6c1e1803d --- /dev/null +++ b/Covid19Radar/Tests/Covid19Radar.UnitTests/Repository/SendEventLogStateRepositoryTests.cs @@ -0,0 +1,196 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using Covid19Radar.Common; +using Covid19Radar.Repository; +using Covid19Radar.Services; +using Covid19Radar.Services.Logs; +using Moq; +using Xunit; + +namespace Covid19Radar.UnitTests.Repository +{ + public class SendEventLogStateRepositoryTests + { + private static EventType DUMMY_EVENT1 = new EventType("DummyEventType1", "DummyEventSubtype1"); + private static EventType DUMMY_EVENT2 = new EventType("DummyEventType2", "DummyEventSubtype2"); + + private readonly MockRepository mockRepository; + private readonly Mock mockLoggerService; + private readonly Mock mockPreferencesService; + + public SendEventLogStateRepositoryTests() + { + mockRepository = new MockRepository(MockBehavior.Default); + mockLoggerService = mockRepository.Create(); + mockPreferencesService = mockRepository.Create(); + } + + private ISendEventLogStateRepository CreateRepository() + => new SendEventLogStateRepository( + mockPreferencesService.Object, + mockLoggerService.Object + ); + + [Fact] + public void GetSendEventLogStateTest_NotExists() + { + mockPreferencesService.Setup(s => s.ContainsKey(PreferenceKey.SendEventLogState)).Returns(false); + + var sendEventLogStateRepository = CreateRepository(); + + var exposureNotificationNotified = sendEventLogStateRepository + .GetSendEventLogState(DUMMY_EVENT1); + + Assert.Equal(SendEventLogState.NotSet, exposureNotificationNotified); + } + + [Fact] + public void GetSendEventLogStateTest_InvalidJson() + { + mockPreferencesService.Setup(s => s.ContainsKey(PreferenceKey.SendEventLogState)).Returns(true); + mockPreferencesService.Setup(s => s.GetStringValue(PreferenceKey.SendEventLogState, It.IsAny())) + .Returns("{]"); + + var sendEventLogStateRepository = CreateRepository(); + + var exposureNotificationNotified = sendEventLogStateRepository + .GetSendEventLogState(DUMMY_EVENT1); + + Assert.Equal(SendEventLogState.NotSet, exposureNotificationNotified); + } + + [Fact] + public void GetSendEventLogStateTest_Blank() + { + mockPreferencesService.Setup(s => s.ContainsKey(PreferenceKey.SendEventLogState)).Returns(true); + mockPreferencesService.Setup(s => s.GetStringValue(PreferenceKey.SendEventLogState, It.IsAny())) + .Returns("{ }"); + + var sendEventLogStateRepository = CreateRepository(); + + var exposureNotificationNotified = sendEventLogStateRepository + .GetSendEventLogState(DUMMY_EVENT1); + + Assert.Equal(SendEventLogState.NotSet, exposureNotificationNotified); + } + + [Fact] + public void GetSendEventLogStateTest_NotSet() + { + mockPreferencesService.Setup(s => s.ContainsKey(PreferenceKey.SendEventLogState)).Returns(true); + mockPreferencesService.Setup(s => s.GetStringValue(PreferenceKey.SendEventLogState, It.IsAny())) + .Returns("{ \"DummyEventType\": 0 }"); + + var sendEventLogStateRepository = CreateRepository(); + + var exposureNotificationNotified = sendEventLogStateRepository + .GetSendEventLogState(DUMMY_EVENT1); + + Assert.Equal(SendEventLogState.NotSet, exposureNotificationNotified); + } + + [Fact] + public void GetSendEventLogStateTest_Disable() + { + mockPreferencesService.Setup(s => s.ContainsKey(PreferenceKey.SendEventLogState)).Returns(true); + mockPreferencesService.Setup(s => s.GetStringValue(PreferenceKey.SendEventLogState, It.IsAny())) + .Returns("{ \"DummyEventType1-DummyEventSubtype1\": -1 }"); + + var sendEventLogStateRepository = CreateRepository(); + + var exposureNotificationNotified = sendEventLogStateRepository + .GetSendEventLogState(DUMMY_EVENT1); + + Assert.Equal(SendEventLogState.Disable, exposureNotificationNotified); + } + + [Fact] + public void GetSendEventLogStateTest_Enable() + { + mockPreferencesService.Setup(s => s.ContainsKey(PreferenceKey.SendEventLogState)).Returns(true); + mockPreferencesService.Setup(s => s.GetStringValue(PreferenceKey.SendEventLogState, It.IsAny())) + .Returns("{ \"DummyEventType1-DummyEventSubtype1\": 1, \"DummyEventType2-DummyEventSubtype2\": -11 }"); + + var sendEventLogStateRepository = CreateRepository(); + + var exposureNotificationNotified = sendEventLogStateRepository + .GetSendEventLogState(DUMMY_EVENT1); + + Assert.Equal(SendEventLogState.Enable, exposureNotificationNotified); + } + + [Fact] + public void SetSendEventLogStateTest_Create() + { + mockPreferencesService.Setup(s => s.ContainsKey(PreferenceKey.SendEventLogState)).Returns(true); + mockPreferencesService.Setup(s => s.GetStringValue(PreferenceKey.SendEventLogState, It.IsAny())) + .Returns("{}"); + + var sendEventLogStateRepository = CreateRepository(); + + sendEventLogStateRepository + .SetSendEventLogState(DUMMY_EVENT1, SendEventLogState.Enable); + + mockPreferencesService.Verify(s => s.SetStringValue( + PreferenceKey.SendEventLogState, + "{\"DummyEventType1-DummyEventSubtype1\":1}" + ), Times.Once()); + } + + [Fact] + public void SetSendEventLogStateTest_Create_InvalidJson() + { + mockPreferencesService.Setup(s => s.ContainsKey(PreferenceKey.SendEventLogState)).Returns(true); + mockPreferencesService.Setup(s => s.GetStringValue(PreferenceKey.SendEventLogState, It.IsAny())) + .Returns("{]"); + + var sendEventLogStateRepository = CreateRepository(); + + sendEventLogStateRepository + .SetSendEventLogState(DUMMY_EVENT1, SendEventLogState.Enable); + + mockPreferencesService.Verify(s => s.SetStringValue( + PreferenceKey.SendEventLogState, + "{\"DummyEventType1-DummyEventSubtype1\":1}" + ), Times.Once()); + } + + [Fact] + public void SetSendEventLogStateTest_Update() + { + mockPreferencesService.Setup(s => s.ContainsKey(PreferenceKey.SendEventLogState)).Returns(true); + mockPreferencesService.Setup(s => s.GetStringValue(PreferenceKey.SendEventLogState, It.IsAny())) + .Returns("{\"DummyEventType1-DummyEventSubtype1\": -1}"); + + var sendEventLogStateRepository = CreateRepository(); + + sendEventLogStateRepository + .SetSendEventLogState(DUMMY_EVENT1, SendEventLogState.Enable); + + mockPreferencesService.Verify(s => s.SetStringValue( + PreferenceKey.SendEventLogState, + "{\"DummyEventType1-DummyEventSubtype1\":1}" + ), Times.Once()); + } + + [Fact] + public void SetSendEventLogStateTest_Append() + { + mockPreferencesService.Setup(s => s.ContainsKey(PreferenceKey.SendEventLogState)).Returns(true); + mockPreferencesService.Setup(s => s.GetStringValue(PreferenceKey.SendEventLogState, It.IsAny())) + .Returns("{ \"DummyEventType0-DummyEventSubtype0\": -1 }"); + + var sendEventLogStateRepository = CreateRepository(); + + sendEventLogStateRepository + .SetSendEventLogState(DUMMY_EVENT1, SendEventLogState.Enable); + + mockPreferencesService.Verify(s => s.SetStringValue( + PreferenceKey.SendEventLogState, + "{\"DummyEventType0-DummyEventSubtype0\":-1,\"DummyEventType1-DummyEventSubtype1\":1}" + ), Times.Once()); + } + } +} diff --git a/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/EventLogServiceTests.cs b/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/EventLogServiceTests.cs new file mode 100644 index 000000000..9a9d5e5e0 --- /dev/null +++ b/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/EventLogServiceTests.cs @@ -0,0 +1,325 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Covid19Radar.Model; +using Covid19Radar.Repository; +using Covid19Radar.Services; +using Covid19Radar.Services.Logs; +using Moq; +using Xunit; + +namespace Covid19Radar.UnitTests.Services +{ + public class EventLogServiceTests + { + private readonly MockRepository mockRepository; + + private readonly Mock _sendEventLogStateRepository; + private readonly Mock _eventLogRepository; + private readonly Mock _essentialsService; + private readonly Mock _deviceVerifier; + private readonly Mock _httpDataService; + + private readonly Mock _loggerService; + + public EventLogServiceTests() + { + mockRepository = new MockRepository(MockBehavior.Default); + + _sendEventLogStateRepository = mockRepository.Create(); + _eventLogRepository = mockRepository.Create(); + _essentialsService = mockRepository.Create(); + _deviceVerifier = mockRepository.Create(); + _httpDataService = mockRepository.Create(); + _loggerService = mockRepository.Create(); + } + + private EventLogService CreateService() + { + return new EventLogService( + _sendEventLogStateRepository.Object, + _eventLogRepository.Object, + _essentialsService.Object, + _deviceVerifier.Object, + _httpDataService.Object, + _loggerService.Object); + } + + private List CreateDummyEventLogList() + { + return new List() + { + new EventLog() + { + HasConsent = true, + Epoch = 1, + Type = EventType.ExposureNotified.Type, + Subtype = EventType.ExposureNotified.SubType, + Content = "{\"key\" : \"value2\"}", + }, + new EventLog() + { + HasConsent = true, + Epoch = 2, + Type = EventType.ExposureData.Type, + Subtype = EventType.ExposureData.SubType, + Content = "{\"key\" : \"value3\"}", + }, + new EventLog() + { + HasConsent = false, + Epoch = 1, + Type = EventType.ExposureNotified.Type, + Subtype = EventType.ExposureNotified.SubType, + Content = "{\"key\" : \"value1\"}", + } + }; + } + + [Fact] + public async Task SendAllAsyncTest1() + { + // Dummy data + List dummyEventLogList = CreateDummyEventLogList(); + + _httpDataService.Setup(x => x.PutEventLog(It.IsAny())) + .ReturnsAsync(new ApiResponse((int)HttpStatusCode.Created, "")); + + // Mock Setup + _sendEventLogStateRepository + .Setup(x => x.GetSendEventLogState(EventType.ExposureNotified)) + .Returns(SendEventLogState.Enable); + _sendEventLogStateRepository + .Setup(x => x.GetSendEventLogState(EventType.ExposureData)) + .Returns(SendEventLogState.Enable); + _eventLogRepository.Setup(x => x.GetLogsAsync(long.MaxValue)) + .ReturnsAsync(dummyEventLogList); + + // Test Case + var unitUnderTest = CreateService(); + await unitUnderTest.SendAllAsync( + long.MaxValue, + 1, + 100 + ); + + _loggerService.Verify(x => x.Warning(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + _loggerService.Verify(x => x.Info("Event log send successful.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Error(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + + _httpDataService.Verify(x => x.PutEventLog(It.Is(x => x.EventLogs.Count == 2)), Times.Once()); + + _deviceVerifier.Verify(x => x.VerifyAsync(It.IsAny()), Times.Once()); + + Assert.True(dummyEventLogList[0].HasConsent); + _eventLogRepository.Verify(x => x.RemoveAsync(dummyEventLogList[0]), Times.Once()); + + Assert.True(dummyEventLogList[1].HasConsent); + _eventLogRepository.Verify(x => x.RemoveAsync(dummyEventLogList[1]), Times.Once()); + } + + [Fact] + public async Task SendAllAsyncTest_withdrawn_consent() + { + // Dummy data + List dummyEventLogList = CreateDummyEventLogList(); + + // Mock Setup + _httpDataService.Setup(x => x.PutEventLog(It.IsAny())) + .ReturnsAsync(new ApiResponse((int)HttpStatusCode.Created, "")); + + _sendEventLogStateRepository + .Setup(x => x.GetSendEventLogState(EventType.ExposureNotified)) + .Returns(SendEventLogState.Enable); + + // Consent withdrawn + _sendEventLogStateRepository + .Setup(x => x.GetSendEventLogState(EventType.ExposureData)) + .Returns(SendEventLogState.Disable); + + _eventLogRepository.Setup(x => x.GetLogsAsync(long.MaxValue)) + .ReturnsAsync(dummyEventLogList); + + // Test Case + var unitUnderTest = CreateService(); + await unitUnderTest.SendAllAsync( + long.MaxValue, + 1, + 100 + ); + + _loggerService.Verify(x => x.Warning(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + _loggerService.Verify(x => x.Info("Event log send successful.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Error("Event log send failed all.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + + _httpDataService.Verify(x => x.PutEventLog(It.Is(x => x.EventLogs.Count == 1 && !x.EventLogs.Exists(x => x.Type != EventType.ExposureNotified.Type))), Times.Once()); + + _deviceVerifier.Verify(x => x.VerifyAsync(It.IsAny()), Times.Once()); + + Assert.True(dummyEventLogList[0].HasConsent); + _eventLogRepository.Verify(x => x.RemoveAsync(dummyEventLogList[0]), Times.Once()); + + // Consent withdrawn + Assert.False(dummyEventLogList[1].HasConsent); + _eventLogRepository.Verify(x => x.RemoveAsync(dummyEventLogList[1]), Times.Once()); + } + + [Fact] + public async Task SendAllAsyncTest_RetrySuccess() + { + // Dummy data + List dummyEventLogList = CreateDummyEventLogList(); + + // Mock Setup + _httpDataService.SetupSequence(x => x.PutEventLog(It.IsAny())) + .ReturnsAsync(new ApiResponse((int)HttpStatusCode.BadGateway, "")) + .ReturnsAsync(new ApiResponse((int)HttpStatusCode.BadGateway, "")) + .ReturnsAsync(new ApiResponse((int)HttpStatusCode.Created, "")); + + _sendEventLogStateRepository + .Setup(x => x.GetSendEventLogState(EventType.ExposureNotified)) + .Returns(SendEventLogState.Enable); + _sendEventLogStateRepository + .Setup(x => x.GetSendEventLogState(EventType.ExposureData)) + .Returns(SendEventLogState.Disable); + + _eventLogRepository.Setup(x => x.GetLogsAsync(long.MaxValue)) + .ReturnsAsync(dummyEventLogList); + + // Test Case + var unitUnderTest = CreateService(); + await unitUnderTest.SendAllAsync( + long.MaxValue, + 3, + 100 + ); + + _loggerService.Verify(x => x.Warning("Event log send failed. tries:1", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. tries:2", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. tries:3", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + _loggerService.Verify(x => x.Info("Event log send successful.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Error("Event log send failed all.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + + _httpDataService.Verify(x => x.PutEventLog(It.IsAny()), Times.Exactly(3)); + _deviceVerifier.Verify(x => x.VerifyAsync(It.IsAny()), Times.Exactly(3)); + _eventLogRepository.Verify(x => x.RemoveAsync(It.IsAny()), Times.Exactly(dummyEventLogList.Count)); + } + + [Fact] + public async Task SendAllAsyncTest_RetryFailure() + { + // Dummy data + List dummyEventLogList = CreateDummyEventLogList(); + + // Mock Setup + _httpDataService.Setup(x => x.PutEventLog(It.IsAny())) + .ReturnsAsync(new ApiResponse((int)HttpStatusCode.BadRequest, "")); + + _sendEventLogStateRepository + .Setup(x => x.GetSendEventLogState(EventType.ExposureNotified)) + .Returns(SendEventLogState.Enable); + _sendEventLogStateRepository + .Setup(x => x.GetSendEventLogState(EventType.ExposureData)) + .Returns(SendEventLogState.Disable); + + _eventLogRepository.Setup(x => x.GetLogsAsync(long.MaxValue)) + .ReturnsAsync(dummyEventLogList); + + // Test Case + var unitUnderTest = CreateService(); + await unitUnderTest.SendAllAsync( + long.MaxValue, + 3, + 100 + ); + + _loggerService.Verify(x => x.Warning("Event log send failed. tries:1", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. tries:2", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. tries:3", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. tries:4", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + _loggerService.Verify(x => x.Info("Event log send successful.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + _loggerService.Verify(x => x.Error("Event log send failed all.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + + _httpDataService.Verify(x => x.PutEventLog(It.IsAny()), Times.Exactly(4)); + _deviceVerifier.Verify(x => x.VerifyAsync(It.IsAny()), Times.Exactly(4)); + _eventLogRepository.Verify(x => x.RemoveAsync(It.IsAny()), Times.Never()); + } + + [Fact] + public async Task SendAllAsyncTest_RetryException() + { + // Dummy data + List dummyEventLogList = CreateDummyEventLogList(); + + // Mock Setup + _httpDataService.Setup(x => x.PutEventLog(It.IsAny())) + .Throws(new Exception()); + + _sendEventLogStateRepository + .Setup(x => x.GetSendEventLogState(EventType.ExposureNotified)) + .Returns(SendEventLogState.Enable); + _sendEventLogStateRepository + .Setup(x => x.GetSendEventLogState(EventType.ExposureData)) + .Returns(SendEventLogState.Disable); + + _eventLogRepository.Setup(x => x.GetLogsAsync(long.MaxValue)) + .ReturnsAsync(dummyEventLogList); + + // Test Case + var unitUnderTest = CreateService(); + await unitUnderTest.SendAllAsync( + long.MaxValue, + 3, + 100 + ); + + _loggerService.Verify(x => x.Warning("Event log send failed. tries:1", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. tries:2", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. tries:3", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. tries:4", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + _loggerService.Verify(x => x.Info("Event log send successful.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + _loggerService.Verify(x => x.Error("Event log send failed all.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + + _httpDataService.Verify(x => x.PutEventLog(It.IsAny()), Times.Exactly(4)); + _deviceVerifier.Verify(x => x.VerifyAsync(It.IsAny()), Times.Exactly(4)); + _eventLogRepository.Verify(x => x.RemoveAsync(It.IsAny()), Times.Never()); + } + + [Fact] + public async Task SendAllAsyncTest_DisableAll() + { + // Dummy data + List dummyEventLogList = CreateDummyEventLogList(); + + // Mock Setup + _httpDataService.Setup(x => x.PutEventLog(It.IsAny())) + .ReturnsAsync(new ApiResponse((int)HttpStatusCode.Created, "")); + + _sendEventLogStateRepository + .Setup(x => x.GetSendEventLogState(EventType.ExposureNotified)) + .Returns(SendEventLogState.Disable); + _sendEventLogStateRepository + .Setup(x => x.GetSendEventLogState(EventType.ExposureData)) + .Returns(SendEventLogState.Disable); + + _eventLogRepository.Setup(x => x.GetLogsAsync(long.MaxValue)) + .ReturnsAsync(dummyEventLogList); + + // Test Case + var unitUnderTest = CreateService(); + await unitUnderTest.SendAllAsync( + long.MaxValue, + 1, + 100 + ); + + _httpDataService.Verify(x => x.PutEventLog(It.IsAny()), Times.Never()); + } + + } +} \ No newline at end of file From 8225eaf4225b6eb5072f8e7f60fbc2bb167baff7 Mon Sep 17 00:00:00 2001 From: cocoa-dev004 <66989461+cocoa-dev004@users.noreply.github.com> Date: Wed, 15 Jun 2022 16:32:32 +0900 Subject: [PATCH 3/5] Fixed code smells --- .../Covid19Radar/Model/V1EventLogRequest.cs | 20 +++++++++---------- .../Repository/EventLogRepository.cs | 2 +- .../Services/ExposureDetectionService.cs | 4 ---- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Covid19Radar/Covid19Radar/Model/V1EventLogRequest.cs b/Covid19Radar/Covid19Radar/Model/V1EventLogRequest.cs index 09becf837..d2500e83c 100644 --- a/Covid19Radar/Covid19Radar/Model/V1EventLogRequest.cs +++ b/Covid19Radar/Covid19Radar/Model/V1EventLogRequest.cs @@ -12,37 +12,37 @@ namespace Covid19Radar.Model public class V1EventLogRequest { [JsonProperty("idempotency_key")] - public string IdempotencyKey; + public string IdempotencyKey { get; set; } [JsonProperty("platform")] - public string Platform; + public string Platform { get; set; } [JsonProperty("appPackageName")] - public string AppPackageName; + public string AppPackageName { get; set; } [JsonProperty("deviceVerificationPayload")] - public string DeviceVerificationPayload; + public string DeviceVerificationPayload { get; set; } [JsonProperty("event_logs")] - public List EventLogs; + public List EventLogs { get; set; } } public class EventLog { [JsonProperty("has_consent")] - public bool HasConsent; + public bool HasConsent { get; set; } [JsonProperty("epoch")] - public long Epoch; + public long Epoch { get; set; } [JsonProperty("type")] - public string Type; + public string Type { get; set; } [JsonProperty("subtype")] - public string Subtype; + public string Subtype { get; set; } [JsonProperty("content")] - public string Content; + public string Content { get; set; } [JsonIgnore] public string Timestamp diff --git a/Covid19Radar/Covid19Radar/Repository/EventLogRepository.cs b/Covid19Radar/Covid19Radar/Repository/EventLogRepository.cs index 21b4f5ebe..7f930cce6 100644 --- a/Covid19Radar/Covid19Radar/Repository/EventLogRepository.cs +++ b/Covid19Radar/Covid19Radar/Repository/EventLogRepository.cs @@ -274,7 +274,7 @@ private async Task AddEventNotifiedAsyncInternal(long maxSize) public class EventContentExposureNotified { [JsonProperty("notified_time_in_millis")] - public long NotifiedTimeInMillis; + public long NotifiedTimeInMillis { get; set; } public string ToJsonString() => JsonConvert.SerializeObject(this, Formatting.Indented); } diff --git a/Covid19Radar/Covid19Radar/Services/ExposureDetectionService.cs b/Covid19Radar/Covid19Radar/Services/ExposureDetectionService.cs index 4a6bfbbc8..cbbde36f8 100644 --- a/Covid19Radar/Covid19Radar/Services/ExposureDetectionService.cs +++ b/Covid19Radar/Covid19Radar/Services/ExposureDetectionService.cs @@ -93,8 +93,6 @@ public async Task ExposureDetectedAsync(ExposureConfiguration exposureConfigurat { _loggerService.Debug("ExposureDetected: ExposureWindows"); - var enVersionStr = enVersion.ToString(); - var (newDailySummaries, newExposureWindows) = await _exposureDataRepository.SetExposureDataAsync( dailySummaries.ToList(), exposureWindows.ToList() @@ -141,8 +139,6 @@ public async Task ExposureDetectedAsync(ExposureConfiguration exposureConfigurat { _loggerService.Info("ExposureDetected: Legacy-V1"); - var enVersionStr = enVersion.ToString(); - ExposureConfiguration.GoogleExposureConfiguration configurationV1 = exposureConfiguration.GoogleExposureConfig; bool isNewExposureDetected = _exposureDataRepository.AppendExposureData( From 6958ff79ca89a7d7432164df2c0cf15e75ecec8b Mon Sep 17 00:00:00 2001 From: cocoa-dev004 <66989461+cocoa-dev004@users.noreply.github.com> Date: Thu, 16 Jun 2022 12:04:57 +0900 Subject: [PATCH 4/5] Fixed retry and device verification --- COPYRIGHT_THIRD_PARTY_SOFTWARE_NOTICES.md | 33 +++++ .../Covid19Radar.Android/Assets/license.html | 32 +++++ .../EventLogSubmissionBackgroundService.cs | 3 +- .../Covid19Radar.iOS/Resources/license.html | 32 +++++ .../EventLogSubmissionBackgroundService.cs | 3 +- .../Covid19Radar/Common/AppConstants.cs | 7 +- Covid19Radar/Covid19Radar/Covid19Radar.csproj | 2 + .../Covid19Radar/Services/EventLogService.cs | 70 ++++++----- .../Services/EventLogServiceNop.cs | 2 +- .../Services/EventLogServiceTests.cs | 116 +++++++++++------- 10 files changed, 216 insertions(+), 84 deletions(-) diff --git a/COPYRIGHT_THIRD_PARTY_SOFTWARE_NOTICES.md b/COPYRIGHT_THIRD_PARTY_SOFTWARE_NOTICES.md index 13d155c5d..f8720ea58 100644 --- a/COPYRIGHT_THIRD_PARTY_SOFTWARE_NOTICES.md +++ b/COPYRIGHT_THIRD_PARTY_SOFTWARE_NOTICES.md @@ -432,6 +432,39 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- +## Polly +--- + +
+New BSD License
+=
+Copyright (c) 2015-2020, App vNext
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of App vNext nor the
+      names of its contributors may be used to endorse or promote products
+      derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL  BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ --- ## (Sample Code) Exposure Notifications API: Android Reference Design --- diff --git a/Covid19Radar/Covid19Radar.Android/Assets/license.html b/Covid19Radar/Covid19Radar.Android/Assets/license.html index 64dc5dfe3..5d32566d3 100644 --- a/Covid19Radar/Covid19Radar.Android/Assets/license.html +++ b/Covid19Radar/Covid19Radar.Android/Assets/license.html @@ -1307,6 +1307,38 @@

System.IdentityModel.Tokens.Jwt

LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+
+

Polly

+
+
+New BSD License
+=
+Copyright (c) 2015-2020, App vNext
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of App vNext nor the
+      names of its contributors may be used to endorse or promote products
+      derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL  BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+

(Sample Code) Exposure Notifications API: Android Reference Design


diff --git a/Covid19Radar/Covid19Radar.Android/Services/EventLogSubmissionBackgroundService.cs b/Covid19Radar/Covid19Radar.Android/Services/EventLogSubmissionBackgroundService.cs index 2c276e8fa..11daf9f39 100644 --- a/Covid19Radar/Covid19Radar.Android/Services/EventLogSubmissionBackgroundService.cs +++ b/Covid19Radar/Covid19Radar.Android/Services/EventLogSubmissionBackgroundService.cs @@ -83,8 +83,7 @@ public override Result DoWork() { eventLogService.SendAllAsync( AppConstants.EventLogMaxRequestSizeInBytes, - AppConstants.EventLogMaxRetry, - AppConstants.EventLogRetryInternval); + AppConstants.EventLogMaxRetry); return Result.InvokeSuccess(); } catch (Exception exception) diff --git a/Covid19Radar/Covid19Radar.iOS/Resources/license.html b/Covid19Radar/Covid19Radar.iOS/Resources/license.html index 64dc5dfe3..5d32566d3 100644 --- a/Covid19Radar/Covid19Radar.iOS/Resources/license.html +++ b/Covid19Radar/Covid19Radar.iOS/Resources/license.html @@ -1307,6 +1307,38 @@

System.IdentityModel.Tokens.Jwt

LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+
+

Polly

+
+
+New BSD License
+=
+Copyright (c) 2015-2020, App vNext
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of App vNext nor the
+      names of its contributors may be used to endorse or promote products
+      derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL  BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+

(Sample Code) Exposure Notifications API: Android Reference Design


diff --git a/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs b/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs index 3b77a5fec..fb45d3428 100644 --- a/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs +++ b/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs @@ -60,8 +60,7 @@ private void HandleSendLogAsync(BGAppRefreshTask task) { await _eventLogService.SendAllAsync( AppConstants.EventLogMaxRequestSizeInBytes, - AppConstants.EventLogMaxRetry, - AppConstants.EventLogRetryInternval); + AppConstants.EventLogMaxRetry); task.SetTaskCompleted(true); } catch (OperationCanceledException exception) diff --git a/Covid19Radar/Covid19Radar/Common/AppConstants.cs b/Covid19Radar/Covid19Radar/Common/AppConstants.cs index c8f094ffc..c240e0e66 100644 --- a/Covid19Radar/Covid19Radar/Common/AppConstants.cs +++ b/Covid19Radar/Covid19Radar/Common/AppConstants.cs @@ -89,12 +89,7 @@ public static readonly DateTime COCOA_FIRST_RELEASE_DATE /// /// Number of retries to send event log. /// - public const int EventLogMaxRetry = 4; - - /// - /// Retry interval for sending event logs. - /// - public const int EventLogRetryInternval = 1000; + public const int EventLogMaxRetry = 3; #region Other Private Methods diff --git a/Covid19Radar/Covid19Radar/Covid19Radar.csproj b/Covid19Radar/Covid19Radar/Covid19Radar.csproj index 4e7d4428e..cb2c13897 100644 --- a/Covid19Radar/Covid19Radar/Covid19Radar.csproj +++ b/Covid19Radar/Covid19Radar/Covid19Radar.csproj @@ -27,6 +27,7 @@ +
@@ -72,6 +73,7 @@ + diff --git a/Covid19Radar/Covid19Radar/Services/EventLogService.cs b/Covid19Radar/Covid19Radar/Services/EventLogService.cs index 2f34bd355..4e7788cba 100644 --- a/Covid19Radar/Covid19Radar/Services/EventLogService.cs +++ b/Covid19Radar/Covid19Radar/Services/EventLogService.cs @@ -11,12 +11,13 @@ using Covid19Radar.Model; using Covid19Radar.Repository; using Covid19Radar.Services.Logs; +using Polly; namespace Covid19Radar.Services { public interface IEventLogService { - public Task SendAllAsync(long maxSize, int maxRetry, int retryInterval); + public Task SendAllAsync(long maxSize, int maxRetry); } public class EventLogService : IEventLogService @@ -47,13 +48,13 @@ ILoggerService loggerService _loggerService = loggerService; } - public async Task SendAllAsync(long maxSize, int maxRetry, int retryInterval) + public async Task SendAllAsync(long maxSize, int maxRetry) { await _semaphore.WaitAsync(); try { - await SendAllInternalAsync(maxSize, maxRetry, retryInterval); + await SendAllInternalAsync(maxSize, maxRetry); } finally { @@ -61,7 +62,7 @@ public async Task SendAllAsync(long maxSize, int maxRetry, int retryInterval) } } - private async Task SendAllInternalAsync(long maxSize, int maxRetry, int retryInterval) + private async Task SendAllInternalAsync(long maxSize, int maxRetry) { _loggerService.StartMethod(); @@ -98,33 +99,27 @@ List agreedEventLogList return; } - int tries = 0; - while (true) - { - bool isSuccess = await SendAsync(idempotencyKey, agreedEventLogList); - - if (isSuccess) - { - _loggerService.Info($"Event log send successful."); - - _loggerService.Info($"Clean up..."); - foreach (var eventLog in eventLogList) - { - await _eventLogRepository.RemoveAsync(eventLog); - } + PolicyResult policyResult = await Policy + .HandleResult(result => !result) + .WaitAndRetryAsync(maxRetry, retryAttempt => { + double delay = Math.Pow(2, retryAttempt - 1); + _loggerService.Warning($"Event log send failed. retryAttempt:{retryAttempt} delay:{delay}sec"); + return TimeSpan.FromSeconds(delay); + }) + .ExecuteAndCaptureAsync(() => SendAsync(idempotencyKey, agreedEventLogList)); - break; - } - else if (tries >= maxRetry) - { - _loggerService.Error("Event log send failed all."); - break; - } + if (policyResult.Outcome == OutcomeType.Failure) + { + _loggerService.Error("Event log send failed all."); + return; + } - _loggerService.Warning($"Event log send failed. tries:{tries + 1}"); - await Task.Delay(retryInterval); + _loggerService.Info($"Event log send successful."); - tries++; + _loggerService.Info($"Clean up..."); + foreach (var eventLog in eventLogList) + { + await _eventLogRepository.RemoveAsync(eventLog); } _loggerService.Info($"Done."); @@ -149,7 +144,24 @@ private async Task SendAsync(string idempotencyKey, List eventLo EventLogs = eventLogList, }; - request.DeviceVerificationPayload = await _deviceVerifier.VerifyAsync(request); + // Create device verification payload + PolicyResult policyResult = await Policy + .HandleResult(result => _deviceVerifier.IsErrorPayload(result)) + .WaitAndRetryAsync(3, retryAttempt => { + double delay = Math.Pow(2, retryAttempt + 1); + _loggerService.Warning($"Payload creation failed. retryAttempt:{retryAttempt} delay:{delay}sec"); + return TimeSpan.FromSeconds(delay); + }) + .ExecuteAndCaptureAsync(() => _deviceVerifier.VerifyAsync(request)); + + if (policyResult.Outcome == OutcomeType.Failure) + { + _loggerService.Error("Payload creation failed all."); + return false; + } + + _loggerService.Info("Payload creation successful."); + request.DeviceVerificationPayload = policyResult.Result; ApiResponse response = await _httpDataService.PutEventLog(request); _loggerService.Info($"PutEventLog() StatusCode:{response.StatusCode}"); diff --git a/Covid19Radar/Covid19Radar/Services/EventLogServiceNop.cs b/Covid19Radar/Covid19Radar/Services/EventLogServiceNop.cs index 8fea52571..d3d96afac 100644 --- a/Covid19Radar/Covid19Radar/Services/EventLogServiceNop.cs +++ b/Covid19Radar/Covid19Radar/Services/EventLogServiceNop.cs @@ -10,7 +10,7 @@ namespace Covid19Radar.Services { public class EventLogServiceNop : IEventLogService { - public Task SendAllAsync(long maxSize, int maxRetry, int retryInterval) + public Task SendAllAsync(long maxSize, int maxRetry) { // do nothing return Task.FromResult(new List()); diff --git a/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/EventLogServiceTests.cs b/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/EventLogServiceTests.cs index 9a9d5e5e0..941e3830d 100644 --- a/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/EventLogServiceTests.cs +++ b/Covid19Radar/Tests/Covid19Radar.UnitTests/Services/EventLogServiceTests.cs @@ -100,13 +100,11 @@ public async Task SendAllAsyncTest1() _eventLogRepository.Setup(x => x.GetLogsAsync(long.MaxValue)) .ReturnsAsync(dummyEventLogList); + _deviceVerifier.Setup(x => x.IsErrorPayload(It.IsAny())).Returns(false); + // Test Case var unitUnderTest = CreateService(); - await unitUnderTest.SendAllAsync( - long.MaxValue, - 1, - 100 - ); + await unitUnderTest.SendAllAsync(long.MaxValue, 1); _loggerService.Verify(x => x.Warning(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); _loggerService.Verify(x => x.Info("Event log send successful.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); @@ -145,13 +143,11 @@ public async Task SendAllAsyncTest_withdrawn_consent() _eventLogRepository.Setup(x => x.GetLogsAsync(long.MaxValue)) .ReturnsAsync(dummyEventLogList); + _deviceVerifier.Setup(x => x.IsErrorPayload(It.IsAny())).Returns(false); + // Test Case var unitUnderTest = CreateService(); - await unitUnderTest.SendAllAsync( - long.MaxValue, - 1, - 100 - ); + await unitUnderTest.SendAllAsync(long.MaxValue, 1); _loggerService.Verify(x => x.Warning(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); _loggerService.Verify(x => x.Info("Event log send successful.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); @@ -191,17 +187,15 @@ public async Task SendAllAsyncTest_RetrySuccess() _eventLogRepository.Setup(x => x.GetLogsAsync(long.MaxValue)) .ReturnsAsync(dummyEventLogList); + _deviceVerifier.Setup(x => x.IsErrorPayload(It.IsAny())).Returns(false); + // Test Case var unitUnderTest = CreateService(); - await unitUnderTest.SendAllAsync( - long.MaxValue, - 3, - 100 - ); - - _loggerService.Verify(x => x.Warning("Event log send failed. tries:1", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); - _loggerService.Verify(x => x.Warning("Event log send failed. tries:2", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); - _loggerService.Verify(x => x.Warning("Event log send failed. tries:3", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + await unitUnderTest.SendAllAsync(long.MaxValue, 3); + + _loggerService.Verify(x => x.Warning("Event log send failed. retryAttempt:1 delay:1sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. retryAttempt:2 delay:2sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. retryAttempt:3 delay:4sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); _loggerService.Verify(x => x.Info("Event log send successful.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); _loggerService.Verify(x => x.Error("Event log send failed all.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); @@ -230,18 +224,16 @@ public async Task SendAllAsyncTest_RetryFailure() _eventLogRepository.Setup(x => x.GetLogsAsync(long.MaxValue)) .ReturnsAsync(dummyEventLogList); + _deviceVerifier.Setup(x => x.IsErrorPayload(It.IsAny())).Returns(false); + // Test Case var unitUnderTest = CreateService(); - await unitUnderTest.SendAllAsync( - long.MaxValue, - 3, - 100 - ); - - _loggerService.Verify(x => x.Warning("Event log send failed. tries:1", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); - _loggerService.Verify(x => x.Warning("Event log send failed. tries:2", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); - _loggerService.Verify(x => x.Warning("Event log send failed. tries:3", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); - _loggerService.Verify(x => x.Warning("Event log send failed. tries:4", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + await unitUnderTest.SendAllAsync(long.MaxValue, 3); + + _loggerService.Verify(x => x.Warning("Event log send failed. retryAttempt:1 delay:1sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. retryAttempt:2 delay:2sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. retryAttempt:3 delay:4sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. retryAttempt:4 delay:8sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); _loggerService.Verify(x => x.Info("Event log send successful.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); _loggerService.Verify(x => x.Error("Event log send failed all.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); @@ -270,18 +262,16 @@ public async Task SendAllAsyncTest_RetryException() _eventLogRepository.Setup(x => x.GetLogsAsync(long.MaxValue)) .ReturnsAsync(dummyEventLogList); + _deviceVerifier.Setup(x => x.IsErrorPayload(It.IsAny())).Returns(false); + // Test Case var unitUnderTest = CreateService(); - await unitUnderTest.SendAllAsync( - long.MaxValue, - 3, - 100 - ); - - _loggerService.Verify(x => x.Warning("Event log send failed. tries:1", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); - _loggerService.Verify(x => x.Warning("Event log send failed. tries:2", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); - _loggerService.Verify(x => x.Warning("Event log send failed. tries:3", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); - _loggerService.Verify(x => x.Warning("Event log send failed. tries:4", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + await unitUnderTest.SendAllAsync(long.MaxValue, 3); + + _loggerService.Verify(x => x.Warning("Event log send failed. retryAttempt:1 delay:1sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. retryAttempt:2 delay:2sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. retryAttempt:3 delay:4sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Event log send failed. retryAttempt:4 delay:8sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); _loggerService.Verify(x => x.Info("Event log send successful.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); _loggerService.Verify(x => x.Error("Event log send failed all.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); @@ -310,16 +300,54 @@ public async Task SendAllAsyncTest_DisableAll() _eventLogRepository.Setup(x => x.GetLogsAsync(long.MaxValue)) .ReturnsAsync(dummyEventLogList); + _deviceVerifier.Setup(x => x.IsErrorPayload(It.IsAny())).Returns(false); + // Test Case var unitUnderTest = CreateService(); - await unitUnderTest.SendAllAsync( - long.MaxValue, - 1, - 100 - ); + await unitUnderTest.SendAllAsync(long.MaxValue, 1); _httpDataService.Verify(x => x.PutEventLog(It.IsAny()), Times.Never()); } + [Fact] + public async Task SendAllAsyncTest_DeviceVerifierFailure() + { + // Dummy data + List dummyEventLogList = CreateDummyEventLogList(); + + // Mock Setup + _httpDataService.Setup(x => x.PutEventLog(It.IsAny())) + .ReturnsAsync(new ApiResponse((int)HttpStatusCode.Created, "")); + + _sendEventLogStateRepository + .Setup(x => x.GetSendEventLogState(EventType.ExposureNotified)) + .Returns(SendEventLogState.Enable); + _sendEventLogStateRepository + .Setup(x => x.GetSendEventLogState(EventType.ExposureData)) + .Returns(SendEventLogState.Disable); + + _eventLogRepository.Setup(x => x.GetLogsAsync(long.MaxValue)) + .ReturnsAsync(dummyEventLogList); + + _deviceVerifier.Setup(x => x.IsErrorPayload(It.IsAny())).Returns(true); + + // Test Case + var unitUnderTest = CreateService(); + await unitUnderTest.SendAllAsync(long.MaxValue, 0); + + _loggerService.Verify(x => x.Warning("Payload creation failed. retryAttempt:1 delay:4sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Payload creation failed. retryAttempt:2 delay:8sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Payload creation failed. retryAttempt:3 delay:16sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _loggerService.Verify(x => x.Warning("Payload creation failed. retryAttempt:4 delay:32sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + + _loggerService.Verify(x => x.Warning("Event log send failed. retryAttempt:1 delay:1sec", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + + _loggerService.Verify(x => x.Info("Event log send successful.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + _loggerService.Verify(x => x.Error("Event log send failed all.", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + + _httpDataService.Verify(x => x.PutEventLog(It.IsAny()), Times.Never()); + _deviceVerifier.Verify(x => x.VerifyAsync(It.IsAny()), Times.Exactly(4)); + _eventLogRepository.Verify(x => x.RemoveAsync(It.IsAny()), Times.Never()); + } } } \ No newline at end of file From 642f67ede23edadd62c322f8d038d76925c0d7cc Mon Sep 17 00:00:00 2001 From: cocoa-dev004 <66989461+cocoa-dev004@users.noreply.github.com> Date: Fri, 17 Jun 2022 10:43:54 +0900 Subject: [PATCH 5/5] Fixed the review pointing out --- Covid19Radar/Covid19Radar.Android/MainApplication.cs | 4 ++-- Covid19Radar/Covid19Radar.iOS/AppDelegate.cs | 4 ++-- .../Services/EventLogSubmissionBackgroundService.cs | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Covid19Radar/Covid19Radar.Android/MainApplication.cs b/Covid19Radar/Covid19Radar.Android/MainApplication.cs index 0c357e822..016bfe599 100644 --- a/Covid19Radar/Covid19Radar.Android/MainApplication.cs +++ b/Covid19Radar/Covid19Radar.Android/MainApplication.cs @@ -93,7 +93,7 @@ private void ScheduleBackgroundTasks() } catch (Exception exception) { - _loggerService.Value.Exception("failed to Scheduling ExposureDetectionBackgroundService", exception); + _loggerService.Value.Exception("Failed to schedule ExposureDetectionBackgroundService", exception); } try { @@ -101,7 +101,7 @@ private void ScheduleBackgroundTasks() } catch (Exception exception) { - _loggerService.Value.Exception("failed to Scheduling EventLogSubmissionBackgroundService", exception); + _loggerService.Value.Exception("Failed to schedule EventLogSubmissionBackgroundService", exception); } } diff --git a/Covid19Radar/Covid19Radar.iOS/AppDelegate.cs b/Covid19Radar/Covid19Radar.iOS/AppDelegate.cs index c4fabdbb5..6c491c375 100644 --- a/Covid19Radar/Covid19Radar.iOS/AppDelegate.cs +++ b/Covid19Radar/Covid19Radar.iOS/AppDelegate.cs @@ -125,7 +125,7 @@ private void ScheduleBackgroundTask() } catch (Exception exception) { - _loggerService.Value.Exception("failed to Scheduling", exception); + _loggerService.Value.Exception("Failed to schedule ExposureDetectionBackgroundService", exception); } try { @@ -133,7 +133,7 @@ private void ScheduleBackgroundTask() } catch (Exception exception) { - _loggerService.Value.Exception("failed to Scheduling EventLogSubmissionBackgroundService", exception); + _loggerService.Value.Exception("Failed to schedule EventLogSubmissionBackgroundService", exception); } } diff --git a/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs b/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs index fb45d3428..eb6b65720 100644 --- a/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs +++ b/Covid19Radar/Covid19Radar.iOS/Services/EventLogSubmissionBackgroundService.cs @@ -34,6 +34,8 @@ ILoggerService loggerService public override void Schedule() { + _loggerService.StartMethod(); + _ = BGTaskScheduler.Shared.Register(IDENTIFIER, null, task => { HandleSendLogAsync((BGAppRefreshTask)task);