Skip to content

Commit 755955c

Browse files
committed
Added integration and api tests
1 parent c5ea59b commit 755955c

File tree

11 files changed

+323
-905
lines changed

11 files changed

+323
-905
lines changed

src/guestStayAccounts/api.e2e.spec.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/* eslint-disable @typescript-eslint/no-floating-promises */
2+
import { type EventStore } from '@event-driven-io/emmett';
3+
import { getEventStoreDBEventStore } from '@event-driven-io/emmett-esdb';
4+
import {
5+
ApiE2ESpecification,
6+
expectResponse,
7+
getApplication,
8+
} from '@event-driven-io/emmett-expressjs';
9+
import {
10+
EventStoreDBContainer,
11+
StartedEventStoreDBContainer,
12+
} from '@event-driven-io/emmett-testcontainers';
13+
import { randomUUID } from 'node:crypto';
14+
import { after, before, beforeEach, describe, it } from 'node:test';
15+
import { guestStayAccountsApi } from './api';
16+
17+
const doesGuestStayExist = (_guestId: string, _roomId: string, _day: Date) =>
18+
Promise.resolve(true);
19+
20+
describe('guestStayAccount E2E', () => {
21+
// const oldTime = new Date();
22+
const now = new Date();
23+
// const formattedNow = formatDateToUtcYYYYMMDD(now);
24+
25+
let guestId: string;
26+
let roomId: string;
27+
// let guestStayAccountId: string;
28+
// const amount = Math.random() * 100;
29+
30+
let esdbContainer: StartedEventStoreDBContainer;
31+
let given: ApiE2ESpecification;
32+
33+
before(async () => {
34+
esdbContainer = await new EventStoreDBContainer().start();
35+
36+
given = ApiE2ESpecification.for(
37+
(): EventStore => getEventStoreDBEventStore(esdbContainer.getClient()),
38+
(eventStore: EventStore) =>
39+
getApplication({
40+
apis: [
41+
guestStayAccountsApi(eventStore, doesGuestStayExist, () => now),
42+
],
43+
}),
44+
);
45+
});
46+
47+
beforeEach(() => {
48+
guestId = randomUUID();
49+
roomId = randomUUID();
50+
// guestStayAccountId = toGuestStayAccountId(guestId, roomId, now);
51+
});
52+
53+
after(() => {
54+
return esdbContainer.stop();
55+
});
56+
57+
describe('When empty', () => {
58+
it('should add product item', () => {
59+
return given()
60+
.when((request) => request.post(`/guests/${guestId}/stays/${roomId}`))
61+
.then([expectResponse(201)]);
62+
});
63+
});
64+
65+
// describe('When empty', () => {
66+
// it('should add product item', () => {
67+
// return given((request) =>
68+
// request
69+
// .post(`/clients/${clientId}/shopping-carts/current/product-items`)
70+
// .send(productItem),
71+
// )
72+
// .when((request) =>
73+
// request.get(`/clients/${clientId}/shopping-carts/current`).send(),
74+
// )
75+
// .then([
76+
// expectResponse(200, {
77+
// body: {
78+
// clientId,
79+
// id: shoppingCartId,
80+
// productItems: [
81+
// {
82+
// quantity: productItem.quantity,
83+
// productId: productItem.productId,
84+
// },
85+
// ],
86+
// status: 'Opened',
87+
// },
88+
// }),
89+
// ]);
90+
// });
91+
// });
92+
});

