diff --git a/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.Core/Extensions/Caching/CacheKeys.cs b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.Core/Extensions/Caching/CacheKeys.cs index 19e0465..869b346 100644 --- a/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.Core/Extensions/Caching/CacheKeys.cs +++ b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.Core/Extensions/Caching/CacheKeys.cs @@ -68,6 +68,20 @@ public static class CacheKeys /// public const string EventsPattern = "thrive:events:*"; + // ============================================ + // Transcript Cache Keys + // ============================================ + + /// + /// Sermon transcript blob. Format: thrive:transcripts:blob:{messageId} + /// + public const string TranscriptBlob = "thrive:transcripts:blob:{0}"; + + /// + /// Pattern for invalidating all transcript caches + /// + public const string TranscriptsPattern = "thrive:transcripts:*"; + // ============================================ // Bible Passage Cache Keys // ============================================ diff --git a/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.Services/Services/TranscriptService.cs b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.Services/Services/TranscriptService.cs index 70ed293..1a43df2 100644 --- a/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.Services/Services/TranscriptService.cs +++ b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.Services/Services/TranscriptService.cs @@ -15,14 +15,23 @@ namespace ThriveChurchOfficialAPI.Services public class TranscriptService : ITranscriptService { private readonly BlobContainerClient _containerClient; + private readonly ICacheService _cache; + + /// + /// Cache expiration for transcript data (365 days - transcripts are immutable once created) + /// + private static readonly TimeSpan CacheExpiration = TimeSpan.FromDays(365); /// /// Constructor for TranscriptService /// /// Azure Storage connection string /// Name of the blob container storing transcripts - public TranscriptService(string connectionString, string containerName) + /// Cache service instance + public TranscriptService(string connectionString, string containerName, ICacheService cache) { + _cache = cache ?? throw new ArgumentNullException(nameof(cache)); + if (string.IsNullOrEmpty(connectionString)) { Log.Warning("TranscriptService initialized with empty connection string"); @@ -39,9 +48,11 @@ public TranscriptService(string connectionString, string containerName) /// Constructor for testing - allows injecting a mock container client /// /// Mock blob container client for testing - public TranscriptService(BlobContainerClient containerClient) + /// Cache service instance + public TranscriptService(BlobContainerClient containerClient, ICacheService cache) { _containerClient = containerClient; + _cache = cache ?? throw new ArgumentNullException(nameof(cache)); Log.Information("TranscriptService initialized with injected container client"); } @@ -212,10 +223,20 @@ public async Task> GetStudyGuideAsync(string #region Private Helper Methods /// - /// Downloads and parses the transcript blob + /// Downloads and parses the transcript blob with caching /// private async Task DownloadBlobAsync(string messageId) { + // Check cache first + var cacheKey = string.Format(CacheKeys.TranscriptBlob, messageId.ToLowerInvariant()); + var cachedBlob = _cache.ReadFromCache(cacheKey); + if (cachedBlob != null) + { + Log.Debug("Cache hit for transcript blob: {MessageId}", messageId); + return cachedBlob; + } + + // Cache miss - download from Azure Blob Storage var blobName = $"{messageId}.json"; var blobClient = _containerClient.GetBlobClient(blobName); @@ -227,7 +248,16 @@ private async Task DownloadBlobAsync(string messageId) var downloadResponse = await blobClient.DownloadContentAsync(); var content = downloadResponse.Value.Content.ToString(); - return JsonConvert.DeserializeObject(content); + var blob = JsonConvert.DeserializeObject(content); + + // Cache the result + if (blob != null) + { + _cache.InsertIntoCache(cacheKey, blob, CacheExpiration); + Log.Debug("Cached transcript blob for message: {MessageId}", messageId); + } + + return blob; } /// diff --git a/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.Tests/Services/TranscriptServiceTests.cs b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.Tests/Services/TranscriptServiceTests.cs index 26b1ac3..d41e740 100644 --- a/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.Tests/Services/TranscriptServiceTests.cs +++ b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI.Tests/Services/TranscriptServiceTests.cs @@ -1,5 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; using System.Threading.Tasks; +using ThriveChurchOfficialAPI.Core; using ThriveChurchOfficialAPI.Services; namespace ThriveChurchOfficialAPI.Tests.Services @@ -12,13 +14,21 @@ namespace ThriveChurchOfficialAPI.Tests.Services [TestClass] public class TranscriptServiceTests { + private Mock _mockCache; + + [TestInitialize] + public void Setup() + { + _mockCache = new Mock(); + } + #region Constructor Tests [TestMethod] public void Constructor_WithNullConnectionString_CreatesServiceWithWarning() { // Act - Using connection string constructor with null - var service = new TranscriptService(null, "transcripts"); + var service = new TranscriptService(null, "transcripts", _mockCache.Object); // Assert - Service should be created but will return errors when used Assert.IsNotNull(service); @@ -28,7 +38,7 @@ public void Constructor_WithNullConnectionString_CreatesServiceWithWarning() public void Constructor_WithEmptyConnectionString_CreatesServiceWithWarning() { // Act - var service = new TranscriptService(string.Empty, "transcripts"); + var service = new TranscriptService(string.Empty, "transcripts", _mockCache.Object); // Assert Assert.IsNotNull(service); @@ -42,7 +52,7 @@ public void Constructor_WithEmptyConnectionString_CreatesServiceWithWarning() public async Task GetTranscriptAsync_NullMessageId_ReturnsError() { // Arrange - var service = new TranscriptService(null, "transcripts"); + var service = new TranscriptService(null, "transcripts", _mockCache.Object); // Act var result = await service.GetTranscriptAsync(null); @@ -56,7 +66,7 @@ public async Task GetTranscriptAsync_NullMessageId_ReturnsError() public async Task GetTranscriptAsync_EmptyMessageId_ReturnsError() { // Arrange - var service = new TranscriptService(null, "transcripts"); + var service = new TranscriptService(null, "transcripts", _mockCache.Object); // Act var result = await service.GetTranscriptAsync(string.Empty); @@ -70,7 +80,7 @@ public async Task GetTranscriptAsync_EmptyMessageId_ReturnsError() public async Task GetTranscriptAsync_WhitespaceMessageId_ReturnsError() { // Arrange - var service = new TranscriptService(null, "transcripts"); + var service = new TranscriptService(null, "transcripts", _mockCache.Object); // Act var result = await service.GetTranscriptAsync(" "); @@ -87,7 +97,7 @@ public async Task GetTranscriptAsync_WhitespaceMessageId_ReturnsError() public async Task GetTranscriptAsync_ServiceNotConfigured_ReturnsConfigurationError() { // Arrange - Create service with null connection string - var unconfiguredService = new TranscriptService(null, "transcripts"); + var unconfiguredService = new TranscriptService(null, "transcripts", _mockCache.Object); var messageId = "507f1f77bcf86cd799439011"; // Act @@ -102,7 +112,7 @@ public async Task GetTranscriptAsync_ServiceNotConfigured_ReturnsConfigurationEr public async Task GetTranscriptAsync_EmptyConnectionString_ReturnsConfigurationError() { // Arrange - var service = new TranscriptService(string.Empty, "transcripts"); + var service = new TranscriptService(string.Empty, "transcripts", _mockCache.Object); var messageId = "507f1f77bcf86cd799439011"; // Act @@ -121,7 +131,7 @@ public async Task GetTranscriptAsync_EmptyConnectionString_ReturnsConfigurationE public async Task GetSermonNotesAsync_NullMessageId_ReturnsError() { // Arrange - var service = new TranscriptService(null, "transcripts"); + var service = new TranscriptService(null, "transcripts", _mockCache.Object); // Act var result = await service.GetSermonNotesAsync(null); @@ -135,7 +145,7 @@ public async Task GetSermonNotesAsync_NullMessageId_ReturnsError() public async Task GetSermonNotesAsync_EmptyMessageId_ReturnsError() { // Arrange - var service = new TranscriptService(null, "transcripts"); + var service = new TranscriptService(null, "transcripts", _mockCache.Object); // Act var result = await service.GetSermonNotesAsync(string.Empty); @@ -149,7 +159,7 @@ public async Task GetSermonNotesAsync_EmptyMessageId_ReturnsError() public async Task GetSermonNotesAsync_WhitespaceMessageId_ReturnsConfigurationError() { // Arrange - var service = new TranscriptService(null, "transcripts"); + var service = new TranscriptService(null, "transcripts", _mockCache.Object); // Act var result = await service.GetSermonNotesAsync(" "); @@ -167,7 +177,7 @@ public async Task GetSermonNotesAsync_WhitespaceMessageId_ReturnsConfigurationEr public async Task GetSermonNotesAsync_ServiceNotConfigured_ReturnsConfigurationError() { // Arrange - Create service with null connection string - var unconfiguredService = new TranscriptService(null, "transcripts"); + var unconfiguredService = new TranscriptService(null, "transcripts", _mockCache.Object); var messageId = "507f1f77bcf86cd799439011"; // Act @@ -182,7 +192,7 @@ public async Task GetSermonNotesAsync_ServiceNotConfigured_ReturnsConfigurationE public async Task GetSermonNotesAsync_EmptyConnectionString_ReturnsConfigurationError() { // Arrange - var service = new TranscriptService(string.Empty, "transcripts"); + var service = new TranscriptService(string.Empty, "transcripts", _mockCache.Object); var messageId = "507f1f77bcf86cd799439011"; // Act @@ -201,7 +211,7 @@ public async Task GetSermonNotesAsync_EmptyConnectionString_ReturnsConfiguration public async Task GetStudyGuideAsync_NullMessageId_ReturnsError() { // Arrange - var service = new TranscriptService(null, "transcripts"); + var service = new TranscriptService(null, "transcripts", _mockCache.Object); // Act var result = await service.GetStudyGuideAsync(null); @@ -215,7 +225,7 @@ public async Task GetStudyGuideAsync_NullMessageId_ReturnsError() public async Task GetStudyGuideAsync_EmptyMessageId_ReturnsError() { // Arrange - var service = new TranscriptService(null, "transcripts"); + var service = new TranscriptService(null, "transcripts", _mockCache.Object); // Act var result = await service.GetStudyGuideAsync(string.Empty); @@ -229,7 +239,7 @@ public async Task GetStudyGuideAsync_EmptyMessageId_ReturnsError() public async Task GetStudyGuideAsync_WhitespaceMessageId_ReturnsConfigurationError() { // Arrange - var service = new TranscriptService(null, "transcripts"); + var service = new TranscriptService(null, "transcripts", _mockCache.Object); // Act var result = await service.GetStudyGuideAsync(" "); @@ -247,7 +257,7 @@ public async Task GetStudyGuideAsync_WhitespaceMessageId_ReturnsConfigurationErr public async Task GetStudyGuideAsync_ServiceNotConfigured_ReturnsConfigurationError() { // Arrange - Create service with null connection string - var unconfiguredService = new TranscriptService(null, "transcripts"); + var unconfiguredService = new TranscriptService(null, "transcripts", _mockCache.Object); var messageId = "507f1f77bcf86cd799439011"; // Act @@ -262,7 +272,7 @@ public async Task GetStudyGuideAsync_ServiceNotConfigured_ReturnsConfigurationEr public async Task GetStudyGuideAsync_EmptyConnectionString_ReturnsConfigurationError() { // Arrange - var service = new TranscriptService(string.Empty, "transcripts"); + var service = new TranscriptService(string.Empty, "transcripts", _mockCache.Object); var messageId = "507f1f77bcf86cd799439011"; // Act diff --git a/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/Startup.cs b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/Startup.cs index 93cc4be..243e840 100644 --- a/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/Startup.cs +++ b/API/ThriveChurchOfficialAPI/ThriveChurchOfficialAPI/Startup.cs @@ -390,7 +390,10 @@ public void ConfigureServices(IServiceCollection services) // Azure Blob Storage services (for transcripts) var azureStorageConnectionString = Configuration["AzureStorageConnectionString"]; services.AddSingleton(sp => - new TranscriptService(azureStorageConnectionString, "transcripts")); + new TranscriptService( + azureStorageConnectionString, + "transcripts", + sp.GetRequiredService())); Log.Information("Services configured."); }