diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index ddb7710..42c9ab5 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -28,7 +28,9 @@ jobs: - run: docker compose -f packages/e2e/docker-compose.yml up -d - run: yarn dlx wait-on tcp:27017 - run: yarn dev & - - run: yarn dlx wait-on tcp:8080 + - run: yarn dlx wait-on tcp:8081 + - run: yarn dlx wait-on tcp:8082 + - run: yarn dlx wait-on tcp:8083 - run: set -o pipefail; yarn test:ci | bash ./predate.sh - uses: actions/upload-artifact@v3 with: diff --git a/package.json b/package.json index db11d0f..f2cc51a 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,13 @@ "devDependencies": { "@types/cookie-parser": "^1.4.7", "cookie-parser": "^1.4.6", - "jsdom": "^24.1.0" + "jsdom": "^24.1.0", + "nest-memory-transport": "^1.0.3" }, "dependencies": { + "@nestjs/config": "^3.2.2", + "@nestjs/microservices": "^10.4.1", + "kafkajs": "^2.2.4", "winston": "^3.13.0" } } diff --git a/packages/client/src/adapters/backend.ts b/packages/client/src/adapters/backend.ts deleted file mode 100644 index b7c49b3..0000000 --- a/packages/client/src/adapters/backend.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { CartSummary, Order, Product } from "@ts-react-tdd/server/src/types"; -import axios, { AxiosInstance } from "axios"; -import { CartAdapter } from "./cart"; -import { OrderAdapter } from "./order"; -import { ProductCatalog } from "./productCatalog"; - -export class HTTPShopBackend implements CartAdapter, OrderAdapter, ProductCatalog { - private axios: AxiosInstance; - constructor(url: string) { - this.axios = axios.create({ baseURL: url }) - } - - addItem = async (cartId: string, productId: string) => - (await this.axios.post(`/cart/${cartId}`, { productId })).data; - - getCount = async (cartId: string) => - (await this.axios.get(`/cart/${cartId}/count`)).data; - - getCartSummary = async (cartId: string) => { - const res = await this.axios.get(`/cart/${cartId}`); - return CartSummary.parse(res.data); - } - - checkout = async (cartId: string) => (await this.axios.post(`/checkout/${cartId}`)).data; - - getOrder = async (orderId: string) => { - const res = await this.axios.get(`/order/${orderId}`); - return Order.parse(res.data); - }; - - findAllProducts = async () => { - const res = await this.axios.get(`/products`); - return res.data.map(p => Product.parse(p)); - }; - -} diff --git a/packages/client/src/adapters/cart.ts b/packages/client/src/adapters/cart.ts deleted file mode 100644 index 7784a51..0000000 --- a/packages/client/src/adapters/cart.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {CartSummary, Product} from "@ts-react-tdd/server/src/types"; - -export interface CartAdapter { - getCount: (cartId: string) => Promise; - addItem: (cartId: string, productId: Product["id"]) => Promise; - checkout: (cartId: string) => Promise; - getCartSummary: (cartId: string) => Promise; -} - diff --git a/packages/client/src/adapters/context.tsx b/packages/client/src/adapters/context.tsx index d37cca1..c2621aa 100644 --- a/packages/client/src/adapters/context.tsx +++ b/packages/client/src/adapters/context.tsx @@ -1,18 +1,25 @@ import React, { PropsWithChildren, useMemo } from "react"; -import { HTTPShopBackend } from './backend'; -import { CartAdapter } from "./cart"; -import { OrderAdapter } from "./order"; -import { ProductCatalog } from "./productCatalog"; +import axios, {AxiosInstance} from "axios"; type Adapters = { - cart: CartAdapter; - productCatalog: ProductCatalog; - orders: OrderAdapter; + cart: AxiosInstance; + productCatalog: AxiosInstance; + orders: AxiosInstance; } export const IOContext = React.createContext(undefined as unknown as Adapters); -export const IOContextProvider: React.FC> = ({backendUrl, children}) => { - const backend = useMemo(() => new HTTPShopBackend(backendUrl), [backendUrl]); - return {children}; +export const MonolithIOProvider: React.FC> = ({backendUrl, children}) => { + const client = useMemo(() => axios.create({ baseURL: backendUrl }), [backendUrl]) + + return {children}; +} + +type Props = PropsWithChildren<{catalogUrl: string, cartUrl: string, ordersUrl: string}>; +export const MicroservicesIOProvider: React.FC = ({catalogUrl, cartUrl, ordersUrl, children}) => { + const cart = useMemo(() => axios.create({ baseURL: cartUrl }), [cartUrl]) + const productCatalog = useMemo(() => axios.create({ baseURL: catalogUrl }), [catalogUrl]) + const orders = useMemo(() => axios.create({ baseURL: ordersUrl }), [ordersUrl]) + + return {children}; } \ No newline at end of file diff --git a/packages/client/src/adapters/harness.tsx b/packages/client/src/adapters/harness.tsx index 8dfcad8..ffcb670 100644 --- a/packages/client/src/adapters/harness.tsx +++ b/packages/client/src/adapters/harness.tsx @@ -1,28 +1,17 @@ import {render, within} from "@testing-library/react"; -import {InMemoryOrderRepository, InMemoryProductRepository} from "@ts-react-tdd/server/src/adapters/fakes"; -import {createServerLogic} from "@ts-react-tdd/server/src/server"; +import {createTestingModule, runMicroservices} from "@ts-react-tdd/server/src/server.testkit"; import {QueryClient, QueryClientProvider} from "react-query"; import {MemoryRouter} from "react-router-dom"; import {App} from "../components/App"; -import {IOContextProvider} from "./context"; +import {MicroservicesIOProvider, MonolithIOProvider} from "./context"; import userEvent from "@testing-library/user-event"; +import {ProductTemplate} from "@ts-react-tdd/server/src/types"; type AppContext = { - productRepo?: InMemoryProductRepository, - orderRepo?: InMemoryOrderRepository + products: ProductTemplate[] }; -export async function makeApp({ - productRepo = new InMemoryProductRepository(), - orderRepo = new InMemoryOrderRepository() - }: AppContext) { - const fastify = createServerLogic(productRepo, orderRepo); - const queryClient = new QueryClient(); - - const baseUrl = await fastify.listen({host: '127.0.0.1', port: 0}); - - const app = render( - ); +function createDriver(app: ReturnType) { const addProductToCart = async (title: string) => { const product = await app.findByLabelText(title) @@ -42,7 +31,7 @@ export async function makeApp({ await userEvent.click(app.getByRole('button', { name: /home/i })); } - const driver = { + return { ...app, addProductToCart, viewCart, @@ -50,12 +39,59 @@ export async function makeApp({ home }; +} + +export async function makeMonolithicApp({ + products = [], + }: AppContext) { + + const {nest, orderRepo, productRepo} = await createTestingModule(products); + + const queryClient = new QueryClient(); + + const server = await nest.listen(0, "127.0.0.1"); + + const app = render( + ); + + const driver = createDriver(app); return { productRepo, orderRepo, driver, - [Symbol.dispose]: () => fastify.close(), + [Symbol.dispose]: () => server.close(), + }; +} + +export async function runBackendAndRender({ + products = [], + }: AppContext) { + + const { catalogApp, ordersApp, cartApp, orderRepo } = await runMicroservices(products) + + const queryClient = new QueryClient(); + + const catalogServer = await catalogApp.listen(0, "127.0.0.1"); + const ordersServer = await ordersApp.listen(0, "127.0.0.1"); + const cartServer = await cartApp.listen(0, "127.0.0.1"); + + const app = render( + + + + ); + + const driver = createDriver(app); + + return { + orderRepo, + app: driver, + [Symbol.dispose]: async () => { + await cartServer.close(); + await catalogServer.close(); + await ordersServer.close(); + }, }; } diff --git a/packages/client/src/adapters/order.ts b/packages/client/src/adapters/order.ts deleted file mode 100644 index 3e6f394..0000000 --- a/packages/client/src/adapters/order.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {Order} from "@ts-react-tdd/server/src/types"; - -export interface OrderAdapter { - getOrder: (orderId: string) => Promise -} - diff --git a/packages/client/src/adapters/productCatalog.ts b/packages/client/src/adapters/productCatalog.ts deleted file mode 100644 index a1c782f..0000000 --- a/packages/client/src/adapters/productCatalog.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {Product} from "@ts-react-tdd/server/src/types"; - -export interface ProductCatalog { - findAllProducts(): Promise; -} - diff --git a/packages/client/src/components/Shop.tsx b/packages/client/src/components/Shop.tsx index 1a2ccf7..4a532af 100644 --- a/packages/client/src/components/Shop.tsx +++ b/packages/client/src/components/Shop.tsx @@ -3,14 +3,14 @@ import React, {useState} from "react"; import {useProducts} from "../hooks/products"; import {useCartWidget} from "../hooks/cart"; -interface ShopProps { +type ShopProps = { cartId: string; } export const Shop: React.FC = ({ cartId }) => { const [ freeTextSearch, setFreeTextSearch ] = useState(''); - const { products, productsLoading, productsError } = useProducts({freeTextSearch}); + const products = useProducts(freeTextSearch); const { viewCart, addItem, itemCount, fetched } = useCartWidget(cartId); return
@@ -20,11 +20,19 @@ export const Shop: React.FC = ({ cartId }) => { setFreeTextSearch(e.target.value)}/>
- + }; -const Products: React.FC<{ products: Product[] | undefined, isLoading: boolean, error: unknown | null, addItem: (id: string) => void }> = ({ products, isLoading, error, addItem }) => { +type ProductsProps = { + products: { + data: Product[] | undefined; + isLoading: boolean; + error: unknown | null; + } + addItem: (id: string) => void; +} +const Products: React.FC = ( {products: {data, isLoading, error}, addItem}) => { if (isLoading) { return
Loading...
@@ -34,7 +42,7 @@ const Products: React.FC<{ products: Product[] | undefined, isLoading: boolean, return
<>Error: {error}
} - return <>{products!.map(({ title, id }) => + return <>{data!.map(({ title, id }) =>

{title}