src/guestStayAccounts/api.int.spec.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import {
2+
getInMemoryEventStore,
3+
type EventStore,
4+
} from '@event-driven-io/emmett';
5+
import {
6+
ApiSpecification,
7+
expectError,
8+
expectNewEvents,
9+
expectResponse,
10+
getApplication,
11+
} from '@event-driven-io/emmett-expressjs';
12+
import type { TestEventStream } from '@event-driven-io/emmett-expressjs/dist/testing/utils';
13+
import { randomUUID } from 'node:crypto';
14+
import { beforeEach, describe, it } from 'node:test';
15+
import { formatDateToUtcYYYYMMDD } from '../core/dates';
16+
import { guestStayAccountsApi } from './api';
17+
import {
18+
toGuestStayAccountId,
19+
type GuestStayAccountEvent,
20+
} from './guestStayAccount';
21+
22+
const doesGuestStayExist = (_guestId: string, _roomId: string, _day: Date) =>
23+
Promise.resolve(true);
24+
25+
void describe('Guest stay account', () => {
26+
//const oldTime = new Date();
27+
const now = new Date();
28+
const formattedNow = formatDateToUtcYYYYMMDD(now);
29+
30+
let guestId: string;
31+
let roomId: string;
32+
let guestStayAccountId: string;
33+
const amount = Math.random() * 100;
34+
35+
beforeEach(() => {
36+
guestId = randomUUID();
37+
roomId = randomUUID();
38+
guestStayAccountId = toGuestStayAccountId(guestId, roomId, now);
39+
});
40+
41+
void describe('When not existing', () => {
42+
const notExistingAccount: TestEventStream<GuestStayAccountEvent>[] = [];
43+
44+
void it('checks in', () =>
45+
given(...notExistingAccount)
46+
.when((request) => request.post(`/guests/${guestId}/stays/${roomId}`))
47+
.then([
48+
expectResponse(201),
49+
expectNewEvents(guestStayAccountId, [
50+
{
51+
type: 'GuestCheckedIn',
52+
data: {
53+
guestStayAccountId,
54+
guestId,
55+
roomId,
56+
checkedInAt: now,
57+
},
58+
},
59+
]),
60+
]));
61+
62+
void it(`doesn't record charge`, () =>
63+
given(...notExistingAccount)
64+
.when((request) =>
65+
request
66+
.post(
67+
`/guests/${guestId}/stays/${roomId}/periods/${formattedNow}/charges`,
68+
)
69+
.send({ amount }),
70+
)
71+
.then(
72+
expectError(403, {
73+
detail: `Guest account doesn't exist!`,
74+
status: 403,
75+
title: 'Forbidden',
76+
type: 'about:blank',
77+
}),
78+
));
79+
80+
void it(`doesn't record payment`, () =>
81+
given(...notExistingAccount)
82+
.when((request) =>
83+
request
84+
.post(
85+
`/guests/${guestId}/stays/${roomId}/periods/${formattedNow}/payments`,
86+
)
87+
.send({ amount }),
88+
)
89+
.then(
90+
expectError(403, {
91+
detail: `Guest account doesn't exist!`,
92+
status: 403,
93+
title: 'Forbidden',
94+
type: 'about:blank',
95+
}),
96+
));
97+
98+
void it(`doesn't checkout`, () =>
99+
given(...notExistingAccount)
100+
.when((request) =>
101+
request.delete(
102+
`/guests/${guestId}/stays/${roomId}/periods/${formattedNow}`,
103+
),
104+
)
105+
.then(expectError(403)));
106+
});
107+
108+
// void describe('When opened with product item', () => {
109+
// void it('should confirm', () => {
110+
// return given(
111+
// existingStream(guestStayAccountId, [
112+
// {
113+
// type: 'ProductItemAddedToShoppingCart',
114+
// data: {
115+
// guestStayAccountId,
116+
// productItem,
117+
// addedAt: oldTime,
118+
// },
119+
// },
120+
// ]),
121+
// )
122+
// .when((request) =>
123+
// request.post(`/clients/${clientId}/shopping-carts/current/confirm`),
124+
// )
125+
// .then([
126+
// expectResponse(204),
127+
// expectNewEvents(guestStayAccountId, [
128+
// {
129+
// type: 'ShoppingCartConfirmed',
130+
// data: {
131+
// guestStayAccountId,
132+
// confirmedAt: now,
133+
// },
134+
// },
135+
// ]),
136+
// ]);
137+
// });
138+
// });
139+
140+
// void describe('When confirmed', () => {
141+
// void it('should not add products', () => {
142+
// return given(
143+
// existingStream(guestStayAccountId, [
144+
// {
145+
// type: 'ProductItemAddedToShoppingCart',
146+
// data: {
147+
// guestStayAccountId,
148+
// productItem,
149+
// addedAt: oldTime,
150+
// },
151+
// },
152+
// {
153+
// type: 'ShoppingCartConfirmed',
154+
// data: { guestStayAccountId, confirmedAt: oldTime },
155+
// },
156+
// ]),
157+
// )
158+
// .when((request) =>
159+
// request
160+
// .post(`/clients/${clientId}/shopping-carts/current/product-items`)
161+
// .send(productItem),
162+
// )
163+
// .then(
164+
// expectError(403, {
165+
// detail: 'Shopping Cart already closed',
166+
// status: 403,
167+
// title: 'Forbidden',
168+
// type: 'about:blank',
169+
// }),
170+
// );
171+
// });
172+
// });
173+
174+
const given = ApiSpecification.for<GuestStayAccountEvent>(
175+
(): EventStore => getInMemoryEventStore(),
176+
(eventStore: EventStore) =>
177+
getApplication({
178+
apis: [guestStayAccountsApi(eventStore, doesGuestStayExist, () => now)],
179+
}),
180+
);
181+
});

