Repository: Sage-Bionetworks/challengeutils

Issue:
Add tests to cli
```
---------- coverage: platform darwin, python 3.7.6-final-0 -----------
Name                                                 Stmts   Miss  Cover
------------------------------------------------------------------------
challengeutils/__init__.py                               3      0   100%
challengeutils/__main__.py                             176    176     0%
challengeutils/__version__.py                            1      0   100%
challengeutils/challenge.py                              6      6     0%
challengeutils/createchallenge.py                      100     14    86%
challengeutils/discussion.py                           127     28    78%
challengeutils/download_current_lead_submission.py      35      2    94%
challengeutils/evaluation_queue.py                      34      0   100%
challengeutils/helpers.py                               67     39    42%
challengeutils/mirrorwiki.py                            51     51     0%
challengeutils/permissions.py                           29      4    86%
challengeutils/utils.py                                202     56    72%
challengeutils/writeup_attacher.py                      18     18     0%
------------------------------------------------------------------------
TOTAL                                                  849    394    54%
```

Discussion:

Patch:
diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml
index 5db2751..6589b67 100644
--- a/.github/workflows/pythonapp.yml
+++ b/.github/workflows/pythonapp.yml
@@ -1,7 +1,7 @@
 # This workflow will install Python dependencies, run tests and lint with a single version of Python
 # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
 
-name: challengeutils test
+name: build
 
 on: [pull_request]
 
@@ -21,15 +21,22 @@ jobs:
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
+        pip install flake8 pytest pytest-cov
         pip install .
         if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
-   # - name: Lint with flake8
-   #   run: |
+    - name: Lint with flake8
+      run: |
         # stop the build if there are Python syntax errors or undefined names
-        # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
         # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
-        # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
     - name: Test with pytest
       run: |
-        pip install pytest
-        pytest
+        pytest tests/ --doctest-modules --cov=challengeutils --cov-report=html
+    - name: Upload pytest test results
+      uses: actions/upload-artifact@master
+      with:
+        name: pytest-results-${{ matrix.python-version }}
+        path: htmlcov
+      # Use always() to always run this step to publish test results when there are test failures
+      if: ${{ always() }}
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 261eeb9..8b9a0c6 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright [yyyy] [name of copyright owner]
+   Copyright 2017 Sage Bionetworks
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/challengeutils/challenge.py b/challengeutils/challenge.py
index bb18b9a..a488a3d 100644
--- a/challengeutils/challenge.py
+++ b/challengeutils/challenge.py
@@ -36,10 +36,10 @@ class ChallengeApi:
             A synapseservices.Challenge
 
         """
-        new_challenge = Challenge(participantTeamId=teamid,
-                                  projectId=projectid)
+        challenge_object = {'participantTeamId': teamid,
+                            'projectId': projectid}
         challenge = self.syn.restPOST('/challenge',
-                                      str(new_challenge))
+                                      json.dumps(challenge_object))
         return Challenge(**challenge)
 
     def get_registered_challenges(self,
@@ -93,11 +93,11 @@ class ChallengeApi:
             A synapseservices.Challenge
 
         """
-        new_challenge = Challenge(id=challengeid,
-                                  participantTeamId=teamid,
-                                  projectId=projectid)
+        challenge_object = {'id': challengeid,
+                            'participantTeamId': teamid,
+                            'projectId': projectid}
         challenge = self.syn.restPUT(f'/challenge/{challengeid}',
-                                     str(new_challenge))
+                                     json.dumps(challenge_object))
         return Challenge(**challenge)
 
     def delete_challenge(self, challengeid: str):
diff --git a/challengeutils/createchallenge.py b/challengeutils/createchallenge.py
index 8595694..83b4ca6 100644
--- a/challengeutils/createchallenge.py
+++ b/challengeutils/createchallenge.py
@@ -281,8 +281,8 @@ def main(syn, challenge_name, live_site=None):
     else:
         project_live = syn.get(live_site)
 
