mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat(plugins): publish host.created when a host is added (#1668)
Plugins that keep an inventory of hosts had no way to hear about a new
one short of polling. Core now publishes host.created with the same
{ hostId, userId } payload as host.updated wherever a host row is
created: the host create route, ctx.hosts.create, JSON and SSH config
bulk import, database file import, and sync writing a host it did not
have yet.
This commit is contained in:
1 parent
893e7e5a4b
commit
16f2f509f2
12 files changed
+185
-6
No files matched your search
@@ -38,6 +38,7 @@ import { attachPluginWebSockets } from "../plugins/ws.js";
|
|||||||
import pluginRoutes from "./routes/plugins.js";
|
import pluginRoutes from "./routes/plugins.js";
|
||||||
import { createPluginAssetsRouter } from "../plugins/assets.js";
|
import { createPluginAssetsRouter } from "../plugins/assets.js";
|
||||||
import { getPluginRuntime } from "../plugins/index.js";
|
import { getPluginRuntime } from "../plugins/index.js";
|
||||||
|
import { pluginEvents, TOPICS } from "../plugins/events.js";
|
||||||
import { createCorsMiddleware } from "../utils/cors-config.js";
|
import { createCorsMiddleware } from "../utils/cors-config.js";
|
||||||
import { createCompressionMiddleware } from "../utils/compression-config.js";
|
import { createCompressionMiddleware } from "../utils/compression-config.js";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@@ -1388,6 +1389,9 @@ app.post(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await applyDefaultsAfterHostWrites(importedHostIds);
|
await applyDefaultsAfterHostWrites(importedHostIds);
|
||||||
|
for (const hostId of importedHostIds) {
|
||||||
|
pluginEvents.emit(TOPICS.hostCreated, { hostId, userId });
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
apiLogger.info("ssh_data table not found in import file, skipping");
|
apiLogger.info("ssh_data table not found in import file, skipping");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,14 @@ export function parseSSHConfig(content: string): SSHConfigHost[] {
|
|||||||
* Tells status checks and other pollers about hosts an import wrote, the way
|
* Tells status checks and other pollers about hosts an import wrote, the way
|
||||||
* a single host create or update does.
|
* a single host create or update does.
|
||||||
*/
|
*/
|
||||||
export function notifyImportedHosts(userId: string, hostIds: number[]): void {
|
export function notifyImportedHosts(
|
||||||
|
userId: string,
|
||||||
|
hostIds: number[],
|
||||||
|
createdHostIds: number[],
|
||||||
|
): void {
|
||||||
|
for (const hostId of createdHostIds) {
|
||||||
|
pluginEvents.emit(TOPICS.hostCreated, { hostId, userId });
|
||||||
|
}
|
||||||
for (const hostId of hostIds) {
|
for (const hostId of hostIds) {
|
||||||
pluginEvents.emit(TOPICS.hostUpdated, { hostId, userId });
|
pluginEvents.emit(TOPICS.hostUpdated, { hostId, userId });
|
||||||
}
|
}
|
||||||
@@ -588,6 +595,7 @@ export function registerHostBulkRoutes(
|
|||||||
|
|
||||||
const knownAuthTypes = listKnownAuthTypes();
|
const knownAuthTypes = listKnownAuthTypes();
|
||||||
const writtenHostIds: number[] = [];
|
const writtenHostIds: number[] = [];
|
||||||
|
const createdHostIds: number[] = [];
|
||||||
for (const { host: hostData, index: i, exportId } of orderedHosts) {
|
for (const { host: hostData, index: i, exportId } of orderedHosts) {
|
||||||
try {
|
try {
|
||||||
const effectiveConnectionType = hostData.connectionType || "ssh";
|
const effectiveConnectionType = hostData.connectionType || "ssh";
|
||||||
@@ -797,6 +805,7 @@ export function registerHostBulkRoutes(
|
|||||||
);
|
);
|
||||||
if (exportId !== undefined) importedIds.set(exportId, saved.id);
|
if (exportId !== undefined) importedIds.set(exportId, saved.id);
|
||||||
savedHostId = saved.id;
|
savedHostId = saved.id;
|
||||||
|
createdHostIds.push(saved.id);
|
||||||
results.success++;
|
results.success++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,7 +837,7 @@ export function registerHostBulkRoutes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await applyDefaultsAfterHostWrites(writtenHostIds);
|
await applyDefaultsAfterHostWrites(writtenHostIds);
|
||||||
notifyImportedHosts(userId, writtenHostIds);
|
notifyImportedHosts(userId, writtenHostIds, createdHostIds);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `Import completed: ${results.success} created, ${results.updated} updated, ${results.failed} failed`,
|
message: `Import completed: ${results.success} created, ${results.updated} updated, ${results.failed} failed`,
|
||||||
@@ -948,6 +957,7 @@ export function registerHostBulkRoutes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const writtenHostIds: number[] = [];
|
const writtenHostIds: number[] = [];
|
||||||
|
const createdHostIds: number[] = [];
|
||||||
for (let i = 0; i < hostsToImport.length; i++) {
|
for (let i = 0; i < hostsToImport.length; i++) {
|
||||||
const hostData = normalizeImportedHost(
|
const hostData = normalizeImportedHost(
|
||||||
hostsToImport[i] as Record<string, unknown>,
|
hostsToImport[i] as Record<string, unknown>,
|
||||||
@@ -1023,6 +1033,7 @@ export function registerHostBulkRoutes(
|
|||||||
sshDataObj,
|
sshDataObj,
|
||||||
);
|
);
|
||||||
writtenHostIds.push(saved.id);
|
writtenHostIds.push(saved.id);
|
||||||
|
createdHostIds.push(saved.id);
|
||||||
results.success++;
|
results.success++;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1034,7 +1045,7 @@ export function registerHostBulkRoutes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await applyDefaultsAfterHostWrites(writtenHostIds);
|
await applyDefaultsAfterHostWrites(writtenHostIds);
|
||||||
notifyImportedHosts(userId, writtenHostIds);
|
notifyImportedHosts(userId, writtenHostIds, createdHostIds);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `Import completed: ${results.success} created, ${results.updated} updated, ${results.failed} failed`,
|
message: `Import completed: ${results.success} created, ${results.updated} updated, ${results.failed} failed`,
|
||||||
|
|||||||
@@ -486,6 +486,7 @@ router.post(
|
|||||||
});
|
});
|
||||||
|
|
||||||
res.json(await withProtocolAuth(stripSensitiveFields(resolvedHost)));
|
res.json(await withProtocolAuth(stripSensitiveFields(resolvedHost)));
|
||||||
|
pluginEvents.emit(TOPICS.hostCreated, { hostId: createdHost.id, userId });
|
||||||
notifyStatsHostUpdated(createdHost.id as number, userId, "host_create");
|
notifyStatsHostUpdated(createdHost.id as number, userId, "host_create");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sshLogger.error("Failed to save SSH host to database", err, {
|
sshLogger.error("Failed to save SSH host to database", err, {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { hostStatusService } from "../hosts/status/host-status-service.js";
|
|||||||
import { parseSshOptions } from "../hosts/ssh-options.js";
|
import { parseSshOptions } from "../hosts/ssh-options.js";
|
||||||
import { hostTerminalExport } from "../database/routes/host-normalizers.js";
|
import { hostTerminalExport } from "../database/routes/host-normalizers.js";
|
||||||
import type { DisposableBag } from "./disposables.js";
|
import type { DisposableBag } from "./disposables.js";
|
||||||
|
import { pluginEvents, TOPICS } from "./events.js";
|
||||||
import { pluginLogger } from "../utils/logger.js";
|
import { pluginLogger } from "../utils/logger.js";
|
||||||
|
|
||||||
type AuditFn = (
|
type AuditFn = (
|
||||||
@@ -502,6 +503,7 @@ export function createPluginHosts({ manifest, bag, audit }: Deps): PluginHosts {
|
|||||||
await writeSettings(created.id, writes);
|
await writeSettings(created.id, writes);
|
||||||
await markSettingsOwn(created.id, writes);
|
await markSettingsOwn(created.id, writes);
|
||||||
await audit("hosts_create", `host ${created.id}`, { success: true });
|
await audit("hosts_create", `host ${created.id}`, { success: true });
|
||||||
|
pluginEvents.emit(TOPICS.hostCreated, { hostId: created.id, userId });
|
||||||
return (
|
return (
|
||||||
await toRecords([created as unknown as Record<string, unknown>])
|
await toRecords([created as unknown as Record<string, unknown>])
|
||||||
)[0];
|
)[0];
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export const TOPICS = {
|
|||||||
hostStatus: "host.status",
|
hostStatus: "host.status",
|
||||||
/** A host's key was accepted again after it changed. */
|
/** A host's key was accepted again after it changed. */
|
||||||
hostKeyUpdated: "host.key.updated",
|
hostKeyUpdated: "host.key.updated",
|
||||||
|
/** A host was added and anything inventorying hosts should pick it up. */
|
||||||
|
hostCreated: "host.created",
|
||||||
/** A host's connection details changed and pollers should re-read them. */
|
/** A host's connection details changed and pollers should re-read them. */
|
||||||
hostUpdated: "host.updated",
|
hostUpdated: "host.updated",
|
||||||
/** A host was deleted and any poller holding it should drop it. */
|
/** A host was deleted and any poller holding it should drop it. */
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
import { FieldCrypto } from "../utils/field-crypto.js";
|
import { FieldCrypto } from "../utils/field-crypto.js";
|
||||||
import { syncLogger } from "../utils/logger.js";
|
import { syncLogger } from "../utils/logger.js";
|
||||||
import { getPluginRuntime } from "../plugins/index.js";
|
import { getPluginRuntime } from "../plugins/index.js";
|
||||||
|
import { pluginEvents, TOPICS } from "../plugins/events.js";
|
||||||
import { singletonSyncId } from "./wire.js";
|
import { singletonSyncId } from "./wire.js";
|
||||||
import { parseDefaultOverrides } from "../../types/host-defaults.js";
|
import { parseDefaultOverrides } from "../../types/host-defaults.js";
|
||||||
import {
|
import {
|
||||||
@@ -174,7 +175,7 @@ export function registerCoreSyncEntities(): void {
|
|||||||
)
|
)
|
||||||
: {},
|
: {},
|
||||||
}),
|
}),
|
||||||
afterWrite: async ({ id, userId, wire, resolveId }) => {
|
afterWrite: async ({ id, userId, wire, created, resolveId }) => {
|
||||||
if (id === null) return;
|
if (id === null) return;
|
||||||
// The server owns host defaults: it keeps what the host set itself
|
// The server owns host defaults: it keeps what the host set itself
|
||||||
// and resolves the rest. A desktop takes the server's values as sent.
|
// and resolves the rest. A desktop takes the server's values as sent.
|
||||||
@@ -195,6 +196,9 @@ export function registerCoreSyncEntities(): void {
|
|||||||
const { SharedHostSecretsManager } =
|
const { SharedHostSecretsManager } =
|
||||||
await import("../utils/shared-host-secrets-manager.js");
|
await import("../utils/shared-host-secrets-manager.js");
|
||||||
await SharedHostSecretsManager.getInstance().resyncHost(id);
|
await SharedHostSecretsManager.getInstance().resyncHost(id);
|
||||||
|
if (created) {
|
||||||
|
pluginEvents.emit(TOPICS.hostCreated, { hostId: id, userId });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
remove: async (row, userId) => {
|
remove: async (row, userId) => {
|
||||||
const { deleteOwnedHost } = await import("../hosts/delete-host.js");
|
const { deleteOwnedHost } = await import("../hosts/delete-host.js");
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ function exportFile(): string {
|
|||||||
|
|
||||||
async function invoke(path: string, user: unknown = { isExternal: false }) {
|
async function invoke(path: string, user: unknown = { isExternal: false }) {
|
||||||
const writes: string[] = [];
|
const writes: string[] = [];
|
||||||
|
const events: Array<[string, unknown]> = [];
|
||||||
const hosts: Array<Record<string, unknown>> = [];
|
const hosts: Array<Record<string, unknown>> = [];
|
||||||
const importHostProtocolLogins = vi.fn();
|
const importHostProtocolLogins = vi.fn();
|
||||||
const handler = runInNewContext(
|
const handler = runInNewContext(
|
||||||
@@ -98,6 +99,11 @@ async function invoke(path: string, user: unknown = { isExternal: false }) {
|
|||||||
importHostDefaults: async () => ({ imported: 0, skipped: 0 }),
|
importHostDefaults: async () => ({ imported: 0, skipped: 0 }),
|
||||||
DatabaseSaveTrigger: { forceSave: async () => {} },
|
DatabaseSaveTrigger: { forceSave: async () => {} },
|
||||||
getErrorMessage: (error: Error) => error.message,
|
getErrorMessage: (error: Error) => error.message,
|
||||||
|
pluginEvents: {
|
||||||
|
emit: (topic: string, payload: unknown) =>
|
||||||
|
events.push([topic, payload]),
|
||||||
|
},
|
||||||
|
TOPICS: { hostCreated: "host.created" },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const res = { json: vi.fn(), status: vi.fn() };
|
const res = { json: vi.fn(), status: vi.fn() };
|
||||||
@@ -106,7 +112,7 @@ async function invoke(path: string, user: unknown = { isExternal: false }) {
|
|||||||
{ userId: "user-1", file: { path, originalname: "export.db" } },
|
{ userId: "user-1", file: { path, originalname: "export.db" } },
|
||||||
res,
|
res,
|
||||||
);
|
);
|
||||||
return { res, writes, hosts, importHostProtocolLogins };
|
return { res, writes, hosts, events, importHostProtocolLogins };
|
||||||
}
|
}
|
||||||
|
|
||||||
it("points imported hosts at the credentials imported with them", async () => {
|
it("points imported hosts at the credentials imported with them", async () => {
|
||||||
@@ -122,6 +128,17 @@ it("points imported hosts at the credentials imported with them", async () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("publishes host.created once for each imported host", async () => {
|
||||||
|
const { events } = await invoke(exportFile());
|
||||||
|
|
||||||
|
expect(events).toEqual(
|
||||||
|
[501, 502, 503, 504].map((hostId) => [
|
||||||
|
"host.created",
|
||||||
|
{ hostId, userId: "user-1" },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("removes the uploaded file when the user is gone", async () => {
|
it("removes the uploaded file when the user is gone", async () => {
|
||||||
const path = exportFile();
|
const path = exportFile();
|
||||||
const { res } = await invoke(path, null);
|
const { res } = await invoke(path, null);
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ describe("notifyImportedHosts", () => {
|
|||||||
seen.push(payload);
|
seen.push(payload);
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
notifyImportedHosts("user-1", [4, 7]);
|
notifyImportedHosts("user-1", [4, 7], []);
|
||||||
} finally {
|
} finally {
|
||||||
off();
|
off();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ const { hosts, sshCredentials } =
|
|||||||
const { saveLevel } = await import("../../../hosts/defaults/service.js");
|
const { saveLevel } = await import("../../../hosts/defaults/service.js");
|
||||||
const { default: hostRouter } =
|
const { default: hostRouter } =
|
||||||
await import("../../../database/routes/host.js");
|
await import("../../../database/routes/host.js");
|
||||||
|
const { pluginEvents, TOPICS } = await import("../../../plugins/events.js");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -192,3 +193,24 @@ describe("host update authType", () => {
|
|||||||
expect(res.status).toBe(400);
|
expect(res.status).toBe(400);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("host.created", () => {
|
||||||
|
it("is published once when a host is created, and not on update", async () => {
|
||||||
|
const seen: unknown[] = [];
|
||||||
|
const off = pluginEvents.on(TOPICS.hostCreated, (payload) => {
|
||||||
|
seen.push(payload);
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const created = await request(app)
|
||||||
|
.post("/host/db/host")
|
||||||
|
.send({ ...base, authType: "password", password: "pw" });
|
||||||
|
expect(created.status).toBe(200);
|
||||||
|
await request(app)
|
||||||
|
.put(`/host/db/host/${created.body.id}`)
|
||||||
|
.send({ ...base, name: "renamed" });
|
||||||
|
expect(seen).toEqual([{ hostId: created.body.id, userId: USER }]);
|
||||||
|
} finally {
|
||||||
|
off();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -37,11 +37,14 @@ vi.mock("../../../database/repositories/factory.js", () => ({
|
|||||||
}));
|
}));
|
||||||
import { registerHostBulkRoutes } from "../../../database/routes/host-bulk-routes.js";
|
import { registerHostBulkRoutes } from "../../../database/routes/host-bulk-routes.js";
|
||||||
import { buildExportPayload } from "../../../../ui/sidebar/host-export-payload";
|
import { buildExportPayload } from "../../../../ui/sidebar/host-export-payload";
|
||||||
|
import { pluginEvents, TOPICS } from "../../../plugins/events.js";
|
||||||
|
|
||||||
let handler: RequestHandler;
|
let handler: RequestHandler;
|
||||||
|
let sshConfigHandler: RequestHandler;
|
||||||
const router = {
|
const router = {
|
||||||
post: (path: string, ...handlers: RequestHandler[]) => {
|
post: (path: string, ...handlers: RequestHandler[]) => {
|
||||||
if (path === "/bulk-import") handler = handlers.at(-1)!;
|
if (path === "/bulk-import") handler = handlers.at(-1)!;
|
||||||
|
if (path === "/ssh-config-import") sshConfigHandler = handlers.at(-1)!;
|
||||||
},
|
},
|
||||||
put: () => {},
|
put: () => {},
|
||||||
patch: () => {},
|
patch: () => {},
|
||||||
@@ -202,3 +205,53 @@ it("does not save dependent hosts when importing their jump host fails", async (
|
|||||||
);
|
);
|
||||||
expect(mocks.create).toHaveBeenCalledTimes(1);
|
expect(mocks.create).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function collectCreated() {
|
||||||
|
const seen: unknown[] = [];
|
||||||
|
const off = pluginEvents.on(TOPICS.hostCreated, (payload) => {
|
||||||
|
seen.push(payload);
|
||||||
|
});
|
||||||
|
return { seen, off };
|
||||||
|
}
|
||||||
|
|
||||||
|
it("publishes host.created once for each host a JSON import creates", async () => {
|
||||||
|
mocks.list.mockResolvedValue([{ ...host, id: 19 }]);
|
||||||
|
mocks.create.mockResolvedValueOnce({ id: 501 });
|
||||||
|
const { seen, off } = collectCreated();
|
||||||
|
try {
|
||||||
|
await importHosts([host, { ...host, ip: "192.0.2.2" }], true);
|
||||||
|
} finally {
|
||||||
|
off();
|
||||||
|
}
|
||||||
|
// The overwritten host already existed, so only the new one is announced.
|
||||||
|
expect(seen).toEqual([{ hostId: 501, userId: "user-1" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("publishes host.created once for each host an SSH config import creates", async () => {
|
||||||
|
mocks.create
|
||||||
|
.mockResolvedValueOnce({ id: 601 })
|
||||||
|
.mockResolvedValueOnce({ id: 602 });
|
||||||
|
const { seen, off } = collectCreated();
|
||||||
|
try {
|
||||||
|
await sshConfigHandler(
|
||||||
|
{
|
||||||
|
userId: "user-1",
|
||||||
|
body: {
|
||||||
|
content:
|
||||||
|
"Host a\n HostName 10.0.0.1\n User root\nHost b\n HostName 10.0.0.2\n User root\n",
|
||||||
|
},
|
||||||
|
} as unknown as Request,
|
||||||
|
{
|
||||||
|
json: vi.fn(),
|
||||||
|
status: vi.fn().mockReturnThis(),
|
||||||
|
} as unknown as Response,
|
||||||
|
vi.fn(),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
off();
|
||||||
|
}
|
||||||
|
expect(seen).toEqual([
|
||||||
|
{ hostId: 601, userId: "user-1" },
|
||||||
|
{ hostId: 602, userId: "user-1" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
@@ -207,6 +207,7 @@ vi.mock("../../hosts/delete-host.js", () => ({
|
|||||||
const { createPluginHosts } = await import("../../plugins/ctx-hosts.js");
|
const { createPluginHosts } = await import("../../plugins/ctx-hosts.js");
|
||||||
const { PluginCapabilityError } =
|
const { PluginCapabilityError } =
|
||||||
await import("@termix-ssh/plugin-sdk/backend");
|
await import("@termix-ssh/plugin-sdk/backend");
|
||||||
|
const { pluginEvents, TOPICS } = await import("../../plugins/events.js");
|
||||||
|
|
||||||
function manifest(capabilities: string[]) {
|
function manifest(capabilities: string[]) {
|
||||||
return {
|
return {
|
||||||
@@ -436,6 +437,29 @@ describe("ctx.hosts", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("publishes host.created for a host it creates", async () => {
|
||||||
|
h.granted = new Set(["hosts:write"]);
|
||||||
|
const hosts = createPluginHosts({
|
||||||
|
manifest: manifest(["hosts:write"]),
|
||||||
|
audit: vi.fn(async () => {}),
|
||||||
|
});
|
||||||
|
const seen: unknown[] = [];
|
||||||
|
const off = pluginEvents.on(TOPICS.hostCreated, (payload) => {
|
||||||
|
seen.push(payload);
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await hosts.create({
|
||||||
|
ip: "10.0.0.5",
|
||||||
|
port: 22,
|
||||||
|
username: "root",
|
||||||
|
authType: "password",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
off();
|
||||||
|
}
|
||||||
|
expect(seen).toEqual([{ hostId: 99, userId: "user-1" }]);
|
||||||
|
});
|
||||||
|
|
||||||
it("refuses a field the SDK does not take instead of dropping it", async () => {
|
it("refuses a field the SDK does not take instead of dropping it", async () => {
|
||||||
h.granted = new Set(["hosts:write"]);
|
h.granted = new Set(["hosts:write"]);
|
||||||
const hosts = createPluginHosts({
|
const hosts = createPluginHosts({
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ const { createResolvers, writeWireRow } = await import("../../sync/store.js");
|
|||||||
const { getEntity } = await import("../../plugins/sync-registry.js");
|
const { getEntity } = await import("../../plugins/sync-registry.js");
|
||||||
const { FieldCrypto } = await import("../../utils/field-crypto.js");
|
const { FieldCrypto } = await import("../../utils/field-crypto.js");
|
||||||
const { syncLogger } = await import("../../utils/logger.js");
|
const { syncLogger } = await import("../../utils/logger.js");
|
||||||
|
const { pluginEvents, TOPICS } = await import("../../plugins/events.js");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json({ limit: "5mb" }));
|
app.use(express.json({ limit: "5mb" }));
|
||||||
@@ -338,6 +339,44 @@ describe("linking a desktop", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("a synced host", () => {
|
||||||
|
it("publishes host.created when it is written for the first time only", async () => {
|
||||||
|
const [desk] = desktops;
|
||||||
|
const seen: unknown[] = [];
|
||||||
|
const off = pluginEvents.on(TOPICS.hostCreated, (payload) => {
|
||||||
|
seen.push(payload);
|
||||||
|
});
|
||||||
|
const wire = {
|
||||||
|
syncId: "host-new",
|
||||||
|
name: "new",
|
||||||
|
ip: "10.0.0.2",
|
||||||
|
port: 22,
|
||||||
|
username: "root",
|
||||||
|
authType: "password",
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await on(desk, async () => {
|
||||||
|
const resolvers = createResolvers(desk.userId);
|
||||||
|
const first = await writeWireRow(
|
||||||
|
getEntity("hosts")!,
|
||||||
|
desk.userId,
|
||||||
|
wire,
|
||||||
|
resolvers,
|
||||||
|
);
|
||||||
|
await writeWireRow(
|
||||||
|
getEntity("hosts")!,
|
||||||
|
desk.userId,
|
||||||
|
{ ...wire, name: "renamed" },
|
||||||
|
resolvers,
|
||||||
|
);
|
||||||
|
expect(seen).toEqual([{ hostId: first.id, userId: desk.userId }]);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
off();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("after linking", () => {
|
describe("after linking", () => {
|
||||||
it("sends edits and deletes both ways", async () => {
|
it("sends edits and deletes both ways", async () => {
|
||||||
const [desk] = desktops;
|
const [desk] = desktops;
|
||||||
|
|||||||
Reference in new issue
Block a user