src/guestStayAccounts/api.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from '@event-driven-io/emmett';
77
import {
88
Created,
9+
Forbidden,
910
NoContent,
1011
NotFound,
1112
OK,
@@ -64,7 +65,11 @@ type GetShoppingCartRequest = Request<
6465
export const guestStayAccountsApi =
6566
(
6667
eventStore: EventStore,
67-
getUnitPrice: (_productId: string) => Promise<number>,
68+
doesGuestStayExist: (
69+
guestId: string,
70+
roomId: string,
71+
day: Date,
72+
) => Promise<boolean>,
6873
getCurrentTime: () => Date,
6974
): WebApiSetup =>
7075
(router: Router) => {
@@ -76,6 +81,9 @@ export const guestStayAccountsApi =
7681
const roomId = assertNotEmptyString(request.params.roomId);
7782
const now = getCurrentTime();
7883

84+
if (!(await doesGuestStayExist(guestId, roomId, now)))
85+
return Forbidden();
86+
7987
const guestStayAccountId = toGuestStayAccountId(guestId, roomId, now);
8088

8189
const command: CheckIn = {
@@ -122,7 +130,7 @@ export const guestStayAccountsApi =
122130
}),
123131
);
124132

125-
// Confirm Shopping Cart
133+
// Record Payment
126134
router.post(
127135
'/guests/:guestId/stays/:roomId/periods/:checkInDate/payments',
128136
on(async (request: RecordPaymentRequest) => {
@@ -157,11 +165,17 @@ export const guestStayAccountsApi =
157165
metadata: { now: getCurrentTime() },
158166
};
159167

160-
await handle(eventStore, guestStayAccountId, (state) =>
161-
checkOut(command, state),
162-
);
168+
let wasCheckedOut = false;
163169

164-
return NoContent();
170+
await handle(eventStore, guestStayAccountId, (state) => {
171+
const result = checkOut(command, state);
172+
173+
wasCheckedOut = result.type === 'GuestCheckedOut';
174+
175+
return result;
176+
});
177+
178+
return wasCheckedOut ? NoContent() : Forbidden();
165179
}),
166180
);
167181

src/guestStayAccounts/businessLogic.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Command } from '@event-driven-io/emmett';
1+
import { IllegalStateError, type Command } from '@event-driven-io/emmett';
22
import type {
33
ChargeRecorded,
44
GuestCheckedIn,
@@ -136,20 +136,21 @@ export const checkOut = (
136136
};
137137

138138
const assertDoesNotExist = (state: GuestStayAccount): state is Opened => {
139-
if (state.status === 'Opened') throw Error(`Guest is already checked-in!`);
139+
if (state.status === 'Opened')
140+
throw new IllegalStateError(`Guest is already checked-in!`);
140141

141142
if (state.status === 'CheckedOut')
142-
throw Error(`Guest account is already checked out`);
143+
throw new IllegalStateError(`Guest account is already checked out`);
143144

144145
return true;
145146
};
146147

147148
const assertIsOpened = (state: GuestStayAccount): state is Opened => {
148149
if (state.status === 'NotExisting')
149-
throw Error(`Guest account doesn't exist!`);
150+
throw new IllegalStateError(`Guest account doesn't exist!`);
150151

151152
if (state.status === 'CheckedOut')
152-
throw Error(`Guest account is already checked out`);
153+
throw new IllegalStateError(`Guest account is already checked out`);
153154

154155
return true;
155156
};

0 commit comments

Comments
 (0)