mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
206 lines
5.8 KiB
TypeScript
206 lines
5.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("../../../database/db/index.js", () => ({
|
|
getDb: () => null,
|
|
getSqlite: () => null,
|
|
}));
|
|
vi.mock("../../../hosts/usable-credential.js", () => ({
|
|
findUsableCredential: async (id: number, userId: string) =>
|
|
id === 5 && userId === "importer" ? { id, username: "u" } : null,
|
|
}));
|
|
|
|
import {
|
|
fromPortableLogin,
|
|
keepUsableProtocolCredentials,
|
|
mergeProtocolLogin,
|
|
readProtocolAuthPayload,
|
|
toPortableLogins,
|
|
} from "../../../hosts/protocol-auth/protocol-auth.js";
|
|
import {
|
|
setHostProtocolSource,
|
|
type DeclaredHostProtocol,
|
|
} from "../../../hosts/protocol-auth/registry.js";
|
|
import type { HostProtocolLogin } from "../../../database/repositories/host-protocol-auth-repository.js";
|
|
|
|
const SPICE: DeclaredHostProtocol = {
|
|
id: "spice",
|
|
credentialFields: [{ key: "display" }, { key: "ticket", secret: true }],
|
|
pluginId: "spice-plugin",
|
|
pluginName: "Spice",
|
|
};
|
|
const REMOTE_X: DeclaredHostProtocol = {
|
|
id: "remote-x",
|
|
credentialFields: [{ key: "domain" }],
|
|
pluginId: "x-plugin",
|
|
pluginName: "X",
|
|
};
|
|
|
|
const stored: HostProtocolLogin = {
|
|
protocol: "spice",
|
|
authType: "direct",
|
|
credentialId: null,
|
|
username: "viewer",
|
|
password: "saved",
|
|
fields: { display: "2" },
|
|
secretFields: { ticket: "t-saved" },
|
|
};
|
|
|
|
beforeEach(() => setHostProtocolSource(() => [SPICE, REMOTE_X]));
|
|
afterEach(() => setHostProtocolSource(() => []));
|
|
|
|
describe("readProtocolAuthPayload", () => {
|
|
it("reads protocolAuth for declared protocols only", () => {
|
|
const patch = readProtocolAuthPayload({
|
|
protocolAuth: {
|
|
spice: { username: "a" },
|
|
"remote-x": null,
|
|
stranger: { username: "b" },
|
|
},
|
|
});
|
|
expect(patch && Object.fromEntries(patch)).toEqual({
|
|
spice: { username: "a" },
|
|
"remote-x": null,
|
|
});
|
|
});
|
|
|
|
it("reads the flat fields a 2.8 client sends, named after the protocol", () => {
|
|
const patch = readProtocolAuthPayload({
|
|
spiceUser: "viewer",
|
|
spicePassword: "pw",
|
|
spiceDisplay: "3",
|
|
spiceAuthType: "direct",
|
|
remoteXUser: "x-user",
|
|
remoteXDomain: "CORP",
|
|
remoteXCredentialId: 9,
|
|
});
|
|
expect(patch && Object.fromEntries(patch)).toEqual({
|
|
spice: {
|
|
authType: "direct",
|
|
username: "viewer",
|
|
password: "pw",
|
|
fields: { display: "3" },
|
|
},
|
|
"remote-x": {
|
|
credentialId: 9,
|
|
username: "x-user",
|
|
fields: { domain: "CORP" },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("treats a 2.8 editor's null password as leaving it alone", () => {
|
|
const patch = readProtocolAuthPayload({
|
|
spiceAuthType: "direct",
|
|
spiceUser: "viewer",
|
|
spicePassword: null,
|
|
});
|
|
expect(patch?.get("spice")).toEqual({
|
|
authType: "direct",
|
|
username: "viewer",
|
|
});
|
|
});
|
|
|
|
it("reads a 2.8 editor's all-null protocol as removed", () => {
|
|
const patch = readProtocolAuthPayload({
|
|
spiceAuthType: null,
|
|
spiceUser: null,
|
|
spicePassword: null,
|
|
spiceCredentialId: null,
|
|
});
|
|
expect(patch?.get("spice")).toBeNull();
|
|
});
|
|
|
|
it("answers null when a write carries no logins", () => {
|
|
expect(readProtocolAuthPayload({ name: "host" })).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("mergeProtocolLogin", () => {
|
|
it("keeps what the input leaves out, including secrets", () => {
|
|
expect(mergeProtocolLogin("spice", SPICE, stored, {})).toEqual(stored);
|
|
});
|
|
|
|
it("keeps a saved password and secret for an empty value", () => {
|
|
const next = mergeProtocolLogin("spice", SPICE, stored, {
|
|
password: "",
|
|
fields: { ticket: "", display: "" },
|
|
});
|
|
expect(next.password).toBe("saved");
|
|
expect(next.secretFields).toEqual({ ticket: "t-saved" });
|
|
// A plain field can be cleared.
|
|
expect(next.fields).toEqual({});
|
|
});
|
|
|
|
it("drops the direct login and undeclared fields for a credential", () => {
|
|
const next = mergeProtocolLogin("spice", SPICE, stored, {
|
|
authType: "credential",
|
|
credentialId: "12",
|
|
fields: { unknown: "x" },
|
|
});
|
|
expect(next).toMatchObject({
|
|
authType: "credential",
|
|
credentialId: 12,
|
|
username: null,
|
|
password: null,
|
|
fields: { display: "2" },
|
|
});
|
|
});
|
|
|
|
it("starts a new login from the input", () => {
|
|
expect(
|
|
mergeProtocolLogin("spice", SPICE, null, {
|
|
username: "new",
|
|
password: "pw",
|
|
fields: { ticket: "t" },
|
|
}),
|
|
).toEqual({
|
|
protocol: "spice",
|
|
authType: "direct",
|
|
credentialId: null,
|
|
username: "new",
|
|
password: "pw",
|
|
fields: {},
|
|
secretFields: { ticket: "t" },
|
|
});
|
|
});
|
|
|
|
it("falls back to direct for an auth type it does not know", () => {
|
|
expect(
|
|
mergeProtocolLogin("spice", SPICE, null, { authType: "magic" }).authType,
|
|
).toBe("direct");
|
|
});
|
|
});
|
|
|
|
describe("portable logins", () => {
|
|
it("round-trips through an export, splitting fields by the declaration", () => {
|
|
const portable = toPortableLogins([stored]);
|
|
expect(portable.spice.fields).toEqual({ display: "2", ticket: "t-saved" });
|
|
expect(fromPortableLogin("spice", portable.spice, null)).toEqual(stored);
|
|
});
|
|
|
|
it("keeps every field of an undeclared protocol secret", () => {
|
|
expect(
|
|
fromPortableLogin("gone", { fields: { display: "1" } }, null)
|
|
?.secretFields,
|
|
).toEqual({ display: "1" });
|
|
});
|
|
|
|
it("drops a credential link a user cannot use", async () => {
|
|
const patch = await keepUsableProtocolCredentials(
|
|
new Map([
|
|
["spice", { authType: "credential", credentialId: 5 }],
|
|
["remote-x", { authType: "credential", credentialId: 6 }],
|
|
]),
|
|
"importer",
|
|
);
|
|
expect(patch.get("spice")).toEqual({
|
|
authType: "credential",
|
|
credentialId: 5,
|
|
});
|
|
expect(patch.get("remote-x")).toEqual({
|
|
authType: "direct",
|
|
credentialId: null,
|
|
});
|
|
});
|
|
});
|