Repository: kmyk/online-judge-tools (python)

Issue:
add a command to check whether it's logged in or not

Pull request description:
#200 add --check option to login command
close #200
-   [x] #287 の後にrebaseしてからmerge (#287 とは独立だが衝突するので)
-   [x] テストを書く

Patch:
diff --git a/LOGIN_WITH_COOKIES.md b/LOGIN_WITH_COOKIES.md
index ad67ad3..018949c 100644
--- a/LOGIN_WITH_COOKIES.md
+++ b/LOGIN_WITH_COOKIES.md
@@ -50,11 +50,11 @@ Please replace current `REVEL_SESSION` value to value which you copied by the br
 5. Confirm logged in.
 
 ``` console
-$ oj login https://yukicoder.me/
+$ oj login --check https://yukicoder.me/
 [x] service recognized: <onlinejudge.service.yukicoder.YukicoderService object at 0x7f4467a0a2b0>: https://yukicoder.me/
 [x] load cookie from: ~/.local/share/online-judge-tools/cookie.jar
 [x] GET: https://yukicoder.me/auth/github
-[x] 200 OK
+[x] 302 Found
 [*] You have already signed in.
 [x] save cookie to: ~/.local/share/online-judge-tools/cookie.jar
 ```
diff --git a/onlinejudge/implementation/command/login.py b/onlinejudge/implementation/command/login.py
index 926d8ad..ab8d70a 100644
--- a/onlinejudge/implementation/command/login.py
+++ b/onlinejudge/implementation/command/login.py
@@ -31,16 +31,25 @@ def login(args: 'argparse.Namespace') -> None:
             log.failure('login for %s: invalid option: --method %s', service.get_name(), args.method)
             sys.exit(1)
 
-    # login
-    def get_credentials() -> Tuple[str, str]:
-        if args.username is None:
-            args.username = input('Username: ')
-        if args.password is None:
-            args.password = getpass.getpass()
-        return args.username, args.password
-
-    log.warning('If you don\'t want to give your password to this program, you can give only your session tokens.')
-    log.info('see: https://github.com/kmyk/online-judge-tools/blob/master/LOGIN_WITH_COOKIES.md')
-
     with utils.with_cookiejar(utils.new_default_session(), path=args.cookie) as sess:
-        service.login(get_credentials, session=sess, **kwargs)  # type: ignore
+
+        if args.check:
+            if service.is_logged_in(session=sess):
+                log.info('You have already signed in.')
+            else:
+                log.info('You are not signed in.')
+                sys.exit(1)
+
+        else:
+            # login
+            def get_credentials() -> Tuple[str, str]:
+                if args.username is None:
+                    args.username = input('Username: ')
+                if args.password is None:
+                    args.password = getpass.getpass()
+                return args.username, args.password
+
+            log.warning('If you don\'t want to give your password to this program, you can give only your session tokens.')
+            log.info('see: https://github.com/kmyk/online-judge-tools/blob/master/LOGIN_WITH_COOKIES.md')
+
+            service.login(get_credentials, session=sess, **kwargs)  # type: ignore
diff --git a/onlinejudge/implementation/main.py b/onlinejudge/implementation/main.py
index 9e2729e..af7652a 100644
--- a/onlinejudge/implementation/main.py
+++ b/onlinejudge/implementation/main.py
@@ -85,6 +85,7 @@ strings for --method:
     subparser.add_argument('url')
     subparser.add_argument('-u', '--username')
     subparser.add_argument('-p', '--password')
+    subparser.add_argument('--check', action='store_true', help='check whether you are logged in or not')
     subparser.add_argument('--method')
 
     # submit
diff --git a/onlinejudge/service/atcoder.py b/onlinejudge/service/atcoder.py
index 8f016b8..81090a4 100644
--- a/onlinejudge/service/atcoder.py
+++ b/onlinejudge/service/atcoder.py
@@ -44,6 +44,13 @@ class AtCoderService(onlinejudge.type.Service):
         AtCoderService._report_messages(msgs)
         return 'login' not in resp.url  # AtCoder redirects to the top page if success
 
+    def is_logged_in(self, session: Optional[requests.Session] = None) -> bool:
+        session = session or utils.new_default_session()
+        url = 'https://practice.contest.atcoder.jp/login'
+        resp = _request('GET', url, session=session, allow_redirects=False)
+        msgs = AtCoderService._get_messages_from_cookie(resp.cookies)
+        return bool(msgs)
+
     def get_url(self) -> str:
         return 'https://atcoder.jp/'
 
