Skip to content

Commit

Permalink
Add secret tests
Browse files Browse the repository at this point in the history
  • Loading branch information
kielbasa-elp committed Feb 26, 2024
1 parent 7b76e30 commit 0a15467
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const SecretKeyList: React.FC<SecretKeyListProps> = ({ items }) => {
<ItemList
className="grid grid-cols-1 gap-2"
items={items}
aria-label="Secret list"
emptyText={<EmptyMessage>There is no Secrets yet...</EmptyMessage>}
renderItem={(item) => (
<SecretKeyItem
Expand Down Expand Up @@ -100,7 +101,7 @@ export const SecretKeyItem: React.FC<SecretKeyItemProps> = ({
<IconButton
size="xs"
variant="ghost"
aria-label="Edit secret"
aria-label={`Edit secret: ${data.name}`}
className="group-hover:opacity-100 !bg-neutral-700 !text-white !text-sm hover:!text-primary-500 lg:opacity-0"
title={`Edit Secret: ${data.name}`}
icon={<Icon iconName="edit" />}
Expand All @@ -110,9 +111,9 @@ export const SecretKeyItem: React.FC<SecretKeyItemProps> = ({
<IconButton
size="xs"
variant="ghost"
aria-label="Remove secret"
aria-label={`Delete secret: ${data.name}`}
className="group-hover:opacity-100 !bg-neutral-700 !text-white !text-sm hover:!text-red-500 lg:opacity-0"
title={`Remove Secret: ${data.name}`}
title={`Delete Secret: ${data.name}`}
icon={<Icon iconName="trash" />}
onClick={handleDelete}
/>
Expand Down
5 changes: 4 additions & 1 deletion apps/web-remix/app/components/pages/secrets/list/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ export function SecretListPage() {

<PageContentWrapper>
<div className="mt-5 mb-6 flex gap-2 justify-end items-center">
<Link to={routes.secretsNew(organizationId)}>
<Link
to={routes.secretsNew(organizationId)}
aria-label="Add new secret"
>
<Button size="sm" tabIndex={0}>
New Secret
</Button>
Expand Down
4 changes: 0 additions & 4 deletions apps/web-remix/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ import { withSentry } from "@sentry/remix";
import type { LinksFunction } from "@remix-run/node";
import Modal from "react-modal";
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useRouteError,
} from "@remix-run/react";
import { Toaster } from "~/components/toasts/Toaster";
import "./tailwind.css";
import { GlobalNotFound } from "~/components/errorBoundaries/GlobalNotFound";
import { GlobalRuntime } from "~/components/errorBoundaries/GlobalRuntime";
import { PageProgress } from "~/components/progressBar/PageProgress";

export { ErrorBoundary } from "~/components/errorBoundaries/RootErrorBoundary";
Expand Down
157 changes: 157 additions & 0 deletions apps/web-remix/app/tests/__tests__/secrets/secrets.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React from "react";
import { test, describe, expect } from "vitest";
import {
loaderWithSession,
RoutesProps,
setupRoutes,
} from "~/tests/setup.tests";
import { render, screen } from "~/tests/render";
import { server } from "~/tests/server.mock";
import { SecretListPage } from "~/components/pages/secrets/list/page";
import { loader as secretsLoader } from "~/components/pages/secrets/list/loader.server";
import { action as secretsAction } from "~/components/pages/secrets/list/action.server";
import { NewSecret } from "~/components/pages/secrets/newSecret/page";
import { loader as newSecretLoader } from "~/components/pages/secrets/newSecret/loader.server";
import { action as newSecretAction } from "~/components/pages/secrets/newSecret/action.server";
import { SecretsHandlers } from "~/tests/handlers/secret.handlers";
import { secretFixture } from "~/tests/fixtures/secrets.fixtures";
import { ListHandle } from "~/tests/handles/List.handle";
import { ButtonHandle } from "~/tests/handles/Button.handle";
import { InputHandle } from "~/tests/handles/Input.handle";

const handlers = () => [
...new SecretsHandlers([
secretFixture(),
secretFixture({ id: "deepgram", name: "deepgram" }),
]).handlers,
];

describe("Secrets", () => {
const setupServer = server(handlers());

beforeAll(() => setupServer.listen());
afterEach(() => setupServer.resetHandlers(...handlers()));
afterAll(() => setupServer.close());

test("should render correct amount of secrets", async () => {
const page = new SecretsObject().render({
initialEntries: ["/2/secrets"],
});

const list = await page.getSecretList();
expect(list.children).toHaveLength(3);
});

test("should render empty list message if secrets empty", async () => {
new SecretsObject().render({
initialEntries: ["/2/secrets"],
});

await screen.findByText(/There is no Secrets yet/i);
});

test("should delete secret", async () => {
const page = new SecretsObject().render({
initialEntries: ["/2/secrets"],
});

const list = await page.getSecretList();
expect(list.children).toHaveLength(3);

const button = await ButtonHandle.fromLabelText(/Delete secret: deepgram/i);
await button.click();

await page.confirmAction();

expect(list.children).toHaveLength(2);
});

test("should add secret", async () => {
const page = new SecretsObject().render({
initialEntries: ["/2/secrets/new"],
});

const name = await InputHandle.fromLabelText(/name/i);
await name.type("NEW");

const value = await InputHandle.fromLabelText(/value/i);
await value.type("NEW");

const submit = await ButtonHandle.fromRole("Save the Secret");
await submit.click();

const list = await page.getSecretList();
expect(list.children).toHaveLength(4);
});

test("should show validation errors", async () => {
new SecretsObject().render({
initialEntries: ["/2/secrets/new"],
});

const submit = await ButtonHandle.fromRole("Save the Secret");
await submit.click();

expect(
await screen.findAllByText(/String must contain at least 2 /i)
).toHaveLength(2);
});

test("should change secret value", async () => {
new SecretsObject().render({
initialEntries: ["/2/secrets"],
});

const editButton = await ButtonHandle.fromLabelText(
/Edit secret: deepgram/i
);
await editButton.click();

const updateSubmit = await ButtonHandle.fromRole("Update Secret");
await updateSubmit.click();

await screen.findByText(/String must contain at least 2 character/i);

const newValue = await InputHandle.fromLabelText(/value/i);
await newValue.type("NEW_VALUE");

await updateSubmit.click();

await screen.findByText(/02:00/i);

Check failure on line 120 in apps/web-remix/app/tests/__tests__/secrets/secrets.test.tsx

View workflow job for this annotation

GitHub Actions / Test

app/tests/__tests__/secrets/secrets.test.tsx > Secrets > should change secret value

TestingLibraryElementError: Unable to find an element with the text: /02:00/i. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body class="" > <div> <div id="_root" > <div class="-mt-1" style="position: fixed; z-index: 9999; top: 16px; left: 16px; right: 16px; bottom: 16px; pointer-events: none;" /> <div class="shadow-navbar-shadowBase h-fit w-full bg-navbar-base py-2 md:px-2" data-testid="navbar" > <header class="gap-navbar-gapHeader flex min-h-smNavbar w-full items-center px-navbar-paddingHorizontal py-navbar-paddingVertical lg:px-navbar-paddingHorizontalLarge lg:py-navbar-paddingVertical" > <h1 class="text-2xl font-medium text-white flex gap-3 items-center" > <span> Secrets and API Keys </span> <span aria-hidden="true" class="font-icons text-primary-500 cursor-pointer text-xl" id="secrets-and-api-keys-helpful-icon" role="img" >  </span> </h1> <div class="grow" /> <button aria-label="open menu" class="w-navbar-widthMenuButton aspect-square lg:hidden !text-white min-w-[24px]" > <svg aria-hidden="true" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" > <path d="M3 12H21M3 6H21M3 18H21" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.4" /> </svg> </button> </header> </div> <aside class="fixed z-50 inset-0 bg-neutral-850 px-4 py-8 w-full transition-opacity duration-300 ease-[cubic-bezier(0.25, 1, 0.5, 1)] flex flex-col sm:w-[360px] sm:transition sm:left-auto sm:right-4 sm:inset-y-4 sm:rounded-xl translate-x-[150%] opacity-0 pointer-events-none !bg-neutral-950" > <header class="flex justify-between gap-1 mb-6 pb-6 border-b border-neutral-100" > <div class="grow-1" > <h3 class="mb-2 text-xl font-medium text-white" > New Secret </h3> <p class="text-neutral-100 text-xs" > Enter your Secret to use them in multiple workflows. </p> </div> <button aria-label="Close sidebar" class="border-iconButton-borderWidthBase flex items-center justify-center rounded-iconButton-radiusBase hover:shadow-iconButton-shadowHover disabled:border-iconButton-basic-disabled disabled:bg-iconButton-basic-disabled disabled:text-iconButton-basic-disabled disabled:hover:shadow-none border-button-primaryOutlined bg-button-primaryOutlined text-button-primaryOutlined hover:border-button-primaryOutlined-hover hover:text-button-primaryOutlined-hover active:border-button-primaryOutlined-active active:text-button-primaryOutlined-active p-iconButton-paddingSmall h-iconButton-heightSM w-iconButton-widthSM" > <span aria-hidden="true" class="font-icons" role="img" >  </span> </button> </header> </aside> <div class="max-w-[1344px] px-4 mx-auto w-full md:px-6 lg:px-10" > <div class="mt-5 mb-6 flex gap-2 justify-end items-center" > <a aria-label="Add new secret" href="/2/secrets/new" > <button class="border-button-borderWidthBase font-button-weightBase relative flex items-center justify-center whitesp
});
});

class SecretsObject {
render(props?: RoutesProps) {
const Routes = setupRoutes([
{
path: "/:organizationId/secrets",
Component: SecretListPage,
loader: loaderWithSession(secretsLoader),
action: loaderWithSession(secretsAction),
},
{
path: "/:organizationId/secrets/new",
Component: NewSecret,
loader: loaderWithSession(newSecretLoader),
action: loaderWithSession(newSecretAction),
},
]);

render(<Routes {...props} />);

return this;
}

async confirmAction() {
const confirmButton = await ButtonHandle.fromRole("Delete Key");

await confirmButton.click();

return this;
}

async getSecretList() {
return ListHandle.fromLabelText(/Secret list/i);
}
}
8 changes: 4 additions & 4 deletions apps/web-remix/app/tests/fixtures/secrets.fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { IAsyncSelectItem } from "~/api/AsyncSelectApi";
import { ISecretKey } from "~/components/pages/secrets/variables.types";

export const secretFixture = (
override?: Partial<IAsyncSelectItem>
): IAsyncSelectItem => {
export const secretFixture = (override?: Partial<ISecretKey>): ISecretKey => {
return {
id: "OPENAI",
name: "OPENAI",
created_at: "07/02/2024 11:35",
updated_at: "07/02/2024 11:35",
...override,
};
};
60 changes: 51 additions & 9 deletions apps/web-remix/app/tests/handlers/secret.handlers.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import { http, HttpResponse } from "msw";
import { IAsyncSelectItem, IAsyncSelectItemList } from "~/api/AsyncSelectApi";
import { secretFixture } from "~/tests/fixtures/secrets.fixtures";
import { ISecretKey } from "~/components/pages/secrets/variables.types";

export class SecretsHandlers {
private secrets: Map<string | number, IAsyncSelectItem> = new Map();
private secrets: Map<string | number, ISecretKey> = new Map();

constructor(initials: IAsyncSelectItem[] = []) {
constructor(initials: ISecretKey[] = []) {
initials.forEach((secret) => this.secrets.set(secret.id, secret));
// this.secrets.set(secretFixture().id, secretFixture());
// this.secrets.set("Test", secretFixture({ name: "Test", id: "Test" }));
}

getSecretsHandler() {
return http.get("/super-api/organizations/:organizationId/secrets", () => {
return HttpResponse.json<{ data: IAsyncSelectItemList }>(
return HttpResponse.json<{ data: ISecretKey[] }>(
{ data: [...this.secrets.values()] },
{ status: 200 }
);
Expand All @@ -25,7 +22,12 @@ export class SecretsHandlers {
"/super-api/organizations/:organizationId/secrets",
async ({ request }) => {
const data = await request.json();
const transformed = { id: data.value, name: data.name };
const transformed: ISecretKey = {
id: data.value,
name: data.name,
created_at: "07/02/2024 11:35",
updated_at: "07/02/2024 11:35",
};

this.secrets.set(data.value, transformed);

Expand All @@ -34,7 +36,47 @@ export class SecretsHandlers {
);
}

deleteHandler() {
return http.delete(
"/super-api/organizations/:organizationId/secrets/:secretId",
async ({ params }) => {
this.secrets.delete(params.secretId.toString());

return HttpResponse.json({}, { status: 200 });
}
);
}

updateHandler() {
return http.put(
"/super-api/organizations/:organizationId/secrets/:secretId",
async ({ params, request }) => {
const secret = this.secrets.get(params.secretId.toString());

if (!secret) {
return HttpResponse.json(
{},
{
status: 404,
}
);
}

this.secrets.set(params.secretId.toString(), {
...secret,
updated_at: "07/02/2024 00:00:00",
});
return HttpResponse.json({}, { status: 200 });
}
);
}

get handlers() {
return [this.getSecretsHandler(), this.createHandler()];
return [
this.getSecretsHandler(),
this.createHandler(),
this.deleteHandler(),
this.updateHandler(),
];
}
}
4 changes: 3 additions & 1 deletion apps/web-remix/app/tests/render.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React, { ReactElement } from "react";
import Modal from "react-modal";
import { Toaster } from "~/components/toasts/Toaster";
import { render, RenderOptions } from "@testing-library/react";
import { NavSidebarContext } from "~/components/sidebar/NavSidebar";
import Modal from "react-modal";

const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
Modal.setAppElement("div");
return (
<div id="_root">
<Toaster />
<NavSidebarContext.Provider
value={{
isOpen: false,
Expand Down

0 comments on commit 0a15467

Please sign in to comment.