Skip to content

Commit d210ca5

Browse files
committed
tests for 19 miiight actually pass
1 parent 8de542b commit d210ca5

File tree

2 files changed

+84
-156
lines changed

2 files changed

+84
-156
lines changed

chapter_19_mocking.asciidoc

Lines changed: 83 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,8 +1382,7 @@ is a good time to check out the
13821382
docs on authentication] for a little more context.((("", startref="Mdespike19")))((("", startref="SDdesp19")))
13831383

13841384

1385-
An Alternative Reason to Use Mocks: Reducing Duplication
1386-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1385+
=== An Alternative Reason to Use Mocks: Reducing Duplication
13871386

13881387
((("mocks", "reducing duplication with", id="Mreduce19")))((("duplication, eliminating", id="dupel19")))So
13891388
far we've used mocks to test external dependencies, like Django's
@@ -1404,10 +1403,12 @@ it returns `None` for invalid tokens, existing users if they already exist,
14041403
and creates new users for valid tokens if they don't exist yet. So, to fully
14051404
test this view, I'd have to write tests for all three of those cases.
14061405

1407-
TIP: ((("combinatorial explosion")))One
1408-
good justification for using mocks is when they will reduce
1409-
duplication between tests. It's one way of avoiding 'combinatorial
1410-
explosion'.
1406+
TIP: One possible justification for using mocks is
1407+
when they will reduce duplication between tests.
1408+
It's one way of avoiding _combinatorial explosion_.
1409+
((("combinatorial explosion")))
1410+
1411+
// TODO: not on london-style tdd and how i personally just don't do this.
14111412

14121413
On top of that, the fact that we're using the Django
14131414
`auth.authenticate` function rather than calling our own code directly is
@@ -1866,7 +1867,7 @@ and non–logged-in users (which our FT relies on):
18661867
{% if user.email %}
18671868
<ul>
18681869
<span class="navbar-text">Logged in as {{ user.email }}</span>
1869-
<a href="{% url 'logout' %}">Log out</a>
1870+
<a href="#TODO">Log out</a>
18701871
</ul>
18711872
{% else %}
18721873
<form method="POST" action="{% url 'send_login_email' %}">
@@ -1895,104 +1896,6 @@ TODO resume updates to chapter from here
18951896
****
18961897

18971898

1898-
And see if that...
1899-
1900-
[subs="specialcharacters,macros"]
1901-
----
1902-
$ pass:quotes[*python src/manage.py test functional_tests.test_login*]
1903-
Internal Server Error: /accounts/login
1904-
[...]
1905-
File "...goat-book/accounts/views.py", line 31, in login
1906-
auth.login(request, user)
1907-
[...]
1908-
ValueError: The following fields do not exist in this model or are m2m fields:
1909-
last_login
1910-
[...]
1911-
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate
1912-
element: Log out
1913-
----
1914-
1915-
1916-
Oh no! Something's not right. But assuming you've kept the `LOGGING`
1917-
config in 'settings.py', you should see the explanatory traceback, as just
1918-
shown. It's saying something about a `last_login` field.
1919-
1920-
https://code.djangoproject.com/ticket/26823[In my opinion] this is a
1921-
bug in Django, but essentially the auth framework expects the user
1922-
model to have a `last_login` field. We don't have one. But never fear!
1923-
There's a way of handling this failure.
1924-
1925-
Let's write a unit test that reproduces the bug first. Since it's to do
1926-
with our custom user model, as good a place to have it as any might be
1927-
'test_models.py':
1928-
1929-
1930-
[role="sourcecode"]
1931-
.src/accounts/tests/test_models.py (ch17l047)
1932-
====
1933-
[source,python]
1934-
----
1935-
from django.contrib import auth
1936-
from django.test import TestCase
1937-
1938-
from accounts.models import Token
1939-
1940-
User = auth.get_user_model()
1941-
1942-
1943-
class UserModelTest(TestCase):
1944-
def test_user_is_valid_with_email_only(self):
1945-
[...]
1946-
def test_email_is_primary_key(self):
1947-
[...]
1948-
1949-
def test_no_problem_with_auth_login(self):
1950-
user = User.objects.create(email="[email protected]")
1951-
user.backend = ""
1952-
request = self.client.request().wsgi_request
1953-
auth.login(request, user) # should not raise
1954-
----
1955-
====
1956-
1957-
We create a request object and a user, and then we pass them into the
1958-
`auth.login` function.
1959-
1960-
That will raise our error:
1961-
1962-
----
1963-
auth.login(request, user) # should not raise
1964-
[...]
1965-
ValueError: The following fields do not exist in this model or are m2m fields:
1966-
last_login
1967-
----
1968-
1969-
1970-
The specific reason for this bug isn't really important for the purposes of
1971-
this book, but if you're curious about what exactly is going on here, take a
1972-
look through the Django source lines listed in the traceback, and have a read up
1973-
of Django's https://docs.djangoproject.com/en/1.11/topics/signals/[docs on
1974-
signals].
1975-
1976-
// TODO this aint right
1977-
1978-
The upshot is that we can fix it like this:
1979-
1980-
[role="sourcecode"]
1981-
.src/accounts/models.py (ch17l048)
1982-
====
1983-
[source,python]
1984-
----
1985-
import uuid
1986-
from django.contrib import auth
1987-
from django.db import models
1988-
1989-
auth.signals.user_logged_in.disconnect(auth.models.update_last_login)
1990-
1991-
1992-
class User(models.Model):
1993-
[...]
1994-
----
1995-
====
19961899

