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/testing approaches #61

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion apps/api/package.json
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.

appears when trying to run jest.

Also test:e2e shows a similar message:

Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?

Copy link
Member Author

@G0maa G0maa Oct 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bassiounix
The intention of this branch is not to test. The intention is to show developer working on Disworse HOW to test. With examples on Unit, E2E, and ((kind of)) integration tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also on the Jest error, I never managed to fix it, I usually silence it with --forceExit.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:e2e": "jest --config ./test/jest-e2e.json --forceExit",
"db:push": "drizzle-kit push --config=./drizzle.config.ts",
"db:generate": "drizzle-kit generate --config=./drizzle.config.ts",
"db:migrate": "drizzle-kit migrate --config=./drizzle.config.ts",
Expand All @@ -40,6 +40,7 @@
"drizzle-orm": "^0.33.0",
"express-session": "^1.18.0",
"graphql": "^16.9.0",
"graphql-tag": "^2.12.6",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"pg": "^8.13.0",
Expand Down
10 changes: 5 additions & 5 deletions apps/api/schema.graphql
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are that fields like created_at nullable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bassiounix iirc this is the type of the result of find by Drizzle.

Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@ type Mutation {
}

type Query {
db: String!
db: [User!]!
hello: String!
}

type User {
bio: String
coverImage: String
createdAt: String!
cover_image: String
created_at: String
deleted_at: String
dob: String!
email: String!
github_id: String
google_id: String
id: Int!
name: String!
profileImage: String
updatedAt: String!
profile_image: String
updated_at: String
username: String!
}
29 changes: 14 additions & 15 deletions apps/api/src/app.resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
import { ContextIdFactory } from "@nestjs/core";
import { Test, TestingModule } from "@nestjs/testing";
import { AppModule } from "./app.module";
import { TestManager } from "../test/TestManager";
import { AppResolver } from "./app.resolver";
import { AppService } from "./app.service";
import { DrizzleModule } from "./drizzle/drizzle.module";

describe("AppService", () => {
describe("[GraphQL] [IntegrationTesting] AppResolver", () => {
let testManager = new TestManager();
let appResolver: AppResolver;

beforeEach(async () => {
// TODO: create proper test module / class.
const app: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
beforeAll(async () => {
await testManager.beforeAll();

// TODO: are there a better handling for this? @xUser5000
const contextId = ContextIdFactory.create();
jest.spyOn(ContextIdFactory, "getByRequest").mockImplementation(
() => contextId,
);

appResolver = await app.resolve(AppResolver, contextId);
appResolver = await testManager.app.resolve(AppResolver, contextId);
});

describe("root", () => {
it('should return "Hello World!"', () => {
expect(appResolver.hello()).toBe("Hello World!");
});
afterAll(async () => {
await testManager.afterAll();
});

it('should return "Hello World!"', async () => {
const result = appResolver.hello();
expect(result).toBe("Hello World!");
});
});
5 changes: 3 additions & 2 deletions apps/api/src/app.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Query, Resolver } from "@nestjs/graphql";
import { AppService } from "./app.service";
import { Public } from "./common/custom-decorators/public-endpoint";
import { User } from "./modules/users/entities/user.entity";

@Resolver()
export class AppResolver {
Expand All @@ -12,8 +13,8 @@ export class AppResolver {
return this.appService.getHello();
}

@Query(() => String)
async db(): Promise<string> {
@Query(() => [User])
async db(): Promise<User[]> {
return this.appService.testDb();
}
}
47 changes: 47 additions & 0 deletions apps/api/src/app.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ContextIdFactory } from "@nestjs/core";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused imports!!!

import { Test, TestingModule } from "@nestjs/testing";
import { TestManager } from "../test/TestManager";
import { AppResolver } from "./app.resolver";
import { AppService } from "./app.service";
import { DrizzleService } from "./drizzle/drizzle.service";

describe("[GraphQL] [UnitTesting] AppService", () => {
let appService: AppService;
let drizzleService: DrizzleService;

// Note: we *shouldn't* (?) need TestManager for unit tests.
beforeAll(async () => {
const app: TestingModule = await Test.createTestingModule({
// Mocking can happen here (if appService has dependencies),
// or add specific mocks to each test case.
providers: [
AppService,
{
provide: DrizzleService,
useValue: {
db: {
query: {
users: {
findMany: jest.fn().mockReturnValue([]),
},
},
},
},
},
],
}).compile();

drizzleService = app.get<DrizzleService>(DrizzleService);
appService = app.get<AppService>(AppService);
});

afterAll(async () => {});

it('should return "Hello World!"', async () => {
const spy = jest.spyOn(drizzleService.db.query.users, "findMany");

const result = await appService.testDb();
expect(spy).toHaveBeenCalled();
expect(result).toStrictEqual([]);
});
});
5 changes: 3 additions & 2 deletions apps/api/src/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable } from "@nestjs/common";
import { DrizzleService } from "./drizzle/drizzle.service";
import { User } from "./modules/users/entities/user.entity";

@Injectable()
export class AppService {
Expand All @@ -9,8 +10,8 @@ export class AppService {
return "Hello World!";
}

async testDb(): Promise<string> {
async testDb(): Promise<User[]> {
const users = await this.drizzleService.db.query.users.findMany();
return `${users}`;
return users;
}
}
32 changes: 16 additions & 16 deletions apps/api/src/modules/users/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,27 @@ export class User {
@Field()
public dob: string;

@Field({ nullable: true })
public bio?: string;
@Field(() => String, { nullable: true })
public bio?: string | null;

@Field()
public createdAt: string;
@Field(() => String, { nullable: true })
public profile_image?: string;

@Field()
public updatedAt: string;
@Field(() => String, { nullable: true })
public cover_image?: string;

@Field({ nullable: true })
public profileImage?: string;
@Field(() => String, { nullable: true })
public google_id: string | null;

@Field({ nullable: true })
public coverImage?: string;
@Field(() => String, { nullable: true })
public github_id: string | null;

@Field({ nullable: true })
public google_id: string;
@Field(() => String, { nullable: true })
public deleted_at?: string | null;

@Field({ nullable: true })
public github_id: string;
@Field(() => String, { nullable: true })
public created_at?: string | null;

@Field({ nullable: true })
public deleted_at: string;
@Field(() => String, { nullable: true })
public updated_at?: string | null;
}
63 changes: 63 additions & 0 deletions apps/api/test/TestManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { INestApplication, ValidationPipe } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Test } from "@nestjs/testing";
import RedisStore from "connect-redis";
import * as session from "express-session";
import * as passport from "passport";
import { createClient } from "redis";
import { AppModule } from "../src/app.module";

export class TestManager {
// biome-ignore lint/suspicious/noExplicitAny: it is any.
public httpServer: any;
public app: INestApplication;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INestApplication<NestExpressApplication> could be more expressive. not mandatory.


// TODO: Find a way to abstract this logic, found in main.ts too.
async beforeAll(): Promise<void> {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
this.app = moduleRef.createNestApplication();

const configService = this.app.get(ConfigService);

this.app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
}),
);

const redisClient = await createClient({
url: String(configService.getOrThrow("REDIS_URL")),
}).connect();

this.app.use(
session({
secret: configService.getOrThrow<string>("SESSION_SECRET"),
resave: false,
saveUninitialized: false,
cookie: {
maxAge: configService.getOrThrow<number>("COOKIE_MAX_AGE"),
httpOnly: true,
},
store: new RedisStore({
client: redisClient,
}),
}),
Comment on lines +36 to +47

Check warning

Code scanning / CodeQL

Clear text transmission of sensitive cookie Medium test

Sensitive cookie sent without enforcing SSL encryption.

Copilot Autofix AI 19 days ago

To fix the problem, we need to ensure that the session cookie is only transmitted over HTTPS by setting the secure attribute to true. This change should be made in the session configuration object within the beforeAll method of the TestManager class.

  1. Locate the session configuration object in the beforeAll method.
  2. Add the secure: true attribute to the cookie object within the session configuration.
Suggested changeset 1
apps/api/test/TestManager.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/apps/api/test/TestManager.ts b/apps/api/test/TestManager.ts
--- a/apps/api/test/TestManager.ts
+++ b/apps/api/test/TestManager.ts
@@ -42,2 +42,3 @@
                     httpOnly: true,
+                    secure: true,
                 },
EOF
@@ -42,2 +42,3 @@
httpOnly: true,
secure: true,
},
Copilot is powered by AI and may make mistakes. Always verify output.
Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

);

this.app.use(passport.initialize());
this.app.use(passport.session());
this.app.enableCors();

this.httpServer = this.app.getHttpServer();
await this.app.init();
}

