Repository: sigmavirus24/github3.py

Issue:
Support for searching commits
Any plans to add support for searching commits?

```
https://developer.github.com/v3/search/#search-commits
```

Discussion:
jacquerie: IMO this is ready for review! Perhaps @omgjlk would like to take a look, given https://github.com/sigmavirus24/github3.py/issues/748#issuecomment-352588562 ?

Patch:
diff --git a/src/github3/github.py b/src/github3/github.py
index ce22a5fb..3cb9f75c 100644
--- a/src/github3/github.py
+++ b/src/github3/github.py
@@ -1669,6 +1669,87 @@ class GitHub(models.GitHubCore):
             number, url, search.CodeSearchResult, self, params, etag, headers
         )
 
+    def search_commits(self, query, sort=None, order=None, per_page=None,
+                       text_match=False, number=-1, etag=None):
+        """Find commits via the commits search API.
+
+        The query can contain any combination of the following supported
+        qualifiers:
+
+        - ``author`` Matches commits authored by the given username.
+          Example: ``author:defunkt``.
+        - ``committer`` Matches commits committed by the given username.
+          Example: ``committer:defunkt``.
+        - ``author-name`` Matches commits authored by a user with the given
+          name. Example: ``author-name:wanstrath``.
+        - ``committer-name`` Matches commits committed by a user with the given
+          name. Example: ``committer-name:wanstrath``.
+        - ``author-email`` Matches commits authored by a user with the given
+          email. Example: ``author-email:chris@github.com``.
+        - ``committer-email`` Matches commits committed by a user with the
+          given email. Example: ``committer-email:chris@github.com``.
+        - ``author-date`` Matches commits authored within the specified date
+          range. Example: ``author-date:<2016-01-01``.
+        - ``committer-date`` Matches commits committed within the specified
+          date range. Example: ``committer-date:>2016-01-01``.
+        - ``merge`` Matches merge commits when set to to ``true``, excludes
+          them when set to ``false``.
+        - ``hash`` Matches commits with the specified hash. Example:
+          ``hash:124a9a0ee1d8f1e15e833aff432fbb3b02632105``.
+        - ``parent`` Matches commits whose parent has the specified hash.
+          Example: ``parent:124a9a0ee1d8f1e15e833aff432fbb3b02632105``.
+        - ``tree`` Matches commits with the specified tree hash. Example:
+          ``tree:99ca967``.
+        - ``is`` Matches public repositories when set to ``public``, private
+          repositories when set to ``private``.
+        - ``user`` or ``org`` or ``repo`` Limits the search to a specific user,
+          organization, or repository.
+
+        For more information about these qualifiers, see: https://git.io/vb7XQ
+
+        :param str query:
+            (required), a valid query as described above, e.g.,
+            ``css repo:octocat/Spoon-Knife``
+        :param str sort:
+            (optional), how the results should be sorted;
+            options: ``author-date``, ``committer-date``;
+            default: best match
+        :param str order:
+            (optional), the direction of the sorted results,
+            options: ``asc``, ``desc``; default: ``desc``
+        :param int per_page:
+            (optional)
+        :param int number:
+            (optional), number of commits to return.
+            Default: -1, returns all available commits
+        :param str etag:
+            (optional), previous ETag header value
+        :return:
+            generator of commit search results
+        :rtype:
+            :class:`~github3.search.commits.CommitSearchResult`
+        """
+        params = {'q': query}
+        headers = {'Accept': 'application/vnd.github.cloak-preview'}
+
+        if sort in ('author-date', 'committer-date'):
+            params['sort'] = sort
+
+        if sort and order in ('asc', 'desc'):
+            params['order'] = order
+
+        if text_match:
+            headers['Accept'] = ', '.join([
+                headers['Accept'],
+                'application/vnd.github.v3.full.text-match+json'
+            ])
+
+        url = self._build_url('search', 'commits')
+        return structs.SearchIterator(
+            number, url, search.CommitSearchResult,
+            self, params, etag, headers
+        )
+
     def search_issues(self, query, sort=None, order=None, per_page=None,
                       text_match=False, number=-1, etag=None):
         """Find issues by state and keyword.
diff --git a/src/github3/search/__init__.py b/src/github3/search/__init__.py
index 97f776b8..37ea15fa 100644
--- a/src/github3/search/__init__.py
+++ b/src/github3/search/__init__.py
@@ -1,8 +1,14 @@
 from .code import CodeSearchResult
+from .commit import CommitSearchResult
 from .issue import IssueSearchResult
 from .repository import RepositorySearchResult
 from .user import UserSearchResult
 
 
-__all__ = [CodeSearchResult, IssueSearchResult, RepositorySearchResult,
-           UserSearchResult]
+__all__ = (
+    'CodeSearchResult',
+    'CommitSearchResult',
+    'IssueSearchResult',
+    'RepositorySearchResult',
+    'UserSearchResult',
+)
diff --git a/src/github3/search/commit.py b/src/github3/search/commit.py
new file mode 100644
index 00000000..7f694146
--- /dev/null
+++ b/src/github3/search/commit.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+"""Commit search results implementation."""
+from __future__ import unicode_literals
+
+from .. import git
+from .. import models
+from .. import repos
+from .. import users
+
+
+class CommitSearchResult(models.GitHubCore):
+    """A representation of a commit search result from the API.
+
+    This object has the following attributes:
+
+    .. attribute:: author
+
+        A :class:`~github3.users.ShortUser` representing the user who
+        authored the found commit.
+
+    .. attribute:: comments_url
+
+        The URL to retrieve the comments on the found commit from the API.
+
+    .. attribute:: commit
+
+        A :class:`~github3.git.ShortCommit` representing the found commit.
+
+    .. attribute:: committer
+
+        A :class:`~github3.users.ShortUser` representing the user who
+        committed the found commit.
+
+    .. attribute:: html_url
+
+        The URL to view the found commit in a browser.
+
+    .. attribute:: repository
+
+        A :class:`~github3.repos.repo.ShortRepository` representing the
+        repository in which the commit was found.
+
+    .. attribute:: score
+
+        The confidence score assigned to the result.
+
+    .. attribute:: sha
+
+        The SHA1 of the found commit.
+
+    .. attribute:: text_matches
+
+        A list of the text matches in the commit that generated this result.
+
+        .. note::
+
+            To receive these, you must pass ``text_match=True`` to
+            :meth:`~github3.github.GitHub.search_commit`.
+    """
+
+    def _update_attributes(self, data):
+        self._api = data['url']
+        self.author = users.ShortUser(data['author'], self)
+        self.comments_url = data['comments_url']
+        self.commit = git.ShortCommit(data['commit'], self)
+        self.committer = users.ShortUser(data['committer'], self)
+        self.html_url = data['html_url']
+        self.repository = repos.ShortRepository(data['repository'], self)
+        self.score = data['score']
+        self.sha = data['sha']
+        self.text_matches = data.get('text_matches', [])
+
+    def _repr(self):
+        return '<CommitSearchResult [{0}]>'.format(self.sha[:7])


Test patch:
diff --git a/tests/cassettes/GitHub_search_commits.json b/tests/cassettes/GitHub_search_commits.json
new file mode 100644
index 00000000..39082376
--- /dev/null
+++ b/tests/cassettes/GitHub_search_commits.json
@@ -0,0 +1,1 @@
+{"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate"], "Accept": ["application/vnd.github.cloak-preview"], "User-Agent": ["github3.py/1.1.0"], "Accept-Charset": ["utf-8"], "Connection": ["keep-alive"], "Content-Type": ["application/json"]}, "method": "GET", "uri": "https://api.github.com/search/commits?q=css+repo%3Aoctocat%2FSpoon-Knife&per_page=100"}, "response": {"body": {"string": "", "base64_string": "H4sIAAAAAAAAA+1ZXW/iRhT9K4jXJtjGhthI0XbVpKttRaKk7HaTaoXG4zEesD3WzDgUrPz33hkbA1EIYLbtS15CMD5n7sfM9bnXRVsyieIxZnkq2wPrrE1TzJIsJpKMORF5LEV7EKJYEPhJkgS+/VW0cx63B+1IykwMDANltDOhMsr9DmANTjImDIYlw0gaf2SMpee/pzQkBvyaUCkM33cwdgPb7xLL8W0U9oKw73nuRT8InDC0EQ5M078I22dtESFY6QhAygIypgGAhlcP/buuJ/0/Y3M4Gs6HV9e94TSjD9P76cMymg27nxePo2vzYXoTPU6/zh6nD73b0cS+Se5nN58ek2H31+nj1cf5wzRIwJJIJvF42/ENp3e7e4y3KkAkleLFOv9agHVG1ILgX5mc9qBxdiEaTTKMchkxrtYNkCSQt65pOedm99x0RpYzsN2B3T833YFpgpEpStQto4i0bssNBhdJgqjaj1UOfk7ZPCKcqM3Yfl45Jslra1jdkdUbWO6g1ztpjYQIgSbKtF84ATdaQi5iIjpYiBZKg1aeKeeC1v31x6vhNdgsOYG7T4q1ohAG6tse8fqhZ9ruhXcR+qhPTKfvhZ6DHN/E2Ol5Ieq79Vk6GFCFDrZHXR4suLbOV8wmNF3HHVZQ567n2l0bCsnmQbzrf/12E+PpF2e4/Lwc3l1ews3oCUnEX251fVHYVUHJBeGYpRKM0LUlN0r6D0+XDlBMeEWiDzxceLMwKbK6MMHNB51ouC9kcczmgH1p63bd26I3alBNQNPJ8QQAKgwmYT+PFf+zcpqKfRVi2xQNKAz1AZVRUQiIMSfBMeZUEDBmnoIdhS7zmiv3BeY0k5SlR0VoCwhEjE9QSpfoaCIAqgKmnzvHuKQBACRP+2vudkRLRGFknD4hvFBh4AQT+gQxPZ7tBRTI5CJTteQLZFxFGB68YxQk6rDpR/HLovZ+ELcEyPtBfD+I/9lBzBDXAuoHyGJkm9jyiB1a9kW/h2zfcd2ud+G6Praw7/oO8XqYEPXgO+jR9Yr2No5Yo5TeBwOev1clmErGF0rbqIeyZZum5XW35cD1/Db+LcafvCX6dv+E09nfw+XH+XA0W4BrlcTb6BvgYpjH8bj65RWpDXfox5Ja9b0YvhfDd1Xy/6gSrYdUE1dNDI4uU3CQA1LLSd3pUdFSQq0FnyHjrYAkoDMl1zKxleUcNBwRLZbGi44qFIzP6uXf7AZ2jikqlj1ScicclD+fKVk5I4vGHApbGPC3EuwYWhDkM/Ca7WtDdhu2RVJAr77mVApWEpQ0NliDgSRibNaYRIOBhAqRk4OE9G5nNYcwVko9zRO/bJ8O0ee7aUs02IiEoJMUOvC3O47dTDVBYaw6O5+jFEfNKVf4wij/01lFk8YmKixQ+DHzG3OoqZAmKAwQFGX/KsenWKUYFX6LkJPwJBMVvibUk5WmedXmKYKaDlpnCSluzLjCG0UVwRilkxzGTY0ZawLIrmrsJ2i5d7yxeyevGYBOTWs49fPTCtWaQ1lYThjg/DZ2eINiTahHFk3TvDm90G7r0XZTtgq+taVPpNwYx9a0P2K+rLqdwljX07JYV8xN/a+q9cq+Tf5q/HdCaPX4UBjFTxmSkapAsIxq25oaW8GNwkcwi+l0OkVEkJ6rJYSfcCpLNNAgjiOYJTW1r1jhQYkkSOpZXajMC6BJihkKGseyJgCyMmVNbSzRm3nOoMVrbJgGb7IlFMbvkqXNa+SaYZM3ZZKGFF5s7Z927i6YWyTFBwHv3MgZiuMz2JWSYgr7FOa+KmMg+kjzqJRoMB/empQzypjAlm0cZU5KfGGUY+SAZDFbnPTGaoNCvasRmHFoH+yO0+1Z8B7h+fvzP+6CgBWjHAAA", "encoding": "utf-8"}, "headers": {"Status": ["200 OK"], "X-RateLimit-Remaining": ["9"], "X-GitHub-Media-Type": ["github.cloak-preview"], "X-Runtime-rack": ["0.066329"], "Content-Security-Policy": ["default-src 'none'"], "X-Content-Type-Options": ["nosniff"], "Access-Control-Expose-Headers": ["ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval"], "Transfer-Encoding": ["chunked"], "X-GitHub-Request-Id": ["BFE0:5172:2C4F2:5A0E2:5B64C901"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "X-XSS-Protection": ["1; mode=block"], "Server": ["GitHub.com"], "X-RateLimit-Limit": ["10"], "Cache-Control": ["no-cache"], "Date": ["Fri, 03 Aug 2018 21:28:33 GMT"], "Access-Control-Allow-Origin": ["*"], "Referrer-Policy": ["origin-when-cross-origin, strict-origin-when-cross-origin"], "Content-Type": ["application/json; charset=utf-8"], "X-Frame-Options": ["deny"], "Content-Encoding": ["gzip"], "X-RateLimit-Reset": ["1533331773"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.github.com/search/commits?q=css+repo%3Aoctocat%2FSpoon-Knife&per_page=100"}, "recorded_at": "2018-08-03T21:28:33"}], "recorded_with": "betamax/0.8.1"}
\ No newline at end of file
diff --git a/tests/cassettes/GitHub_search_commits_with_text_match.json b/tests/cassettes/GitHub_search_commits_with_text_match.json
new file mode 100644
index 00000000..1ec94cb5
--- /dev/null
+++ b/tests/cassettes/GitHub_search_commits_with_text_match.json
@@ -0,0 +1,1 @@
+{"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate"], "Accept": ["application/vnd.github.cloak-preview, application/vnd.github.v3.full.text-match+json"], "User-Agent": ["github3.py/1.1.0"], "Accept-Charset": ["utf-8"], "Connection": ["keep-alive"], "Content-Type": ["application/json"]}, "method": "GET", "uri": "https://api.github.com/search/commits?q=css+repo%3Aoctocat%2FSpoon-Knife&per_page=100"}, "response": {"body": {"string": "", "base64_string": "H4sIAAAAAAAAA+1ZbW/bNhD+K4a/LrEkS7YlA0VXNFnRDUrRzO2aDIVBUZRFWxIFkoprC/nvO1KybAd1Y8vd9iUIECeynof3wjveHcuuZBIlU8yKTHbH1kWXZpileUIkmXIiikSK7jhCiSDwlSQp/Pd32S140h13YylzMTYMlNPejMq4CHqANTjJmTAYlgwjafyZM5Zd/pHRiBjwbUqlMILAwdgN7aBPLCewUTQIo6HnuaNhGDpRZCMcmmYwiroXXREjWOkEQMZCMqUhgPyru+HHvieDvxLTn/hL/+p64M9zeje/nd+t44Xff7+6n1ybd/Ob+H7+eXE/vxt8mMzsm/R2cfPuPvX7v83vr94s7+ZhCpLEMk2m+4rvKH1Y3VO0VQYimRRP1vnXDKw9ohYE/SrndMetvQvWaONhVMiYcbVuiCQBv/VNy7k0+5emM7Gcse2O7eGl6Y5NE4TMUKpemcSk86HaYPCQpIiq/Vj74NeMLWPCidqM3ceNYpJ8bw2rP7EGY8sdDwZnrZESIdBMifaWE1CjI+QqIaKHheigLOwUuVIu7Nxev7nyr0FmyQm8fZatFYUw0ND2iDeMPNN2R94oCtCQmM7QizwHOYGJsTPwIjR0m1g6GlCbDrZHkx4seLb1V8JmNNvaHVZQcTdw7b4NiWQ3ED8OP3+5SfD8k+Ov36/9j69ewcvoAUnEn251/VDYdUIpBOGYZRKE0LmlMCr61w+vHKCY8ZpEBzw8+GFiUmRNYoKXj4poeC9iScKWgH0q637e26M3GlBDQLPZ6QQAKg0mYT9PFf+jUpqK5zLEvigaUBrqAzKjohBgY07CU8SpISDMMgM5Sp3mNVcRCMxpLinLTrLQHhCIGJ+hjK7RyUQAVAlMnzunqKQBACQPz+fcfYtWiNLIOX1AeKXMwAkm9AFsejrbEyiQyVWucskn8LiyMBy8UxSmKtj0Ufw0qb0E4l4B8hKIL4H4nwVijrguoH5CWYxsE1sesSPLHg0HyA4c1+17I9cNsIUDN3CIN8CEqIPvqKPrO7W3ccIaVel9NODxa52CqWR8pWobdShbtmlaXn+/HLhefkh+T/A7b42+3D7gbPHNX79Z+pPFClSrS7ydvgEeRkWSTOtvvlNqwxv6WFKrviTDl2T4UpX8P1WJrodUE1dPDE5OUxDIIWnKSd3pUdFRhVoHPiPGOyFJoc6UXJeJnbzgUMMR0WFZsuqpRMH4oln+h93AwTFFzfJMKXkQDpU/X6iyckFWrTkUtjTgd12wY2hBUMBAa/ZcG3JYsD2SEnr1LaeqYCVBaWuBNRhIYsYWrUk0GEioEAU5qpA+rKzmEMamUs+KNKjap2Pq88O0FRpkRELQWQYd+I87jsNMDUFpbDq7gKMMx+0pN/jSqP7SXkWz1iIqLFAECQtac6ipkCYoDSgoqv5VTs+RSjEq/B4hJ9FZIip8Q6gnK239qsVTBA0dtM4SXNyacYM3ytqCCcpmBYybWjM2BOBd1djP0PrZ8cbhnbxlADo1reE0KM5LVFsOJWE1YYD4ba3wDsWWUI8s2rp5d3qh1daj7bZsNXxvS59JuTOObWh/xnxZdTulsc2nVbKumdvqX2frjXy7/PX47wzT6vGhMMpfciRjlYFgGdW2tRW2hhtlgGAW0+v1ypggPVdLCT8jKis00CCOY5gltZWv3OChEkmR1LO6SIkXQpOUMBS2tmVDAGSVy9rKWKF3/ZxDi9daMA3eZUspjN8ly9rnyC3DLm/GJI0oXGw9P+08nDD3SMrXAu7cyAVKkgvYlZJiCvsU5r7KY1D0kfZWqdAgPtyaVDPKhMCWbW1lTip8aVRj5JDkCVuddWO1Q6HuagRmHNoHu+f0hwO3b6uq9Jucwi5WdZEesrBgTrBsu+/qpHrKpVy9YD2TfasTvV/f91x0c85ywiUMPCD6q0sg6EI4mimzHH8ftKuhUhmQcHekwiwLKdaqW86FNfr6WP38AyuRIHW5HQAA", "encoding": "utf-8"}, "headers": {"Status": ["200 OK"], "X-RateLimit-Remaining": ["8"], "X-GitHub-Media-Type": ["github.cloak-preview; param=full.text-match"], "X-Runtime-rack": ["0.043624"], "Content-Security-Policy": ["default-src 'none'"], "X-Content-Type-Options": ["nosniff"], "Access-Control-Expose-Headers": ["ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval"], "Transfer-Encoding": ["chunked"], "X-GitHub-Request-Id": ["B258:516C:12CE5:2A7F2:5B64C902"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "X-XSS-Protection": ["1; mode=block"], "Server": ["GitHub.com"], "X-RateLimit-Limit": ["10"], "Cache-Control": ["no-cache"], "Date": ["Fri, 03 Aug 2018 21:28:34 GMT"], "Access-Control-Allow-Origin": ["*"], "Referrer-Policy": ["origin-when-cross-origin, strict-origin-when-cross-origin"], "Content-Type": ["application/json; charset=utf-8"], "X-Frame-Options": ["deny"], "Content-Encoding": ["gzip"], "X-RateLimit-Reset": ["1533331773"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.github.com/search/commits?q=css+repo%3Aoctocat%2FSpoon-Knife&per_page=100"}, "recorded_at": "2018-08-03T21:28:34"}], "recorded_with": "betamax/0.8.1"}
\ No newline at end of file
diff --git a/tests/integration/test_github.py b/tests/integration/test_github.py
index b88c275f..b827194b 100644
--- a/tests/integration/test_github.py
+++ b/tests/integration/test_github.py
@@ -523,6 +523,27 @@ class TestGitHub(IntegrationHelper):
         assert isinstance(code_result, github3.search.CodeSearchResult)
         assert len(code_result.text_matches) > 0
 
+    def test_search_commits(self):
+        """Test the ability to search for commits."""
+        cassette_name = self.cassette_name('search_commits')
+        with self.recorder.use_cassette(cassette_name):
+            result_iterator = self.gh.search_commits(
+                'css repo:octocat/Spoon-Knife')
+            commit_result = next(result_iterator)
+
+        assert isinstance(commit_result, github3.search.CommitSearchResult)
+
+    def test_search_commits_with_text_match(self):
+        """Test the ability to search for commits  with text matches."""
+        cassette_name = self.cassette_name('search_commits_with_text_match')
+        with self.recorder.use_cassette(cassette_name):
+            result_iterator = self.gh.search_commits(
+                'css repo:octocat/Spoon-Knife', text_match=True)
+            commit_result = next(result_iterator)
+
+        assert isinstance(commit_result, github3.search.CommitSearchResult)
+        assert len(commit_result.text_matches) > 0
+
     def test_search_users(self):
         """Test the ability to use the user search endpoint."""
         cassette_name = self.cassette_name('search_users')
diff --git a/tests/unit/helper.py b/tests/unit/helper.py
index a38bf53e..9a819e98 100644
--- a/tests/unit/helper.py
+++ b/tests/unit/helper.py
@@ -241,6 +241,24 @@ class UnitIteratorHelper(UnitHelper):
         self.get_json_mock.stop()
 
 
+class UnitSearchIteratorHelper(UnitIteratorHelper):
+
+    """Base class for search iterator based unit tests."""
+
+    def patch_get_json(self):
+        """Patch a SearchIterator's _get_json method."""
+        self.get_json_mock = mock.patch.object(
+            github3.structs.SearchIterator, '_get_json'
+        )
+        self.patched_get_json = self.get_json_mock.start()
+        self.patched_get_json.return_value = []
+
+    def setUp(self):
+        """Use UnitIteratorHelper's setUp and patch _get_json."""
+        super(UnitSearchIteratorHelper, self).setUp()
+        self.patch_get_json()
+
+
 class UnitRequiresAuthenticationHelper(UnitHelper):
 
     """Helper for unit tests that demonstrate authentication is required."""