diff --git a/onlinejudge/service/codeforces.py b/onlinejudge/service/codeforces.py
index a20a870..13f3ea0 100644
--- a/onlinejudge/service/codeforces.py
+++ b/onlinejudge/service/codeforces.py
@@ -44,6 +44,12 @@ class CodeforcesService(onlinejudge.type.Service):
             log.failure('Invalid handle or password.')
             return False
 
+    def is_logged_in(self, session: Optional[requests.Session] = None) -> bool:
+        session = session or utils.new_default_session()
+        url = 'https://codeforces.com/enter'
+        resp = utils.request('GET', url, session=session, allow_redirects=False)
+        return resp.status_code == 302
+
     def get_url(self) -> str:
         return 'https://codeforces.com/'
 
diff --git a/onlinejudge/service/yukicoder.py b/onlinejudge/service/yukicoder.py
index 9c564c4..152d13a 100644
--- a/onlinejudge/service/yukicoder.py
+++ b/onlinejudge/service/yukicoder.py
@@ -66,6 +66,13 @@ class YukicoderService(onlinejudge.type.Service):
         url = 'https://yukicoder.me/auth/twitter'
         raise NotImplementedError
 
+    def is_logged_in(self, session: Optional[requests.Session] = None, method: Optional[str] = None) -> bool:
+        session = session or utils.new_default_session()
+        url = 'https://yukicoder.me/auth/github'
+        resp = utils.request('GET', url, session=session, allow_redirects=False)
+        assert resp.status_code == 302
+        return 'oauth' not in resp.headers['Location']
+
     def get_url(self) -> str:
         return 'https://yukicoder.me/'
 
diff --git a/onlinejudge/type.py b/onlinejudge/type.py
index 9d3e77a..d7fe56a 100644
--- a/onlinejudge/type.py
+++ b/onlinejudge/type.py
@@ -11,6 +11,9 @@ class Service(object):
     def login(self, get_credentials: CredentialsProvider, session: Optional['requests.Session'] = None) -> bool:
         raise NotImplementedError
 
+    def is_logged_in(self, session: Optional['requests.Session'] = None) -> bool:
+        raise NotImplementedError
+
     def get_url(self) -> str:
         raise NotImplementedError
 


Test patch:
diff --git a/tests/command_login.py b/tests/command_login.py
new file mode 100644
index 0000000..4f442d0
--- /dev/null
+++ b/tests/command_login.py
@@ -0,0 +1,42 @@
+import os
+import subprocess
+import sys
+import time
+import unittest
+
+import tests.utils
+
+
+class LoginTest(unittest.TestCase):
+    def snippet_call_login_check_failure(self, url):
+        ojtools = os.path.abspath('oj')
+        with tests.utils.sandbox(files=[]) as tempdir:
+            env = dict(**os.environ)
+            env['HOME'] = tempdir
+            self.assertRaises
+            proc = subprocess.run([ojtools, 'login', '--check', url], env=env, stdout=sys.stdout, stderr=sys.stderr)
+            self.assertEqual(proc.returncode, 1)
+
+    def test_call_login_check_atcoder_failure(self):
+        self.snippet_call_login_check_failure('https://atcoder.jp/')
+
+    def test_call_login_check_codeforces_failure(self):
+        self.snippet_call_login_check_failure('https://codeforces.com/')
+
+    def test_call_login_check_yukicoder_failure(self):
+        self.snippet_call_login_check_failure('https://yukicoder.me/')
+
+    @unittest.skipIf('CI' in os.environ, 'login is required')
+    def test_call_login_check_atcoder_success(self):
+        ojtools = os.path.abspath('oj')
+        subprocess.check_call([ojtools, 'login', '--check', 'https://atcoder.jp/'], stdout=sys.stdout, stderr=sys.stderr)
+
+    @unittest.skipIf('CI' in os.environ, 'login is required')
+    def test_call_login_check_codeforces_success(self):
+        ojtools = os.path.abspath('oj')
+        subprocess.check_call([ojtools, 'login', '--check', 'https://codeforces.com/'], stdout=sys.stdout, stderr=sys.stderr)
+
+    @unittest.skipIf('CI' in os.environ, 'login is required')
+    def test_call_login_check_yukicoder_success(self):
+        ojtools = os.path.abspath('oj')
+        subprocess.check_call([ojtools, 'login', '--check', 'https://yukicoder.me/'], stdout=sys.stdout, stderr=sys.stderr)