api_docs: Detect missing arguments in curl examples.

This commit adds automated tests that make sure that every curl
example command in our API docs has the '-X (POST|GET)' argument.

Fixes: #11927
This commit is contained in:
Eeshan Garg
2019-05-16 18:08:53 -02:30
committed by Tim Abbott
parent 8339c21637
commit cecea75457
38 changed files with 175 additions and 63 deletions

View File

@@ -15,7 +15,7 @@ appear in messages and topics.
{tab|curl} {tab|curl}
``` ``` curl
curl -X POST {{ api_url }}/v1/realm/filters \ curl -X POST {{ api_url }}/v1/realm/filters \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d "pattern=#(?P<id>[0-9]+)" \ -d "pattern=#(?P<id>[0-9]+)" \

View File

@@ -52,8 +52,8 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl {{ api_url }}/v1/users/me/subscriptions \ curl -X POST {{ api_url }}/v1/users/me/subscriptions \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d 'subscriptions=[{"name": "Verona"}]' -d 'subscriptions=[{"name": "Verona"}]'
``` ```
@@ -61,8 +61,8 @@ curl {{ api_url }}/v1/users/me/subscriptions \
To subscribe another user to a stream, you may pass in To subscribe another user to a stream, you may pass in
the `principals` argument, like so: the `principals` argument, like so:
``` ``` curl
curl {{ api_url }}/v1/users/me/subscriptions \ curl -X POST {{ api_url }}/v1/users/me/subscriptions \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d 'subscriptions=[{"name": "Verona"}]' \ -d 'subscriptions=[{"name": "Verona"}]' \
-d 'principals=["ZOE@zulip.com"]' -d 'principals=["ZOE@zulip.com"]'

View File

@@ -38,8 +38,8 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl {{ api_url }}/v1/users \ curl -X POST {{ api_url }}/v1/users \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d "email=newbie@zulip.com" \ -d "email=newbie@zulip.com" \
-d "full_name=New User" \ -d "full_name=New User" \

View File

@@ -19,7 +19,7 @@ the Zulip Help Center.
{tab|curl} {tab|curl}
``` ``` curl
curl -X DELETE {{ api_url }}/v1/messages/{message_id} \ curl -X DELETE {{ api_url }}/v1/messages/{message_id} \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
``` ```

View File

@@ -41,7 +41,7 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl -X "DELETE" {{ api_url }}/v1/events \ curl -X "DELETE" {{ api_url }}/v1/events \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
-d 'queue_id=1515096410:1' -d 'queue_id=1515096410:1'

View File

@@ -17,8 +17,8 @@ in a Zulip production server.
{start_tabs} {start_tabs}
{tab|curl} {tab|curl}
``` ``` curl
curl {{ api_url }}/v1/dev_fetch_api_key \ curl -X POST {{ api_url }}/v1/dev_fetch_api_key \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d "username=iago@zulip.com" -d "username=iago@zulip.com"
``` ```

View File

@@ -31,14 +31,14 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl -X GET {{ api_url }}/v1/streams -u BOT_EMAIL_ADDRESS:BOT_API_KEY curl -X GET {{ api_url }}/v1/streams -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```
You may pass in one or more of the parameters mentioned above You may pass in one or more of the parameters mentioned above
as URL query parameters, like so: as URL query parameters, like so:
``` ``` curl
curl -X GET {{ api_url }}/v1/streams?include_public=false \ curl -X GET {{ api_url }}/v1/streams?include_public=false \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```

View File

@@ -33,13 +33,13 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl -X GET {{ api_url }}/v1/users -u BOT_EMAIL_ADDRESS:BOT_API_KEY curl -X GET {{ api_url }}/v1/users -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```
You may pass the `client_gravatar` query parameter as follows: You may pass the `client_gravatar` query parameter as follows:
``` ``` curl
curl -X GET {{ api_url }}/v1/users?client_gravatar=true \ curl -X GET {{ api_url }}/v1/users?client_gravatar=true \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```

View File

