core icon indicating copy to clipboard operation
core copied to clipboard

JAPA does not make correct API calls, unpredictably

Open magnus-trent opened this issue 1 year ago • 4 comments

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=\"{&quot;component&quot;:&quot;home&quot;,&quot;version&quot;:&quot;1&quot;,&quot;props&quot;:{&quot;version&quot;:2},&quot;url&quot;:&quot;/&quot;}\"></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

magnus-trent avatar Jun 27 '24 19:06 magnus-trent

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.

magnus-trent avatar Jun 27 '24 20:06 magnus-trent

I do still find it odd that it redirects instead of raising the Validation Error to the console.

magnus-trent avatar Jun 27 '24 20:06 magnus-trent

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.

RomainLanz avatar Jun 28 '24 07:06 RomainLanz

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.

magnus-trent avatar Jun 28 '24 16:06 magnus-trent

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.

thetutlage avatar Jul 03 '24 04:07 thetutlage