JAPA does not make correct API calls, unpredictably
Package version
6.9.1
Describe the bug
When running HTTP tests, despite the OpenAPI Schema endpoints matching, as well as controllers, tests, route setups, JAPA defaults to GET http://localhost:3333/ with no request data.
This appears as an Error: schema not found for {"path":"","method":"get","status":200}, and logging the output things get weirder.
This is the output of the only successful PATCH request, which shows that await client.patch('/v2/stations/1').json({ name: 'Helpdesk 2', }) works.
{
"cookiesJar": {
"adonis-session": {
"name": "adonis-session",
"value": "r0aui9ddz27csqcbku9j2orz",
"maxAge": 7200,
"path": "/",
"httpOnly": true,
"sameSite": "Lax"
}
},
"request": {
"hooks": {},
"request": {
"method": "PATCH",
"url": "http://localhost:3333/v2/stations/1",
"data": {
"name": "Helpdesk 2"
},
"headers": {
"content-type": "application/json"
}
},
"cookiesJar": {},
"config": {
"baseUrl": "http://localhost:3333",
"method": "PATCH",
"endpoint": "/v2/stations/1",
"hooks": {
"setup": [],
"teardown": []
},
"serializers": {
"cookie": {}
}
}
},
"response": {
"req": {
"method": "PATCH",
"url": "http://localhost:3333/v2/stations/1",
"data": {
"name": "Helpdesk 2"
},
"headers": {
"content-type": "application/json"
}
},
"header": {
"x-frame-options": "DENY",
"strict-transport-security": "max-age=15552000",
"x-content-type-options": "nosniff",
"set-cookie": [
"adonis-session=s%3AeyJtZXNzYWdlIjoicjBhdWk5ZGR6Mjdjc3FjYmt1OWoyb3J6IiwicHVycG9zZSI6ImFkb25pcy1zZXNzaW9uIn0.4EKncghFqRZsxVw4rHdsJQZOOqPw7r5FgekowRl_Tts; Max-Age=7200; Path=/; HttpOnly; SameSite=Lax"
],
"date": "Thu, 27 Jun 2024 19:23:12 GMT",
"connection": "close",
"transfer-encoding": "chunked"
},
"status": 200,
"text": ""
},
"config": {
"baseUrl": "http://localhost:3333",
"method": "PATCH",
"endpoint": "/v2/stations/1",
"hooks": {
"setup": [],
"teardown": []
},
"serializers": {
"cookie": {}
}
},
"assert": {
"assertions": {
"total": 0,
"mismatchError": null
}
}
}
However, this is one of the failing PATCH requests. Run with await client.patch('/v2/stations/config/bookmarks/2').json({ name: 'Googol', }).
{
"cookiesJar": {
"adonis-session": {
"name": "adonis-session",
"value": "tr4bbngesj7sxl88m87vi9qo",
"maxAge": 7200,
"path": "/",
"httpOnly": true,
"sameSite": "Lax"
}
},
"request": {
"hooks": {},
"request": {
"method": "GET",
"url": "http://localhost:3333/",
"data": null,
"headers": {
"accept-encoding": "gzip, deflate"
}
},
"cookiesJar": {},
"config": {
"baseUrl": "http://localhost:3333",
"method": "PATCH",
"endpoint": "/v2/stations/config/bookmarks/2",
"hooks": {
"setup": [],
"teardown": []
},
"serializers": {
"cookie": {}
}
}
},
"response": {
"req": {
"method": "GET",
"url": "http://localhost:3333/",
"data": null,
"headers": {
"accept-encoding": "gzip, deflate"
}
},
"header": {
"x-frame-options": "DENY",
"strict-transport-security": "max-age=15552000",
"x-content-type-options": "nosniff",
"set-cookie": [
"adonis-session=s%3AeyJtZXNzYWdlIjoidHI0YmJuZ2VzajdzeGw4OG04N3ZpOXFvIiwicHVycG9zZSI6ImFkb25pcy1zZXNzaW9uIn0.ZEslPN3oxbjkRuwDnA3zqKXq6iJm5dNxrXTa04vcOtw; Max-Age=7200; Path=/; HttpOnly; SameSite=Lax"
],
"content-length": "908",
"content-type": "text/html; charset=utf-8",
"date": "Thu, 27 Jun 2024 19:23:11 GMT",
"connection": "close"
},
"status": 200,
"text": "<!DOCTYPE html>\r\n<html>\r\n\r\n<head>\r\n <meta charset=\"utf-8\">\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\r\n\r\n <title inertia>AdonisJS x Inertia x React</title>\r\n<script type=\"module\">\nimport RefreshRuntime from '/@react-refresh'\nRefreshRuntime.injectIntoGlobalHook(window)\nwindow.$RefreshReg$ = () => {}\nwindow.$RefreshSig$ = () => (type) =>
type\nwindow.__vite_plugin_react_preamble_installed__ = true\n</script>\r\n\r\n<link rel=\"stylesheet\" as=\"style\" href=\"/inertia/css/app.css\"/>\n<script type=\"module\" src=\"/@vite/client\"></script>\n<script type=\"module\" defer src=\"/inertia/app/app.tsx\"></script>\n<script type=\"module\" defer src=\"/inertia/pages/home.tsx\"></script>\r\n</head>\r\n\r\n<body><div id=\"app\" data-page=\"{"component":"home","version":"1","props":{"version":2},"url":"/"}\"></div>\r\n</body>\r\n\r\n</html>"
},
"config": {
"baseUrl": "http://localhost:3333",
"method": "PATCH",
"endpoint": "/v2/stations/config/bookmarks/2",
"hooks": {
"setup": [],
"teardown": []
},
"serializers": {
"cookie": {}
}
},
"assert": {
"assertions": {
"total": 0,
"mismatchError": null
}
}
}
These are the routes, within a "v2" group.
router.resource('stations', StationsController).apiOnly()
router.resource('stations/config/procedures', StationConfigProceduresController).apiOnly()
router.resource('stations/config/bookmarks', StationConfigBookmarksController).apiOnly()
And my schema.
/stations/{station}:
get:
operationId: getStation
summary: Get a Station by its ID
responses:
200:
description: A single Station
content:
application/json:
schema:
$ref: '#/components/schemas/Station'
404:
description: Station not found
content:
application/json:
schema:
type: object
tags:
- Stations
patch:
operationId: updateStation
summary: Update a Station
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/StationUpdate'
responses:
200:
description: Station updated
content:
application/json:
schema:
type: object
404:
description: Station not found
content:
application/json:
schema:
type: object
tags:
- Stations
delete:
operationId: deleteStation
summary: Delete a Station
responses:
200:
description: Station deleted
content:
application/json:
schema:
type: object
404:
description: Station not found
content:
application/json:
schema:
type: object
tags:
- Stations
parameters:
- name: station
required: true
in: path
description: The Station ID
schema:
type: string
/stations/config/bookmarks/{bookmark}:
get:
operationId: getStationBookmark
summary: Get a single Station Bookmark
responses:
200:
description: A single Station Bookmark
content:
application/json:
schema:
$ref: '#/components/schemas/StationBookmark'
404:
description: Station Bookmark not found
content:
application/json:
schema:
type: object
tags:
- Station Bookmarks
patch:
operationId: updateStationBookmark
summary: Update a Station Bookmark
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/StationBookmarkUpdate'
responses:
200:
description: Station Bookmark updated
content:
application/json:
schema:
$ref: '#/components/schemas/StationBookmark'
404:
description: Station Bookmark not found
content:
application/json:
schema:
type: object
tags:
- Station Bookmarks
delete:
operationId: deleteStationBookmark
summary: Delete a Station Bookmark
responses:
200:
description: Station Bookmark deleted
content:
application/json:
schema:
type: object
404:
description: Station Bookmark not found
content:
application/json:
schema:
type: object
tags:
- Station Bookmarks
parameters:
- name: bookmark
required: true
in: path
description: The Station Bookmark ID
schema:
type: string
And finally, the test output.
yarn test
[ info ] booting application to run tests...
❯ Executed 8 migrations (83 ms)
❯ completed database/seeders/station_bookmark_seeder
❯ completed database/seeders/station_procedure_seeder
functional / Station Bookmarks success (tests\functional\stations\stations.config.bookmarks.spec.ts)
√ creates a station bookmark (70.84ms)
√ returns a station bookmark list (37.76ms)
√ returns only 4 station bookmarks (9.94ms)
× updates a station bookmark (345.96ms)
√ deletes a station bookmark (25.98ms)
functional / Station Procedures success (tests\functional\stations\stations.config.procedures.spec.ts)
× creates a station procedure (44.83ms)
√ returns a station procedure list (20.36ms)
√ returns only 4 station procedures (5.11ms)
× updates a station procedure (9.56ms)
√ deletes a station procedure (9.13ms)
functional / Station Procedures failure (tests\functional\stations\stations.config.procedures.spec.ts)
√ cannot find station procedure that does not exist (8.5ms)
× cannot update station procedure that does not exist (7.64ms)
√ cannot delete station procedure that does not exist (4.16ms)
functional / Stations success (tests\functional\stations\stations.spec.ts)
√ creates a station (15.49ms)
√ returns a station list (23.05ms)
√ returns only 4 stations (4.69ms)
√ updates a station (12.75ms)
√ deletes a station (11.56ms)
functional / Stations failure (tests\functional\stations\stations.spec.ts)
√ cannot find station that does not exist (9.43ms)
√ cannot update station that does not exist (4.03ms)
√ cannot delete station that does not exist (3.4ms)
functional / User Passwords success (tests\functional\users\user.passwords.spec.ts)
√ creates a user password (16.44ms)
√ returns a user password list (15.93ms)
√ returns only 4 user passwords (4.5ms)
× updates a user password (8.15ms)
√ deletes a user password (8.46ms)
functional / User Passwords failure (tests\functional\users\user.passwords.spec.ts)
√ cannot find user password that does not exist (7.52ms)
× cannot update user password that does not exist (6.72ms)
√ cannot delete user password that does not exist (3.26ms)
❯ Reverted 8 migrations (52 ms)
Reproduction repo
No response
Thanks to this issue: https://github.com/adonisjs/core/issues/1916, I was able to determine that my validators were wrong, not setting optional() for the update requests.
I do still find it odd that it redirects instead of raising the Validation Error to the console.
I do still find it odd that it redirects instead of raising the Validation Error to the console.
It redirects because you probably didn't request a JSON answer (with the Accept header). By default, errors are 302 redirects with the information in a flash message.
Fixing the validators immediately fixed the issue, manual testing may have sped things up finding the problem but these were all tests run with await client.patch(...).json(...). I'm still learning the framework and its components but I simply wanted to raise awareness that when validators fail, for example not setting properties optional and passing partial data, tests fail cryptically.
As far as making a JSON request in tests I was certain the .json() chain was for that purpose.
The .json chain is not for that purpose. It means you are setting the Content-type of the request as JSON. Whereas, the validator error handling relies on the Accept header. The Accept header is for content negotiation.
If you are building an API. The first thing you should be doing is either validating the Accept header of the request or always force it to JSON.