@@ -62,8 +62,8 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl -G {{ api_url }}/v1/events \ curl -X GET {{ api_url }}/v1/events \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
-d "queue_id=1375801870:2942" \ -d "queue_id=1375801870:2942" \
-d "last_event_id=-1" -d "last_event_id=-1"

View File

@@ -18,7 +18,7 @@ Note that edit history may be disabled in some organizations; see the
{tab|curl} {tab|curl}
``` ``` curl
curl -X GET {{ api_url }}/v1/messages/<message_id>/history \ curl -X GET {{ api_url }}/v1/messages/<message_id>/history \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```

View File

@@ -63,7 +63,7 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl -X GET {{ api_url }}/v1/messages \ curl -X GET {{ api_url }}/v1/messages \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d "anchor=42" \ -d "anchor=42" \

View File

@@ -29,7 +29,7 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl -X GET {{ api_url }}/v1/realm/emoji \ curl -X GET {{ api_url }}/v1/realm/emoji \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```

View File

@@ -23,7 +23,7 @@ for details on the data model for presence in Zulip.
{tab|curl} {tab|curl}
``` ``` curl
curl -X GET {{ api_url }}/v1/users/<email>/presence \ curl -X GET {{ api_url }}/v1/users/<email>/presence \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```

View File

@@ -31,7 +31,7 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl -X GET {{ api_url }}/v1/users/me \ curl -X GET {{ api_url }}/v1/users/me \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```

View File

@@ -18,7 +18,7 @@ UI).
{tab|curl} {tab|curl}
``` ``` curl
curl -X GET {{ api_url }}/v1/messages/<msg_id> \ curl -X GET {{ api_url }}/v1/messages/<msg_id> \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
``` ```

View File

@@ -30,8 +30,8 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl X GET {{ api_url }}/v1/get_stream_id?stream=Denmark \ curl -X GET {{ api_url }}/v1/get_stream_id?stream=Denmark \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```

View File

@@ -31,7 +31,7 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl -X GET {{ api_url }}/v1/users/me/<stream_id>/topics \ curl -X GET {{ api_url }}/v1/users/me/<stream_id>/topics \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```

View File

@@ -32,7 +32,7 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl -X GET {{ api_url }}/v1/users/me/subscriptions \ curl -X GET {{ api_url }}/v1/users/me/subscriptions \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```

View File

@@ -15,7 +15,7 @@ Fetches all of the user groups in the organization.
<div data-language="curl" markdown="1"> <div data-language="curl" markdown="1">
``` ``` curl
curl -X GET {{ api_url }}/v1/user_groups \ curl -X GET {{ api_url }}/v1/user_groups \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```

View File

@@ -16,8 +16,8 @@ in messages and topics.
{tab|curl} {tab|curl}
``` ``` curl
curl {{ api_url }}/v1/realm/filters \ curl -X GET {{ api_url }}/v1/realm/filters \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
``` ```

View File

@@ -13,7 +13,7 @@ Marks all of the current user's unread messages as read.
{tab|curl} {tab|curl}
``` ``` curl
curl -X POST {{ api_url }}/v1/mark_all_as_read \ curl -X POST {{ api_url }}/v1/mark_all_as_read \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```
@@ -48,7 +48,7 @@ Mark all the unread messages in a stream as read.
{tab|curl} {tab|curl}
``` ``` curl
curl -X POST {{ api_url }}/v1/mark_stream_as_read \ curl -X POST {{ api_url }}/v1/mark_stream_as_read \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d "stream_id=42" -d "stream_id=42"
@@ -84,7 +84,7 @@ Mark all the unread messages in a topic as read.
{tab|curl} {tab|curl}
``` ``` curl
curl -X POST {{ api_url }}/v1/mark_topic_as_read \ curl -X POST {{ api_url }}/v1/mark_topic_as_read \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d "stream_id=42" \ -d "stream_id=42" \

View File

@@ -15,7 +15,7 @@ UI, and are not included in the user's unread count totals.
{tab|curl} {tab|curl}
``` ``` curl
curl -X PATCH {{ api_url }}/v1/users/me/subscriptions/muted_topics \ curl -X PATCH {{ api_url }}/v1/users/me/subscriptions/muted_topics \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d "stream=Verona" -d "stream=Verona"

View File

@@ -75,8 +75,8 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl {{ api_url }}/v1/register \ curl -X POST {{ api_url }}/v1/register \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
-d 'event_types=["message"]' -d 'event_types=["message"]'
``` ```

View File

@@ -15,7 +15,7 @@ in messages and topics.
{tab|curl} {tab|curl}
``` ``` curl
curl -X DELETE {{ api_url }}/v1/realm/filters/<filter_id> \ curl -X DELETE {{ api_url }}/v1/realm/filters/<filter_id> \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```

