@rip-lang/server 1.3.98 → 1.3.99

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.
@@ -0,0 +1,125 @@
1
+ # test-registry.rip — edge/registry, scheduler, and queue tests
2
+
3
+ import { test, eq, ok } from '../test.rip'
4
+ import { createAppRegistry, registerApp, resolveHost, getAppState, removeApp } from '../edge/registry.rip'
5
+ import { isCurrentVersion, getNextAvailableSocket, releaseWorker, shouldRetryBodylessBusy, drainQueueOnce } from '../edge/queue.rip'
6
+
7
+ # --- Registry ---
8
+
9
+ test "createAppRegistry returns empty maps", ->
10
+ r = createAppRegistry()
11
+ eq r.apps.size, 0
12
+ eq r.hostIndex.size, 0
13
+
14
+ test "registerApp adds app and indexes hosts", ->
15
+ r = createAppRegistry()
16
+ registerApp(r, 'myapp', { hosts: ['example.com', 'www.example.com'] })
17
+ eq r.apps.size, 1
18
+ eq r.hostIndex.size, 2
19
+ eq resolveHost(r, 'example.com'), 'myapp'
20
+ eq resolveHost(r, 'www.example.com'), 'myapp'
21
+
22
+ test "resolveHost is case-insensitive", ->
23
+ r = createAppRegistry()
24
+ registerApp(r, 'app1', { hosts: ['Example.COM'] })
25
+ eq resolveHost(r, 'example.com'), 'app1'
26
+ eq resolveHost(r, 'EXAMPLE.COM'), 'app1'
27
+
28
+ test "resolveHost returns null for unknown host", ->
29
+ r = createAppRegistry()
30
+ eq resolveHost(r, 'unknown.com'), null
31
+
32
+ test "getAppState returns app or null", ->
33
+ r = createAppRegistry()
34
+ registerApp(r, 'app1', { hosts: [] })
35
+ ok getAppState(r, 'app1')
36
+ eq getAppState(r, 'nope'), null
37
+
38
+ test "removeApp cleans up hosts and app", ->
39
+ r = createAppRegistry()
40
+ registerApp(r, 'app1', { hosts: ['a.com', 'b.com'] })
41
+ removeApp(r, 'app1')
42
+ eq r.apps.size, 0
43
+ eq r.hostIndex.size, 0
44
+ eq resolveHost(r, 'a.com'), null
45
+
46
+ test "AppState has correct defaults", ->
47
+ r = createAppRegistry()
48
+ state = registerApp(r, 'app1', { hosts: ['h.com'] })
49
+ eq state.sockets.length, 0
50
+ eq state.inflightTotal, 0
51
+ eq state.maxQueue, 512
52
+ eq state.queueTimeoutMs, 30000
53
+
54
+ # --- Scheduler ---
55
+
56
+ test "getNextAvailableSocket returns worker with 0 inflight", ->
57
+ workers = [{ socket: '/a', inflight: 0, version: 1, workerId: 0 }]
58
+ sock = getNextAvailableSocket(workers, null)
59
+ eq sock.socket, '/a'
60
+
61
+ test "getNextAvailableSocket skips inflight workers", ->
62
+ workers = [
63
+ { socket: '/a', inflight: 1, version: 1, workerId: 0 }
64
+ { socket: '/b', inflight: 0, version: 1, workerId: 1 }
65
+ ]
66
+ sock = getNextAvailableSocket(workers, null)
67
+ eq sock.socket, '/b'
68
+
69
+ test "getNextAvailableSocket returns null when all busy", ->
70
+ workers = [{ socket: '/a', inflight: 1, version: 1, workerId: 0 }]
71
+ sock = getNextAvailableSocket(workers, null)
72
+ eq sock, null
73
+
74
+ test "isCurrentVersion with no version tracking", ->
75
+ ok isCurrentVersion(null, { version: 1 })
76
+ ok isCurrentVersion(null, { version: null })
77
+
78
+ test "releaseWorker resets inflight and re-adds to pool", ->
79
+ w = { socket: '/a', inflight: 1, version: 1, workerId: 0 }
80
+ sockets = [w]
81
+ available = []
82
+ releaseWorker(sockets, available, null, w)
83
+ eq w.inflight, 0
84
+ eq available.length, 1
85
+
86
+ test "shouldRetryBodylessBusy for GET + 503 + busy header", ->
87
+ req = { method: 'GET' }
88
+ headers = new Headers({ 'Rip-Worker-Busy': '1' })
89
+ res = { status: 503, headers }
90
+ ok shouldRetryBodylessBusy(req, res)
91
+
92
+ test "shouldRetryBodylessBusy rejects POST", ->
93
+ req = { method: 'POST' }
94
+ headers = new Headers({ 'Rip-Worker-Busy': '1' })
95
+ res = { status: 503, headers }
96
+ eq shouldRetryBodylessBusy(req, res), false
97
+
98
+ # --- Queue ---
99
+
100
+ test "drainQueueOnce processes one job from queue", ->
101
+ processed = []
102
+ queue = [{ req: 'r1', resolve: (->), enqueuedAt: Date.now() }]
103
+ inflightOut = drainQueueOnce(
104
+ 0, 2, (-> 1),
105
+ (-> queue.shift()),
106
+ (-> false),
107
+ (->),
108
+ (-> { socket: '/a', inflight: 0 }),
109
+ ((job, worker) -> processed.push(job.req))
110
+ )
111
+ eq processed.length, 1
112
+ eq inflightOut, 1
113
+
114
+ test "drainQueueOnce resolves timed-out job", ->
115
+ resolved = []
116
+ queue = [{ req: 'r1', resolve: ((r) -> resolved.push('timeout')), enqueuedAt: 0 }]
117
+ drainQueueOnce(
118
+ 0, 2, (-> 1),
119
+ (-> queue.shift()),
120
+ (-> true),
121
+ (-> 'timeout-response'),
122
+ (-> null),
123
+ (->)
124
+ )
125
+ eq resolved[0], 'timeout'
@@ -0,0 +1,95 @@
1
+ # security.rip — rate limiter, request validation, and request ID tests
2
+
3
+ import { test, eq, ok } from '../test.rip'
4
+ import { createRateLimiter, rateLimitResponse } from '../edge/ratelimit.rip'
5
+ import { validateRequest } from '../edge/security.rip'
6
+ import { generateRequestId } from '../edge/forwarding.rip'
7
+
8
+ # --- Request ID ---
9
+
10
+ test "generateRequestId returns req- prefixed hex", ->
11
+ id = generateRequestId()
12
+ ok id.startsWith('req-')
13
+ eq id.length, 16 # "req-" + 12 hex chars (6 random bytes)
14
+
15
+ test "generateRequestId is unique", ->
16
+ a = generateRequestId()
17
+ b = generateRequestId()
18
+ ok a isnt b
19
+
20
+ # --- Rate limiter ---
21
+
22
+ test "createRateLimiter allows requests under limit", ->
23
+ limiter = createRateLimiter(5, 60000)
24
+ result = limiter.check('1.2.3.4')
25
+ eq result.allowed, true
26
+ eq result.remaining, 4
27
+ limiter.stop()
28
+
29
+ test "rate limiter rejects when limit exceeded", ->
30
+ limiter = createRateLimiter(3, 60000)
31
+ limiter.check('1.2.3.4')
32
+ limiter.check('1.2.3.4')
33
+ limiter.check('1.2.3.4')
34
+ result = limiter.check('1.2.3.4')
35
+ eq result.allowed, false
36
+ ok result.retryAfter > 0
37
+ limiter.stop()
38
+
39
+ test "rate limiter tracks IPs independently", ->
40
+ limiter = createRateLimiter(2, 60000)
41
+ limiter.check('1.1.1.1')
42
+ limiter.check('1.1.1.1')
43
+ result = limiter.check('2.2.2.2')
44
+ eq result.allowed, true
45
+ limiter.stop()
46
+
47
+ test "rate limiter reset clears IP", ->
48
+ limiter = createRateLimiter(2, 60000)
49
+ limiter.check('1.1.1.1')
50
+ limiter.check('1.1.1.1')
51
+ limiter.reset('1.1.1.1')
52
+ result = limiter.check('1.1.1.1')
53
+ eq result.allowed, true
54
+ limiter.stop()
55
+
56
+ test "rateLimitResponse returns 429", ->
57
+ res = rateLimitResponse(30)
58
+ eq res.status, 429
59
+ eq res.headers.get('Retry-After'), '30'
60
+
61
+ # --- Request validation ---
62
+
63
+ test "validateRequest accepts normal request", ->
64
+ req = new Request('http://localhost/hello')
65
+ result = validateRequest(req)
66
+ eq result.valid, true
67
+
68
+ test "validateRequest rejects CL+TE conflict", ->
69
+ req = new Request 'http://localhost/',
70
+ headers: { 'content-length': '10', 'transfer-encoding': 'chunked' }
71
+ result = validateRequest(req)
72
+ eq result.valid, false
73
+ eq result.status, 400
74
+
75
+ test "validateRequest rejects multiple Host headers", ->
76
+ req = new Request 'http://localhost/',
77
+ headers: { host: 'a.com, b.com' }
78
+ result = validateRequest(req)
79
+ eq result.valid, false
80
+
81
+ test "URL parser normalizes basic path traversal", ->
82
+ # Bun's URL parser resolves .. before it reaches validateRequest
83
+ url = new URL('http://localhost/../../../etc/passwd')
84
+ eq url.pathname, '/etc/passwd' # already normalized — safe
85
+
86
+ test "validateRequest rejects double-encoded traversal", ->
87
+ # %252e%252e decodes to %2e%2e on first pass, then .. on second
88
+ req = new Request('http://localhost/%252e%252e/%252e%252e/etc/passwd')
89
+ result = validateRequest(req)
90
+ eq result.valid, false
91
+
92
+ test "validateRequest accepts normal paths with dots", ->
93
+ req = new Request('http://localhost/api/v1.0/users')
94
+ result = validateRequest(req)
95
+ eq result.valid, true