-    challenge = create_challenge_widget(syn, project_live,
-                                        teams['team_part_id'])
+    challenge_obj = create_challenge_widget(syn, project_live,
+                                            teams['team_part_id'])
     create_evaluation_queue(syn, '%s Project Submission' % challenge_name,
                             'Project Submission',
                             project_live.id)
@@ -304,7 +304,7 @@ def main(syn, challenge_name, live_site=None):
     for page in new_wikiids:
         wikipage = syn.getWiki(project_staging, page['id'])
         wikipage.markdown = _update_wikipage_string(wikipage.markdown,
-                                                    challenge.id,
+                                                    challenge_obj.id,
                                                     teams['team_part_id'],
                                                     challenge_name,
                                                     project_live.id)
diff --git a/challengeutils/discussion.py b/challengeutils/discussion.py
index 1a32828..46b507e 100644
--- a/challengeutils/discussion.py
+++ b/challengeutils/discussion.py
@@ -357,6 +357,23 @@ def create_thread_reply(syn, threadid, message):
 
 
 def copy_thread(syn, thread, project):
+    """Copies a discussion thread and its replies to a project
+
+    Args:
+        syn: synapse object
+        thread: Synapse Thread
+        project: Synapse Project or its id to copy thread to
+
+    Returns:
+        dict: Thread bundle
+    """
+    new_thread_obj = _copy_thread(syn, thread, project)
+    thread_replies = get_thread_replies(syn, thread['id'])
+    for reply in thread_replies:
+        copy_reply(syn, reply, new_thread_obj['id'])
+
+
+def _copy_thread(syn, thread, project):
     """Copies a discussion thread to a project
 
     Args:
@@ -374,8 +391,9 @@ def copy_thread(syn, thread, project):
     on_behalf_of = "On behalf of @{user}\n\n".format(user=username)
     text = get_thread_text(syn, thread['messageKey'])
     new_thread_text = on_behalf_of + text
+    new_thread_obj = create_thread(syn, projectid, title, new_thread_text)
 
-    return create_thread(syn, projectid, title, new_thread_text)
+    return new_thread_obj
 
 
 def copy_reply(syn, reply, thread):
@@ -408,7 +426,4 @@ def copy_forum(syn, project, new_project):
     """
     threads = get_forum_threads(syn, project)
     for thread in threads:
-        new_thread_obj = copy_thread(syn, thread, new_project)
-        thread_replies = get_thread_replies(syn, thread['id'])
-        for reply in thread_replies:
-            copy_reply(syn, reply, new_thread_obj['id'])
+        copy_thread(syn, thread, new_project)
diff --git a/challengeutils/evaluation_queue.py b/challengeutils/evaluation_queue.py
index de14d67..fdc28e2 100644
--- a/challengeutils/evaluation_queue.py
+++ b/challengeutils/evaluation_queue.py
@@ -1,6 +1,8 @@
 """Evaluation Queue helper functions"""
 import time
 
+from synapseclient import Synapse
+
 
 def _convert_date_to_epoch(date_string):
     """Converts local time dates in YEAR-MM-DDTHH:MM:SS format
@@ -83,7 +85,7 @@ def _create_quota(round_start: str = None, round_end: str = None,
     return quota
 
 
-def set_evaluation_quota(syn: 'Synapse', evalid: int, **kwargs):
+def set_evaluation_quota(syn: Synapse, evalid: int, **kwargs):
     """Sets evaluation submission limit quota.  This WILL erase any old quota
     you had previously set. Note - round_start must be specified with either
     round_end or round_duration and number_of_rounds must be defined for the