View File

@@ -40,7 +40,7 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl -X "DELETE" {{ api_url }}/v1/users/me/subscriptions \ curl -X "DELETE" {{ api_url }}/v1/users/me/subscriptions \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d 'subscriptions=["Denmark"]' -d 'subscriptions=["Denmark"]'
@@ -48,7 +48,7 @@ curl -X "DELETE" {{ api_url }}/v1/users/me/subscriptions \
You may specify the `principals` argument like so: You may specify the `principals` argument like so:
``` ``` curl
curl -X "DELETE" {{ api_url }}/v1/users/me/subscriptions \ curl -X "DELETE" {{ api_url }}/v1/users/me/subscriptions \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d 'subscriptions=["Denmark"]' \ -d 'subscriptions=["Denmark"]' \

View File

@@ -34,8 +34,8 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl {{ api_url }}/v1/messages/render \ curl -X POST {{ api_url }}/v1/messages/render \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d $"content=**foo**" -d $"content=**foo**"

View File

@@ -51,7 +51,7 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
# For stream messages # For stream messages
curl -X POST {{ api_url }}/v1/messages \ curl -X POST {{ api_url }}/v1/messages \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \

View File

@@ -21,8 +21,8 @@ Fetch global settings for a Zulip server.
{tab|curl} {tab|curl}
``` ``` curl
curl {{ api_url }}/v1/server_settings \ curl -X GET {{ api_url }}/v1/server_settings \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY -u BOT_EMAIL_ADDRESS:BOT_API_KEY
``` ```

View File

@@ -38,7 +38,7 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl -X POST {{ api_url }}/v1/typing \ curl -X POST {{ api_url }}/v1/typing \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d "op=start" \ -d "op=start" \

View File

@@ -44,7 +44,7 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl -X POST {{ api_url }}/v1/messages/flags \ curl -X POST {{ api_url }}/v1/messages/flags \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d "messages=[4,8,15]" \ -d "messages=[4,8,15]" \

View File

