Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fe test setup #12

Merged
merged 16 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = "user@example.com";
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"), "test@example.com");
await user.type(screen.getByLabelText("Password"), "password123");
await user.click(screen.getByRole("button", { name: "Login" }));

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