Merge lp:~jkakar/todo/login-command into lp:todo

Proposed by Jamu Kakar
Status: Needs review
Proposed branch: lp:~jkakar/todo/login-command
Merge into: lp:todo
Prerequisite: lp:~jkakar/todo/project-skeleton
Diff against target: 644 lines (+571/-1)
11 files modified
todo/application.py (+41/-0)
todo/commands.py (+57/-0)
todo/entry_point.py (+3/-1)
todo/model.py (+57/-0)
todo/testing/__init__.py (+15/-0)
todo/testing/basic.py (+27/-0)
todo/testing/doubles.py (+87/-0)
todo/tests/__init__.py (+15/-0)
todo/tests/test_application.py (+62/-0)
todo/tests/test_commands.py (+126/-0)
todo/tests/test_model.py (+81/-0)
To merge this branch: bzr merge lp:~jkakar/todo/login-command
Reviewer Review Type Date Requested Status
Todo Team Pending
Review via email: mp+47195@code.launchpad.net

Description of the change

This branch introduces the following changes:

- A bunch of testing support has been added in a new todo.testing
  package. There's a TodoTestCase with support for using test
  resources and for testing Twisted-based code. There's a also a
  todo.doubles module with a FakeEndpoint that can be used to write
  tests for code that use txFluidDB. The MockEndpoint in txFluidDB
  itself isn't useful, unfortunately, because it only allows one
  response to be mocked at a time. At some point soon we'll need more
  than that.

- A new todo.application module contains helper functions to get the
  default path for configuration data and to create the default
  endpoint.

- A new todo.model module contains the beginnings of a model layer.
  So far it has a login function and an empty Person class (which I
  expect to eventually expose methods like 'create_task',
  'create_category', etc.)

- A new todo.commands module contains a new 'login' command. It uses
  the model layer to perform actual authentication and takes care of
  writing credentials to disk (which I guess is specific to the CLI
  interface).

Hopefully subsequent branches will be a bit easier to push forward,
now that testing support is in place. The 'login' command doesn't
create a namespace for tasks yet. I decided to defer that to a
subsequent branch, since this one is already getting a bit big.

To post a comment you must log in.
Revision history for this message
Jamu Kakar (jkakar) wrote :

Just had a thought, which I'll record here so I don't forget it:

It'd be nice to add a test to ensure that cached credentials are
completely rewritten when the 'login' command is run more than once.

Revision history for this message
Jamu Kakar (jkakar) wrote :

Actually, thinking about this more, the call to /user/<username>
doesn't actually do any validation of the password... does it? Maybe
this branch isn't ready for review. Would appreciate a scan and some
advice about what to do in the todo.model.login function.

lp:~jkakar/todo/login-command updated
19. By Jamu Kakar

- Fixed typo in test.

Revision history for this message
Jamu Kakar (jkakar) wrote :

After talking with Terry on IRC I realize the previous question
doesn't make sense. Things should be fine.

Revision history for this message
Terry Jones (terrycojones) wrote :

The above question was answered (yes) in IRC just now.

Revision history for this message
Jamu Kakar (jkakar) wrote :

Ah, just realized something... even though the credentials are being
checked, I haven't verified that the HTTP response when they're
invalid or for an unknown user are the same as what I'm using in the
test. That should be done before this branch is merged.

Unmerged revisions

19. By Jamu Kakar

- Fixed typo in test.

18. By Jamu Kakar

- Fixed type in program name.

17. By Jamu Kakar

- Added a todo.commands module with a 'login' command that
  authenticates a user against FluidDB and writes credentials to disk.

16. By Jamu Kakar

- Add a new todo.model module with a login function and the beginnings
  of a Person model class.

15. By Jamu Kakar

- A new todo.testing.doubles module contains a FakeEndpoint (and
  FakeRequest) that can be used to test code that uses txFluidDB.

14. By Jamu Kakar

- Updated TodoTestCase to work for Twisted-based code.

13. By Jamu Kakar

- Added a new todo.application module with functions to get the
  configuration path and create a default txfluiddb.client.Endpoint.

12. By Jamu Kakar

- Added todo.tests package.

11. By Jamu Kakar

