diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/begin/tsAliasReplacer.js b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/begin/tsAliasReplacer.js index b895d7576..25156a11d 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/begin/tsAliasReplacer.js +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/begin/tsAliasReplacer.js @@ -5,14 +5,19 @@ */ const replacements = [ - // We use only @dddforum/shared in the production code, so no need to include @dddforum/backend, @dddforum/frontend, etc. - ['@dddforum/shared/src', '@dddforum/shared/dist'], + ["@dddforum/shared/src", "@dddforum/shared/dist"], + ["@dddforum/backend/src", "@dddforum/backend/dist"], + ["@dddforum/frontend/src", "@dddforum/frontend/dist"], ]; /** * tsc-alias only supports commonjs replacers. */ -exports.default = function tsAliasesReplacer({ orig: originalImport, _file, _config }) { +exports.default = function tsAliasesReplacer({ + orig: originalImport, + _file, + _config, +}) { let newImport = originalImport; replacements.forEach(([fromRule, toRule]) => { @@ -20,4 +25,4 @@ exports.default = function tsAliasesReplacer({ orig: originalImport, _file, _con }); return newImport; -}; \ No newline at end of file +}; diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/.eslintignore b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/.eslintignore new file mode 100644 index 000000000..fe17425e4 --- /dev/null +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/.eslintignore @@ -0,0 +1 @@ +**/dist \ No newline at end of file diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/package.json b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/package.json index e33113e76..0b4cc304e 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/package.json +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/package.json @@ -9,16 +9,17 @@ "node": ">=16.16.0" }, "scripts": { - "test:e2e:front": "npm run test:e2e --workspace=@dddforum/frontend", - "test:e2e:back": "npm run test:e2e --workspace=@dddforum/backend", + "test:e2e:frontend": "npm run test:e2e --workspace=@dddforum/frontend", + "test:e2e:backend": "npm run test:e2e --workspace=@dddforum/backend", "test:infra:all": "npm run test:infra --workspaces --if-present", - "test:infra:back": "npm run test:infra --workspace=@dddforum/backend", - "test:unit:back": "npm run test:unit --workspace=@dddforum/backend", + "test:infra:backend": "npm run test:infra --workspace=@dddforum/backend", + "test:unit:backend": "npm run test:unit --workspace=@dddforum/backend", + "test:unit": "npm run test:unit --workspaces --if-present", "clean": "npm run clean --workspaces --if-present", "build": "npm run build --workspaces --if-present", + "start:dev:backend": "npm run start:dev --workspace=@dddforum/backend", + "start:dev:frontend": "npm run start:dev --workspace=@dddforum/frontend", "lint": "npm run lint --workspaces --if-present", - "start:dev:back": "npm run start:dev --workspace=@dddforum/backend", - "start:dev:front": "npm run start:dev --workspace=@dddforum/frontend", "prettier-format": "run-script-os", "prettier-format:win32": "prettier --config .prettierrc \"./**/src/**/*.ts\" --write", "prettier-format:darwin:linux": "prettier --config .prettierrc './**/src/**/*.ts' --write", @@ -37,7 +38,7 @@ "eslint-plugin-jest": "^27.6.0", "eslint-plugin-prettier": "^5.0.1", "jest": "^29.5.0", - "jest-cucumber": "^3.0.1", + "jest-cucumber": "3.0.1", "jest-mock-extended": "^3.0.7", "rimraf": "^3.0.2", "run-script-os": "^1.1.6", @@ -50,6 +51,7 @@ "author": "", "license": "ISC", "dependencies": { - "nodemailer": "^6.9.13" + "nodemailer": "^6.9.13", + "tsconfig-paths": "^4.2.0" } } diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/.env.development b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/.env.development index 6bfcb6472..083c815f9 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/.env.development +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/.env.development @@ -1,2 +1 @@ -NODE_ENV=development -DATABASE_URL= +NODE_ENV=development \ No newline at end of file diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/.env.test b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/.env.test index afaf253db..f46533d04 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/.env.test +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/.env.test @@ -1,2 +1 @@ -NODE_ENV=test -DATABASE_URL= +NODE_ENV=test \ No newline at end of file diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/.eslintignore b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/.eslintignore new file mode 100644 index 000000000..fe17425e4 --- /dev/null +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/.eslintignore @@ -0,0 +1 @@ +**/dist \ No newline at end of file diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/modules/notifications/adapters/transactionalEmailAPI/mailjetTransactionalEmailAPI.ts b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/modules/notifications/adapters/transactionalEmailAPI/mailjetTransactionalEmailAPI.ts index 11840d46e..bc6ec0176 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/modules/notifications/adapters/transactionalEmailAPI/mailjetTransactionalEmailAPI.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/modules/notifications/adapters/transactionalEmailAPI/mailjetTransactionalEmailAPI.ts @@ -1,19 +1,42 @@ -import { Spy } from "../../../../shared/testDoubles/spy"; -import { - SendMailInput, - TransactionalEmailAPI, -} from "../../ports/transactionalEmailAPI"; +import nodemailer from "nodemailer"; +import { SendMailInput, TransactionalEmailAPI } from "../../ports/transactionalEmailAPI"; -export class TransactionalEmailAPISpy - extends Spy - implements TransactionalEmailAPI -{ - constructor() { - super(); - } - async sendMail(input: SendMailInput): Promise { - this.addCall("sendMail", [input]); - return true; +const mailSettings = { + service: process.env.MAIL_SENDER_SERVICE || "gmail", + user: process.env.MAIL_SENDER_EMAIL_ADDRESS, + pass: process.env.MAIL_SENDER_PASSWORD, +}; + +// Create a transporter object using SMTP +const transporter = nodemailer.createTransport({ + service: mailSettings.service, + auth: { + user: mailSettings.user, + pass: mailSettings.pass, + }, + authMethod: "PLAIN", +}); + +export class MailjetTransactionalEmail implements TransactionalEmailAPI { + async sendMail(input: SendMailInput) { + // Email content + const mailOptions = { + from: mailSettings.user, + ...input, + }; + + try { + new Promise((resolve, reject) => { + transporter.sendMail(mailOptions, (error, info) => { + if (error) return reject(error); + return resolve(info); + }); + }); + + return true; + } catch (err) { + return false; + } } } diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/modules/notifications/adapters/transactionalEmailAPI/transactionalEmailAPISpy.ts b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/modules/notifications/adapters/transactionalEmailAPI/transactionalEmailAPISpy.ts index 142ffff95..ada7cb7ea 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/modules/notifications/adapters/transactionalEmailAPI/transactionalEmailAPISpy.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/modules/notifications/adapters/transactionalEmailAPI/transactionalEmailAPISpy.ts @@ -1,44 +1,20 @@ -import nodemailer from "nodemailer"; + +import { Spy } from "../../../../shared/testDoubles/spy"; import { SendMailInput, TransactionalEmailAPI, } from "../../ports/transactionalEmailAPI"; -const mailSettings = { - service: process.env.MAIL_SENDER_SERVICE || "gmail", - user: process.env.MAIL_SENDER_EMAIL_ADDRESS, - pass: process.env.MAIL_SENDER_PASSWORD, -}; - -// Create a transporter object using SMTP -const transporter = nodemailer.createTransport({ - service: mailSettings.service, - auth: { - user: mailSettings.user, - pass: mailSettings.pass, - }, - authMethod: "PLAIN", -}); - -export class MailjetTransactionalEmail implements TransactionalEmailAPI { - async sendMail(input: SendMailInput) { - // Email content - const mailOptions = { - from: mailSettings.user, - ...input, - }; - - try { - new Promise((resolve, reject) => { - transporter.sendMail(mailOptions, (error, info) => { - if (error) return reject(error); - return resolve(info); - }); - }); +export class TransactionalEmailAPISpy + extends Spy + implements TransactionalEmailAPI +{ + constructor() { + super(); + } - return true; - } catch (err) { - return false; - } + async sendMail(input: SendMailInput): Promise { + this.addCall("sendMail", [input]); + return true; } -} +} \ No newline at end of file diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/modules/notifications/notificationsModule.ts b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/modules/notifications/notificationsModule.ts index 996a7261a..2627819d6 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/modules/notifications/notificationsModule.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/modules/notifications/notificationsModule.ts @@ -1,7 +1,6 @@ import { Config } from "../../shared/config"; import { ApplicationModule } from "../../shared/modules/applicationModule"; -import { TransactionalEmailAPISpy } from "./adapters/transactionalEmailAPI/mailjetTransactionalEmailAPI"; -import { MailjetTransactionalEmail } from "./adapters/transactionalEmailAPI/transactionalEmailAPISpy"; +import { TransactionalEmailAPISpy } from "./adapters/transactionalEmailAPI/transactionalEmailAPISpy"; import { TransactionalEmailAPI } from "./ports/transactionalEmailAPI"; export class NotificationsModule extends ApplicationModule { @@ -22,7 +21,7 @@ export class NotificationsModule extends ApplicationModule { private createTransactionalEmailAPI() { if (this.getEnvironment() === "production") { - return new MailjetTransactionalEmail(); + return new TransactionalEmailAPISpy(); } /** diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/shared/config/index.ts b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/shared/config/index.ts index b3cf0d045..6e8ce33fa 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/shared/config/index.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/shared/config/index.ts @@ -10,10 +10,12 @@ export type Script = export class Config { env: Environment; script: Script; + apiURL: string; constructor(script: Script) { this.env = (process.env.NODE_ENV as Environment) || "development"; this.script = script; + this.apiURL = this.getAPIURL(); } getEnvironment() { @@ -23,4 +25,17 @@ export class Config { getScript() { return this.script; } + + getAPIURL() { + const fallback = "http://localhost:3000"; + if (this.isStaging()) { + return process.env.API_URL_STAGING || fallback + } + + return fallback + } + + isStaging() { + return this.env === "staging"; + } } diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/shared/http/webServer.ts b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/shared/http/webServer.ts index fc6a01214..34a96ad3d 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/shared/http/webServer.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/src/shared/http/webServer.ts @@ -40,7 +40,7 @@ export class WebServer { async start(): Promise { return new Promise((resolve, _reject) => { ProcessService.killProcessOnPort(this.config.port, () => { - if (this.config.env === " test") { + if (this.config.env === "test") { resolve(); } console.log("Starting the server"); diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/tests/features/registration/registration.unit.ts b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/tests/features/registration/registration.unit.ts index 7710a1149..c23c4330c 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/tests/features/registration/registration.unit.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/backend/tests/features/registration/registration.unit.ts @@ -4,7 +4,7 @@ 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/mailjetTransactionalEmailAPI'; +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'; diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/package.json b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/package.json index ac815f593..69604806c 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/package.json +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/package.json @@ -1,13 +1,14 @@ { - "name": "@dddforum/shared", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": {} - } \ No newline at end of file + "name": "@dddforum/shared", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "clean": "rimraf ./dist", + "build": "npm run clean && tsc -b tsconfig.build.json" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": {} +} diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/src/api/marketing.ts b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/src/api/marketing.ts index 3c6ff98ad..ed4347fbd 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/src/api/marketing.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/src/api/marketing.ts @@ -18,7 +18,7 @@ export const createMarketingAPI = (apiURL: string) => { }); return successResponse.data as AddEmailToListResponse; } catch (err) { - //@ts-ignore + //@ts-expect-error return err.response.data as APIResponse; } }, diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/src/api/posts.ts b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/src/api/posts.ts index f98bb1aad..47faf5205 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/src/api/posts.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/src/api/posts.ts @@ -27,7 +27,7 @@ export const createPostsAPI = (apiURL: string) => { const successResponse = await axios.get(`${apiURL}/posts?sort=${sort}`); return successResponse.data as GetPostsResponse; } catch (err) { - //@ts-ignore + //@ts-expect-error return err.response.data as GetPostsResponse; } }, diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/tests/support/fixtures/databaseFixture.ts b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/tests/support/fixtures/databaseFixture.ts index 78cf73cf8..5c62ce06f 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/tests/support/fixtures/databaseFixture.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/tests/support/fixtures/databaseFixture.ts @@ -9,19 +9,14 @@ export class DatabaseFixture { async resetDatabase() { const connection = this.composition.getDatabase().getConnection(); - const deleteAllComments = connection.comment.deleteMany(); - const deleteAllVotes = connection.vote.deleteMany(); - const deleteAllPosts = connection.post.deleteMany(); - const deleteMembers = connection.member.deleteMany(); - const deleteAllUsers = connection.user.deleteMany(); try { await connection.$transaction([ - deleteAllComments, - deleteAllVotes, - deleteAllPosts, - deleteMembers, - deleteAllUsers, + connection.comment.deleteMany(), + connection.vote.deleteMany(), + connection.post.deleteMany(), + connection.member.deleteMany(), + connection.user.deleteMany(), ]); } catch (error) { console.error(error); diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/tsconfig.build.json b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/tsconfig.build.json deleted file mode 100644 index 6284549ca..000000000 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/tsconfig.build.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.build.json", - "compilerOptions": { - "outDir": "./dist", - "types": ["node"] - }, - "include": ["./src", "/tests"], -} \ No newline at end of file diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/tsconfig.json b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/tsconfig.json index c42f0413f..30e4b09b6 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/tsconfig.json +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/packages/shared/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "types": ["node", "jest"], + "types": ["node"] }, -} \ No newline at end of file +} diff --git a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/tsAliasReplacer.js b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/tsAliasReplacer.js index b895d7576..25156a11d 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/tsAliasReplacer.js +++ b/ThePhasesOfCraftship/2_best_practice_first/advancedTesting/assignment/end/tsAliasReplacer.js @@ -5,14 +5,19 @@ */ const replacements = [ - // We use only @dddforum/shared in the production code, so no need to include @dddforum/backend, @dddforum/frontend, etc. - ['@dddforum/shared/src', '@dddforum/shared/dist'], + ["@dddforum/shared/src", "@dddforum/shared/dist"], + ["@dddforum/backend/src", "@dddforum/backend/dist"], + ["@dddforum/frontend/src", "@dddforum/frontend/dist"], ]; /** * tsc-alias only supports commonjs replacers. */ -exports.default = function tsAliasesReplacer({ orig: originalImport, _file, _config }) { +exports.default = function tsAliasesReplacer({ + orig: originalImport, + _file, + _config, +}) { let newImport = originalImport; replacements.forEach(([fromRule, toRule]) => { @@ -20,4 +25,4 @@ exports.default = function tsAliasesReplacer({ orig: originalImport, _file, _con }); return newImport; -}; \ No newline at end of file +}; diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/.editorconfig b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/.editorconfig new file mode 100644 index 000000000..4ac4973fb --- /dev/null +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/.editorconfig @@ -0,0 +1,11 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/.gitignore b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/.gitignore new file mode 100644 index 000000000..d54980254 --- /dev/null +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/.gitignore @@ -0,0 +1,5 @@ +node_modules +build +dist +.vscode +.env \ No newline at end of file diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/.prettierrc b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/.prettierrc new file mode 100644 index 000000000..264e964a7 --- /dev/null +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} \ No newline at end of file diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/package.json b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/package.json index d468a1871..0b4cc304e 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/package.json +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/package.json @@ -19,7 +19,6 @@ "build": "npm run build --workspaces --if-present", "start:dev:backend": "npm run start:dev --workspace=@dddforum/backend", "start:dev:frontend": "npm run start:dev --workspace=@dddforum/frontend", - "generate:database": "npm run generate --workspace=@dddforum/backend", "lint": "npm run lint --workspaces --if-present", "prettier-format": "run-script-os", "prettier-format:win32": "prettier --config .prettierrc \"./**/src/**/*.ts\" --write", diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/.env.development b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/.env.development index c0d665211..083c815f9 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/.env.development +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/.env.development @@ -1 +1 @@ -NODE_ENV=development +NODE_ENV=development \ No newline at end of file diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/src/modules/users/adapters/inMemoryUserRepositorySpy.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/src/modules/users/adapters/inMemoryUserRepositorySpy.ts index 3921f3d86..bb9954748 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/src/modules/users/adapters/inMemoryUserRepositorySpy.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/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/begin/packages/backend/src/modules/users/adapters/productionUserRepository.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/src/modules/users/adapters/productionUserRepository.ts index 9d76102a2..930676355 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/src/modules/users/adapters/productionUserRepository.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/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/begin/packages/backend/src/modules/users/ports/userRepository.infra.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/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/begin/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/begin/packages/backend/src/modules/users/ports/usersRepository.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/src/modules/users/ports/usersRepository.ts index d201d1018..e808b7c16 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/src/modules/users/ports/usersRepository.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/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/begin/packages/backend/src/modules/users/usersService.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/src/modules/users/usersService.ts index ab06c0703..c40bb9639 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/src/modules/users/usersService.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/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/begin/packages/backend/tests/api/registration.api.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/tests/api/registration.api.infra.ts similarity index 69% rename from ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/tests/api/registration.api.ts rename to ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/tests/api/registration.api.infra.ts index a32534d14..c98576532 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/tests/api/registration.api.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/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/begin/packages/backend/tests/features/registration/registration.e2e.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/tests/features/registration/registration.e2e.ts index 9a9b85775..b9279e873 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/tests/features/registration/registration.e2e.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/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/begin/packages/backend/tests/features/registration/registration.infra.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/tests/features/registration/registration.infra.ts index e2684f323..2f3d8405a 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/tests/features/registration/registration.infra.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/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/begin/packages/backend/tests/features/registration/registration.unit.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/tests/features/registration/registration.unit.ts index 830372b72..c23c4330c 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/tests/features/registration/registration.unit.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/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/begin/packages/backend/tsconfig.build.json b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/tsconfig.build.json deleted file mode 100644 index 39f02375d..000000000 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/backend/tsconfig.build.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.build.json", - "compilerOptions": { - "outDir": "./dist", - "types": ["node"] - }, - "include": ["./src"] -} diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/src/api.tsx b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/src/api.tsx index 04b49fe42..f4570f908 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/src/api.tsx +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/src/api.tsx @@ -1,6 +1,6 @@ // api.tsx -import { CreateUserParams } from '@dddforum/shared/src/api/users' +import { RegistrationInput } from "./components/registrationForm"; import axios from 'axios' export const api = { @@ -9,7 +9,7 @@ export const api = { return axios.get('http://localhost:3000/posts?sort=recent') } }, - register: (input: CreateUserParams) => { + register: (input: RegistrationInput) => { return axios.post('http://localhost:3000/users/new', { ...input }) diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/src/components/postsList.tsx b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/src/components/postsList.tsx index 43360bb01..9738349d1 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/src/components/postsList.tsx +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/src/components/postsList.tsx @@ -5,9 +5,7 @@ import moment from 'moment'; type Vote = { id: number, postId: number, voteType: 'Upvote' | 'Downvote' }; -type Comment = { - content: string -}; +type Comment = {}; type Post = { title: string; diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/src/pages/mainPage.tsx b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/src/pages/mainPage.tsx index 8fa31b198..e7c272110 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/src/pages/mainPage.tsx +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/src/pages/mainPage.tsx @@ -10,7 +10,7 @@ export const MainPage = () => { const loadPosts = async () => { try { const response = await api.posts.getPosts('recent'); - // @ts-expect-error - we know the response has a posts field + // @ts-ignore setPosts(response.data.posts) } catch (err) { console.log(err); diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/tests/features/registration.e2e.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/tests/features/registration.e2e.ts index 1e1c16bbf..5bd8d19ef 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/tests/features/registration.e2e.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/tests/features/registration.e2e.ts @@ -9,8 +9,6 @@ import * as path from 'path'; import { PuppeteerPageDriver } from '../support/driver'; import { App, createAppObject } from '../support/pages'; import { DatabaseFixture } from '@dddforum/shared/tests/support/fixtures/databaseFixture'; -import { Config } from "@dddforum/backend/src/shared/config"; -import { CompositionRoot } from "@dddforum/backend/src/shared/compositionRoot"; const feature = loadFeature(path.join(sharedTestRoot, 'features/registration.feature'), { tagFilter: '@frontend' }); @@ -22,12 +20,10 @@ defineFeature(feature, (test) => { let user: CreateUserParams; let users: CreateUserParams[]; let databaseFixture: DatabaseFixture - let composition: CompositionRoot - const config: Config = new Config("test:e2e"); + beforeAll(async () => { - composition = CompositionRoot.createCompositionRoot(config); - databaseFixture = new DatabaseFixture(composition); + databaseFixture = new DatabaseFixture(); puppeteerPageDriver = await PuppeteerPageDriver.create({ headless: false, slowMo: 50, diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/tests/support/components/appNotifications.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/tests/support/components/appNotifications.ts index d69fea428..19be7a342 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/tests/support/components/appNotifications.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/tests/support/components/appNotifications.ts @@ -8,7 +8,7 @@ export class AppNotifications extends Component { } async getErrorNotificationText() { - const usernameElement = await this.driver.page + let usernameElement = await this.driver.page .waitForSelector(appSelectors.notifications.failure, { timeout: 2000 }) .then((el) => { return el?.evaluate((e) => e.textContent); diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/tests/support/components/headerComponent.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/tests/support/components/headerComponent.ts index 64cd0146d..f24a4f6d2 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/tests/support/components/headerComponent.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/frontend/tests/support/components/headerComponent.ts @@ -14,7 +14,7 @@ export class HeaderComponent extends Component { } async getUsernameFromHeader () { - const usernameElement = await this.elements.get('header'); + let usernameElement = await this.elements.get('header'); return usernameElement?.evaluate((e) => e.textContent); } diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/shared/src/api/users.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/shared/src/api/users.ts index ae105a232..6513a16f9 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/shared/src/api/users.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/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/begin/packages/shared/tests/support/builders/createUserBuilder.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/shared/tests/support/builders/users/createUserCommandBuilder.ts similarity index 96% rename from ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/shared/tests/support/builders/createUserBuilder.ts rename to ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/shared/tests/support/builders/users/createUserCommandBuilder.ts index f450ae009..88e8decf0 100644 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/shared/tests/support/builders/createUserBuilder.ts +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/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/begin/packages/shared/tests/support/builders/users/index.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/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/begin/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/begin/packages/shared/tests/support/builders/users/validatedUserBuilder.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/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/begin/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/begin/packages/shared/tests/support/stubs/userResponseStub.ts b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/shared/tests/support/stubs/userResponseStub.ts deleted file mode 100644 index 7ae357dce..000000000 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/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; - } -} diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/shared/tsconfig.build.json b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/shared/tsconfig.build.json deleted file mode 100644 index af83312cf..000000000 --- a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/begin/packages/shared/tsconfig.build.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.build.json", - "compilerOptions": { - "outDir": "./dist", - "types": ["node"] - }, - "include": ["./src"] -} \ No newline at end of file diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/.editorconfig b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/.editorconfig new file mode 100644 index 000000000..4ac4973fb --- /dev/null +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/.editorconfig @@ -0,0 +1,11 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/.eslintignore b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/.eslintignore new file mode 100644 index 000000000..fe17425e4 --- /dev/null +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/.eslintignore @@ -0,0 +1 @@ +**/dist \ No newline at end of file diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/.gitignore b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/.gitignore new file mode 100644 index 000000000..d54980254 --- /dev/null +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/.gitignore @@ -0,0 +1,5 @@ +node_modules +build +dist +.vscode +.env \ No newline at end of file diff --git a/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/.prettierrc b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/.prettierrc new file mode 100644 index 000000000..264e964a7 --- /dev/null +++ b/ThePhasesOfCraftship/2_best_practice_first/deployment/assignment/end/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} \ No newline at end of file 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 82c0eb494..126c6959f 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,17 +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