mirror of
https://github.com/gethomepage/homepage.git
synced 2026-03-31 07:12:17 -07:00
Compare commits
24 Commits
v1.10.1
...
feature/om
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b48d283dc2 | ||
|
|
d313e0a124 | ||
|
|
51d718a21a | ||
|
|
29e2502d74 | ||
|
|
d529f81cb4 | ||
|
|
1645c1b8a1 | ||
|
|
614a87d768 | ||
|
|
862c5d9f38 | ||
|
|
d3374dc461 | ||
|
|
795e2505ca | ||
|
|
cb8421df0b | ||
|
|
152888d611 | ||
|
|
ea527e4fb1 | ||
|
|
09bab7637e | ||
|
|
597f6ecf16 | ||
|
|
fe0b214334 | ||
|
|
cdc96438cd | ||
|
|
ca7dfb56c8 | ||
|
|
95852d23c2 | ||
|
|
84231a1754 | ||
|
|
f4f54cea60 | ||
|
|
06595ef107 | ||
|
|
91b9aa479a | ||
|
|
08cde2f597 |
@@ -9,11 +9,11 @@ coverage:
|
||||
project:
|
||||
default:
|
||||
target: 100%
|
||||
threshold: 25%
|
||||
threshold: 15%
|
||||
patch:
|
||||
default:
|
||||
target: 100%
|
||||
threshold: 25%
|
||||
threshold: 10%
|
||||
|
||||
comment:
|
||||
layout: "reach,diff,flags,files"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
title: "[Feature Request] "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
#### ⚠️ Don't forget to search [existing issues](https://github.com/gethomepage/homepage/search?q=&type=issues) and [discussions](https://github.com/gethomepage/homepage/search?q=&type=discussions) (including closed ones!).
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
|
||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -35,8 +35,8 @@ What type of change does your PR introduce to Homepage?
|
||||
## Checklist:
|
||||
|
||||
- [ ] If applicable, I have added corresponding documentation changes.
|
||||
- [ ] If applicable, I have added or updated tests for new features and bug fixes.
|
||||
- [ ] If applicable, I have reviewed the [feature / enhancement](https://gethomepage.dev/more/development/#new-feature-guidelines) and / or [service widget guidelines](https://gethomepage.dev/more/development/#service-widget-guidelines).
|
||||
- [ ] I have checked that all code style checks pass using [pre-commit hooks](https://gethomepage.dev/more/development/#code-formatting-with-pre-commit-hooks) and [linting checks](https://gethomepage.dev/more/development/#code-linting).
|
||||
- [ ] If applicable, I have added or updated tests for new features and bug fixes (see [testing](https://gethomepage.dev/widgets/authoring/getting-started/#testing)).
|
||||
- [ ] If applicable, I have reviewed the [feature / enhancement](https://gethomepage.dev/widgets/authoring/getting-started/#new-feature-guidelines) and / or [service widget guidelines](https://gethomepage.dev/widgets/authoring/getting-started/#service-widget-guidelines).
|
||||
- [ ] I have checked that all code style checks pass using [pre-commit hooks](https://gethomepage.dev/widgets/authoring/getting-started/#code-formatting-with-pre-commit-hooks) and [linting checks](https://gethomepage.dev/widgets/authoring/getting-started/#code-linting).
|
||||
- [ ] If applicable, I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
||||
- [ ] In the description above I have disclosed the use of AI tools in the coding of this PR.
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -13,13 +13,13 @@ jobs:
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
|
||||
@@ -38,11 +38,11 @@ People _love_ thorough bug reports. I'm not even kidding.
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
Please see the [documentation regarding development](https://gethomepage.dev/more/development/) and specifically the [guidelines for new service widgets](https://gethomepage.dev/more/development/#service-widget-guidelines) if you are considering making one.
|
||||
Please see the [documentation regarding development](https://gethomepage.dev/widgets/authoring/getting-started/#development) and specifically the [guidelines for new service widgets](https://gethomepage.dev/widgets/authoring/getting-started/#service-widget-guidelines) if you are considering making one.
|
||||
|
||||
## Use a Consistent Coding Style
|
||||
|
||||
Please see information in the docs regarding [code formatting with pre-commit hooks](https://gethomepage.dev/more/development/#code-formatting-with-pre-commit-hooks).
|
||||
Please see information in the docs regarding [code formatting with pre-commit hooks](https://gethomepage.dev/widgets/authoring/getting-started/#code-formatting-with-pre-commit-hooks).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -177,6 +177,16 @@ labels:
|
||||
- homepage.widget.fields=["field1","field2"] # optional
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
If you use mapping syntax (`:`) for labels instead of list syntax (`-`), array values like `fields` must be wrapped in single quotes so they are passed as a string:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
...
|
||||
homepage.widget.fields: '["field1","field2"]'
|
||||
```
|
||||
|
||||
Multiple widgets can be specified by incrementing the index, e.g.
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -7,13 +7,17 @@ You can include all or some of the available resources. If you do not want to se
|
||||
|
||||
The disk path is the path reported by `df` (Mounted On), or the mount point of the disk.
|
||||
|
||||
!!! note
|
||||
|
||||
Any disk you wish to access must be mounted to your container as a volume.
|
||||
|
||||
The cpu and memory resource information are the container's usage while [glances](glances.md) displays statistics for the host machine on which it is installed.
|
||||
|
||||
The resources widget primarily relies on a popular tool called [systeminformation](https://systeminformation.io). Thus, any limitiations of that software apply, for example, BRTFS RAID is not supported for the disk usage. In this case users may want to use the [glances widget](glances.md) instead.
|
||||
|
||||
_Note: unfortunately, the package used for getting CPU temp ([systeminformation](https://systeminformation.io)) is not compatible with some setups and will not report any value(s) for CPU temp._
|
||||
!!! warning
|
||||
|
||||
**Any disk you wish to access must be mounted to your container as a volume.**
|
||||
The package used for getting CPU temp ([systeminformation](https://systeminformation.io)) is not compatible with some setups and will not report any value(s) for CPU temp.
|
||||
|
||||
```yaml
|
||||
- resources:
|
||||
@@ -75,3 +79,10 @@ You can additionally supply an optional `expanded` property set to true in order
|
||||
```
|
||||
|
||||

|
||||
|
||||
To monitor a named host network interface in Docker (for example `network: eno1`), mount host `/sys` (read-only):
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- /sys:/sys:ro
|
||||
```
|
||||
|
||||
@@ -67,7 +67,7 @@ You can also find a list of all available service widgets in the sidebar navigat
|
||||
- [Jackett](jackett.md)
|
||||
- [JDownloader](jdownloader.md)
|
||||
- [Jellyfin](jellyfin.md)
|
||||
- [Jellyseerr](jellyseerr.md)
|
||||
- [Seerr](seerr.md)
|
||||
- [Jellystat](jellystat.md)
|
||||
- [Kavita](kavita.md)
|
||||
- [Komga](komga.md)
|
||||
@@ -101,7 +101,6 @@ You can also find a list of all available service widgets in the sidebar navigat
|
||||
- [OpenMediaVault](openmediavault.md)
|
||||
- [OpenWRT](openwrt.md)
|
||||
- [OPNsense](opnsense.md)
|
||||
- [Overseerr](overseerr.md)
|
||||
- [PaperlessNGX](paperlessngx.md)
|
||||
- [Peanut](peanut.md)
|
||||
- [pfSense](pfsense.md)
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Jellyfin Widget Configuration
|
||||
|
||||
Learn more about [Jellyfin](https://github.com/jellyfin/jellyfin).
|
||||
|
||||
You can create an API key from inside Jellyfin at `Settings > Advanced > Api Keys`.
|
||||
You can create an API key from inside the Jellyfin Administration Dashboard under `Advanced > API Keys`.
|
||||
|
||||
As of v0.6.11 the widget supports fields `["movies", "series", "episodes", "songs"]`. These blocks are disabled by default but can be enabled with the `enableBlocks` option, and the "Now Playing" feature (enabled by default) can be disabled with the `enableNowPlaying` option.
|
||||
|
||||
@@ -17,7 +17,7 @@ As of v0.6.11 the widget supports fields `["movies", "series", "episodes", "song
|
||||
```yaml
|
||||
widget:
|
||||
type: jellyfin
|
||||
url: http://jellyfin.host.or.ip
|
||||
url: http://jellyfin.host.or.ip:port
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
version: 2 # optional, default is 1
|
||||
enableBlocks: true # optional, defaults to false
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
title: Jellyseerr
|
||||
description: Jellyseerr Widget Configuration
|
||||
---
|
||||
|
||||
Learn more about [Jellyseerr](https://github.com/Fallenbagel/jellyseerr).
|
||||
|
||||
Find your API key under `Settings > General > API Key`.
|
||||
|
||||
Allowed fields: `["pending", "approved", "available", "issues"]`.
|
||||
Default fields: `["pending", "approved", "available"]`.
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: jellyseerr
|
||||
url: http://jellyseerr.host.or.ip
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
```
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: Overseerr
|
||||
description: Overseerr Widget Configuration
|
||||
---
|
||||
|
||||
Learn more about [Overseerr](https://github.com/sct/overseerr).
|
||||
|
||||
Find your API key under `Settings > General`.
|
||||
|
||||
Allowed fields: `["pending", "approved", "available", "processing"]`.
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: overseerr
|
||||
url: http://overseerr.host.or.ip
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
```
|
||||
@@ -12,7 +12,7 @@ Allowed fields: no configurable fields for this widget.
|
||||
```yaml
|
||||
widget:
|
||||
type: tautulli
|
||||
url: http://tautulli.host.or.ip
|
||||
url: http://tautulli.host.or.ip:port
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
enableUser: true # optional, defaults to false
|
||||
showEpisodeNumber: true # optional, defaults to false
|
||||
|
||||
20
docs/widgets/services/seerr.md
Normal file
20
docs/widgets/services/seerr.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Seerr Widget
|
||||
description: Seerr Widget Configuration
|
||||
---
|
||||
|
||||
Learn more about [Seerr](https://github.com/seerr-team/seerr).
|
||||
|
||||
Find your API key under `Settings > General > API Key`.
|
||||
|
||||
_Jellyseerr and Overseerr merged into Seerr. Use `type: seerr` (legacy `type: jellyseerr` and `type: overseerr` are aliased)._
|
||||
|
||||
Allowed fields: `["pending", "approved", "available", "completed", "processing", "issues"]`.
|
||||
Default fields: `["pending", "approved", "completed"]`.
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: seerr
|
||||
url: http://seerr.host.or.ip
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
```
|
||||
15
docs/widgets/services/sparkyfitness.md
Normal file
15
docs/widgets/services/sparkyfitness.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: SparkyFitness
|
||||
description: SparkyFitness Widget Configuration
|
||||
---
|
||||
|
||||
Learn more about [SparkyFitness](https://github.com/CodeWithCJ/SparkyFitness).
|
||||
|
||||
Allowed fields: `["eaten", "burned", "remaining", "steps"]`.
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: sparkyfitness
|
||||
url: http://sparkyfitness.host.or.ip
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
```
|
||||
21
docs/widgets/services/tracearr.md
Normal file
21
docs/widgets/services/tracearr.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: Tracearr
|
||||
description: Tracearr Widget Configuration
|
||||
---
|
||||
|
||||
Learn more about [Tracearr](https://www.tracearr.com/).
|
||||
|
||||
Provides detailed information about currently active streams across multiple servers.
|
||||
|
||||
Allowed fields (for summary view): `["streams", "transcodes", "directplay", "bitrate"]`.
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: tracearr
|
||||
url: http://tracearr.host.or.ip:3000
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
view: both # optional, "summary", "details", or "both", defaults to "details"
|
||||
enableUser: true # optional, defaults to false
|
||||
showEpisodeNumber: true # optional, defaults to false
|
||||
expandOneStreamToTwoRows: false # optional, defaults to true
|
||||
```
|
||||
@@ -91,7 +91,6 @@ nav:
|
||||
- widgets/services/jackett.md
|
||||
- widgets/services/jdownloader.md
|
||||
- widgets/services/jellyfin.md
|
||||
- widgets/services/jellyseerr.md
|
||||
- widgets/services/jellystat.md
|
||||
- widgets/services/kavita.md
|
||||
- widgets/services/komga.md
|
||||
@@ -125,7 +124,6 @@ nav:
|
||||
- widgets/services/openmediavault.md
|
||||
- widgets/services/opnsense.md
|
||||
- widgets/services/openwrt.md
|
||||
- widgets/services/overseerr.md
|
||||
- widgets/services/pangolin.md
|
||||
- widgets/services/paperlessngx.md
|
||||
- widgets/services/peanut.md
|
||||
@@ -151,8 +149,10 @@ nav:
|
||||
- widgets/services/rutorrent.md
|
||||
- widgets/services/sabnzbd.md
|
||||
- widgets/services/scrutiny.md
|
||||
- widgets/services/seerr.md
|
||||
- widgets/services/slskd.md
|
||||
- widgets/services/sonarr.md
|
||||
- widgets/services/sparkyfitness.md
|
||||
- widgets/services/speedtest-tracker.md
|
||||
- widgets/services/spoolman.md
|
||||
- widgets/services/stash.md
|
||||
@@ -165,6 +165,7 @@ nav:
|
||||
- widgets/services/technitium.md
|
||||
- widgets/services/tdarr.md
|
||||
- widgets/services/traefik.md
|
||||
- widgets/services/tracearr.md
|
||||
- widgets/services/transmission.md
|
||||
- widgets/services/trilium.md
|
||||
- widgets/services/truenas.md
|
||||
|
||||
@@ -184,6 +184,13 @@
|
||||
"no_active": "No Active Streams",
|
||||
"plex_connection_error": "Check Plex Connection"
|
||||
},
|
||||
"tracearr": {
|
||||
"no_active": "No Active Streams",
|
||||
"streams": "Streams",
|
||||
"transcodes": "Transcodes",
|
||||
"directplay": "Direct Play",
|
||||
"bitrate": "Bitrate"
|
||||
},
|
||||
"omada": {
|
||||
"connectedAp": "Connected APs",
|
||||
"activeUser": "Active devices",
|
||||
@@ -282,17 +289,13 @@
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"seerr": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"available": "Available",
|
||||
"issues": "Open Issues"
|
||||
},
|
||||
"overseerr": {
|
||||
"pending": "Pending",
|
||||
"completed": "Completed",
|
||||
"processing": "Processing",
|
||||
"approved": "Approved",
|
||||
"available": "Available"
|
||||
"issues": "Open Issues"
|
||||
},
|
||||
"netalertx": {
|
||||
"total": "Total",
|
||||
@@ -1171,5 +1174,11 @@
|
||||
"paused": "Paused",
|
||||
"total": "Total",
|
||||
"environment_not_found": "Environment Not Found"
|
||||
},
|
||||
"sparkyfitness": {
|
||||
"eaten": "Eaten",
|
||||
"burned": "Burned",
|
||||
"remaining": "Remaining",
|
||||
"steps": "Steps"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,4 +344,17 @@ describe("pages/api/services/proxy", () => {
|
||||
expect(res.statusCode).toBe(500);
|
||||
expect(res.body).toEqual({ error: "Unexpected error" });
|
||||
});
|
||||
|
||||
it("returns 500 when an async proxy handler throws", async () => {
|
||||
getServiceWidget.mockResolvedValue({ type: "linkwarden" });
|
||||
handlerFn.handler.mockRejectedValueOnce(new Error("proxy boom"));
|
||||
|
||||
const req = { method: "GET", query: { group: "g", service: "s", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await servicesProxy(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(500);
|
||||
expect(res.body).toEqual({ error: "Unexpected error" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,17 +90,74 @@ describe("pages/api/widgets/resources", () => {
|
||||
});
|
||||
|
||||
it("returns 404 when requested network interface does not exist", async () => {
|
||||
si.networkStats.mockResolvedValueOnce([{ iface: "en0" }]);
|
||||
si.networkStats.mockResolvedValueOnce([{ iface: "en0" }]).mockResolvedValueOnce([
|
||||
{
|
||||
iface: "missing",
|
||||
operstate: "unknown",
|
||||
rx_bytes: 0,
|
||||
rx_dropped: 0,
|
||||
rx_errors: 0,
|
||||
tx_bytes: 0,
|
||||
tx_dropped: 0,
|
||||
tx_errors: 0,
|
||||
rx_sec: null,
|
||||
tx_sec: null,
|
||||
ms: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
const req = { query: { type: "network", interfaceName: "missing" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(si.networkStats).toHaveBeenNthCalledWith(1, "*");
|
||||
expect(si.networkStats).toHaveBeenNthCalledWith(2, "missing");
|
||||
expect(res.statusCode).toBe(404);
|
||||
expect(res.body).toEqual({ error: "Interface not found" });
|
||||
});
|
||||
|
||||
it("falls back to direct named interface query when wildcard enumeration misses it", async () => {
|
||||
si.networkStats.mockResolvedValueOnce([{ iface: "eth0", rx_bytes: 1 }]).mockResolvedValueOnce([
|
||||
{
|
||||
iface: "eno1",
|
||||
operstate: "up",
|
||||
rx_bytes: 1000,
|
||||
rx_dropped: 0,
|
||||
rx_errors: 0,
|
||||
tx_bytes: 500,
|
||||
tx_dropped: 0,
|
||||
tx_errors: 0,
|
||||
rx_sec: null,
|
||||
tx_sec: null,
|
||||
ms: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
const req = { query: { type: "network", interfaceName: "eno1" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(si.networkStats).toHaveBeenNthCalledWith(1, "*");
|
||||
expect(si.networkStats).toHaveBeenNthCalledWith(2, "eno1");
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body.interface).toBe("eno1");
|
||||
expect(res.body.network).toEqual({
|
||||
iface: "eno1",
|
||||
operstate: "up",
|
||||
rx_bytes: 1000,
|
||||
rx_dropped: 0,
|
||||
rx_errors: 0,
|
||||
tx_bytes: 500,
|
||||
tx_dropped: 0,
|
||||
tx_errors: 0,
|
||||
rx_sec: null,
|
||||
tx_sec: null,
|
||||
ms: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns default interface network stats", async () => {
|
||||
si.networkStats.mockResolvedValueOnce([{ iface: "en0", rx_bytes: 1 }]);
|
||||
si.networkInterfaceDefault.mockResolvedValueOnce("en0");
|
||||
|
||||
@@ -9,6 +9,8 @@ import { buildHighlightConfig } from "utils/highlights";
|
||||
const ALIASED_WIDGETS = {
|
||||
pialert: "netalertx",
|
||||
hoarder: "karakeep",
|
||||
jellyseerr: "seerr",
|
||||
overseerr: "seerr",
|
||||
};
|
||||
|
||||
export default function Container({ error = false, children, service }) {
|
||||
|
||||
@@ -58,6 +58,26 @@ describe("components/services/widget/container", () => {
|
||||
expect(screen.getByTestId("karakeep.count")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("supports seerr aliases when filtering (jellyseerr/overseerr -> seerr)", () => {
|
||||
renderWithProviders(
|
||||
<Container service={{ widget: { type: "jellyseerr", fields: ["pending"] } }}>
|
||||
<Dummy label="seerr.pending" />
|
||||
</Container>,
|
||||
{ settings: {} },
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("seerr.pending")).toBeInTheDocument();
|
||||
|
||||
renderWithProviders(
|
||||
<Container service={{ widget: { type: "overseerr", fields: ["processing"] } }}>
|
||||
<Dummy label="seerr.processing" />
|
||||
</Container>,
|
||||
{ settings: {} },
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("seerr.processing")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("returns null when errors are hidden via settings.hideErrors", () => {
|
||||
const { container } = renderWithProviders(
|
||||
<Container error="nope" service={{ widget: { type: "omada", hide_errors: false } }}>
|
||||
|
||||
@@ -11,7 +11,7 @@ import Resource from "../widget/resource";
|
||||
import Resources from "../widget/resources";
|
||||
import WidgetLabel from "../widget/widget_label";
|
||||
|
||||
const cpuSensorLabels = ["cpu_thermal", "Core", "Tctl"];
|
||||
const cpuSensorLabels = ["cpu_thermal", "Core", "Tctl", "Temperature"];
|
||||
|
||||
function convertToFahrenheit(t) {
|
||||
return (t * 9) / 5 + 32;
|
||||
|
||||
@@ -29,7 +29,7 @@ export default async function handler(req, res) {
|
||||
if (serviceProxyHandler instanceof Function) {
|
||||
// quick return for no endpoint services, calendar is an exception
|
||||
if (!req.query.endpoint || serviceProxyHandler === calendarProxyHandler) {
|
||||
return serviceProxyHandler(req, res);
|
||||
return await serviceProxyHandler(req, res);
|
||||
}
|
||||
|
||||
// map opaque endpoints to their actual endpoint
|
||||
@@ -90,15 +90,15 @@ export default async function handler(req, res) {
|
||||
}
|
||||
|
||||
if (endpointProxy instanceof Function) {
|
||||
return endpointProxy(req, res, map);
|
||||
return await endpointProxy(req, res, map);
|
||||
}
|
||||
|
||||
return serviceProxyHandler(req, res, map);
|
||||
return await serviceProxyHandler(req, res, map);
|
||||
}
|
||||
|
||||
if (widget.allowedEndpoints instanceof RegExp) {
|
||||
if (widget.allowedEndpoints.test(req.query.endpoint)) {
|
||||
return serviceProxyHandler(req, res);
|
||||
return await serviceProxyHandler(req, res);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,21 @@ import createLogger from "utils/logger";
|
||||
|
||||
const logger = createLogger("resources");
|
||||
|
||||
function isMissingNetworkStat(networkData, interfaceName) {
|
||||
return (
|
||||
networkData.operstate === "unknown" &&
|
||||
networkData.rx_bytes === 0 &&
|
||||
networkData.rx_dropped === 0 &&
|
||||
networkData.rx_errors === 0 &&
|
||||
networkData.tx_bytes === 0 &&
|
||||
networkData.tx_dropped === 0 &&
|
||||
networkData.tx_errors === 0 &&
|
||||
networkData.rx_sec === null &&
|
||||
networkData.tx_sec === null &&
|
||||
networkData.ms === 0
|
||||
);
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { type, target, interfaceName = "default" } = req.query;
|
||||
|
||||
@@ -64,6 +79,17 @@ export default async function handler(req, res) {
|
||||
logger.debug("networkData:", JSON.stringify(networkData));
|
||||
if (interfaceName && interfaceName !== "default") {
|
||||
networkData = networkData.filter((network) => network.iface === interfaceName).at(0);
|
||||
if (!networkData) {
|
||||
// Fallback for e.g. docker where networkStats("*") may not return stats for host interfaces
|
||||
const directNetworkData = await si.networkStats(interfaceName);
|
||||
logger.debug("directNetworkData:", JSON.stringify(directNetworkData));
|
||||
networkData = Array.isArray(directNetworkData) ? directNetworkData.at(0) : null;
|
||||
|
||||
// si returns unknown + zeroes when interface truly does not exist
|
||||
if (!networkData || isMissingNetworkStat(networkData, interfaceName)) {
|
||||
networkData = null;
|
||||
}
|
||||
}
|
||||
if (!networkData) {
|
||||
return res.status(404).json({
|
||||
error: "Interface not found",
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { expect } from "vitest";
|
||||
|
||||
export function findServiceBlockByLabel(container, label) {
|
||||
const blocks = Array.from(container.querySelectorAll(".service-block"));
|
||||
return blocks.find((b) => b.textContent?.includes(label));
|
||||
}
|
||||
|
||||
export function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ export function cleanServiceGroups(groups) {
|
||||
enableNowPlaying,
|
||||
enableMediaControl,
|
||||
|
||||
// emby, jellyfin, tautulli
|
||||
// emby, jellyfin, tautulli, tracearr
|
||||
enableUser,
|
||||
expandOneStreamToTwoRows,
|
||||
showEpisodeNumber,
|
||||
@@ -542,12 +542,15 @@ export function cleanServiceGroups(groups) {
|
||||
if (enableBlocks !== undefined) widget.enableBlocks = JSON.parse(enableBlocks);
|
||||
if (enableNowPlaying !== undefined) widget.enableNowPlaying = JSON.parse(enableNowPlaying);
|
||||
}
|
||||
if (["emby", "jellyfin", "tautulli"].includes(type)) {
|
||||
if (["emby", "jellyfin", "tautulli", "tracearr"].includes(type)) {
|
||||
if (expandOneStreamToTwoRows !== undefined)
|
||||
widget.expandOneStreamToTwoRows = !!JSON.parse(expandOneStreamToTwoRows);
|
||||
if (showEpisodeNumber !== undefined) widget.showEpisodeNumber = !!JSON.parse(showEpisodeNumber);
|
||||
if (enableUser !== undefined) widget.enableUser = !!JSON.parse(enableUser);
|
||||
}
|
||||
if (type === "tracearr") {
|
||||
if (view !== undefined) widget.view = view;
|
||||
}
|
||||
if (["sonarr", "radarr"].includes(type)) {
|
||||
if (enableQueue !== undefined) widget.enableQueue = JSON.parse(enableQueue);
|
||||
}
|
||||
|
||||
@@ -312,6 +312,13 @@ describe("utils/config/service-helpers", () => {
|
||||
{ type: "healthchecks", uuid: "u" },
|
||||
{ type: "speedtest", bitratePrecision: "3", version: "1" },
|
||||
{ type: "stocks", watchlist: "AAPL", showUSMarketStatus: true },
|
||||
{
|
||||
type: "tracearr",
|
||||
expandOneStreamToTwoRows: "true",
|
||||
showEpisodeNumber: "true",
|
||||
enableUser: "true",
|
||||
view: "both",
|
||||
},
|
||||
{ type: "wgeasy", threshold: "10", version: "1" },
|
||||
{ type: "technitium", range: "24h" },
|
||||
{ type: "lubelogger", vehicleID: "12" },
|
||||
@@ -350,6 +357,14 @@ describe("utils/config/service-helpers", () => {
|
||||
expect(widgets.find((w) => w.type === "speedtest")).toEqual(
|
||||
expect.objectContaining({ bitratePrecision: 3, version: 1 }),
|
||||
);
|
||||
expect(widgets.find((w) => w.type === "tracearr")).toEqual(
|
||||
expect.objectContaining({
|
||||
expandOneStreamToTwoRows: true,
|
||||
showEpisodeNumber: true,
|
||||
enableUser: true,
|
||||
view: "both",
|
||||
}),
|
||||
);
|
||||
expect(widgets.find((w) => w.type === "jellystat")).toEqual(expect.objectContaining({ days: 7 }));
|
||||
expect(widgets.find((w) => w.type === "lubelogger")).toEqual(expect.objectContaining({ vehicleID: 12 }));
|
||||
});
|
||||
|
||||
@@ -74,6 +74,21 @@ const toNumber = (value) => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const extractNumericToken = (value) => {
|
||||
if (typeof value !== "string") return undefined;
|
||||
const match = value.match(/[-+]?\d[\d\s.,]*/);
|
||||
if (!match) return undefined;
|
||||
|
||||
const token = match[0].trim();
|
||||
if (!token) return undefined;
|
||||
|
||||
const prefix = value.slice(0, match.index).trim();
|
||||
const suffix = value.slice((match.index ?? 0) + match[0].length).trim();
|
||||
if (/\d/.test(prefix) || /\d/.test(suffix)) return undefined;
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
const parseNumericValue = (value) => {
|
||||
if (value === null || value === undefined) return undefined;
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
@@ -85,7 +100,9 @@ const parseNumericValue = (value) => {
|
||||
const direct = Number(trimmed);
|
||||
if (!Number.isNaN(direct)) return direct;
|
||||
|
||||
const compact = trimmed.replace(/\s+/g, "");
|
||||
const candidate = extractNumericToken(trimmed);
|
||||
const numericString = candidate ?? trimmed;
|
||||
const compact = numericString.replace(/\s+/g, "");
|
||||
if (!compact || !/^[-+]?[0-9.,]+$/.test(compact)) return undefined;
|
||||
|
||||
const commaCount = (compact.match(/,/g) || []).length;
|
||||
|
||||
@@ -136,6 +136,9 @@ describe("utils/highlights", () => {
|
||||
const cfg = buildHighlightConfig(null, {
|
||||
// string numeric rule values go through toNumber()
|
||||
gt: { numeric: { when: "gt", value: "5", level: "warn" } },
|
||||
withUnitSuffix: { numeric: { when: "gt", value: 5, level: "warn" } },
|
||||
withUnitPrefix: { numeric: { when: "gt", value: 5, level: "warn" } },
|
||||
localizedUnitSuffix: { numeric: { when: "gt", value: 0.5, level: "warn" } },
|
||||
commaGrouped: { numeric: { when: "eq", value: 1234, level: "good" } },
|
||||
commaDecimal: { numeric: { when: "eq", value: 12.34, level: "good" } },
|
||||
dotDecimal: { numeric: { when: "eq", value: 12.34, level: "good" } },
|
||||
@@ -143,6 +146,12 @@ describe("utils/highlights", () => {
|
||||
});
|
||||
|
||||
expect(evaluateHighlight("gt", "6", cfg)).toMatchObject({ level: "warn", source: "numeric" });
|
||||
expect(evaluateHighlight("withUnitSuffix", "5.2 ms", cfg)).toMatchObject({ level: "warn", source: "numeric" });
|
||||
expect(evaluateHighlight("withUnitPrefix", "ms 5.2", cfg)).toMatchObject({ level: "warn", source: "numeric" });
|
||||
expect(evaluateHighlight("localizedUnitSuffix", "0,71\u202Fms", cfg)).toMatchObject({
|
||||
level: "warn",
|
||||
source: "numeric",
|
||||
});
|
||||
expect(evaluateHighlight("commaGrouped", "1,234", cfg)).toMatchObject({ level: "good", source: "numeric" });
|
||||
expect(evaluateHighlight("commaDecimal", "12,34", cfg)).toMatchObject({ level: "good", source: "numeric" });
|
||||
// Include a space so Number(trimmed) fails and we exercise the dot parsing branch.
|
||||
@@ -161,6 +170,9 @@ describe("utils/highlights", () => {
|
||||
// "1.2.3" is not a valid grouped or decimal number for our parser.
|
||||
expect(evaluateHighlight("num", "1.2.3", cfg)).toBeNull();
|
||||
|
||||
// Multiple numbers in one string should not be treated as a single numeric value.
|
||||
expect(evaluateHighlight("num", "5/10 ms", cfg)).toBeNull();
|
||||
|
||||
// JSX-ish values should not be treated as numeric.
|
||||
expect(evaluateHighlight("num", { props: { children: "x" } }, cfg)).toBeNull();
|
||||
});
|
||||
|
||||
@@ -64,6 +64,7 @@ export default async function credentialedProxyHandler(req, res, map) {
|
||||
"pangolin",
|
||||
"tailscale",
|
||||
"tandoor",
|
||||
"tracearr",
|
||||
"pterodactyl",
|
||||
"vikunja",
|
||||
"firefly",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
|
||||
@@ -13,13 +14,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const blocks = Array.from(container.querySelectorAll(".service-block"));
|
||||
const block = blocks.find((b) => b.textContent?.includes(label));
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/arcane/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
|
||||
@@ -13,13 +14,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const blocks = Array.from(container.querySelectorAll(".service-block"));
|
||||
const block = blocks.find((b) => b.textContent?.includes(label));
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/argocd/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
|
||||
@@ -13,13 +14,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const blocks = Array.from(container.querySelectorAll(".service-block"));
|
||||
const block = blocks.find((b) => b.textContent?.includes(label));
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/audiobookshelf/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
|
||||
@@ -13,13 +14,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const blocks = Array.from(container.querySelectorAll(".service-block"));
|
||||
const block = blocks.find((b) => b.textContent?.includes(label));
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/authentik/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
|
||||
@@ -13,13 +14,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const blocks = Array.from(container.querySelectorAll(".service-block"));
|
||||
const block = blocks.find((b) => b.textContent?.includes(label));
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/autobrr/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
|
||||
@@ -13,13 +14,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const blocks = Array.from(container.querySelectorAll(".service-block"));
|
||||
const block = blocks.find((b) => b.textContent?.includes(label));
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/azuredevops/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
|
||||
@@ -12,12 +12,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/beszel/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
|
||||
@@ -12,12 +12,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/caddy/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
|
||||
@@ -12,12 +12,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/changedetectionio/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
|
||||
@@ -12,12 +12,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/channelsdvrserver/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
|
||||
@@ -12,12 +12,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/checkmk/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
|
||||
@@ -12,12 +12,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/cloudflared/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -65,7 +65,7 @@ const components = {
|
||||
jackett: dynamic(() => import("./jackett/component")),
|
||||
jdownloader: dynamic(() => import("./jdownloader/component")),
|
||||
jellyfin: dynamic(() => import("./jellyfin/component")),
|
||||
jellyseerr: dynamic(() => import("./jellyseerr/component")),
|
||||
jellyseerr: dynamic(() => import("./seerr/component")),
|
||||
jellystat: dynamic(() => import("./jellystat/component")),
|
||||
kavita: dynamic(() => import("./kavita/component")),
|
||||
komga: dynamic(() => import("./komga/component")),
|
||||
@@ -97,7 +97,7 @@ const components = {
|
||||
ombi: dynamic(() => import("./ombi/component")),
|
||||
opendtu: dynamic(() => import("./opendtu/component")),
|
||||
opnsense: dynamic(() => import("./opnsense/component")),
|
||||
overseerr: dynamic(() => import("./overseerr/component")),
|
||||
overseerr: dynamic(() => import("./seerr/component")),
|
||||
openmediavault: dynamic(() => import("./openmediavault/component")),
|
||||
openwrt: dynamic(() => import("./openwrt/component")),
|
||||
paperlessngx: dynamic(() => import("./paperlessngx/component")),
|
||||
@@ -124,8 +124,10 @@ const components = {
|
||||
rutorrent: dynamic(() => import("./rutorrent/component")),
|
||||
sabnzbd: dynamic(() => import("./sabnzbd/component")),
|
||||
scrutiny: dynamic(() => import("./scrutiny/component")),
|
||||
seerr: dynamic(() => import("./seerr/component")),
|
||||
slskd: dynamic(() => import("./slskd/component")),
|
||||
sonarr: dynamic(() => import("./sonarr/component")),
|
||||
sparkyfitness: dynamic(() => import("./sparkyfitness/component")),
|
||||
speedtest: dynamic(() => import("./speedtest/component")),
|
||||
spoolman: dynamic(() => import("./spoolman/component")),
|
||||
stash: dynamic(() => import("./stash/component")),
|
||||
@@ -138,6 +140,7 @@ const components = {
|
||||
tautulli: dynamic(() => import("./tautulli/component")),
|
||||
technitium: dynamic(() => import("./technitium/component")),
|
||||
tdarr: dynamic(() => import("./tdarr/component")),
|
||||
tracearr: dynamic(() => import("./tracearr/component")),
|
||||
traefik: dynamic(() => import("./traefik/component")),
|
||||
transmission: dynamic(() => import("./transmission/component")),
|
||||
trilium: dynamic(() => import("./trilium/component")),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
|
||||
@@ -12,12 +12,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/crowdsec/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
|
||||
@@ -16,12 +16,6 @@ vi.mock("../../components/widgets/queue/queueEntry", () => ({
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/deluge/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/diskstation/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/dispatcharr/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/dockhand/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/downloadstation/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/esphome/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/evcc/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/filebrowser/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/fileflows/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/flood/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/freshrss/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/frigate/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component, { fritzboxDefaultFields } from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/fritzbox/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/gamedig/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/gatus/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/ghostfolio/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/gitea/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/gitlab/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -8,7 +8,9 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
const statusMap = {
|
||||
running: <ResolvedIcon icon="mdi-circle" width={32} height={32} />,
|
||||
healthy: <ResolvedIcon icon="mdi-circle" width={32} height={32} />,
|
||||
paused: <ResolvedIcon icon="mdi-circle-outline" width={32} height={32} />,
|
||||
stopped: <ResolvedIcon icon="mdi-circle-double" width={32} height={32} />,
|
||||
};
|
||||
|
||||
const defaultInterval = 1000;
|
||||
|
||||
@@ -11,6 +11,15 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
// Avoid pulling Next/Image + ThemeContext requirements into these unit tests.
|
||||
vi.mock("components/resolvedicon", () => ({ default: () => <span data-testid="resolvedicon" /> }));
|
||||
|
||||
vi.mock("next-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key, opts) => (key === "common.bytes" ? `${key}:${opts?.value}` : key),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Avoid pulling Next/Image + ThemeContext requirements into these unit tests.
|
||||
vi.mock("components/resolvedicon", () => ({ default: () => <span data-testid="resolvedicon" /> }));
|
||||
|
||||
import Component from "./containers";
|
||||
|
||||
describe("widgets/glances/metrics/containers", () => {
|
||||
@@ -21,4 +30,78 @@ describe("widgets/glances/metrics/containers", () => {
|
||||
});
|
||||
expect(screen.getByText("-")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a placeholder while loading", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
|
||||
renderWithProviders(<Component service={{ widget: { chart: false, version: 3 } }} />, {
|
||||
settings: { hideErrors: false },
|
||||
});
|
||||
expect(screen.getByText("-")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders nothing when there is an error", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: undefined, error: new Error("fail") });
|
||||
renderWithProviders(<Component service={{ widget: { chart: false, version: 3 } }} />, {
|
||||
settings: { hideErrors: false },
|
||||
});
|
||||
expect(screen.queryByText("resources.cpu")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("-")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders container rows using v3 keys and formats values", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: [
|
||||
{
|
||||
Id: "one",
|
||||
Status: "running",
|
||||
name: "alpha",
|
||||
cpu_percent: 12.34,
|
||||
memory: { usage: 1000, inactive_file: 400 },
|
||||
},
|
||||
{
|
||||
Id: "two",
|
||||
Status: "paused",
|
||||
name: "beta",
|
||||
cpu_percent: 99.99,
|
||||
memory: { usage: 2000, inactive_file: 1000 },
|
||||
},
|
||||
],
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { chart: false, version: 3 } }} />, {
|
||||
settings: { hideErrors: false },
|
||||
});
|
||||
|
||||
// data.splice(1) keeps only one item when chart is false
|
||||
expect(screen.getByText("resources.cpu")).toBeInTheDocument();
|
||||
expect(screen.getByText("resources.mem")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("alpha")).toBeInTheDocument();
|
||||
expect(screen.queryByText("beta")).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("12.3%")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.bytes:600")).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId("resolvedicon")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("limits rows to 5 when chart is enabled", () => {
|
||||
const data = Array.from({ length: 6 }).map((_, index) => ({
|
||||
Id: `id-${index}`,
|
||||
Status: "healthy",
|
||||
name: `item-${index}`,
|
||||
cpu_percent: index + 0.1,
|
||||
memory: { usage: 100 * (index + 1), inactive_file: 0 },
|
||||
}));
|
||||
|
||||
useWidgetAPI.mockReturnValue({ data, error: undefined });
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { chart: true, version: 3 } }} />, {
|
||||
settings: { hideErrors: false },
|
||||
});
|
||||
|
||||
expect(screen.getByText("item-0")).toBeInTheDocument();
|
||||
expect(screen.getByText("item-4")).toBeInTheDocument();
|
||||
expect(screen.queryByText("item-5")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function Component({ service }) {
|
||||
let listYPosition = "bottom-4";
|
||||
if (chart) {
|
||||
headerYPosition = "-top-6";
|
||||
listYPosition = "-top-3";
|
||||
listYPosition = "-top-2";
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/gluetun/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/gotify/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/grafana/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/hdhomerun/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/headscale/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/healthchecks/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component, { homeboxDefaultFields } from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/homebox/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/immich/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/jackett/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/jdownloader/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import Block from "components/services/widget/block";
|
||||
import Container from "components/services/widget/container";
|
||||
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export const jellyseerrDefaultFields = ["pending", "approved", "available"];
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
|
||||
widget.fields = widget?.fields?.length ? widget.fields : jellyseerrDefaultFields;
|
||||
const isIssueEnabled = widget.fields.includes("issues");
|
||||
|
||||
const { data: statsData, error: statsError } = useWidgetAPI(widget, "request/count");
|
||||
const { data: issueData, error: issueError } = useWidgetAPI(widget, isIssueEnabled ? "issue/count" : "");
|
||||
if (statsError || (isIssueEnabled && issueError)) {
|
||||
return <Container service={service} error={statsError ? statsError : issueError} />;
|
||||
}
|
||||
|
||||
if (!statsData || (isIssueEnabled && !issueData)) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="jellyseerr.pending" />
|
||||
<Block label="jellyseerr.approved" />
|
||||
<Block label="jellyseerr.available" />
|
||||
<Block label="jellyseerr.issues" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="jellyseerr.pending" value={statsData.pending} />
|
||||
<Block label="jellyseerr.approved" value={statsData.approved} />
|
||||
<Block label="jellyseerr.available" value={statsData.available} />
|
||||
<Block label="jellyseerr.issues" value={`${issueData?.open} / ${issueData?.total}`} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component, { jellyseerrDefaultFields } from "./component";
|
||||
|
||||
describe("widgets/jellyseerr/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("defaults fields and filters to 3 blocks while loading when issues are not enabled", () => {
|
||||
useWidgetAPI
|
||||
.mockReturnValueOnce({ data: undefined, error: undefined }) // request/count
|
||||
.mockReturnValueOnce({ data: undefined, error: undefined }); // issue/count disabled (endpoint = "")
|
||||
|
||||
const service = { widget: { type: "jellyseerr", url: "http://x" } };
|
||||
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
|
||||
|
||||
expect(service.widget.fields).toEqual(jellyseerrDefaultFields);
|
||||
expect(useWidgetAPI.mock.calls[1][1]).toBe("");
|
||||
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
|
||||
expect(screen.getByText("jellyseerr.pending")).toBeInTheDocument();
|
||||
expect(screen.getByText("jellyseerr.approved")).toBeInTheDocument();
|
||||
expect(screen.getByText("jellyseerr.available")).toBeInTheDocument();
|
||||
expect(screen.queryByText("jellyseerr.issues")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders issues when enabled (and calls the issue/count endpoint)", () => {
|
||||
useWidgetAPI
|
||||
.mockReturnValueOnce({ data: { pending: 1, approved: 2, available: 3 }, error: undefined })
|
||||
.mockReturnValueOnce({ data: { open: 1, total: 2 }, error: undefined });
|
||||
|
||||
const service = {
|
||||
widget: { type: "jellyseerr", url: "http://x", fields: ["pending", "approved", "available", "issues"] },
|
||||
};
|
||||
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
|
||||
|
||||
expect(useWidgetAPI.mock.calls[1][1]).toBe("issue/count");
|
||||
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||
expect(screen.getByText("1 / 2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders error UI when issues are enabled and issue/count errors", () => {
|
||||
useWidgetAPI
|
||||
.mockReturnValueOnce({ data: { pending: 0, approved: 0, available: 0 }, error: undefined })
|
||||
.mockReturnValueOnce({ data: undefined, error: { message: "nope" } });
|
||||
|
||||
renderWithProviders(
|
||||
<Component service={{ widget: { type: "jellyseerr", url: "http://x", fields: ["issues"] } }} />,
|
||||
{ settings: { hideErrors: false } },
|
||||
);
|
||||
|
||||
expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("nope")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component, { karakeepDefaultFields } from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/karakeep/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/komga/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/kopia/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/lidarr/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/linkwarden/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/lubelogger/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/mailcow/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/mastodon/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/mealie/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/medusa/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/mikrotik/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/minecraft/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/miniflux/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/moonraker/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/mylar/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/myspeed/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/netalertx/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/netdata/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/npm/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/nzbget/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,19 +4,13 @@ import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
import { expectBlockValue } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/octoprint/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -6,6 +6,40 @@ const proxyName = "omadaProxyHandler";
|
||||
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
function parseOmadaJson(data, { step, status, contentType, url }) {
|
||||
const body = Buffer.isBuffer(data) ? data.toString() : String(data ?? "");
|
||||
|
||||
try {
|
||||
return JSON.parse(body);
|
||||
} catch (error) {
|
||||
logger.debug(
|
||||
"Failed parsing Omada %s response as JSON (HTTP %d, content-type: %s, url: %s). Body: %s",
|
||||
step,
|
||||
status,
|
||||
contentType ?? "unknown",
|
||||
url,
|
||||
body,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function isLikelyHtmlResponse(contentType, data) {
|
||||
const body = Buffer.isBuffer(data) ? data.toString() : String(data ?? "");
|
||||
return contentType?.includes("text/html") || body.startsWith("<!DOCTYPE") || body.startsWith("<html");
|
||||
}
|
||||
|
||||
function extractCookieHeader(responseHeaders) {
|
||||
const setCookieHeader = responseHeaders?.["set-cookie"];
|
||||
if (!setCookieHeader) return undefined;
|
||||
|
||||
if (Array.isArray(setCookieHeader)) {
|
||||
return setCookieHeader.map((cookie) => cookie.split(";")[0]).join("; ");
|
||||
}
|
||||
|
||||
return String(setCookieHeader).split(";")[0];
|
||||
}
|
||||
|
||||
async function login(loginUrl, username, password, controllerVersionMajor) {
|
||||
const params = {
|
||||
username,
|
||||
@@ -20,15 +54,17 @@ async function login(loginUrl, username, password, controllerVersionMajor) {
|
||||
};
|
||||
}
|
||||
|
||||
const [status, contentType, data] = await httpProxy(loginUrl, {
|
||||
const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, {
|
||||
method: "POST",
|
||||
cookieHeader: "X-Bypass-Cookie",
|
||||
body: JSON.stringify(params),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
return [status, JSON.parse(data.toString())];
|
||||
return [status, contentType, data, extractCookieHeader(responseHeaders)];
|
||||
}
|
||||
|
||||
export default async function omadaProxyHandler(req, res) {
|
||||
@@ -86,25 +122,33 @@ export default async function omadaProxyHandler(req, res) {
|
||||
break;
|
||||
}
|
||||
|
||||
const [loginStatus, loginResponseData] = await login(
|
||||
const [loginStatus, loginContentType, loginData, loginCookieHeader] = await login(
|
||||
loginUrl,
|
||||
widget.username,
|
||||
widget.password,
|
||||
controllerVersionMajor,
|
||||
);
|
||||
const loginResponseData = parseOmadaJson(loginData, {
|
||||
step: "login",
|
||||
status: loginStatus,
|
||||
contentType: loginContentType,
|
||||
url: loginUrl,
|
||||
});
|
||||
|
||||
if (loginStatus !== 200 || loginResponseData.errorCode > 0) {
|
||||
return res
|
||||
.status(status)
|
||||
.json({ error: { message: "Error logging in to Oamda controller", url: loginUrl, data: loginResponseData } });
|
||||
.status(loginStatus)
|
||||
.json({ error: { message: "Error logging in to Omada controller", url: loginUrl, data: loginResponseData } });
|
||||
}
|
||||
|
||||
const { token } = loginResponseData.result;
|
||||
let omadaCookieHeader = loginCookieHeader;
|
||||
|
||||
let sitesUrl;
|
||||
let body = {};
|
||||
let params = { token };
|
||||
let headers = { "Csrf-Token": token };
|
||||
if (omadaCookieHeader) headers.Cookie = omadaCookieHeader;
|
||||
let method = "GET";
|
||||
|
||||
switch (controllerVersionMajor) {
|
||||
@@ -134,9 +178,72 @@ export default async function omadaProxyHandler(req, res) {
|
||||
params,
|
||||
body: JSON.stringify(body),
|
||||
headers,
|
||||
cookieHeader: "X-Bypass-Cookie",
|
||||
});
|
||||
|
||||
const sitesResponseData = JSON.parse(data);
|
||||
let sitesResponseData;
|
||||
try {
|
||||
sitesResponseData = parseOmadaJson(data, {
|
||||
step: "sites list",
|
||||
status,
|
||||
contentType,
|
||||
url: sitesUrl,
|
||||
});
|
||||
} catch (parseError) {
|
||||
if (!isLikelyHtmlResponse(contentType, data)) {
|
||||
throw parseError;
|
||||
}
|
||||
|
||||
logger.debug("Received HTML response for Omada sites list; retrying with a fresh login.");
|
||||
|
||||
const [retryLoginStatus, retryLoginContentType, retryLoginData, retryLoginCookieHeader] = await login(
|
||||
loginUrl,
|
||||
widget.username,
|
||||
widget.password,
|
||||
controllerVersionMajor,
|
||||
);
|
||||
const retryLoginResponseData = parseOmadaJson(retryLoginData, {
|
||||
step: "login (retry)",
|
||||
status: retryLoginStatus,
|
||||
contentType: retryLoginContentType,
|
||||
url: loginUrl,
|
||||
});
|
||||
|
||||
if (retryLoginStatus !== 200 || retryLoginResponseData.errorCode > 0) {
|
||||
return res.status(retryLoginStatus).json({
|
||||
error: {
|
||||
message: "Error re-authenticating to Omada controller",
|
||||
url: loginUrl,
|
||||
data: retryLoginResponseData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const retryToken = retryLoginResponseData.result?.token;
|
||||
omadaCookieHeader = retryLoginCookieHeader;
|
||||
const retrySitesUrlObj = new URL(sitesUrl);
|
||||
retrySitesUrlObj.searchParams.set("token", retryToken);
|
||||
const retrySitesUrl = retrySitesUrlObj.toString();
|
||||
|
||||
[status, contentType, data] = await httpProxy(retrySitesUrl, {
|
||||
method,
|
||||
params: { token: retryToken },
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
...headers,
|
||||
"Csrf-Token": retryToken,
|
||||
...(omadaCookieHeader ? { Cookie: omadaCookieHeader } : {}),
|
||||
},
|
||||
cookieHeader: "X-Bypass-Cookie",
|
||||
});
|
||||
|
||||
sitesResponseData = parseOmadaJson(data, {
|
||||
step: "sites list (retry)",
|
||||
status,
|
||||
contentType,
|
||||
url: retrySitesUrl,
|
||||
});
|
||||
}
|
||||
|
||||
if (status !== 200 || sitesResponseData.errorCode > 0) {
|
||||
logger.debug(`HTTP ${status} getting sites list: ${sitesResponseData.msg}`);
|
||||
@@ -174,6 +281,7 @@ export default async function omadaProxyHandler(req, res) {
|
||||
},
|
||||
};
|
||||
headers = { "Content-Type": "application/json" };
|
||||
if (omadaCookieHeader) headers.Cookie = omadaCookieHeader;
|
||||
params = { token };
|
||||
|
||||
[status, contentType, data] = await httpProxy(switchUrl, {
|
||||
@@ -181,9 +289,15 @@ export default async function omadaProxyHandler(req, res) {
|
||||
params,
|
||||
body: JSON.stringify(body),
|
||||
headers,
|
||||
cookieHeader: "X-Bypass-Cookie",
|
||||
});
|
||||
|
||||
const switchResponseData = JSON.parse(data);
|
||||
const switchResponseData = parseOmadaJson(data, {
|
||||
step: "switch site",
|
||||
status,
|
||||
contentType,
|
||||
url: switchUrl,
|
||||
});
|
||||
if (status !== 200 || switchResponseData.errorCode > 0) {
|
||||
logger.error(`HTTP ${status} getting sites list: ${data}`);
|
||||
return res.status(status).json({ error: { message: "Error switching site", url: switchUrl, data } });
|
||||
@@ -197,9 +311,15 @@ export default async function omadaProxyHandler(req, res) {
|
||||
method: "getGlobalStat",
|
||||
}),
|
||||
headers,
|
||||
cookieHeader: "X-Bypass-Cookie",
|
||||
});
|
||||
|
||||
siteResponseData = JSON.parse(data);
|
||||
siteResponseData = parseOmadaJson(data, {
|
||||
step: "global stats",
|
||||
status,
|
||||
contentType,
|
||||
url: statsUrl,
|
||||
});
|
||||
|
||||
if (status !== 200 || siteResponseData.errorCode > 0) {
|
||||
return res.status(status).json({ error: { message: "Error getting stats", url: statsUrl, data } });
|
||||
@@ -218,14 +338,27 @@ export default async function omadaProxyHandler(req, res) {
|
||||
[status, contentType, data] = await httpProxy(siteStatsUrl, {
|
||||
headers: {
|
||||
"Csrf-Token": token,
|
||||
...(omadaCookieHeader ? { Cookie: omadaCookieHeader } : {}),
|
||||
},
|
||||
cookieHeader: "X-Bypass-Cookie",
|
||||
});
|
||||
|
||||
siteResponseData = JSON.parse(data);
|
||||
siteResponseData = parseOmadaJson(data, {
|
||||
step: "overview stats",
|
||||
status,
|
||||
contentType,
|
||||
url: siteStatsUrl,
|
||||
});
|
||||
|
||||
if (status !== 200 || siteResponseData.errorCode > 0) {
|
||||
logger.debug(`HTTP ${status} getting stats for site ${widget.site} with message ${siteResponseData.msg}`);
|
||||
return res.status(500).send(data);
|
||||
return res.status(status === 200 ? 500 : status).json({
|
||||
error: {
|
||||
message: "Error getting stats",
|
||||
url: siteStatsUrl,
|
||||
data: siteResponseData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const alertUrl =
|
||||
@@ -236,9 +369,16 @@ export default async function omadaProxyHandler(req, res) {
|
||||
[status, contentType, data] = await httpProxy(alertUrl, {
|
||||
headers: {
|
||||
"Csrf-Token": token,
|
||||
...(omadaCookieHeader ? { Cookie: omadaCookieHeader } : {}),
|
||||
},
|
||||
cookieHeader: "X-Bypass-Cookie",
|
||||
});
|
||||
const alertResponseData = parseOmadaJson(data, {
|
||||
step: "alerts",
|
||||
status,
|
||||
contentType,
|
||||
url: alertUrl,
|
||||
});
|
||||
const alertResponseData = JSON.parse(data);
|
||||
|
||||
activeUser = siteResponseData.result.totalClientNum;
|
||||
connectedAp = siteResponseData.result.connectedApNum;
|
||||
|
||||
@@ -151,7 +151,7 @@ describe("widgets/omada/proxy", () => {
|
||||
await omadaProxyHandler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body.error.message).toBe("Error logging in to Oamda controller");
|
||||
expect(res.body.error.message).toBe("Error logging in to Omada controller");
|
||||
expect(res.body.error.url).toBe("http://omada/api/v2/login");
|
||||
expect(res.body.error.data).toEqual({ errorCode: 1, msg: "nope" });
|
||||
});
|
||||
@@ -288,7 +288,7 @@ describe("widgets/omada/proxy", () => {
|
||||
expect(res.body.error.message).toBe("Error switching site");
|
||||
});
|
||||
|
||||
it("returns 500 with the raw payload when overview stats retrieval fails (v5)", async () => {
|
||||
it("returns a structured error when overview stats retrieval fails (v5)", async () => {
|
||||
getServiceWidget.mockResolvedValue({ url: "http://omada", username: "u", password: "p", site: "Default" });
|
||||
|
||||
httpProxy
|
||||
@@ -316,6 +316,81 @@ describe("widgets/omada/proxy", () => {
|
||||
await omadaProxyHandler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(500);
|
||||
expect(res.body).toBe(JSON.stringify({ errorCode: 1, msg: "bad" }));
|
||||
expect(res.body).toEqual({
|
||||
error: {
|
||||
message: "Error getting stats",
|
||||
url: "http://omada/cid/api/v2/sites/siteid/dashboard/overviewDiagram?token=t¤tPage=1¤tPageSize=1000",
|
||||
data: { errorCode: 1, msg: "bad" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("retries login when sites list returns HTML", async () => {
|
||||
getServiceWidget.mockResolvedValue({ url: "http://omada", username: "u", password: "p", site: "Default" });
|
||||
|
||||
httpProxy
|
||||
.mockResolvedValueOnce([
|
||||
200,
|
||||
"application/json",
|
||||
JSON.stringify({ result: { omadacId: "cid", controllerVer: "5.0.0" } }),
|
||||
])
|
||||
// initial login
|
||||
.mockResolvedValueOnce([
|
||||
200,
|
||||
"application/json",
|
||||
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t1" } })),
|
||||
])
|
||||
// sites list unexpectedly returns HTML
|
||||
.mockResolvedValueOnce([200, "text/html;charset=utf-8", "<!DOCTYPE html><html><body>login</body></html>"])
|
||||
// retry login
|
||||
.mockResolvedValueOnce([
|
||||
200,
|
||||
"application/json",
|
||||
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t2" } })),
|
||||
])
|
||||
// retry sites list works
|
||||
.mockResolvedValueOnce([
|
||||
200,
|
||||
"application/json",
|
||||
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", id: "siteid" }] } }),
|
||||
])
|
||||
// overview works
|
||||
.mockResolvedValueOnce([
|
||||
200,
|
||||
"application/json",
|
||||
JSON.stringify({
|
||||
errorCode: 0,
|
||||
result: {
|
||||
totalClientNum: 11,
|
||||
connectedApNum: 3,
|
||||
connectedGatewayNum: 1,
|
||||
connectedSwitchNum: 2,
|
||||
},
|
||||
}),
|
||||
])
|
||||
// alerts works
|
||||
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 5 } })]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await omadaProxyHandler(req, res);
|
||||
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
"Received HTML response for Omada sites list; retrying with a fresh login.",
|
||||
);
|
||||
expect(httpProxy.mock.calls[1][1].cookieHeader).toBe("X-Bypass-Cookie");
|
||||
expect(httpProxy.mock.calls[2][1].cookieHeader).toBe("X-Bypass-Cookie");
|
||||
expect(httpProxy.mock.calls[3][1].cookieHeader).toBe("X-Bypass-Cookie");
|
||||
expect(httpProxy.mock.calls[4][1].cookieHeader).toBe("X-Bypass-Cookie");
|
||||
expect(res.body).toBe(
|
||||
JSON.stringify({
|
||||
connectedAp: 3,
|
||||
activeUser: 11,
|
||||
alerts: 5,
|
||||
connectedGateways: 1,
|
||||
connectedSwitches: 2,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user