@@ -1382,8 +1382,7 @@ is a good time to check out the
1382
1382
docs on authentication] for a little more context.((("", startref="Mdespike19")))((("", startref="SDdesp19")))
1383
1383
1384
1384
1385
- An Alternative Reason to Use Mocks: Reducing Duplication
1386
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1385
+ === An Alternative Reason to Use Mocks: Reducing Duplication
1387
1386
1388
1387
((("mocks", "reducing duplication with", id="Mreduce19")))((("duplication, eliminating", id="dupel19")))So
1389
1388
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,
1404
1403
and creates new users for valid tokens if they don't exist yet. So, to fully
1405
1404
test this view, I'd have to write tests for all three of those cases.
1406
1405
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.
1411
1412
1412
1413
On top of that, the fact that we're using the Django
1413
1414
`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):
1866
1867
{% if user.email %}
1867
1868
<ul>
1868
1869
<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>
1870
1871
</ul>
1871
1872
{% else %}
1872
1873
<form method="POST" action="{% url 'send_login_email' %}">
@@ -1895,104 +1896,6 @@ TODO resume updates to chapter from here
1895
1896
****
1896
1897
1897
1898
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
- ====
1996
1899
1997
1900
1998
1901
How does our FT look now?
@@ -2011,13 +1914,12 @@ OK
2011
1914
2012
1915
2013
1916
2014
- It Works in Theory! Does It Work in Practice?
2015
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1917
+ === It Works in Theory! Does It Work in Practice?
2016
1918
2017
1919
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`:
2021
1923
2022
1924
2023
1925
[role="skipme"]
@@ -2033,9 +1935,7 @@ ConnectionRefusedError: [Errno 111] Connection refused
2033
1935
----
2034
1936
2035
1937
2036
- Using Our New Environment Variable, and Saving It to .env
2037
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2038
-
1938
+ ==== Using Our New Environment Variable, and Saving It to .env
2039
1939
2040
1940
You'll probably get an error, like I did, when you try to run things manually.
2041
1941
It's because of two things:
@@ -2047,9 +1947,9 @@ It's because of two things:
2047
1947
====
2048
1948
[source,python]
2049
1949
----
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" )
2053
1953
EMAIL_PORT = 587
2054
1954
EMAIL_USE_TLS = True
2055
1955
----
@@ -2105,6 +2005,7 @@ $ *python src/manage.py runserver*
2105
2005
2106
2006
...you should see something like <<despiked-success-message>>.
2107
2007
2008
+ //TODO: update screenshot
2108
2009
2109
2010
[[despiked-success-message]]
2110
2011
.Check your email....
@@ -2129,56 +2030,94 @@ $ *git commit -m "Custom passwordless auth backend + custom user model"*
2129
2030
2130
2031
2131
2032
2132
- Finishing Off Our FT, Testing Logout
2133
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2033
+ === Finishing Off Our FT, Testing Logout
2134
2034
2135
2035
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:
2139
2040
2140
2041
[role="sourcecode"]
2141
- .functional_tests/test_login.py (ch17l050)
2042
+ .src/ functional_tests/test_login.py (ch17l050)
2142
2043
====
2143
2044
[source,python]
2144
2045
----
2145
2046
[...]
2146
2047
# 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")
2151
2050
self.assertIn(TEST_EMAIL, navbar.text)
2152
2051
2153
2052
# 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()
2155
2054
2156
2055
# She is logged out
2157
2056
self.wait_for(
2158
- lambda: self.browser.find_element_by_name(' email' )
2057
+ lambda: self.browser.find_element(By.CSS_SELECTOR, "input[name= email]" )
2159
2058
)
2160
- navbar = self.browser.find_element_by_css_selector('. navbar' )
2059
+ navbar = self.browser.find_element(By.CSS_SELECTOR, ". navbar" )
2161
2060
self.assertNotIn(TEST_EMAIL, navbar.text)
2162
2061
----
2163
2062
====
2164
2063
2165
2064
With that, we can see that the test is failing because the logout button
2166
- doesn't work :
2065
+ doesn't actually do anything :
2167
2066
2168
2067
[subs=""]
2169
2068
----
2170
2069
$ <strong>python src/manage.py test functional_tests.test_login</strong>
2171
2070
[...]
2172
2071
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate
2173
- element: [name=" email" ]
2072
+ element: input [name=email]; [... ]
2174
2073
----
2175
2074
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":
2179
2077
2180
2078
[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)
2182
2121
====
2183
2122
[source,python]
2184
2123
----
@@ -2195,17 +2134,6 @@ urlpatterns = [
2195
2134
----
2196
2135
====
2197
2136
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
-
2209
2137
2210
2138
And that gets us a fully passing FT--indeed, a fully passing test suite:
2211
2139
@@ -2215,22 +2143,22 @@ And that gets us a fully passing FT--indeed, a fully passing test suite:
2215
2143
$ pass:quotes[*python src/manage.py test functional_tests.test_login*]
2216
2144
[...]
2217
2145
OK
2218
- $ pass:quotes[*python src/ manage.py test*]
2146
+ $ pass:quotes[*cd src && python manage.py test*]
2219
2147
[...]
2220
- Ran 59 tests in 78.124s
2148
+ Ran 57 tests in 78.124s
2221
2149
2222
2150
OK
2223
2151
----
2224
2152
//54
2225
2153
2226
2154
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")))
2234
2162
2235
2163
2236
2164
In the next chapter, we'll start trying to put our login system to good use.
0 commit comments