FazBrowse GitHub Viewer
|
Trending
|
URL:
|
Home
Tools:
[Download Repo ZIP]
[View Raw Code]
[Original HTTPS Page]
JavaScriptSolidServer/test/app-paths.test.js at gh-pages · JavaScriptSolidServer/JavaScriptSolidServer · GitHub
JavaScriptSolidServer
JavaScriptSolidServer
Repository navigation
Code
Issues
164
(164)
Pull requests
14
(14)
Actions
Projects
Security and quality
Insights
Expand file tree
Breadcrumbs
JavaScriptSolidServer
/
test
/
app-paths.test.js
Copy path
More file actions
More file actions
Latest commit
History
History
History
233 lines (213 loc) · 9.86 KB
Breadcrumbs
JavaScriptSolidServer
/
test
/
app-paths.test.js
Copy path
File metadata and controls
233 lines (213 loc) · 9.86 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
/**
* appPaths — WAC-exempt application mount points (#582).
*
* The global WAC preHandler authorizes every URL against pod ACLs and
* rejects before routes run; the only escapes are its hardcoded prefix
* list (/storage/, /db, /mcp, …). That means a third-party app plugin
* registering routes on the returned fastify instance (#206's plugin-zero
* pattern, e.g. a game mounted at /tideholm) has its POSTs swallowed by
* WAC with no way to opt out.
*
* createServer({ appPaths: ['/myapp'] }) declares URL prefixes owned by
* registered applications: requests at or below an app path skip the WAC
* hook, and the app owns authentication and authorization under its
* prefix — exactly the deal the bundled pseudo-plugins already have.
*
* Tests verify the seam end-to-end: an app route mounted on the returned
* instance receives unauthenticated requests (WAC stays out), sibling LDP
* paths keep full WAC enforcement, and malformed appPaths entries are
* dropped rather than becoming accidental holes.
*/
import
{
describe
,
it
,
before
,
after
,
afterEach
}
from
'node:test'
;
import
assert
from
'node:assert'
;
import
{
createServer
}
from
'../src/server.js'
;
import
fs
from
'fs-extra'
;
import
path
from
'path'
;
import
os
from
'os'
;
const
TEST_DATA_DIR
=
'./test-data-app-paths'
;
let
server
;
let
baseUrl
;
let
originalDataRoot
;
// A plugin that mounts at its prefix but handles ONE path only (#611).
const
PARTIAL_PLUGIN
=
`export async function activate(api) {
api.fastify.get(api.prefix + '/echo', async () => ({ app: true, plugin: true }));
}
`
;
let
fixtureDir
;
// a fresh temp dir per run, so parallel runs never collide
// routes: 'all' mounts the app on the prefix and its whole subtree (the
// #582 shape); 'partial' registers one route only, leaving the rest of
// the subtree unhandled (#611); 'plugin' loads PARTIAL_PLUGIN instead.
async
function
startWith
(
appPaths
,
routes
=
'all'
)
{
await
fs
.
emptyDir
(
TEST_DATA_DIR
)
;
if
(
routes
===
'plugin'
)
{
fixtureDir
??=
await
fs
.
mkdtemp
(
path
.
join
(
os
.
tmpdir
(
)
,
'jss-app-paths-'
)
)
;
await
fs
.
writeFile
(
path
.
join
(
fixtureDir
,
'partial.js'
)
,
PARTIAL_PLUGIN
)
;
}
server
=
createServer
(
{
logger
:
false
,
forceCloseConnections
:
true
,
root
:
TEST_DATA_DIR
,
...
(
routes
===
'plugin'
?
{
plugins
:
[
{
id
:
'partial'
,
module
:
path
.
join
(
fixtureDir
,
'partial.js'
)
,
prefix
:
'/myapp'
}
]
}
:
{
appPaths
}
)
,
}
)
;
// An app in the #206 plugin-zero shape: routes registered on the returned
// instance, answering with its own status codes (its own "auth").
if
(
routes
===
'all'
)
{
server
.
all
(
'/myapp'
,
echo
)
;
server
.
all
(
'/myapp/*'
,
echo
)
;
}
else
if
(
routes
===
'partial'
)
{
server
.
get
(
'/myapp/echo'
,
echo
)
;
}
async
function
echo
(
request
,
reply
)
{
reply
.
code
(
200
)
.
send
(
{
app
:
true
,
method
:
request
.
method
,
url
:
request
.
url
,
webId
:
request
.
webId
??
null
,
// hook skipped -> never set
}
)
;
}
await
server
.
listen
(
{
port
:
0
,
host
:
'127.0.0.1'
}
)
;
const
address
=
server
.
server
.
address
(
)
;
baseUrl
=
`http://127.0.0.1:
${
address
.
port
}
`
;
}
describe
(
'appPaths application mount points (#582)'
,
(
)
=>
{
before
(
(
)
=>
{
// createServer({ root }) mutates process.env.DATA_ROOT; snapshot so the
// test dir doesn't leak into later suites.
originalDataRoot
=
process
.
env
.
DATA_ROOT
;
}
)
;
afterEach
(
async
(
)
=>
{
if
(
server
)
{
await
server
.
close
(
)
;
server
=
null
;
}
await
fs
.
remove
(
TEST_DATA_DIR
)
;
}
)
;
after
(
async
(
)
=>
{
if
(
originalDataRoot
===
undefined
)
delete
process
.
env
.
DATA_ROOT
;
else
process
.
env
.
DATA_ROOT
=
originalDataRoot
;
if
(
fixtureDir
)
await
fs
.
remove
(
fixtureDir
)
;
}
)
;
it
(
'unauthenticated POST below an app path reaches the app handler'
,
async
(
)
=>
{
await
startWith
(
[
'/myapp'
]
)
;
const
res
=
await
fetch
(
`
${
baseUrl
}
/myapp/api/action`
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
}
,
body
:
JSON
.
stringify
(
{
hello
:
'world'
}
)
,
}
)
;
assert
.
strictEqual
(
res
.
status
,
200
)
;
const
body
=
await
res
.
json
(
)
;
assert
.
strictEqual
(
body
.
app
,
true
)
;
assert
.
strictEqual
(
body
.
method
,
'POST'
)
;
}
)
;
it
(
'the bare app path and query-string forms are exempt too'
,
async
(
)
=>
{
await
startWith
(
[
'/myapp'
]
)
;
for
(
const
path
of
[
'/myapp'
,
'/myapp?tab=map'
]
)
{
const
res
=
await
fetch
(
`
${
baseUrl
}
${
path
}
`
,
{
method
:
'POST'
}
)
;
assert
.
strictEqual
(
res
.
status
,
200
,
`
${
path
}
should reach the app`
)
;
}
}
)
;
it
(
'the WAC hook never sets request.webId on app-path requests'
,
async
(
)
=>
{
await
startWith
(
[
'/myapp'
]
)
;
const
res
=
await
fetch
(
`
${
baseUrl
}
/myapp/whoami`
)
;
const
body
=
await
res
.
json
(
)
;
assert
.
strictEqual
(
body
.
webId
,
null
)
;
}
)
;
it
(
'sibling LDP paths keep full WAC enforcement'
,
async
(
)
=>
{
await
startWith
(
[
'/myapp'
]
)
;
// Writing outside the app prefix without auth must still be rejected.
const
res
=
await
fetch
(
`
${
baseUrl
}
/notes.jsonld`
,
{
method
:
'PUT'
,
headers
:
{
'Content-Type'
:
'application/ld+json'
}
,
body
:
JSON
.
stringify
(
{
'@id'
:
''
,
name
:
'x'
}
)
,
}
)
;
assert
.
ok
(
res
.
status
===
401
||
res
.
status
===
403
,
`expected WAC rejection, got
${
res
.
status
}
`
)
;
}
)
;
it
(
'a prefix match is a path-segment match, not a string prefix'
,
async
(
)
=>
{
await
startWith
(
[
'/myapp'
]
)
;
// /myapplication must NOT be exempt just because it shares characters.
const
res
=
await
fetch
(
`
${
baseUrl
}
/myapplication.jsonld`
,
{
method
:
'PUT'
,
headers
:
{
'Content-Type'
:
'application/ld+json'
}
,
body
:
JSON
.
stringify
(
{
'@id'
:
''
,
name
:
'x'
}
)
,
}
)
;
assert
.
ok
(
res
.
status
===
401
||
res
.
status
===
403
,
`expected WAC rejection, got
${
res
.
status
}
`
)
;
}
)
;
it
(
'trailing-slash entries are normalized, children still exempt'
,
async
(
)
=>
{
await
startWith
(
[
'/myapp/'
]
)
;
const
res
=
await
fetch
(
`
${
baseUrl
}
/myapp/api/action`
,
{
method
:
'POST'
}
)
;
assert
.
strictEqual
(
res
.
status
,
200
)
;
}
)
;
it
(
'malformed appPaths entries are dropped, not accidental holes'
,
async
(
)
=>
{
// No leading slash and bare '/' are both invalid; with them filtered out
// the route registrations still exist but WAC fires first.
await
startWith
(
[
'myapp'
,
'/'
,
'///'
,
' '
]
)
;
const
res
=
await
fetch
(
`
${
baseUrl
}
/myapp/api/action`
,
{
method
:
'POST'
}
)
;
assert
.
ok
(
res
.
status
===
401
||
res
.
status
===
403
,
`expected WAC rejection (invalid entries dropped), got
${
res
.
status
}
`
)
;
}
)
;
it
(
'omitting appPaths changes nothing (default off)'
,
async
(
)
=>
{
await
startWith
(
undefined
)
;
const
res
=
await
fetch
(
`
${
baseUrl
}
/myapp/api/action`
,
{
method
:
'POST'
}
)
;
assert
.
ok
(
res
.
status
===
401
||
res
.
status
===
403
,
`expected WAC rejection with no appPaths, got
${
res
.
status
}
`
)
;
}
)
;
// #611: WAC is skipped under an app path, and the LDP wildcards sit
// beneath every app route. A path the app never registered must be a
// 404 inside the app's namespace, never an unauthenticated storage
// read or write.
describe
(
'unhandled paths under an app subtree (#611)'
,
(
)
=>
{
for
(
const
shape
of
[
'partial'
,
'plugin'
]
)
{
it
(
`
${
shape
}
: anonymous PUT below the prefix is 404 and writes nothing`
,
async
(
)
=>
{
await
startWith
(
[
'/myapp'
]
,
shape
)
;
// The container exists, which is when the fall-through used to 201.
await
fs
.
ensureDir
(
path
.
join
(
TEST_DATA_DIR
,
'myapp'
)
)
;
const
res
=
await
fetch
(
`
${
baseUrl
}
/myapp/planted.txt`
,
{
method
:
'PUT'
,
headers
:
{
'Content-Type'
:
'text/plain'
}
,
body
:
'planted'
,
}
)
;
assert
.
strictEqual
(
res
.
status
,
404
)
;
assert
.
strictEqual
(
await
fs
.
pathExists
(
path
.
join
(
TEST_DATA_DIR
,
'myapp'
,
'planted.txt'
)
)
,
false
)
;
}
)
;
it
(
`
${
shape
}
: anonymous GET, DELETE and PATCH below the prefix are 404, the app route still answers`
,
async
(
)
=>
{
await
startWith
(
[
'/myapp'
]
,
shape
)
;
await
fs
.
ensureDir
(
path
.
join
(
TEST_DATA_DIR
,
'myapp'
)
)
;
await
fs
.
writeFile
(
path
.
join
(
TEST_DATA_DIR
,
'myapp'
,
'secret.txt'
)
,
'private'
)
;
for
(
const
method
of
[
'GET'
,
'HEAD'
,
'DELETE'
,
'PATCH'
]
)
{
const
res
=
await
fetch
(
`
${
baseUrl
}
/myapp/secret.txt`
,
{
method
,
...
(
method
===
'PATCH'
?
{
headers
:
{
'Content-Type'
:
'text/n3'
}
,
body
:
''
}
:
{
}
)
,
}
)
;
assert
.
strictEqual
(
res
.
status
,
404
,
`
${
method
}
should be 404`
)
;
}
assert
.
strictEqual
(
await
fs
.
readFile
(
path
.
join
(
TEST_DATA_DIR
,
'myapp'
,
'secret.txt'
)
,
'utf8'
)
,
'private'
)
;
const
ok
=
await
fetch
(
`
${
baseUrl
}
/myapp/echo`
)
;
assert
.
strictEqual
(
ok
.
status
,
200
)
;
assert
.
strictEqual
(
(
await
ok
.
json
(
)
)
.
app
,
true
)
;
}
)
;
}
it
(
'OPTIONS below the prefix is a bare 204 for existing and missing paths alike (no existence leak)'
,
async
(
)
=>
{
await
startWith
(
[
'/myapp'
]
,
'plugin'
)
;
await
fs
.
ensureDir
(
path
.
join
(
TEST_DATA_DIR
,
'myapp'
,
'dir'
)
)
;
await
fs
.
writeFile
(
path
.
join
(
TEST_DATA_DIR
,
'myapp'
,
'secret.txt'
)
,
'private'
)
;
const
seen
=
[
]
;
for
(
const
p
of
[
'/myapp/secret.txt'
,
'/myapp/missing.txt'
,
'/myapp/dir/'
,
'/myapp/nodir/'
]
)
{
const
res
=
await
fetch
(
`
${
baseUrl
}
${
p
}
`
,
{
method
:
'OPTIONS'
,
headers
:
{
Origin
:
'https://app.example'
}
}
)
;
assert
.
strictEqual
(
res
.
status
,
204
,
`
${
p
}
preflight should be 204`
)
;
assert
.
strictEqual
(
res
.
headers
.
get
(
'access-control-allow-origin'
)
,
'https://app.example'
)
;
seen
.
push
(
[
res
.
headers
.
get
(
'link'
)
,
res
.
headers
.
get
(
'accept-patch'
)
,
res
.
headers
.
get
(
'allow'
)
]
.
join
(
'|'
)
)
;
}
assert
.
strictEqual
(
new
Set
(
seen
)
.
size
,
1
,
`OPTIONS headers must not vary with storage:
${
seen
.
join
(
' / '
)
}
`
)
;
assert
.
strictEqual
(
seen
[
0
]
.
includes
(
'Container'
)
,
false
)
;
}
)
;
it
(
'a sibling LDP path is untouched: PUT is still 401, not 404'
,
async
(
)
=>
{
await
startWith
(
[
'/myapp'
]
,
'partial'
)
;
const
res
=
await
fetch
(
`
${
baseUrl
}
/elsewhere.txt`
,
{
method
:
'PUT'
,
headers
:
{
'Content-Type'
:
'text/plain'
}
,
body
:
'x'
}
)
;
assert
.
strictEqual
(
res
.
status
,
401
)
;
}
)
;
}
)
;
}
)
;
Back
|
FazBrowse Home
|
New Git URL