mirror of
https://github.com/zulip/zulip.git
synced 2025-11-14 02:48:00 +00:00
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:
@@ -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]+)" \
|
||||||
|
|||||||
@@ -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"]'
|
||||||
|
|||||||
@@ -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" \
|
||||||
|
|||||||
@@ -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 \
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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" \
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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 \
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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 \
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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" \
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"]'
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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"]' \
|
||||||
|
|||||||
@@ -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**"
|
||||||
|
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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" \
|
||||||
|
|||||||
@@ -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]" \
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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, \
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user