mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-06-20 06:28:55 +00:00
Adding --api-test for CI/CD
This commit is contained in:
parent
d570f8e91f
commit
91bf58b54e
8 changed files with 292 additions and 10 deletions
7
.github/workflows/tests.yml
vendored
7
.github/workflows/tests.yml
vendored
|
|
@ -47,7 +47,10 @@ jobs:
|
|||
run: python -B -m unittest discover -s tests -p "test_*.py"
|
||||
|
||||
- name: Smoke test
|
||||
run: python sqlmap.py --smoke
|
||||
run: python sqlmap.py --smoke-test
|
||||
|
||||
- name: Vuln test
|
||||
run: python sqlmap.py --vuln
|
||||
run: python sqlmap.py --vuln-test
|
||||
|
||||
- name: API test
|
||||
run: python sqlmap.py --api-test
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.
|
|||
5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py
|
||||
914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py
|
||||
885042ed021e60f1739e2a849e3405cc3a4c2a67a5a169a30399d1c53446460f lib/core/optiondict.py
|
||||
3ec59b5eb336d9808d28496f1cbbad716b4a0e276b5399023142826e460e3fd2 lib/core/optiondict.py
|
||||
3ff871fe8391952c3ec3bb528ba592a13926c80ca0b68fd322a317f69a651ef7 lib/core/option.py
|
||||
ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py
|
||||
49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py
|
||||
|
|
@ -188,18 +188,18 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch
|
|||
48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py
|
||||
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
|
||||
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
|
||||
70ddf88d4efda5486853c3616ba64114757a836640585ddae309dd3d335d697a lib/core/settings.py
|
||||
d9180ce5490c781b8f8771b0d5754d27f550aae963ad36731e0d0941a0f8590c lib/core/settings.py
|
||||
cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py
|
||||
bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py
|
||||
70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py
|
||||
8bbc9312147ee8ca719860bc7ad472eac25230e4d46976fbb405efe43fe15ef6 lib/core/testing.py
|
||||
daf2ad65fcea430b6272e3c538022c9871fdc3aba78f71669130fb0bc954c78e lib/core/testing.py
|
||||
e3e653364d08d04d7492aa40a2bd29c6a28f4d78fecdd6c10f21f6cb28b98b4c lib/core/threads.py
|
||||
b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py
|
||||
53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py
|
||||
2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py
|
||||
54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py
|
||||
3f298a58a41225ef67c57b2cf08c71f2eacbab8f98463b4461f45933d6a82f69 lib/parse/cmdline.py
|
||||
053079fe796dfce09cf94ac6f094043f2dfa393b5631387fadb4f735cf1ac6a4 lib/parse/cmdline.py
|
||||
02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py
|
||||
c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py
|
||||
5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py
|
||||
|
|
@ -492,7 +492,7 @@ cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generi
|
|||
46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py
|
||||
f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml
|
||||
627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf
|
||||
d5128ba488b85080a18df85cc08b58f0baeac59494eb5ef43b9e34d66538f091 sqlmap.py
|
||||
f8974aac701639b54ca34b0e11803c836e5cb1e1c5a6eaf275315949b6487310 sqlmap.py
|
||||
eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py
|
||||
a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py
|
||||
cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py
|
||||
|
|
@ -585,6 +585,7 @@ c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_has
|
|||
205e84827461101a78b2cffaa3de49795a1214e92276fc7fd40f3456657062b9 tests/test_identifiers_output.py
|
||||
5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py
|
||||
caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py
|
||||
57fa9713a3186020be8bcc3f06399e92bf9ce82ec6d3413c76babe19606bb698 tests/test_openapi_drift.py
|
||||
cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py
|
||||
4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py
|
||||
6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py
|
||||
|
|
|
|||
|
|
@ -273,6 +273,7 @@ optDict = {
|
|||
"forceDns": "boolean",
|
||||
"murphyRate": "integer",
|
||||
"smokeTest": "boolean",
|
||||
"apiTest": "boolean",
|
||||
},
|
||||
|
||||
"API": {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from lib.core.enums import OS
|
|||
from thirdparty import six
|
||||
|
||||
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
||||
VERSION = "1.10.6.110"
|
||||
VERSION = "1.10.6.111"
|
||||
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
|
||||
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
|
||||
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ See the file 'LICENSE' for copying permission
|
|||
"""
|
||||
|
||||
import doctest
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import socket
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
|
|
@ -20,17 +22,22 @@ import time
|
|||
from extra.vulnserver import vulnserver
|
||||
from lib.core.common import clearConsoleLine
|
||||
from lib.core.common import dataToStdout
|
||||
from lib.core.common import getSafeExString
|
||||
from lib.core.common import randomInt
|
||||
from lib.core.common import randomStr
|
||||
from lib.core.common import shellExec
|
||||
from lib.core.compat import round
|
||||
from lib.core.compat import xrange
|
||||
from lib.core.convert import encodeBase64
|
||||
from lib.core.convert import getBytes
|
||||
from lib.core.convert import getText
|
||||
from lib.core.data import kb
|
||||
from lib.core.data import logger
|
||||
from lib.core.data import paths
|
||||
from lib.core.data import queries
|
||||
from lib.core.patch import unisonRandom
|
||||
from lib.core.settings import IS_WIN
|
||||
from lib.core.settings import RESTAPI_VERSION
|
||||
|
||||
def vulnTest():
|
||||
"""
|
||||
|
|
@ -224,6 +231,156 @@ def vulnTest():
|
|||
|
||||
return retVal
|
||||
|
||||
def apiTest():
|
||||
"""
|
||||
Runs a basic live test of the REST API: launches the server in a separate process
|
||||
('sqlmapapi.py -s') and drives the control-plane endpoints with an HTTP client - a real
|
||||
server + client round-trip, without launching an actual scan. A separate process (rather
|
||||
than an in-process thread) isolates the single-threaded server from the client's GIL and
|
||||
from sqlmap's global HTTP machinery, which otherwise makes the round-trip flaky.
|
||||
"""
|
||||
|
||||
retVal = True
|
||||
|
||||
# pick a free port the same way vulnTest() does
|
||||
while True:
|
||||
address, port = "127.0.0.1", random.randint(10000, 65535)
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
if s.connect_ex((address, port)):
|
||||
break
|
||||
else:
|
||||
time.sleep(1)
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
username, password = "test", "test"
|
||||
apipath = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "sqlmapapi.py"))
|
||||
|
||||
try:
|
||||
devnull = subprocess.DEVNULL
|
||||
except AttributeError:
|
||||
devnull = open(os.devnull, "wb")
|
||||
|
||||
process = subprocess.Popen([sys.executable, apipath, "-s", "-H", address, "-p", str(port), "--username", username, "--password", password], stdout=devnull, stderr=devnull)
|
||||
|
||||
base = "http://%s:%d" % (address, port)
|
||||
|
||||
def _call(path, data=None, authorize=True):
|
||||
# NOTE: a raw socket is used deliberately instead of urllib/http.client. The host sqlmap
|
||||
# process installs a global keep-alive opener and patches http.client, which makes a
|
||||
# library client flaky against the single-threaded server; a hand-rolled HTTP/1.0 request
|
||||
# (Connection: close, read to EOF) is hermetic and immune to all of that.
|
||||
method = "POST" if data is not None else "GET"
|
||||
lines = ["%s %s HTTP/1.0" % (method, path), "Host: %s:%d" % (address, port)]
|
||||
if authorize:
|
||||
lines.append("Authorization: Basic %s" % encodeBase64("%s:%s" % (username, password), binary=False))
|
||||
body = getBytes(json.dumps(data)) if data is not None else b""
|
||||
if data is not None:
|
||||
lines.append("Content-Type: application/json")
|
||||
lines.append("Content-Length: %d" % len(body))
|
||||
lines.append("Connection: close")
|
||||
request = getBytes("\r\n".join(lines) + "\r\n\r\n") + body
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(10)
|
||||
try:
|
||||
s.connect((address, port))
|
||||
s.sendall(request)
|
||||
raw = b""
|
||||
while True:
|
||||
chunk = s.recv(8192)
|
||||
if not chunk:
|
||||
break
|
||||
raw += chunk
|
||||
except Exception as ex:
|
||||
logger.debug("API test: request to '%s' failed (%s)" % (path, getSafeExString(ex)))
|
||||
return None, None
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
head, _, payload = raw.partition(b"\r\n\r\n")
|
||||
try:
|
||||
code = int(head.split(b"\r\n")[0].split(b" ")[1])
|
||||
except (IndexError, ValueError):
|
||||
return None, None
|
||||
try:
|
||||
return code, json.loads(getText(payload))
|
||||
except ValueError:
|
||||
return code, None
|
||||
|
||||
try:
|
||||
# wait for the server process to come up (or die trying)
|
||||
for _ in xrange(200):
|
||||
if process.poll() is not None:
|
||||
logger.error("API test: server process exited prematurely (address: '%s')" % base)
|
||||
return False
|
||||
code, data = _call("/version")
|
||||
if code == 200 and data and data.get("success"):
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
logger.error("API test: server did not come up (address: '%s')" % base)
|
||||
return False
|
||||
|
||||
logger.info("REST API server running at '%s'..." % base)
|
||||
|
||||
results = []
|
||||
|
||||
def _check(name, condition):
|
||||
results.append((name, bool(condition)))
|
||||
if not condition:
|
||||
logger.error("API test: check '%s' FAILED" % name)
|
||||
|
||||
# GET /version - success envelope + MAJOR-only integer api_version
|
||||
code, data = _call("/version")
|
||||
_check("version", code == 200 and data and data.get("success") is True and data.get("api_version") == int(RESTAPI_VERSION.split(".")[0]) and data.get("version"))
|
||||
|
||||
# the auth hook must reject an unauthenticated request
|
||||
code, _ = _call("/version", authorize=False)
|
||||
_check("auth-401", code == 401)
|
||||
|
||||
# GET /task/new - mint a task
|
||||
code, data = _call("/task/new")
|
||||
taskid = data.get("taskid") if data else None
|
||||
_check("task-new", code == 200 and data and data.get("success") and taskid)
|
||||
|
||||
# POST /option/<taskid>/set then read it back via /get and /list (JSON round-trip + IPC)
|
||||
code, data = _call("/option/%s/set" % taskid, {"flushSession": True})
|
||||
_check("option-set", code == 200 and data and data.get("success"))
|
||||
|
||||
code, data = _call("/option/%s/get" % taskid, ["flushSession"])
|
||||
_check("option-get", data and data.get("success") and (data.get("options") or {}).get("flushSession") is True)
|
||||
|
||||
code, data = _call("/option/%s/list" % taskid)
|
||||
_check("option-list", data and data.get("success") and isinstance(data.get("options"), dict))
|
||||
|
||||
# GET /admin/list - the IP-bound listing (our client is the task's creator) must see it
|
||||
code, data = _call("/admin/list")
|
||||
_check("admin-list", data and data.get("success") and taskid in (data.get("tasks") or {}))
|
||||
|
||||
# a bogus task ID must produce a failure envelope (not a crash)
|
||||
code, data = _call("/option/%s/list" % "nonexistent")
|
||||
_check("invalid-task", data is not None and data.get("success") is False)
|
||||
|
||||
# GET /task/<taskid>/delete - tear the task down
|
||||
code, data = _call("/task/%s/delete" % taskid)
|
||||
_check("task-delete", data and data.get("success"))
|
||||
|
||||
if all(ok for _, ok in results):
|
||||
logger.info("API test final result: PASSED")
|
||||
else:
|
||||
retVal = False
|
||||
logger.error("API test final result: FAILED (%s)" % ", ".join(name for name, ok in results if not ok))
|
||||
finally:
|
||||
try:
|
||||
process.terminate()
|
||||
process.wait()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return retVal
|
||||
|
||||
def smokeTest():
|
||||
"""
|
||||
Runs the basic smoke testing of a program
|
||||
|
|
|
|||
|
|
@ -875,6 +875,9 @@ def cmdLineParser(argv=None):
|
|||
parser.add_argument("--vuln-test", dest="vulnTest", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
parser.add_argument("--api-test", dest="apiTest", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
parser.add_argument("--disable-json", dest="disableJson", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
|
|
@ -1129,7 +1132,7 @@ def cmdLineParser(argv=None):
|
|||
else:
|
||||
args.stdinPipe = None
|
||||
|
||||
if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.updateAll, args.smokeTest, args.vulnTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)):
|
||||
if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.updateAll, args.smokeTest, args.vulnTest, args.apiTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)):
|
||||
errMsg = "missing a mandatory option (-d, -u, -l, -m, -r, -g, -c, --wizard, --shell, --update, --purge, --list-tampers or --dependencies). "
|
||||
errMsg += "Use -h for basic and -hh for advanced help\n"
|
||||
parser.error(errMsg)
|
||||
|
|
|
|||
|
|
@ -188,6 +188,9 @@ def main():
|
|||
elif conf.vulnTest:
|
||||
from lib.core.testing import vulnTest
|
||||
os._exitcode = 1 - (vulnTest() or 0)
|
||||
elif conf.apiTest:
|
||||
from lib.core.testing import apiTest
|
||||
os._exitcode = 1 - (apiTest() or 0)
|
||||
else:
|
||||
from lib.controller.controller import start
|
||||
if conf.profile:
|
||||
|
|
@ -600,7 +603,7 @@ def main():
|
|||
except OSError:
|
||||
pass
|
||||
|
||||
if any((conf.vulnTest, conf.smokeTest)) or not filterNone(filepath for filepath in glob.glob(os.path.join(tempDir, '*')) if not any(filepath.endswith(_) for _ in (".lock", ".exe", ".so", '_'))): # ignore junk files
|
||||
if any((conf.vulnTest, conf.smokeTest, conf.apiTest)) or not filterNone(filepath for filepath in glob.glob(os.path.join(tempDir, '*')) if not any(filepath.endswith(_) for _ in (".lock", ".exe", ".so", '_'))): # ignore junk files
|
||||
try:
|
||||
shutil.rmtree(tempDir, ignore_errors=True)
|
||||
except OSError:
|
||||
|
|
|
|||
114
tests/test_openapi_drift.py
Normal file
114
tests/test_openapi_drift.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Contract test: the OpenAPI spec (sqlmapapi.yaml) must stay in lock-step with the
|
||||
REST API actually served by lib/utils/api.py. The spec is hand-maintained, so it
|
||||
is the exact thing that silently drifts when an endpoint is added/renamed/retyped.
|
||||
|
||||
This walks the live Bottle route table (every @get/@post registers at import time)
|
||||
and the spec's `paths:` block, and asserts the (method, path) sets are identical
|
||||
in BOTH directions - no undocumented route, no phantom spec entry - plus that the
|
||||
spec's advertised version matches the runtime RESTAPI_VERSION.
|
||||
|
||||
PyYAML is not bundled (and the suite is stdlib-only / no pip), so the spec is read
|
||||
with a tiny indentation-aware scanner that only needs the paths + info.version.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap
|
||||
bootstrap()
|
||||
|
||||
import lib.utils.api # noqa: F401 (importing registers every route on Bottle's default app)
|
||||
from lib.core.settings import RESTAPI_VERSION
|
||||
from thirdparty.bottle.bottle import default_app
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SPEC = os.path.join(ROOT, "sqlmapapi.yaml")
|
||||
|
||||
# Bottle-only routes that are not part of the documented public contract
|
||||
INTERNAL_RULES = ("/error/401",)
|
||||
|
||||
HTTP_METHODS = ("get", "post", "put", "delete", "patch", "head", "options")
|
||||
|
||||
|
||||
def _normalize_rule(rule):
|
||||
# Bottle '<taskid>' / '<filename:path>' -> OpenAPI '{taskid}' / '{filename}'
|
||||
return re.sub(r"<([^:>]+)(?::[^>]+)?>", r"{\1}", rule)
|
||||
|
||||
|
||||
def _app_pairs():
|
||||
pairs = set()
|
||||
for route in default_app().routes:
|
||||
rule = _normalize_rule(route.rule)
|
||||
if rule in INTERNAL_RULES:
|
||||
continue
|
||||
pairs.add((route.method.lower(), rule))
|
||||
return pairs
|
||||
|
||||
|
||||
def _spec_paths_and_version(text):
|
||||
"""Returns (set of (method, path), info.version) from the YAML text."""
|
||||
pairs = set()
|
||||
version = None
|
||||
section = None
|
||||
current_path = None
|
||||
|
||||
for line in text.splitlines():
|
||||
if not line.strip() or line.lstrip().startswith("#"):
|
||||
continue
|
||||
|
||||
top = re.match(r"^(\S[^:]*):", line) # a column-0 key starts a new top-level section
|
||||
if top:
|
||||
section = top.group(1)
|
||||
current_path = None
|
||||
continue
|
||||
|
||||
if section == "info":
|
||||
m = re.match(r"^ version:\s*(.+?)\s*$", line)
|
||||
if m:
|
||||
version = m.group(1).strip().strip('"').strip("'")
|
||||
elif section == "paths":
|
||||
m = re.match(r"^ (/\S*):\s*$", line) # 2-space path key
|
||||
if m:
|
||||
current_path = m.group(1)
|
||||
continue
|
||||
m = re.match(r"^ (\w+):\s*$", line) # 4-space method key
|
||||
if m and current_path and m.group(1).lower() in HTTP_METHODS:
|
||||
pairs.add((m.group(1).lower(), current_path))
|
||||
|
||||
return pairs, version
|
||||
|
||||
|
||||
class TestOpenAPIDrift(unittest.TestCase):
|
||||
def setUp(self):
|
||||
with open(SPEC) as f:
|
||||
self.spec_pairs, self.spec_version = _spec_paths_and_version(f.read())
|
||||
self.app_pairs = _app_pairs()
|
||||
|
||||
def test_parsers_found_something(self):
|
||||
# guard against a silently-empty parse making the equality checks vacuously pass
|
||||
self.assertTrue(len(self.app_pairs) >= 15, self.app_pairs)
|
||||
self.assertEqual(len(self.spec_pairs), len(self.app_pairs))
|
||||
|
||||
def test_no_undocumented_endpoint(self):
|
||||
missing = self.app_pairs - self.spec_pairs
|
||||
self.assertEqual(missing, set(), "served but absent from sqlmapapi.yaml: %s" % sorted(missing))
|
||||
|
||||
def test_no_phantom_spec_entry(self):
|
||||
extra = self.spec_pairs - self.app_pairs
|
||||
self.assertEqual(extra, set(), "in sqlmapapi.yaml but not served: %s" % sorted(extra))
|
||||
|
||||
def test_version_matches_runtime(self):
|
||||
self.assertEqual(self.spec_version, RESTAPI_VERSION, "sqlmapapi.yaml version '%s' != RESTAPI_VERSION '%s'" % (self.spec_version, RESTAPI_VERSION))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue