Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Added Api Specification for integration testing #25

Merged
merged 5 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ We recommend providing different web app configurations for different endpoints'
That's what we did in our case. We've set up our Shopping Carts API and injected external dependencies:

- event store to store and retrieve events,
- The `getUnitPrice` method represents a call to an external service to get the price of a product added to the shopping cart.
- The `getUnitPrice` method represents a call to an external service to get the price of a product added to the shopping cart,
- We're also passing the current date generator. Embracing this non-deterministic dependency will be helpful for integration testing later on.

That clearly explains what dependencies this API needs, and by reading the file, you can understand what your application technology needs. That should cut the onboarding time for new people grasping our system setup.

Expand Down
8 changes: 3 additions & 5 deletions docs/snippets/gettingStarted/businessLogic.unit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,18 @@ describe('ShoppingCart', () => {
},
})
.when({
type: 'AddProductItemToShoppingCart',
type: 'ConfirmShoppingCart',
data: {
shoppingCartId,
productItem,
},
metadata: { now },
})
.then([
{
type: 'ProductItemAddedToShoppingCart',
type: 'ShoppingCartConfirmed',
data: {
shoppingCartId,
productItem,
addedAt: now,
confirmedAt: now,
},
},
]);
Expand Down
140 changes: 140 additions & 0 deletions docs/snippets/gettingStarted/webApi/apiBDD.int.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import {
getInMemoryEventStore,
type EventStore,
} from '@event-driven-io/emmett';
import {
ApiSpecification,
existingStream,
expectError,
expectNewEvents,
expectResponse,
getApplication,
} from '@event-driven-io/emmett-expressjs';
import { beforeEach, describe, it } from 'node:test';
import { v4 as uuid } from 'uuid';
import type { PricedProductItem, ShoppingCartEvent } from '../events';
import { shoppingCartApi } from './simpleApi';

const getUnitPrice = (_productId: string) => {
return Promise.resolve(100);
};

describe('ShoppingCart', () => {
let clientId: string;
let shoppingCartId: string;
beforeEach(() => {
clientId = uuid();
shoppingCartId = `shopping_cart:${clientId}:current`;
});

describe('When empty', () => {
it('should add product item', () => {
return given()
.when((request) =>
request
.post(`/clients/${clientId}/shopping-carts/current/product-items`)
.send(productItem),
)
.then([
expectNewEvents(shoppingCartId, [
{
type: 'ProductItemAddedToShoppingCart',
data: {
shoppingCartId,
productItem,
addedAt: now,
},
},
]),
]);
});
});

describe('When opened with product item', () => {
it('should confirm', () => {
return given(
existingStream(shoppingCartId, [
{
type: 'ProductItemAddedToShoppingCart',
data: {
shoppingCartId,
productItem,
addedAt: oldTime,
},
},
]),
)
.when((request) =>
request.post(`/clients/${clientId}/shopping-carts/current/confirm`),
)
.then([
expectResponse(204),
expectNewEvents(shoppingCartId, [
{
type: 'ShoppingCartConfirmed',
data: {
shoppingCartId,
confirmedAt: now,
},
},
]),
]);
});
});

describe('When confirmed', () => {
it('should not add products', () => {
return given(
existingStream(shoppingCartId, [
{
type: 'ProductItemAddedToShoppingCart',
data: {
shoppingCartId,
productItem,
addedAt: oldTime,
},
},
{
type: 'ShoppingCartConfirmed',
data: { shoppingCartId, confirmedAt: oldTime },
},
]),
)
.when((request) =>
request
.post(`/clients/${clientId}/shopping-carts/current/product-items`)
.send(productItem),
)
.then(
expectError(403, {
detail: 'Shopping Cart already closed',
status: 403,
title: 'Forbidden',
type: 'about:blank',
}),
);
});
});

const oldTime = new Date();
const now = new Date();

const given = ApiSpecification.for<ShoppingCartEvent>(
(): EventStore => getInMemoryEventStore(),
(eventStore: EventStore) =>
getApplication({
apis: [shoppingCartApi(eventStore, getUnitPrice, () => now)],
}),
);

const getRandomProduct = (): PricedProductItem => {
return {
productId: uuid(),
unitPrice: 100,
quantity: Math.random() * 10,
};
};

const productItem = getRandomProduct();
});
6 changes: 5 additions & 1 deletion docs/snippets/gettingStarted/webApi/apiSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@ import { getInMemoryEventStore } from '@event-driven-io/emmett';

const eventStore = getInMemoryEventStore();

const shoppingCarts = shoppingCartApi(eventStore, getUnitPrice);
const shoppingCarts = shoppingCartApi(
eventStore,
getUnitPrice,
() => new Date(),
);
// #endregion getting-started-api-setup
4 changes: 3 additions & 1 deletion docs/snippets/gettingStarted/webApi/simpleApi.int.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ describe('Simple Api from getting started', () => {

beforeEach(() => {
eventStore = getInMemoryEventStore();
app = getApplication({ apis: [shoppingCartApi(eventStore, getUnitPrice)] });
app = getApplication({
apis: [shoppingCartApi(eventStore, getUnitPrice, () => new Date())],
});
});

it('Should handle requests correctly', async () => {
Expand Down
5 changes: 5 additions & 0 deletions docs/snippets/gettingStarted/webApi/simpleApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const shoppingCartApi =
(
eventStore: EventStore,
getUnitPrice: (_productId: string) => Promise<number>,
getCurrentTime: () => Date,
): WebApiSetup =>
(router: Router) => {
// #region complete-api
Expand All @@ -55,6 +56,7 @@ export const shoppingCartApi =
unitPrice: await getUnitPrice(productId),
},
},
metadata: { now: getCurrentTime() },
};

await handle(eventStore, shoppingCartId, (state) =>
Expand Down Expand Up @@ -83,6 +85,7 @@ export const shoppingCartApi =
unitPrice: assertPositiveNumber(Number(request.query.unitPrice)),
},
},
metadata: { now: getCurrentTime() },
};

await handle(eventStore, shoppingCartId, (state) =>
Expand All @@ -104,6 +107,7 @@ export const shoppingCartApi =
const command: ConfirmShoppingCart = {
type: 'ConfirmShoppingCart',
data: { shoppingCartId },
metadata: { now: getCurrentTime() },
};

await handle(eventStore, shoppingCartId, (state) =>
Expand All @@ -125,6 +129,7 @@ export const shoppingCartApi =
const command: CancelShoppingCart = {
type: 'CancelShoppingCart',
data: { shoppingCartId },
metadata: { now: getCurrentTime() },
};

await handle(eventStore, shoppingCartId, (state) =>
Expand Down
6 changes: 5 additions & 1 deletion docs/snippets/gettingStarted/webApi/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import type { Server } from 'http';

const eventStore = getInMemoryEventStore();

const shoppingCarts = shoppingCartApi(eventStore, getUnitPrice);
const shoppingCarts = shoppingCartApi(
eventStore,
getUnitPrice,
() => new Date(),
);

const application: Application = getApplication({
apis: [shoppingCarts],
Expand Down
1 change: 1 addition & 0 deletions packages/emmett-expressjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { problemDetailsMiddleware } from './middlewares/problemDetailsMiddleware

export * from './etag';
export * from './handler';
export * from './testing';

export type ErrorToProblemDetailsMapping = (
error: Error,
Expand Down
Loading