19971900

19981901
How does our FT look now?
@@ -2011,13 +1914,12 @@ OK
20111914

20121915

20131916

2014-
It Works in Theory! Does It Work in Practice?
2015-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1917+
=== It Works in Theory! Does It Work in Practice?
20161918

20171919

2018-
((("mocks", "practical application of")))Wow!
2019-
Can you believe it? I scarcely can! Time for a manual look around with
2020-
`runserver`:
1920+
((("mocks", "practical application of")))
1921+
Wow! Can you believe it? I scarcely can!
1922+
Time for a manual look around with `runserver`:
20211923

20221924

20231925
[role="skipme"]
@@ -2033,9 +1935,7 @@ ConnectionRefusedError: [Errno 111] Connection refused
20331935
----
20341936

20351937

2036-
Using Our New Environment Variable, and Saving It to .env
2037-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2038-
1938+
==== Using Our New Environment Variable, and Saving It to .env
20391939

20401940
You'll probably get an error, like I did, when you try to run things manually.
20411941
It's because of two things:
@@ -2047,9 +1947,9 @@ It's because of two things:
20471947
====
20481948
[source,python]
20491949
----
2050-
EMAIL_HOST = 'smtp.gmail.com'
2051-
EMAIL_HOST_USER = '[email protected]'
2052-
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASSWORD')
1950+
EMAIL_HOST = "smtp.gmail.com"
1951+
EMAIL_HOST_USER = "[email protected]"
1952+
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_PASSWORD")
20531953
EMAIL_PORT = 587
20541954
EMAIL_USE_TLS = True
20551955
----
@@ -2105,6 +2005,7 @@ $ *python src/manage.py runserver*
21052005

21062006
...you should see something like <<despiked-success-message>>.
21072007

2008+
//TODO: update screenshot
21082009

21092010
[[despiked-success-message]]
21102011
.Check your email....
@@ -2129,56 +2030,94 @@ $ *git commit -m "Custom passwordless auth backend + custom user model"*
21292030

21302031

21312032

2132-
Finishing Off Our FT, Testing Logout
2133-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2033+
=== Finishing Off Our FT, Testing Logout
21342034

21352035

2136-
((("mocks", "logout link")))The
2137-
last thing we need to do before we call it a day is to test the logout
2138-
link. We extend the FT with a couple more steps:
2036+
((("mocks", "logout link")))
2037+
The last thing we need to do before we call it a day is to test the logout link
2038+
(you may remember the URL just says `#TODO` at the moment.)
2039+
We extend the FT with a couple more steps:
21392040

21402041
[role="sourcecode"]
2141-
.functional_tests/test_login.py (ch17l050)
2042+
.src/functional_tests/test_login.py (ch17l050)
21422043
====
21432044
[source,python]
21442045
----
21452046
[...]
21462047
# she is logged in!
2147-
self.wait_for(
2148-
lambda: self.browser.find_element_by_link_text('Log out')
2149-
)
2150-
navbar = self.browser.find_element_by_css_selector('.navbar')
2048+
self.wait_for(lambda: self.browser.find_element(By.LINK_TEXT, "Log out"))
2049+
navbar = self.browser.find_element(By.CSS_SELECTOR, ".navbar")
21512050
self.assertIn(TEST_EMAIL, navbar.text)
21522051
21532052
# Now she logs out
2154-
self.browser.find_element_by_link_text('Log out').click()
2053+
self.browser.find_element(By.LINK_TEXT, "Log out").click()
21552054
21562055
# She is logged out
21572056
self.wait_for(
2158-
lambda: self.browser.find_element_by_name('email')
2057+
lambda: self.browser.find_element(By.CSS_SELECTOR, "input[name=email]")
21592058
)
2160-
navbar = self.browser.find_element_by_css_selector('.navbar')
2059+
navbar = self.browser.find_element(By.CSS_SELECTOR, ".navbar")
21612060
self.assertNotIn(TEST_EMAIL, navbar.text)
21622061
----
21632062
====
21642063

