Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cf6465f8c | |||
| 855d6412ef | |||
| aeb763c42c | |||
| 69867b3c10 | |||
| c3ae5b3c76 | |||
| d02ab15d3f | |||
| 9a8641e596 | |||
| 802f018519 | |||
| be74c4e5b7 | |||
| 9198a1d549 | |||
| 53cc24744f | |||
| 9dcf5832b6 | |||
| 10c27d6338 | |||
| 89f12f707b | |||
| 0cef55a02f | |||
| faa8895321 | |||
| e434edb2ea | |||
| 9601d96701 | |||
| d5ce2d78dc | |||
| 84b59d2e4f | |||
| 1970e53059 | |||
| 0c7b60fef8 | |||
| de6e758be6 | |||
| 0f2efa8e74 | |||
| 44cf317aa9 | |||
| 468f6b58cd | |||
| c0db7215c9 | |||
| b0b277c917 | |||
| 7793f3ae54 | |||
| c900fad726 | |||
| 6c4c40d93c | |||
| ff49cc7219 | |||
| 3e391c38ec | |||
| 010bfa624c | |||
| 006b7d416a | |||
| 7b3733ca8e | |||
| a87503eec3 | |||
| f0dd191b58 | |||
| 6cffef4e49 | |||
| 12356819ea | |||
| 84016afbc5 | |||
| e5fe47d0a6 | |||
| 2fde21b51f | |||
| 2d2e8d1695 | |||
| 217b606ee7 | |||
| fd815ac97b | |||
| 9fbfa39060 | |||
| 3ab8689b6e | |||
| 914e22cc55 | |||
| f42a457514 | |||
| 8faa1cfaf9 | |||
| 227f7a3205 | |||
| 17faed6fa7 | |||
| b761d28494 | |||
| 9108939503 | |||
| 8555abf680 | |||
| dd9b7fad19 | |||
| a18203c8cb | |||
| 630b822a9a | |||
| f0fd17c3a3 | |||
| f7f1aafc64 | |||
| e77074c5d4 | |||
| 22bf3b088e | |||
| 811453a129 | |||
| f8d628909c | |||
| 507e377a37 | |||
| 0aac8f6628 |
@@ -0,0 +1,8 @@
|
||||
### Summary of changes
|
||||
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Changes represent a *discrete update*
|
||||
- [ ] Commit messages are meaningful and descriptive
|
||||
- [ ] Changeset does not include any extraneous changes unrelated to the discrete change
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
name: Test on Python ${{ matrix.py_version }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
py_version: [2.7, 3.5, 3.6, 3.7]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python ${{ matrix.py_version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.py_version }}
|
||||
- name: Install mock for Python 2.7
|
||||
run: pip install mock
|
||||
- name: Run tests
|
||||
run: python setup.py test
|
||||
@@ -4,6 +4,7 @@ python:
|
||||
- "2.7"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
- "pypy"
|
||||
- "pypy3"
|
||||
|
||||
|
||||
@@ -6,3 +6,7 @@ Ben Lopatin
|
||||
Daniel Zohar
|
||||
Matheus Fernandes
|
||||
Scott Nixon
|
||||
Jason Dorweiler
|
||||
Pierre-Alain Dupont
|
||||
Karl Goetz
|
||||
Alex Kerney
|
||||
|
||||
+20
-2
@@ -12,6 +12,21 @@ Please use `Google docstring format
|
||||
|
||||
This *will* be enforced.
|
||||
|
||||
Pull requests
|
||||
=============
|
||||
|
||||
Reviewing and merging pull requests is work, so whatever you can do to make this
|
||||
easier for the package maintainer not only speed up the process of getting your
|
||||
changes merged but also ensure they are. These few guidelines help significantly.
|
||||
If they are confusing or you need help understanding how to accomplish them,
|
||||
please ask for help in an issue.
|
||||
|
||||
- Please do make sure your chnageset represents a *discrete update*. If you would like to fix formatting, by all means, but don't mix that up with a bug fix. Those are separate PRs.
|
||||
- Please do make sure that both your pull request description and your commits are meaningful and descriptive. Rebase first, if need be.
|
||||
- Please do make sure your changeset does not include more commits than necessary. Rebase first, if need be.
|
||||
- Please do make sure the changeset is not very big. If you have a large change propose it in an issue first.
|
||||
- Please do make sure your changeset is based on a branch from the current HEAD of the fork you wish to merge against. This is a general best practice. Rebase first, if need be.
|
||||
|
||||
Testing
|
||||
=======
|
||||
|
||||
@@ -54,5 +69,8 @@ Once running you can access the Discourse install at http://localhost:4000.
|
||||
TODO
|
||||
====
|
||||
|
||||
Refer to, https://github.com/discourse/discourse_api/blob/master/routes.txt for
|
||||
a list of all operations available in Discourse.
|
||||
For a list of all operations:
|
||||
|
||||
you can just run rake routes inside of the discourse repo to get an up to date list
|
||||
|
||||
Or check the old [`routes.txt`](https://github.com/discourse/discourse_api/blob/aa75df6cd851f0666f9e8071c4ef9dfdd39fc8f8/routes.txt) file, though this is certainly outdated.
|
||||
|
||||
+52
@@ -3,6 +3,58 @@
|
||||
Release history
|
||||
===============
|
||||
|
||||
1.0.0 (2019-12-04)
|
||||
------------------
|
||||
|
||||
This is a *potentially* breaking change if you're working with a significantly older Discourse deployment.
|
||||
It's not entirely clear if header based authentication has been available since day 1 or was introduced in
|
||||
a recent version (you should be fine! ... but caveat emptor).
|
||||
|
||||
- Adds new invitation methods
|
||||
- Hard switch from query param based authorization to header-based authorization
|
||||
|
||||
0.9.0
|
||||
-----
|
||||
|
||||
- Added rate limiting support
|
||||
- Added some support for user activation
|
||||
|
||||
0.8.0
|
||||
-----
|
||||
|
||||
- Add some PR guidance
|
||||
- Add support for files in the core request methods
|
||||
- Adds numerous new API controls, including:
|
||||
- tag_group
|
||||
- user_actions
|
||||
- upload_image
|
||||
- block
|
||||
- trust_level_lock
|
||||
- create_site_customization (theme)
|
||||
- create_color_scheme
|
||||
- color_schemes
|
||||
- add_group_members
|
||||
- group_members
|
||||
- group_owners
|
||||
- delete_group
|
||||
- create_group
|
||||
- group
|
||||
- customize_site_texts
|
||||
- delete_category
|
||||
- user_emails
|
||||
- update_topic_status
|
||||
- create_post
|
||||
- update_topic
|
||||
- update_avatar
|
||||
- user_all
|
||||
|
||||
|
||||
0.7.0
|
||||
-----
|
||||
|
||||
* Place request parameters in the request body for POST and PUT requests.
|
||||
Allows larger request sizes and solves for `URI Too Large` error.
|
||||
|
||||
0.6.0
|
||||
-----
|
||||
|
||||
|
||||
+7
-2
@@ -2,9 +2,14 @@
|
||||
pydiscourse
|
||||
===========
|
||||
|
||||
.. image:: https://secure.travis-ci.org/bennylope/pydiscourse.svg?branch=master
|
||||
.. image:: https://github.com/bennylope/pydiscourse/workflows/Tests/badge.svg
|
||||
:alt: Build Status
|
||||
:target: http://travis-ci.org/bennylope/pydiscourse
|
||||
:target: https://github.com/bennylope/pydiscourse/actions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Check%20out%20the-Docs-blue.svg
|
||||
:alt: Check out the Docs
|
||||
:target: https://discourse.readthedocs.io/en/latest/
|
||||
|
||||
|
||||
A Python library for working with Discourse.
|
||||
|
||||
|
||||
+2
-2
@@ -51,9 +51,9 @@ copyright = u'2014, Marc Sibson'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.6'
|
||||
version = '1.0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.6.0'
|
||||
release = '1.0.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__version__ = '0.6.0'
|
||||
__version__ = "1.0.0"
|
||||
|
||||
from pydiscourse.client import DiscourseClient
|
||||
|
||||
+682
-128
File diff suppressed because it is too large
Load Diff
@@ -11,3 +11,7 @@ class DiscourseServerError(DiscourseError):
|
||||
|
||||
class DiscourseClientError(DiscourseError):
|
||||
""" An invalid request has been made """
|
||||
|
||||
|
||||
class DiscourseRateLimitedError(DiscourseError):
|
||||
""" Request required more than the permissible number of retries """
|
||||
|
||||
+20
-16
@@ -12,30 +12,32 @@ from pydiscourse.client import DiscourseClient, DiscourseError
|
||||
|
||||
|
||||
class DiscourseCmd(cmd.Cmd):
|
||||
prompt = 'discourse>'
|
||||
prompt = "discourse>"
|
||||
output = sys.stdout
|
||||
|
||||
def __init__(self, client):
|
||||
cmd.Cmd.__init__(self)
|
||||
self.client = client
|
||||
self.prompt = '%s>' % self.client.host
|
||||
self.prompt = "%s>" % self.client.host
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr.startswith('do_'):
|
||||
if attr.startswith("do_"):
|
||||
method = getattr(self.client, attr[3:])
|
||||
|
||||
def wrapper(arg):
|
||||
args = arg.split()
|
||||
kwargs = dict(a.split('=') for a in args if '=' in a)
|
||||
args = [a for a in args if '=' not in a]
|
||||
kwargs = dict(a.split("=") for a in args if "=" in a)
|
||||
args = [a for a in args if "=" not in a]
|
||||
try:
|
||||
return method(*args, **kwargs)
|
||||
|
||||
except DiscourseError as e:
|
||||
print(e, e.response.text)
|
||||
return e.response
|
||||
|
||||
return wrapper
|
||||
|
||||
elif attr.startswith('help_'):
|
||||
elif attr.startswith("help_"):
|
||||
method = getattr(self.client, attr[5:])
|
||||
|
||||
def wrapper():
|
||||
@@ -47,24 +49,26 @@ class DiscourseCmd(cmd.Cmd):
|
||||
|
||||
def postcmd(self, result, line):
|
||||
try:
|
||||
json.dump(result, self.output, sort_keys=True, indent=4, separators=(',', ': '))
|
||||
json.dump(
|
||||
result, self.output, sort_keys=True, indent=4, separators=(",", ": ")
|
||||
)
|
||||
except TypeError:
|
||||
self.output.write(result.text)
|
||||
|
||||
|
||||
def main():
|
||||
op = optparse.OptionParser()
|
||||
op.add_option('--host', default='http://localhost:4000')
|
||||
op.add_option('--api-user', default='system')
|
||||
op.add_option('-v', '--verbose', action='store_true')
|
||||
op.add_option("--host", default="http://localhost:4000")
|
||||
op.add_option("--api-user", default="system")
|
||||
op.add_option("-v", "--verbose", action="store_true")
|
||||
|
||||
options, args = op.parse_args()
|
||||
if not options.host.startswith('http'):
|
||||
op.error('host must include protocol, eg http://')
|
||||
if not options.host.startswith("http"):
|
||||
op.error("host must include protocol, eg http://")
|
||||
|
||||
api_key = os.environ.get('DISCOURSE_API_KEY')
|
||||
api_key = os.environ.get("DISCOURSE_API_KEY")
|
||||
if not api_key:
|
||||
op.error('please set DISCOURSE_API_KEY')
|
||||
op.error("please set DISCOURSE_API_KEY")
|
||||
|
||||
client = DiscourseClient(options.host, options.api_user, api_key)
|
||||
|
||||
@@ -74,12 +78,12 @@ def main():
|
||||
|
||||
c = DiscourseCmd(client)
|
||||
if args:
|
||||
line = ' '.join(args)
|
||||
line = " ".join(args)
|
||||
result = c.onecmd(line)
|
||||
c.postcmd(result, line)
|
||||
else:
|
||||
c.cmdloop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+23
-19
@@ -45,34 +45,36 @@ def sso_validate(payload, signature, secret):
|
||||
return value: The nonce used by discourse to validate the redirect URL
|
||||
"""
|
||||
if None in [payload, signature]:
|
||||
raise DiscourseError('No SSO payload or signature.')
|
||||
raise DiscourseError("No SSO payload or signature.")
|
||||
|
||||
if not secret:
|
||||
raise DiscourseError('Invalid secret..')
|
||||
raise DiscourseError("Invalid secret..")
|
||||
|
||||
payload = unquote(payload)
|
||||
if not payload:
|
||||
raise DiscourseError('Invalid payload..')
|
||||
raise DiscourseError("Invalid payload..")
|
||||
|
||||
decoded = b64decode(payload.encode('utf-8')).decode('utf-8')
|
||||
if 'nonce' not in decoded:
|
||||
raise DiscourseError('Invalid payload..')
|
||||
decoded = b64decode(payload.encode("utf-8")).decode("utf-8")
|
||||
if "nonce" not in decoded:
|
||||
raise DiscourseError("Invalid payload..")
|
||||
|
||||
h = hmac.new(secret.encode('utf-8'), payload.encode('utf-8'), digestmod=hashlib.sha256)
|
||||
h = hmac.new(
|
||||
secret.encode("utf-8"), payload.encode("utf-8"), digestmod=hashlib.sha256
|
||||
)
|
||||
this_signature = h.hexdigest()
|
||||
|
||||
if this_signature != signature:
|
||||
raise DiscourseError('Payload does not match signature.')
|
||||
raise DiscourseError("Payload does not match signature.")
|
||||
|
||||
# Discourse returns querystring encoded value. We only need `nonce`
|
||||
qs = parse_qs(decoded)
|
||||
return qs['nonce'][0]
|
||||
return qs["nonce"][0]
|
||||
|
||||
|
||||
def sso_payload(secret, **kwargs):
|
||||
return_payload = b64encode(urlencode(kwargs).encode('utf-8'))
|
||||
h = hmac.new(secret.encode('utf-8'), return_payload, digestmod=hashlib.sha256)
|
||||
query_string = urlencode({'sso': return_payload, 'sig': h.hexdigest()})
|
||||
return_payload = b64encode(urlencode(kwargs).encode("utf-8"))
|
||||
h = hmac.new(secret.encode("utf-8"), return_payload, digestmod=hashlib.sha256)
|
||||
query_string = urlencode({"sso": return_payload, "sig": h.hexdigest()})
|
||||
return query_string
|
||||
|
||||
|
||||
@@ -86,11 +88,13 @@ def sso_redirect_url(nonce, secret, email, external_id, username, **kwargs):
|
||||
|
||||
return value: URL to redirect users back to discourse, now logged in as user_username
|
||||
"""
|
||||
kwargs.update({
|
||||
'nonce': nonce,
|
||||
'email': email,
|
||||
'external_id': external_id,
|
||||
'username': username
|
||||
})
|
||||
kwargs.update(
|
||||
{
|
||||
"nonce": nonce,
|
||||
"email": email,
|
||||
"external_id": external_id,
|
||||
"username": username,
|
||||
}
|
||||
)
|
||||
|
||||
return '/session/sso_login?%s' % sso_payload(secret, **kwargs)
|
||||
return "/session/sso_login?%s" % sso_payload(secret, **kwargs)
|
||||
|
||||
@@ -8,7 +8,7 @@ with open("pydiscourse/__init__.py", "r") as module_file:
|
||||
for line in module_file:
|
||||
if line.startswith("__version__"):
|
||||
version_string = line.split("=")[1]
|
||||
VERSION = version_string.strip().replace("'", "")
|
||||
VERSION = version_string.strip().replace("\"", "")
|
||||
|
||||
|
||||
setup(
|
||||
@@ -22,7 +22,7 @@ setup(
|
||||
url="https://github.com/bennylope/pydiscourse",
|
||||
packages=find_packages(exclude=["tests.*", "tests"]),
|
||||
install_requires=[
|
||||
'requests>=2.0.0',
|
||||
'requests>=2.4.2',
|
||||
],
|
||||
tests_require=[
|
||||
'mock',
|
||||
@@ -34,15 +34,16 @@ setup(
|
||||
]
|
||||
},
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Web Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
'Programming Language :: Python :: Implementation :: PyPy',
|
||||
],
|
||||
zip_safe=False,
|
||||
|
||||
+76
-54
@@ -1,22 +1,32 @@
|
||||
import sys
|
||||
import unittest
|
||||
import mock
|
||||
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
import mock
|
||||
|
||||
from pydiscourse import client
|
||||
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3,):
|
||||
|
||||
def b(x):
|
||||
return x
|
||||
|
||||
|
||||
else:
|
||||
import codecs
|
||||
|
||||
def b(x):
|
||||
return codecs.latin_1_encode(x)[0]
|
||||
|
||||
|
||||
def prepare_response(request):
|
||||
# we need to mocked response to look a little more real
|
||||
request.return_value = mock.MagicMock(headers={'content-type': 'application/json; charset=utf-8'})
|
||||
request.return_value = mock.MagicMock(
|
||||
headers={"content-type": "application/json; charset=utf-8"}
|
||||
)
|
||||
|
||||
|
||||
class ClientBaseTestCase(unittest.TestCase):
|
||||
@@ -25,9 +35,9 @@ class ClientBaseTestCase(unittest.TestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.host = 'http://testhost'
|
||||
self.api_username = 'testuser'
|
||||
self.api_key = 'testkey'
|
||||
self.host = "http://testhost"
|
||||
self.api_username = "testuser"
|
||||
self.api_key = "testkey"
|
||||
|
||||
self.client = client.DiscourseClient(self.host, self.api_username, self.api_key)
|
||||
|
||||
@@ -39,11 +49,12 @@ class ClientBaseTestCase(unittest.TestCase):
|
||||
self.assertEqual(args[0], verb)
|
||||
self.assertEqual(args[1], self.host + url)
|
||||
|
||||
kwargs = kwargs['params']
|
||||
self.assertEqual(kwargs.pop('api_username'), self.api_username)
|
||||
self.assertEqual(kwargs.pop('api_key'), self.api_key)
|
||||
self.assertEqual(kwargs, params)
|
||||
headers = kwargs["headers"]
|
||||
self.assertEqual(headers.pop("Api-Username"), self.api_username)
|
||||
self.assertEqual(headers.pop("Api-Key"), self.api_key)
|
||||
|
||||
if verb == "GET":
|
||||
self.assertEqual(kwargs["params"], params)
|
||||
|
||||
|
||||
class TestClientRequests(ClientBaseTestCase):
|
||||
@@ -51,14 +62,14 @@ class TestClientRequests(ClientBaseTestCase):
|
||||
Tests for common request handling
|
||||
"""
|
||||
|
||||
@mock.patch('pydiscourse.client.requests')
|
||||
@mock.patch("pydiscourse.client.requests")
|
||||
def test_empty_content_http_ok(self, mocked_requests):
|
||||
"""Empty content should not raise error
|
||||
|
||||
Critical to test against *bytestrings* rather than unicode
|
||||
"""
|
||||
mocked_response = mock.MagicMock()
|
||||
mocked_response.content = b(' ')
|
||||
mocked_response.content = b(" ")
|
||||
mocked_response.status_code = 200
|
||||
mocked_response.headers = {"content-type": "text/plain; charset=utf-8"}
|
||||
|
||||
@@ -67,126 +78,137 @@ class TestClientRequests(ClientBaseTestCase):
|
||||
mocked_requests.request = mock.MagicMock()
|
||||
mocked_requests.request.return_value = mocked_response
|
||||
|
||||
resp = self.client._request('GET', '/users/admin/1/unsuspend', {})
|
||||
resp = self.client._request("GET", "/users/admin/1/unsuspend", {})
|
||||
self.assertIsNone(resp)
|
||||
|
||||
|
||||
@mock.patch('requests.request')
|
||||
@mock.patch("requests.request")
|
||||
class TestUser(ClientBaseTestCase):
|
||||
|
||||
def test_user(self, request):
|
||||
prepare_response(request)
|
||||
self.client.user('someuser')
|
||||
self.assertRequestCalled(request, 'GET', '/users/someuser.json')
|
||||
self.client.user("someuser")
|
||||
self.assertRequestCalled(request, "GET", "/users/someuser.json")
|
||||
|
||||
def test_create_user(self, request):
|
||||
prepare_response(request)
|
||||
self.client.create_user('Test User', 'testuser', 'test@example.com', 'notapassword')
|
||||
self.client.create_user(
|
||||
"Test User", "testuser", "test@example.com", "notapassword"
|
||||
)
|
||||
self.assertEqual(request.call_count, 2)
|
||||
# XXX incomplete
|
||||
|
||||
# XXX incomplete
|
||||
|
||||
def test_update_email(self, request):
|
||||
prepare_response(request)
|
||||
email = 'test@example.com'
|
||||
self.client.update_email('someuser', email)
|
||||
self.assertRequestCalled(request, 'PUT', '/users/someuser/preferences/email', email=email)
|
||||
email = "test@example.com"
|
||||
self.client.update_email("someuser", email)
|
||||
self.assertRequestCalled(
|
||||
request, "PUT", "/users/someuser/preferences/email", email=email
|
||||
)
|
||||
|
||||
def test_update_user(self, request):
|
||||
prepare_response(request)
|
||||
self.client.update_user('someuser', a='a', b='b')
|
||||
self.assertRequestCalled(request, 'PUT', '/users/someuser', a='a', b='b')
|
||||
self.client.update_user("someuser", a="a", b="b")
|
||||
self.assertRequestCalled(request, "PUT", "/users/someuser", a="a", b="b")
|
||||
|
||||
def test_update_username(self, request):
|
||||
prepare_response(request)
|
||||
self.client.update_username('someuser', 'newname')
|
||||
self.assertRequestCalled(request, 'PUT',
|
||||
'/users/someuser/preferences/username',
|
||||
username='newname')
|
||||
self.client.update_username("someuser", "newname")
|
||||
self.assertRequestCalled(
|
||||
request, "PUT", "/users/someuser/preferences/username", username="newname"
|
||||
)
|
||||
|
||||
def test_by_external_id(self, request):
|
||||
prepare_response(request)
|
||||
self.client.by_external_id(123)
|
||||
self.assertRequestCalled(request, 'GET',
|
||||
'/users/by-external/123')
|
||||
self.assertRequestCalled(request, "GET", "/users/by-external/123")
|
||||
|
||||
def test_suspend_user(self, request):
|
||||
prepare_response(request)
|
||||
self.client.suspend(123, 1, "Testing")
|
||||
self.assertRequestCalled(request, 'PUT', '/admin/users/123/suspend',
|
||||
duration=1, reason="Testing")
|
||||
self.assertRequestCalled(
|
||||
request, "PUT", "/admin/users/123/suspend", duration=1, reason="Testing"
|
||||
)
|
||||
|
||||
def test_unsuspend_user(self, request):
|
||||
prepare_response(request)
|
||||
self.client.unsuspend(123)
|
||||
self.assertRequestCalled(request, 'PUT', '/admin/users/123/unsuspend')
|
||||
self.assertRequestCalled(request, "PUT", "/admin/users/123/unsuspend")
|
||||
|
||||
def test_user_bagdes(self, request):
|
||||
prepare_response(request)
|
||||
self.client.user_badges('username')
|
||||
self.assertRequestCalled(request, 'GET', '/user-badges/{}.json'.format('username'))
|
||||
self.client.user_badges("username")
|
||||
self.assertRequestCalled(
|
||||
request, "GET", "/user-badges/{}.json".format("username")
|
||||
)
|
||||
|
||||
|
||||
@mock.patch('requests.request')
|
||||
@mock.patch("requests.request")
|
||||
class TestTopics(ClientBaseTestCase):
|
||||
|
||||
def test_hot_topics(self, request):
|
||||
prepare_response(request)
|
||||
self.client.hot_topics()
|
||||
self.assertRequestCalled(request, 'GET', '/hot.json')
|
||||
self.assertRequestCalled(request, "GET", "/hot.json")
|
||||
|
||||
def test_latest_topics(self, request):
|
||||
prepare_response(request)
|
||||
self.client.latest_topics()
|
||||
self.assertRequestCalled(request, 'GET', '/latest.json')
|
||||
self.assertRequestCalled(request, "GET", "/latest.json")
|
||||
|
||||
def test_new_topics(self, request):
|
||||
prepare_response(request)
|
||||
self.client.new_topics()
|
||||
self.assertRequestCalled(request, 'GET', '/new.json')
|
||||
self.assertRequestCalled(request, "GET", "/new.json")
|
||||
|
||||
def test_topic(self, request):
|
||||
prepare_response(request)
|
||||
self.client.topic('some-test-slug', 22)
|
||||
self.assertRequestCalled(request, 'GET', '/t/some-test-slug/22.json')
|
||||
self.client.topic("some-test-slug", 22)
|
||||
self.assertRequestCalled(request, "GET", "/t/some-test-slug/22.json")
|
||||
|
||||
def test_topics_by(self, request):
|
||||
prepare_response(request)
|
||||
r = self.client.topics_by('someuser')
|
||||
self.assertRequestCalled(request, 'GET', '/topics/created-by/someuser.json')
|
||||
self.assertEqual(r, request().json()['topic_list']['topics'])
|
||||
r = self.client.topics_by("someuser")
|
||||
self.assertRequestCalled(request, "GET", "/topics/created-by/someuser.json")
|
||||
self.assertEqual(r, request().json()["topic_list"]["topics"])
|
||||
|
||||
def invite_user_to_topic(self, request):
|
||||
prepare_response(request)
|
||||
email = 'test@example.com'
|
||||
email = "test@example.com"
|
||||
self.client.invite_user_to_topic(email, 22)
|
||||
self.assertRequestCalled(request, 'POST', '/t/22/invite.json', email=email, topic_id=22)
|
||||
self.assertRequestCalled(
|
||||
request, "POST", "/t/22/invite.json", email=email, topic_id=22
|
||||
)
|
||||
|
||||
|
||||
@mock.patch('requests.request')
|
||||
@mock.patch("pydiscourse.client.requests.request")
|
||||
class MiscellaneousTests(ClientBaseTestCase):
|
||||
|
||||
def test_search(self, request):
|
||||
prepare_response(request)
|
||||
self.client.search('needle')
|
||||
self.assertRequestCalled(request, 'GET', '/search.json', term='needle')
|
||||
self.client.search("needle")
|
||||
self.assertRequestCalled(request, "GET", "/search.json", term="needle")
|
||||
|
||||
def test_categories(self, request):
|
||||
prepare_response(request)
|
||||
r = self.client.categories()
|
||||
self.assertRequestCalled(request, 'GET', '/categories.json')
|
||||
self.assertEqual(r, request().json()['category_list']['categories'])
|
||||
self.assertRequestCalled(request, "GET", "/categories.json")
|
||||
self.assertEqual(r, request().json()["category_list"]["categories"])
|
||||
|
||||
def test_users(self, request):
|
||||
prepare_response(request)
|
||||
self.client.users()
|
||||
self.assertRequestCalled(request, 'GET', '/admin/users/list/active.json')
|
||||
self.assertRequestCalled(request, "GET", "/admin/users/list/active.json")
|
||||
|
||||
def test_badges(self, request):
|
||||
prepare_response(request)
|
||||
self.client.badges()
|
||||
self.assertRequestCalled(request, 'GET', '/admin/badges.json')
|
||||
self.assertRequestCalled(request, "GET", "/admin/badges.json")
|
||||
|
||||
def test_grant_badge_to(self, request):
|
||||
prepare_response(request)
|
||||
self.client.grant_badge_to('username', 1)
|
||||
self.assertRequestCalled(request, 'POST', '/user_badges', username='username', badge_id=1)
|
||||
self.client.grant_badge_to("username", 1)
|
||||
self.assertRequestCalled(
|
||||
request, "POST", "/user_badges", username="username", badge_id=1
|
||||
)
|
||||
|
||||
+35
-24
@@ -19,56 +19,67 @@ from pydiscourse.exceptions import DiscourseError
|
||||
|
||||
|
||||
class SSOTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# values from https://meta.discourse.org/t/official-single-sign-on-for-discourse/13045
|
||||
self.secret = 'd836444a9e4084d5b224a60c208dce14'
|
||||
self.nonce = 'cb68251eefb5211e58c00ff1395f0c0b'
|
||||
self.payload = 'bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGI%3D%0A'
|
||||
self.signature = '2828aa29899722b35a2f191d34ef9b3ce695e0e6eeec47deb46d588d70c7cb56'
|
||||
self.secret = "d836444a9e4084d5b224a60c208dce14"
|
||||
self.nonce = "cb68251eefb5211e58c00ff1395f0c0b"
|
||||
self.payload = "bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGI%3D%0A"
|
||||
self.signature = "2828aa29899722b35a2f191d34ef9b3ce695e0e6eeec47deb46d588d70c7cb56"
|
||||
|
||||
self.name = u'sam'
|
||||
self.username = u'samsam'
|
||||
self.external_id = u'hello123'
|
||||
self.email = u'test@test.com'
|
||||
self.redirect_url = u'/session/sso_login?sso=bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT1z%0AYW0mdXNlcm5hbWU9c2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRl%0Acm5hbF9pZD1oZWxsbzEyMw%3D%3D%0A&sig=1c884222282f3feacd76802a9dd94e8bc8deba5d619b292bed75d63eb3152c0b'
|
||||
self.name = u"sam"
|
||||
self.username = u"samsam"
|
||||
self.external_id = u"hello123"
|
||||
self.email = u"test@test.com"
|
||||
self.redirect_url = u"/session/sso_login?sso=bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT1z%0AYW0mdXNlcm5hbWU9c2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRl%0Acm5hbF9pZD1oZWxsbzEyMw%3D%3D%0A&sig=1c884222282f3feacd76802a9dd94e8bc8deba5d619b292bed75d63eb3152c0b"
|
||||
|
||||
def test_missing_args(self):
|
||||
with self.assertRaises(DiscourseError):
|
||||
sso.sso_validate(None, self.signature, self.secret)
|
||||
|
||||
with self.assertRaises(DiscourseError):
|
||||
sso.sso_validate('', self.signature, self.secret)
|
||||
sso.sso_validate("", self.signature, self.secret)
|
||||
|
||||
with self.assertRaises(DiscourseError):
|
||||
sso.sso_validate(self.payload, None, self.secret)
|
||||
|
||||
def test_invalid_signature(self):
|
||||
with self.assertRaises(DiscourseError):
|
||||
sso.sso_validate(self.payload, 'notavalidsignature', self.secret)
|
||||
sso.sso_validate(self.payload, "notavalidsignature", self.secret)
|
||||
|
||||
def test_valid_nonce(self):
|
||||
nonce = sso.sso_validate(self.payload, self.signature, self.secret)
|
||||
self.assertEqual(nonce, self.nonce)
|
||||
|
||||
def test_valid_redirect_url(self):
|
||||
url = sso.sso_redirect_url(self.nonce, self.secret, self.email, self.external_id, self.username, name='sam')
|
||||
url = sso.sso_redirect_url(
|
||||
self.nonce,
|
||||
self.secret,
|
||||
self.email,
|
||||
self.external_id,
|
||||
self.username,
|
||||
name="sam",
|
||||
)
|
||||
|
||||
self.assertIn('/session/sso_login', url[:20])
|
||||
self.assertIn("/session/sso_login", url[:20])
|
||||
|
||||
# check its valid, using our own handy validator
|
||||
params = parse_qs(urlparse(url).query)
|
||||
payload = params['sso'][0]
|
||||
sso.sso_validate(payload, params['sig'][0], self.secret)
|
||||
payload = params["sso"][0]
|
||||
sso.sso_validate(payload, params["sig"][0], self.secret)
|
||||
|
||||
# check the params have all the data we expect
|
||||
payload = b64decode(payload.encode('utf-8')).decode('utf-8')
|
||||
payload = b64decode(payload.encode("utf-8")).decode("utf-8")
|
||||
payload = unquote(payload)
|
||||
payload = dict((p.split('=') for p in payload.split('&')))
|
||||
payload = dict((p.split("=") for p in payload.split("&")))
|
||||
|
||||
self.assertEqual(payload, {
|
||||
'username': self.username,
|
||||
'nonce': self.nonce,
|
||||
'external_id': self.external_id,
|
||||
'name': self.name,
|
||||
'email': self.email
|
||||
})
|
||||
self.assertEqual(
|
||||
payload,
|
||||
{
|
||||
"username": self.username,
|
||||
"nonce": self.nonce,
|
||||
"external_id": self.external_id,
|
||||
"name": self.name,
|
||||
"email": self.email,
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user