diff --git a/tests/unit/test_github.py b/tests/unit/test_github.py
index c4af3a6f..44e97243 100644
--- a/tests/unit/test_github.py
+++ b/tests/unit/test_github.py
@@ -1295,6 +1295,85 @@ class TestGitHubIterators(helper.UnitIteratorHelper):
         )
 
 
+class TestGitHubSearchIterators(helper.UnitSearchIteratorHelper):
+
+    """Test GitHub methods that return search iterators."""
+
+    described_class = GitHub
+    example_data = None
+
+    def test_search_code(self):
+        """Verify the request to search for code."""
+        i = self.instance.search_code(
+            'addClass in:file language:js repo:jquery/jquery')
+        self.get_next(i)
+
+        self.session.get.assert_called_once_with(
+            url_for('search/code'),
+            params={'per_page': 100,
+                    'q': 'addClass in:file language:js repo:jquery/jquery'},
+            headers={}
+        )
+
+    def test_search_commits(self):
+        """Verify the request to search for commits."""
+        i = self.instance.search_commits(
+            'css repo:octocat/Spoon-Knife')
+        self.get_next(i)
+
+        self.session.get.assert_called_once_with(
+            url_for('search/commits'),
+            params={'per_page': 100,
+                    'q': 'css repo:octocat/Spoon-Knife'},
+            headers={'Accept': 'application/vnd.github.cloak-preview'}
+        )
+
+    def test_search_issues(self):
+        """Verify the request to search for issues."""
+        i = self.instance.search_issues(
+            'windows label:bug language:python state:open',
+            sort='created', order='asc')
+        self.get_next(i)
+
+        self.session.get.assert_called_once_with(
+            url_for('search/issues'),
+            params={'order': 'asc',
+                    'per_page': 100,
+                    'q': 'windows label:bug language:python state:open',
+                    'sort': 'created'},
+            headers={}
+        )
+
+    def test_search_repositories(self):
+        """Verify the request to search for repositories."""
+        i = self.instance.search_repositories(
+            'tetris language:assembly',
+            sort='stars', order='asc')
+        self.get_next(i)
+
+        self.session.get.assert_called_once_with(
+            url_for('search/repositories'),
+            params={'order': 'asc',
+                    'per_page': 100,
+                    'q': 'tetris language:assembly',
+                    'sort': 'stars'},
+            headers={}
+        )
+
+    def test_search_users(self):
+        """Verify the request to search for users."""
+        i = self.instance.search_users(
+            'tom repos:>42 followers:>1000')
+        self.get_next(i)
+
+        self.session.get.assert_called_once_with(
+            url_for('search/users'),
+            params={'per_page': 100,
+                    'q': 'tom repos:>42 followers:>1000'},
+            headers={}
+        )
+
+
 class TestGitHubRequiresAuthentication(
         helper.UnitRequiresAuthenticationHelper):