Skip to content

Commit

Permalink
Merge pull request #12 from Selleo/jw/fe-test-setup
Browse files Browse the repository at this point in the history
feat: fe test setup
  • Loading branch information
typeWolffo authored Aug 6, 2024
2 parents 2636bc1 + fb064f2 commit 6fd0cd5
Show file tree
Hide file tree
Showing 34 changed files with 2,725 additions and 174 deletions.
4 changes: 3 additions & 1 deletion examples/common_nestjs_remix/apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:e2e:watch": "jest --config ./test/jest-e2e.json --watch",
"db:migrate": "drizzle-kit migrate",
"db:generate": "drizzle-kit generate"
"db:generate": "drizzle-kit generate",
"db:seed": "ts-node -r tsconfig-paths/register ./src/seed.ts"
},
"dependencies": {
"@repo/email-templates": "workspace:*",
Expand Down Expand Up @@ -74,6 +75,7 @@
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"dotenv": "^16.4.5",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
Expand Down
9 changes: 8 additions & 1 deletion examples/common_nestjs_remix/apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import jwtConfig from "./common/configuration/jwt";
import emailConfig from "./common/configuration/email";
import awsConfig from "./common/configuration/aws";
import { APP_GUARD } from "@nestjs/core";
import { JwtAuthGuard } from "./common/guards/jwt-auth-guard";
import { JwtAuthGuard } from "./common/guards/jwt-auth.guard";
import { EmailModule } from "./common/emails/emails.module";
import { TestConfigModule } from "./test-config/test-config.module";
import { StagingGuard } from "./common/guards/staging.guard";

@Module({
imports: [
Expand Down Expand Up @@ -51,13 +53,18 @@ import { EmailModule } from "./common/emails/emails.module";
AuthModule,
UsersModule,
EmailModule,
TestConfigModule,
],
controllers: [],
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
{
provide: APP_GUARD,
useClass: StagingGuard,
},
],
})
export class AppModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Request, Response } from "express";
import { Validate } from "nestjs-typebox";
import { baseResponse, BaseResponse, nullResponse } from "src/common";
import { Public } from "src/common/decorators/public.decorator";
import { RefreshTokenGuard } from "src/common/guards/refresh-token-guard";
import { RefreshTokenGuard } from "src/common/guards/refresh-token.guard";
import { UUIDType } from "src/common/index";
import { commonUserSchema } from "src/common/schemas/common-user.schema";
import { AuthService } from "../auth.service";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { SetMetadata } from "@nestjs/common";

export const OnlyStaging = () => SetMetadata("onlyStaging", true);
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";

@Injectable()
export class StagingGuard implements CanActivate {
constructor(private reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const onlyStaging = this.reflector.get<boolean>(
"onlyStaging",
context.getHandler(),
);
if (!onlyStaging) {
return true;
}
return process.env.NODE_ENV === "staging";
}
}
112 changes: 112 additions & 0 deletions examples/common_nestjs_remix/apps/api/src/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import postgres from "postgres";
import { drizzle } from "drizzle-orm/postgres-js";
import { users, credentials } from "./storage/schema";
import { DatabasePg } from "./common";
import { faker } from "@faker-js/faker";
import * as dotenv from "dotenv";
import hashPassword from "./common/helpers/hashPassword";

dotenv.config({ path: "./.env" });

if (!("DATABASE_URL" in process.env))
throw new Error("DATABASE_URL not found on .env");

async function seed() {
const connectionString = process.env.DATABASE_URL!;
const testUserEmail = "[email protected]";
const adminPassword = "password";

const sql = postgres(connectionString);
const db = drizzle(sql) as DatabasePg;

try {
const adminUserData = {
id: faker.string.uuid(),
email: testUserEmail,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};

const [insertedAdminUser] = await db
.insert(users)
.values(adminUserData)
.returning();

const adminCredentialData = {
id: faker.string.uuid(),
userId: insertedAdminUser.id,
password: await hashPassword(adminPassword),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};

const [insertedAdminCredential] = await db
.insert(credentials)
.values(adminCredentialData)
.returning();

console.log("Created admin user:", {
...insertedAdminUser,
credentials: {
...insertedAdminCredential,
password: adminPassword,
},
});

const usersWithCredentials = await Promise.all(
Array.from({ length: 5 }, async () => {
const userData = {
id: faker.string.uuid(),
email: faker.internet.email(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};

const [insertedUser] = await db
.insert(users)
.values(userData)
.returning();

const password = faker.internet.password();
const credentialData = {
id: faker.string.uuid(),
userId: insertedUser.id,
password: await hashPassword(password),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};

const [insertedCredential] = await db
.insert(credentials)
.values(credentialData)
.returning();

return {
...insertedUser,
credentials: {
...insertedCredential,
password: password,
},
};
}),
);

console.log("Created users with credentials:", usersWithCredentials);
console.log("Seeding completed successfully");
} catch (error) {
console.error("Seeding failed:", error);
} finally {
await sql.end();
}
}

if (require.main === module) {
seed()
.then(() => process.exit(0))
.catch((error) => {
console.error("An error occurred:", error);
process.exit(1);
});
}

export default seed;
22 changes: 22 additions & 0 deletions examples/common_nestjs_remix/apps/api/src/swagger/api-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,28 @@
}
}
}
},
"/test-config/setup": {
"post": {
"operationId": "TestConfigController_setup",
"parameters": [],
"responses": {
"201": {
"description": ""
}
}
}
},
"/test-config/teardown": {
"post": {
"operationId": "TestConfigController_teardown",
"parameters": [],
"responses": {
"201": {
"description": ""
}
}
}
}
},
"info": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Controller, Post } from "@nestjs/common";
import { TestConfigService } from "../test-config.service";
import { OnlyStaging } from "src/common/decorators/staging.decorator";
import { Public } from "src/common/decorators/public.decorator";

