Skip to content
Merged
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
5 changes: 3 additions & 2 deletions src/Torrentarr.Core/Configuration/ConfigurationLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1755,9 +1755,10 @@ private string GenerateTomlContent(TorrentarrConfig config)
}
sb.AppendLine();

// All qBit instances — "qBit" written first (primary), then additional [qBit-XXX]
// All qBit instances — "qBit" written first (primary), then additional [qBit-XXX].
// Include CHANGE_ME placeholders so multi-instance stubs survive WebUI round-trips.
var orderedQbit = config.QBitInstances
.Where(kv => !string.IsNullOrEmpty(kv.Value.Host) && kv.Value.Host != "CHANGE_ME")
.Where(kv => !string.IsNullOrEmpty(kv.Value.Host))
.OrderBy(kv => kv.Key == "qBit" ? 0 : 1).ThenBy(kv => kv.Key);

foreach (var (name, qbit) in orderedQbit)
Expand Down
2 changes: 1 addition & 1 deletion src/Torrentarr.Host/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2626,7 +2626,7 @@ static async Task<IResult> HandleTestConnection(TestConnectionRequest req, Torre
var currentObj = new Newtonsoft.Json.Linq.JObject();
currentObj["Settings"] = Newtonsoft.Json.Linq.JObject.FromObject(cfg.Settings, serializer);
currentObj["WebUI"] = Newtonsoft.Json.Linq.JObject.FromObject(cfg.WebUI, serializer);
foreach (var (key, qbit) in cfg.QBitInstances.Where(kv => kv.Value.Host != "CHANGE_ME"))
foreach (var (key, qbit) in cfg.QBitInstances)
currentObj[key] = Newtonsoft.Json.Linq.JObject.FromObject(qbit, serializer);
foreach (var (key, arr) in cfg.ArrInstances)
currentObj[key] = Newtonsoft.Json.Linq.JObject.FromObject(arr, serializer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,33 @@ public void SaveConfig_PreservesQBitTrackers()
reloaded.Settings.LoopSleepTimer.Should().Be(10);
}

[Fact]
public void SaveConfig_PreservesChangeMeQBitPlaceholderSection()
{
WriteToml("""
[Settings]
LoopSleepTimer = 5

[qBit]
Host = "localhost"
Port = 8080

[qBit-seedbox]
Host = "CHANGE_ME"
Port = 8080
""");

var loader = new ConfigurationLoader(_tempFilePath);
var config = loader.Load();
config.Settings.LoopSleepTimer = 10;
loader.SaveConfig(config);

var reloaded = new ConfigurationLoader(_tempFilePath).Load();
reloaded.QBitInstances.Should().ContainKey("qBit-seedbox");
reloaded.QBitInstances["qBit-seedbox"].Host.Should().Be("CHANGE_ME");
reloaded.Settings.LoopSleepTimer.Should().Be(10);
}

[Fact]
public void Save_WebUI_WritesAuthBooleans()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using FluentAssertions;
using System.Net;
using System.Text;
using System.Text.Json;
using Xunit;

namespace Torrentarr.Host.Tests.Api;

/// <summary>
/// Regression: CHANGE_ME qBit placeholder sections must survive unrelated POST /web/config saves.
/// </summary>
[Collection("HostWeb")]
public class ConfigPlaceholderPreservationTests : IClassFixture<MultiQBitPlaceholderWebApplicationFactory>
{
private readonly MultiQBitPlaceholderWebApplicationFactory _factory;

public ConfigPlaceholderPreservationTests(MultiQBitPlaceholderWebApplicationFactory factory)
{
_factory = factory;
}

[Fact]
public async Task PostConfig_UnrelatedChange_PreservesChangeMeQBitSeedboxStubOnDisk()
{
_factory.SetConfigEnv();
var client = _factory.CreateClientWithApiToken();

var payload = new
{
changes = new Dictionary<string, object>
{
["settings.loopSleepTimer"] = 12
}
};
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");

var response = await client.PostAsync("/web/config", content);
response.StatusCode.Should().Be(HttpStatusCode.OK);

var savedToml = await File.ReadAllTextAsync(_factory.TempConfigPath);
savedToml.Should().Contain("[qBit-seedbox]");
savedToml.Should().Contain("Host = \"CHANGE_ME\"");
}
}
38 changes: 38 additions & 0 deletions tests/Torrentarr.Host.Tests/Api/TorrentarrWebApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,44 @@ protected override void Dispose(bool disposing)
}
}

/// <summary>Factory with configured qBit plus a CHANGE_ME placeholder qBit-seedbox stub.</summary>
public class MultiQBitPlaceholderWebApplicationFactory : TorrentarrWebApplicationFactory
{
public MultiQBitPlaceholderWebApplicationFactory() => RewriteConfigFile();

protected override string GetTestConfigToml() => """
[Settings]
ConfigVersion = "6.12.2"
LoopSleepTimer = 5
FailedCategory = "failed"
RecheckCategory = "recheck"
PingURLS = ["one.one.one.one"]

[WebUI]
Host = "0.0.0.0"
Port = 6969
Token = "test-api-token"
AuthDisabled = true
LocalAuthEnabled = false
OIDCEnabled = false
LiveArr = false

[qBit]
Host = "localhost"
Port = 8080
UserName = "admin"
Password = "adminpass"
ManagedCategories = ["radarr"]

[qBit-seedbox]
Host = "CHANGE_ME"
Port = 8080
UserName = "CHANGE_ME"
Password = "CHANGE_ME"
ManagedCategories = []
""";
}

/// <summary>Factory with Radarr, Sonarr, Lidarr, and qBit sections for catalog/validation endpoint tests.</summary>
public class ArrCatalogWebApplicationFactory : TorrentarrWebApplicationFactory
{
Expand Down
Loading