feat: add simple app ping (#580)

* feat: add simple ping

* refactor: make ping run on server and show errors

* fix: format issues

* fix: missing translation for enabled ping option for app

* refactor: remove ping queue as no longer needed

* chore: address pull request feedback

* test: add some unit tests

* fix: format issues

* fix: deepsource issues
This commit is contained in:
Meier Lukas
2024-06-08 17:33:16 +02:00
committed by GitHub
parent 3dca787fa7
commit d7ecdf5567
25 changed files with 542 additions and 22 deletions

View File

@@ -25,6 +25,7 @@
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/integrations": "workspace:^0.1.0",
"@homarr/log": "workspace:^",
"@homarr/ping": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0",
"@homarr/tasks": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",

View File

@@ -0,0 +1,51 @@
import { describe, expect, test, vi } from "vitest";
import type { Session } from "@homarr/auth";
import { createDb } from "@homarr/db/test";
import * as ping from "@homarr/ping";
import { appRouter } from "../../widgets/app";
// Mock the auth module to return an empty session
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
vi.mock("@homarr/ping", () => ({ sendPingRequestAsync: async () => await Promise.resolve(null) }));
describe("ping should call sendPingRequestAsync with url and return result", () => {
test("ping with error response should return error and url", async () => {
// Arrange
const spy = vi.spyOn(ping, "sendPingRequestAsync");
const url = "http://localhost";
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
});
spy.mockImplementation(() => Promise.resolve({ error: "error" }));
// Act
const result = await caller.ping({ url });
// Assert
expect(result.url).toBe(url);
expect("error" in result).toBe(true);
});
test("ping with success response should return statusCode and url", async () => {
// Arrange
const spy = vi.spyOn(ping, "sendPingRequestAsync");
const url = "http://localhost";
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
});
spy.mockImplementation(() => Promise.resolve({ statusCode: 200 }));
// Act
const result = await caller.ping({ url });
// Assert
expect(result.url).toBe(url);
expect("statusCode" in result).toBe(true);
});
});

View File

@@ -0,0 +1,42 @@
import { observable } from "@trpc/server/observable";
import { sendPingRequestAsync } from "@homarr/ping";
import { pingChannel, pingUrlChannel } from "@homarr/redis";
import { z } from "@homarr/validation";
import { createTRPCRouter, publicProcedure } from "../../trpc";
export const appRouter = createTRPCRouter({
ping: publicProcedure.input(z.object({ url: z.string() })).query(async ({ input }) => {
const pingResult = await sendPingRequestAsync(input.url);
return {
url: input.url,
...pingResult,
};
}),
updatedPing: publicProcedure
.input(
z.object({
url: z.string(),
}),
)
.subscription(async ({ input }) => {
await pingUrlChannel.addAsync(input.url);
const pingResult = await sendPingRequestAsync(input.url);
return observable<{ url: string; statusCode: number } | { url: string; error: string }>((emit) => {
emit.next({ url: input.url, ...pingResult });
pingChannel.subscribe((message) => {
// Only emit if same url
if (message.url !== input.url) return;
emit.next(message);
});
return () => {
void pingUrlChannel.removeAsync(input.url);
};
});
}),
});

View File

@@ -1,4 +1,5 @@
import { createTRPCRouter } from "../../trpc";
import { appRouter } from "./app";
import { dnsHoleRouter } from "./dns-hole";
import { notebookRouter } from "./notebook";
import { weatherRouter } from "./weather";
@@ -6,5 +7,6 @@ import { weatherRouter } from "./weather";
export const widgetRouter = createTRPCRouter({
notebook: notebookRouter,
weather: weatherRouter,
app: appRouter,
dnsHole: dnsHoleRouter,
});