@@ -38,7 +38,7 @@ zulip(config).then((client) => {
{tab|curl} {tab|curl}
``` ``` curl
curl -X "PATCH" {{ api_url }}/v1/messages/<msg_id> \ curl -X "PATCH" {{ api_url }}/v1/messages/<msg_id> \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d $"content=New content" -d $"content=New content"

View File

@@ -15,7 +15,7 @@ per-stream notification settings.
{tab|curl} {tab|curl}
``` ``` curl
curl -X POST {{ api_url }}/v1/users/me/subscriptions/properties \ curl -X POST {{ api_url }}/v1/users/me/subscriptions/properties \
-u BOT_EMAIL_ADDRESS:BOT_API_KEY \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
-d 'subscription_data=[{"stream_id": 1, \ -d 'subscription_data=[{"stream_id": 1, \

View File

@@ -16,8 +16,8 @@ organization. Access to this endpoint depends on the
{tab|curl} {tab|curl}
``` ``` curl
curl {{ api_url }}/v1/realm/emoji/<emoji_name> \ curl -X POST {{ api_url }}/v1/realm/emoji/<emoji_name> \
-F "data=@/path/to/img.png" \ -F "data=@/path/to/img.png" \
-u USER_EMAIL:API_KEY -u USER_EMAIL:API_KEY
``` ```

View File

@@ -80,8 +80,9 @@ import re
import markdown import markdown
from django.utils.html import escape from django.utils.html import escape
from markdown.extensions.codehilite import CodeHilite, CodeHiliteExtension from markdown.extensions.codehilite import CodeHilite, CodeHiliteExtension
from zerver.lib.exceptions import BugdownRenderingException
from zerver.lib.tex import render_tex from zerver.lib.tex import render_tex
from typing import Any, Dict, Iterable, List, MutableSequence from typing import Any, Dict, Iterable, List, MutableSequence, Optional
# Global vars # Global vars
FENCE_RE = re.compile(""" FENCE_RE = re.compile("""
@@ -107,12 +108,44 @@ FENCE_RE = re.compile("""
CODE_WRAP = '<pre><code%s>%s\n</code></pre>' CODE_WRAP = '<pre><code%s>%s\n</code></pre>'
LANG_TAG = ' class="%s"' LANG_TAG = ' class="%s"'
def validate_curl_content(lines: List[str]) -> None:
error_msg = """
Missing required -X argument in curl command:
{command}
""".strip()
for line in lines:
regex = r'curl [-]X "?(GET|DELETE|PATCH|POST)"?'
if line.startswith('curl'):
if re.search(regex, line) is None:
raise BugdownRenderingException(error_msg.format(command=line.strip()))
CODE_VALIDATORS = {
'curl': validate_curl_content,
}
class FencedCodeExtension(markdown.Extension): class FencedCodeExtension(markdown.Extension):
def __init__(self, config: Optional[Dict[str, Any]]=None) -> None:
if config is None:
config = {}
self.config = {
'run_content_validators': [
config.get('run_content_validators', False),
'Boolean specifying whether to run content validation code in CodeHandler'
]
}
for key, value in config.items():
self.setConfig(key, value)
def extendMarkdown(self, md: markdown.Markdown, md_globals: Dict[str, Any]) -> None: def extendMarkdown(self, md: markdown.Markdown, md_globals: Dict[str, Any]) -> None:
""" Add FencedBlockPreprocessor to the Markdown instance. """ """ Add FencedBlockPreprocessor to the Markdown instance. """
md.registerExtension(self) md.registerExtension(self)
md.preprocessors.register(FencedBlockPreprocessor(md), 'fenced_code_block', 25) processor = FencedBlockPreprocessor(
md, run_content_validators=self.config['run_content_validators'][0])
md.preprocessors.register(processor, 'fenced_code_block', 25)
class BaseHandler: class BaseHandler:
@@ -122,42 +155,51 @@ class BaseHandler:
def done(self) -> None: def done(self) -> None:
raise NotImplementedError() raise NotImplementedError()
def generic_handler(processor: Any, output: MutableSequence[str], fence: str, lang: str) -> BaseHandler: def generic_handler(processor: Any, output: MutableSequence[str],
fence: str, lang: str,
run_content_validators: Optional[bool]=False) -> BaseHandler:
if lang in ('quote', 'quoted'): if lang in ('quote', 'quoted'):
return QuoteHandler(processor, output, fence) return QuoteHandler(processor, output, fence)
elif lang in ('math', 'tex', 'latex'): elif lang in ('math', 'tex', 'latex'):
return TexHandler(processor, output, fence) return TexHandler(processor, output, fence)
else: else:
return CodeHandler(processor, output, fence, lang) return CodeHandler(processor, output, fence, lang, run_content_validators)
def check_for_new_fence(processor: Any, output: MutableSequence[str], line: str) -> None: def check_for_new_fence(processor: Any, output: MutableSequence[str], line: str,
run_content_validators: Optional[bool]=False) -> None:
m = FENCE_RE.match(line) m = FENCE_RE.match(line)
if m: if m:
fence = m.group('fence') fence = m.group('fence')
lang = m.group('lang') lang = m.group('lang')
handler = generic_handler(processor, output, fence, lang)
handler = generic_handler(processor, output, fence, lang, run_content_validators)
processor.push(handler) processor.push(handler)
else: else:
output.append(line) output.append(line)
class OuterHandler(BaseHandler): class OuterHandler(BaseHandler):
def __init__(self, processor: Any, output: MutableSequence[str]) -> None: def __init__(self, processor: Any, output: MutableSequence[str],
run_content_validators: Optional[bool]=False) -> None:
self.output = output self.output = output
self.processor = processor self.processor = processor
self.run_content_validators = run_content_validators
def handle_line(self, line: str) -> None: def handle_line(self, line: str) -> None:
check_for_new_fence(self.processor, self.output, line) check_for_new_fence(self.processor, self.output, line,
self.run_content_validators)
def done(self) -> None: def done(self) -> None:
self.processor.pop() self.processor.pop()
class CodeHandler(BaseHandler): class CodeHandler(BaseHandler):
def __init__(self, processor: Any, output: MutableSequence[str], fence: str, lang: str) -> None: def __init__(self, processor: Any, output: MutableSequence[str],
fence: str, lang: str, run_content_validators: Optional[bool]=False) -> None:
self.processor = processor self.processor = processor
self.output = output self.output = output
self.fence = fence self.fence = fence
self.lang = lang self.lang = lang
self.lines = [] # type: List[str] self.lines = [] # type: List[str]
self.run_content_validators = run_content_validators
def handle_line(self, line: str) -> None: def handle_line(self, line: str) -> None:
if line.rstrip() == self.fence: if line.rstrip() == self.fence:
@@ -167,6 +209,12 @@ class CodeHandler(BaseHandler):
def done(self) -> None: def done(self) -> None:
text = '\n'.join(self.lines) text = '\n'.join(self.lines)
# run content validators (if any)
if self.run_content_validators:
validator = CODE_VALIDATORS.get(self.lang, lambda text: None)
validator(self.lines)
text = self.processor.format_code(self.lang, text) text = self.processor.format_code(self.lang, text)
text = self.processor.placeholder(text) text = self.processor.placeholder(text)
processed_lines = text.split('\n') processed_lines = text.split('\n')
@@ -222,10 +270,11 @@ class TexHandler(BaseHandler):
class FencedBlockPreprocessor(markdown.preprocessors.Preprocessor): class FencedBlockPreprocessor(markdown.preprocessors.Preprocessor):
def __init__(self, md: markdown.Markdown) -> None: def __init__(self, md: markdown.Markdown, run_content_validators: Optional[bool]=False) -> None:
markdown.preprocessors.Preprocessor.__init__(self, md) markdown.preprocessors.Preprocessor.__init__(self, md)
self.checked_for_codehilite = False self.checked_for_codehilite = False
self.run_content_validators = run_content_validators
self.codehilite_conf = {} # type: Dict[str, List[Any]] self.codehilite_conf = {} # type: Dict[str, List[Any]]
def push(self, handler: BaseHandler) -> None: def push(self, handler: BaseHandler) -> None:
@@ -242,7 +291,7 @@ class FencedBlockPreprocessor(markdown.preprocessors.Preprocessor):
processor = self processor = self
self.handlers = [] # type: List[BaseHandler] self.handlers = [] # type: List[BaseHandler]
handler = OuterHandler(processor, output) handler = OuterHandler(processor, output, self.run_content_validators)
self.push(handler) self.push(handler)
for line in lines: for line in lines:
@@ -324,7 +373,7 @@ class FencedBlockPreprocessor(markdown.preprocessors.Preprocessor):
def makeExtension(*args: Any, **kwargs: None) -> FencedCodeExtension: def makeExtension(*args: Any, **kwargs: None) -> FencedCodeExtension:
return FencedCodeExtension(*args, **kwargs) return FencedCodeExtension(kwargs)
if __name__ == "__main__": if __name__ == "__main__":
import doctest import doctest

View File

@@ -103,7 +103,9 @@ def render_markdown_path(markdown_file_path: str,
linenums=False, linenums=False,
guess_lang=False guess_lang=False
), ),
zerver.lib.bugdown.fenced_code.makeExtension(), zerver.lib.bugdown.fenced_code.makeExtension(
run_content_validators=context.get('run_content_validators', False)
),
zerver.lib.bugdown.api_arguments_table_generator.makeExtension( zerver.lib.bugdown.api_arguments_table_generator.makeExtension(
base_path='templates/zerver/api/'), base_path='templates/zerver/api/'),
zerver.lib.bugdown.api_code_examples.makeExtension(), zerver.lib.bugdown.api_code_examples.makeExtension(),

View File

@@ -1709,6 +1709,50 @@ class BugdownErrorTests(ZulipTestCase):
with self.assertRaises(BugdownRenderingException): with self.assertRaises(BugdownRenderingException):
bugdown_convert(msg) bugdown_convert(msg)
def test_curl_code_block_validation(self) -> None:
processor = bugdown.fenced_code.FencedBlockPreprocessor(None)
processor.run_content_validators = True
# Simulate code formatting.
processor.format_code = lambda lang, code: lang + ':' + code # type: ignore # mypy doesn't allow monkey-patching functions
processor.placeholder = lambda s: '**' + s.strip('\n') + '**' # type: ignore # https://github.com/python/mypy/issues/708
markdown = [
'``` curl',
'curl {{ api_url }}/v1/register',
' -u BOT_EMAIL_ADDRESS:BOT_API_KEY',
' -d "queue_id=1375801870:2942"',
'```',
]
with self.assertRaises(BugdownRenderingException):
processor.run(markdown)
def test_curl_code_block_without_validation(self) -> None:
processor = bugdown.fenced_code.FencedBlockPreprocessor(None)
# Simulate code formatting.
processor.format_code = lambda lang, code: lang + ':' + code # type: ignore # mypy doesn't allow monkey-patching functions
processor.placeholder = lambda s: '**' + s.strip('\n') + '**' # type: ignore # https://github.com/python/mypy/issues/708
markdown = [
'``` curl',
'curl {{ api_url }}/v1/register',
' -u BOT_EMAIL_ADDRESS:BOT_API_KEY',
' -d "queue_id=1375801870:2942"',
'```',
]
expected = [
'',
'**curl:curl {{ api_url }}/v1/register',
' -u BOT_EMAIL_ADDRESS:BOT_API_KEY',
' -d "queue_id=1375801870:2942"**',
'',
''
]
result = processor.run(markdown)
self.assertEqual(result, expected)
class BugdownAvatarTestCase(ZulipTestCase): class BugdownAvatarTestCase(ZulipTestCase):
def test_possible_avatar_emails(self) -> None: def test_possible_avatar_emails(self) -> None:

View File

@@ -89,6 +89,22 @@ class DocPageTest(ZulipTestCase):
if not doc_html_str: if not doc_html_str:
self.assert_in_success_response(['<meta name="robots" content="noindex,nofollow">'], result) self.assert_in_success_response(['<meta name="robots" content="noindex,nofollow">'], result)
@slow("Tests dozens of endpoints")
def test_api_doc_endpoints(self) -> None:
current_dir = os.path.dirname(os.path.abspath(__file__))
api_docs_dir = os.path.join(current_dir, '..', '..', 'templates/zerver/api/')
files = os.listdir(api_docs_dir)
def _filter_func(fp: str) -> bool:
ignored_files = ['sidebar_index.md', 'index.md', 'missing.md']
return fp.endswith('.md') and fp not in ignored_files
files = list(filter(_filter_func, files))
for f in files:
endpoint = '/api/{}'.format(os.path.splitext(f)[0])
self._test(endpoint, '', doc_html_str=True)
@slow("Tests dozens of endpoints, including generating lots of emails") @slow("Tests dozens of endpoints, including generating lots of emails")
def test_doc_endpoints(self) -> None: def test_doc_endpoints(self) -> None:
self._test('/api/', 'The Zulip API') self._test('/api/', 'The Zulip API')

View File

@@ -124,6 +124,7 @@ class MarkdownDirectoryView(ApiURLView):
# An "article" might require the api_uri_context to be rendered # An "article" might require the api_uri_context to be rendered
api_uri_context = {} # type: Dict[str, Any] api_uri_context = {} # type: Dict[str, Any]
add_api_uri_context(api_uri_context, self.request) add_api_uri_context(api_uri_context, self.request)
api_uri_context["run_content_validators"] = True
context["api_uri_context"] = api_uri_context context["api_uri_context"] = api_uri_context
return context return context