diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/src/shared/database/database.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/src/shared/database/database.ts index 82c0eb494..3d9ad509e 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/src/shared/database/database.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/src/shared/database/database.ts @@ -1,18 +1,4 @@ import { PrismaClient } from "@prisma/client"; -import { User } from "@dddforum/shared/src/api/users"; -import { Post } from "@dddforum/shared/src/api/posts"; -import { CreateUserCommand } from "../../modules/users/usersCommand"; - -export interface UsersPersistence { - save(user: CreateUserCommand): Promise; - findUserByEmail(email: string): Promise; - findUserByUsername(username: string): Promise; -} - -export interface PostsPersistence { - findPosts(sort: string): Promise; -} - export interface Database { getConnection(): PrismaClient connect(): Promise; diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/package.json b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/package.json index 1b58c320f..3440b4300 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/package.json +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/package.json @@ -4,15 +4,13 @@ "description": "The backend for dddforum", "main": "index.js", "scripts": { - "clean": "rimraf ./dist", - "build": "npm run clean && tsc -b tsconfig.build.json && tsc-alias -p tsconfig.build.json -r ../../tsAliasReplacer.js", + "build": "tsc -b tsconfig.json && npm run generate", "generate": "ts-node prepareEnv.ts prisma generate --schema=./src/shared/database/prisma/schema.prisma", "migrate": "ts-node prepareEnv.ts prisma migrate dev --schema=./src/shared/database/prisma/schema.prisma", + "db:seed": "ts-node prepareEnv.ts prisma db seed --schema=./src/shared/database/prisma/schema.prisma", "db:reset": "ts-node prepareEnv.ts prisma migrate reset --preview-feature --schema src/shared/database/prisma/schema.prisma && npm run migrate && npm run generate", - "start:dev": "ts-node -r tsconfig-paths/register prepareDevCli.ts .env.development && dotenv -e .env.development -- nodemon", - "start:dev:no-watch": "npm run generate && npm run migrate && ts-node prepareEnv.ts ts-node src/index.ts", - "start:ci": "node dist/index.js", - "migrate:deploy": "npx prisma migrate deploy --schema src/shared/database/prisma/schema.prisma", + "start:dev": "npm run migrate && npm run generate && npx nodemon", + "start:dev:no-watch": "npm run migrate && npm run generate && ts-node prepareEnv.ts ts-node src/index.ts", "lint": "eslint . --ext .ts --fix", "test": "jest", "test:dev": "jest --watchAll", @@ -21,14 +19,16 @@ "test:infra": "jest -c jest.config.infra.ts", "test:infra:dev": "jest -c jest.config.infra.ts --watch", "test:unit": "jest -c jest.config.unit.ts", - "test:unit:dev": "jest -c jest.config.unit.ts --watchAll", - "test:staging": "npm run generate && npm run migrate && npm run test:e2e" + "test:unit:dev": "jest -c jest.config.unit.ts --watchAll" }, "husky": { "hooks": { "pre-commit": "npm run test && npm run prettier-format && npm run lint" } }, + "prisma": { + "seed": "ts-node ./src/shared/database/prisma/seed.ts" + }, "keywords": [], "author": "", "license": "ISC", diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/adapters/inMemoryUserRepositorySpy.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/adapters/inMemoryUserRepositorySpy.ts index 3921f3d86..bb9954748 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/adapters/inMemoryUserRepositorySpy.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/adapters/inMemoryUserRepositorySpy.ts @@ -1,7 +1,8 @@ -import { User } from "@dddforum/shared/src/api/users"; +import { ValidatedUser } from "@dddforum/shared/src/api/users"; import { Spy } from "../../../shared/testDoubles/spy"; import { UsersRepository } from "../ports/usersRepository"; import { CreateUserCommand } from "../usersCommand"; +import { User } from "@prisma/client"; export class InMemoryUserRepositorySpy extends Spy @@ -14,11 +15,12 @@ export class InMemoryUserRepositorySpy this.users = []; } - save(user: CreateUserCommand): Promise { + save(user: ValidatedUser): Promise { this.addCall("save", [user]); - const newUser: User = { - ...user.props, + const newUser = { + ...user, id: this.users.length > 0 ? this.users[this.users.length - 1].id + 1 : 1, + password: '', }; this.users.push(newUser); return Promise.resolve({ ...newUser, password: "password" }); diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/adapters/productionUserRepository.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/adapters/productionUserRepository.ts index 9d76102a2..930676355 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/adapters/productionUserRepository.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/adapters/productionUserRepository.ts @@ -1,8 +1,8 @@ -import { PrismaClient } from "@prisma/client"; + +import { PrismaClient, User } from "@prisma/client"; import { UsersRepository } from "../ports/usersRepository"; -import { User } from "@dddforum/shared/src/api/users"; -import { CreateUserCommand } from "../usersCommand"; import { generateRandomPassword } from "../../../shared/utils"; +import { ValidatedUser } from "@dddforum/shared/src/api/users"; export class ProductionUserRepository implements UsersRepository { constructor(private prisma: PrismaClient) {} @@ -27,7 +27,7 @@ export class ProductionUserRepository implements UsersRepository { } } - async save(userData: CreateUserCommand) { + async save(userData: ValidatedUser) { const { email, firstName, lastName, username } = userData; return await this.prisma.$transaction(async () => { const user = await this.prisma.user.create({ @@ -74,7 +74,7 @@ export class ProductionUserRepository implements UsersRepository { async update( id: number, - props: Partial, + props: Partial, ): Promise { const prismaUser = await this.prisma.user.update({ where: { id }, diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/ports/userRepository.infra.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/ports/userRepository.infra.ts new file mode 100644 index 000000000..b8aabecb6 --- /dev/null +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/ports/userRepository.infra.ts @@ -0,0 +1,55 @@ + +import { PrismaClient } from "@prisma/client"; +import { ProductionUserRepository } from "../adapters/productionUserRepository"; +import { UserBuilder } from '@dddforum/shared/tests/support/builders/users' +import { UsersRepository } from "./usersRepository"; +import { InMemoryUserRepositorySpy } from "../adapters/inMemoryUserRepositorySpy"; + +describe("userRepo", () => { + let userRepos: UsersRepository[] = [ + new ProductionUserRepository(new PrismaClient()), + new InMemoryUserRepositorySpy() + ]; + + it("can save and retrieve users by email", () => { + let createUserInput = new UserBuilder() + .makeValidatedUserBuilder() + .withAllRandomDetails() + .build() + + userRepos.forEach(async (userRepo) => { + let savedUserResult = await userRepo.save({ + ...createUserInput, + password: '', + }); + let fetchedUserResult = await userRepo.findUserByEmail( + createUserInput.email, + ); + + expect(savedUserResult).toBeDefined(); + expect(fetchedUserResult).toBeDefined(); + expect(savedUserResult.email).toEqual(fetchedUserResult?.email); + }); + }); + + it("can find a user by username", () => { + let createUserInput = new UserBuilder() + .makeValidatedUserBuilder() + .withAllRandomDetails() + .build(); + + userRepos.forEach(async (userRepo) => { + let savedUserResult = await userRepo.save({ + ...createUserInput, + password: "", + }); + let fetchedUserResult = await userRepo.findUserByUsername( + createUserInput.username, + ); + + expect(savedUserResult).toBeDefined(); + expect(fetchedUserResult).toBeDefined(); + expect(savedUserResult.username).toEqual(fetchedUserResult?.username); + }); + }); +}); diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/ports/usersRepository.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/ports/usersRepository.ts index d201d1018..e808b7c16 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/ports/usersRepository.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/ports/usersRepository.ts @@ -1,14 +1,12 @@ -import { User } from "@dddforum/shared/src/api/users"; -import { CreateUserCommand } from "../usersCommand"; + +import { ValidatedUser } from "@dddforum/shared/src/api/users"; +import { User } from "@prisma/client"; export interface UsersRepository { findUserByEmail(email: string): Promise; - // @note The ideal return type here is a domain object, not a DTO. For - // demonstration purposes, we've kept it intentionally simple to focus on testing. - // @see Pattern-First for domain objects - save(user: CreateUserCommand): Promise; + save(user: ValidatedUser): Promise; findById(id: number): Promise; delete(email: string): Promise; findUserByUsername(username: string): Promise; - update(id: number, props: Partial): Promise; + update(id: number, props: Partial): Promise; } diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/usersService.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/usersService.ts index ab06c0703..c40bb9639 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/usersService.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/modules/users/usersService.ts @@ -4,9 +4,11 @@ import { UserNotFoundException, UsernameAlreadyTakenException, } from "./usersExceptions"; -import { User } from "@dddforum/shared/src/api/users"; +import { ValidatedUser } from "@dddforum/shared/src/api/users"; import { TransactionalEmailAPI } from "../notifications/ports/transactionalEmailAPI"; import { UsersRepository } from "./ports/usersRepository"; +import { TextUtil } from "@dddforum/shared/src/utils/textUtil"; + export class UsersService { constructor( @@ -14,7 +16,7 @@ export class UsersService { private emailAPI: TransactionalEmailAPI, ) {} - async createUser(userData: CreateUserCommand): Promise { + async createUser(userData: CreateUserCommand) { const existingUserByEmail = await this.repository.findUserByEmail( userData.email, ); @@ -28,25 +30,31 @@ export class UsersService { if (existingUserByUsername) { throw new UsernameAlreadyTakenException(userData.username); } - const { password, ...user } = await this.repository.save(userData); + + const validatedUser: ValidatedUser = { + ...userData.props, + password: TextUtil.createRandomText(10) + } + + const prismaUser = await this.repository.save(validatedUser); await this.emailAPI.sendMail({ - to: user.email, + to: validatedUser.email, subject: "Your login details to DDDForum", text: `Welcome to DDDForum. You can login with the following details
- email: ${user.email} - password: ${password}`, + email: ${validatedUser.email} + password: ${validatedUser.password}`, }); - return user; + return prismaUser; } async getUserByEmail(email: string) { - const user = await this.repository.findUserByEmail(email); - if (!user) { + const prismaUser = await this.repository.findUserByEmail(email); + if (!prismaUser) { throw new UserNotFoundException(email); } - return user; + return prismaUser; } async deleteUser(email: string) { diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/database.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/database.ts index 126c6959f..3d9ad509e 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/database.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/database.ts @@ -1,5 +1,4 @@ import { PrismaClient } from "@prisma/client"; - export interface Database { getConnection(): PrismaClient connect(): Promise; diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/prisma/migrations/20231125035334_initial/migration.sql b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/prisma/migrations/20231125035334_initial/migration.sql new file mode 100644 index 000000000..ed40ccad3 --- /dev/null +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/prisma/migrations/20231125035334_initial/migration.sql @@ -0,0 +1,55 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "email" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "username" TEXT NOT NULL, + "password" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "Member" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "userId" INTEGER NOT NULL, + CONSTRAINT "Member_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Post" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "memberId" INTEGER NOT NULL, + "postType" TEXT NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "dateCreated" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Post_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "Member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Comment" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "postId" INTEGER NOT NULL, + "text" TEXT NOT NULL, + "memberId" INTEGER NOT NULL, + "parentCommentId" INTEGER, + CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Comment_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "Member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Comment_parentCommentId_fkey" FOREIGN KEY ("parentCommentId") REFERENCES "Comment" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Vote" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "postId" INTEGER NOT NULL, + "memberId" INTEGER NOT NULL, + "voteType" TEXT NOT NULL, + CONSTRAINT "Vote_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Vote_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "Member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Member_userId_key" ON "Member"("userId"); diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/prisma/migrations/migration_lock.toml b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/prisma/migrations/migration_lock.toml index fbffa92c2..e5e5c4705 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/prisma/migrations/migration_lock.toml +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file +provider = "sqlite" \ No newline at end of file diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/prisma/schema.prisma b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/prisma/schema.prisma index 50433cfe4..4bae5e562 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/prisma/schema.prisma +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/src/shared/database/prisma/schema.prisma @@ -1,4 +1,3 @@ - // This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema @@ -7,8 +6,8 @@ generator client { } datasource db { - provider = "postgresql" - url = env("DATABASE_URL") + provider = "sqlite" + url = "file:./../dev.db" // Replace with your SQLite file path } model User { @@ -20,17 +19,17 @@ model User { password String // Define a one-to-one relationship with Member - member Member? + member Member? } model Member { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique // relation scalar field (used in the `@relation` attribute above) - posts Post[] - votes Vote[] - comments Comment[] + user User @relation(fields: [userId], references: [id]) + userId Int @unique // relation scalar field (used in the `@relation` attribute above) + posts Post[] + votes Vote[] + comments Comment[] } model Post { @@ -46,29 +45,29 @@ model Post { } model Comment { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) // Foreign key relation to the Post table postId Int // Foreign key to the Post table - post Post @relation(fields: [postId], references: [id]) - text String + post Post @relation(fields: [postId], references: [id]) + text String - memberId Int // Foreign key to the Member table - memberPostedBy Member @relation(fields: [memberId], references: [id]) + memberId Int // Foreign key to the Member table + memberPostedBy Member @relation(fields: [memberId], references: [id]) - parentCommentId Int? - parentComment Comment? @relation("Replies", fields: [parentCommentId], references: [id]) - replyComments Comment[] @relation("Replies") + parentCommentId Int? + parentComment Comment? @relation("Replies", fields: [parentCommentId], references: [id]) + replyComments Comment[] @relation("Replies") } model Vote { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) postId Int // Foreign key to the Post table postBelongsTo Post @relation(fields: [postId], references: [id]) - memberId Int // Foreign key to the Member table + memberId Int // Foreign key to the Member table memberPostedBy Member @relation(fields: [memberId], references: [id]) - voteType String // 'Upvote' or 'Downvote' + voteType String // 'Upvote' or 'Downvote' } diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/api/registration.api.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/api/registration.api.infra.ts similarity index 69% rename from ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/api/registration.api.ts rename to ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/api/registration.api.infra.ts index a32534d14..c98576532 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/api/registration.api.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/api/registration.api.infra.ts @@ -1,13 +1,12 @@ import { createAPIClient } from "@dddforum/shared/src/api"; -import { UserResponseStub } from "@dddforum/shared/tests/support/stubs/userResponseStub"; -import { CreateUserBuilder } from "@dddforum/shared/tests/support/builders/createUserBuilder"; +import { UserBuilder } from "@dddforum/shared/tests/support/builders/users"; import { CompositionRoot } from "../../src/shared/compositionRoot"; import { Config } from "../../src/shared/config"; describe("users http API", () => { - const config: Config = new Config("test:infra"); - const client = createAPIClient(config.getAPIURL()); + const client = createAPIClient("http://localhost:3000"); + const config = new Config("test:infra"); const composition = CompositionRoot.createCompositionRoot(config); const server = composition.getWebServer(); @@ -30,14 +29,19 @@ describe("users http API", () => { }); it("can create users", async () => { - const createUserParams = new CreateUserBuilder() + const createUserParams = new UserBuilder() + .makeCreateUserCommandBuilder() .withAllRandomDetails() .withFirstName("Khalil") .withLastName("Stemmler") .build(); - const createUserResponseStub = new UserResponseStub() - .fromParams(createUserParams) + const createUserResponseStub = new UserBuilder() + .makeValidatedUserBuilder() + .withEmail(createUserParams.email) + .withFirstName(createUserParams.firstName) + .withLastName(createUserParams.lastName) + .withUsername(createUserParams.username) .build(); createUserSpy.mockResolvedValue(createUserResponseStub); diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/features/registration/registration.e2e.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/features/registration/registration.e2e.ts index 9a9b85775..b9279e873 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/features/registration/registration.e2e.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/features/registration/registration.e2e.ts @@ -1,9 +1,12 @@ import * as path from "path"; import { defineFeature, loadFeature } from "jest-cucumber"; import { sharedTestRoot } from "@dddforum/shared/src/paths"; -import { CreateUserBuilder } from "@dddforum/shared/tests/support/builders/createUserBuilder"; +import { UserBuilder } from "@dddforum/shared/tests/support/builders/users"; import { DatabaseFixture } from "@dddforum/shared/tests/support/fixtures/databaseFixture"; -import { CreateUserParams, CreateUserResponse } from "@dddforum/shared/src/api/users"; +import { + CreateUserParams, + CreateUserResponse, +} from "@dddforum/shared/src/api/users"; import { createAPIClient } from "@dddforum/shared/src/api"; import { CompositionRoot } from "@dddforum/backend/src/shared/compositionRoot"; import { WebServer } from "@dddforum/backend/src/shared/http/webServer"; @@ -17,17 +20,14 @@ const feature = loadFeature( defineFeature(feature, (test) => { let databaseFixture: DatabaseFixture; - let composition: CompositionRoot - let server: WebServer + const apiClient = createAPIClient("http://localhost:3000"); + let composition: CompositionRoot; + let server: WebServer; const config: Config = new Config("test:e2e"); - const apiClient = createAPIClient(config.getAPIURL()); - - let response: CreateUserResponse + let response: CreateUserResponse; let createUserResponses: CreateUserResponse[] = []; let addEmailToListResponse: AddEmailToListResponse; - let dbConnection: Database - - + let dbConnection: Database; beforeAll(async () => { composition = CompositionRoot.createCompositionRoot(config); @@ -41,28 +41,39 @@ defineFeature(feature, (test) => { afterEach(async () => { await databaseFixture.resetDatabase(); - createUserResponses = [] - }, 10000); + createUserResponses = []; + }); afterAll(async () => { await server.stop(); }); - test('Successful registration with marketing emails accepted', ({ given, when, then, and }) => { + test("Successful registration with marketing emails accepted", ({ + given, + when, + then, + and, + }) => { let user: CreateUserParams; - - given('I am a new user', async () => { - user = new CreateUserBuilder() + + given("I am a new user", async () => { + user = new UserBuilder() + .makeCreateUserCommandBuilder() .withAllRandomDetails() .build(); }); - when('I register with valid account details accepting marketing emails', async () => { - response = await apiClient.users.register(user); - addEmailToListResponse = await apiClient.marketing.addEmailToList(user.email); - }); + when( + "I register with valid account details accepting marketing emails", + async () => { + response = await apiClient.users.register(user); + addEmailToListResponse = await apiClient.marketing.addEmailToList( + user.email, + ); + }, + ); - then('I should be granted access to my account', async () => { + then("I should be granted access to my account", async () => { const { data, success, error } = response; // Expect a successful response (Result Verification) @@ -76,34 +87,44 @@ defineFeature(feature, (test) => { // And the user exists (State Verification) const getUserResponse = await apiClient.users.getUserByEmail(user.email); - const {data: getUserData} = getUserResponse; + const { data: getUserData } = getUserResponse; expect(user.email).toEqual(getUserData!.email); }); - and('I should expect to receive marketing emails', () => { + and("I should expect to receive marketing emails", () => { // How can we test this? what do we want to place under test? // Well, what's the tool they'll use? mailchimp? // And do we want to expect that mailchimp is going to get called to add - // a new contact to a list? Yes, we do. But we're not going to worry + // a new contact to a list? Yes, we do. But we're not going to worry // about this yet because we need to learn how to validate this without - // filling up a production Mailchimp account with test data. - const { success } = addEmailToListResponse + // filling up a production Mailchimp account with test data. + const { success } = addEmailToListResponse; expect(success).toBeTruthy(); }); }); - test("Successful registration without marketing emails accepted", ({ given, when, then, and }) => { + test("Successful registration without marketing emails accepted", ({ + given, + when, + then, + and, + }) => { let user: CreateUserParams; - given("I am a new user", () => { - user = new CreateUserBuilder().withAllRandomDetails().build(); + user = new UserBuilder() + .makeCreateUserCommandBuilder() + .withAllRandomDetails() + .build(); }); - when("I register with valid account details declining marketing emails", async () => { - response = await apiClient.users.register(user); - }); + when( + "I register with valid account details declining marketing emails", + async () => { + response = await apiClient.users.register(user); + }, + ); then("I should be granted access to my account", () => { expect(response.success).toBe(true); @@ -116,7 +137,7 @@ defineFeature(feature, (test) => { }); and("I should not expect to receive marketing emails", () => { - const { success } = addEmailToListResponse + const { success } = addEmailToListResponse; expect(success).toBeTruthy(); // How can we test this? what do we want to place under test? @@ -133,7 +154,10 @@ defineFeature(feature, (test) => { let user: any; given("I am a new user", () => { - const validUser = new CreateUserBuilder().withAllRandomDetails().build(); + const validUser = new UserBuilder() + .makeCreateUserCommandBuilder() + .withAllRandomDetails() + .build(); user = { firstName: validUser.firstName, @@ -164,7 +188,8 @@ defineFeature(feature, (test) => { given("a set of users already created accounts", async (table) => { existingUsers = table.map((row: any) => { - return new CreateUserBuilder() + return new UserBuilder() + .makeCreateUserCommandBuilder() .withFirstName(row.firstName) .withLastName(row.lastName) .withEmail(row.email) @@ -209,7 +234,8 @@ defineFeature(feature, (test) => { "a set of users have already created their accounts with valid details", async (table) => { existingUsers = table.map((row: any) => { - return new CreateUserBuilder() + return new UserBuilder() + .makeCreateUserCommandBuilder() .withFirstName(row.firstName) .withLastName(row.lastName) .withEmail(row.email) @@ -224,7 +250,8 @@ defineFeature(feature, (test) => { "new users attempt to register with already taken usernames", async (table) => { const newUsers: CreateUserParams[] = table.map((row: any) => { - return new CreateUserBuilder() + return new UserBuilder() + .makeCreateUserCommandBuilder() .withFirstName(row.firstName) .withLastName(row.lastName) .withEmail(row.email) diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/features/registration/registration.infra.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/features/registration/registration.infra.ts index e2684f323..2f3d8405a 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/features/registration/registration.infra.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/features/registration/registration.infra.ts @@ -2,19 +2,17 @@ import { defineFeature, loadFeature } from 'jest-cucumber'; import * as path from 'path'; import { sharedTestRoot } from '@dddforum/shared/src/paths'; import { CompositionRoot } from '../../../src/shared/compositionRoot'; +import { TransactionalEmailAPISpy } from '../../../src/modules/notifications/adapters/transactionalEmailAPI/transactionalEmailAPISpy'; import { ContactListAPISpy } from '../../../src/modules/marketing/adapters/contactListAPI/contactListSpy'; import { Config } from '../../../src/shared/config'; -import { CreateUserBuilder } from '@dddforum/shared/tests/support/builders/createUserBuilder'; +import { UserBuilder } from '@dddforum/shared/tests/support/builders/users'; import { Application } from '../../../src/shared/application/applicationInterface'; import { CreateUserCommand } from '../../../src/modules/users/usersCommand'; import { DatabaseFixture } from '@dddforum/shared/tests/support/fixtures/databaseFixture'; import { CreateUserParams } from '@dddforum/shared/src/api/users'; -import { TransactionalEmailAPISpy } from '@dddforum/backend/src/modules/notifications/adapters/transactionalEmailAPI/transactionalEmailAPISpy'; - const feature = loadFeature(path.join(sharedTestRoot, 'features/registration.feature'), { tagFilter: '@backend' }); - defineFeature(feature, (test) => { let createUserCommand: CreateUserCommand; let userResponse: any; @@ -46,7 +44,8 @@ defineFeature(feature, (test) => { test('Successful registration with marketing emails accepted', ({ given, when, then, and }) => { given('I am a new user', async () => { - createUserCommand = new CreateUserBuilder() + createUserCommand = new UserBuilder() + .makeCreateUserCommandBuilder() .withAllRandomDetails() .buildCommand() }) @@ -88,7 +87,7 @@ defineFeature(feature, (test) => { test('Successful registration without marketing emails accepted', ({ given, when, then, and }) => { given('I am a new user', () => { - createUserCommand = new CreateUserBuilder().withAllRandomDetails().buildCommand() + createUserCommand = new UserBuilder().makeCreateUserCommandBuilder().withAllRandomDetails().buildCommand() }); when('I register with valid account details declining marketing emails', async () => { @@ -122,7 +121,7 @@ defineFeature(feature, (test) => { let params: CreateUserParams; let error: any; given('I am a new user', () => { - params = new CreateUserBuilder() + params = new UserBuilder().makeCreateUserCommandBuilder() .withAllRandomDetails() .withLastName('') .build(); @@ -150,7 +149,7 @@ defineFeature(feature, (test) => { test('Account already created with email', ({ given, when, then, and }) => { given('a set of users already created accounts', async (table) => { table.forEach((item: any) => { - commands.push(new CreateUserBuilder() + commands.push(new UserBuilder().makeCreateUserCommandBuilder() .withAllRandomDetails() .withFirstName(item.firstName) .withLastName(item.lastName) @@ -186,7 +185,7 @@ defineFeature(feature, (test) => { test('Username already taken', ({ given, when, then, and }) => { given('a set of users have already created their accounts with valid details', async (table) => { table.forEach((item: any) => { - commands.push(new CreateUserBuilder() + commands.push(new UserBuilder().makeCreateUserCommandBuilder() .withUsername(item.username) .withFirstName(item.firstName) .withLastName(item.lastName) @@ -219,4 +218,4 @@ defineFeature(feature, (test) => { expect(transactionalEmailAPISpy.getTimesMethodCalled('sendMail')).toEqual(0); }); }); -}) \ No newline at end of file +}) diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/features/registration/registration.unit.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/features/registration/registration.unit.ts index 830372b72..c23c4330c 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/features/registration/registration.unit.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/backend/tests/features/registration/registration.unit.ts @@ -4,229 +4,220 @@ import * as path from 'path'; import { sharedTestRoot } from '@dddforum/shared/src/paths'; import { CreateUserCommand } from '../../../src/modules/users/usersCommand'; import { CompositionRoot } from '../../../src/shared/compositionRoot'; +import { TransactionalEmailAPISpy } from '../../../src/modules/notifications/adapters/transactionalEmailAPI/transactionalEmailAPISpy'; import { ContactListAPISpy } from '../../../src/modules/marketing/adapters/contactListAPI/contactListSpy'; import { Application } from '../../../src/shared/application/applicationInterface'; import { InMemoryUserRepositorySpy } from '../../../src/modules/users/adapters/inMemoryUserRepositorySpy'; import { Config } from '../../../src/shared/config'; -import { CreateUserBuilder } from '@dddforum/shared/tests/support/builders/createUserBuilder'; +import { UserBuilder } from "@dddforum/shared/tests/support/builders/users"; import { DatabaseFixture } from '@dddforum/shared/tests/support/fixtures/databaseFixture'; import { CreateUserParams } from '@dddforum/shared/src/api/users'; -import { TransactionalEmailAPISpy } from '@dddforum/backend/src/modules/notifications/adapters/transactionalEmailAPI/transactionalEmailAPISpy'; const feature = loadFeature(path.join(sharedTestRoot, 'features/registration.feature'), { tagFilter: '@backend' }); defineFeature(feature, (test) => { - let createUserCommand: CreateUserCommand; - let createUserResponse: any; - let addEmailToListResponse: boolean | undefined; - let composition: CompositionRoot; - let transactionalEmailAPISpy: TransactionalEmailAPISpy; - let contactListAPISpy: ContactListAPISpy; - let application: Application; - let userRepoSpy: InMemoryUserRepositorySpy; - let commands: CreateUserCommand[] = []; - let createUserResponses: any[] = []; - let databaseFixture: DatabaseFixture; - - beforeAll(async () => { - composition = CompositionRoot.createCompositionRoot(new Config('test:unit')); - application = composition.getApplication(); - contactListAPISpy = composition.getContactListAPI() as ContactListAPISpy; - transactionalEmailAPISpy = composition.getTransactionalEmailAPI() as TransactionalEmailAPISpy; - userRepoSpy = composition.getRepositories().users as InMemoryUserRepositorySpy; - createUserResponses = [] - databaseFixture = new DatabaseFixture(composition); - }) + let createUserCommand: CreateUserCommand; + let createUserResponse: any; + let addEmailToListResponse: boolean | undefined; + let composition: CompositionRoot; + let transactionalEmailAPISpy: TransactionalEmailAPISpy; + let contactListAPISpy: ContactListAPISpy; + let application: Application; + let userRepoSpy: InMemoryUserRepositorySpy; + let commands: CreateUserCommand[] = []; + let createUserResponses: any[] = []; + let databaseFixture: DatabaseFixture; + + beforeAll(async () => { + composition = CompositionRoot.createCompositionRoot(new Config('test:unit')); + application = composition.getApplication(); + contactListAPISpy = composition.getContactListAPI() as ContactListAPISpy; + transactionalEmailAPISpy = composition.getTransactionalEmailAPI() as TransactionalEmailAPISpy; + userRepoSpy = composition.getRepositories().users as InMemoryUserRepositorySpy; + createUserResponses = [] + databaseFixture = new DatabaseFixture(composition); + }) + + afterEach(async () => { + contactListAPISpy.reset(); + transactionalEmailAPISpy.reset(); + commands = []; + createUserResponses = []; + addEmailToListResponse = undefined; + await userRepoSpy.reset(); + }); + + test('Successful registration with marketing emails accepted', ({ given, when, then, and }) => { + given('I am a new user', async () => { + createUserCommand = new UserBuilder().makeCreateUserCommandBuilder() + .withAllRandomDetails() + .withFirstName('Khalil') + .withLastName('Stemmler') + .buildCommand(); + }); - afterEach(async () => { - contactListAPISpy.reset(); - transactionalEmailAPISpy.reset(); - commands = []; - createUserResponses = []; - addEmailToListResponse = undefined; - await userRepoSpy.reset(); - }); - - test('Successful registration with marketing emails accepted', ({ given, when, then, and }) => { - - given('I am a new user', async () => { - createUserCommand = new CreateUserBuilder() - .withAllRandomDetails() - .withFirstName('Khalil') - .withLastName('Stemmler') - .buildCommand(); - }); - - when('I register with valid account details accepting marketing emails', async () => { - createUserResponse = await application.users.createUser(createUserCommand); - addEmailToListResponse = await application.marketing.addEmailToList(createUserCommand.email); - }); - - then('I should be granted access to my account', async () => { - expect(createUserResponse.id).toBeDefined(); - expect(createUserResponse.email).toEqual(createUserCommand.email); - expect(createUserResponse.firstName).toEqual(createUserCommand.firstName); - expect(createUserResponse.lastName).toEqual(createUserCommand.lastName); - expect(createUserResponse.username).toEqual(createUserCommand.username); - - // And the user exists (State Verification) - const getUserResponse = await application.users.getUserByEmail(createUserCommand.email); - expect(createUserCommand.email).toEqual(getUserResponse.email); - - expect(userRepoSpy.getTimesMethodCalled('save')).toEqual(1); - - // Verify that an email has been sent (Communication Verification) - expect(transactionalEmailAPISpy.getTimesMethodCalled('sendMail')).toEqual(1); - }) - - and('I should expect to receive marketing emails', () => { - // How can we test this? what do we want to place under test? - // Well, what's the tool they'll use? mailchimp? - // And do we want to expect that mailchimp is going to get called to add - // a new contact to a list? Yes, we do. But we're not going to worry - // about this yet because we need to learn how to validate this without - // filling up a production Mailchimp account with test data. + when('I register with valid account details accepting marketing emails', async () => { + createUserResponse = await application.users.createUser(createUserCommand); + addEmailToListResponse = await application.marketing.addEmailToList(createUserCommand.email); + }); + + then('I should be granted access to my account', async () => { + expect(createUserResponse.id).toBeDefined(); + expect(createUserResponse.email).toEqual(createUserCommand.email); + expect(createUserResponse.firstName).toEqual(createUserCommand.firstName); + expect(createUserResponse.lastName).toEqual(createUserCommand.lastName); + expect(createUserResponse.username).toEqual(createUserCommand.username); - expect(addEmailToListResponse).toBeTruthy(); - expect(contactListAPISpy.getTimesMethodCalled('addEmailToList')).toEqual(1); - }); + // And the user exists (State Verification) + const getUserResponse = await application.users.getUserByEmail(createUserCommand.email); + expect(createUserCommand.email).toEqual(getUserResponse.email); + + expect(userRepoSpy.getTimesMethodCalled('save')).toEqual(1); + + // Verify that an email has been sent (Communication Verification) + expect(transactionalEmailAPISpy.getTimesMethodCalled('sendMail')).toEqual(1); + }) + + and('I should expect to receive marketing emails', () => { + expect(addEmailToListResponse).toBeTruthy(); + expect(contactListAPISpy.getTimesMethodCalled('addEmailToList')).toEqual(1); + }); + }) + + test('Successful registration without marketing emails accepted', ({ given, when, then, and }) => { + given('I am a new user', () => { + createUserCommand = new UserBuilder().makeCreateUserCommandBuilder() + .withAllRandomDetails() + .withFirstName('Khalil') + .withLastName('Stemmler') + .buildCommand() }) - test('Successful registration without marketing emails accepted', ({ given, when, then, and }) => { - given('I am a new user', () => { - createUserCommand = new CreateUserBuilder() - .withAllRandomDetails() - .withFirstName('Khalil') - .withLastName('Stemmler') - .buildCommand() - }) - - - when('I register with valid account details declining marketing emails', async () => { - createUserResponse = await application.users.createUser(createUserCommand); - }); - - then('I should be granted access to my account', async () => { - - expect(createUserResponse.id).toBeDefined(); - expect(createUserResponse.email).toEqual(createUserCommand.email); - expect(createUserResponse.firstName).toEqual(createUserCommand.firstName); - expect(createUserResponse.lastName).toEqual(createUserCommand.lastName); - expect(createUserResponse.username).toEqual(createUserCommand.username); - - expect(userRepoSpy.getTimesMethodCalled('save')).toEqual(1); - - // And the user exists (State Verification) - const getUserResponse = await application.users.getUserByEmail(createUserCommand.email); - expect(createUserCommand.email).toEqual(getUserResponse.email); - - - // Verify that an email has been sent (Communication Verification) - expect(transactionalEmailAPISpy.getTimesMethodCalled('sendMail')).toEqual(1); - }); - - and('I should not expect to receive marketing emails', () => { - expect(addEmailToListResponse).toBeFalsy(); - expect(contactListAPISpy.getTimesMethodCalled('addEmailToList')).toEqual(0); - }); - }); - - test('Invalid or missing registration details', ({ given, when, then, and }) => { - let params: CreateUserParams; - let error: any; - given('I am a new user', () => { - params = new CreateUserBuilder() - .withAllRandomDetails() - .withLastName('') - .build(); - }); - - when('I register with invalid account details', async () => { - try { - createUserCommand = CreateUserCommand.fromProps(params); - await application.users.createUser(createUserCommand); - } catch (e) { - error = e; - } - }); - - then('I should see an error notifying me that my input is invalid', async () => { - expect(userRepoSpy.getTimesMethodCalled('save')).toEqual(0); - expect(error).toBeDefined(); - }); - - and('I should not have been sent access to account details', () => { - expect(transactionalEmailAPISpy.getTimesMethodCalled('sendMail')).toEqual(0); - }); - }); - - test('Username already taken', ({ given, when, then, and }) => { - given('a set of users have already created their accounts with valid details', async (table) => { - table.forEach((item: any) => { - commands.push(new CreateUserBuilder() - .withFirstName(item.firstName) - .withLastName(item.lastName) - .withUsername(item.username) - .withEmail(item.email) - .buildCommand() - ) - }); - await databaseFixture.setupWithExistingUsersFromCommands(commands); - transactionalEmailAPISpy.reset(); - }); - - when('new users attempt to register with already taken usernames', async (table) => { - for (const item of table) { - const response = application.users.createUser(item); - createUserResponses.push(response); - } - }); - - then('they see an error notifying them that the username has already been taken', () => { - for (const response of createUserResponses) { - expect(response).rejects.toThrow(expect.objectContaining({ - type: 'UsernameAlreadyTakenException' - })); - } - }); - - and('they should not have been sent access to account details', () => { - expect(transactionalEmailAPISpy.getTimesMethodCalled('sendMail')).toEqual(0); - }); - }); - - test('Account already created with email', ({ given, when, then, and }) => { - given('a set of users already created accounts', async (table) => { - table.forEach((item: any) => { - commands.push(new CreateUserBuilder() - .withUsername(item.username) - .withFirstName(item.firstName) - .withLastName(item.lastName) - .withEmail(item.email) - .buildCommand() - ); - }) - await databaseFixture.setupWithExistingUsersFromCommands(commands); - transactionalEmailAPISpy.reset(); - }); - - when('new users attempt to register with those emails', async () => { - for (const command of commands) { - const response = application.users.createUser(command); - createUserResponses.push(response); - } - }); - - then('they should see an error notifying them that the account already exists', async () => { - for (const response of createUserResponses) { - expect(response).rejects.toThrow(expect.objectContaining({ - type: 'EmailAlreadyInUseException' - })); - } - }); - - and('they should not have been sent access to account details', () => { - expect(transactionalEmailAPISpy.getTimesMethodCalled('sendMail')).toEqual(0); - }); - }); -}) \ No newline at end of file + + when('I register with valid account details declining marketing emails', async () => { + createUserResponse = await application.users.createUser(createUserCommand); + }); + + then('I should be granted access to my account', async () => { + expect(createUserResponse.id).toBeDefined(); + expect(createUserResponse.email).toEqual(createUserCommand.email); + expect(createUserResponse.firstName).toEqual(createUserCommand.firstName); + expect(createUserResponse.lastName).toEqual(createUserCommand.lastName); + expect(createUserResponse.username).toEqual(createUserCommand.username); + + expect(userRepoSpy.getTimesMethodCalled('save')).toEqual(1); + + // And the user exists (State Verification) + const getUserResponse = await application.users.getUserByEmail(createUserCommand.email); + expect(createUserCommand.email).toEqual(getUserResponse.email); + + + // Verify that an email has been sent (Communication Verification) + expect(transactionalEmailAPISpy.getTimesMethodCalled('sendMail')).toEqual(1); + }); + + and('I should not expect to receive marketing emails', () => { + expect(addEmailToListResponse).toBeFalsy(); + expect(contactListAPISpy.getTimesMethodCalled('addEmailToList')).toEqual(0); + }); + }); + + test('Invalid or missing registration details', ({ given, when, then, and }) => { + let params: CreateUserParams; + let error: any; + given('I am a new user', () => { + params = new UserBuilder().makeCreateUserCommandBuilder() + .withAllRandomDetails() + .withLastName('') + .build(); + }); + + when('I register with invalid account details', async () => { + try { + createUserCommand = CreateUserCommand.fromProps(params); + await application.users.createUser(createUserCommand); + } catch (e) { + error = e; + } + }); + + then('I should see an error notifying me that my input is invalid', async () => { + expect(userRepoSpy.getTimesMethodCalled('save')).toEqual(0); + expect(error).toBeDefined(); + }); + + and('I should not have been sent access to account details', () => { + expect(transactionalEmailAPISpy.getTimesMethodCalled('sendMail')).toEqual(0); + }); + }); + + test('Username already taken', ({ given, when, then, and }) => { + given('a set of users have already created their accounts with valid details', async (table) => { + table.forEach((item: any) => { + commands.push(new UserBuilder().makeCreateUserCommandBuilder() + .withFirstName(item.firstName) + .withLastName(item.lastName) + .withUsername(item.username) + .withEmail(item.email) + .buildCommand() + ) + }); + await databaseFixture.setupWithExistingUsersFromCommands(commands); + transactionalEmailAPISpy.reset(); + }); + + when('new users attempt to register with already taken usernames', async (table) => { + for (const item of table) { + const response = application.users.createUser(item); + createUserResponses.push(response); + } + }); + + then('they see an error notifying them that the username has already been taken', () => { + for (const response of createUserResponses) { + expect(response).rejects.toThrow(expect.objectContaining({ + type: 'UsernameAlreadyTakenException' + })); + } + }); + + and('they should not have been sent access to account details', () => { + expect(transactionalEmailAPISpy.getTimesMethodCalled('sendMail')).toEqual(0); + }); + }); + + test('Account already created with email', ({ given, when, then, and }) => { + given('a set of users already created accounts', async (table) => { + table.forEach((item: any) => { + commands.push(new UserBuilder().makeCreateUserCommandBuilder() + .withUsername(item.username) + .withFirstName(item.firstName) + .withLastName(item.lastName) + .withEmail(item.email) + .buildCommand() + ); + }) + await databaseFixture.setupWithExistingUsersFromCommands(commands); + transactionalEmailAPISpy.reset(); + }); + + when('new users attempt to register with those emails', async () => { + for (const command of commands) { + const response = application.users.createUser(command); + createUserResponses.push(response); + } + }); + + then('they should see an error notifying them that the account already exists', async () => { + for (const response of createUserResponses) { + expect(response).rejects.toThrow(expect.objectContaining({ + type: 'EmailAlreadyInUseException' + })); + } + }); + + and('they should not have been sent access to account details', () => { + expect(transactionalEmailAPISpy.getTimesMethodCalled('sendMail')).toEqual(0); + }); + }); +}) diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/src/api/users.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/src/api/users.ts index ae105a232..6513a16f9 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/src/api/users.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/src/api/users.ts @@ -1,6 +1,15 @@ import axios from "axios"; import { APIResponse, GenericErrors, ServerError } from "."; +export type ValidatedUser = { + id?: number; + firstName: string; + lastName: string; + email: string; + username: string; + password: string; +} + export type CreateUserParams = { firstName: string; lastName: string; @@ -8,7 +17,7 @@ export type CreateUserParams = { username: string; }; -export type User = { +export type UserDTO = { id: number; email: string; firstName: string; @@ -22,11 +31,11 @@ export type CreateUserErrors = | GenericErrors | EmailAlreadyInUseError | UsernameAlreadyTakenError; -export type CreateUserResponse = APIResponse; +export type CreateUserResponse = APIResponse; export type UserNotFoundError = "UserNotFound"; export type GetUserByEmailErrors = ServerError | UserNotFoundError; -export type GetUserByEmailResponse = APIResponse; +export type GetUserByEmailResponse = APIResponse; export type GetUserErrors = GetUserByEmailErrors | CreateUserErrors; export type UserResponse = APIResponse< @@ -43,7 +52,7 @@ export const createUsersAPI = (apiURL: string) => { }); return successResponse.data as CreateUserResponse; } catch (err) { - //@ts-expect-error + //@ts-ignore return err.response.data as CreateUserResponse; } }, @@ -52,7 +61,7 @@ export const createUsersAPI = (apiURL: string) => { const successResponse = await axios.get(`${apiURL}/users/${email}`); return successResponse.data as GetUserByEmailResponse; } catch (err) { - //@ts-expect-error + //@ts-ignore return err.response.data as GetUserByEmailResponse; } }, diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/builders/createUserBuilder.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/builders/users/createUserCommandBuilder.ts similarity index 96% rename from ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/builders/createUserBuilder.ts rename to ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/builders/users/createUserCommandBuilder.ts index f450ae009..88e8decf0 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/builders/createUserBuilder.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/builders/users/createUserCommandBuilder.ts @@ -2,7 +2,8 @@ import { CreateUserCommand } from "@dddforum/backend/src/modules/users/usersComm import { CreateUserParams } from "@dddforum/shared/src/api/users"; import { TextUtil } from "@dddforum/shared/src/utils/textUtil"; -export class CreateUserBuilder { +export class CreateUserCommandBuilder { + private props: CreateUserParams; constructor() { diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/builders/users/index.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/builders/users/index.ts new file mode 100644 index 000000000..f05ad42ae --- /dev/null +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/builders/users/index.ts @@ -0,0 +1,13 @@ +import { CreateUserCommandBuilder } from "./createUserCommandBuilder"; +import { ValidatedUserBuilder } from "./validatedUserBuilder"; + +export class UserBuilder { + + makeCreateUserCommandBuilder () { + return new CreateUserCommandBuilder() + } + + makeValidatedUserBuilder () { + return new ValidatedUserBuilder (); + } +} diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/builders/users/validatedUserBuilder.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/builders/users/validatedUserBuilder.ts new file mode 100644 index 000000000..90e4d4cbb --- /dev/null +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/builders/users/validatedUserBuilder.ts @@ -0,0 +1,63 @@ +import { ValidatedUser } from "@dddforum/shared/src/api/users"; +import { NumberUtil } from "@dddforum/shared/src/utils/numberUtil"; +import { TextUtil } from "@dddforum/shared/src/utils/textUtil"; + +export class ValidatedUserBuilder { + private props: ValidatedUser; + + constructor() { + this.props = { + id: -1, + firstName: "", + lastName: "", + email: "", + username: "", + password: '', + }; + } + + public withAllRandomDetails() { + this.props.id = NumberUtil.generateRandomInteger(100000, 8000000); + this.withFirstName(TextUtil.createRandomText(10)); + this.withLastName(TextUtil.createRandomText(10)); + this.withEmail(TextUtil.createRandomEmail()); + this.withUsername(TextUtil.createRandomText(10)); + return this; + } + + public withFirstName(firstName: string) { + this.props = { + ...this.props, + firstName, + }; + return this; + } + + public withLastName(lastName: string) { + this.props = { + ...this.props, + lastName, + }; + return this; + } + + public withEmail(email: string) { + this.props = { + ...this.props, + email, + }; + return this; + } + + public withUsername(username: string) { + this.props = { + ...this.props, + username, + }; + return this; + } + + public build() { + return this.props; + } +} diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/stubs/userResponseStub.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/stubs/userResponseStub.ts deleted file mode 100644 index 7ae357dce..000000000 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/packages/shared/tests/support/stubs/userResponseStub.ts +++ /dev/null @@ -1,79 +0,0 @@ - -import { CreateUserParams, User } from "@dddforum/shared/src/api/users"; -import { NumberUtil } from "@dddforum/shared/src/utils/numberUtil"; -import { TextUtil } from "@dddforum/shared/src/utils/textUtil"; - -export class UserResponseStub { - private props: User; - - constructor() { - this.props = { - id: NumberUtil.generateRandomInteger(1000, 100000), - email: '', - firstName: '', - lastName: '', - username: '', - }; - } - - public fromParams (command: CreateUserParams) { - this.props.firstName = command.firstName; - this.props.lastName = command.lastName; - this.props.username = command.username; - this.props.email = command.email; - - return this; - } - - public withAllRandomDetails () { - this.withRandomId(); - this.withFirstName(TextUtil.createRandomText(10)); - this.withLastName(TextUtil.createRandomText(10)); - this.withRandomEmail(); - this.withRandomUsername(); - return this; - } - - withRandomId () { - this.props.id = NumberUtil.generateRandomInteger(1000, 100000); - return this; - } - - public withId (value: number) { - this.props.id = value; - return this; - } - - public withFirstName(value: string) { - this.props.firstName = value; - return this; - } - withLastName(value: string) { - this.props.lastName = value; - return this; - } - - withUsername (value: string) { - this.props.username = value; - return this; - } - - withRandomUsername() { - this.props.username = `username-${NumberUtil.generateRandomInteger(1000, 100000)}`; - return this; - } - withRandomEmail() { - const randomSequence = NumberUtil.generateRandomInteger(1000, 100000); - this.props.email = `testEmail-${randomSequence}@gmail.com`; - return this; - } - - withEmail (email: string) { - this.props.email = email; - return this; - } - - build() { - return this.props; - } -}