feat: add integration access settings (#725)
* feat: add integration access settings * fix: typecheck and test issues * fix: test timeout * chore: address pull request feedback * chore: add throw if action forbidden for integration permissions * fix: unable to create new migrations because of duplicate prevId in sqlite snapshots * chore: add sqlite migration for integration permissions * test: add unit tests for integration access * test: add permission checks to integration router tests * test: add unit test for integration permissions * chore: add mysql migration * fix: format issues
This commit is contained in:
@@ -158,7 +158,7 @@ describe("getAllBoards should return all boards accessable to the current user",
|
||||
expect(result.map(({ name }) => name)).toStrictEqual(["public", "private2"]);
|
||||
});
|
||||
|
||||
test.each([["board-view"], ["board-change"]] satisfies [BoardPermission][])(
|
||||
test.each([["view"], ["modify"]] satisfies [BoardPermission][])(
|
||||
"with %s group board permission it should show board",
|
||||
async (permission) => {
|
||||
// Arrange
|
||||
@@ -222,7 +222,7 @@ describe("getAllBoards should return all boards accessable to the current user",
|
||||
},
|
||||
);
|
||||
|
||||
test.each([["board-view"], ["board-change"]] satisfies [BoardPermission][])(
|
||||
test.each([["view"], ["modify"]] satisfies [BoardPermission][])(
|
||||
"with %s user board permission it should show board",
|
||||
async (permission) => {
|
||||
// Arrange
|
||||
@@ -347,7 +347,7 @@ describe("rename board should rename board", () => {
|
||||
});
|
||||
expect(dbBoard).toBeDefined();
|
||||
expect(dbBoard?.name).toBe("newName");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full-access");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full");
|
||||
});
|
||||
|
||||
test("should throw error when similar board name exists", async () => {
|
||||
@@ -422,7 +422,7 @@ describe("changeBoardVisibility should change board visibility", () => {
|
||||
});
|
||||
expect(dbBoard).toBeDefined();
|
||||
expect(dbBoard?.isPublic).toBe(visibility === "public");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full-access");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full");
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -452,7 +452,7 @@ describe("deleteBoard should delete board", () => {
|
||||
where: eq(boards.id, boardId),
|
||||
});
|
||||
expect(dbBoard).toBeUndefined();
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full-access");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full");
|
||||
});
|
||||
|
||||
test("should throw error when board not found", async () => {
|
||||
@@ -485,7 +485,7 @@ describe("getHomeBoard should return home board", () => {
|
||||
name: "home",
|
||||
...fullBoardProps,
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-view");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "view");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -506,7 +506,7 @@ describe("getBoardByName should return board by name", () => {
|
||||
name,
|
||||
...fullBoardProps,
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-view");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "view");
|
||||
});
|
||||
|
||||
it("should throw error when not present", async () => {
|
||||
@@ -583,7 +583,7 @@ describe("savePartialBoardSettings should save general settings", () => {
|
||||
expect(dbBoard?.primaryColor).toBe(newPrimaryColor);
|
||||
expect(dbBoard?.secondaryColor).toBe(newSecondaryColor);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "modify");
|
||||
});
|
||||
|
||||
it("should throw error when board not found", async () => {
|
||||
@@ -638,7 +638,7 @@ describe("saveBoard should save full board", () => {
|
||||
expect(definedBoard.sections.length).toBe(1);
|
||||
expect(definedBoard.sections[0]?.id).not.toBe(sectionId);
|
||||
expect(section).toBeUndefined();
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "modify");
|
||||
});
|
||||
it("should remove item when not present in input", async () => {
|
||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||
@@ -692,7 +692,7 @@ describe("saveBoard should save full board", () => {
|
||||
expect(firstSection.items.length).toBe(1);
|
||||
expect(firstSection.items[0]?.id).not.toBe(itemId);
|
||||
expect(item).toBeUndefined();
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "modify");
|
||||
});
|
||||
it("should remove integration reference when not present in input", async () => {
|
||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||
@@ -759,7 +759,7 @@ describe("saveBoard should save full board", () => {
|
||||
expect(firstItem.integrations.length).toBe(1);
|
||||
expect(firstItem.integrations[0]?.integrationId).not.toBe(integrationId);
|
||||
expect(integration).toBeUndefined();
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "modify");
|
||||
});
|
||||
it.each([[{ kind: "empty" as const }], [{ kind: "category" as const, name: "My first category" }]])(
|
||||
"should add section when present in input",
|
||||
@@ -811,7 +811,7 @@ describe("saveBoard should save full board", () => {
|
||||
expect(addedSection.name).toBe(partialSection.name);
|
||||
}
|
||||
expect(section).toBeDefined();
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "modify");
|
||||
},
|
||||
);
|
||||
it("should add item when present in input", async () => {
|
||||
@@ -875,7 +875,7 @@ describe("saveBoard should save full board", () => {
|
||||
expect(addedItem.xOffset).toBe(3);
|
||||
expect(addedItem.yOffset).toBe(2);
|
||||
expect(item).toBeDefined();
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "modify");
|
||||
});
|
||||
it("should add integration reference when present in input", async () => {
|
||||
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
|
||||
@@ -942,7 +942,7 @@ describe("saveBoard should save full board", () => {
|
||||
expect(firstItem.integrations.length).toBe(1);
|
||||
expect(firstItem.integrations[0]?.integrationId).toBe(integration.id);
|
||||
expect(integrationItem).toBeDefined();
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "modify");
|
||||
});
|
||||
it("should update section when present in input", async () => {
|
||||
const db = createDb();
|
||||
@@ -1052,7 +1052,7 @@ describe("saveBoard should save full board", () => {
|
||||
expect(firstItem.width).toBe(2);
|
||||
expect(firstItem.xOffset).toBe(7);
|
||||
expect(firstItem.yOffset).toBe(5);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "board-change");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "modify");
|
||||
});
|
||||
it("should fail when board not found", async () => {
|
||||
const db = createDb();
|
||||
@@ -1091,12 +1091,12 @@ describe("getBoardPermissions should return board permissions", () => {
|
||||
await db.insert(boardUserPermissions).values([
|
||||
{
|
||||
userId: user1,
|
||||
permission: "board-view",
|
||||
permission: "view",
|
||||
boardId,
|
||||
},
|
||||
{
|
||||
userId: user2,
|
||||
permission: "board-change",
|
||||
permission: "modify",
|
||||
boardId,
|
||||
},
|
||||
]);
|
||||
@@ -1109,7 +1109,7 @@ describe("getBoardPermissions should return board permissions", () => {
|
||||
|
||||
await db.insert(boardGroupPermissions).values({
|
||||
groupId,
|
||||
permission: "board-view",
|
||||
permission: "view",
|
||||
boardId,
|
||||
});
|
||||
|
||||
@@ -1122,26 +1122,26 @@ describe("getBoardPermissions should return board permissions", () => {
|
||||
const result = await caller.getBoardPermissions({ id: boardId });
|
||||
|
||||
// Assert
|
||||
expect(result.groupPermissions).toEqual([{ group: { id: groupId, name: "group1" }, permission: "board-view" }]);
|
||||
expect(result.userPermissions).toEqual(
|
||||
expect(result.groups).toEqual([{ group: { id: groupId, name: "group1" }, permission: "view" }]);
|
||||
expect(result.users).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
user: { id: user1, name: null, image: null },
|
||||
permission: "board-view",
|
||||
permission: "view",
|
||||
},
|
||||
{
|
||||
user: { id: user2, name: null, image: null },
|
||||
permission: "board-change",
|
||||
permission: "modify",
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(result.inherited).toEqual([{ group: { id: groupId, name: "group1" }, permission: "admin" }]);
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full-access");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full");
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveUserBoardPermissions should save user board permissions", () => {
|
||||
test.each([["board-view"], ["board-change"]] satisfies [BoardPermission][])(
|
||||
test.each([["view"], ["modify"]] satisfies [BoardPermission][])(
|
||||
"should save user board permissions",
|
||||
async (permission) => {
|
||||
// Arrange
|
||||
@@ -1163,10 +1163,10 @@ describe("saveUserBoardPermissions should save user board permissions", () => {
|
||||
|
||||
// Act
|
||||
await caller.saveUserBoardPermissions({
|
||||
id: boardId,
|
||||
entityId: boardId,
|
||||
permissions: [
|
||||
{
|
||||
itemId: user1,
|
||||
principalId: user1,
|
||||
permission,
|
||||
},
|
||||
],
|
||||
@@ -1177,13 +1177,13 @@ describe("saveUserBoardPermissions should save user board permissions", () => {
|
||||
where: eq(boardUserPermissions.userId, user1),
|
||||
});
|
||||
expect(dbUserPermission).toBeDefined();
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full-access");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("saveGroupBoardPermissions should save group board permissions", () => {
|
||||
test.each([["board-view"], ["board-change"]] satisfies [BoardPermission][])(
|
||||
test.each([["view"], ["modify"]] satisfies [BoardPermission][])(
|
||||
"should save group board permissions",
|
||||
async (permission) => {
|
||||
// Arrange
|
||||
@@ -1210,10 +1210,10 @@ describe("saveGroupBoardPermissions should save group board permissions", () =>
|
||||
|
||||
// Act
|
||||
await caller.saveGroupBoardPermissions({
|
||||
id: boardId,
|
||||
entityId: boardId,
|
||||
permissions: [
|
||||
{
|
||||
itemId: groupId,
|
||||
principalId: groupId,
|
||||
permission,
|
||||
},
|
||||
],
|
||||
@@ -1224,7 +1224,7 @@ describe("saveGroupBoardPermissions should save group board permissions", () =>
|
||||
where: eq(boardGroupPermissions.groupId, groupId),
|
||||
});
|
||||
expect(dbGroupPermission).toBeDefined();
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full-access");
|
||||
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "full");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -18,14 +18,11 @@ const expectActToBeAsync = async (act: () => Promise<void>, success: boolean) =>
|
||||
await expect(act()).resolves.toBeUndefined();
|
||||
};
|
||||
|
||||
// TODO: most of this test can be used for constructBoardPermissions
|
||||
// TODO: the tests for the board-access can be reduced to about 4 tests (as the unit has shrunk)
|
||||
|
||||
describe("throwIfActionForbiddenAsync should check access to board and return boolean", () => {
|
||||
test.each([
|
||||
["full-access" as const, true],
|
||||
["board-change" as const, true],
|
||||
["board-view" as const, true],
|
||||
["full" as const, true],
|
||||
["modify" as const, true],
|
||||
["view" as const, true],
|
||||
])("with permission %s should return %s when hasFullAccess is true", async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
@@ -52,9 +49,9 @@ describe("throwIfActionForbiddenAsync should check access to board and return bo
|
||||
});
|
||||
|
||||
test.each([
|
||||
["full-access" as const, false],
|
||||
["board-change" as const, true],
|
||||
["board-view" as const, true],
|
||||
["full" as const, false],
|
||||
["modify" as const, true],
|
||||
["view" as const, true],
|
||||
])("with permission %s should return %s when hasChangeAccess is true", async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
@@ -81,9 +78,9 @@ describe("throwIfActionForbiddenAsync should check access to board and return bo
|
||||
});
|
||||
|
||||
test.each([
|
||||
["full-access" as const, false],
|
||||
["board-change" as const, false],
|
||||
["board-view" as const, true],
|
||||
["full" as const, false],
|
||||
["modify" as const, false],
|
||||
["view" as const, true],
|
||||
])("with permission %s should return %s when hasViewAccess is true", async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
@@ -110,9 +107,9 @@ describe("throwIfActionForbiddenAsync should check access to board and return bo
|
||||
});
|
||||
|
||||
test.each([
|
||||
["full-access" as const, false],
|
||||
["board-change" as const, false],
|
||||
["board-view" as const, false],
|
||||
["full" as const, false],
|
||||
["modify" as const, false],
|
||||
["view" as const, false],
|
||||
])("with permission %s should return %s when hasViewAccess is false", async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
@@ -143,7 +140,7 @@ describe("throwIfActionForbiddenAsync should check access to board and return bo
|
||||
const db = createDb();
|
||||
|
||||
// Act
|
||||
const act = () => throwIfActionForbiddenAsync({ db, session: null }, eq(boards.id, createId()), "full-access");
|
||||
const act = () => throwIfActionForbiddenAsync({ db, session: null }, eq(boards.id, createId()), "full");
|
||||
|
||||
// Assert
|
||||
await expect(act()).rejects.toThrow("Board not found");
|
||||
|
||||
@@ -364,7 +364,7 @@ describe("savePermissions should save permissions for group", () => {
|
||||
// Act
|
||||
await caller.savePermissions({
|
||||
groupId,
|
||||
permissions: ["integration-use-all", "board-full-access"],
|
||||
permissions: ["integration-use-all", "board-full-all"],
|
||||
});
|
||||
|
||||
// Assert
|
||||
@@ -373,7 +373,7 @@ describe("savePermissions should save permissions for group", () => {
|
||||
});
|
||||
|
||||
expect(permissions.length).toBe(2);
|
||||
expect(permissions.map(({ permission }) => permission)).toEqual(["integration-use-all", "board-full-access"]);
|
||||
expect(permissions.map(({ permission }) => permission)).toEqual(["integration-use-all", "board-full-all"]);
|
||||
});
|
||||
|
||||
test("with non existing group it should throw not found error", async () => {
|
||||
@@ -390,7 +390,7 @@ describe("savePermissions should save permissions for group", () => {
|
||||
const actAsync = async () =>
|
||||
await caller.savePermissions({
|
||||
groupId: createId(),
|
||||
permissions: ["integration-create", "board-full-access"],
|
||||
permissions: ["integration-create", "board-full-all"],
|
||||
});
|
||||
|
||||
// Assert
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import * as authShared from "@homarr/auth/shared";
|
||||
import { createId, eq } from "@homarr/db";
|
||||
import { integrations, users } from "@homarr/db/schema/sqlite";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { throwIfActionForbiddenAsync } from "../../integration/integration-access";
|
||||
|
||||
const defaultCreatorId = createId();
|
||||
|
||||
const expectActToBeAsync = async (act: () => Promise<void>, success: boolean) => {
|
||||
if (!success) {
|
||||
await expect(act()).rejects.toThrow("Integration not found");
|
||||
return;
|
||||
}
|
||||
|
||||
await expect(act()).resolves.toBeUndefined();
|
||||
};
|
||||
|
||||
describe("throwIfActionForbiddenAsync should check access to integration and return boolean", () => {
|
||||
test.each([
|
||||
["full" as const, true],
|
||||
["interact" as const, true],
|
||||
["use" as const, true],
|
||||
])("with permission %s should return %s when hasFullAccess is true", async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructIntegrationPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: true,
|
||||
hasInteractAccess: false,
|
||||
hasUseAccess: false,
|
||||
});
|
||||
|
||||
const integrationId = createId();
|
||||
await db.insert(integrations).values({
|
||||
id: integrationId,
|
||||
name: "test",
|
||||
kind: "adGuardHome",
|
||||
url: "http://localhost:3000",
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
throwIfActionForbiddenAsync({ db, session: null }, eq(integrations.id, integrationId), permission);
|
||||
|
||||
// Assert
|
||||
await expectActToBeAsync(act, expectedResult);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["full" as const, false],
|
||||
["interact" as const, true],
|
||||
["use" as const, true],
|
||||
])("with permission %s should return %s when hasInteractAccess is true", async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructIntegrationPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: false,
|
||||
hasInteractAccess: true,
|
||||
hasUseAccess: false,
|
||||
});
|
||||
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const integrationId = createId();
|
||||
await db.insert(integrations).values({
|
||||
id: integrationId,
|
||||
name: "test",
|
||||
kind: "adGuardHome",
|
||||
url: "http://localhost:3000",
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
throwIfActionForbiddenAsync({ db, session: null }, eq(integrations.id, integrationId), permission);
|
||||
|
||||
// Assert
|
||||
await expectActToBeAsync(act, expectedResult);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["full" as const, false],
|
||||
["interact" as const, false],
|
||||
["use" as const, true],
|
||||
])("with permission %s should return %s when hasUseAccess is true", async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructIntegrationPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: false,
|
||||
hasInteractAccess: false,
|
||||
hasUseAccess: true,
|
||||
});
|
||||
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const integrationId = createId();
|
||||
await db.insert(integrations).values({
|
||||
id: integrationId,
|
||||
name: "test",
|
||||
kind: "adGuardHome",
|
||||
url: "http://localhost:3000",
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
throwIfActionForbiddenAsync({ db, session: null }, eq(integrations.id, integrationId), permission);
|
||||
|
||||
// Assert
|
||||
await expectActToBeAsync(act, expectedResult);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["full" as const, false],
|
||||
["interact" as const, false],
|
||||
["use" as const, false],
|
||||
])("with permission %s should return %s when hasUseAccess is false", async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructIntegrationPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: false,
|
||||
hasInteractAccess: false,
|
||||
hasUseAccess: false,
|
||||
});
|
||||
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const integrationId = createId();
|
||||
await db.insert(integrations).values({
|
||||
id: integrationId,
|
||||
name: "test",
|
||||
kind: "adGuardHome",
|
||||
url: "http://localhost:3000",
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
throwIfActionForbiddenAsync({ db, session: null }, eq(integrations.id, integrationId), permission);
|
||||
|
||||
// Assert
|
||||
await expectActToBeAsync(act, expectedResult);
|
||||
});
|
||||
|
||||
test("should throw when integration is not found", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
|
||||
// Act
|
||||
const act = () => throwIfActionForbiddenAsync({ db, session: null }, eq(integrations.id, createId()), "full");
|
||||
|
||||
// Assert
|
||||
await expect(act()).rejects.toThrow("Integration not found");
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,26 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { encryptSecret } from "@homarr/common";
|
||||
import { createId } from "@homarr/db";
|
||||
import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
|
||||
import { integrationRouter } from "../../integration/integration-router";
|
||||
import { expectToBeDefined } from "../helper";
|
||||
|
||||
const defaultUserId = createId();
|
||||
const defaultSessionWithPermissions = (permissions: GroupPermissionKey[] = []) =>
|
||||
({
|
||||
user: {
|
||||
id: defaultUserId,
|
||||
permissions,
|
||||
},
|
||||
expires: new Date().toISOString(),
|
||||
}) satisfies Session;
|
||||
|
||||
// Mock the auth module to return an empty session
|
||||
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
|
||||
vi.mock("../../integration/integration-test-connection", () => ({
|
||||
@@ -17,11 +28,11 @@ vi.mock("../../integration/integration-test-connection", () => ({
|
||||
}));
|
||||
|
||||
describe("all should return all integrations", () => {
|
||||
it("should return all integrations", async () => {
|
||||
test("with any session should return all integrations", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
session: defaultSessionWithPermissions(),
|
||||
});
|
||||
|
||||
await db.insert(integrations).values([
|
||||
@@ -47,11 +58,11 @@ describe("all should return all integrations", () => {
|
||||
});
|
||||
|
||||
describe("byId should return an integration by id", () => {
|
||||
it("should return an integration by id", async () => {
|
||||
test("with full access should return an integration by id", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
session: defaultSessionWithPermissions(["integration-full-all"]),
|
||||
});
|
||||
|
||||
await db.insert(integrations).values([
|
||||
@@ -73,22 +84,22 @@ describe("byId should return an integration by id", () => {
|
||||
expect(result.kind).toBe("plex");
|
||||
});
|
||||
|
||||
it("should throw an error if the integration does not exist", async () => {
|
||||
test("with full access should throw an error if the integration does not exist", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
session: defaultSessionWithPermissions(["integration-full-all"]),
|
||||
});
|
||||
|
||||
const actAsync = async () => await caller.byId({ id: "2" });
|
||||
await expect(actAsync()).rejects.toThrow("Integration not found");
|
||||
});
|
||||
|
||||
it("should only return the public secret values", async () => {
|
||||
test("with full access should only return the public secret values", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
session: defaultSessionWithPermissions(["integration-full-all"]),
|
||||
});
|
||||
|
||||
await db.insert(integrations).values([
|
||||
@@ -129,14 +140,38 @@ describe("byId should return an integration by id", () => {
|
||||
const apiKey = expectToBeDefined(result.secrets.find((secret) => secret.kind === "apiKey"));
|
||||
expect(apiKey.value).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("create should create a new integration", () => {
|
||||
it("should create a new integration", async () => {
|
||||
test("without full access should throw integration not found error", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
session: defaultSessionWithPermissions(["integration-interact-all"]),
|
||||
});
|
||||
|
||||
await db.insert(integrations).values([
|
||||
{
|
||||
id: "1",
|
||||
name: "Home assistant",
|
||||
kind: "homeAssistant",
|
||||
url: "http://homeassist.local",
|
||||
},
|
||||
]);
|
||||
|
||||
// Act
|
||||
const actAsync = async () => await caller.byId({ id: "1" });
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrow("Integration not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("create should create a new integration", () => {
|
||||
test("with create integration access should create a new integration", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: defaultSessionWithPermissions(["integration-create"]),
|
||||
});
|
||||
const input = {
|
||||
name: "Jellyfin",
|
||||
@@ -164,14 +199,35 @@ describe("create should create a new integration", () => {
|
||||
expect(dbSecret!.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
|
||||
expect(dbSecret!.updatedAt).toEqual(fakeNow);
|
||||
});
|
||||
});
|
||||
|
||||
describe("update should update an integration", () => {
|
||||
it("should update an integration", async () => {
|
||||
test("without create integration access should throw permission error", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
session: defaultSessionWithPermissions(["integration-interact-all"]),
|
||||
});
|
||||
const input = {
|
||||
name: "Jellyfin",
|
||||
kind: "jellyfin" as const,
|
||||
url: "http://jellyfin.local",
|
||||
secrets: [{ kind: "apiKey" as const, value: "1234567890" }],
|
||||
};
|
||||
|
||||
// Act
|
||||
const actAsync = async () => await caller.create(input);
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrow("Permission denied");
|
||||
});
|
||||
});
|
||||
|
||||
describe("update should update an integration", () => {
|
||||
test("with full access should update an integration", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: defaultSessionWithPermissions(["integration-full-all"]),
|
||||
});
|
||||
|
||||
const lastWeek = new Date("2023-06-24T00:00:00Z");
|
||||
@@ -241,11 +297,11 @@ describe("update should update an integration", () => {
|
||||
expect(apiKey.value).not.toEqual(input.secrets[2]!.value);
|
||||
});
|
||||
|
||||
it("should throw an error if the integration does not exist", async () => {
|
||||
test("with full access should throw an error if the integration does not exist", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
session: defaultSessionWithPermissions(["integration-full-all"]),
|
||||
});
|
||||
|
||||
const actAsync = async () =>
|
||||
@@ -257,14 +313,35 @@ describe("update should update an integration", () => {
|
||||
});
|
||||
await expect(actAsync()).rejects.toThrow("Integration not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete should delete an integration", () => {
|
||||
it("should delete an integration", async () => {
|
||||
test("without full access should throw permission error", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: null,
|
||||
session: defaultSessionWithPermissions(["integration-interact-all"]),
|
||||
});
|
||||
|
||||
// Act
|
||||
const actAsync = async () =>
|
||||
await caller.update({
|
||||
id: createId(),
|
||||
name: "Pi Hole",
|
||||
url: "http://hole.local",
|
||||
secrets: [],
|
||||
});
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrow("Integration not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete should delete an integration", () => {
|
||||
test("with full access should delete an integration", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: defaultSessionWithPermissions(["integration-full-all"]),
|
||||
});
|
||||
|
||||
const integrationId = createId();
|
||||
@@ -291,4 +368,19 @@ describe("delete should delete an integration", () => {
|
||||
const dbSecrets = await db.query.integrationSecrets.findMany();
|
||||
expect(dbSecrets.length).toBe(0);
|
||||
});
|
||||
|
||||
test("without full access should throw permission error", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: defaultSessionWithPermissions(["integration-interact-all"]),
|
||||
});
|
||||
|
||||
// Act
|
||||
const actAsync = async () => await caller.delete({ id: createId() });
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrow("Integration not found");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user