@navios/adapter-xml 0.1.0
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/LICENSE +7 -0
- package/README.md +532 -0
- package/bun-plugin.mts +129 -0
- package/bunPlugin.cache +1 -0
- package/bunfig.toml +3 -0
- package/dist/bun-plugin.d.mts +4 -0
- package/dist/bun-plugin.d.mts.map +1 -0
- package/dist/e2e/bun/xml-stream.spec.d.ts +2 -0
- package/dist/e2e/bun/xml-stream.spec.d.ts.map +1 -0
- package/dist/e2e/fastify/xml-stream.spec.d.ts +2 -0
- package/dist/e2e/fastify/xml-stream.spec.d.ts.map +1 -0
- package/dist/src/adapters/index.d.mts +2 -0
- package/dist/src/adapters/index.d.mts.map +1 -0
- package/dist/src/adapters/xml-stream-adapter.service.d.mts +21 -0
- package/dist/src/adapters/xml-stream-adapter.service.d.mts.map +1 -0
- package/dist/src/decorators/component.decorator.d.mts +17 -0
- package/dist/src/decorators/component.decorator.d.mts.map +1 -0
- package/dist/src/decorators/component.decorator.spec.d.mts +2 -0
- package/dist/src/decorators/component.decorator.spec.d.mts.map +1 -0
- package/dist/src/decorators/index.d.mts +4 -0
- package/dist/src/decorators/index.d.mts.map +1 -0
- package/dist/src/decorators/xml-stream.decorator.d.mts +42 -0
- package/dist/src/decorators/xml-stream.decorator.d.mts.map +1 -0
- package/dist/src/define-environment.d.mts +31 -0
- package/dist/src/define-environment.d.mts.map +1 -0
- package/dist/src/handlers/index.d.mts +2 -0
- package/dist/src/handlers/index.d.mts.map +1 -0
- package/dist/src/handlers/xml-stream.d.mts +23 -0
- package/dist/src/handlers/xml-stream.d.mts.map +1 -0
- package/dist/src/index.d.mts +12 -0
- package/dist/src/index.d.mts.map +1 -0
- package/dist/src/jsx-dev-runtime.d.mts +5 -0
- package/dist/src/jsx-dev-runtime.d.mts.map +1 -0
- package/dist/src/jsx-runtime.d.mts +3 -0
- package/dist/src/jsx-runtime.d.mts.map +1 -0
- package/dist/src/jsx.d.mts +18 -0
- package/dist/src/jsx.d.mts.map +1 -0
- package/dist/src/runtime/create-element.d.mts +25 -0
- package/dist/src/runtime/create-element.d.mts.map +1 -0
- package/dist/src/runtime/fragment.d.mts +2 -0
- package/dist/src/runtime/fragment.d.mts.map +1 -0
- package/dist/src/runtime/index.d.mts +5 -0
- package/dist/src/runtime/index.d.mts.map +1 -0
- package/dist/src/runtime/render-to-xml.d.mts +20 -0
- package/dist/src/runtime/render-to-xml.d.mts.map +1 -0
- package/dist/src/runtime/render-to-xml.spec.d.mts +2 -0
- package/dist/src/runtime/render-to-xml.spec.d.mts.map +1 -0
- package/dist/src/runtime/special-nodes.d.mts +24 -0
- package/dist/src/runtime/special-nodes.d.mts.map +1 -0
- package/dist/src/tags/define-tag.d.mts +33 -0
- package/dist/src/tags/define-tag.d.mts.map +1 -0
- package/dist/src/tags/define-tag.spec.d.mts +2 -0
- package/dist/src/tags/define-tag.spec.d.mts.map +1 -0
- package/dist/src/tags/index.d.mts +3 -0
- package/dist/src/tags/index.d.mts.map +1 -0
- package/dist/src/types/component.d.mts +15 -0
- package/dist/src/types/component.d.mts.map +1 -0
- package/dist/src/types/config.d.mts +10 -0
- package/dist/src/types/config.d.mts.map +1 -0
- package/dist/src/types/index.d.mts +5 -0
- package/dist/src/types/index.d.mts.map +1 -0
- package/dist/src/types/xml-node.d.mts +35 -0
- package/dist/src/types/xml-node.d.mts.map +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/tsconfig.spec.tsbuildinfo +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/tsup.config.d.mts +3 -0
- package/dist/tsup.config.d.mts.map +1 -0
- package/dist/vitest.config.d.mts +3 -0
- package/dist/vitest.config.d.mts.map +1 -0
- package/dist/vitest.e2e.fastify.config.d.mts +3 -0
- package/dist/vitest.e2e.fastify.config.d.mts.map +1 -0
- package/e2e/bun/xml-stream.spec.tsx +553 -0
- package/e2e/fastify/xml-stream.spec.tsx +569 -0
- package/jsx.d.ts +42 -0
- package/lib/_tsup-dts-rollup.d.mts +414 -0
- package/lib/_tsup-dts-rollup.d.ts +414 -0
- package/lib/chunk-6OR6LGJA.mjs +153 -0
- package/lib/chunk-6OR6LGJA.mjs.map +1 -0
- package/lib/index.d.mts +29 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.js +376 -0
- package/lib/index.js.map +1 -0
- package/lib/index.mjs +256 -0
- package/lib/index.mjs.map +1 -0
- package/lib/jsx-dev-runtime.d.mts +4 -0
- package/lib/jsx-dev-runtime.d.ts +4 -0
- package/lib/jsx-dev-runtime.js +61 -0
- package/lib/jsx-dev-runtime.js.map +1 -0
- package/lib/jsx-dev-runtime.mjs +9 -0
- package/lib/jsx-dev-runtime.mjs.map +1 -0
- package/lib/jsx-runtime.d.mts +3 -0
- package/lib/jsx-runtime.d.ts +3 -0
- package/lib/jsx-runtime.js +57 -0
- package/lib/jsx-runtime.js.map +1 -0
- package/lib/jsx-runtime.mjs +3 -0
- package/lib/jsx-runtime.mjs.map +1 -0
- package/lib/jsx.d.mts +1 -0
- package/lib/jsx.d.ts +1 -0
- package/lib/jsx.js +4 -0
- package/lib/jsx.js.map +1 -0
- package/lib/jsx.mjs +3 -0
- package/lib/jsx.mjs.map +1 -0
- package/package.json +80 -0
- package/project.json +91 -0
- package/src/adapters/index.mts +1 -0
- package/src/adapters/xml-stream-adapter.service.mts +121 -0
- package/src/decorators/component.decorator.mts +102 -0
- package/src/decorators/component.decorator.spec.mts +345 -0
- package/src/decorators/index.mts +4 -0
- package/src/decorators/xml-stream.decorator.mts +93 -0
- package/src/define-environment.mts +40 -0
- package/src/handlers/index.mts +1 -0
- package/src/handlers/xml-stream.mts +31 -0
- package/src/index.mts +41 -0
- package/src/jsx-dev-runtime.mts +8 -0
- package/src/jsx-runtime.mts +2 -0
- package/src/jsx.mts +25 -0
- package/src/runtime/create-element.mts +113 -0
- package/src/runtime/fragment.mts +1 -0
- package/src/runtime/index.mts +4 -0
- package/src/runtime/render-to-xml.mts +214 -0
- package/src/runtime/render-to-xml.spec.mts +360 -0
- package/src/runtime/special-nodes.mts +32 -0
- package/src/tags/define-tag.mts +54 -0
- package/src/tags/define-tag.spec.mts +250 -0
- package/src/tags/index.mts +2 -0
- package/src/types/component.mts +16 -0
- package/src/types/config.mts +15 -0
- package/src/types/index.mts +23 -0
- package/src/types/jsx.d.ts +21 -0
- package/src/types/xml-node.mts +50 -0
- package/tsconfig.json +24 -0
- package/tsconfig.lib.json +8 -0
- package/tsconfig.spec.json +25 -0
- package/tsup.config.mts +18 -0
- package/vitest.config.mts +9 -0
- package/vitest.e2e.fastify.config.mts +29 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import type { XmlComponent, XmlStreamParams } from '../../src/index.mjs'
|
|
2
|
+
|
|
3
|
+
import { builder } from '@navios/builder'
|
|
4
|
+
import {
|
|
5
|
+
Controller,
|
|
6
|
+
Endpoint,
|
|
7
|
+
inject,
|
|
8
|
+
Injectable,
|
|
9
|
+
InjectableScope,
|
|
10
|
+
Module,
|
|
11
|
+
NaviosApplication,
|
|
12
|
+
NaviosFactory,
|
|
13
|
+
type EndpointParams,
|
|
14
|
+
} from '@navios/core'
|
|
15
|
+
import { defineBunEnvironment } from '@navios/adapter-bun'
|
|
16
|
+
|
|
17
|
+
import supertest from 'supertest'
|
|
18
|
+
import { afterAll, beforeAll, describe, expect, it } from 'bun:test'
|
|
19
|
+
import { z } from 'zod/v4'
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
CData,
|
|
23
|
+
Component,
|
|
24
|
+
DangerouslyInsertRawXml,
|
|
25
|
+
declareXmlStream,
|
|
26
|
+
defineXmlEnvironment,
|
|
27
|
+
Fragment,
|
|
28
|
+
XmlStream,
|
|
29
|
+
createElement,
|
|
30
|
+
} from '../../src/index.mjs'
|
|
31
|
+
|
|
32
|
+
// Helper to merge environments
|
|
33
|
+
function mergeEnvironments(...envs: Array<{ httpTokens: Map<any, any> }>): {
|
|
34
|
+
httpTokens: Map<any, any>
|
|
35
|
+
} {
|
|
36
|
+
const merged = new Map()
|
|
37
|
+
for (const env of envs) {
|
|
38
|
+
for (const [key, value] of env.httpTokens) {
|
|
39
|
+
merged.set(key, value)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { httpTokens: merged }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('XML Stream with Bun adapter', () => {
|
|
46
|
+
let server: NaviosApplication
|
|
47
|
+
let serverUrl: string
|
|
48
|
+
|
|
49
|
+
// Request scoped service for tracking
|
|
50
|
+
@Injectable({
|
|
51
|
+
scope: InjectableScope.Request,
|
|
52
|
+
})
|
|
53
|
+
class RequestTrackerService {
|
|
54
|
+
private requestId: string = Math.random().toString(36).substring(7)
|
|
55
|
+
|
|
56
|
+
getRequestId(): string {
|
|
57
|
+
return this.requestId
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Injectable data service
|
|
62
|
+
@Injectable()
|
|
63
|
+
class DataService {
|
|
64
|
+
getItems() {
|
|
65
|
+
return [
|
|
66
|
+
{ id: '1', title: 'First Item', description: 'Description 1' },
|
|
67
|
+
{ id: '2', title: 'Second Item', description: 'Description 2' },
|
|
68
|
+
{ id: '3', title: 'Third Item', description: 'Description 3' },
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getItem(id: string) {
|
|
73
|
+
return this.getItems().find((item) => item.id === id)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Class component with DI
|
|
78
|
+
@Component()
|
|
79
|
+
class ItemComponent implements XmlComponent {
|
|
80
|
+
private dataService = inject(DataService)
|
|
81
|
+
private tracker = inject(RequestTrackerService)
|
|
82
|
+
|
|
83
|
+
constructor(private props: { id: string }) {}
|
|
84
|
+
|
|
85
|
+
render() {
|
|
86
|
+
const item = this.dataService.getItem(this.props.id)
|
|
87
|
+
if (!item) {
|
|
88
|
+
return createElement('error', null, 'Item not found')
|
|
89
|
+
}
|
|
90
|
+
return createElement(
|
|
91
|
+
'item',
|
|
92
|
+
{ id: item.id, requestId: this.tracker.getRequestId() },
|
|
93
|
+
createElement('title', null, item.title),
|
|
94
|
+
createElement('description', null, item.description),
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Async component
|
|
100
|
+
async function AsyncDataComponent({ delay }: { delay: number }) {
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
102
|
+
return createElement('async-data', null, `Loaded after ${delay}ms`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Simple RSS feed endpoint
|
|
106
|
+
const getRssFeed = declareXmlStream({
|
|
107
|
+
method: 'GET',
|
|
108
|
+
url: '/feed.xml',
|
|
109
|
+
querySchema: undefined,
|
|
110
|
+
requestSchema: undefined,
|
|
111
|
+
contentType: 'application/rss+xml',
|
|
112
|
+
xmlDeclaration: true,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Atom feed with query params
|
|
116
|
+
const getAtomFeed = declareXmlStream({
|
|
117
|
+
method: 'GET',
|
|
118
|
+
url: '/atom.xml',
|
|
119
|
+
querySchema: z.object({
|
|
120
|
+
limit: z.coerce.number().optional().default(10),
|
|
121
|
+
}),
|
|
122
|
+
requestSchema: undefined,
|
|
123
|
+
contentType: 'application/atom+xml',
|
|
124
|
+
xmlDeclaration: true,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Sitemap endpoint
|
|
128
|
+
const getSitemap = declareXmlStream({
|
|
129
|
+
method: 'GET',
|
|
130
|
+
url: '/sitemap.xml',
|
|
131
|
+
querySchema: undefined,
|
|
132
|
+
requestSchema: undefined,
|
|
133
|
+
contentType: 'application/xml',
|
|
134
|
+
xmlDeclaration: true,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Dynamic XML with URL params (use path without extension for Bun compatibility)
|
|
138
|
+
const getItemXml = declareXmlStream({
|
|
139
|
+
method: 'GET',
|
|
140
|
+
url: '/items/$id',
|
|
141
|
+
querySchema: undefined,
|
|
142
|
+
requestSchema: undefined,
|
|
143
|
+
contentType: 'application/xml',
|
|
144
|
+
xmlDeclaration: true,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// POST endpoint for XML generation
|
|
148
|
+
const postGenerateXml = declareXmlStream({
|
|
149
|
+
method: 'POST',
|
|
150
|
+
url: '/generate.xml',
|
|
151
|
+
querySchema: undefined,
|
|
152
|
+
requestSchema: z.object({
|
|
153
|
+
title: z.string(),
|
|
154
|
+
items: z.array(z.string()),
|
|
155
|
+
}),
|
|
156
|
+
contentType: 'application/xml',
|
|
157
|
+
xmlDeclaration: true,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Endpoint with async components
|
|
161
|
+
const getAsyncXml = declareXmlStream({
|
|
162
|
+
method: 'GET',
|
|
163
|
+
url: '/async.xml',
|
|
164
|
+
querySchema: undefined,
|
|
165
|
+
requestSchema: undefined,
|
|
166
|
+
contentType: 'application/xml',
|
|
167
|
+
xmlDeclaration: true,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Endpoint with class components (DI) - use path without extension for Bun compatibility
|
|
171
|
+
const getDiXml = declareXmlStream({
|
|
172
|
+
method: 'GET',
|
|
173
|
+
url: '/di/$id',
|
|
174
|
+
querySchema: undefined,
|
|
175
|
+
requestSchema: undefined,
|
|
176
|
+
contentType: 'application/xml',
|
|
177
|
+
xmlDeclaration: true,
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// Endpoint with CDATA and raw XML
|
|
181
|
+
const getSpecialXml = declareXmlStream({
|
|
182
|
+
method: 'GET',
|
|
183
|
+
url: '/special.xml',
|
|
184
|
+
querySchema: undefined,
|
|
185
|
+
requestSchema: undefined,
|
|
186
|
+
contentType: 'application/xml',
|
|
187
|
+
xmlDeclaration: true,
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// Also test regular JSON endpoint alongside XML
|
|
191
|
+
const getJsonEndpoint = builder().declareEndpoint({
|
|
192
|
+
method: 'GET',
|
|
193
|
+
url: '/api/data',
|
|
194
|
+
responseSchema: z.object({
|
|
195
|
+
message: z.string(),
|
|
196
|
+
}),
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
@Controller()
|
|
200
|
+
class FeedController {
|
|
201
|
+
private dataService = inject(DataService)
|
|
202
|
+
private tracker = inject(RequestTrackerService)
|
|
203
|
+
|
|
204
|
+
@XmlStream(getRssFeed)
|
|
205
|
+
async getRssFeed(_params: XmlStreamParams<typeof getRssFeed>) {
|
|
206
|
+
const items = this.dataService.getItems()
|
|
207
|
+
return (
|
|
208
|
+
<rss version="2.0">
|
|
209
|
+
<channel>
|
|
210
|
+
<title>My Blog</title>
|
|
211
|
+
<link>https://example.com</link>
|
|
212
|
+
<description>A sample RSS feed</description>
|
|
213
|
+
{items.map((item) => (
|
|
214
|
+
<item key={item.id}>
|
|
215
|
+
<title>{item.title}</title>
|
|
216
|
+
<link>https://example.com/items/{item.id}</link>
|
|
217
|
+
<description>{item.description}</description>
|
|
218
|
+
</item>
|
|
219
|
+
))}
|
|
220
|
+
</channel>
|
|
221
|
+
</rss>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@XmlStream(getAtomFeed)
|
|
226
|
+
async getAtomFeed(params: XmlStreamParams<typeof getAtomFeed>) {
|
|
227
|
+
const limit = params.params.limit
|
|
228
|
+
const items = this.dataService.getItems().slice(0, limit)
|
|
229
|
+
return (
|
|
230
|
+
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
231
|
+
<title>My Atom Feed</title>
|
|
232
|
+
<link href="https://example.com/atom.xml" rel="self" />
|
|
233
|
+
<id>https://example.com/</id>
|
|
234
|
+
<updated>2025-01-01T00:00:00Z</updated>
|
|
235
|
+
{items.map((item) => (
|
|
236
|
+
<entry key={item.id}>
|
|
237
|
+
<title>{item.title}</title>
|
|
238
|
+
<id>https://example.com/items/{item.id}</id>
|
|
239
|
+
<updated>2025-01-01T00:00:00Z</updated>
|
|
240
|
+
<summary>{item.description}</summary>
|
|
241
|
+
</entry>
|
|
242
|
+
))}
|
|
243
|
+
</feed>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@XmlStream(getSitemap)
|
|
248
|
+
async getSitemap(_params: XmlStreamParams<typeof getSitemap>) {
|
|
249
|
+
const items = this.dataService.getItems()
|
|
250
|
+
return (
|
|
251
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
252
|
+
<url>
|
|
253
|
+
<loc>https://example.com/</loc>
|
|
254
|
+
<lastmod>2025-01-01</lastmod>
|
|
255
|
+
<changefreq>daily</changefreq>
|
|
256
|
+
<priority>1.0</priority>
|
|
257
|
+
</url>
|
|
258
|
+
{items.map((item) => (
|
|
259
|
+
<url key={item.id}>
|
|
260
|
+
<loc>https://example.com/items/{item.id}</loc>
|
|
261
|
+
<lastmod>2025-01-01</lastmod>
|
|
262
|
+
<changefreq>weekly</changefreq>
|
|
263
|
+
<priority>0.8</priority>
|
|
264
|
+
</url>
|
|
265
|
+
))}
|
|
266
|
+
</urlset>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@XmlStream(getItemXml)
|
|
271
|
+
async getItemXml(params: XmlStreamParams<typeof getItemXml>) {
|
|
272
|
+
const item = this.dataService.getItem(params.urlParams.id as string)
|
|
273
|
+
if (!item) {
|
|
274
|
+
return (
|
|
275
|
+
<error>
|
|
276
|
+
<message>Item not found</message>
|
|
277
|
+
<requestedId>{params.urlParams.id}</requestedId>
|
|
278
|
+
</error>
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
return (
|
|
282
|
+
<item id={item.id}>
|
|
283
|
+
<title>{item.title}</title>
|
|
284
|
+
<description>{item.description}</description>
|
|
285
|
+
</item>
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@XmlStream(postGenerateXml)
|
|
290
|
+
async postGenerateXml(params: XmlStreamParams<typeof postGenerateXml>) {
|
|
291
|
+
const { title, items } = params.data
|
|
292
|
+
return (
|
|
293
|
+
<generated>
|
|
294
|
+
<title>{title}</title>
|
|
295
|
+
<items count={String(items.length)}>
|
|
296
|
+
{items.map((item, index) => (
|
|
297
|
+
<item index={String(index)} key={index}>
|
|
298
|
+
{item}
|
|
299
|
+
</item>
|
|
300
|
+
))}
|
|
301
|
+
</items>
|
|
302
|
+
</generated>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@XmlStream(getAsyncXml)
|
|
307
|
+
async getAsyncXml(_params: XmlStreamParams<typeof getAsyncXml>) {
|
|
308
|
+
return (
|
|
309
|
+
<root>
|
|
310
|
+
<sync>Immediate content</sync>
|
|
311
|
+
<AsyncDataComponent delay={10} />
|
|
312
|
+
<AsyncDataComponent delay={5} />
|
|
313
|
+
</root>
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@XmlStream(getDiXml)
|
|
318
|
+
async getDiXml(params: XmlStreamParams<typeof getDiXml>) {
|
|
319
|
+
return (
|
|
320
|
+
<root requestId={this.tracker.getRequestId()}>
|
|
321
|
+
<ItemComponent id={params.urlParams.id as string} />
|
|
322
|
+
</root>
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@XmlStream(getSpecialXml)
|
|
327
|
+
async getSpecialXml(_params: XmlStreamParams<typeof getSpecialXml>) {
|
|
328
|
+
return (
|
|
329
|
+
<root>
|
|
330
|
+
<cdata-example>
|
|
331
|
+
<CData>
|
|
332
|
+
{'This is <raw> HTML & XML content that should not be escaped'}
|
|
333
|
+
</CData>
|
|
334
|
+
</cdata-example>
|
|
335
|
+
<raw-xml-example>
|
|
336
|
+
<DangerouslyInsertRawXml>
|
|
337
|
+
{'<nested><element attr="value">text</element></nested>'}
|
|
338
|
+
</DangerouslyInsertRawXml>
|
|
339
|
+
</raw-xml-example>
|
|
340
|
+
<escaped>{'<this> & "that" should be escaped'}</escaped>
|
|
341
|
+
</root>
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
@Endpoint(getJsonEndpoint)
|
|
346
|
+
async getJsonData(_params: EndpointParams<typeof getJsonEndpoint>) {
|
|
347
|
+
return { message: 'JSON endpoint works alongside XML' }
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
@Module({
|
|
352
|
+
controllers: [FeedController],
|
|
353
|
+
})
|
|
354
|
+
class AppModule {}
|
|
355
|
+
|
|
356
|
+
beforeAll(async () => {
|
|
357
|
+
const bunEnv = defineBunEnvironment()
|
|
358
|
+
const xmlEnv = defineXmlEnvironment()
|
|
359
|
+
const mergedEnv = mergeEnvironments(bunEnv, xmlEnv)
|
|
360
|
+
|
|
361
|
+
server = await NaviosFactory.create(AppModule, {
|
|
362
|
+
adapter: mergedEnv,
|
|
363
|
+
})
|
|
364
|
+
await server.init()
|
|
365
|
+
await server.listen({ port: 3002, host: 'localhost' })
|
|
366
|
+
serverUrl = server.getServer().url.href
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
afterAll(async () => {
|
|
370
|
+
await server.close()
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
describe('Basic XML endpoints', () => {
|
|
374
|
+
it('should return RSS feed with correct content type', async () => {
|
|
375
|
+
const response = await supertest(serverUrl).get('/feed.xml')
|
|
376
|
+
|
|
377
|
+
expect(response.status).toBe(200)
|
|
378
|
+
expect(response.headers['content-type']).toContain('application/rss+xml')
|
|
379
|
+
expect(response.text).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
380
|
+
expect(response.text).toContain('<rss version="2.0">')
|
|
381
|
+
expect(response.text).toContain('<title>My Blog</title>')
|
|
382
|
+
expect(response.text).toContain('<item>')
|
|
383
|
+
expect(response.text).toContain('<title>First Item</title>')
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('should return Atom feed with query params', async () => {
|
|
387
|
+
const response = await supertest(serverUrl).get('/atom.xml?limit=2')
|
|
388
|
+
|
|
389
|
+
expect(response.status).toBe(200)
|
|
390
|
+
expect(response.headers['content-type']).toContain('application/atom+xml')
|
|
391
|
+
expect(response.text).toContain(
|
|
392
|
+
'<feed xmlns="http://www.w3.org/2005/Atom">',
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
// Count entries - should be limited to 2
|
|
396
|
+
const entryCount = (response.text.match(/<entry>/g) || []).length
|
|
397
|
+
expect(entryCount).toBe(2)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('should return sitemap XML', async () => {
|
|
401
|
+
const response = await supertest(serverUrl).get('/sitemap.xml')
|
|
402
|
+
|
|
403
|
+
expect(response.status).toBe(200)
|
|
404
|
+
expect(response.headers['content-type']).toContain('application/xml')
|
|
405
|
+
expect(response.text).toContain(
|
|
406
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
407
|
+
)
|
|
408
|
+
expect(response.text).toContain('<loc>https://example.com/</loc>')
|
|
409
|
+
expect(response.text).toContain('<priority>1.0</priority>')
|
|
410
|
+
})
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
describe('URL parameters', () => {
|
|
414
|
+
it('should handle URL parameters in XML endpoint', async () => {
|
|
415
|
+
const response = await supertest(serverUrl).get('/items/1')
|
|
416
|
+
|
|
417
|
+
expect(response.status).toBe(200)
|
|
418
|
+
expect(response.text).toContain('<item id="1">')
|
|
419
|
+
expect(response.text).toContain('<title>First Item</title>')
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it('should handle non-existent item', async () => {
|
|
423
|
+
const response = await supertest(serverUrl).get('/items/999')
|
|
424
|
+
|
|
425
|
+
expect(response.status).toBe(200)
|
|
426
|
+
expect(response.text).toContain('<error>')
|
|
427
|
+
expect(response.text).toContain('<message>Item not found</message>')
|
|
428
|
+
expect(response.text).toContain('<requestedId>999</requestedId>')
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
describe('POST with request body', () => {
|
|
433
|
+
it('should generate XML from POST body', async () => {
|
|
434
|
+
const response = await supertest(serverUrl)
|
|
435
|
+
.post('/generate.xml')
|
|
436
|
+
.send({
|
|
437
|
+
title: 'Generated Document',
|
|
438
|
+
items: ['Alpha', 'Beta', 'Gamma'],
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
expect(response.status).toBe(200)
|
|
442
|
+
expect(response.text).toContain('<generated>')
|
|
443
|
+
expect(response.text).toContain('<title>Generated Document</title>')
|
|
444
|
+
expect(response.text).toContain('<items count="3">')
|
|
445
|
+
expect(response.text).toContain('<item index="0">Alpha</item>')
|
|
446
|
+
expect(response.text).toContain('<item index="1">Beta</item>')
|
|
447
|
+
expect(response.text).toContain('<item index="2">Gamma</item>')
|
|
448
|
+
})
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
describe('Async components', () => {
|
|
452
|
+
it('should resolve async components in XML', async () => {
|
|
453
|
+
const response = await supertest(serverUrl).get('/async.xml')
|
|
454
|
+
|
|
455
|
+
expect(response.status).toBe(200)
|
|
456
|
+
expect(response.text).toContain('<sync>Immediate content</sync>')
|
|
457
|
+
expect(response.text).toContain(
|
|
458
|
+
'<async-data>Loaded after 10ms</async-data>',
|
|
459
|
+
)
|
|
460
|
+
expect(response.text).toContain(
|
|
461
|
+
'<async-data>Loaded after 5ms</async-data>',
|
|
462
|
+
)
|
|
463
|
+
})
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
describe('Class components with DI', () => {
|
|
467
|
+
it('should render class components with injected services', async () => {
|
|
468
|
+
const response = await supertest(serverUrl).get('/di/1')
|
|
469
|
+
|
|
470
|
+
expect(response.status).toBe(200)
|
|
471
|
+
expect(response.text).toContain('<root requestId=')
|
|
472
|
+
expect(response.text).toContain('<item id="1"')
|
|
473
|
+
expect(response.text).toContain('<title>First Item</title>')
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
it('should isolate request-scoped services across parallel requests', async () => {
|
|
477
|
+
const requests = [
|
|
478
|
+
supertest(serverUrl).get('/di/1'),
|
|
479
|
+
supertest(serverUrl).get('/di/2'),
|
|
480
|
+
supertest(serverUrl).get('/di/3'),
|
|
481
|
+
]
|
|
482
|
+
|
|
483
|
+
const responses = await Promise.all(requests)
|
|
484
|
+
|
|
485
|
+
// All should succeed
|
|
486
|
+
responses.forEach((response) => {
|
|
487
|
+
expect(response.status).toBe(200)
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
// Extract request IDs from the responses
|
|
491
|
+
const requestIds = responses.map((response) => {
|
|
492
|
+
const match = response.text.match(/requestId="([^"]+)"/)
|
|
493
|
+
return match ? match[1] : null
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
// All request IDs should be unique
|
|
497
|
+
const uniqueIds = new Set(requestIds.filter(Boolean))
|
|
498
|
+
expect(uniqueIds.size).toBe(3)
|
|
499
|
+
})
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
describe('Special XML content', () => {
|
|
503
|
+
it('should handle CDATA and raw XML correctly', async () => {
|
|
504
|
+
const response = await supertest(serverUrl).get('/special.xml')
|
|
505
|
+
|
|
506
|
+
expect(response.status).toBe(200)
|
|
507
|
+
|
|
508
|
+
// CDATA should preserve raw content
|
|
509
|
+
expect(response.text).toContain(
|
|
510
|
+
'<![CDATA[This is <raw> HTML & XML content that should not be escaped]]>',
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
// Raw XML should be inserted without escaping
|
|
514
|
+
expect(response.text).toContain(
|
|
515
|
+
'<nested><element attr="value">text</element></nested>',
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
// Regular content should be escaped
|
|
519
|
+
expect(response.text).toContain(
|
|
520
|
+
'<this> & "that" should be escaped',
|
|
521
|
+
)
|
|
522
|
+
})
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
describe('Mixed endpoints', () => {
|
|
526
|
+
it('should work alongside regular JSON endpoints', async () => {
|
|
527
|
+
const xmlResponse = await supertest(serverUrl).get('/feed.xml')
|
|
528
|
+
const jsonResponse = await supertest(serverUrl).get('/api/data')
|
|
529
|
+
|
|
530
|
+
expect(xmlResponse.status).toBe(200)
|
|
531
|
+
expect(xmlResponse.headers['content-type']).toContain(
|
|
532
|
+
'application/rss+xml',
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
expect(jsonResponse.status).toBe(200)
|
|
536
|
+
expect(jsonResponse.body.message).toBe(
|
|
537
|
+
'JSON endpoint works alongside XML',
|
|
538
|
+
)
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
describe('Fragment support', () => {
|
|
543
|
+
it('should correctly render fragments within XML', async () => {
|
|
544
|
+
// The RSS feed uses fragments implicitly with array mapping
|
|
545
|
+
const response = await supertest(serverUrl).get('/feed.xml')
|
|
546
|
+
|
|
547
|
+
expect(response.status).toBe(200)
|
|
548
|
+
// Multiple items should be rendered without extra wrapper
|
|
549
|
+
const itemCount = (response.text.match(/<item>/g) || []).length
|
|
550
|
+
expect(itemCount).toBeGreaterThan(1)
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
})
|