Skip to content

Commit

Permalink
Added first draft of Api Specification
Browse files Browse the repository at this point in the history
  • Loading branch information
oskardudycz committed Feb 24, 2024
1 parent 4394de0 commit f18c721
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 1 deletion.
126 changes: 126 additions & 0 deletions docs/snippets/gettingStarted/webApi/apiBDD.int.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import {
getInMemoryEventStore,
type EventStore,
} from '@event-driven-io/emmett';
import {
ApiSpecification,
getApplication,
} from '@event-driven-io/emmett-expressjs';
import { 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);
};

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

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

// describe('When opened', () => {
// it('should confirm', () => {
// given({
// type: 'ProductItemAddedToShoppingCart',
// data: {
// shoppingCartId,
// productItem,
// addedAt: oldTime,
// },
// })
// .when({
// type: 'AddProductItemToShoppingCart',
// data: {
// shoppingCartId,
// productItem,
// },
// metadata: { now },
// })
// .then([
// {
// type: 'ProductItemAddedToShoppingCart',
// data: {
// shoppingCartId,
// productItem,
// addedAt: now,
// },
// },
// ]);
// });
// });

// describe('When confirmed', () => {
// it('should not add products', () => {
// given([
// {
// type: 'ProductItemAddedToShoppingCart',
// data: {
// shoppingCartId,
// productItem,
// addedAt: oldTime,
// },
// },
// {
// type: 'ShoppingCartConfirmed',
// data: { shoppingCartId, confirmedAt: oldTime },
// },
// ])
// .when({
// type: 'AddProductItemToShoppingCart',
// data: {
// shoppingCartId,
// productItem,
// },
// metadata: { now },
// })
// .thenThrows(
// (error: Error) => error.message === 'Shopping Cart already closed',
// );
// });
// });

const getRandomProduct = (): PricedProductItem => {
return {
productId: uuid(),
unitPrice: Math.random() * 10,
quantity: Math.random() * 10,
};
};
const oldTime = new Date();
const now = new Date();
const shoppingCartId = uuid();

const productItem = getRandomProduct();
});
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
72 changes: 72 additions & 0 deletions packages/emmett-expressjs/src/testing/apiSpecification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { Event, EventStore, Flavour } from '@event-driven-io/emmett';
import { type Application } from 'express';
import assert from 'node:assert/strict';
import type { Response, Test } from 'supertest';
import supertest from 'supertest';
import type TestAgent from 'supertest/lib/agent';

export type TestEventStream<EventType extends Event = Event> = {
streamName: string;
events: EventType[];
};

export type ApiSpecification<EventType extends Event = Event> = (
givenStreams: TestEventStream<EventType>[],
) => {
when: (setupRequest: (request: TestAgent<supertest.Test>) => Test) => {
then: (verify: ApiSpecificationAssert<EventType>) => Promise<void>;
};
};

export type ApiSpecificationAssert<EventType extends Event = Event> =
| Flavour<TestEventStream<EventType>[], 'appendedEvents'>
| Flavour<(response: Response) => boolean, 'responseAssert'>
| Flavour<
{
events: TestEventStream<EventType>[];
responseMatches: (response: Response) => boolean;
},
'fullAssert'
>;

export const ApiSpecification = {
for: <EventType extends Event = Event>(
getEventStore: () => EventStore,
getApplication: (eventStore: EventStore) => Application,
): ApiSpecification<EventType> => {
{
return (givenStreams: TestEventStream<EventType>[]) => {
return {
when: (
setupRequest: (request: TestAgent<supertest.Test>) => Test,
) => {
const handle = async () => {
const eventStore = getEventStore();
const application = getApplication(eventStore);

for (const { streamName, events } of givenStreams) {
await eventStore.appendToStream(streamName, events);
}

return setupRequest(supertest(application));
};

return {
then: async (
verify: ApiSpecificationAssert<EventType>,
): Promise<void> => {
const response = await handle();

if (verify.__brand === 'responseAssert') {
assert.ok(verify(response));
} else if (verify.__brand === 'fullAssert') {
assert.ok(verify.responseMatches(response));
}
},
};
},
};
};
}
},
};
1 change: 1 addition & 0 deletions packages/emmett-expressjs/src/testing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './apiSpecification';
2 changes: 1 addition & 1 deletion packages/emmett/src/eventStore/eventStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface EventStore<StreamVersion = DefaultStreamVersionType> {
): Promise<ReadStreamResult<EventType, StreamVersion>>;

appendToStream<EventType extends Event>(
streamId: string,
streamName: string,
events: EventType[],
options?: AppendToStreamOptions<StreamVersion>,
): Promise<AppendToStreamResult<StreamVersion>>;
Expand Down

0 comments on commit f18c721

Please sign in to comment.