@Controller("test-config")
export class TestConfigController {
constructor(private testConfigService: TestConfigService) {}

@Public()
@Post("setup")
@OnlyStaging()
async setup(): Promise<void> {
return this.testConfigService.setup();
}

@Post("teardown")
@OnlyStaging()
async teardown(): Promise<void> {
return this.testConfigService.teardown();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from "@nestjs/common";
import { TestConfigController } from "./api/test-config.controller";
import { TestConfigService } from "./test-config.service";

@Module({
imports: [],
controllers: [TestConfigController],
providers: [TestConfigService],
exports: [],
})
export class TestConfigModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Injectable, NotImplementedException } from "@nestjs/common";

@Injectable()
export class TestConfigService {
constructor() {}

public async setup() {
throw new NotImplementedException();
}

public async teardown() {
throw new NotImplementedException();
}
}
4 changes: 4 additions & 0 deletions examples/common_nestjs_remix/apps/web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ node_modules
/.cache
/build
.env
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export function useLoginUser() {
return useMutation({
mutationFn: async (options: LoginUserOptions) => {
const response = await ApiClient.auth.authControllerLogin(options.data);

return response.data;
},
onSuccess: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export default function ThemeToggle({

const ToggleIcon: React.FC<LucideProps> = (props) =>
match(theme)
.with("light", () => <Sun {...props} />)
.with("dark", () => <Moon {...props} />)
.with("light", () => <Sun aria-label="Switch to lightmode" {...props} />)
.with("dark", () => <Moon aria-label="Switch to darkmode" {...props} />)
.exhaustive();

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { screen, fireEvent } from "@testing-library/react";

import ThemeToggle from "../ThemeToggle";
import { renderWith } from "../../../utils/testUtils";

describe("ThemeToggle", () => {
it("renders without crashing", () => {
renderWith({
withTheme: true,
}).render(<ThemeToggle />);

expect(screen.getByRole("button")).toBeInTheDocument();
});

it("toggles theme", async () => {
renderWith({
withTheme: true,
}).render(<ThemeToggle />);

const button = screen.getByRole("button");

expect(button).toBeInTheDocument();

fireEvent.click(button);
expect(await screen.findByLabelText(/dark/)).toBeInTheDocument();
fireEvent.click(button);
expect(await screen.findByLabelText(/light/)).toBeInTheDocument();
});
});
1 change: 1 addition & 0 deletions examples/common_nestjs_remix/apps/web/app/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vitest/globals" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { createRemixStub } from "@remix-run/testing";
import { screen, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import {
mockRemixReact,
mockedUseNavigate,
} from "~/utils/mocks/remix-run-mock";
import { renderWith } from "~/utils/testUtils";
import LoginPage from "./Login.page";

vi.mock("../../../api/api-client");

mockRemixReact();

describe("Login page", () => {
beforeEach(() => {
vi.resetAllMocks();
});

const RemixStub = createRemixStub([
{
path: "/",
Component: LoginPage,
},
]);

it("renders without crashing", () => {
renderWith({ withQuery: true }).render(<RemixStub />);

expect(screen.getByRole("heading", { name: "Login" })).toBeInTheDocument();
});

it("submits the form with valid data", async () => {
renderWith({ withQuery: true }).render(<RemixStub />);

const user = userEvent.setup();
await user.type(screen.getByLabelText("Email"), "[email protected]");
await user.type(screen.getByLabelText("Password"), "password123");
await user.click(screen.getByRole("button", { name: "Login" }));

await waitFor(() => {
expect(mockedUseNavigate).toHaveBeenCalledWith("/dashboard");
});
});
});
Loading

0 comments on commit 6fd0cd5

Please sign in to comment.