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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions DatabaseProjectAPI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabaseProjectAPI", "Datab
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KubsConnect", "KubsConnect\KubsConnect.csproj", "{3ED8AE85-9161-47E3-95AC-B4A224D49AAA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XUnitTest", "XUnitTest\XUnitTest.csproj", "{63016646-DDEC-41FA-92A1-4F33E9EAD8CE}"
ProjectSection(ProjectDependencies) = postProject
{BCFC56D8-061F-4C1C-AA30-41E784E4AE13} = {BCFC56D8-061F-4C1C-AA30-41E784E4AE13}
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -21,6 +26,10 @@ Global
{3ED8AE85-9161-47E3-95AC-B4A224D49AAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3ED8AE85-9161-47E3-95AC-B4A224D49AAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3ED8AE85-9161-47E3-95AC-B4A224D49AAA}.Release|Any CPU.Build.0 = Release|Any CPU
{63016646-DDEC-41FA-92A1-4F33E9EAD8CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{63016646-DDEC-41FA-92A1-4F33E9EAD8CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{63016646-DDEC-41FA-92A1-4F33E9EAD8CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{63016646-DDEC-41FA-92A1-4F33E9EAD8CE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
131 changes: 64 additions & 67 deletions DatabaseProjectAPI/Actions/AutoDeleteAction.cs
Original file line number Diff line number Diff line change
@@ -1,91 +1,88 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using DatabaseProjectAPI.DataContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using DatabaseProjectAPI.DataContext;

namespace DatabaseProjectAPI.Actions
namespace DatabaseProjectAPI.Actions;

public interface IAutoDeleteService
{
Task DeleteOldStockHistoryAsync(CancellationToken cancellationToken);
Task DeleteOldApiCallLogsAsync(CancellationToken cancellationToken);
}

public class AutoDeleteAction : IAutoDeleteService
{
public interface IAutoDeleteService
private readonly DpapiDbContext _dbContext;
private readonly ILogger<AutoDeleteAction> _logger;

public AutoDeleteAction(DpapiDbContext dbContext, ILogger<AutoDeleteAction> logger)
{
Task DeleteOldStockHistoryAsync(CancellationToken cancellationToken);
Task DeleteOldApiCallLogsAsync(CancellationToken cancellationToken);
_dbContext = dbContext;
_logger = logger;
}

public class AutoDeleteAction : IAutoDeleteService
public async Task DeleteOldStockHistoryAsync(CancellationToken cancellationToken)
{
private readonly DpapiDbContext _dbContext;
private readonly ILogger<AutoDeleteAction> _logger;
var ninetyDaysAgo = DateTime.UtcNow.AddDays(-90);

public AutoDeleteAction(DpapiDbContext dbContext, ILogger<AutoDeleteAction> logger)
try
{
_dbContext = dbContext;
_logger = logger;
}

var oldHistories = await _dbContext.StockHistories
.Where(sh => sh.Timestamp < ninetyDaysAgo)
.ToListAsync(cancellationToken);

public async Task DeleteOldStockHistoryAsync(CancellationToken cancellationToken)
{
var ninetyDaysAgo = DateTime.UtcNow.AddDays(-90);

try
{

int deletedCount = await _dbContext.StockHistories
.Where(sh => sh.Timestamp < ninetyDaysAgo)
.ExecuteDeleteAsync(cancellationToken);

if (deletedCount > 0)
{
_logger.LogInformation("{Count} old stock history records deleted.", deletedCount);
}
else
{
_logger.LogInformation("No stock history records found to delete.");
}
}
catch (OperationCanceledException)
if (oldHistories.Any())
{
_logger.LogInformation("Deletion of old stock history was cancelled.");
throw;
_dbContext.StockHistories.RemoveRange(oldHistories);
await _dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation("{Count} old stock history records deleted.", oldHistories.Count);
}
catch (Exception ex)
else
{
_logger.LogError(ex, "An error occurred while deleting old stock history records.");
throw;
_logger.LogInformation("No stock history records found to delete.");
}
}

public async Task DeleteOldApiCallLogsAsync(CancellationToken cancellationToken)
catch (OperationCanceledException)
{
var ninetyDaysAgo = DateTime.UtcNow.AddDays(-90);
_logger.LogInformation("Deletion of old stock history was cancelled.");
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while deleting old stock history records.");
throw;
}
}

try
{
// Use ExecuteDeleteAsync for efficient bulk deletion (EF Core 7.0+)
int deletedCount = await _dbContext.ApiCallLog
.Where(log => log.CallDate < ninetyDaysAgo)
.ExecuteDeleteAsync(cancellationToken);
public async Task DeleteOldApiCallLogsAsync(CancellationToken cancellationToken)
{
var ninetyDaysAgo = DateTime.UtcNow.AddDays(-90);

if (deletedCount > 0)
{
_logger.LogInformation("{Count} old API call log records deleted.", deletedCount);
}
else
{
_logger.LogInformation("No API call log records found to delete.");
}
}
catch (OperationCanceledException)
try
{
var oldLogs = await _dbContext.ApiCallLog
.Where(log => log.CallDate < ninetyDaysAgo)
.ToListAsync(cancellationToken);

if (oldLogs.Any())
{
_logger.LogInformation("Deletion of old API call logs was cancelled.");
throw;
_dbContext.ApiCallLog.RemoveRange(oldLogs);
await _dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation("{Count} old API call log records deleted.", oldLogs.Count);
}
catch (Exception ex)
else
{
_logger.LogError(ex, "An error occurred while deleting old API call log records.");
throw;
_logger.LogInformation("No API call log records found to delete.");
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("Deletion of old API call logs was cancelled.");
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while deleting old API call log records.");
throw;
}
}
}
1 change: 1 addition & 0 deletions DatabaseProjectAPI/DatabaseProjectAPI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.10" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
Expand Down
2 changes: 1 addition & 1 deletion DatabaseProjectAPI/Services/StockQuoteBackgroundService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
private async Task FetchAndSaveStockDataAsync(DpapiDbContext dbContext, IApiRequestLogger apiRequestLogger, string callType, TrackedStock trackedStock, CancellationToken cancellationToken)
public async Task FetchAndSaveStockDataAsync(DpapiDbContext dbContext, IApiRequestLogger apiRequestLogger, string callType, TrackedStock trackedStock, CancellationToken cancellationToken)
{
var symbol = trackedStock.Symbol;
_logger.LogInformation("FetchAndSaveStockDataAsync started for symbol {Symbol} with call type {CallType}", symbol, callType);
Expand Down
54 changes: 54 additions & 0 deletions XUnitTest/APIrequestloggerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
namespace XUnitTests;
public class ApiRequestLoggerTests
{
private readonly DpapiDbContext _dbContext;
private readonly Mock<ILogger<ApiRequestLogger>> _loggerMock;
private readonly ApiRequestLogger _logger;

public ApiRequestLoggerTests()
{
var options = new DbContextOptionsBuilder<DpapiDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;

_dbContext = new DpapiDbContext(options);
_loggerMock = new Mock<ILogger<ApiRequestLogger>>();
_logger = new ApiRequestLogger(_dbContext, _loggerMock.Object);
}

[Fact]
public async Task HasMadeApiCallTodayAsync_ReturnsTrue_WhenLogExists()
{
var today = DateTime.UtcNow.Date;
await _dbContext.ApiCallLog.AddAsync(new ApiCallLog
{
CallDate = today,
CallType = "Quote",
Symbol = "AAPL"
});
await _dbContext.SaveChangesAsync();

var result = await _logger.HasMadeApiCallTodayAsync("Quote", "AAPL", CancellationToken.None);
Assert.True(result);
}

[Fact]
public async Task HasMadeApiCallTodayAsync_ReturnsFalse_WhenNoLogExists()
{
var result = await _logger.HasMadeApiCallTodayAsync("Quote", "MSFT", CancellationToken.None);
Assert.False(result);
}

[Fact]
public async Task LogApiCallAsync_AddsLogSuccessfully()
{
var symbol = "GOOG";
var callType = "News";

await _logger.LogApiCallAsync(callType, symbol, CancellationToken.None);

var entry = await _dbContext.ApiCallLog.FirstOrDefaultAsync(a => a.Symbol == symbol && a.CallType == callType);
Assert.NotNull(entry);
Assert.Equal(DateTime.UtcNow.Date, entry.CallDate);
}
}
100 changes: 100 additions & 0 deletions XUnitTest/AlphaVantageServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
namespace XUnitTests;
public class AlphaVantageServiceTests
{
private readonly Mock<HttpMessageHandler> _handlerMock;
private readonly Mock<ILogger<AlphaVantageService>> _loggerMock;
private readonly HttpClient _httpClient;
private readonly AlphaVantageService _service;

public AlphaVantageServiceTests()
{
_handlerMock = new Mock<HttpMessageHandler>();
_loggerMock = new Mock<ILogger<AlphaVantageService>>();

_httpClient = new HttpClient(_handlerMock.Object);
var settings = new AlphaVantageSettings { ApiKey = "demo" };

_service = new AlphaVantageService(_httpClient, settings, _loggerMock.Object);
}

[Fact]
public async Task GetStockQuoteAsync_ReturnsValidStockQuote()
{
var today = DateTime.UtcNow.Date;

var responseObject = new Dictionary<string, object>
{
["Global Quote"] = new Dictionary<string, object>
{
["01. symbol"] = "AAPL",
["02. open"] = "170.5",
["03. high"] = "173.0",
["04. low"] = "169.5",
["05. price"] = "172.1",
["06. volume"] = "5000000",
["07. latest trading day"] = today.ToString("yyyy-MM-dd"),
["08. previous close"] = "171.0",
["10. change percent"] = "0.64%"
}
};

var responseContent = JsonConvert.SerializeObject(responseObject);

_handlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(responseContent, Encoding.UTF8, "application/json")
});

var result = await _service.GetStockQuoteAsync("AAPL");

Assert.NotNull(result);
Assert.Equal("AAPL", result.Symbol);
Assert.Equal(170.5m, result.Open);
Assert.Equal(172.1m, result.Price);
Assert.Equal(5000000, result.Volume);
Assert.Equal(today, result.LatestTradingDay.Date);
}


[Fact]
public async Task GetStockQuoteAsync_ThrowsOnRateLimit()
{
var rateLimitResponse = JsonConvert.SerializeObject(new { Note = "Rate limit exceeded" });

_handlerMock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(rateLimitResponse)
});

await Assert.ThrowsAsync<AlphaVantageService.ApiRateLimitExceededException>(() => _service.GetStockQuoteAsync("AAPL"));
}

[Fact]
public async Task GetStockQuoteAsync_ThrowsOnErrorMessage()
{
var errorResponse = JsonConvert.SerializeObject(new { ErrorMessage = "Invalid API call" });

_handlerMock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(errorResponse)
});

await Assert.ThrowsAsync<AlphaVantageService.InvalidApiResponseException>(() => _service.GetStockQuoteAsync("INVALID"));
}
}
Loading