mirror of
https://github.com/inventree/InvenTree.git
synced 2026-11-10 10:57:52 +00:00
Add CI checks for /api-doc/ endpoint (#13210)
* Add CI checks for /api-doc/ endpoint Ref: https://github.com/inventree/InvenTree/issues/13200 * fix playwright test * Reduce flakiness
This commit is contained in:
1 parent
3866f7dbf5
commit
df2a5d853e
3 files changed
+110
-12
No files matched your search
@@ -1,6 +1,7 @@
|
||||
"""Unit tests for the main web views."""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
@@ -18,12 +19,55 @@ class ViewTests(InvenTreeTestCase):
|
||||
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver']
|
||||
)
|
||||
def test_api_doc(self):
|
||||
"""Test that the api-doc view works."""
|
||||
"""Test that the api-doc view renders a usable Redoc page.
|
||||
|
||||
A 200 response alone is not sufficient - the page is a thin HTML shell
|
||||
which loads Redoc from a CDN and points it at the schema endpoint.
|
||||
Ref: https://github.com/inventree/InvenTree/issues/13200
|
||||
"""
|
||||
api_url = os.path.join(reverse('index'), 'api-doc') + '/'
|
||||
|
||||
response = self.client.get(api_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
html = response.content.decode()
|
||||
schema_url = reverse('schema')
|
||||
|
||||
# The page must point Redoc at our schema endpoint
|
||||
self.assertIn(schema_url, html)
|
||||
|
||||
# Find the Redoc bundle which is loaded by the page
|
||||
scripts = re.findall(r'<script([^>]*)\ssrc="([^"]+)"', html)
|
||||
redoc_scripts = [(attrs, src) for attrs, src in scripts if 'redoc' in src]
|
||||
self.assertEqual(len(redoc_scripts), 1, 'Redoc script not found in api-doc')
|
||||
|
||||
attrs, src = redoc_scripts[0]
|
||||
|
||||
# The Redoc version must be pinned - '@latest' can (and did) change under us
|
||||
match = re.search(r'redoc@(\d+)(?:\.\d+)*/', src)
|
||||
self.assertIsNotNone(
|
||||
match, f'Redoc distribution must be pinned to a version: {src}'
|
||||
)
|
||||
|
||||
# Redoc >= 3 is ES-module only and cannot be loaded via a classic <script>
|
||||
if int(match.group(1)) >= 3:
|
||||
self.assertIn(
|
||||
'type="module"',
|
||||
attrs,
|
||||
f'Redoc {match.group(1)}.x must be loaded as an ES module: {src}',
|
||||
)
|
||||
|
||||
# The schema which Redoc loads must contain generated API data
|
||||
response = self.client.get(schema_url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
schema = response.json()
|
||||
self.assertTrue(schema['openapi'].startswith('3.'))
|
||||
self.assertEqual(schema['info']['title'], 'InvenTree API')
|
||||
self.assertIn('/api/part/', schema['paths'])
|
||||
self.assertIn('/api/stock/', schema['paths'])
|
||||
self.assertGreater(len(schema['paths']), 100)
|
||||
|
||||
@override_settings(
|
||||
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver']
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Page, expect } from '@playwright/test';
|
||||
import { type Locator, type Page, expect } from '@playwright/test';
|
||||
import { createApi } from './api';
|
||||
|
||||
export const clickOnParamFilter = async (page: Page, name: string) => {
|
||||
@@ -117,6 +117,31 @@ export const navigate = async (
|
||||
await page.goto(path, { waitUntil: options?.waitUntil ?? 'load' });
|
||||
};
|
||||
|
||||
/**
|
||||
* Ctrl+click a link, and return the new tab which it opens (in the same browser context).
|
||||
*
|
||||
* A modified click occasionally fails to open a new tab in CI - so the
|
||||
* click is retried (with a short per-attempt timeout) rather than
|
||||
* waiting for the entire test timeout on a single attempt.
|
||||
*/
|
||||
export const openInNewTab = async (
|
||||
page: Page,
|
||||
link: Locator,
|
||||
url: string | RegExp
|
||||
): Promise<Page> => {
|
||||
const context = page.context();
|
||||
let popup: Page | undefined;
|
||||
|
||||
await expect(async () => {
|
||||
const popupPromise = context.waitForEvent('page', { timeout: 5000 });
|
||||
await link.click({ modifiers: ['ControlOrMeta'], timeout: 5000 });
|
||||
popup = await popupPromise;
|
||||
}).toPass({ timeout: 30000 });
|
||||
|
||||
await popup!.waitForURL(url);
|
||||
return popup!;
|
||||
};
|
||||
|
||||
/**
|
||||
* CLick on the 'tab' element with the provided name
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from './baseFixtures.js';
|
||||
import { adminuser, stevenuser } from './defaults.js';
|
||||
import { globalSearch } from './helpers.js';
|
||||
import { adminuser, apiUrl, stevenuser } from './defaults.js';
|
||||
import { globalSearch, openInNewTab } from './helpers.js';
|
||||
import { doCachedLogin } from './login.js';
|
||||
|
||||
/**
|
||||
@@ -45,10 +45,7 @@ test('General - Search', async ({ browser }) => {
|
||||
.first();
|
||||
await expect(customerResult).toHaveAttribute('href', /\/web\/company\/\d+/);
|
||||
|
||||
const popupPromise = page.context().waitForEvent('page');
|
||||
await customerResult.click({ modifiers: ['Control'] });
|
||||
const popup = await popupPromise;
|
||||
await popup.waitForURL('**/web/company/**');
|
||||
const popup = await openInNewTab(page, customerResult, '**/web/company/**');
|
||||
await popup.close();
|
||||
|
||||
// Click through to the "Customer" results
|
||||
@@ -60,10 +57,9 @@ test('General - Search', async ({ browser }) => {
|
||||
/\/web\/sales\/index\/customers\?search=another\+customer/
|
||||
);
|
||||
|
||||
const overviewPopupPromise = page.context().waitForEvent('page');
|
||||
await viewAllCustomers.click({ modifiers: ['Control'] });
|
||||
const overviewPopup = await overviewPopupPromise;
|
||||
await overviewPopup.waitForURL(
|
||||
const overviewPopup = await openInNewTab(
|
||||
page,
|
||||
viewAllCustomers,
|
||||
'**/web/sales/index/customers?search=another+customer'
|
||||
);
|
||||
await overviewPopup.close();
|
||||
@@ -92,3 +88,36 @@ test('General - Search', async ({ browser }) => {
|
||||
await page.waitForURL('**/purchasing/index/manufacturer-parts**');
|
||||
await page.getByRole('cell', { name: 'RT0402BRD07100KL' }).waitFor();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that the API documentation page actually renders.
|
||||
*
|
||||
* The /api-doc/ view is a thin HTML shell which loads Redoc from a CDN;
|
||||
* a 200 response does not mean that the documentation is displayed.
|
||||
* Ref: https://github.com/inventree/InvenTree/issues/13200
|
||||
*/
|
||||
test('General - API Documentation', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, {
|
||||
user: stevenuser
|
||||
});
|
||||
|
||||
const errors: string[] = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
|
||||
// Note: api-doc page is not forwarded to port 5173
|
||||
const docUrl = new URL('http://localhost:8000/api-doc/', apiUrl).toString();
|
||||
const response = await page.goto(docUrl);
|
||||
await page.waitForLoadState('networkidle');
|
||||
expect(response?.status()).toBe(200);
|
||||
|
||||
// Redoc renders the schema title and the generated API endpoints
|
||||
await page
|
||||
.getByRole('heading', { name: /InvenTree API/ })
|
||||
.first()
|
||||
.waitFor({ timeout: 30000 });
|
||||
|
||||
await page.getByText('Download OpenAPI specification').first().waitFor();
|
||||
await page.getByText('/api/part/', { exact: false }).first().waitFor();
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
Reference in new issue
Block a user