v1.1.1: SCROBBLING, fixed and rewrote caching, refactored and fixed WebUI, fixed logs, fixed cron scheduling bugs, hardened security, added Global Mappings, made the proxy more 'transparent', added playlists from Tidal to search
Docker Build & Push / build-and-test (push) Has been cancelled
Docker Build & Push / docker (push) Has been cancelled

This commit is contained in:
2026-02-20 18:59:37 -05:00
parent 40338ce25f
commit 375b7c6909
114 changed files with 32947 additions and 1263 deletions
+214
View File
@@ -0,0 +1,214 @@
using Xunit;
using Moq;
using Microsoft.Extensions.Logging;
using allstarr.Services.Common;
using System.IO;
namespace allstarr.Tests;
public class EnvMigrationServiceTests
{
private readonly Mock<ILogger<EnvMigrationService>> _mockLogger;
private readonly string _testEnvPath;
public EnvMigrationServiceTests()
{
_mockLogger = new Mock<ILogger<EnvMigrationService>>();
_testEnvPath = Path.Combine(Path.GetTempPath(), $"test-{Guid.NewGuid()}.env");
}
[Fact]
public void MigrateEnvFile_RemovesQuotesFromPasswords()
{
// Arrange - passwords with quotes (old incorrect format)
var envContent = @"SCROBBLING_LASTFM_USERNAME=testuser
SCROBBLING_LASTFM_PASSWORD=""test!pass123""
MUSICBRAINZ_PASSWORD=""fake&Pass*Word$123""
SOME_OTHER_VAR=value";
File.WriteAllText(_testEnvPath, envContent);
var service = new TestEnvMigrationService(_mockLogger.Object, _testEnvPath);
// Act
service.MigrateEnvFile();
// Assert - quotes should be removed
var result = File.ReadAllText(_testEnvPath);
Assert.Contains("SCROBBLING_LASTFM_PASSWORD=test!pass123", result);
Assert.DoesNotContain("SCROBBLING_LASTFM_PASSWORD=\"test!pass123\"", result);
Assert.Contains("MUSICBRAINZ_PASSWORD=fake&Pass*Word$123", result);
Assert.DoesNotContain("MUSICBRAINZ_PASSWORD=\"fake&Pass*Word$123\"", result);
Assert.Contains("SCROBBLING_LASTFM_USERNAME=testuser", result);
Assert.Contains("SOME_OTHER_VAR=value", result);
// Cleanup
File.Delete(_testEnvPath);
}
[Fact]
public void MigrateEnvFile_LeavesUnquotedPasswordsAlone()
{
// Arrange - passwords without quotes (correct format)
var envContent = @"SCROBBLING_LASTFM_PASSWORD=already-unquoted!
MUSICBRAINZ_PASSWORD=also-unquoted&*$";
File.WriteAllText(_testEnvPath, envContent);
var service = new TestEnvMigrationService(_mockLogger.Object, _testEnvPath);
// Act
service.MigrateEnvFile();
// Assert - should remain unchanged
var result = File.ReadAllText(_testEnvPath);
Assert.Contains("SCROBBLING_LASTFM_PASSWORD=already-unquoted!", result);
Assert.Contains("MUSICBRAINZ_PASSWORD=also-unquoted&*$", result);
// Cleanup
File.Delete(_testEnvPath);
}
[Fact]
public void MigrateEnvFile_HandlesEmptyPasswords()
{
// Arrange
var envContent = @"SCROBBLING_LASTFM_PASSWORD=
MUSICBRAINZ_PASSWORD=";
File.WriteAllText(_testEnvPath, envContent);
var service = new TestEnvMigrationService(_mockLogger.Object, _testEnvPath);
// Act
service.MigrateEnvFile();
// Assert
var result = File.ReadAllText(_testEnvPath);
Assert.Contains("SCROBBLING_LASTFM_PASSWORD=", result);
Assert.DoesNotContain("SCROBBLING_LASTFM_PASSWORD=\"\"", result);
// Cleanup
File.Delete(_testEnvPath);
}
[Fact]
public void MigrateEnvFile_PreservesComments()
{
// Arrange
var envContent = @"# This is a comment
SCROBBLING_LASTFM_PASSWORD=fake!test123
# Another comment
MUSICBRAINZ_PASSWORD=test&pass*word";
File.WriteAllText(_testEnvPath, envContent);
var service = new TestEnvMigrationService(_mockLogger.Object, _testEnvPath);
// Act
service.MigrateEnvFile();
// Assert
var result = File.ReadAllText(_testEnvPath);
Assert.Contains("# This is a comment", result);
Assert.Contains("# Another comment", result);
// Cleanup
File.Delete(_testEnvPath);
}
[Theory]
[InlineData("DEEZER_ARL", "\"abc123def456!@#\"")]
[InlineData("QOBUZ_USER_AUTH_TOKEN", "\"token&with*special$chars\"")]
[InlineData("SCROBBLING_LASTFM_SESSION_KEY", "\"session!key@here\"")]
[InlineData("SPOTIFY_API_SESSION_COOKIE", "\"cookie$value&here\"")]
public void MigrateEnvFile_RemovesQuotesFromAllSensitiveKeys(string key, string quotedValue)
{
// Arrange - value with quotes (old incorrect format)
var envContent = $"{key}={quotedValue}";
File.WriteAllText(_testEnvPath, envContent);
var service = new TestEnvMigrationService(_mockLogger.Object, _testEnvPath);
// Act
service.MigrateEnvFile();
// Assert - quotes should be removed
var result = File.ReadAllText(_testEnvPath);
var unquotedValue = quotedValue.Substring(1, quotedValue.Length - 2);
Assert.Contains($"{key}={unquotedValue}", result);
Assert.DoesNotContain(quotedValue, result);
// Cleanup
File.Delete(_testEnvPath);
}
[Fact]
public void MigrateEnvFile_HandlesMultipleQuotedPasswords()
{
// Arrange - all with quotes (old incorrect format)
var envContent = @"SCROBBLING_LASTFM_PASSWORD=""fakepass1!""
MUSICBRAINZ_PASSWORD=""testpass2&""
DEEZER_ARL=""fakearl3*""
QOBUZ_USER_AUTH_TOKEN=""testtoken4$""";
File.WriteAllText(_testEnvPath, envContent);
var service = new TestEnvMigrationService(_mockLogger.Object, _testEnvPath);
// Act
service.MigrateEnvFile();
// Assert - all quotes should be removed
var result = File.ReadAllText(_testEnvPath);
Assert.Contains("SCROBBLING_LASTFM_PASSWORD=fakepass1!", result);
Assert.DoesNotContain("SCROBBLING_LASTFM_PASSWORD=\"fakepass1!\"", result);
Assert.Contains("MUSICBRAINZ_PASSWORD=testpass2&", result);
Assert.DoesNotContain("MUSICBRAINZ_PASSWORD=\"testpass2&\"", result);
Assert.Contains("DEEZER_ARL=fakearl3*", result);
Assert.DoesNotContain("DEEZER_ARL=\"fakearl3*\"", result);
Assert.Contains("QOBUZ_USER_AUTH_TOKEN=testtoken4$", result);
Assert.DoesNotContain("QOBUZ_USER_AUTH_TOKEN=\"testtoken4$\"", result);
// Cleanup
File.Delete(_testEnvPath);
}
[Fact]
public void MigrateEnvFile_NoFileExists_LogsWarning()
{
// Arrange
var nonExistentPath = Path.Combine(Path.GetTempPath(), $"nonexistent-{Guid.NewGuid()}.env");
var service = new TestEnvMigrationService(_mockLogger.Object, nonExistentPath);
// Act
service.MigrateEnvFile();
// Assert - should not throw, just log warning
_mockLogger.Verify(
x => x.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("No .env file found")),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}
// Helper class to allow testing with custom path
private class TestEnvMigrationService : EnvMigrationService
{
private readonly string _customPath;
public TestEnvMigrationService(ILogger<EnvMigrationService> logger, string customPath)
: base(logger)
{
_customPath = customPath;
// Use reflection to set the private field
var field = typeof(EnvMigrationService).GetField("_envFilePath",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
field?.SetValue(this, _customPath);
}
}
}