diff --git a/challengeutils/utils.py b/challengeutils/utils.py
index fd71c2b..d10615e 100644
--- a/challengeutils/utils.py
+++ b/challengeutils/utils.py
@@ -244,44 +244,6 @@ def update_all_submissions_annotation_acl(syn, evaluationid, annotations,
         syn.store(new_status)
 
 
-def invite_member_to_team(syn, team, user=None, email=None, message=None):
-    """
-    Invite members to a team
-
-    Args:
-        syn: Synapse object
-        team: Synapse Team id or name
-        user: Synapse username or profile id
-        email: Email of user, do not specify both email and user,
-               but must specify one
-        message: Message for people getting invited to the team
-    """
-    teamid = syn.getTeam(team)['id']
-    is_member = False
-    invite = {'teamId': str(teamid)}
-
-    if email is None:
-        userid = syn.getUserProfile(user)['ownerId']
-        request = \
-            "/team/{teamId}/member/{individualId}/membershipStatus".format(
-                teamId=str(teamid),
-                individualId=str(userid))
-        membership_status = syn.restGET(request)
-        is_member = membership_status['isMember']
-        invite['inviteeId'] = str(userid)
-    else:
-        invite['inviteeEmail'] = email
-
-    if message is not None:
-        invite['message'] = message
-
-    if not is_member:
-        invite = syn.restPOST("/membershipInvitation", body=json.dumps(invite))
-        return invite
-
-    return None
-
-
 def change_submission_status(syn, submissionid, status='RECEIVED'):
     '''
     Function to change a submission status


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/synapseservices/test_challenge_obj.py b/tests/synapseservices/test_challenge_obj.py
new file mode 100644
index 0000000..dbcc459
--- /dev/null
+++ b/tests/synapseservices/test_challenge_obj.py
@@ -0,0 +1,12 @@
+"""Test challeng service"""
+from challengeutils.synapseservices.challenge import Challenge
+
+
+def test_challenge_obj():
+    """Tests that a challenge object can be instantiated"""
+    challenge_dict = {'id': 'challenge_1',
+                      'projectId': 'project_1',
+                      'participantTeamId': 'team_1',
+                      'etag': 'etag_1'}
+    challenge_obj = Challenge(**challenge_dict)
+    assert challenge_obj.to_dict() == challenge_dict
diff --git a/tests/test_challenge.py b/tests/test_challenge.py
index 882a569..4ba86d5 100644
--- a/tests/test_challenge.py
+++ b/tests/test_challenge.py
@@ -11,16 +11,6 @@ from challengeutils.synapseservices.challenge import Challenge
 from challengeutils.challenge import ChallengeApi
 
 
-def test_challenge_obj():
-    """Tests that a challenge object can be instantiated"""
-    challenge_dict = {'id': 'challenge_1',
-                      'projectId': 'project_1',
-                      'participantTeamId': 'team_1',
-                      'etag': 'etag_1'}
-    challenge_obj = Challenge(**challenge_dict)
-    assert challenge_obj.to_dict() == challenge_dict
-
-
 class TestChallengeApi:
     """Tests ChallengeApi class"""
     def setup(self):
@@ -56,6 +46,8 @@ class TestChallengeApi:
         """Tests that a challenge object can be instantiated"""
         new_challenge = Challenge(projectId=self.projectid,
                                   participantTeamId=self.teamid)
+        challenge_object = {'participantTeamId': self.teamid,
+                            'projectId': self.projectid}
         with patch.object(self.syn, "restPOST",
                           return_value=self.challenge_dict) as patch_restpost:
             challenge_obj = self.challenge_api.create_challenge(
@@ -63,8 +55,9 @@ class TestChallengeApi:
                 projectid=self.projectid
             )
             assert challenge_obj == self.expected
-            patch_restpost.assert_called_once_with('/challenge',
-                                                   str(new_challenge))
+            patch_restpost.assert_called_once_with(
+                '/challenge', json.dumps(challenge_object)
+            )
 
     def test_get_challenge__with_id(self):
         """Tests getting challenge with challenge id"""
@@ -92,6 +85,9 @@ class TestChallengeApi:
 
     def test_update_challenge(self):
         """Tests updating Challenge"""
+        challenge_dict = {"id": self.challengeid,
+                          "participantTeamId": self.teamid,
+                          "projectId": self.projectid}
         with patch.object(self.syn, "restPUT",
                           return_value=self.challenge_dict) as patch_restput:
             challenge_obj = self.challenge_api.update_challenge(
@@ -102,7 +98,7 @@ class TestChallengeApi:
             assert challenge_obj == self.expected
             patch_restput.assert_called_once_with(
                 f'/challenge/{self.challengeid}',
-                str(self.input)
+                json.dumps(challenge_dict)
             )
 
     def test_delete_challenge(self):
diff --git a/tests/test_discussion.py b/tests/test_discussion.py
index 917b231..ad2e42a 100644
--- a/tests/test_discussion.py
+++ b/tests/test_discussion.py
@@ -3,6 +3,7 @@ Test challengeutils.discussion functions
 '''
 import json
 from unittest import mock
+from unittest.mock import patch
 import uuid
 
 import requests
@@ -211,7 +212,7 @@ def test_get_entity_threads():
         assert entity_threads == response
 
 
-def test_copy_thread():
+def test__copy_thread():
     """Tests copying of threads"""
     profile = synapseclient.UserProfile(ownerId="test",
                                         userName="foobar")
@@ -225,7 +226,7 @@ def test_copy_thread():
                            return_value=thread_text) as patch_thread_text,\
          mock.patch.object(discussion, "create_thread",
                            return_value=THREAD_OBJ) as patch_create_thread:
