mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 02:38:55 -04:00
Lock download client type after creation (#831)
This commit is contained in:
1 parent
de16a41b6c
commit
85e956dd92
5 files changed
+72
-3
No files matched your search
+24
@@ -302,6 +302,30 @@ public class DownloadClientControllerTests : IDisposable
|
||||
() => _controller.UpdateDownloadClientConfig(client.Id, request));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateDownloadClientConfig_ChangedTypeName_PropagatesValidationException()
|
||||
{
|
||||
// Arrange
|
||||
DownloadClientConfig client = NewClient("orig", DownloadClientTypeName.qBittorrent);
|
||||
_dataContext.DownloadClients.Add(client);
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
UpdateDownloadClientRequest request = new()
|
||||
{
|
||||
Name = "renamed",
|
||||
Host = "http://newhost:9090",
|
||||
TypeName = DownloadClientTypeName.rTorrent,
|
||||
};
|
||||
|
||||
// Act / Assert
|
||||
Cleanuparr.Domain.Exceptions.ValidationException exception = await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
|
||||
() => _controller.UpdateDownloadClientConfig(client.Id, request));
|
||||
exception.Message.ShouldContain("cannot be changed");
|
||||
|
||||
DownloadClientConfig saved = await _dataContext.DownloadClients.AsNoTracking().FirstAsync(c => c.Id == client.Id);
|
||||
saved.TypeName.ShouldBe(DownloadClientTypeName.qBittorrent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteDownloadClientConfig_ExistingClient_RemovesAndUnregistersHttpClient()
|
||||
{
|
||||
|
||||
+6
@@ -5,6 +5,7 @@ using Cleanuparr.Api.Extensions;
|
||||
using Cleanuparr.Api.Features.DownloadClient.Contracts.Requests;
|
||||
using Cleanuparr.Api.Features.DownloadClient.Contracts.Responses;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Domain.Exceptions;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
|
||||
using Cleanuparr.Persistence;
|
||||
@@ -101,6 +102,11 @@ public sealed class DownloadClientController : ControllerBase
|
||||
return this.ProblemResult(StatusCodes.Status404NotFound, $"Download client with ID {id} not found");
|
||||
}
|
||||
|
||||
if (updatedClient.TypeName != existingClient.TypeName)
|
||||
{
|
||||
throw new ValidationException("Download client type cannot be changed");
|
||||
}
|
||||
|
||||
var clientToPersist = updatedClient.ApplyTo(existingClient);
|
||||
clientToPersist.Validate();
|
||||
|
||||
|
||||
+21
-2
@@ -116,12 +116,15 @@ function hintFor(fixture: ComponentFixture<DownloadClientsComponent>, label: str
|
||||
return field?.querySelector('.input-hint')?.textContent!.trim() ?? '';
|
||||
}
|
||||
|
||||
function clientTypeTrigger(fixture: ComponentFixture<DownloadClientsComponent>): HTMLButtonElement {
|
||||
return fixture.nativeElement.querySelector('.select-trigger') as HTMLButtonElement;
|
||||
}
|
||||
|
||||
function chooseClientType(
|
||||
fixture: ComponentFixture<DownloadClientsComponent>,
|
||||
label: string,
|
||||
): void {
|
||||
const trigger = fixture.nativeElement.querySelector('.select-trigger') as HTMLButtonElement;
|
||||
trigger.click();
|
||||
clientTypeTrigger(fixture).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
const option = Array.from(fixture.nativeElement.querySelectorAll('.select-option')).find(
|
||||
@@ -417,6 +420,22 @@ describe('DownloadClientsComponent', () => {
|
||||
expect(fixture.nativeElement.textContent).toContain('Edit Client');
|
||||
});
|
||||
|
||||
it('locks the client type while editing and unlocks it when adding', () => {
|
||||
const { fixture, component } = setup();
|
||||
|
||||
component.openEditModal(QBIT);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.clientForm.typeName().disabled()).toBe(true);
|
||||
expect(clientTypeTrigger(fixture).disabled).toBe(true);
|
||||
|
||||
component.openAddModal();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.clientForm.typeName().disabled()).toBe(false);
|
||||
expect(clientTypeTrigger(fixture).disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('creates a client from the modal values and reloads the list', () => {
|
||||
const { fixture, component, api, toast } = setup();
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
import { Component, ChangeDetectionStrategy, inject, signal, computed } from '@angular/core';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { form, required, FormField } from '@angular/forms/signals';
|
||||
import { form, required, disabled, FormField } from '@angular/forms/signals';
|
||||
import { PageHeaderComponent } from '@layout/page-header/page-header.component';
|
||||
import {
|
||||
CardComponent, ButtonComponent, InputComponent, ToggleComponent,
|
||||
@@ -114,6 +114,7 @@ export class DownloadClientsComponent implements HasPendingChanges {
|
||||
when: ({ valueOf }) => valueOf(p.typeName) === DownloadClientTypeName.Sabnzbd,
|
||||
message: 'API key is required',
|
||||
});
|
||||
disabled(p.typeName, () => this.editingClient() !== null);
|
||||
});
|
||||
|
||||
readonly hasModalErrors = computed(() => this.clientForm().invalid());
|
||||
|
||||
@@ -137,6 +137,25 @@ test.describe('DownloadClient: invalid type validation', () => {
|
||||
|
||||
await api.downloadClient.delete(created.id);
|
||||
});
|
||||
|
||||
test('update changing typeName returns 400 and keeps the original type', async ({ api }) => {
|
||||
const payload = buildDownloadClientPayload('qbittorrent', {
|
||||
name: 'base-for-type-change',
|
||||
host: TEST_CONFIG.mocks.downloadClientUrl,
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
});
|
||||
const created = await (await api.downloadClient.create(payload)).json();
|
||||
|
||||
const res = await api.downloadClient.update(created.id, { ...payload, typeName: 'rTorrent' });
|
||||
expect(res.status).toBe(400);
|
||||
|
||||
const list = await (await api.downloadClient.list()).json();
|
||||
const stored = list.clients.find((c: { id: string }) => c.id === created.id);
|
||||
expect(stored.typeName.toLowerCase()).toBe(payload.typeName?.toLowerCase());
|
||||
|
||||
await api.downloadClient.delete(created.id);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('DownloadClient: API key validation', () => {
|
||||
|
||||
Reference in new issue
Block a user