mirror of
https://github.com/gethomepage/homepage.git
synced 2026-05-18 19:40:58 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
233721cc90 | ||
|
|
d294a25145 | ||
|
|
0ff9af4c3c | ||
|
|
a6c753c8aa | ||
|
|
5d71c3aa65 | ||
|
|
1cc4608d72 | ||
|
|
d65e5447e4 | ||
|
|
d3256596d8 |
44
.github/release-drafter.yml
vendored
44
.github/release-drafter.yml
vendored
@@ -47,15 +47,34 @@ categories:
|
||||
- 'documentation'
|
||||
|
||||
autolabeler:
|
||||
- label: 'bug'
|
||||
title:
|
||||
- '/^fix(\(.+\))?:/i'
|
||||
body:
|
||||
- '/- \[[xX]\] Bug fix \(non-breaking change which fixes an issue\)/'
|
||||
|
||||
- label: 'enhancement'
|
||||
title:
|
||||
- '/^(feature|enhancement)(\(.+\))?:/i'
|
||||
body:
|
||||
- '/- \[[xX]\] New service widget/'
|
||||
- '/- \[[xX]\] New feature or enhancement \(non-breaking change which adds functionality\)/'
|
||||
|
||||
- label: 'documentation'
|
||||
files:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '.github/**/*.md'
|
||||
title:
|
||||
- '/^(documentation|docs)(\(.+\))?:/i'
|
||||
body:
|
||||
- '/- \[[xX]\] Documentation only/'
|
||||
|
||||
- label: 'chore'
|
||||
body:
|
||||
- '/- \[[xX]\] Other \(please explain\)/'
|
||||
|
||||
- label: 'ci'
|
||||
files:
|
||||
- '.github/workflows/**'
|
||||
branch:
|
||||
- '/^(ci|workflow|actions)\//'
|
||||
title:
|
||||
- '/^(ci|workflow|actions)(\(.+\))?:/i'
|
||||
|
||||
- label: 'dependencies'
|
||||
files:
|
||||
@@ -64,19 +83,6 @@ autolabeler:
|
||||
- 'pyproject.toml'
|
||||
- 'uv.lock'
|
||||
|
||||
- label: 'feature'
|
||||
files:
|
||||
- 'src/components/**'
|
||||
- 'src/widgets/**'
|
||||
- 'src/pages/**'
|
||||
- 'src/utils/**'
|
||||
|
||||
- label: 'chore'
|
||||
files:
|
||||
- 'Dockerfile*'
|
||||
- 'docker-entrypoint.sh'
|
||||
- 'k3d/**'
|
||||
|
||||
- label: 'translation'
|
||||
files:
|
||||
- 'public/locales/**'
|
||||
|
||||
43
.github/workflows/issue-triage.yml
vendored
Normal file
43
.github/workflows/issue-triage.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Issue Triage
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
close-needs-discussion:
|
||||
name: Issues Need Discussion
|
||||
if: github.event.label.name == 'needs-discussion'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const issueNumber = context.payload.issue.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
body: 'This issue is being closed because it was opened before a maintainer asked for an issue to be created. Please start with a discussion and follow the issue template; only open an issue when a maintainer asks you to do so.',
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned',
|
||||
});
|
||||
|
||||
await github.rest.issues.lock({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
lock_reason: 'off-topic',
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.13.0",
|
||||
"version": "1.13.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
@@ -344,15 +344,15 @@
|
||||
"address": "Adresse",
|
||||
"expires": "Expire",
|
||||
"never": "Jamais",
|
||||
"user": "User",
|
||||
"hostname": "Hostname",
|
||||
"name": "Name",
|
||||
"client_version": "Client Version",
|
||||
"user": "Utilisateur",
|
||||
"hostname": "Nom d'hôte",
|
||||
"name": "Nom",
|
||||
"client_version": "Version client",
|
||||
"os": "OS",
|
||||
"created": "Created",
|
||||
"authorized": "Authorized",
|
||||
"is_external": "Is External",
|
||||
"update_available": "Update Available",
|
||||
"created": "Créé",
|
||||
"authorized": "Autorisée",
|
||||
"is_external": "Externe",
|
||||
"update_available": "Mise à jour disponible",
|
||||
"tags": "Tags",
|
||||
"last_seen": "Vu pour la dernière fois",
|
||||
"now": "Maintenant",
|
||||
@@ -363,8 +363,8 @@
|
||||
"minutes": "{{number}}m",
|
||||
"seconds": "{{number}}s",
|
||||
"ago": "Il y a {{value}}",
|
||||
"true": "Yes",
|
||||
"false": "No"
|
||||
"true": "Oui",
|
||||
"false": "Non"
|
||||
},
|
||||
"technitium": {
|
||||
"totalQueries": "Requêtes",
|
||||
@@ -937,16 +937,16 @@
|
||||
"criticals": "Urgent"
|
||||
},
|
||||
"ntfy": {
|
||||
"title": "Title",
|
||||
"priority": "Priority",
|
||||
"lastReceived": "Last Received",
|
||||
"title": "Titre",
|
||||
"priority": "Priorité",
|
||||
"lastReceived": "Dernière réception",
|
||||
"message": "Message",
|
||||
"tags": "Tags",
|
||||
"none": "None",
|
||||
"none": "Aucun",
|
||||
"min": "Min",
|
||||
"low": "Low",
|
||||
"default": "Default",
|
||||
"high": "High",
|
||||
"low": "Bas",
|
||||
"default": "Défaut",
|
||||
"high": "Haut",
|
||||
"urgent": "Urgent"
|
||||
},
|
||||
"plantit": {
|
||||
|
||||
@@ -67,14 +67,14 @@
|
||||
"empty_data": "Статус подсистемы неизвестен"
|
||||
},
|
||||
"unifi_drive": {
|
||||
"healthy": "Healthy",
|
||||
"degraded": "Degraded",
|
||||
"no_data": "No storage data available"
|
||||
"healthy": "Здоров",
|
||||
"degraded": "Деградация",
|
||||
"no_data": "Нет доступных данных о хранилище"
|
||||
},
|
||||
"docker": {
|
||||
"rx": "RX",
|
||||
"tx": "TX",
|
||||
"mem": "Память",
|
||||
"mem": "ОЗУ",
|
||||
"cpu": "ЦП",
|
||||
"running": "Запущено",
|
||||
"offline": "Не в сети",
|
||||
@@ -184,15 +184,15 @@
|
||||
},
|
||||
"tautulli": {
|
||||
"playing": "Играет",
|
||||
"transcoding": "Транскодируется",
|
||||
"transcoding": "Перекодируется",
|
||||
"bitrate": "Битрейт",
|
||||
"no_active": "Нет активных стримов",
|
||||
"no_active": "Нет активных потоков",
|
||||
"plex_connection_error": "Проверка соединения Plex"
|
||||
},
|
||||
"tracearr": {
|
||||
"no_active": "Нет активных потоков",
|
||||
"streams": "Потоки",
|
||||
"transcodes": "Transcodes",
|
||||
"transcodes": "Перекодирования",
|
||||
"directplay": "Прямое воспроизведение",
|
||||
"bitrate": "Битрейт"
|
||||
},
|
||||
@@ -215,18 +215,18 @@
|
||||
"tv": "Сериалы"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "",
|
||||
"rate": "Скорость",
|
||||
"queue": "Очередь",
|
||||
"timeleft": "Осталось"
|
||||
},
|
||||
"rutorrent": {
|
||||
"active": "Активно",
|
||||
"upload": "Загрузка",
|
||||
"upload": "Отдача",
|
||||
"download": "Скачивание"
|
||||
},
|
||||
"transmission": {
|
||||
"download": "Скачивание",
|
||||
"upload": "Загрузка",
|
||||
"upload": "Отдача",
|
||||
"leech": "Лич",
|
||||
"seed": "Сид"
|
||||
},
|
||||
@@ -295,7 +295,7 @@
|
||||
"available": "Доступно"
|
||||
},
|
||||
"seerr": {
|
||||
"pending": "Pending",
|
||||
"pending": "Ожидают",
|
||||
"approved": "Одобрено",
|
||||
"available": "Доступно",
|
||||
"completed": "Завершено",
|
||||
@@ -344,16 +344,16 @@
|
||||
"address": "Адрес",
|
||||
"expires": "Истекает",
|
||||
"never": "Никогда",
|
||||
"user": "User",
|
||||
"hostname": "Hostname",
|
||||
"name": "Name",
|
||||
"client_version": "Client Version",
|
||||
"os": "OS",
|
||||
"created": "Created",
|
||||
"authorized": "Authorized",
|
||||
"is_external": "Is External",
|
||||
"update_available": "Update Available",
|
||||
"tags": "Tags",
|
||||
"user": "Пользователь",
|
||||
"hostname": "Имя хоста",
|
||||
"name": "Имя",
|
||||
"client_version": "Версия клиента",
|
||||
"os": "ОС",
|
||||
"created": "Создано",
|
||||
"authorized": "Авторизовано",
|
||||
"is_external": "Внешний",
|
||||
"update_available": "Доступно обновление",
|
||||
"tags": "Теги",
|
||||
"last_seen": "Последнее посещение",
|
||||
"now": "Только что",
|
||||
"years": "{{number}}г",
|
||||
@@ -363,8 +363,8 @@
|
||||
"minutes": "{{number}}м",
|
||||
"seconds": "{{number}}с",
|
||||
"ago": "{{value}} назад",
|
||||
"true": "Yes",
|
||||
"false": "No"
|
||||
"true": "Да",
|
||||
"false": "Нет"
|
||||
},
|
||||
"technitium": {
|
||||
"totalQueries": "Запросы",
|
||||
@@ -632,12 +632,12 @@
|
||||
},
|
||||
"pangolin": {
|
||||
"orgs": "Orgs",
|
||||
"sites": "Sites",
|
||||
"resources": "Resources",
|
||||
"targets": "Targets",
|
||||
"traffic": "Traffic",
|
||||
"in": "In",
|
||||
"out": "Out"
|
||||
"sites": "Сайты",
|
||||
"resources": "Ресурсы",
|
||||
"targets": "Цели",
|
||||
"traffic": "Трафик",
|
||||
"in": "Входящий",
|
||||
"out": "Исходящий"
|
||||
},
|
||||
"peanut": {
|
||||
"battery_charge": "Заряд батареи",
|
||||
@@ -736,8 +736,8 @@
|
||||
"volumeAvailable": "Доступно"
|
||||
},
|
||||
"dispatcharr": {
|
||||
"channels": "Channels",
|
||||
"streams": "Streams"
|
||||
"channels": "Каналы",
|
||||
"streams": "Потоки"
|
||||
},
|
||||
"mylar": {
|
||||
"series": "Серии",
|
||||
@@ -828,10 +828,10 @@
|
||||
"series": "Серии"
|
||||
},
|
||||
"booklore": {
|
||||
"libraries": "Libraries",
|
||||
"books": "Books",
|
||||
"reading": "Reading",
|
||||
"finished": "Finished"
|
||||
"libraries": "Библиотеки",
|
||||
"books": "Книги",
|
||||
"reading": "Читаю",
|
||||
"finished": "Завершено"
|
||||
},
|
||||
"jdownloader": {
|
||||
"downloadCount": "Очередь",
|
||||
@@ -937,17 +937,17 @@
|
||||
"criticals": "Критические"
|
||||
},
|
||||
"ntfy": {
|
||||
"title": "Title",
|
||||
"priority": "Priority",
|
||||
"title": "Название",
|
||||
"priority": "Приоритет",
|
||||
"lastReceived": "Last Received",
|
||||
"message": "Message",
|
||||
"tags": "Tags",
|
||||
"none": "None",
|
||||
"min": "Min",
|
||||
"low": "Low",
|
||||
"default": "Default",
|
||||
"high": "High",
|
||||
"urgent": "Urgent"
|
||||
"message": "Сообщение",
|
||||
"tags": "Теги",
|
||||
"none": "Отсутствует",
|
||||
"min": "Минимальный",
|
||||
"low": "Низкий",
|
||||
"default": "По-умолчанию",
|
||||
"high": "Высокий",
|
||||
"urgent": "Срочный"
|
||||
},
|
||||
"plantit": {
|
||||
"events": "События",
|
||||
@@ -1087,7 +1087,7 @@
|
||||
},
|
||||
"gitlab": {
|
||||
"groups": "Группы",
|
||||
"issues": "Issues",
|
||||
"issues": "Задачи",
|
||||
"merges": "Мердж-реквесты",
|
||||
"projects": "Проекты"
|
||||
},
|
||||
@@ -1150,26 +1150,26 @@
|
||||
"nextRenewingSubscription": "Следующая оплата"
|
||||
},
|
||||
"unraid": {
|
||||
"STARTED": "Started",
|
||||
"STOPPED": "Stopped",
|
||||
"STARTED": "Запущено",
|
||||
"STOPPED": "Остановлено",
|
||||
"NEW_ARRAY": "Новый массив",
|
||||
"RECON_DISK": "Reconstructing Disk",
|
||||
"DISABLE_DISK": "Disk Disabled",
|
||||
"SWAP_DSBL": "Swap Disable",
|
||||
"INVALID_EXPANSION": "Invalid Expansion",
|
||||
"RECON_DISK": "Восстановление Диска",
|
||||
"DISABLE_DISK": "Диск отключен",
|
||||
"SWAP_DSBL": "Swap отключён",
|
||||
"INVALID_EXPANSION": "Неверное Расширение",
|
||||
"PARITY_NOT_BIGGEST": "Parity Not Biggest",
|
||||
"TOO_MANY_MISSING_DISKS": "Too Many Missing Disks",
|
||||
"NEW_DISK_TOO_SMALL": "New Disk Too Small",
|
||||
"NO_DATA_DISKS": "No Data Disks",
|
||||
"TOO_MANY_MISSING_DISKS": "Слишком много отсутствующих дисков",
|
||||
"NEW_DISK_TOO_SMALL": "Новый диск слишком мал",
|
||||
"NO_DATA_DISKS": "Нет дисков данных",
|
||||
"notifications": "Уведомления",
|
||||
"status": "Статус",
|
||||
"cpu": "ЦП",
|
||||
"memoryUsed": "Использовано ОЗУ",
|
||||
"memoryAvailable": "Memory Available",
|
||||
"memoryAvailable": "Доступная память",
|
||||
"arrayUsed": "Array Used",
|
||||
"arrayFree": "Array Free",
|
||||
"poolUsed": "{{pool}} Used",
|
||||
"poolFree": "{{pool}} Free"
|
||||
"poolUsed": "{{pool}} Использовано",
|
||||
"poolFree": "{{pool}} Свободно"
|
||||
},
|
||||
"backrest": {
|
||||
"num_plans": "Plans",
|
||||
@@ -1180,29 +1180,29 @@
|
||||
"bytes_added_30": "Bytes Added"
|
||||
},
|
||||
"yourspotify": {
|
||||
"songs": "Songs",
|
||||
"songs": "Треков",
|
||||
"time": "Время",
|
||||
"artists": "Artists"
|
||||
"artists": "Исполнителей"
|
||||
},
|
||||
"arcane": {
|
||||
"containers": "Containers",
|
||||
"images": "Images",
|
||||
"image_updates": "Image Updates",
|
||||
"images_unused": "Unused",
|
||||
"environment_required": "Environment ID Required"
|
||||
"containers": "Контейнеров",
|
||||
"images": "Образов",
|
||||
"image_updates": "Обновлений",
|
||||
"images_unused": "Не используется",
|
||||
"environment_required": "Требуется ID окружения"
|
||||
},
|
||||
"dockhand": {
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"cpu": "CPU",
|
||||
"memory": "Memory",
|
||||
"images": "Images",
|
||||
"volumes": "Volumes",
|
||||
"events_today": "Events Today",
|
||||
"pending_updates": "Pending Updates",
|
||||
"stacks": "Stacks",
|
||||
"paused": "Paused",
|
||||
"total": "Total",
|
||||
"running": "Запущено",
|
||||
"stopped": "Остановлено",
|
||||
"cpu": "ЦП",
|
||||
"memory": "ОЗУ",
|
||||
"images": "Образов",
|
||||
"volumes": "Томов",
|
||||
"events_today": "Событий сегодня",
|
||||
"pending_updates": "Обновлений",
|
||||
"stacks": "Стеков",
|
||||
"paused": "На паузе",
|
||||
"total": "Всего",
|
||||
"environment_not_found": "Среда не найдена"
|
||||
},
|
||||
"sparkyfitness": {
|
||||
|
||||
@@ -8,6 +8,13 @@ export default function validateWidgetData(widget, endpoint, data) {
|
||||
let dataParsed = data;
|
||||
let error;
|
||||
let mapping;
|
||||
const mappings = widgets[widget.type]?.mappings;
|
||||
if (mappings) {
|
||||
mapping = Object.values(mappings).find((m) => m.endpoint === endpoint);
|
||||
}
|
||||
|
||||
if (mapping?.allowEmpty && Buffer.isBuffer(data) && data.length === 0) return true;
|
||||
|
||||
if (Buffer.isBuffer(data)) {
|
||||
try {
|
||||
dataParsed = JSON.parse(data);
|
||||
@@ -23,16 +30,12 @@ export default function validateWidgetData(widget, endpoint, data) {
|
||||
}
|
||||
|
||||
if (dataParsed && Object.entries(dataParsed).length) {
|
||||
const mappings = widgets[widget.type]?.mappings;
|
||||
if (mappings) {
|
||||
mapping = Object.values(mappings).find((m) => m.endpoint === endpoint);
|
||||
mapping?.validate?.forEach((key) => {
|
||||
if (dataParsed[key] === undefined) {
|
||||
valid = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
logger.error(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { loggerError } = vi.hoisted(() => ({
|
||||
loggerError: vi.fn(),
|
||||
@@ -18,6 +18,10 @@ vi.mock("widgets/widgets", () => ({
|
||||
endpoint: "foo",
|
||||
validate: ["a", "b"],
|
||||
},
|
||||
empty: {
|
||||
endpoint: "empty",
|
||||
allowEmpty: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -26,6 +30,10 @@ vi.mock("widgets/widgets", () => ({
|
||||
import validateWidgetData from "./validate-widget-data";
|
||||
|
||||
describe("utils/proxy/validate-widget-data", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("returns false when buffer JSON cannot be parsed", () => {
|
||||
expect(validateWidgetData({ type: "test" }, "foo", Buffer.from("not json"))).toBe(false);
|
||||
expect(loggerError).toHaveBeenCalled();
|
||||
@@ -41,4 +49,9 @@ describe("utils/proxy/validate-widget-data", () => {
|
||||
expect(validateWidgetData({ type: "test" }, "foo", Buffer.from(JSON.stringify({ a: 1 })))).toBe(false);
|
||||
expect(loggerError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows empty buffer responses for mappings that explicitly allow them", () => {
|
||||
expect(validateWidgetData({ type: "test" }, "empty", Buffer.from(""))).toBe(true);
|
||||
expect(loggerError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { asJson } from "utils/proxy/api-helpers";
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const noMessages = {
|
||||
title: null,
|
||||
message: null,
|
||||
priority: 3,
|
||||
time: null,
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const widget = {
|
||||
api: "{url}/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
@@ -7,6 +16,14 @@ const widget = {
|
||||
mappings: {
|
||||
messages: {
|
||||
endpoint: "{topic}/json?poll=1&since=latest",
|
||||
allowEmpty: true,
|
||||
map: (data) => {
|
||||
if (Buffer.isBuffer(data) && data.length === 0) {
|
||||
return noMessages;
|
||||
}
|
||||
|
||||
return asJson(data);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { expectWidgetConfigShape } from "test-utils/widget-config";
|
||||
|
||||
@@ -8,4 +8,18 @@ describe("ntfy widget config", () => {
|
||||
it("exports a valid widget config", () => {
|
||||
expectWidgetConfigShape(widget);
|
||||
});
|
||||
|
||||
it("maps an empty latest message response to the no messages state", () => {
|
||||
expect(widget.mappings.messages.map(Buffer.from(""))).toEqual({
|
||||
title: null,
|
||||
message: null,
|
||||
priority: 3,
|
||||
time: null,
|
||||
tags: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("parses latest message responses", () => {
|
||||
expect(widget.mappings.messages.map(Buffer.from('{"message":"hello"}'))).toEqual({ message: "hello" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ const widget = {
|
||||
},
|
||||
"nodes/localhost/tasks": {
|
||||
endpoint: "nodes/localhost/tasks",
|
||||
params: ["errors", "limit", "since"],
|
||||
},
|
||||
"nodes/localhost/status": {
|
||||
endpoint: "nodes/localhost/status",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { expectWidgetConfigShape } from "test-utils/widget-config";
|
||||
|
||||
@@ -8,4 +8,8 @@ describe("proxmoxbackupserver widget config", () => {
|
||||
it("exports a valid widget config", () => {
|
||||
expectWidgetConfigShape(widget);
|
||||
});
|
||||
|
||||
it("requires failed task query params for the tasks endpoint", () => {
|
||||
expect(widget.mappings["nodes/localhost/tasks"].params).toEqual(["errors", "limit", "since"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,12 +41,12 @@ export default async function qbittorrentProxyHandler(req, res) {
|
||||
if (status === 403) {
|
||||
[status, data] = await login(widget);
|
||||
|
||||
if (status !== 200) {
|
||||
if (![200, 204].includes(status)) {
|
||||
logger.error("HTTP %d logging in to qBittorrent. Data: %s", status, data);
|
||||
return res.status(status).end(data);
|
||||
}
|
||||
|
||||
if (data.toString() !== "Ok.") {
|
||||
if (status === 200 && data.toString() !== "Ok.") {
|
||||
logger.error("Error logging in to qBittorrent: Data: %s", data);
|
||||
return res.status(401).end(data);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,25 @@ describe("widgets/qbittorrent/proxy", () => {
|
||||
expect(res.body).toEqual(Buffer.from("data"));
|
||||
});
|
||||
|
||||
it("accepts qBittorrent 5.2.0 no-content login responses", async () => {
|
||||
getServiceWidget.mockResolvedValue({ url: "http://qb", username: "u", password: "p" });
|
||||
|
||||
httpProxy
|
||||
.mockResolvedValueOnce([403, "application/json", Buffer.from("nope")])
|
||||
.mockResolvedValueOnce([204, null, Buffer.from("")])
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from("data")]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", endpoint: "torrents/info", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await qbittorrentProxyHandler(req, res);
|
||||
|
||||
expect(httpProxy).toHaveBeenCalledTimes(3);
|
||||
expect(httpProxy.mock.calls[1][0]).toBe("http://qb/api/v2/auth/login");
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toEqual(Buffer.from("data"));
|
||||
});
|
||||
|
||||
it("returns 401 when login succeeds but response body is not Ok.", async () => {
|
||||
getServiceWidget.mockResolvedValue({ url: "http://qb", username: "u", password: "p" });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user