-        thread = discussion.copy_thread(syn, THREAD_OBJ, PROJECTID)
+        thread = discussion._copy_thread(syn, THREAD_OBJ, PROJECTID)
         patch_getuserprofile.assert_called_once_with(THREAD_OBJ['createdBy'])
         patch_thread_text.assert_called_once_with(syn, THREAD_OBJ['messageKey'])
         patch_create_thread.assert_called_once_with(syn, PROJECTID,
@@ -234,6 +235,20 @@ def test_copy_thread():
         assert thread == THREAD_OBJ
 
 
+def test_copy_thread():
+    """Tests copying thread and replies"""
+    with patch.object(discussion, "_copy_thread",
+                      return_value=THREAD_OBJ) as patch_copy_threads,\
+         patch.object(discussion, "get_thread_replies",
+                      return_value=[REPLY_OBJ]) as patch_thread_replies,\
+         patch.object(discussion, "copy_reply") as patch_copy_reply:
+        discussion.copy_thread(syn, THREAD_OBJ, PROJECTID)
+        patch_copy_threads.assert_called_once_with(syn, THREAD_OBJ, PROJECTID)
+        patch_thread_replies.assert_called_once_with(syn, THREAD_OBJ['id'])
+        patch_copy_reply.assert_called_once_with(syn, REPLY_OBJ,
+                                                 THREAD_OBJ['id'])
+
+
 def test_copy_reply():
     """Tests copying of replies"""
     profile = synapseclient.UserProfile(ownerId="test",
@@ -264,14 +279,8 @@ def test_copy_forum():
     with mock.patch.object(discussion, "get_forum_threads",
                            return_value=[THREAD_OBJ]) as patch_get_threads,\
          mock.patch.object(discussion, "copy_thread",
-                           return_value=new_thread) as patch_copy_thread,\
-         mock.patch.object(discussion, "get_thread_replies",
-                           return_value=[REPLY_OBJ]) as patch_thread_replies,\
-         mock.patch.object(discussion, "copy_reply") as patch_copy_reply:
+                           return_value=new_thread) as patch_copy_thread:
         discussion.copy_forum(syn, PROJECTID, new_projectid)
         patch_get_threads.assert_called_once_with(syn, PROJECTID)
         patch_copy_thread.assert_called_once_with(syn, THREAD_OBJ,
                                                   new_projectid)
-        patch_thread_replies.assert_called_once_with(syn, THREAD_OBJ['id'])
-        patch_copy_reply.assert_called_once_with(syn, REPLY_OBJ,
-                                                 new_thread['id'])