21652064
With that, we can see that the test is failing because the logout button
2166-
doesn't work:
2065+
doesn't actually do anything:
21672066

21682067
[subs=""]
21692068
----
21702069
$ <strong>python src/manage.py test functional_tests.test_login</strong>
21712070
[...]
21722071
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate
2173-
element: [name="email"]
2072+
element: input[name=email]; [...]
21742073
----
21752074

2176-
Implementing a logout button is actually very simple: we can use Django's
2177-
http://bit.ly/SuI0hA[built-in logout view], which clears down the user's
2178-
session and redirects them to a page of our choice:
2075+
2076+
So let's tell the base template that we want a new url named "logout":
21792077

21802078
[role="sourcecode small-code"]
2181-
.src/accounts/urls.py (ch19l051)
2079+
.src/lists/templates/base.html (ch19l051)
2080+
====
2081+
[source,html]
2082+
----
2083+
{% if user.email %}
2084+
<ul>
2085+
<span class="navbar-text">Logged in as {{ user.email }}</span>
2086+
<a href="{% url 'logout' %}">Log out</a>
2087+
</ul>
2088+
{% else %}
2089+
----
2090+
====
2091+
2092+
If you try the FTs at this point,
2093+
you'll see an error saying that URL doesn't exist yet:
2094+
2095+
[subs="specialcharacters,macros"]
2096+
----
2097+
$ pass:quotes[*python src/manage.py test functional_tests.test_login*]
2098+
Internal Server Error: /
2099+
[...]
2100+
django.urls.exceptions.NoReverseMatch: Reverse for 'logout' not found. 'logout'
2101+
is not a valid view function or pattern name.
2102+
2103+
======================================================================
2104+
ERROR: test_can_get_email_link_to_log_in
2105+
(functional_tests.test_login.LoginTest.test_can_get_email_link_to_log_in)
2106+
[...]
2107+
2108+
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate
2109+
element: Log out; [...]
2110+
----
2111+
2112+
2113+
2114+
Implementing a logout URL is actually very simple:
2115+
we can use Django's
2116+
https://docs.djangoproject.com/en/4.2/topics/auth/default/#module-django.contrib.auth.views[built-in logout view],
2117+
which clears down the user's session and redirects them to a page of our choice:
2118+
2119+
[role="sourcecode small-code"]
2120+
.src/accounts/urls.py (ch19l052)
21822121
====
21832122
[source,python]
21842123
----
@@ -2195,17 +2134,6 @@ urlpatterns = [
21952134
----
21962135
====
21972136

2198-
And in 'base.html', we just make the logout into a real URL link:
2199-
2200-
[role="sourcecode"]
2201-
.src/lists/templates/base.html (ch19l052)
2202-
====
2203-
[source,python]
2204-
----
2205-
<a href="{% url 'logout' %}">Log out</a>
2206-
----
2207-
====
2208-
22092137

22102138
And that gets us a fully passing FT--indeed, a fully passing test suite:
22112139

@@ -2215,22 +2143,22 @@ And that gets us a fully passing FT--indeed, a fully passing test suite:
22152143
$ pass:quotes[*python src/manage.py test functional_tests.test_login*]
22162144
[...]
22172145
OK
2218-
$ pass:quotes[*python src/manage.py test*]
2146+
$ pass:quotes[*cd src && python manage.py test*]
22192147
[...]
2220-
Ran 59 tests in 78.124s
2148+
Ran 57 tests in 78.124s
22212149
22222150
OK
22232151
----
22242152
//54
22252153

22262154

2227-
WARNING: ((("security issues and settings", "login systems")))We're
2228-
nowhere near a truly secure or acceptable login system
2229-
here. Since this is just an example app for a book, we'll leave it
2230-
at that, but in "real life" you'd want to explore a lot more security
2231-
and usability issues before calling the job done. We're dangerously
2232-
close to "rolling our own crypto" here, and relying on a more established
2233-
login system would be much safer.
2155+
WARNING: We're nowhere near a truly secure or acceptable login system here.
2156+
Since this is just an example app for a book, we'll leave it at that,
2157+
but in "real life" you'd want to explore a lot more security
2158+
and usability issues before calling the job done.
2159+
We're dangerously close to "rolling our own crypto" here,
2160+
and relying on a more established login system would be much safer.
2161+
((("security issues and settings", "login systems")))
22342162

22352163

22362164
In the next chapter, we'll start trying to put our login system to good use.

0 commit comments

Comments
 (0)