mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-07-10 18:43:07 +00:00
Update of unit tests
This commit is contained in:
parent
b933ff3f63
commit
cb20a446ae
48 changed files with 15116 additions and 190 deletions
410
tests/test_dump_format.py
Normal file
410
tests/test_dump_format.py
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Output formatting of the result dumper (lib/core/dump.py) and the SQLite
|
||||
replication backend (lib/core/replication.py).
|
||||
|
||||
dump.Dump turns extracted DB structures (schemas, table/column listings, row
|
||||
counts, single facts, user lists) into the human-readable ASCII tables printed
|
||||
to the console, and serializes per-table row data to CSV / HTML / SQLite files.
|
||||
None of that needs a live target, network or DBMS: the console renderers route
|
||||
every line through Dump._write (overridden here to capture instead of print),
|
||||
and the file renderers just write to a path we point at a temp dir. These tests
|
||||
pin the rendered layout/escaping contracts so a formatting regression is caught
|
||||
without an end-to-end scan.
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from collections import OrderedDict as _PlainOrderedDict
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap
|
||||
bootstrap()
|
||||
|
||||
from lib.core.common import Backend
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.dump import Dump
|
||||
from lib.core.enums import DUMP_FORMAT
|
||||
from lib.core.replication import Replication
|
||||
|
||||
|
||||
# --- console-rendering tests (no files): capture every Dump._write line --------------------------
|
||||
|
||||
class _CaptureCase(unittest.TestCase):
|
||||
"""Base for the console renderers: pins a neutral case-preserving DBMS, disables api/report
|
||||
side channels, and replaces Dump._write with an in-memory capture so nothing hits stdout."""
|
||||
|
||||
_CONF_KEYS = ("api", "reportCollector", "dumpFormat", "col", "csvDel", "dumpPath", "dumpFile",
|
||||
"limitStart", "limitStop", "forceDbms", "dbms")
|
||||
_KB_KEYS = ("forcedDbms", "dbms")
|
||||
|
||||
def setUp(self):
|
||||
self._saved = dict((k, conf.get(k)) for k in self._CONF_KEYS)
|
||||
self._savedKb = dict((k, kb.get(k)) for k in self._KB_KEYS)
|
||||
conf.forceDbms = conf.dbms = None
|
||||
kb.dbms = None
|
||||
Backend.forceDbms("MySQL")
|
||||
conf.api = False
|
||||
conf.reportCollector = None
|
||||
conf.col = None
|
||||
conf.csvDel = ","
|
||||
self.lines = []
|
||||
self.d = Dump()
|
||||
self.d._write = self._capture
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved.items():
|
||||
conf[k] = v
|
||||
for k, v in self._savedKb.items():
|
||||
kb[k] = v
|
||||
|
||||
def _capture(self, data, newline=True, console=True, content_type=None):
|
||||
# mirror Dump._write's own line-vs-space join so multi-call lines reassemble faithfully
|
||||
self.lines.append("%s%s" % (data, "\n" if newline else " "))
|
||||
|
||||
def text(self):
|
||||
return "".join(self.lines)
|
||||
|
||||
|
||||
class TestStringAndLister(_CaptureCase):
|
||||
def test_string_scalar_quoted(self):
|
||||
# a plain string fact is rendered as "header: 'value'"
|
||||
self.d.string("current user", "root@localhost")
|
||||
self.assertIn("current user: 'root@localhost'", self.text())
|
||||
|
||||
def test_string_multiline_block(self):
|
||||
# a value containing a newline switches to the fenced ---\n...\n--- block form
|
||||
self.d.string("banner", "line1\nline2")
|
||||
out = self.text()
|
||||
self.assertIn("banner:\n---\nline1\nline2\n---", out)
|
||||
|
||||
def test_string_singleton_list_unwrapped(self):
|
||||
# a one-element list is unwrapped to the scalar form (not the lister "[N]:" form)
|
||||
self.d.string("current database", ["testdb"])
|
||||
out = self.text()
|
||||
self.assertIn("current database: 'testdb'", out)
|
||||
self.assertNotIn("[1]", out)
|
||||
|
||||
def test_lister_sorts_and_counts(self):
|
||||
# lister prints a "[count]:" header, one "[*] item" per element, sorted case-insensitively
|
||||
self.d.lister("available databases", ["mysql", "Alpha", "zebra"])
|
||||
out = self.text()
|
||||
self.assertIn("available databases [3]:", out)
|
||||
body = out[out.index("[3]:"):]
|
||||
# case-insensitive ascending: Alpha, mysql, zebra
|
||||
self.assertLess(body.index("[*] Alpha"), body.index("[*] mysql"))
|
||||
self.assertLess(body.index("[*] mysql"), body.index("[*] zebra"))
|
||||
|
||||
def test_lister_dedupes(self):
|
||||
# the sort path also de-duplicates (set()) before listing
|
||||
self.d.lister("database management system users", ["root", "root", "guest"])
|
||||
out = self.text()
|
||||
self.assertIn("database management system users [2]:", out)
|
||||
self.assertEqual(out.count("[*] root"), 1)
|
||||
|
||||
def test_lister_unsorted_preserves_order(self):
|
||||
# sort=False (e.g. rFile) keeps insertion order
|
||||
self.d.lister("files saved to", ["/z", "/a", "/m"], sort=False)
|
||||
out = self.text()
|
||||
self.assertLess(out.index("[*] /z"), out.index("[*] /a"))
|
||||
self.assertLess(out.index("[*] /a"), out.index("[*] /m"))
|
||||
|
||||
|
||||
class TestCurrentDb(_CaptureCase):
|
||||
def test_label_default_dbms(self):
|
||||
# MySQL is not in the schema/owner special-cased lists -> plain "current database"
|
||||
self.d.currentDb("testdb")
|
||||
self.assertIn("current database: 'testdb'", self.text())
|
||||
|
||||
def test_label_schema_dbms(self):
|
||||
# Oracle is in the schema-equivalent list -> the label is annotated accordingly
|
||||
Backend.forceDbms("Oracle")
|
||||
self.d.currentDb("SYSTEM")
|
||||
out = self.text()
|
||||
self.assertIn("equivalent to schema on Oracle", out)
|
||||
self.assertIn("SYSTEM", out)
|
||||
|
||||
|
||||
class TestDbTables(_CaptureCase):
|
||||
def test_table_listing_box(self):
|
||||
self.d.dbTables({"testdb": ["users", "logs"]})
|
||||
out = self.text()
|
||||
self.assertIn("Database: testdb", out)
|
||||
self.assertIn("[2 tables]", out)
|
||||
self.assertIn("| users", out)
|
||||
self.assertIn("| logs", out)
|
||||
# box borders present
|
||||
self.assertIn("+", out)
|
||||
|
||||
def test_single_table_singular(self):
|
||||
self.d.dbTables({"testdb": ["only"]})
|
||||
self.assertIn("[1 table]", self.text())
|
||||
|
||||
def test_no_tables(self):
|
||||
self.d.dbTables({})
|
||||
self.assertIn("No tables found", self.text())
|
||||
|
||||
def test_box_width_matches_longest_table(self):
|
||||
# the border length tracks the longest table name (+2 padding)
|
||||
self.d.dbTables({"testdb": ["a", "elephant"]})
|
||||
out = self.text()
|
||||
# "elephant" is 8 chars -> a border line of 8+2 = 10 dashes exists
|
||||
self.assertIn("+%s+" % ("-" * 10), out)
|
||||
|
||||
|
||||
class TestDbTableColumns(_CaptureCase):
|
||||
def test_typed_columns_two_column_box(self):
|
||||
self.d.dbTableColumns({"testdb": {"users": {"id": "int", "name": "varchar(50)"}}})
|
||||
out = self.text()
|
||||
self.assertIn("Database: testdb", out)
|
||||
self.assertIn("Table: users", out)
|
||||
self.assertIn("[2 columns]", out)
|
||||
self.assertIn("| Column", out)
|
||||
self.assertIn("| Type", out)
|
||||
self.assertIn("int", out)
|
||||
self.assertIn("varchar(50)", out)
|
||||
|
||||
def test_typeless_columns_single_box(self):
|
||||
# when no column carries a type, only the Column box is rendered (no Type header)
|
||||
self.d.dbTableColumns({"testdb": {"users": {"id": None, "name": None}}})
|
||||
out = self.text()
|
||||
self.assertIn("| Column", out)
|
||||
self.assertNotIn("| Type", out)
|
||||
|
||||
def test_mixed_types_still_show_type_header(self):
|
||||
# even if the alphabetically-last column is type-less, a Type column must appear
|
||||
self.d.dbTableColumns({"testdb": {"t": {"aaa": "int", "zzz": None}}})
|
||||
self.assertIn("| Type", self.text())
|
||||
|
||||
|
||||
class TestDbTablesCount(_CaptureCase):
|
||||
def test_count_box_sorted_desc(self):
|
||||
self.d.dbTablesCount({"testdb": {5: ["small"], 100: ["big"]}})
|
||||
out = self.text()
|
||||
self.assertIn("Database: testdb", out)
|
||||
self.assertIn("| Table", out)
|
||||
self.assertIn("| Entries", out)
|
||||
# higher count first (reverse sort)
|
||||
self.assertLess(out.index("big"), out.index("small"))
|
||||
self.assertIn("100", out)
|
||||
|
||||
|
||||
class TestUserSettings(_CaptureCase):
|
||||
def test_privileges_listed_with_admin_flag(self):
|
||||
# userSettings accepts (settingsDict, adminsSet); admins get an "(administrator)" tag
|
||||
settings = ({"root": ["ALL"], "guest": ["SELECT"]}, set(["root"]))
|
||||
self.d.userSettings("database management system users privileges", settings, "privilege")
|
||||
out = self.text()
|
||||
self.assertIn("[*] root (administrator)", out)
|
||||
self.assertIn("[*] guest", out)
|
||||
self.assertNotIn("guest (administrator)", out)
|
||||
self.assertIn("privilege: ALL", out)
|
||||
self.assertIn("privilege: SELECT", out)
|
||||
|
||||
|
||||
# --- file-rendering tests (CSV / HTML / SQLite): point output at a temp dir ----------------------
|
||||
|
||||
class _FileDumpCase(unittest.TestCase):
|
||||
_CONF_KEYS = ("dumpFormat", "dumpPath", "dumpFile", "col", "api", "reportCollector",
|
||||
"limitStart", "limitStop", "csvDel", "forceDbms", "dbms")
|
||||
_KB_KEYS = ("forcedDbms", "dbms")
|
||||
|
||||
def setUp(self):
|
||||
self._saved = dict((k, conf.get(k)) for k in self._CONF_KEYS)
|
||||
self._savedKb = dict((k, kb.get(k)) for k in self._KB_KEYS)
|
||||
conf.forceDbms = conf.dbms = None
|
||||
kb.dbms = None
|
||||
Backend.forceDbms("MySQL")
|
||||
self.tmp = tempfile.mkdtemp(prefix="sqlmap-dumpfmt-test")
|
||||
conf.dumpPath = self.tmp
|
||||
conf.dumpFile = None
|
||||
conf.col = None
|
||||
conf.api = False
|
||||
conf.reportCollector = None
|
||||
conf.limitStart = conf.limitStop = None
|
||||
conf.csvDel = ","
|
||||
self.d = Dump()
|
||||
self.d._write = lambda *a, **k: None # silence the console table
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved.items():
|
||||
conf[k] = v
|
||||
for k, v in self._savedKb.items():
|
||||
kb[k] = v
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
def _path(self, table_values, ext):
|
||||
db = table_values["__infos__"]["db"] or "All"
|
||||
return os.path.join(self.tmp, db, "%s.%s" % (table_values["__infos__"]["table"], ext))
|
||||
|
||||
def _dump(self, table_values, fmt, ext):
|
||||
conf.dumpFormat = fmt
|
||||
self.d.dbTableValues(table_values)
|
||||
with io.open(self._path(table_values, ext), encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
class TestCsvDump(_FileDumpCase):
|
||||
def _sample(self):
|
||||
return _PlainOrderedDict([
|
||||
("__infos__", {"count": 2, "db": "testdb", "table": "users"}),
|
||||
("id", {"length": 2, "values": ["1", "2"]}),
|
||||
("name", {"length": 6, "values": ["luther", "fluffy"]}),
|
||||
])
|
||||
|
||||
def test_header_and_rows(self):
|
||||
content = self._dump(self._sample(), DUMP_FORMAT.CSV, "csv")
|
||||
lines = [l for l in content.splitlines() if l.strip()]
|
||||
self.assertEqual(lines[0].split(","), ["id", "name"])
|
||||
self.assertEqual(lines[1].split(","), ["1", "luther"])
|
||||
self.assertEqual(lines[2].split(","), ["2", "fluffy"])
|
||||
|
||||
def test_delimiter_in_value_is_quoted(self):
|
||||
# RFC-4180: a value containing the delimiter must be wrapped in quotes
|
||||
tv = _PlainOrderedDict([
|
||||
("__infos__", {"count": 1, "db": "testdb", "table": "t"}),
|
||||
("a", {"length": 8, "values": ["x,y"]}),
|
||||
("b", {"length": 1, "values": ["z"]}),
|
||||
])
|
||||
content = self._dump(tv, DUMP_FORMAT.CSV, "csv")
|
||||
self.assertIn('"x,y"', content)
|
||||
|
||||
def test_null_and_blank_markers(self):
|
||||
# the display replacements apply to CSV too: DB NULL (" ") -> NULL, empty ("") -> <blank>
|
||||
tv = _PlainOrderedDict([
|
||||
("__infos__", {"count": 1, "db": "testdb", "table": "t"}),
|
||||
("a", {"length": 4, "values": [" "]}),
|
||||
("b", {"length": 7, "values": [""]}),
|
||||
("c", {"length": 1, "values": ["x"]}),
|
||||
])
|
||||
content = self._dump(tv, DUMP_FORMAT.CSV, "csv")
|
||||
row = [l for l in content.splitlines() if l.strip()][1]
|
||||
self.assertEqual(row.split(","), ["NULL", "<blank>", "x"])
|
||||
|
||||
def test_custom_delimiter(self):
|
||||
conf.csvDel = ";"
|
||||
content = self._dump(self._sample(), DUMP_FORMAT.CSV, "csv")
|
||||
self.assertEqual(content.splitlines()[0].split(";"), ["id", "name"])
|
||||
|
||||
|
||||
class TestHtmlDump(_FileDumpCase):
|
||||
def _sample(self):
|
||||
return _PlainOrderedDict([
|
||||
("__infos__", {"count": 1, "db": "testdb", "table": "users"}),
|
||||
("id", {"length": 2, "values": ["1"]}),
|
||||
("name", {"length": 6, "values": ["luther"]}),
|
||||
])
|
||||
|
||||
def test_html_scaffold_and_cells(self):
|
||||
content = self._dump(self._sample(), DUMP_FORMAT.HTML, "html")
|
||||
self.assertIn("<!DOCTYPE html>", content)
|
||||
self.assertIn("<title>testdb.users</title>", content)
|
||||
self.assertIn("<th", content)
|
||||
self.assertIn(">id</th>", content)
|
||||
self.assertIn(">name</th>", content)
|
||||
self.assertIn("<td>1</td>", content)
|
||||
self.assertIn("<td>luther</td>", content)
|
||||
self.assertIn("</table>", content)
|
||||
self.assertIn("</html>", content)
|
||||
|
||||
def test_html_escapes_markup(self):
|
||||
# a value with HTML metacharacters must be escaped, not emitted raw
|
||||
tv = _PlainOrderedDict([
|
||||
("__infos__", {"count": 1, "db": "testdb", "table": "t"}),
|
||||
("payload", {"length": 16, "values": ["<script>x</script>"]}),
|
||||
])
|
||||
content = self._dump(tv, DUMP_FORMAT.HTML, "html")
|
||||
self.assertNotIn("<td><script>x</script></td>", content)
|
||||
self.assertIn("<", content)
|
||||
|
||||
|
||||
class TestSqliteDump(_FileDumpCase):
|
||||
def test_rows_and_inferred_types(self):
|
||||
tv = _PlainOrderedDict([
|
||||
("__infos__", {"count": 2, "db": "testdb", "table": "people"}),
|
||||
("id", {"length": 2, "values": ["1", "2"]}), # all ints -> INTEGER
|
||||
("ratio", {"length": 4, "values": ["1.5", "2.0"]}), # floats -> REAL
|
||||
("name", {"length": 6, "values": ["alice", " "]}), # text with a NULL marker
|
||||
])
|
||||
conf.dumpFormat = DUMP_FORMAT.SQLITE
|
||||
self.d.dbTableValues(tv)
|
||||
|
||||
import sqlite3
|
||||
dbfile = os.path.join(self.tmp, "testdb.sqlite3")
|
||||
self.assertTrue(os.path.exists(dbfile))
|
||||
conn = sqlite3.connect(dbfile)
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT id, ratio, name FROM people ORDER BY id")
|
||||
rows = cur.fetchall()
|
||||
self.assertEqual(rows[0], (1, 1.5, "alice"))
|
||||
# the DB NULL marker (" ") was stored as a real NULL, not the "NULL" text
|
||||
self.assertEqual(rows[1], (2, 2.0, None))
|
||||
# column affinities inferred from the values
|
||||
cur.execute("PRAGMA table_info(people)")
|
||||
types = {name: ctype for (_cid, name, ctype, _nn, _dv, _pk) in cur.fetchall()}
|
||||
self.assertEqual(types["id"], "INTEGER")
|
||||
self.assertEqual(types["ratio"], "REAL")
|
||||
self.assertEqual(types["name"], "TEXT")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# --- replication backend tests (pure sqlite3, no network/DBMS) -----------------------------------
|
||||
|
||||
class TestReplication(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.mkdtemp(prefix="sqlmap-repl-test")
|
||||
self.path = os.path.join(self.tmp, "out.sqlite3")
|
||||
self.repl = Replication(self.path)
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
self.repl.connection.close()
|
||||
except Exception:
|
||||
pass
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
def test_create_insert_select_roundtrip(self):
|
||||
t = self.repl.createTable("t", [("id", Replication.INTEGER), ("name", Replication.TEXT)])
|
||||
t.beginTransaction()
|
||||
t.insert(["1", "alice"])
|
||||
t.insert(["2", "bob"])
|
||||
t.endTransaction()
|
||||
rows = sorted(t.select())
|
||||
self.assertEqual(rows, [(1, "alice"), (2, "bob")])
|
||||
|
||||
def test_select_with_condition(self):
|
||||
t = self.repl.createTable("t", [("id", Replication.INTEGER), ("name", Replication.TEXT)])
|
||||
t.insert(["1", "alice"])
|
||||
t.insert(["2", "bob"])
|
||||
self.assertEqual(t.select("name = 'bob'"), [(2, "bob")])
|
||||
|
||||
def test_insert_wrong_arity_raises(self):
|
||||
from lib.core.exception import SqlmapValueException
|
||||
t = self.repl.createTable("t", [("id", Replication.INTEGER), ("name", Replication.TEXT)])
|
||||
with self.assertRaises(SqlmapValueException):
|
||||
t.insert(["only-one-value"])
|
||||
|
||||
def test_typeless_table(self):
|
||||
t = self.repl.createTable("t", ["a", "b"], typeless=True)
|
||||
t.insert(["x", "y"])
|
||||
self.assertEqual(t.select(), [("x", "y")])
|
||||
|
||||
def test_datatype_str(self):
|
||||
self.assertEqual(str(Replication.TEXT), "TEXT")
|
||||
self.assertEqual(str(Replication.INTEGER), "INTEGER")
|
||||
self.assertIn("DataType", repr(Replication.REAL))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Loading…
Add table
Add a link
Reference in a new issue