Compare commits

...

71 Commits

Author SHA1 Message Date
Alex Kerney 6e31953118 Fix 413 response from Discourse due to empty dict passed as json 2020-08-11 13:17:52 -04:00
Christian Kindel 2ad158e195 Fix call to get group info by name 2020-08-10 21:13:26 -04:00
Ben Lopatin 719035e9a9 Fix classifier error 2020-07-21 17:22:55 -04:00
Ben Lopatin 3a4af08827 Bump version 1.1.0 2020-07-21 17:21:10 -04:00
Alex Kerney 5d334f1d80 Use immutable arguments and adjust naming to simplify 2020-07-21 17:14:54 -04:00
Alex Kerney 361bf77949 Allow client methods to override requests kwargs
Due to some changes in the Discourse API, certain methods now work better if redirects are allowed.

Get /c/{id}.json will redirect to /c/{category_slug}.json which will cause the client.category_topics(id) method to fail as the redirect is not followed by default.

Now the keyword arguments to requests.request can be overridden by individual methods. This is implemented for .category_topics
2020-07-21 17:14:54 -04:00
Alex Kerney aeb763c42c Authenticate via headers
Closes bennylope/pydiscourse#27
2019-12-04 18:09:00 -05:00
Ben Lopatin 69867b3c10 Run test workflow on pull request 2019-10-13 18:30:40 -04:00
Ben Lopatin c3ae5b3c76 Update return documentation 2019-10-13 18:12:23 -04:00
Ben Lopatin d02ab15d3f Formatting 2019-10-13 18:10:12 -04:00
Richard Leyton 9a8641e596 Added invite() and invite_link() methods 2019-10-12 18:09:52 +01:00
Ben Lopatin 802f018519 Replace bad link with route documentation
Closes gh-173
2019-10-06 12:12:43 -04:00
Ben Lopatin be74c4e5b7 Add GitHub Actions testing workflow (#24)
* Add GitHub Actions testing workflow

* Update named Python versions and dev status

* Use unittest.mock as default mock source

Fall back to package for Python 2.7

* Try installing mock outside of setup

* Switch to GitHub actions shield
2019-10-06 12:04:51 -04:00
Ben Lopatin 9198a1d549 Version bump 0.9.0 2019-10-06 11:43:56 -04:00
Logan Kilpatrick 53cc24744f Update README.rst (#21)
Update README.rst with link to web accessible documentation.
2019-10-06 12:05:50 +11:00
Ben Lopatin 9dcf5832b6 Merge pull request #18 from Lakshmipathi/master
Fix activate and deactivate call
2019-07-21 18:21:15 -04:00
Lakshmipathi.G 10c27d6338 Fix activate and deactivate call
Signed-off-by: Lakshmipathi.G <Lakshmipathi.G@giis.co.in>
2019-07-14 12:43:37 +05:30
Ben Lopatin 89f12f707b Break out of infinite loop for ok responses 2019-04-21 13:52:14 -04:00
Karl Goetz 0cef55a02f Handle HTTP 429, rate limiting
Per the announcement on Discourse meta, global API rate limits have been
introduced to the Discourse API.
This change adds a new DiscourseRateLimitedError class and a retry mechanism on
receipt of a 429.

https://meta.discourse.org/t/global-rate-limits-in-discourse/78612

Closes: #11

----

Added by Maintainer:

Closes: #13
Formatted with black as well
2019-04-21 13:39:34 -04:00
Ben Lopatin faa8895321 Merge pull request #16 from cck197/master
extra methods for deactivate/activate hack; see https://meta.discours…
2019-04-21 13:32:18 -04:00
Ben Lopatin e434edb2ea Merge pull request #17 from orsonmmz/topics
Added category_topics() and delete_topic() methods
2019-01-02 09:37:03 -05:00
Maciej Suminski 9601d96701 Added category_topics() and delete_topic() methods 2019-01-01 14:12:33 +01:00
Christopher Kelly d5ce2d78dc extra methods for deactivate/activate hack; see https://meta.discourse.org/t/creating-active-users-via-the-api-gem/33133/3 2018-12-07 19:37:55 -08:00
Ben Lopatin 84b59d2e4f Merge pull request #15 from cck197/master
groups endpoint moved; see https://meta.discourse.org/t/group-api-emp…
2018-12-07 16:52:56 -05:00
Christopher Kelly 1970e53059 suspend uses suspend_until ISO date string 2018-12-07 12:11:25 -08:00
Christopher Kelly 0c7b60fef8 groups endpoint moved; see https://meta.discourse.org/t/group-api-empty-response/67892 2018-12-07 11:28:21 -08:00
Ben Lopatin de6e758be6 Remove Python 3.7 from Travis testing
From
https://docs.travis-ci.com/user/languages/python/#development-releases-support:

> Recent Python branches require OpenSSL 1.0.2+. As this library is not
> available for Trusty, 3.7, 3.7-dev, 3.8-dev, and nightly do not work (or
> use outdated archive).
2018-10-29 18:35:43 -04:00
Ben Lopatin 0f2efa8e74 Update Python versions supported 2018-10-29 18:31:33 -04:00
Ben Lopatin 44cf317aa9 Fix version cleaning 2018-10-29 18:15:41 -04:00
Ben Lopatin 468f6b58cd Version bump 0.8.0
Closes gh-14
2018-10-29 18:11:35 -04:00
Ben Lopatin c0db7215c9 Format with black 2018-10-29 18:02:58 -04:00
Ben Lopatin b0b277c917 Add PR template 2018-10-29 18:02:19 -04:00
Ben Lopatin 7793f3ae54 Merge pull request #12 from goetzk/patch-1
Increase minimum requests version
2018-02-14 19:25:47 -05:00
Karl Goetz c900fad726 Increase minimum requests version
With the addition of json support in PR #9, the required version of requests has increased to that which introduced json={} syntax.
2018-02-15 11:03:19 +11:00
Ben Lopatin 6c4c40d93c Merge pull request #10 from goetzk/new-api-methods
New api methods
2017-12-13 20:27:58 -05:00
Karl Goetz ff49cc7219 Merge branch 'master' into new-api-methods 2017-11-30 22:25:00 +11:00
Karl Goetz 3e391c38ec Merge branch 'master' into new-api-methods 2017-11-30 22:22:36 +11:00
Karl Goetz 010bfa624c Add docstrings to methods
Requested in #10
2017-11-09 09:25:38 +11:00
Karl Goetz 006b7d416a Revert change to site_settings
Adding a settings arg changed the interface, something I was trying to avoid on
this branch.
Picked up by @bennylope when reviewing #10.
2017-11-09 08:40:16 +11:00
Ben Lopatin 7b3733ca8e Merge pull request #9 from goetzk/json-support
Json support
2017-11-08 12:35:31 -05:00
Alvaro Molina Alvarez a87503eec3 adding support over topics 2017-10-21 16:08:26 +11:00
Alvaro Molina Alvarez f0dd191b58 getting user emails 2017-10-21 16:07:52 +11:00
Alvaro Molina Alvarez 6cffef4e49 Change group_members to handle paging 2017-10-21 16:06:13 +11:00
Alvaro Molina Alvarez 12356819ea adding tags support 2017-10-21 16:04:55 +11:00
Alvaro Molina Alvarez 84016afbc5 adding new client's methods 2017-10-21 16:04:23 +11:00
Alvaro Molina Alvarez e5fe47d0a6 updating client methods 2017-10-21 16:01:59 +11:00
Alvaro Molina Alvarez 2fde21b51f adding trust level lock method 2017-10-21 16:01:38 +11:00
Alvaro Molina Alvarez 2d2e8d1695 adding upload image client method 2017-10-21 16:00:42 +11:00
Alvaro Molina Alvarez 217b606ee7 adding avatar support when a group is added 2017-10-21 15:58:47 +11:00
Alvaro Molina Alvarez fd815ac97b adding color schemes method 2017-10-21 15:58:02 +11:00
Alvaro Molina Alvarez 9fbfa39060 adding new functions to the client 2017-10-21 15:56:44 +11:00
Alvaro Molina Alvarez 3ab8689b6e Add JSON support to more methods
This includes proper support for JSON being added to _put, originally in [1]

[1] a1ac18e852
2017-10-21 15:52:56 +11:00
Alvaro Molina Alvarez 914e22cc55 adding update_avatar client method 2017-10-21 15:47:39 +11:00
Karl Goetz f42a457514 New add_group_members API call
Taken from [1] by Alvaro Molina Alvarez, split to keep merges on a single
topic.
[1] 74414d1429
2017-10-21 15:45:03 +11:00
Karl Goetz 8faa1cfaf9 Group kwargs for Discourse API v1.7
Taken from [1] by Alvaro Molina Alvarez
[1] 74414d1429
2017-10-21 15:42:41 +11:00
Karl Goetz 227f7a3205 Add JSON support to client library
Newer versions of Discourse API use JSON PUTs and POSTs extensively.

This is a modified cherry pick of [1] by Alvaro Molina Alvarez
[1] 74414d1429
2017-10-21 15:40:29 +11:00
Ben Lopatin 17faed6fa7 Update CONTRIBUTING.rst 2017-10-06 09:41:18 -04:00
Ben Lopatin b761d28494 Update CONTRIBUTING.rst 2017-10-06 09:39:36 -04:00
Ben Lopatin 9108939503 Merge pull request #7 from citadelgrad/patch-1
Update client.py
2017-01-17 10:17:31 -05:00
Scott Nixon 8555abf680 Update client.py
Updated create_category method documentation.
2017-01-15 14:03:23 -08:00
Ben Lopatin dd9b7fad19 Merge pull request #6 from Polytechnique-org/master
Adding some API calls, notably on groups and users
2016-10-17 15:53:04 -04:00
Pierre-Alain Dupont a18203c8cb Adds the possibility of deleting a group 2016-10-16 15:38:09 +02:00
wilhelmhb 630b822a9a add method for getting all the available informations about a user 2016-10-15 19:34:02 +02:00
wilhelmhb f0fd17c3a3 fonctions to get data on group members/owners 2016-10-15 17:58:30 +02:00
wilhelmhb f7f1aafc64 add forgotten argument 2016-10-15 16:06:18 +02:00
wilhelmhb e77074c5d4 add possibility to create a group 2016-10-15 15:49:22 +02:00
Ben Lopatin 22bf3b088e Version bump 0.7.0 2016-09-09 10:09:22 -04:00
Ben Lopatin 811453a129 Merge pull request #5 from jdorweiler/master
pass params in data
2016-09-09 10:04:24 -04:00
jddorweiler f8d628909c update test 2016-09-08 14:38:22 -04:00
jddorweiler 507e377a37 use data for posts and put 2016-09-08 14:23:38 -04:00
jddorweiler 0aac8f6628 pass params in data 2016-09-07 15:27:40 -04:00
17 changed files with 977 additions and 255 deletions
+8
View File
@@ -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
+23
View File
@@ -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
+1
View File
@@ -4,6 +4,7 @@ python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
- "pypy"
- "pypy3"
+4
View File
@@ -6,3 +6,7 @@ Ben Lopatin
Daniel Zohar
Matheus Fernandes
Scott Nixon
Jason Dorweiler
Pierre-Alain Dupont
Karl Goetz
Alex Kerney
+20 -2
View File
@@ -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
View File
@@ -3,6 +3,58 @@
Release history
===============
1.1.0
-----
- Added ability to follow redirects in requests
1.0.0
-----
- Authenticate with headers
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
View File
@@ -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
View File
@@ -51,9 +51,9 @@ copyright = u'2014, Marc Sibson'
# built documents.
#
# The short X.Y version.
version = '0.6'
version = '1.1'
# The full version, including alpha/beta/rc tags.
release = '0.6.0'
release = '1.1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
+1 -1
View File
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
__version__ = '0.6.0'
__version__ = "1.1.0"
from pydiscourse.client import DiscourseClient
+695 -130
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+5 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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,
},
)
+1 -1
View File
@@ -1,5 +1,5 @@
[tox]
envlist = py27, py34, py35, pypy, pypy3
envlist = py27, py34, py35, py36, py37, pypy, pypy3
[testenv]
setenv =