- Add a new todo.testing package to contain support for the test
  suite. A TodoTestCase combines the testresources and testtools test
  case classes.

10. By Jamu Kakar

- Added 'commandant' to the requires list in setup.py.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'todo/application.py'
2--- todo/application.py 1970-01-01 00:00:00 +0000
3+++ todo/application.py 2011-01-23 23:29:09 +0000
4@@ -0,0 +1,41 @@
5+# Todo is a task tracker for teams!
6+# Copyright (C) 2011 Jamshed Kakar <jkakar@kakar.ca> and Nicholas Tollervey
7+# <ntoll@ntoll.com>.
8+#
9+# This program is free software: you can redistribute it and/or modify it
10+# under the terms of the GNU General Public License as published by the Free
11+# Software Foundation, either version 3 of the License, or (at your option)
12+# any later version.
13+#
14+# This program is distributed in the hope that it will be useful, but WITHOUT
15+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17+# more details.
18+
19+import os
20+
21+from txfluiddb.client import Endpoint, BasicCreds
22+
23+
24+def get_config_path():
25+ """Get the location to use when reading or writing configuration data.
26+
27+ Todo stores configuration data in C{$HOME/.config/todo}. It will be
28+ created automatically if it doesn't exist.
29+
30+ @return: The directory for configuration data.
31+ """
32+ base_path = os.environ["HOME"]
33+ path = os.path.join(base_path, ".config/todo")
34+ if not os.path.exists(path):
35+ os.makedirs(path)
36+ return path
37+
38+
39+def create_default_endpoint(username, password):
40+ """
41+ Create a C{txfluiddb.client.Endpoint} instance configured using the
42+ specified C{username} and C{password} and the default FluidDB API URL.
43+ """
44+ credentials = BasicCreds(username, password)
45+ return Endpoint(creds=credentials)
46
47=== added file 'todo/commands.py'
48--- todo/commands.py 1970-01-01 00:00:00 +0000
49+++ todo/commands.py 2011-01-23 23:29:09 +0000
50@@ -0,0 +1,57 @@
51+# Todo is a task tracker for teams!
52+# Copyright (C) 2011 Jamshed Kakar <jkakar@kakar.ca> and Nicholas Tollervey
53+# <ntoll@ntoll.com>.
54+#
55+# This program is free software: you can redistribute it and/or modify it
56+# under the terms of the GNU General Public License as published by the Free
57+# Software Foundation, either version 3 of the License, or (at your option)
58+# any later version.
59+#
60+# This program is distributed in the hope that it will be useful, but WITHOUT
61+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
62+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
63+# more details.
64+
65+from getpass import getpass
66+import os
67+
68+from commandant.commands import TwistedCommand
69+
70+from todo.application import create_default_endpoint, get_config_path
71+from todo.model import InvalidCredentialsError, UnknownUserError, login
72+
73+
74+class cmd_login(TwistedCommand):
75+ """Authenticate with FluidDB and cache credentials locally."""
76+
77+ endpoint_factory = None
78+ takes_args = ["username"]
79+
80+ def run(self, username):
81+ password = getpass("Password: ")
82+ endpoint = self.create_endpoint(username, password)
83+ deferred = login(endpoint)
84+
85+ def write_credentials(person):
86+ credentials_path = os.path.join(get_config_path(),
87+ "credentials.txt")
88+ with open(credentials_path, "w") as file:
89+ file.write(person.endpoint.creds.encode())
90+
91+ def print_error(failure):
92+ failure.trap(InvalidCredentialsError, UnknownUserError)
93+ self.outf.write(failure.getErrorMessage())
94+
95+ deferred.addCallback(write_credentials)
96+ deferred.addErrback(print_error)
97+ return deferred
98+
99+ def create_endpoint(self, username, password):
100+ """
101+ Get a C{txfluiddb.client.Endpoint} instance to use when interacting
102+ with FluidDB.
103+ """
104+ if self.endpoint_factory:
105+ return self.endpoint_factory(username, password)
106+ else:
107+ return create_default_endpoint(username, password)
108
109=== modified file 'todo/entry_point.py'
110--- todo/entry_point.py 2011-01-23 23:29:09 +0000
111+++ todo/entry_point.py 2011-01-23 23:29:09 +0000
112@@ -18,6 +18,7 @@
113 from commandant.controller import CommandController
114
115 from todo import __version__
116+from todo import commands
117
118
119 def main(argv):
120@@ -32,10 +33,11 @@
121 if len(argv) < 2:
122 argv.append("help")
123
124- controller = CommandController("todo", __version__,
125+ controller = CommandController("td", __version__,
126 "Todo is a task tracker for teams!",
127 "https://launchpad.net/todo")
128 controller.load_module(builtins)
129+ controller.load_module(commands)
130 controller.install_bzrlib_hooks()
131 try:
132 controller.run(argv[1:])
133
134=== added file 'todo/model.py'
135--- todo/model.py 1970-01-01 00:00:00 +0000
136+++ todo/model.py 2011-01-23 23:29:09 +0000
137@@ -0,0 +1,57 @@
138+# Todo is a task tracker for teams!
139+# Copyright (C) 2011 Jamshed Kakar <jkakar@kakar.ca> and Nicholas Tollervey
140+# <ntoll@ntoll.com>.
141+#
142+# This program is free software: you can redistribute it and/or modify it
143+# under the terms of the GNU General Public License as published by the Free
144+# Software Foundation, either version 3 of the License, or (at your option)
145+# any later version.
146+#
147+# This program is distributed in the hope that it will be useful, but WITHOUT
148+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
149+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
150+# more details.
151+
152+from urllib import basejoin
153+
154+
155+class InvalidCredentialsError(Exception):
156+ """Raised when invalid credentials are provided."""
157+
158+
159+class UnknownUserError(Exception):
160+ """Raised when credentials don't match a user in FluidDB."""
161+
162+
163+def login(endpoint):
164+ """Authenticate a L{Person} against FluidDB.
165+
166+ @param endpoint: A C{txfluiddb.client.Endpoint} instance that can be used
167+ to authenticate with FluidDB.
168+ @raises UnknownUserError: The C{Deferred} will fire an errback if the
169+ specified credentials are invalid.
170+ @return: A C{Deferred} that will fire with a L{Person} instance.
171+ """
172+ url = basejoin(endpoint.getRootURL(), "/user/%s" % endpoint.creds.username)
173+ deferred = endpoint.submit(url, "GET", parsePayload=False)
174+
175+ def handle_response((status, headers, data)):
176+ if status == 200:
177+ return Person(endpoint)
178+ elif status == 404:
179+ raise UnknownUserError("Unknown user.")
180+ elif status == 400:
181+ raise InvalidCredentialsError("Invalid credentials.")
182+
183+ return deferred.addCallback(handle_response)
184+
185+
186+class Person(object):
187+ """The primary actor, a person creating and modifying tasks.
188+
189+ @param endpoint: A C{txfluiddb.client.Endpoint} instance that can be used
190+ to interact with FluidDB on behalf of the person.
191+ """
192+
193+ def __init__(self, endpoint):
194+ self.endpoint = endpoint
195
196=== added directory 'todo/testing'
197=== added file 'todo/testing/__init__.py'
198--- todo/testing/__init__.py 1970-01-01 00:00:00 +0000
199+++ todo/testing/__init__.py 2011-01-23 23:29:09 +0000
200@@ -0,0 +1,15 @@
201+# Todo is a task tracker for teams!
202+# Copyright (C) 2011 Jamshed Kakar <jkakar@kakar.ca> and Nicholas Tollervey
203+# <ntoll@ntoll.com>.
204+#
205+# This program is free software: you can redistribute it and/or modify it
206+# under the terms of the GNU General Public License as published by the Free
207+# Software Foundation, either version 3 of the License, or (at your option)
208+# any later version.
209+#
210+# This program is distributed in the hope that it will be useful, but WITHOUT
211+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
212+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
213+# more details.
214+
215+"""Testing support such as test case classes, doubles, etc."""
216
217=== added file 'todo/testing/basic.py'
218--- todo/testing/basic.py 1970-01-01 00:00:00 +0000
219+++ todo/testing/basic.py 2011-01-23 23:29:09 +0000
220@@ -0,0 +1,27 @@
221+# Todo is a task tracker for teams!
222+# Copyright (C) 2011 Jamshed Kakar <jkakar@kakar.ca> and Nicholas Tollervey
223+# <ntoll@ntoll.com>.
224+#
225+# This program is free software: you can redistribute it and/or modify it
226+# under the terms of the GNU General Public License as published by the Free
227+# Software Foundation, either version 3 of the License, or (at your option)
228+# any later version.
229+#
230+# This program is distributed in the hope that it will be useful, but WITHOUT
231+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
232+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
233+# more details.
234+
235+from testresources import ResourcedTestCase
236+from twisted.trial.unittest import TestCase
237+
238+
239+class TodoTestCase(TestCase, ResourcedTestCase):
240+
241+ def setUp(self):
242+ TestCase.setUp(self)
243+ ResourcedTestCase.setUp(self)
244+
245+ def tearDown(self):
246+ TestCase.tearDown(self)
247+ ResourcedTestCase.tearDown(self)
248
249=== added file 'todo/testing/doubles.py'
250--- todo/testing/doubles.py 1970-01-01 00:00:00 +0000
251+++ todo/testing/doubles.py 2011-01-23 23:29:09 +0000
252@@ -0,0 +1,87 @@
253+# Todo is a task tracker for teams!
254+# Copyright (C) 2011 Jamshed Kakar <jkakar@kakar.ca> and Nicholas Tollervey
255+# <ntoll@ntoll.com>.
256+#
257+# This program is free software: you can redistribute it and/or modify it
258+# under the terms of the GNU General Public License as published by the Free
259+# Software Foundation, either version 3 of the License, or (at your option)
260+# any later version.
261+#
262+# This program is distributed in the hope that it will be useful, but WITHOUT
263+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
264+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
265+# more details.
266+
267+from urllib import basejoin
268+
269+from twisted.internet.defer import succeed
270+from txfluiddb.client import Endpoint
271+
272+
273+class FakeRequest(object):
274+ """A fake request for testing purposes.
275+
276+ @param url: The URL for this request.
277+ @param method: The HTTP method for this request.
278+ @param data: The HTTP payload for this request.
279+ @param headers: A C{dict} containing HTTP request headers.
280+ """
281+
282+ def __init__(self, url, method, data=None, headers=None):
283+ self.url = url
284+ self.method = method
285+ self.data = data
286+ self.headers = headers
287+
288+ def respond(self, data, headers=None, code=None):
289+ """Record the response that will be sent when the request is made.
290+
291+ @param data: The HTTP response payload.
292+ @param headers: A C{dict} containing HTTP response headers. Defaults
293+ to an empty C{dict}.
294+ @param code: The HTTP status code. Defaults to 200.
295+ """
296+ self._response_data = data
297+ self._response_headers = headers if headers else {}
298+ self._response_code = code if code else 200
299+
300+ def get_response(self):
301+ """Get the response code, headers and data as a 3-tuple."""
302+ return (self._response_code, self._response_headers,
303+ self._response_data)
304+
305+
306+class FakeEndpoint(Endpoint):
307+ """A fake C{txfluiddb.client.Endpoint} for testing purposes."""
308+
309+ requests = []
310+
311+ def getPage(self, url, method, postdata=None, headers=None,
312+ *args, **kwargs):
313+ """Return fake results instead of submitting a request."""
314+ request = self.requests.pop(0)
315+ if request.url != url:
316+ raise RuntimeError("Expected URL '%s' but got '%s'."
317+ % (request.url, url))
318+ if request.method != method:
319+ raise RuntimeError("Expected method '%s' but got '%s'."
320+ % (request.method, method))
321+ if request.data != postdata:
322+ raise RuntimeError("Expected data '%s' but got '%s'."
323+ % (request.data, postdata))
324+ if request.headers != headers:
325+ raise RuntimeError("Expected headers '%s' but got '%s'."
326+ % (request.headers, headers))
327+ return succeed(request.get_response())
328+
329+ def expect(self, path, method, data=None, headers=None):
330+ """Record an expectation of an HTTP request.
331+
332+ @return: A L{FakeRequest} instance representing the request.
333+ """
334+ if method == "GET" and data is not None:
335+ raise RuntimeError("GET requests should never include POST data.")
336+ url = basejoin(self.getRootURL(), path)
337+ request = FakeRequest(url, method, data, headers)
338+ self.requests.append(request)
339+ return request
340
341=== added directory 'todo/tests'
342=== added file 'todo/tests/__init__.py'
343--- todo/tests/__init__.py 1970-01-01 00:00:00 +0000
344+++ todo/tests/__init__.py 2011-01-23 23:29:09 +0000
345@@ -0,0 +1,15 @@
346+# Todo is a task tracker for teams!
347+# Copyright (C) 2011 Jamshed Kakar <jkakar@kakar.ca> and Nicholas Tollervey
348+# <ntoll@ntoll.com>.
349+#
350+# This program is free software: you can redistribute it and/or modify it
351+# under the terms of the GNU General Public License as published by the Free
352+# Software Foundation, either version 3 of the License, or (at your option)
353+# any later version.
354+#
355+# This program is distributed in the hope that it will be useful, but WITHOUT
356+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
357+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
358+# more details.
359+
360+"""Unit tests for L{todo}."""
361
362=== added file 'todo/tests/test_application.py'
363--- todo/tests/test_application.py 1970-01-01 00:00:00 +0000
364+++ todo/tests/test_application.py 2011-01-23 23:29:09 +0000
365@@ -0,0 +1,62 @@
366+# Todo is a task tracker for teams!
367+# Copyright (C) 2011 Jamshed Kakar <jkakar@kakar.ca> and Nicholas Tollervey
368+# <ntoll@ntoll.com>.
369+#
370+# This program is free software: you can redistribute it and/or modify it
371+# under the terms of the GNU General Public License as published by the Free
372+# Software Foundation, either version 3 of the License, or (at your option)
373+# any later version.
374+#
375+# This program is distributed in the hope that it will be useful, but WITHOUT
376+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
377+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
378+# more details.
379+
380+import os
381+
382+from commandant.testing.resources import TemporaryDirectoryResource
383+from txfluiddb.client import FLUIDDB_ENDPOINT
384+
385+from todo.application import get_config_path, create_default_endpoint
386+from todo.testing.basic import TodoTestCase
387+
388+
389+class GetConfigPathTest(TodoTestCase):
390+
391+ resources = [("fs", TemporaryDirectoryResource())]
392+
393+ def setUp(self):
394+ super(GetConfigPathTest, self).setUp()
395+ self.addCleanup(os.putenv, "HOME", os.environ["HOME"])
396+ os.environ["HOME"] = self.fs.make_dir()
397+
398+ def test_get_config_path(self):
399+ """L{get_config_path} will returns the path for configuration data."""
400+ expected_path = os.path.join(os.environ["HOME"], ".config/todo")
401+ os.makedirs(expected_path)
402+ self.assertTrue(os.path.exists(expected_path))
403+ self.assertEqual(expected_path, get_config_path())
404+
405+ def test_get_config_path_creates_path(self):
406+ """
407+ L{get_config_path} creates the configuration directory, if it doesn't
408+ exist.
409+ """
410+ expected_path = os.path.join(os.environ["HOME"], ".config/todo")
411+ self.assertFalse(os.path.exists(expected_path))
412+ self.assertEqual(expected_path, get_config_path())
413+ self.assertTrue(os.path.exists(expected_path))
414+
415+
416+class CreateDefaultEndpointTest(TodoTestCase):
417+
418+ def test_create_default_endpoint(self):
419+ """
420+ L{create_default_endpoint} returns a C{txfluiddb.client.Endpoint},
421+ configured to use the specified username, password and FluidDB API
422+ URL.
423+ """
424+ endpoint = create_default_endpoint("username", "password")
425+ self.assertEqual(FLUIDDB_ENDPOINT, endpoint.baseURL)
426+ self.assertEqual("username", endpoint.creds.username)
427+ self.assertEqual("password", endpoint.creds.password)
428
429=== added file 'todo/tests/test_commands.py'
430--- todo/tests/test_commands.py 1970-01-01 00:00:00 +0000
431+++ todo/tests/test_commands.py 2011-01-23 23:29:09 +0000
432@@ -0,0 +1,126 @@
433+# Todo is a task tracker for teams!
434+# Copyright (C) 2011 Jamshed Kakar <jkakar@kakar.ca> and Nicholas Tollervey
435+# <ntoll@ntoll.com>.
436+#
437+# This program is free software: you can redistribute it and/or modify it
438+# under the terms of the GNU General Public License as published by the Free
439+# Software Foundation, either version 3 of the License, or (at your option)
440+# any later version.
441+#
442+# This program is distributed in the hope that it will be useful, but WITHOUT
443+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
444+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
445+# more details.
446+
447+from getpass import getpass
448+from json import dumps
449+import os
450+
451+from commandant.testing.mocker import MockerResource
452+from commandant.testing.resources import (
453+ CommandFactoryResource, TemporaryDirectoryResource)
454+from txfluiddb.client import BasicCreds, FLUIDDB_ENDPOINT
455+
456+from todo.application import get_config_path
457+from todo.commands import cmd_login
458+from todo.testing.basic import TodoTestCase
459+from todo.testing.doubles import FakeEndpoint
460+
461+
462+class LoginCommandTest(TodoTestCase):
463+
464+ resources = [("factory", CommandFactoryResource()),
465+ ("fs", TemporaryDirectoryResource()),
466+ ("mocker", MockerResource())]
467+
468+ def setUp(self):
469+ super(LoginCommandTest, self).setUp()
470+ self.addCleanup(os.putenv, "HOME", os.environ["HOME"])
471+ os.environ["HOME"] = self.fs.make_dir()
472+ self.credentials = BasicCreds("username", "password")
473+ self.endpoint = FakeEndpoint("http://fluiddb.example.com",
474+ creds=self.credentials)
475+ self.command = self.factory.create_command("login", cmd_login)
476+ self.command.endpoint_factory = lambda *args: self.endpoint
477+
478+ def test_default_endpoint(self):
479+ """
480+ The default endpoint is a C{txfluiddb.client.Endpoint} instances
481+ configured with the specified username, password and FluidDB API URL.
482+ """
483+ command = self.factory.create_command("login", cmd_login)
484+ endpoint = command.create_endpoint("username", "password")
485+ self.assertEqual(FLUIDDB_ENDPOINT, endpoint.baseURL)
486+ self.assertEqual("username", endpoint.creds.username)
487+ self.assertEqual("password", endpoint.creds.password)
488+
489+ def test_login(self):
490+ """
491+ The password specified by the user is used to authenticate with
492+ FluidDB.
493+ """
494+ getpass_mock = self.mocker.replace(getpass, passthrough=False)
495+ getpass_mock("Password: ")
496+ self.mocker.result("password")
497+ self.mocker.replay()
498+
499+ request = self.endpoint.expect(
500+ "/user/username", "GET",
501+ headers={"Authorization": self.credentials.encode()})
502+ request.respond(dumps({"name": "Test User",
503+ "id": "492d803f-f36c-459c-9c49-9f8dff0af3c1"}))
504+
505+ def check(_):
506+ credentials_path = os.path.join(get_config_path(),
507+ "credentials.txt")
508+ with open(credentials_path, "r") as file:
509+ self.assertEqual(self.endpoint.creds.encode(), file.read())
510+
511+ return self.command.run("username").addCallback(check)
512+
513+ def test_login_with_unknown_user(self):
514+ """
515+ An error message is displayed if the specified credentials don't match
516+ a user in FluidDB.
517+ """
518+ getpass_mock = self.mocker.replace(getpass, passthrough=False)
519+ getpass_mock("Password: ")
520+ self.mocker.result("password")
521+ self.mocker.replay()
522+
523+ request = self.endpoint.expect(
524+ "/user/username", "GET",
525+ headers={"Authorization": self.credentials.encode()})
526+ request.respond("Unknown user.", code=404)
527+
528+ def check(_):
529+ credentials_path = os.path.join(get_config_path(),
530+ "credentials.txt")
531+ self.assertFalse(os.path.exists(credentials_path))
532+ self.assertEqual("Unknown user.", self.command.outf.getvalue())
533+
534+ return self.command.run("username").addCallback(check)
535+
536+ def test_login_with_invalid_credentials(self):
537+ """
538+ An error message is displayed if the specified credentials are
539+ invalid.
540+ """
541+ getpass_mock = self.mocker.replace(getpass, passthrough=False)
542+ getpass_mock("Password: ")
543+ self.mocker.result("password")
544+ self.mocker.replay()
545+
546+ request = self.endpoint.expect(
547+ "/user/username", "GET",
548+ headers={"Authorization": self.credentials.encode()})
549+ request.respond("Invalid credentials.", code=400)
550+
551+ def check(_):
552+ credentials_path = os.path.join(get_config_path(),
553+ "credentials.txt")
554+ self.assertFalse(os.path.exists(credentials_path))
555+ self.assertEqual("Invalid credentials.",
556+ self.command.outf.getvalue())
557+
558+ return self.command.run("username").addCallback(check)
559
560=== added file 'todo/tests/test_model.py'
561--- todo/tests/test_model.py 1970-01-01 00:00:00 +0000
562+++ todo/tests/test_model.py 2011-01-23 23:29:09 +0000
563@@ -0,0 +1,81 @@
564+# Todo is a task tracker for teams!
565+# Copyright (C) 2011 Jamshed Kakar <jkakar@kakar.ca> and Nicholas Tollervey
566+# <ntoll@ntoll.com>.
567+#
568+# This program is free software: you can redistribute it and/or modify it
569+# under the terms of the GNU General Public License as published by the Free
570+# Software Foundation, either version 3 of the License, or (at your option)
571+# any later version.
572+#
573+# This program is distributed in the hope that it will be useful, but WITHOUT
574+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
575+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
576+# more details.
577+
578+from json import dumps
579+
580+from txfluiddb.client import BasicCreds
581+
582+from todo.model import InvalidCredentialsError, UnknownUserError, login
583+from todo.testing.basic import TodoTestCase
584+from todo.testing.doubles import FakeEndpoint
585+
586+
587+class LoginTest(TodoTestCase):
588+
589+ def setUp(self):
590+ super(LoginTest, self).setUp()
591+ self.credentials = BasicCreds("username", "password")
592+ self.endpoint = FakeEndpoint("http://example.com",
593+ creds=self.credentials)
594+
595+ def test_login(self):
596+ """
597+ L{login} returns a L{Person} instance if the specified endpoint
598+ contains valid credentials that are successfully authenticated against
599+ FluidDB.
600+ """
601+ request = self.endpoint.expect(
602+ "/user/username", "GET",
603+ headers={"Authorization": self.credentials.encode()})
604+ request.respond(dumps({"name": "Test User",
605+ "id": "492d803f-f36c-459c-9c49-9f8dff0af3c1"}))
606+
607+ def check(person):
608+ self.assertIdentical(self.endpoint, person.endpoint)
609+
610+ return login(self.endpoint).addCallback(check)
611+
612+ def test_login_as_unknown_user(self):
613+ """
614+ An L{UnknownUserError} is returned if the specified endpoint
615+ contains credentials for an unknown user in FluidDB.
616+ """
617+ request = self.endpoint.expect(
618+ "/user/username", "GET",
619+ headers={"Authorization": self.credentials.encode()})
620+ request.respond("Unknown user.", code=404)
621+
622+ def check(error):
623+ self.assertEqual("Unknown user.", str(error))
624+
625+ deferred = login(self.endpoint)
626+ self.assertFailure(deferred, UnknownUserError)
627+ return deferred.addCallback(check)
628+
629+ def test_login_with_invalid_credentials(self):
630+ """
631+ An L{InvalidCredentialsError} is returned if the specified endpoint
632+ contains credentials that aren't valid for a user in FluidDB.
633+ """
634+ request = self.endpoint.expect(
635+ "/user/username", "GET",
636+ headers={"Authorization": self.credentials.encode()})
637+ request.respond("Invalid credentials.", code=400)
638+
639+ def check(error):
640+ self.assertEqual("Invalid credentials.", str(error))
641+
642+ deferred = login(self.endpoint)
643+ self.assertFailure(deferred, InvalidCredentialsError)
644+ return deferred.addCallback(check)

Subscribers

People subscribed via source and target branches

to all changes: