Skip to content

Commit 5445572

Browse files
committed
Implement more tests.
1 parent 561417d commit 5445572

18 files changed

Lines changed: 434 additions & 326 deletions

.github/dependabot.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ updates:
1717
prefix-development: chore
1818

1919
- package-ecosystem: docker
20-
directory: "/test/eventsourcingdb"
20+
directory: "/test/docker"
2121
schedule:
2222
interval: weekly
2323
open-pull-requests-limit: 10

src/Client.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { isStreamError } from './stream/isStreamError.js';
1313
import { isStreamEventType } from './stream/isStreamEventType.js';
1414
import { isStreamHeartbeat } from './stream/isStreamHeartbeat.js';
1515
import { isStreamSubject } from './stream/isStreamSubject.js';
16+
import { hasShapeOf } from './types/hasShapeOf.js';
1617

1718
class Client {
1819
#url: URL;
@@ -29,35 +30,47 @@ class Client {
2930

3031
public async ping(): Promise<void> {
3132
const url = this.#getUrl('/api/v1/ping');
32-
33-
const response = await axios({
34-
url,
33+
const response = await fetch(url, {
3534
method: 'get',
36-
responseType: 'json',
3735
});
3836

39-
const eventType = 'io.eventsourcingdb.api.ping-received';
37+
if (response.status !== 200) {
38+
throw new Error(`Failed to ping, got HTTP status code '${response.status}', expected '200'.`);
39+
}
4040

41-
if (response.data.type !== eventType) {
41+
const responseBody = await response.json();
42+
if (!hasShapeOf(responseBody, { type: 'string' })) {
43+
throw new Error('Failed to parse response.');
44+
}
45+
46+
const eventType = 'io.eventsourcingdb.api.ping-received';
47+
if (responseBody.type !== eventType) {
4248
throw new Error('Failed to ping.');
4349
}
4450
}
4551

4652
public async verifyApiToken(): Promise<void> {
4753
const url = this.#getUrl('/api/v1/verify-api-token');
48-
49-
const response = await axios({
50-
url,
54+
const response = await fetch(url, {
5155
method: 'post',
5256
headers: {
5357
authorization: `Bearer ${this.#apiToken}`,
5458
},
55-
responseType: 'json',
5659
});
5760

58-
const eventType = 'io.eventsourcingdb.api.api-token-verified';
61+
if (response.status !== 200) {
62+
throw new Error(
63+
`Failed to verify API token, got HTTP status code '${response.status}', expected '200'.`,
64+
);
65+
}
66+
67+
const responseBody = await response.json();
68+
if (!hasShapeOf(responseBody, { type: 'string' })) {
69+
throw new Error('Failed to parse response.');
70+
}
5971

60-
if (response.data.type !== eventType) {
72+
const eventType = 'io.eventsourcingdb.api.api-token-verified';
73+
if (responseBody.type !== eventType) {
6174
throw new Error('Failed to verify API token.');
6275
}
6376
}
@@ -67,22 +80,25 @@ class Client {
6780
preconditions: Precondition[] = [],
6881
): Promise<Event[]> {
6982
const url = this.#getUrl('/api/v1/write-events');
70-
71-
const response = await axios({
72-
url,
83+
const response = await fetch(url, {
7384
method: 'post',
7485
headers: {
7586
authorization: `Bearer ${this.#apiToken}`,
7687
'content-type': 'application/json',
7788
},
78-
data: {
89+
body: JSON.stringify({
7990
events,
8091
preconditions,
81-
},
82-
responseType: 'json',
92+
}),
8393
});
8494

85-
const responseBody = response.data;
95+
if (response.status !== 200) {
96+
throw new Error(
97+
`Failed to write events, got HTTP status code '${response.status}', expected '200'.`,
98+
);
99+
}
100+
101+
const responseBody = await response.json();
86102

87103
if (!Array.isArray(responseBody)) {
88104
throw new Error('Failed to parse response.');

src/EventType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
interface EventType {
22
eventType: string;
33
isPhantom: boolean;
4-
schema?: string;
4+
schema?: Record<string, unknown>;
55
}
66

77
export type { EventType };
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import assert from 'node:assert/strict';
2+
import { suite, test } from 'node:test';
3+
import type { CloudEvent } from './CloudEvent.js';
4+
import { convertCloudEventToEvent } from './convertCloudEventToEvent.js';
5+
6+
suite('convertCloudEventToEvent', () => {
7+
test('converts a cloud event to an event.', () => {
8+
const now = Date.now();
9+
10+
const cloudEvent: CloudEvent = {
11+
specversion: '1.0',
12+
id: '123',
13+
time: new Date(now).toISOString(),
14+
source: 'https://www.eventsourcingdb.io',
15+
subject: '/test',
16+
type: 'io.eventsourcingdb.test',
17+
datacontenttype: 'application/json',
18+
data: { key: 'value' },
19+
hash: '55a1f59420da66b2c4c87b565660054cff7c2aad5ebe5f56e04ae0f2a20f00a9',
20+
predecessorhash: '4f67e993373952b6b6733a9b99de21842c42ed68ff881169ac914488b49dfeef',
21+
};
22+
23+
const event = convertCloudEventToEvent(cloudEvent);
24+
25+
assert.strictEqual(event.specversion, cloudEvent.specversion);
26+
assert.strictEqual(event.id, cloudEvent.id);
27+
assert.deepStrictEqual(event.time, new Date(now));
28+
assert.strictEqual(event.source, cloudEvent.source);
29+
assert.strictEqual(event.subject, cloudEvent.subject);
30+
assert.strictEqual(event.type, cloudEvent.type);
31+
assert.strictEqual(event.datacontenttype, cloudEvent.datacontenttype);
32+
assert.deepStrictEqual(event.data, cloudEvent.data);
33+
assert.deepStrictEqual(event.hash, cloudEvent.hash);
34+
assert.deepStrictEqual(event.predecessorhash, cloudEvent.predecessorhash);
35+
});
36+
});

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Client } from './Client.js';
2+
import type { EventCandidate } from './EventCandidate.js';
23
import type { ObserveEventsOptions } from './ObserveEventsOptions.js';
34
import type { ReadEventsOptions } from './ReadEventsOptions.js';
45
import { isSubjectOnEventId } from './isSubjectOnEventId.js';
56
import { isSubjectPristine } from './isSubjectPristine.js';
67

78
export { Client, isSubjectPristine, isSubjectOnEventId };
8-
export type { ReadEventsOptions, ObserveEventsOptions };
9+
export type { EventCandidate, ReadEventsOptions, ObserveEventsOptions };

src/isCloudEvent.ts

Lines changed: 15 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,21 @@
11
import type { CloudEvent } from './CloudEvent.js';
2+
import { hasShapeOf } from './types/hasShapeOf.js';
3+
4+
const blueprint: CloudEvent = {
5+
specversion: 'string',
6+
id: 'string',
7+
time: 'string',
8+
source: 'string',
9+
subject: 'string',
10+
type: 'string',
11+
datacontenttype: 'string',
12+
data: {},
13+
hash: 'string',
14+
predecessorhash: 'string',
15+
};
216

3-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: This function is long, but the complexity is unfortunately needed.
417
const isCloudEvent = (value: unknown): value is CloudEvent => {
5-
if (typeof value !== 'object') {
6-
return false;
7-
}
8-
if (value === null) {
9-
return false;
10-
}
11-
if (Array.isArray(value)) {
12-
return false;
13-
}
14-
15-
if (!('specversion' in value)) {
16-
return false;
17-
}
18-
if (typeof value.specversion !== 'string') {
19-
return false;
20-
}
21-
22-
if (!('id' in value)) {
23-
return false;
24-
}
25-
if (typeof value.id !== 'string') {
26-
return false;
27-
}
28-
29-
if (!('time' in value)) {
30-
return false;
31-
}
32-
if (typeof value.time !== 'string') {
33-
return false;
34-
}
35-
36-
if (!('source' in value)) {
37-
return false;
38-
}
39-
if (typeof value.source !== 'string') {
40-
return false;
41-
}
42-
43-
if (!('subject' in value)) {
44-
return false;
45-
}
46-
if (typeof value.subject !== 'string') {
47-
return false;
48-
}
49-
50-
if (!('type' in value)) {
51-
return false;
52-
}
53-
if (typeof value.type !== 'string') {
54-
return false;
55-
}
56-
57-
if (!('datacontenttype' in value)) {
58-
return false;
59-
}
60-
if (typeof value.datacontenttype !== 'string') {
61-
return false;
62-
}
63-
64-
if (!('data' in value)) {
65-
return false;
66-
}
67-
if (typeof value.data !== 'object') {
68-
return false;
69-
}
70-
if (value.data === null) {
71-
return false;
72-
}
73-
if (Array.isArray(value.data)) {
74-
return false;
75-
}
76-
77-
if (!('hash' in value)) {
78-
return false;
79-
}
80-
if (typeof value.hash !== 'string') {
81-
return false;
82-
}
83-
84-
if (!('predecessorhash' in value)) {
85-
return false;
86-
}
87-
if (typeof value.predecessorhash !== 'string') {
88-
return false;
89-
}
90-
91-
if ('traceparent' in value) {
92-
if (typeof value.traceparent !== 'string') {
93-
return false;
94-
}
95-
}
96-
if ('tracestate' in value) {
97-
if (typeof value.tracestate !== 'string') {
98-
return false;
99-
}
100-
}
101-
102-
return true;
18+
return hasShapeOf(value, blueprint);
10319
};
10420

10521
export { isCloudEvent };

src/stream/StreamEventType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ interface StreamEventType {
33
payload: {
44
eventType: string;
55
isPhantom: boolean;
6-
schema?: string;
6+
schema?: Record<string, unknown>;
77
};
88
}
99

src/stream/isStreamCloudEvent.ts

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,24 @@
1-
import { isCloudEvent } from '../isCloudEvent.js';
1+
import { hasShapeOf } from '../types/hasShapeOf.js';
22
import type { StreamCloudEvent } from './StreamCloudEvent.js';
33

4-
const isStreamCloudEvent = (line: unknown): line is StreamCloudEvent => {
5-
if (typeof line !== 'object') {
6-
return false;
7-
}
8-
if (line === null) {
9-
return false;
10-
}
11-
if (Array.isArray(line)) {
12-
return false;
13-
}
14-
15-
if (!('type' in line)) {
16-
return false;
17-
}
18-
if (line.type !== 'event') {
19-
return false;
20-
}
21-
22-
if (!('payload' in line)) {
23-
return false;
24-
}
25-
if (typeof line.payload !== 'object') {
26-
return false;
27-
}
28-
if (line.payload === null) {
29-
return false;
30-
}
31-
if (Array.isArray(line.payload)) {
32-
return false;
33-
}
34-
35-
if (!isCloudEvent(line.payload)) {
36-
return false;
37-
}
4+
const blueprint: StreamCloudEvent = {
5+
type: 'event',
6+
payload: {
7+
specversion: 'string',
8+
id: 'string',
9+
time: 'string',
10+
source: 'string',
11+
subject: 'string',
12+
type: 'string',
13+
datacontenttype: 'string',
14+
data: {},
15+
hash: 'string',
16+
predecessorhash: 'string',
17+
},
18+
};
3819

39-
return true;
20+
const isStreamCloudEvent = (line: unknown): line is StreamCloudEvent => {
21+
return hasShapeOf(line, blueprint);
4022
};
4123

4224
export { isStreamCloudEvent };

0 commit comments

Comments
 (0)