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
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ namespace SpotifyDaily.Worker.Services.Contracts;
public interface ISpotifyClientService
{
Task ConfigureNewClientAsync(string code, CancellationToken cancellationToken);
Task<SpotifyClient> GetClientAsync(string? code = null, CancellationToken cancellationToken = default);
Task<ISpotifyClient> GetClientAsync(string? code = null, CancellationToken cancellationToken = default);
}
12 changes: 6 additions & 6 deletions src/SpotifyDaily.Worker/Services/PlaylistService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class PlaylistService(ILogger<PlaylistService> logger, IOptions<SpotifyOp

public async Task UpdateDailyPlaylistAsync(CancellationToken cancellationToken = default)
{
SpotifyClient client = await spotifyClientService.GetClientAsync(cancellationToken: cancellationToken);
var client = await spotifyClientService.GetClientAsync(cancellationToken: cancellationToken);
if (client == null)
{
throw new PlaylistServiceException("Spotify client is not configured. Please call ConfigureClientAsync first.");
Expand All @@ -36,7 +36,7 @@ public async Task UpdateDailyPlaylistAsync(CancellationToken cancellationToken =
logger.LogInformation("Playlist update completed at: {Time}", DateTime.Now);
}

private async Task UpdatePlaylistDescriptionAsync(FullTrack fullTrack, SpotifyClient client, string playlistId, CancellationToken cancellationToken)
private async Task UpdatePlaylistDescriptionAsync(FullTrack fullTrack, ISpotifyClient client, string playlistId, CancellationToken cancellationToken)
{
logger.LogInformation("Updating playlist description...");

Expand All @@ -59,7 +59,7 @@ private string GetPlaylistDescription(FullTrack fullTrack, CancellationToken can
return $"{artistName} at the top. Last update: {updateDateTimeValue}";
}

private async Task AddTracksAsync(IEnumerable<FullTrack> topTracks, string playlistId, SpotifyClient client, CancellationToken cancellationToken)
private async Task AddTracksAsync(IEnumerable<FullTrack> topTracks, string playlistId, ISpotifyClient client, CancellationToken cancellationToken)
{
logger.LogInformation("Adding top tracks to the playlist...");

Expand All @@ -69,7 +69,7 @@ private async Task AddTracksAsync(IEnumerable<FullTrack> topTracks, string playl
logger.LogInformation("Added {Count} tracks to the playlist.", topTracks.Count());
}

private async Task<IEnumerable<FullTrack>> GetTopTracksAsync(SpotifyClient client, CancellationToken cancellationToken)
private async Task<IEnumerable<FullTrack>> GetTopTracksAsync(ISpotifyClient client, CancellationToken cancellationToken)
{
logger.LogInformation("Fetching top tracks...");

Expand All @@ -79,7 +79,7 @@ private async Task<IEnumerable<FullTrack>> GetTopTracksAsync(SpotifyClient clien
return topTracksResponse.Items;
}

private async Task RemoveCurrentTracksAsync(SpotifyClient client, string playlistId, CancellationToken cancellationToken)
private async Task RemoveCurrentTracksAsync(ISpotifyClient client, string playlistId, CancellationToken cancellationToken)
{
IEnumerable<FullTrack>? currentTracks = await GetCurrentPlaylistTracksAsync(client, playlistId, cancellationToken);
if (currentTracks == null || !currentTracks.Any())
Expand All @@ -99,7 +99,7 @@ private async Task RemoveCurrentTracksAsync(SpotifyClient client, string playlis
logger.LogInformation("Current tracks removed from the playlist.");
}

private async Task<IEnumerable<FullTrack>?> GetCurrentPlaylistTracksAsync(SpotifyClient client, string playlistId, CancellationToken cancellationToken)
private async Task<IEnumerable<FullTrack>?> GetCurrentPlaylistTracksAsync(ISpotifyClient client, string playlistId, CancellationToken cancellationToken)
{
Paging<PlaylistTrack<IPlayableItem>> playlistItems = await client.Playlists.GetItems(playlistId, new PlaylistGetItemsRequest(PlaylistGetItemsRequest.AdditionalTypes.Track), cancellationToken);
if (playlistItems.Items == null)
Expand Down
2 changes: 1 addition & 1 deletion src/SpotifyDaily.Worker/Services/SpotifyClientService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ private async Task SaveTokensAsync(string accessToken, string? refreshToken, Dat

}

public async Task<SpotifyClient> GetClientAsync(string? code = null, CancellationToken cancellationToken = default)
public async Task<ISpotifyClient> GetClientAsync(string? code = null, CancellationToken cancellationToken = default)
{

if (_appConfig.ExpireDate > DateTime.Now && !string.IsNullOrWhiteSpace(_appConfig.Token))
Expand Down
22 changes: 22 additions & 0 deletions src/SpotifyDaily.sln
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{63914694-B031-4C79-BD7D-4262D8421F9B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpotifyDaily.Tests.Worker", "Tests\SpotifyDaily.Tests.Worker\SpotifyDaily.Tests.Worker.csproj", "{8A547896-F80D-4340-A692-632F8FABF4CC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -32,8 +36,26 @@ Global
{27EDCFDA-40EE-4DC9-962A-5A44A97F499B}.Release|x64.Build.0 = Release|Any CPU
{27EDCFDA-40EE-4DC9-962A-5A44A97F499B}.Release|x86.ActiveCfg = Release|Any CPU
{27EDCFDA-40EE-4DC9-962A-5A44A97F499B}.Release|x86.Build.0 = Release|Any CPU
{8A547896-F80D-4340-A692-632F8FABF4CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A547896-F80D-4340-A692-632F8FABF4CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A547896-F80D-4340-A692-632F8FABF4CC}.Debug|x64.ActiveCfg = Debug|Any CPU
{8A547896-F80D-4340-A692-632F8FABF4CC}.Debug|x64.Build.0 = Debug|Any CPU
{8A547896-F80D-4340-A692-632F8FABF4CC}.Debug|x86.ActiveCfg = Debug|Any CPU
{8A547896-F80D-4340-A692-632F8FABF4CC}.Debug|x86.Build.0 = Debug|Any CPU
{8A547896-F80D-4340-A692-632F8FABF4CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A547896-F80D-4340-A692-632F8FABF4CC}.Release|Any CPU.Build.0 = Release|Any CPU
{8A547896-F80D-4340-A692-632F8FABF4CC}.Release|x64.ActiveCfg = Release|Any CPU
{8A547896-F80D-4340-A692-632F8FABF4CC}.Release|x64.Build.0 = Release|Any CPU
{8A547896-F80D-4340-A692-632F8FABF4CC}.Release|x86.ActiveCfg = Release|Any CPU
{8A547896-F80D-4340-A692-632F8FABF4CC}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{8A547896-F80D-4340-A692-632F8FABF4CC} = {63914694-B031-4C79-BD7D-4262D8421F9B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D58A2A72-DA7F-4FA2-9FFE-8890A9620400}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using AutoFixture;
using AutoFixture.AutoMoq;
using AutoFixture.Xunit2;
using SpotifyAPI.Web;

namespace SpotifyDaily.Tests.Worker.Attributes;

public class AutoMoqDataAttribute : AutoDataAttribute
{
public AutoMoqDataAttribute() : base(() =>
{
var fixture = new Fixture();
fixture.Customize(new AutoMoqCustomization
{
ConfigureMembers = true,
GenerateDelegates = true
});
fixture.Customize<ISpotifyClient>(sb => sb.FromFactory(() => new SpotifyClient("token")));
return fixture;
})
{
}
}
109 changes: 109 additions & 0 deletions src/Tests/SpotifyDaily.Tests.Worker/Fakes/FakeSpotifyClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using SpotifyAPI.Web;
using SpotifyAPI.Web.Http;

public class FakeSpotifyClient : ISpotifyClient
{
public IPlaylistsClient Playlists { get; } = default!;
public IUserProfileClient UserProfile { get; } = default!;

// Add any other required properties from ISpotifyClient interface with default implementations

public FakeSpotifyClient()
{

}

public FakeSpotifyClient(IPlaylistsClient playlists, IUserProfileClient userProfile)
{
Playlists = playlists;
UserProfile = userProfile;
}


public AlbumsClient Albums => throw new NotImplementedException();
public ArtistsClient Artists => throw new NotImplementedException();
public BrowseClient Browse => throw new NotImplementedException();

public IPaginator DefaultPaginator => throw new NotImplementedException();

IUserProfileClient ISpotifyClient.UserProfile => UserProfile;

IBrowseClient ISpotifyClient.Browse => Browse;

public IShowsClient Shows => throw new NotImplementedException();

IPlaylistsClient ISpotifyClient.Playlists => Playlists;

public ISearchClient Search => throw new NotImplementedException();

public IFollowClient Follow => throw new NotImplementedException();

public ITracksClient Tracks => throw new NotImplementedException();

public IPlayerClient Player => throw new NotImplementedException();

IAlbumsClient ISpotifyClient.Albums => Albums;

IArtistsClient ISpotifyClient.Artists => Artists;

public IPersonalizationClient Personalization => throw new NotImplementedException();

public IEpisodesClient Episodes => throw new NotImplementedException();

public ILibraryClient Library => throw new NotImplementedException();

public IAudiobooksClient Audiobooks => throw new NotImplementedException();

public IChaptersClient Chapters => throw new NotImplementedException();

public IResponse? LastResponse => throw new NotImplementedException();

// Add other required properties with placeholder implementations

public void Dispose() { }

public Task<IList<T>> PaginateAll<T>(IPaginatable<T> firstPage, IPaginator? paginator = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}

public Task<IList<T>> PaginateAll<T, TNext>(IPaginatable<T, TNext> firstPage, Func<TNext, IPaginatable<T, TNext>> mapper, IPaginator? paginator = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}

public IAsyncEnumerable<T> Paginate<T>(IPaginatable<T> firstPage, IPaginator? paginator = null, CancellationToken cancel = default)
{
throw new NotImplementedException();
}

public IAsyncEnumerable<T> Paginate<T, TNext>(IPaginatable<T, TNext> firstPage, Func<TNext, IPaginatable<T, TNext>> mapper, IPaginator? paginator = null, CancellationToken cancel = default)
{
throw new NotImplementedException();
}

public Task<Paging<T>> NextPage<T>(Paging<T> paging)
{
throw new NotImplementedException();
}

public Task<CursorPaging<T>> NextPage<T>(CursorPaging<T> cursorPaging)
{
throw new NotImplementedException();
}

public Task<TNext> NextPage<T, TNext>(IPaginatable<T, TNext> paginatable)
{
throw new NotImplementedException();
}

public Task<Paging<T>> PreviousPage<T>(Paging<T> paging)
{
throw new NotImplementedException();
}

public Task<TNext> PreviousPage<T, TNext>(Paging<T, TNext> paging)
{
throw new NotImplementedException();
}
}
138 changes: 138 additions & 0 deletions src/Tests/SpotifyDaily.Tests.Worker/Services/AppConfigServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Moq;
using SpotifyDaily.Worker.Models;
using SpotifyDaily.Worker.Services;
using System.Text.Json;

namespace SpotifyDaily.Tests.Worker.Services;

public class AppConfigServiceTests : IDisposable
{
private readonly Mock<IHostEnvironment> _mockEnvironment;
private readonly Mock<IConfiguration> _mockConfiguration;
private readonly Mock<IOptionsMonitor<AppConfig>> _mockOptionsMonitor;
private readonly string _tempDir;
private readonly string _tempFile;
private readonly AppConfig _initialConfig;

public AppConfigServiceTests()
{
_initialConfig = new AppConfig
{
LastRun = DateTime.Now.AddDays(-1),
Token = "test-token",
RefreshToken = "test-refresh-token",
ExpireDate = DateTime.Now.AddDays(1)
};

_mockEnvironment = new Mock<IHostEnvironment>();
_mockConfiguration = new Mock<IConfiguration>();
_mockOptionsMonitor = new Mock<IOptionsMonitor<AppConfig>>();
_mockOptionsMonitor.Setup(m => m.CurrentValue).Returns(_initialConfig);

// Setup temp directory for tests
_tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(_tempDir);
_tempFile = Path.Combine(_tempDir, "appconfig.json");

// Setup mock environment to return our temp directory
_mockEnvironment.Setup(e => e.ContentRootPath).Returns(_tempDir);
}

[Fact]
public void Constructor_InitializesProperties()
{
// Arrange & Act
var service = CreateService();

// Assert
Assert.Equal(_initialConfig, service.Current);
}

[Fact]
public void Current_Get_ReturnsCurrentConfig()
{
// Arrange
var service = CreateService();

// Act
var result = service.Current;

// Assert
Assert.Equal(_initialConfig, result);
}

[Fact]
public void Current_Set_UpdatesConfigAndRaisesEvent()
{
// Arrange
var service = CreateService();
var newConfig = new AppConfig { Token = "new-token" };
var eventRaised = false;
service.OnChange += config => eventRaised = config.Token == "new-token";

// Act
service.Current = newConfig;

// Assert
Assert.True(eventRaised, "OnChange event should be raised");
Assert.Equal("new-token", service.Current.Token);
Assert.True(File.Exists(_tempFile), "Config file should be created");

// Verify file contents
var fileContent = File.ReadAllText(_tempFile);
using var doc = JsonDocument.Parse(fileContent);
var appConfigJson = doc.RootElement.GetProperty("AppConfig").GetRawText();
var deserializedContent = JsonSerializer.Deserialize<AppConfig>(appConfigJson);
Assert.NotNull(deserializedContent);
Assert.Equal("new-token", deserializedContent!.Token);
}

[Fact]
public async Task UpdateAsync_WritesConfigToFile()
{
// Arrange
var service = CreateService();
var newConfig = new AppConfig
{
LastRun = DateTime.Now,
Token = "updated-token",
RefreshToken = "updated-refresh"
};

// Act
await service.UpdateAsync(newConfig);

// Assert
Assert.True(File.Exists(_tempFile), "Config file should be created");

// Verify file contents
var fileContent = File.ReadAllText(_tempFile);
using var doc = JsonDocument.Parse(fileContent);
var appConfigJson = doc.RootElement.GetProperty("AppConfig").GetRawText();
var deserializedContent = JsonSerializer.Deserialize<AppConfig>(appConfigJson);
Assert.NotNull(deserializedContent);
Assert.Equal("updated-token", deserializedContent!.Token);
}

private AppConfigService CreateService()
{
return new AppConfigService(
_mockEnvironment.Object,
_mockConfiguration.Object,
_mockOptionsMonitor.Object);
}

public void Dispose()
{
// Clean up temp files
try
{
if (Directory.Exists(_tempDir))
Directory.Delete(_tempDir, true);
}
catch { /* Ignore cleanup errors */ }
}
}
Loading