async afterAll() {
await this.app.close();
}

// Helper functions can be added here if needed e.g. generateUser().
}
18 changes: 9 additions & 9 deletions apps/api/test/app.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { INestApplication } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import * as request from "supertest";
import { AppModule } from "./../src/app.module";
import { TestManager } from "./TestManager";

describe("AppController (e2e)", () => {
describe("[GraphQL] [E2E] AppModule", () => {
const testManager = new TestManager();
let app: INestApplication;

beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
beforeAll(async () => {
await testManager.beforeAll();
app = testManager.app;
});

app = moduleFixture.createNestApplication();
await app.init();
afterAll(async () => {
await testManager.afterAll();
});

it("/graphql helloworld", () => {
Expand Down
4 changes: 3 additions & 1 deletion apps/api/test/jest-e2e.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"rootDir": "./../",
"globalSetup": "./test/setup.ts",
"globalTeardown": "./test/teardown.ts",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
Expand Down
23 changes: 23 additions & 0 deletions apps/api/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import "tsconfig-paths/register";

import { createClient } from "redis";
import { TestManager } from "./TestManager";

export default async (): Promise<void> => {
console.log("# Started Jest globalSetup.");
const testManager = new TestManager();

await testManager.beforeAll();

await testManager.app.init();

// TODO: Apply Database migrations/seeders.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are the db stuff?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... I can't recall why I didn't add them, I can work on them this weekend, but I prefer that we merge this anway.


await testManager.app.close();

// Delete records in redis.
const client = createClient();
await client.connect();
await client.flushAll();
console.log("# Finished Jest globalSetup.");
};
23 changes: 23 additions & 0 deletions apps/api/test/teardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import "tsconfig-paths/register";

import { createClient } from "redis";
import { TestManager } from "./TestManager";

export default async (): Promise<void> => {
console.log("# Started Jest globalTeardown.");
const testManager = new TestManager();

await testManager.beforeAll();

await testManager.app.init();

// TODO: Apply Database migrations/seeders.

await testManager.app.close();

// Delete records in redis.
const client = createClient();
await client.connect();
await client.flushAll();
console.log("# Finished Jest globalTeardown.");
};
Loading