@rip-lang/server 1.3.115 → 1.3.117
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/README.md +435 -622
- package/api.rip +4 -4
- package/control/cli.rip +221 -1
- package/control/control.rip +9 -0
- package/control/lifecycle.rip +6 -1
- package/control/watchers.rip +10 -0
- package/control/workers.rip +9 -5
- package/default.rip +3 -1
- package/docs/READ_VALIDATORS.md +656 -0
- package/docs/edge/CONFIG_LIFECYCLE.md +70 -32
- package/docs/edge/CONTRACTS.md +60 -69
- package/docs/edge/EDGEFILE_CONTRACT.md +258 -29
- package/docs/edge/M0B_REVIEW_NOTES.md +5 -5
- package/edge/config.rip +584 -52
- package/edge/forwarding.rip +6 -2
- package/edge/metrics.rip +19 -1
- package/edge/registry.rip +29 -3
- package/edge/router.rip +138 -0
- package/edge/runtime.rip +98 -0
- package/edge/static.rip +69 -0
- package/edge/tls.rip +23 -0
- package/edge/upstream.rip +272 -0
- package/edge/verify.rip +73 -0
- package/middleware.rip +3 -3
- package/package.json +2 -2
- package/server.rip +775 -393
- package/tests/control.rip +18 -0
- package/tests/edgefile.rip +165 -0
- package/tests/metrics.rip +16 -0
- package/tests/proxy.rip +22 -1
- package/tests/registry.rip +27 -0
- package/tests/router.rip +101 -0
- package/tests/runtime_entrypoints.rip +16 -0
- package/tests/servers.rip +262 -0
- package/tests/static.rip +64 -0
- package/tests/streams_clienthello.rip +108 -0
- package/tests/streams_index.rip +53 -0
- package/tests/streams_pipe.rip +70 -0
- package/tests/streams_router.rip +39 -0
- package/tests/streams_runtime.rip +38 -0
- package/tests/streams_upstream.rip +34 -0
- package/tests/upstream.rip +191 -0
- package/tests/verify.rip +148 -0
- package/tests/watchers.rip +15 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { test, eq, ok } from '../test.rip'
|
|
2
|
+
import { handleReloadControl } from '../control/control.rip'
|
|
3
|
+
|
|
4
|
+
test "handleReloadControl ignores non-POST requests", ->
|
|
5
|
+
req = new Request('http://localhost/reload', { method: 'GET' })
|
|
6
|
+
eq handleReloadControl(req, { ok: true }).handled, false
|
|
7
|
+
|
|
8
|
+
test "handleReloadControl returns 200 for successful reload", ->
|
|
9
|
+
req = new Request('http://localhost/reload', { method: 'POST' })
|
|
10
|
+
res = handleReloadControl(req, { ok: true, id: 'reload-1', result: 'applied' })
|
|
11
|
+
eq res.handled, true
|
|
12
|
+
eq res.response.status, 200
|
|
13
|
+
|
|
14
|
+
test "handleReloadControl returns 409 for rejected reload", ->
|
|
15
|
+
req = new Request('http://localhost/reload', { method: 'POST' })
|
|
16
|
+
res = handleReloadControl(req, { ok: false, id: 'reload-2', result: 'rolled_back', reason: 'boom' })
|
|
17
|
+
eq res.handled, true
|
|
18
|
+
eq res.response.status, 409
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { test, eq, ok } from '../test.rip'
|
|
2
|
+
import { normalizeEdgeConfig, normalizeLegacyConfig, summarizeConfig, formatConfigErrors } from '../edge/config.rip'
|
|
3
|
+
|
|
4
|
+
getErrors = (fn) ->
|
|
5
|
+
try
|
|
6
|
+
fn()
|
|
7
|
+
[]
|
|
8
|
+
catch e
|
|
9
|
+
e.validationErrors or []
|
|
10
|
+
|
|
11
|
+
test "normalizeEdgeConfig accepts hosts with upstreams", ->
|
|
12
|
+
normalized = normalizeEdgeConfig(
|
|
13
|
+
hosts:
|
|
14
|
+
'*.example.com':
|
|
15
|
+
routes: [
|
|
16
|
+
{ path: '/*', upstream: 'app' }
|
|
17
|
+
]
|
|
18
|
+
upstreams:
|
|
19
|
+
app:
|
|
20
|
+
targets: ['http://127.0.0.1:3000']
|
|
21
|
+
, '/srv')
|
|
22
|
+
eq normalized.kind, 'edge'
|
|
23
|
+
eq Object.keys(normalized.upstreams).length, 1
|
|
24
|
+
|
|
25
|
+
test "normalizeEdgeConfig accepts hosts with apps", ->
|
|
26
|
+
normalized = normalizeEdgeConfig(
|
|
27
|
+
hosts:
|
|
28
|
+
'*.example.com':
|
|
29
|
+
app: 'admin'
|
|
30
|
+
apps:
|
|
31
|
+
admin:
|
|
32
|
+
entry: './admin/index.rip'
|
|
33
|
+
, '/srv')
|
|
34
|
+
eq Object.keys(normalized.apps).length, 1
|
|
35
|
+
eq normalized.sites['*.example.com'].routes[0].app, 'admin'
|
|
36
|
+
|
|
37
|
+
test "normalizeEdgeConfig version and edge are optional", ->
|
|
38
|
+
config = normalizeEdgeConfig(
|
|
39
|
+
hosts:
|
|
40
|
+
'*.example.com':
|
|
41
|
+
root: '/mnt/site'
|
|
42
|
+
, '/srv')
|
|
43
|
+
eq config.version, 1
|
|
44
|
+
ok config.edge
|
|
45
|
+
|
|
46
|
+
test "normalizeEdgeConfig rejects wildcard ACME domains", ->
|
|
47
|
+
errors = getErrors ->
|
|
48
|
+
normalizeEdgeConfig(
|
|
49
|
+
edge:
|
|
50
|
+
acme: true
|
|
51
|
+
acmeDomains: ['*.example.com']
|
|
52
|
+
hosts:
|
|
53
|
+
'*.example.com':
|
|
54
|
+
root: '/mnt/site'
|
|
55
|
+
, '/srv')
|
|
56
|
+
ok errors.some((err) -> err.code is 'E_ACME_WILDCARD')
|
|
57
|
+
|
|
58
|
+
test "normalizeEdgeConfig rejects routes with multiple actions", ->
|
|
59
|
+
errors = getErrors ->
|
|
60
|
+
normalizeEdgeConfig(
|
|
61
|
+
upstreams:
|
|
62
|
+
api:
|
|
63
|
+
targets: ['http://127.0.0.1:4000']
|
|
64
|
+
apps:
|
|
65
|
+
admin:
|
|
66
|
+
entry: './admin/index.rip'
|
|
67
|
+
hosts:
|
|
68
|
+
'*.example.com':
|
|
69
|
+
routes: [
|
|
70
|
+
{ path: '/x', upstream: 'api', app: 'admin' }
|
|
71
|
+
]
|
|
72
|
+
, '/srv')
|
|
73
|
+
ok errors.some((err) -> err.code is 'E_ROUTE_ACTION')
|
|
74
|
+
|
|
75
|
+
test "normalizeEdgeConfig rejects websocket route without upstream", ->
|
|
76
|
+
errors = getErrors ->
|
|
77
|
+
normalizeEdgeConfig(
|
|
78
|
+
apps:
|
|
79
|
+
admin:
|
|
80
|
+
entry: './admin/index.rip'
|
|
81
|
+
hosts:
|
|
82
|
+
'*.example.com':
|
|
83
|
+
routes: [
|
|
84
|
+
{ path: '/ws', app: 'admin', websocket: true }
|
|
85
|
+
]
|
|
86
|
+
, '/srv')
|
|
87
|
+
ok errors.some((err) -> err.code is 'E_ROUTE_WEBSOCKET')
|
|
88
|
+
|
|
89
|
+
test "normalizeEdgeConfig preserves verify policy", ->
|
|
90
|
+
normalized = normalizeEdgeConfig(
|
|
91
|
+
edge:
|
|
92
|
+
verify:
|
|
93
|
+
requireHealthyUpstreams: false
|
|
94
|
+
requireReadyApps: true
|
|
95
|
+
includeUnroutedManagedApps: false
|
|
96
|
+
minHealthyTargetsPerUpstream: 2
|
|
97
|
+
hosts:
|
|
98
|
+
'*.example.com':
|
|
99
|
+
root: '/mnt/site'
|
|
100
|
+
, '/srv')
|
|
101
|
+
eq normalized.edge.verify.requireHealthyUpstreams, false
|
|
102
|
+
eq normalized.edge.verify.includeUnroutedManagedApps, false
|
|
103
|
+
eq normalized.edge.verify.minHealthyTargetsPerUpstream, 2
|
|
104
|
+
|
|
105
|
+
test "normalizeEdgeConfig accepts streamUpstreams and streams", ->
|
|
106
|
+
normalized = normalizeEdgeConfig(
|
|
107
|
+
hosts:
|
|
108
|
+
'*.example.com':
|
|
109
|
+
root: '/mnt/site'
|
|
110
|
+
streamUpstreams:
|
|
111
|
+
incus:
|
|
112
|
+
targets: ['127.0.0.1:8443']
|
|
113
|
+
streams: [
|
|
114
|
+
{ listen: 8443, sni: ['incus.example.com'], upstream: 'incus' }
|
|
115
|
+
]
|
|
116
|
+
, '/srv')
|
|
117
|
+
ok normalized.streamUpstreams['incus']
|
|
118
|
+
eq normalized.streams.length, 1
|
|
119
|
+
|
|
120
|
+
test "normalizeEdgeConfig rejects unknown stream upstream refs", ->
|
|
121
|
+
errors = getErrors ->
|
|
122
|
+
normalizeEdgeConfig(
|
|
123
|
+
hosts:
|
|
124
|
+
'*.example.com':
|
|
125
|
+
root: '/mnt/site'
|
|
126
|
+
streams: [
|
|
127
|
+
{ listen: 8443, sni: ['incus.example.com'], upstream: 'missing' }
|
|
128
|
+
]
|
|
129
|
+
, '/srv')
|
|
130
|
+
ok errors.some((err) -> err.code is 'E_STREAM_UPSTREAM_REF')
|
|
131
|
+
|
|
132
|
+
test "formatConfigErrors includes code path and hint", ->
|
|
133
|
+
output = formatConfigErrors('Edgefile.rip', [
|
|
134
|
+
code: 'E_ROUTE_ACTION'
|
|
135
|
+
path: 'routes[0]'
|
|
136
|
+
message: 'route must define exactly one action'
|
|
137
|
+
hint: 'Choose one action field per route.'
|
|
138
|
+
])
|
|
139
|
+
ok output.includes('[E_ROUTE_ACTION]')
|
|
140
|
+
ok output.includes('routes[0]')
|
|
141
|
+
ok output.includes('Hint:')
|
|
142
|
+
|
|
143
|
+
test "summarizeConfig reports canonical counts", ->
|
|
144
|
+
summary = summarizeConfig('/srv/Edgefile.rip',
|
|
145
|
+
kind: 'edge'
|
|
146
|
+
version: 1
|
|
147
|
+
edge: {}
|
|
148
|
+
upstreams: { app: { targets: ['http://127.0.0.1:3000'] } }
|
|
149
|
+
apps: { admin: { entry: '/srv/admin/index.rip' } }
|
|
150
|
+
routes: []
|
|
151
|
+
sites: { '*.example.com': { routes: [{ path: '/*', upstream: 'app' }] } }
|
|
152
|
+
)
|
|
153
|
+
eq summary.counts.upstreams, 1
|
|
154
|
+
eq summary.counts.apps, 1
|
|
155
|
+
eq summary.counts.sites, 1
|
|
156
|
+
|
|
157
|
+
test "normalizeLegacyConfig validates legacy apps object", ->
|
|
158
|
+
normalized = normalizeLegacyConfig(
|
|
159
|
+
apps:
|
|
160
|
+
web:
|
|
161
|
+
entry: './index.rip'
|
|
162
|
+
hosts: ['example.com']
|
|
163
|
+
, '/srv')
|
|
164
|
+
eq normalized.kind, 'legacy'
|
|
165
|
+
eq Object.keys(normalized.apps).length, 1
|
package/tests/metrics.rip
CHANGED
|
@@ -71,3 +71,19 @@ test "snapshot returns expected shape", ->
|
|
|
71
71
|
eq snap.counters.responses['2xx'], 90
|
|
72
72
|
eq snap.counters.responses['5xx'], 10
|
|
73
73
|
eq snap.apps, []
|
|
74
|
+
|
|
75
|
+
test "snapshot includes upstream health gauges", ->
|
|
76
|
+
m = createMetrics()
|
|
77
|
+
upstreamPool =
|
|
78
|
+
upstreams: new Map([
|
|
79
|
+
['app',
|
|
80
|
+
targets: [
|
|
81
|
+
{ healthy: true, circuitState: 'closed' }
|
|
82
|
+
{ healthy: false, circuitState: 'open' }
|
|
83
|
+
]
|
|
84
|
+
]
|
|
85
|
+
])
|
|
86
|
+
snap = m.snapshot(Date.now() - 1000, null, upstreamPool)
|
|
87
|
+
eq snap.upstreams.length, 1
|
|
88
|
+
eq snap.gauges.upstreamTargetsHealthy, 1
|
|
89
|
+
eq snap.gauges.upstreamTargetsUnhealthy, 1
|
package/tests/proxy.rip
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# proxy.rip — reverse proxy and config loader tests
|
|
2
2
|
|
|
3
3
|
import { test, eq, ok } from '../test.rip'
|
|
4
|
-
import { normalizeAppConfig, applyConfig } from '../edge/config.rip'
|
|
4
|
+
import { normalizeAppConfig, applyConfig, applyEdgeConfig } from '../edge/config.rip'
|
|
5
5
|
import { createAppRegistry, registerApp, resolveHost, getAppState } from '../edge/registry.rip'
|
|
6
6
|
import { broadcastBinary, createHub, addClient } from '../edge/realtime.rip'
|
|
7
7
|
import { setEventJsonMode, logEvent } from '../control/lifecycle.rip'
|
|
@@ -49,6 +49,27 @@ test "applyConfig handles empty apps", ->
|
|
|
49
49
|
registered = applyConfig({ apps: {} }, registry, registerApp, '/srv')
|
|
50
50
|
eq registered.length, 0
|
|
51
51
|
|
|
52
|
+
test "applyEdgeConfig preserves managed app entry workers and env", ->
|
|
53
|
+
registry = createAppRegistry()
|
|
54
|
+
config =
|
|
55
|
+
kind: 'edge'
|
|
56
|
+
apps:
|
|
57
|
+
admin:
|
|
58
|
+
entry: '/srv/admin/index.rip'
|
|
59
|
+
hosts: ['admin.example.com']
|
|
60
|
+
workers: 3
|
|
61
|
+
maxQueue: 700
|
|
62
|
+
queueTimeoutMs: 1234
|
|
63
|
+
readTimeoutMs: 5678
|
|
64
|
+
env: { ADMIN_MODE: '1' }
|
|
65
|
+
registered = applyEdgeConfig(config, registry, registerApp, '/srv')
|
|
66
|
+
eq registered.length, 1
|
|
67
|
+
state = getAppState(registry, 'admin')
|
|
68
|
+
eq state.config.entry, '/srv/admin/index.rip'
|
|
69
|
+
eq state.config.workers, 3
|
|
70
|
+
eq state.config.env.ADMIN_MODE, '1'
|
|
71
|
+
eq state.maxQueue, 700
|
|
72
|
+
|
|
52
73
|
# --- Binary broadcast ---
|
|
53
74
|
|
|
54
75
|
test "broadcastBinary delivers to group members", ->
|
package/tests/registry.rip
CHANGED
|
@@ -29,6 +29,27 @@ test "resolveHost returns null for unknown host", ->
|
|
|
29
29
|
r = createAppRegistry()
|
|
30
30
|
eq resolveHost(r, 'unknown.com'), null
|
|
31
31
|
|
|
32
|
+
test "resolveHost matches wildcard host", ->
|
|
33
|
+
r = createAppRegistry()
|
|
34
|
+
registerApp(r, 'wild', { hosts: ['*.example.com'] })
|
|
35
|
+
eq resolveHost(r, 'api.example.com'), 'wild'
|
|
36
|
+
|
|
37
|
+
test "resolveHost wildcard does not span multiple labels", ->
|
|
38
|
+
r = createAppRegistry()
|
|
39
|
+
registerApp(r, 'wild', { hosts: ['*.example.com'] })
|
|
40
|
+
eq resolveHost(r, 'a.b.example.com'), null
|
|
41
|
+
|
|
42
|
+
test "resolveHost falls back to catchall host", ->
|
|
43
|
+
r = createAppRegistry()
|
|
44
|
+
registerApp(r, 'catchall', { hosts: ['*'] })
|
|
45
|
+
eq resolveHost(r, 'unknown.com'), 'catchall'
|
|
46
|
+
|
|
47
|
+
test "resolveHost prefers exact host over wildcard", ->
|
|
48
|
+
r = createAppRegistry()
|
|
49
|
+
registerApp(r, 'wild', { hosts: ['*.example.com'] })
|
|
50
|
+
registerApp(r, 'exact', { hosts: ['api.example.com'] })
|
|
51
|
+
eq resolveHost(r, 'api.example.com'), 'exact'
|
|
52
|
+
|
|
32
53
|
test "getAppState returns app or null", ->
|
|
33
54
|
r = createAppRegistry()
|
|
34
55
|
registerApp(r, 'app1', { hosts: [] })
|
|
@@ -43,6 +64,12 @@ test "removeApp cleans up hosts and app", ->
|
|
|
43
64
|
eq r.hostIndex.size, 0
|
|
44
65
|
eq resolveHost(r, 'a.com'), null
|
|
45
66
|
|
|
67
|
+
test "removeApp cleans wildcard entries", ->
|
|
68
|
+
r = createAppRegistry()
|
|
69
|
+
registerApp(r, 'wild', { hosts: ['*.example.com'] })
|
|
70
|
+
removeApp(r, 'wild')
|
|
71
|
+
eq resolveHost(r, 'api.example.com'), null
|
|
72
|
+
|
|
46
73
|
test "AppState has correct defaults", ->
|
|
47
74
|
r = createAppRegistry()
|
|
48
75
|
state = registerApp(r, 'app1', { hosts: ['h.com'] })
|
package/tests/router.rip
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { test, eq, ok } from '../test.rip'
|
|
2
|
+
import { compileRouteTable, matchRoute, describeRoute } from '../edge/router.rip'
|
|
3
|
+
|
|
4
|
+
test "compileRouteTable keeps global route order", ->
|
|
5
|
+
table = compileRouteTable([
|
|
6
|
+
{ id: 'one', path: '/a', upstream: 'app' }
|
|
7
|
+
{ id: 'two', path: '/b', upstream: 'app' }
|
|
8
|
+
])
|
|
9
|
+
eq table.routes.length, 2
|
|
10
|
+
eq table.routes[0].order, 0
|
|
11
|
+
eq table.routes[1].order, 1
|
|
12
|
+
|
|
13
|
+
test "compileRouteTable merges site routes with inherited host", ->
|
|
14
|
+
table = compileRouteTable([],
|
|
15
|
+
admin.example.com:
|
|
16
|
+
routes: [{ path: '/*', app: 'admin' }]
|
|
17
|
+
)
|
|
18
|
+
eq table.routes.length, 1
|
|
19
|
+
eq table.routes[0].host, 'admin.example.com'
|
|
20
|
+
|
|
21
|
+
test "matchRoute prefers exact host over catchall", ->
|
|
22
|
+
table = compileRouteTable([
|
|
23
|
+
{ path: '/*', upstream: 'fallback' }
|
|
24
|
+
{ host: 'api.example.com', path: '/*', upstream: 'api' }
|
|
25
|
+
])
|
|
26
|
+
eq matchRoute(table, 'api.example.com', '/users').upstream, 'api'
|
|
27
|
+
|
|
28
|
+
test "matchRoute prefers wildcard host over catchall", ->
|
|
29
|
+
table = compileRouteTable([
|
|
30
|
+
{ path: '/*', upstream: 'fallback' }
|
|
31
|
+
{ host: '*.example.com', path: '/*', upstream: 'wild' }
|
|
32
|
+
])
|
|
33
|
+
eq matchRoute(table, 'api.example.com', '/users').upstream, 'wild'
|
|
34
|
+
|
|
35
|
+
test "matchRoute does not let wildcard span multiple labels", ->
|
|
36
|
+
table = compileRouteTable([
|
|
37
|
+
{ host: '*.example.com', path: '/*', upstream: 'wild' }
|
|
38
|
+
{ path: '/*', upstream: 'fallback' }
|
|
39
|
+
])
|
|
40
|
+
eq matchRoute(table, 'a.b.example.com', '/users').upstream, 'fallback'
|
|
41
|
+
|
|
42
|
+
test "matchRoute prefers exact path over prefix", ->
|
|
43
|
+
table = compileRouteTable([
|
|
44
|
+
{ path: '/api/*', upstream: 'api' }
|
|
45
|
+
{ path: '/api/users', upstream: 'users' }
|
|
46
|
+
])
|
|
47
|
+
eq matchRoute(table, 'example.com', '/api/users').upstream, 'users'
|
|
48
|
+
|
|
49
|
+
test "matchRoute prefers longer prefix", ->
|
|
50
|
+
table = compileRouteTable([
|
|
51
|
+
{ path: '/api/*', upstream: 'api' }
|
|
52
|
+
{ path: '/api/admin/*', upstream: 'admin' }
|
|
53
|
+
])
|
|
54
|
+
eq matchRoute(table, 'example.com', '/api/admin/users').upstream, 'admin'
|
|
55
|
+
|
|
56
|
+
test "matchRoute uses lower priority number before order", ->
|
|
57
|
+
table = compileRouteTable([
|
|
58
|
+
{ path: '/api/*', upstream: 'one', priority: 10 }
|
|
59
|
+
{ path: '/api/*', upstream: 'two', priority: 5 }
|
|
60
|
+
])
|
|
61
|
+
eq matchRoute(table, 'example.com', '/api/x').upstream, 'two'
|
|
62
|
+
|
|
63
|
+
test "matchRoute filters by method", ->
|
|
64
|
+
table = compileRouteTable([
|
|
65
|
+
{ path: '/submit', upstream: 'read', methods: ['GET'] }
|
|
66
|
+
{ path: '/submit', upstream: 'write', methods: ['POST'] }
|
|
67
|
+
])
|
|
68
|
+
eq matchRoute(table, 'example.com', '/submit', 'POST').upstream, 'write'
|
|
69
|
+
|
|
70
|
+
test "matchRoute returns null when nothing matches", ->
|
|
71
|
+
table = compileRouteTable([
|
|
72
|
+
{ host: 'api.example.com', path: '/api/*', upstream: 'api' }
|
|
73
|
+
])
|
|
74
|
+
eq matchRoute(table, 'example.com', '/api/x'), null
|
|
75
|
+
|
|
76
|
+
test "describeRoute summarizes action", ->
|
|
77
|
+
table = compileRouteTable([
|
|
78
|
+
{ host: 'api.example.com', path: '/api/*', upstream: 'api' }
|
|
79
|
+
])
|
|
80
|
+
summary = describeRoute(matchRoute(table, 'api.example.com', '/api/x'))
|
|
81
|
+
ok summary.includes('proxy:api')
|
|
82
|
+
|
|
83
|
+
test "compiled route preserves root field", ->
|
|
84
|
+
table = compileRouteTable([], {
|
|
85
|
+
'*.example.com':
|
|
86
|
+
routes: [
|
|
87
|
+
{ path: '/*', static: '.', root: '/mnt/site', spa: true }
|
|
88
|
+
]
|
|
89
|
+
})
|
|
90
|
+
matched = matchRoute(table, 'app.example.com', '/index.html')
|
|
91
|
+
eq matched.root, '/mnt/site'
|
|
92
|
+
|
|
93
|
+
test "compiled route preserves spa field", ->
|
|
94
|
+
table = compileRouteTable([], {
|
|
95
|
+
'*.example.com':
|
|
96
|
+
routes: [
|
|
97
|
+
{ path: '/*', static: '.', root: '/mnt/site', spa: true }
|
|
98
|
+
]
|
|
99
|
+
})
|
|
100
|
+
matched = matchRoute(table, 'app.example.com', '/index.html')
|
|
101
|
+
eq matched.spa, true
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { test, eq, ok } from '../test.rip'
|
|
2
|
+
import { coerceInt, parseFlags } from '../control/cli.rip'
|
|
3
|
+
import { createEdgeRuntime, createReloadHistoryEntry } from '../edge/runtime.rip'
|
|
4
|
+
|
|
5
|
+
test "control cli helpers load and parse basic flags", ->
|
|
6
|
+
flags = parseFlags(['bun', 'server', '--https-port=9445', '--edgefile=./Edgefile.rip', 'packages/server/default.rip'])
|
|
7
|
+
eq coerceInt('42', 0), 42
|
|
8
|
+
eq flags.httpsPort, 9445
|
|
9
|
+
ok flags.edgefilePath.endsWith('Edgefile.rip')
|
|
10
|
+
|
|
11
|
+
test "edge runtime helpers load and create summary entries", ->
|
|
12
|
+
runtime = createEdgeRuntime()
|
|
13
|
+
entry = createReloadHistoryEntry('reload-1', 'test', null, 1, 'applied')
|
|
14
|
+
eq runtime.configInfo.kind, 'none'
|
|
15
|
+
eq entry.id, 'reload-1'
|
|
16
|
+
eq entry.result, 'applied'
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { test, eq, ok } from '../test.rip'
|
|
2
|
+
import { normalizeEdgeConfig } from '../edge/config.rip'
|
|
3
|
+
|
|
4
|
+
# --- v2 basic normalization ---
|
|
5
|
+
|
|
6
|
+
test "normalizes hosts into sites object shape", ->
|
|
7
|
+
config = normalizeEdgeConfig({
|
|
8
|
+
version: 2
|
|
9
|
+
edge: {}
|
|
10
|
+
hosts:
|
|
11
|
+
'*.trusthealth.com':
|
|
12
|
+
cert: '/ssl/trusthealth.com.crt'
|
|
13
|
+
key: '/ssl/trusthealth.com.key'
|
|
14
|
+
root: '/mnt/trusthealth'
|
|
15
|
+
routes: [
|
|
16
|
+
{ path: '/*', static: '.', spa: true }
|
|
17
|
+
]
|
|
18
|
+
}, '/home/shreeve')
|
|
19
|
+
eq config.version, 2
|
|
20
|
+
eq config.kind, 'edge'
|
|
21
|
+
ok config.sites['*.trusthealth.com']
|
|
22
|
+
eq config.sites['*.trusthealth.com'].host, '*.trusthealth.com'
|
|
23
|
+
eq config.sites['*.trusthealth.com'].routes.length, 1
|
|
24
|
+
|
|
25
|
+
test "v2 extracts cert map from server blocks", ->
|
|
26
|
+
config = normalizeEdgeConfig({
|
|
27
|
+
version: 2
|
|
28
|
+
edge: {}
|
|
29
|
+
hosts:
|
|
30
|
+
'*.trusthealth.com':
|
|
31
|
+
cert: '/ssl/trusthealth.com.crt'
|
|
32
|
+
key: '/ssl/trusthealth.com.key'
|
|
33
|
+
routes: [
|
|
34
|
+
{ path: '/*', static: '/mnt/web' }
|
|
35
|
+
]
|
|
36
|
+
'*.zionlabshare.com':
|
|
37
|
+
cert: '/ssl/zionlabshare.com.crt'
|
|
38
|
+
key: '/ssl/zionlabshare.com.key'
|
|
39
|
+
routes: [
|
|
40
|
+
{ path: '/*', static: '/mnt/zion' }
|
|
41
|
+
]
|
|
42
|
+
}, '/home/shreeve')
|
|
43
|
+
ok config.certs
|
|
44
|
+
ok config.certs['*.trusthealth.com']
|
|
45
|
+
ok config.certs['*.zionlabshare.com']
|
|
46
|
+
ok config.certs['*.trusthealth.com'].certPath.endsWith('trusthealth.com.crt')
|
|
47
|
+
|
|
48
|
+
test "v2 inherits server root into routes", ->
|
|
49
|
+
config = normalizeEdgeConfig({
|
|
50
|
+
version: 2
|
|
51
|
+
edge: {}
|
|
52
|
+
hosts:
|
|
53
|
+
'*.example.com':
|
|
54
|
+
root: '/mnt/site'
|
|
55
|
+
routes: [
|
|
56
|
+
{ path: '/*', static: '.' }
|
|
57
|
+
]
|
|
58
|
+
}, '/home/shreeve')
|
|
59
|
+
route = config.sites['*.example.com'].routes[0]
|
|
60
|
+
ok route.root.includes('mnt/site')
|
|
61
|
+
|
|
62
|
+
test "v2 route root overrides server root", ->
|
|
63
|
+
config = normalizeEdgeConfig({
|
|
64
|
+
version: 2
|
|
65
|
+
edge: {}
|
|
66
|
+
hosts:
|
|
67
|
+
'*.example.com':
|
|
68
|
+
root: '/mnt/default'
|
|
69
|
+
routes: [
|
|
70
|
+
{ path: '/*', static: '.', root: '/mnt/override' }
|
|
71
|
+
]
|
|
72
|
+
}, '/home/shreeve')
|
|
73
|
+
route = config.sites['*.example.com'].routes[0]
|
|
74
|
+
ok route.root.includes('mnt/override')
|
|
75
|
+
|
|
76
|
+
test "v2 routes inherit host from server key", ->
|
|
77
|
+
config = normalizeEdgeConfig({
|
|
78
|
+
version: 2
|
|
79
|
+
edge: {}
|
|
80
|
+
hosts:
|
|
81
|
+
app.example.com:
|
|
82
|
+
routes: [
|
|
83
|
+
{ path: '/*', static: '/mnt/app' }
|
|
84
|
+
]
|
|
85
|
+
}, '/home/shreeve')
|
|
86
|
+
route = config.sites['app.example.com'].routes[0]
|
|
87
|
+
eq route.host, 'app.example.com'
|
|
88
|
+
|
|
89
|
+
test "v2 preserves spa flag on routes", ->
|
|
90
|
+
config = normalizeEdgeConfig({
|
|
91
|
+
version: 2
|
|
92
|
+
edge: {}
|
|
93
|
+
hosts:
|
|
94
|
+
'*.example.com':
|
|
95
|
+
routes: [
|
|
96
|
+
{ path: '/*', static: '/mnt/app', spa: true }
|
|
97
|
+
]
|
|
98
|
+
}, '/home/shreeve')
|
|
99
|
+
route = config.sites['*.example.com'].routes[0]
|
|
100
|
+
eq route.spa, true
|
|
101
|
+
|
|
102
|
+
# --- validation errors ---
|
|
103
|
+
|
|
104
|
+
test "rejects server block with no routes and no root", ->
|
|
105
|
+
threw = false
|
|
106
|
+
try
|
|
107
|
+
normalizeEdgeConfig({
|
|
108
|
+
version: 1
|
|
109
|
+
edge: {}
|
|
110
|
+
hosts:
|
|
111
|
+
'*.example.com':
|
|
112
|
+
cert: '/ssl/example.com.crt'
|
|
113
|
+
key: '/ssl/example.com.key'
|
|
114
|
+
}, '/home/shreeve')
|
|
115
|
+
catch e
|
|
116
|
+
threw = true
|
|
117
|
+
ok e.validationErrors.some((err) -> err.code is 'E_SERVER_ROUTES')
|
|
118
|
+
ok threw
|
|
119
|
+
|
|
120
|
+
test "root-only server block generates implicit static route", ->
|
|
121
|
+
config = normalizeEdgeConfig({
|
|
122
|
+
version: 1
|
|
123
|
+
edge: {}
|
|
124
|
+
hosts:
|
|
125
|
+
'*.example.com':
|
|
126
|
+
root: '/mnt/site'
|
|
127
|
+
}, '/home/shreeve')
|
|
128
|
+
routes = config.sites['*.example.com'].routes
|
|
129
|
+
eq routes.length, 1
|
|
130
|
+
eq routes[0].static, '.'
|
|
131
|
+
ok routes[0].root.includes('mnt/site')
|
|
132
|
+
|
|
133
|
+
test "root-only server block with spa", ->
|
|
134
|
+
config = normalizeEdgeConfig({
|
|
135
|
+
version: 1
|
|
136
|
+
edge: {}
|
|
137
|
+
hosts:
|
|
138
|
+
'*.example.com':
|
|
139
|
+
root: '/mnt/site'
|
|
140
|
+
spa: true
|
|
141
|
+
}, '/home/shreeve')
|
|
142
|
+
eq config.sites['*.example.com'].routes[0].spa, true
|
|
143
|
+
|
|
144
|
+
test "passthrough server block creates stream entries", ->
|
|
145
|
+
config = normalizeEdgeConfig({
|
|
146
|
+
version: 1
|
|
147
|
+
hosts:
|
|
148
|
+
incus.example.com:
|
|
149
|
+
passthrough: '127.0.0.1:8443'
|
|
150
|
+
}, '/home/shreeve')
|
|
151
|
+
ok config.streamUpstreams['passthrough-incus.example.com']
|
|
152
|
+
eq config.streams.length, 1
|
|
153
|
+
eq config.streams[0].sni[0], 'incus.example.com'
|
|
154
|
+
|
|
155
|
+
test "passthrough server block has no sites entry", ->
|
|
156
|
+
config = normalizeEdgeConfig({
|
|
157
|
+
version: 1
|
|
158
|
+
hosts:
|
|
159
|
+
incus.example.com:
|
|
160
|
+
passthrough: '127.0.0.1:8443'
|
|
161
|
+
}, '/home/shreeve')
|
|
162
|
+
eq config.sites['incus.example.com'], undefined
|
|
163
|
+
|
|
164
|
+
test "edge and version are optional", ->
|
|
165
|
+
config = normalizeEdgeConfig({
|
|
166
|
+
hosts:
|
|
167
|
+
'*.example.com':
|
|
168
|
+
root: '/mnt/site'
|
|
169
|
+
}, '/home/shreeve')
|
|
170
|
+
eq config.version, 1
|
|
171
|
+
ok config.edge
|
|
172
|
+
|
|
173
|
+
test "v2 rejects route with host field", ->
|
|
174
|
+
threw = false
|
|
175
|
+
try
|
|
176
|
+
normalizeEdgeConfig({
|
|
177
|
+
version: 2
|
|
178
|
+
edge: {}
|
|
179
|
+
hosts:
|
|
180
|
+
'*.example.com':
|
|
181
|
+
routes: [{ path: '/*', static: '.', host: 'other.com' }]
|
|
182
|
+
}, '/home/shreeve')
|
|
183
|
+
catch e
|
|
184
|
+
threw = true
|
|
185
|
+
ok e.validationErrors.some((err) -> err.code is 'E_SERVER_ROUTE_HOST')
|
|
186
|
+
ok threw
|
|
187
|
+
|
|
188
|
+
test "v2 validates upstream references in server routes", ->
|
|
189
|
+
threw = false
|
|
190
|
+
try
|
|
191
|
+
normalizeEdgeConfig({
|
|
192
|
+
version: 2
|
|
193
|
+
edge: {}
|
|
194
|
+
hosts:
|
|
195
|
+
'*.example.com':
|
|
196
|
+
routes: [{ path: '/api/*', upstream: 'nonexistent' }]
|
|
197
|
+
}, '/home/shreeve')
|
|
198
|
+
catch e
|
|
199
|
+
threw = true
|
|
200
|
+
ok e.validationErrors.some((err) -> err.code is 'E_ROUTE_UPSTREAM_REF')
|
|
201
|
+
ok threw
|
|
202
|
+
|
|
203
|
+
test "v2 validates app references in server routes", ->
|
|
204
|
+
threw = false
|
|
205
|
+
try
|
|
206
|
+
normalizeEdgeConfig({
|
|
207
|
+
version: 2
|
|
208
|
+
edge: {}
|
|
209
|
+
hosts:
|
|
210
|
+
'*.example.com':
|
|
211
|
+
routes: [{ path: '/*', app: 'nonexistent' }]
|
|
212
|
+
}, '/home/shreeve')
|
|
213
|
+
catch e
|
|
214
|
+
threw = true
|
|
215
|
+
ok e.validationErrors.some((err) -> err.code is 'E_ROUTE_APP_REF')
|
|
216
|
+
ok threw
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
test "server block with app field creates app route", ->
|
|
220
|
+
config = normalizeEdgeConfig({
|
|
221
|
+
hosts:
|
|
222
|
+
'dev.example.com':
|
|
223
|
+
cert: '/ssl/example.com.crt'
|
|
224
|
+
key: '/ssl/example.com.key'
|
|
225
|
+
app: 'browser'
|
|
226
|
+
apps:
|
|
227
|
+
browser:
|
|
228
|
+
entry: '/srv/default.rip'
|
|
229
|
+
root: '/mnt/www'
|
|
230
|
+
}, '/home/shreeve')
|
|
231
|
+
route = config.sites['dev.example.com'].routes[0]
|
|
232
|
+
eq route.app, 'browser'
|
|
233
|
+
eq route.host, 'dev.example.com'
|
|
234
|
+
ok config.certs['dev.example.com']
|
|
235
|
+
|
|
236
|
+
test "server block rejects unknown app reference", ->
|
|
237
|
+
threw = false
|
|
238
|
+
try
|
|
239
|
+
normalizeEdgeConfig({
|
|
240
|
+
hosts:
|
|
241
|
+
'dev.example.com':
|
|
242
|
+
app: 'nonexistent'
|
|
243
|
+
}, '/home/shreeve')
|
|
244
|
+
catch e
|
|
245
|
+
threw = true
|
|
246
|
+
ok e.validationErrors.some((err) -> err.code is 'E_SERVER_APP_REF')
|
|
247
|
+
ok threw
|
|
248
|
+
|
|
249
|
+
test "inline app definition on host block", ->
|
|
250
|
+
config = normalizeEdgeConfig({
|
|
251
|
+
hosts:
|
|
252
|
+
'api.example.com':
|
|
253
|
+
cert: '/ssl/example.com.crt'
|
|
254
|
+
key: '/ssl/example.com.key'
|
|
255
|
+
app:
|
|
256
|
+
entry: '/srv/myapi/index.rip'
|
|
257
|
+
root: '/srv/myapi'
|
|
258
|
+
}, '/home/shreeve')
|
|
259
|
+
route = config.sites['api.example.com'].routes[0]
|
|
260
|
+
eq route.app, 'app-api.example.com'
|
|
261
|
+
ok config.apps['app-api.example.com']
|
|
262
|
+
ok config.apps['app-api.example.com'].entry.includes('myapi')
|