@codeleap/debug 5.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +21 -0
- package/package.json.bak +21 -0
- package/src/FakeRestApi/class.ts +243 -0
- package/src/FakeRestApi/factor.ts +29 -0
- package/src/FakeRestApi/index.ts +3 -0
- package/src/FakeRestApi/tests.spec.ts +368 -0
- package/src/FakeRestApi/types.ts +41 -0
- package/src/index.ts +1 -0
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeleap/debug",
|
|
3
|
+
"version": "5.8.8",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
|
|
8
|
+
"type": "git",
|
|
9
|
+
"directory": "packages/debug"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@codeleap/config": "5.8.8",
|
|
13
|
+
"ts-node-dev": "1.1.8"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "echo 'No build needed'"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"typescript": "5.5.2"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json.bak
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeleap/debug",
|
|
3
|
+
"version": "5.8.8",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
|
|
8
|
+
"type": "git",
|
|
9
|
+
"directory": "packages/debug"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@codeleap/config": "workspace:*",
|
|
13
|
+
"ts-node-dev": "1.1.8"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "echo 'No build needed'"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"typescript": "5.5.2"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { FakeItem, FakeRestApiOptions, PaginationResponse } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A fake REST API implementation for testing and prototyping.
|
|
5
|
+
* Provides CRUD operations with pagination support and optional request delays.
|
|
6
|
+
* @template T - The type of items to manage, must extend FakeItem
|
|
7
|
+
* @template F - The type of filters to apply when listing items
|
|
8
|
+
*/
|
|
9
|
+
export class FakeRestApi<T extends FakeItem, F = Record<string, unknown>> {
|
|
10
|
+
private data: T[] = []
|
|
11
|
+
private options: Required<FakeRestApiOptions<T, F>>
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Gets the ID of the last item in the data array.
|
|
15
|
+
* @private
|
|
16
|
+
* @returns The highest ID or 0 if no items exist
|
|
17
|
+
*/
|
|
18
|
+
private get lastId(): T['id'] {
|
|
19
|
+
return this.data.length > 0 ? Math.max(...this.data.map((i) => i.id)) : 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Gets the name of the API.
|
|
24
|
+
* @returns The API name
|
|
25
|
+
*/
|
|
26
|
+
public get name(): string {
|
|
27
|
+
return this.options.name
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Gets the current count of items.
|
|
32
|
+
* @returns The number of items in the data array
|
|
33
|
+
*/
|
|
34
|
+
public get count(): number {
|
|
35
|
+
return this.data.length
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates a new FakeRestApi instance.
|
|
40
|
+
* @param options - Configuration options for the API
|
|
41
|
+
*/
|
|
42
|
+
constructor(options: FakeRestApiOptions<T, F>) {
|
|
43
|
+
this.options = {
|
|
44
|
+
delayMs: 2500,
|
|
45
|
+
maxCount: 100,
|
|
46
|
+
enableDelay: false,
|
|
47
|
+
filterFn: () => true,
|
|
48
|
+
...options,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.initialize()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initializes the data array with items up to maxCount.
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
private initialize(): void {
|
|
59
|
+
this.data = Array.from({ length: this.options.maxCount }, (_, i) =>
|
|
60
|
+
this.generateItem(i + 1)
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Updates the API options partially.
|
|
66
|
+
* @param newOptions - Partial options to update
|
|
67
|
+
*/
|
|
68
|
+
public setOptions(newOptions: Partial<FakeRestApiOptions<T, F>>): void {
|
|
69
|
+
this.options = { ...this.options, ...newOptions }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generates a new item using the generator function.
|
|
74
|
+
* @param id - The ID for the new item (defaults to lastId + 1)
|
|
75
|
+
* @returns A newly generated item
|
|
76
|
+
*/
|
|
77
|
+
public generateItem(id: T['id'] = this.lastId + 1): T {
|
|
78
|
+
return this.options.generatorFn(id)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Introduces an artificial delay if enabled in options.
|
|
83
|
+
* @private
|
|
84
|
+
* @returns A promise that resolves after the delay period
|
|
85
|
+
*/
|
|
86
|
+
private async delay(): Promise<void> {
|
|
87
|
+
if (!this.options.enableDelay) return
|
|
88
|
+
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
setTimeout(resolve, this.options.delayMs)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Builds a URL with pagination parameters.
|
|
96
|
+
* @private
|
|
97
|
+
* @param limit - Number of items per page
|
|
98
|
+
* @param offset - Starting position
|
|
99
|
+
* @returns A formatted URL string
|
|
100
|
+
*/
|
|
101
|
+
private buildUrl(limit: number, offset: number): string {
|
|
102
|
+
return `https://api.${this.name}?limit=${limit}&offset=${offset}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Retrieves a paginated list of items with optional filtering.
|
|
107
|
+
* @param limit - Number of items to return (default: 10)
|
|
108
|
+
* @param offset - Starting position (default: 0)
|
|
109
|
+
* @param filters - Optional filters to apply
|
|
110
|
+
* @returns A promise that resolves to a paginated response
|
|
111
|
+
*/
|
|
112
|
+
async listItems(
|
|
113
|
+
limit = 10,
|
|
114
|
+
offset = 0,
|
|
115
|
+
filters?: F
|
|
116
|
+
): Promise<PaginationResponse<T>> {
|
|
117
|
+
await this.delay()
|
|
118
|
+
|
|
119
|
+
const hasFilters = filters && Object.keys(filters).length > 0
|
|
120
|
+
const items = hasFilters
|
|
121
|
+
? this.data.filter((item) => this.options.filterFn(item, filters))
|
|
122
|
+
: this.data
|
|
123
|
+
|
|
124
|
+
const total = items.length
|
|
125
|
+
const start = Math.max(0, offset)
|
|
126
|
+
const end = Math.min(start + limit, total)
|
|
127
|
+
|
|
128
|
+
const results = items.slice(start, end)
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
count: total,
|
|
132
|
+
next: end < total ? this.buildUrl(limit, end) : null,
|
|
133
|
+
previous: start > 0 ? this.buildUrl(limit, Math.max(0, start - limit)) : null,
|
|
134
|
+
results,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Retrieves a single item by ID.
|
|
140
|
+
* @param id - The ID of the item to retrieve
|
|
141
|
+
* @returns A promise that resolves to the requested item
|
|
142
|
+
* @throws {Error} If the item is not found
|
|
143
|
+
*/
|
|
144
|
+
async retrieveItem(id: T['id']): Promise<T> {
|
|
145
|
+
await this.delay()
|
|
146
|
+
|
|
147
|
+
const item = this.data.find((i) => i.id === id)
|
|
148
|
+
|
|
149
|
+
if (!item) {
|
|
150
|
+
throw new Error(`${this.name} with id ${id} not found`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return item
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Creates a new item and adds it to the data array.
|
|
158
|
+
* @param item - Optional partial item data (ID will be auto-generated)
|
|
159
|
+
* @returns A promise that resolves to the created item
|
|
160
|
+
*/
|
|
161
|
+
async createItem(item?: Partial<T>): Promise<T> {
|
|
162
|
+
await this.delay()
|
|
163
|
+
|
|
164
|
+
const id = this.lastId + 1
|
|
165
|
+
const newItem = item ? { ...item, id } as T : this.generateItem(id)
|
|
166
|
+
|
|
167
|
+
this.data.push(newItem)
|
|
168
|
+
|
|
169
|
+
return newItem
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Updates an existing item by ID.
|
|
174
|
+
* @param id - The ID of the item to update
|
|
175
|
+
* @param updates - Partial item data to update
|
|
176
|
+
* @returns A promise that resolves to the updated item
|
|
177
|
+
* @throws {Error} If the item is not found
|
|
178
|
+
*/
|
|
179
|
+
async updateItem(id: T['id'], updates: Partial<T>): Promise<T> {
|
|
180
|
+
await this.delay()
|
|
181
|
+
|
|
182
|
+
const index = this.data.findIndex((i) => i.id === id)
|
|
183
|
+
|
|
184
|
+
if (index === -1) {
|
|
185
|
+
throw new Error(`${this.name} with id ${id} not found`)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.data[index] = { ...this.data[index], ...updates, id }
|
|
189
|
+
|
|
190
|
+
return this.data[index]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Deletes an item by ID.
|
|
195
|
+
* @param id - The ID of the item to delete
|
|
196
|
+
* @returns A promise that resolves to the deleted item
|
|
197
|
+
* @throws {Error} If the item is not found
|
|
198
|
+
*/
|
|
199
|
+
async deleteItem(id: T['id']): Promise<T> {
|
|
200
|
+
await this.delay()
|
|
201
|
+
|
|
202
|
+
const index = this.data.findIndex((i) => i.id === id)
|
|
203
|
+
|
|
204
|
+
if (index === -1) {
|
|
205
|
+
throw new Error(`${this.name} with id ${id} not found`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const [deleted] = this.data.splice(index, 1)
|
|
209
|
+
|
|
210
|
+
return deleted
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Resets the data array to its initial state by regenerating all items.
|
|
215
|
+
*/
|
|
216
|
+
public reset(): void {
|
|
217
|
+
this.initialize()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Clears all items from the data array.
|
|
222
|
+
*/
|
|
223
|
+
public clear(): void {
|
|
224
|
+
this.data = []
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Gets a readonly copy of all items in the data array.
|
|
229
|
+
* @returns A frozen array of all items
|
|
230
|
+
*/
|
|
231
|
+
public getData(): readonly T[] {
|
|
232
|
+
return Object.freeze([...this.data])
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Gets a single item by ID without delay.
|
|
237
|
+
* @param id - The ID of the item to retrieve
|
|
238
|
+
* @returns The item if found, undefined otherwise
|
|
239
|
+
*/
|
|
240
|
+
public getItem(id: T['id']): T | undefined {
|
|
241
|
+
return this.data.find((i) => i.id === id)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { FakeRestApi } from './class'
|
|
2
|
+
import { FakeItem, FakeRestApiOptions } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Factory function to create a new FakeRestApi instance.
|
|
6
|
+
* @template T - The type of items to manage, must extend FakeItem
|
|
7
|
+
* @template F - The type of filters to apply when listing items
|
|
8
|
+
* @param options - Configuration options for the API
|
|
9
|
+
* @returns A new FakeRestApi instance
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* type User = { id: number; name: string; email: string }
|
|
13
|
+
*
|
|
14
|
+
* const api = createFakeRestApi<User>({
|
|
15
|
+
* name: 'users',
|
|
16
|
+
* maxCount: 50,
|
|
17
|
+
* generatorFn: (id) => ({
|
|
18
|
+
* id,
|
|
19
|
+
* name: `User ${id}`,
|
|
20
|
+
* email: `user${id}@example.com`
|
|
21
|
+
* })
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function createFakeRestApi<T extends FakeItem, F = Record<string, unknown>>(
|
|
26
|
+
options: FakeRestApiOptions<T, F>
|
|
27
|
+
): FakeRestApi<T, F> {
|
|
28
|
+
return new FakeRestApi<T, F>(options)
|
|
29
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test'
|
|
2
|
+
import { FakeRestApi } from './class'
|
|
3
|
+
import { createFakeRestApi } from './factor'
|
|
4
|
+
|
|
5
|
+
type User = {
|
|
6
|
+
id: number
|
|
7
|
+
name: string
|
|
8
|
+
email: string
|
|
9
|
+
age: number
|
|
10
|
+
active: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type UserFilters = {
|
|
14
|
+
name?: string
|
|
15
|
+
minAge?: number
|
|
16
|
+
active?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('FakeRestApi', () => {
|
|
20
|
+
let api: FakeRestApi<User, UserFilters>
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
api = createFakeRestApi<User, UserFilters>({
|
|
24
|
+
name: 'users',
|
|
25
|
+
enableDelay: false,
|
|
26
|
+
maxCount: 20,
|
|
27
|
+
generatorFn: (id) => ({
|
|
28
|
+
id,
|
|
29
|
+
name: `User ${id}`,
|
|
30
|
+
email: `user${id}@example.com`,
|
|
31
|
+
age: 20 + id,
|
|
32
|
+
active: id % 2 === 0,
|
|
33
|
+
}),
|
|
34
|
+
filterFn: (item, filters) => {
|
|
35
|
+
if (filters.name && !item.name.toLowerCase().includes(filters.name.toLowerCase())) {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (filters.minAge !== undefined && item.age < filters.minAge) {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (filters.active !== undefined && item.active !== filters.active) {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return true
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('Constructor', () => {
|
|
53
|
+
test('should create the API with default settings', () => {
|
|
54
|
+
expect(api.name).toBe('users')
|
|
55
|
+
expect(api.count).toBe(20)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('should automatically generate initial items', () => {
|
|
59
|
+
const data = api.getData()
|
|
60
|
+
expect(data).toHaveLength(20)
|
|
61
|
+
expect(data[0].id).toBe(1)
|
|
62
|
+
expect(data[0].name).toBe('User 1')
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('listItems', () => {
|
|
67
|
+
test('should list items with default pagination', async () => {
|
|
68
|
+
const result = await api.listItems()
|
|
69
|
+
|
|
70
|
+
expect(result.count).toBe(20)
|
|
71
|
+
expect(result.results).toHaveLength(10)
|
|
72
|
+
expect(result.next).toBe('https://api.users?limit=10&offset=10')
|
|
73
|
+
expect(result.previous).toBeNull()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('should respect limit and offset', async () => {
|
|
77
|
+
const result = await api.listItems(5, 10)
|
|
78
|
+
|
|
79
|
+
expect(result.results).toHaveLength(5)
|
|
80
|
+
expect(result.results[0].id).toBe(11)
|
|
81
|
+
expect(result.next).toBe('https://api.users?limit=5&offset=15')
|
|
82
|
+
expect(result.previous).toBe('https://api.users?limit=5&offset=5')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('should return null for next when there are no more items', async () => {
|
|
86
|
+
const result = await api.listItems(10, 15)
|
|
87
|
+
|
|
88
|
+
expect(result.results).toHaveLength(5)
|
|
89
|
+
expect(result.next).toBeNull()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('should return null for previous when offset is 0', async () => {
|
|
93
|
+
const result = await api.listItems(10, 0)
|
|
94
|
+
|
|
95
|
+
expect(result.previous).toBeNull()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('should filter by name', async () => {
|
|
99
|
+
const result = await api.listItems(20, 0, { name: 'User 1' })
|
|
100
|
+
|
|
101
|
+
expect(result.count).toBe(11)
|
|
102
|
+
expect(result.results.every(u => u.name.includes('User 1'))).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('should filter by minimum age', async () => {
|
|
106
|
+
const result = await api.listItems(20, 0, { minAge: 30 })
|
|
107
|
+
|
|
108
|
+
expect(result.results.every(u => u.age >= 30)).toBe(true)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('should filter by active status', async () => {
|
|
112
|
+
const result = await api.listItems(20, 0, { active: true })
|
|
113
|
+
|
|
114
|
+
expect(result.results.every(u => u.active === true)).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('should apply multiple filters', async () => {
|
|
118
|
+
const result = await api.listItems(20, 0, { active: true, minAge: 25 })
|
|
119
|
+
|
|
120
|
+
expect(result.results.every(u => u.active === true && u.age >= 25)).toBe(true)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('should handle negative offset', async () => {
|
|
124
|
+
const result = await api.listItems(10, -5)
|
|
125
|
+
|
|
126
|
+
expect(result.results[0].id).toBe(1)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('retrieveItem', () => {
|
|
131
|
+
test('should retrieve item by id', async () => {
|
|
132
|
+
const user = await api.retrieveItem(5)
|
|
133
|
+
|
|
134
|
+
expect(user.id).toBe(5)
|
|
135
|
+
expect(user.name).toBe('User 5')
|
|
136
|
+
expect(user.email).toBe('user5@example.com')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('should throw an error when item does not exist', async () => {
|
|
140
|
+
expect(async () => {
|
|
141
|
+
await api.retrieveItem(999)
|
|
142
|
+
}).toThrow('users with id 999 not found')
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe('createItem', () => {
|
|
147
|
+
test('should create item with provided data', async () => {
|
|
148
|
+
const newUser = await api.createItem({
|
|
149
|
+
name: 'John Smith',
|
|
150
|
+
email: 'john@example.com',
|
|
151
|
+
age: 28,
|
|
152
|
+
active: true,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
expect(newUser.id).toBe(21)
|
|
156
|
+
expect(newUser.name).toBe('John Smith')
|
|
157
|
+
expect(api.count).toBe(21)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('should generate item automatically when not provided', async () => {
|
|
161
|
+
const newUser = await api.createItem()
|
|
162
|
+
|
|
163
|
+
expect(newUser.id).toBe(21)
|
|
164
|
+
expect(newUser.name).toBe('User 21')
|
|
165
|
+
expect(api.count).toBe(21)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('should auto-increment the id', async () => {
|
|
169
|
+
const user1 = await api.createItem()
|
|
170
|
+
const user2 = await api.createItem()
|
|
171
|
+
|
|
172
|
+
expect(user1.id).toBe(21)
|
|
173
|
+
expect(user2.id).toBe(22)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('updateItem', () => {
|
|
178
|
+
test('should update existing item', async () => {
|
|
179
|
+
const updated = await api.updateItem(5, {
|
|
180
|
+
name: 'User Updated',
|
|
181
|
+
age: 99,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
expect(updated.id).toBe(5)
|
|
185
|
+
expect(updated.name).toBe('User Updated')
|
|
186
|
+
expect(updated.age).toBe(99)
|
|
187
|
+
expect(updated.email).toBe('user5@example.com')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('should preserve the original id', async () => {
|
|
191
|
+
const updated = await api.updateItem(5, { id: 999 } as any)
|
|
192
|
+
|
|
193
|
+
expect(updated.id).toBe(5)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('should throw an error when item does not exist', async () => {
|
|
197
|
+
expect(async () => {
|
|
198
|
+
await api.updateItem(999, { name: 'Test' })
|
|
199
|
+
}).toThrow('users with id 999 not found')
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
describe('deleteItem', () => {
|
|
204
|
+
test('should delete existing item', async () => {
|
|
205
|
+
const deleted = await api.deleteItem(5)
|
|
206
|
+
|
|
207
|
+
expect(deleted.id).toBe(5)
|
|
208
|
+
expect(api.count).toBe(19)
|
|
209
|
+
expect(api.getItem(5)).toBeUndefined()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('should throw an error when item does not exist', async () => {
|
|
213
|
+
expect(async () => {
|
|
214
|
+
await api.deleteItem(999)
|
|
215
|
+
}).toThrow('users with id 999 not found')
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe('Utility methods', () => {
|
|
220
|
+
test('getItem should return existing item', () => {
|
|
221
|
+
const user = api.getItem(5)
|
|
222
|
+
|
|
223
|
+
expect(user).toBeDefined()
|
|
224
|
+
expect(user?.id).toBe(5)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('getItem should return undefined for non-existing item', () => {
|
|
228
|
+
const user = api.getItem(999)
|
|
229
|
+
|
|
230
|
+
expect(user).toBeUndefined()
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('getData should return frozen array', () => {
|
|
234
|
+
const data = api.getData()
|
|
235
|
+
|
|
236
|
+
expect(data).toHaveLength(20)
|
|
237
|
+
expect(Object.isFrozen(data)).toBe(true)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('clear should remove all items', () => {
|
|
241
|
+
api.clear()
|
|
242
|
+
|
|
243
|
+
expect(api.count).toBe(0)
|
|
244
|
+
expect(api.getData()).toHaveLength(0)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('reset should restore initial items', () => {
|
|
248
|
+
api.clear()
|
|
249
|
+
expect(api.count).toBe(0)
|
|
250
|
+
|
|
251
|
+
api.reset()
|
|
252
|
+
|
|
253
|
+
expect(api.count).toBe(20)
|
|
254
|
+
expect(api.getData()[0].id).toBe(1)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test('generateItem should generate new item with provided id', () => {
|
|
258
|
+
const user = api.generateItem(100)
|
|
259
|
+
|
|
260
|
+
expect(user.id).toBe(100)
|
|
261
|
+
expect(user.name).toBe('User 100')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('generateItem should use lastId + 1 when not provided', () => {
|
|
265
|
+
const user = api.generateItem()
|
|
266
|
+
|
|
267
|
+
expect(user.id).toBe(21)
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
describe('setOptions', () => {
|
|
272
|
+
test('should update options', () => {
|
|
273
|
+
api.setOptions({ enableDelay: true, delayMs: 5000 })
|
|
274
|
+
|
|
275
|
+
expect(api.name).toBe('users')
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
describe('Delay', () => {
|
|
280
|
+
test('should respect delay when enabled', async () => {
|
|
281
|
+
const apiWithDelay = createFakeRestApi<User>({
|
|
282
|
+
name: 'delayed',
|
|
283
|
+
enableDelay: true,
|
|
284
|
+
delayMs: 100,
|
|
285
|
+
maxCount: 5,
|
|
286
|
+
generatorFn: (id) => ({
|
|
287
|
+
id,
|
|
288
|
+
name: `User ${id}`,
|
|
289
|
+
email: `user${id}@example.com`,
|
|
290
|
+
age: 20,
|
|
291
|
+
active: true,
|
|
292
|
+
}),
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
const start = Date.now()
|
|
296
|
+
await apiWithDelay.listItems()
|
|
297
|
+
const duration = Date.now() - start
|
|
298
|
+
|
|
299
|
+
expect(duration).toBeGreaterThanOrEqual(100)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test('should be instantaneous when delay is disabled', async () => {
|
|
303
|
+
const start = Date.now()
|
|
304
|
+
await api.listItems()
|
|
305
|
+
const duration = Date.now() - start
|
|
306
|
+
|
|
307
|
+
expect(duration).toBeLessThan(50)
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
describe('Edge cases', () => {
|
|
312
|
+
test('should handle maxCount = 0', () => {
|
|
313
|
+
const emptyApi = createFakeRestApi<User>({
|
|
314
|
+
name: 'empty',
|
|
315
|
+
maxCount: 0,
|
|
316
|
+
generatorFn: (id) => ({
|
|
317
|
+
id,
|
|
318
|
+
name: `User ${id}`,
|
|
319
|
+
email: `user${id}@example.com`,
|
|
320
|
+
age: 20,
|
|
321
|
+
active: true,
|
|
322
|
+
}),
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
expect(emptyApi.count).toBe(0)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('should handle empty filters', async () => {
|
|
329
|
+
const result = await api.listItems(10, 0, {})
|
|
330
|
+
|
|
331
|
+
expect(result.count).toBe(20)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
test('should handle limit greater than total', async () => {
|
|
335
|
+
const result = await api.listItems(100, 0)
|
|
336
|
+
|
|
337
|
+
expect(result.results).toHaveLength(20)
|
|
338
|
+
expect(result.next).toBeNull()
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test('should handle offset greater than total', async () => {
|
|
342
|
+
const result = await api.listItems(10, 100)
|
|
343
|
+
|
|
344
|
+
expect(result.results).toHaveLength(0)
|
|
345
|
+
expect(result.next).toBeNull()
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
describe('API without filterFn', () => {
|
|
350
|
+
test('should work without filterFn defined', async () => {
|
|
351
|
+
const simpleApi = createFakeRestApi<User>({
|
|
352
|
+
name: 'simple',
|
|
353
|
+
maxCount: 10,
|
|
354
|
+
generatorFn: (id) => ({
|
|
355
|
+
id,
|
|
356
|
+
name: `User ${id}`,
|
|
357
|
+
email: `user${id}@example.com`,
|
|
358
|
+
age: 20,
|
|
359
|
+
active: true,
|
|
360
|
+
}),
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
const result = await simpleApi.listItems(5, 0, { name: 'test' })
|
|
364
|
+
|
|
365
|
+
expect(result.count).toBe(10)
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a basic item with an ID.
|
|
3
|
+
*/
|
|
4
|
+
export type FakeItem = {
|
|
5
|
+
id: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents a paginated response structure.
|
|
10
|
+
* @template T - The type of items in the results array, must extend FakeItem
|
|
11
|
+
*/
|
|
12
|
+
export type PaginationResponse<T extends FakeItem> = {
|
|
13
|
+
/** Total count of items */
|
|
14
|
+
count: number
|
|
15
|
+
/** URL for the next page, or null if there are no more pages */
|
|
16
|
+
next: string | null
|
|
17
|
+
/** URL for the previous page, or null if this is the first page */
|
|
18
|
+
previous: string | null
|
|
19
|
+
/** Array of items for the current page */
|
|
20
|
+
results: T[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Configuration options for creating a FakeRestApi instance.
|
|
25
|
+
* @template T - The type of items to manage, must extend FakeItem
|
|
26
|
+
* @template F - The type of filters to apply when listing items
|
|
27
|
+
*/
|
|
28
|
+
export type FakeRestApiOptions<T extends FakeItem, F = Record<string, unknown>> = {
|
|
29
|
+
/** Name of the API (used in URLs and error messages) */
|
|
30
|
+
name: string
|
|
31
|
+
/** Whether to enable artificial delay for requests */
|
|
32
|
+
enableDelay?: boolean
|
|
33
|
+
/** Maximum number of items to generate on initialization */
|
|
34
|
+
maxCount?: number
|
|
35
|
+
/** Delay duration in milliseconds */
|
|
36
|
+
delayMs?: number
|
|
37
|
+
/** Function to generate a new item given an ID */
|
|
38
|
+
generatorFn: (id: T['id']) => T
|
|
39
|
+
/** Optional function to filter items based on criteria */
|
|
40
|
+
filterFn?: (item: T, filters: F) => boolean
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FakeRestApi'
|