Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
8ea3e204
Commit
8ea3e204
authored
Oct 07, 2013
by
David Ormsbee
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1231 from edx/ormsbee/verify_tests3
Model tests for verify_student flow.
parents
31980150
02fc6a40
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
293 additions
and
30 deletions
+293
-30
lms/djangoapps/verify_student/models.py
+20
-9
lms/djangoapps/verify_student/tests/test_models.py
+273
-21
No files found.
lms/djangoapps/verify_student/models.py
View file @
8ea3e204
...
...
@@ -177,7 +177,7 @@ class PhotoVerification(StatusModel):
@classmethod
def
user_is_verified
(
cls
,
user
,
earliest_allowed_date
=
None
):
"""
Return
s
whether or not a user has satisfactorily proved their
Return whether or not a user has satisfactorily proved their
identity. Depending on the policy, this can expire after some period of
time, so a user might have to renew periodically.
"""
...
...
@@ -194,7 +194,11 @@ class PhotoVerification(StatusModel):
@classmethod
def
user_has_valid_or_pending
(
cls
,
user
,
earliest_allowed_date
=
None
):
"""
TODO: eliminate duplication with user_is_verified
Return whether the user has a complete verification attempt that is or
*might* be good. This means that it's approved, been submitted, or would
have been submitted but had an non-user error when it was being
submitted. It's basically any situation in which the user has signed off
on the contents of the attempt, and we have not yet received a denial.
"""
valid_statuses
=
[
'must_retry'
,
'submitted'
,
'approved'
]
earliest_allowed_date
=
(
...
...
@@ -210,11 +214,8 @@ class PhotoVerification(StatusModel):
@classmethod
def
active_for_user
(
cls
,
user
):
"""
Return all PhotoVerifications that are still active (i.e. not
approved or denied).
Should there only be one active at any given time for a user? Enforced
at the DB level?
Return the most recent PhotoVerification that is marked ready (i.e. the
user has said they're set, but we haven't submitted anything yet).
"""
# This should only be one at the most, but just in case we create more
# by mistake, we'll grab the most recently created one.
...
...
@@ -361,7 +362,9 @@ class PhotoVerification(StatusModel):
reviewing_service
=
""
):
"""
Mark that this attempt could not be completed because of a system error.
Status should be moved to `must_retry`.
Status should be moved to `must_retry`. For example, if Software Secure
reported to us that they couldn't process our submission because they
couldn't decrypt the image we sent.
"""
if
self
.
status
in
[
"approved"
,
"denied"
]:
return
# If we were already approved or denied, just leave it.
...
...
@@ -480,6 +483,8 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
self
.
save
()
except
Exception
as
error
:
log
.
exception
(
error
)
self
.
status
=
"must_retry"
self
.
save
()
def
image_url
(
self
,
name
):
"""
...
...
@@ -549,7 +554,13 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
return
headers
,
body
def
request_message_txt
(
self
):
"""This is the body of the request we send across."""
"""
This is the body of the request we send across. This is never actually
used in the code, but exists for debugging purposes -- you can call
`print attempt.request_message_txt()` on the console and get a readable
rendering of the request that would be sent across, without actually
sending anything.
"""
headers
,
body
=
self
.
create_request
()
header_txt
=
"
\n
"
.
join
(
...
...
lms/djangoapps/verify_student/tests/test_models.py
View file @
8ea3e204
# -*- coding: utf-8 -*-
from
datetime
import
timedelta
import
json
from
nose.tools
import
(
assert_in
,
assert_is_none
,
assert_equals
,
assert_raises
,
assert_not_equals
assert_in
,
assert_is_none
,
assert_equals
,
assert_not_equals
,
assert_raises
,
assert_true
,
assert_false
)
from
mock
import
MagicMock
,
patch
from
django.test
import
TestCase
from
django.conf
import
settings
import
requests
import
requests.exceptions
from
student.tests.factories
import
UserFactory
from
verify_student.models
import
SoftwareSecurePhotoVerification
,
VerificationException
from
util.testing
import
UrlResetMixin
import
verify_student.models
FAKE_SETTINGS
=
{
"SOFTWARE_SECURE"
:
{
"FACE_IMAGE_AES_KEY"
:
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
,
"API_ACCESS_KEY"
:
"BBBBBBBBBBBBBBBBBBBB"
,
"API_SECRET_KEY"
:
"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
,
"RSA_PUBLIC_KEY"
:
"""-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu2fUn20ZQtDpa1TKeCA/
rDA2cEeFARjEr41AP6jqP/k3O7TeqFX6DgCBkxcjojRCs5IfE8TimBHtv/bcSx9o
7PANTq/62ZLM9xAMpfCcU6aAd4+CVqQkXSYjj5TUqamzDFBkp67US8IPmw7I2Gaa
tX8ErZ9D7ieOJ8/0hEiphHpCZh4TTgGuHgjon6vMV8THtq3AQMaAQ/y5R3V7Lezw
dyZCM9pBcvcH+60ma+nNg8GVGBAW/oLxILBtg+T3PuXSUvcu/r6lUFMHk55pU94d
9A/T8ySJm379qU24ligMEetPk1o9CUasdaI96xfXVDyFhrzrntAmdD+HYCSPOQHz
iwIDAQAB
-----END PUBLIC KEY-----"""
,
"API_URL"
:
"http://localhost/verify_student/fake_endpoint"
,
"AWS_ACCESS_KEY"
:
"FAKEACCESSKEY"
,
"AWS_SECRET_KEY"
:
"FAKESECRETKEY"
,
"S3_BUCKET"
:
"fake-bucket"
}
}
class
MockKey
(
object
):
"""
Mocking a boto S3 Key object. It's a really dumb mock because once we
write data to S3, we never read it again. We simply generate a link to it
and pass that to Software Secure. Because of that, we don't even implement
the ability to pull back previously written content in this mock.
Testing that the encryption/decryption roundtrip on the data works is in
test_ssencrypt.py
"""
def
__init__
(
self
,
bucket
):
self
.
bucket
=
bucket
def
set_contents_from_string
(
self
,
contents
):
self
.
contents
=
contents
def
generate_url
(
self
,
duration
):
return
"http://fake-edx-s3.edx.org/"
class
MockBucket
(
object
):
"""Mocking a boto S3 Bucket object."""
def
__init__
(
self
,
name
):
self
.
name
=
name
class
MockS3Connection
(
object
):
"""Mocking a boto S3 Connection"""
def
__init__
(
self
,
access_key
,
secret_key
):
pass
def
get_bucket
(
self
,
bucket_name
):
return
MockBucket
(
bucket_name
)
def
mock_software_secure_post
(
url
,
headers
=
None
,
data
=
None
,
**
kwargs
):
"""
Mocks our interface when we post to Software Secure. Does basic assertions
on the fields we send over to make sure we're not missing headers or giving
total garbage.
"""
data_dict
=
json
.
loads
(
data
)
# Basic sanity checking on the keys
EXPECTED_KEYS
=
[
"EdX-ID"
,
"ExpectedName"
,
"PhotoID"
,
"PhotoIDKey"
,
"SendResponseTo"
,
"UserPhoto"
,
"UserPhotoKey"
,
]
for
key
in
EXPECTED_KEYS
:
assert_true
(
data_dict
.
get
(
key
),
"'{}' must be present and not blank in JSON submitted to Software Secure"
.
format
(
key
)
)
# The keys should be stored as Base64 strings, i.e. this should not explode
photo_id_key
=
data_dict
[
"PhotoIDKey"
]
.
decode
(
"base64"
)
user_photo_key
=
data_dict
[
"UserPhotoKey"
]
.
decode
(
"base64"
)
response
=
requests
.
Response
()
response
.
status_code
=
200
return
response
def
mock_software_secure_post_error
(
url
,
headers
=
None
,
data
=
None
,
**
kwargs
):
"""
Simulates what happens if our post to Software Secure is rejected, for
whatever reason.
"""
response
=
requests
.
Response
()
response
.
status_code
=
400
return
response
def
mock_software_secure_post_unavailable
(
url
,
headers
=
None
,
data
=
None
,
**
kwargs
):
"""Simulates a connection failure when we try to submit to Software Secure."""
raise
requests
.
exceptions
.
ConnectionError
# Lots of patching to stub in our own settings, S3 substitutes, and HTTP posting
@patch.dict
(
settings
.
VERIFY_STUDENT
,
FAKE_SETTINGS
)
@patch
(
'verify_student.models.S3Connection'
,
new
=
MockS3Connection
)
@patch
(
'verify_student.models.Key'
,
new
=
MockKey
)
@patch
(
'verify_student.models.requests.post'
,
new
=
mock_software_secure_post
)
class
TestPhotoVerification
(
TestCase
):
def
test_state_transitions
(
self
):
"""Make sure we can't make unexpected status transitions.
"""
Make sure we can't make unexpected status transitions.
The status transitions we expect are::
→ → → must_retry
↑ ↑ ↓
created → ready → submitted → approved
↑ ↓
→ denied
↓
↑ ↓
↓ →
→ denied
"""
user
=
UserFactory
.
create
()
attempt
=
SoftwareSecurePhotoVerification
(
user
=
user
)
assert_equals
(
attempt
.
status
,
SoftwareSecurePhotoVerification
.
STATUS
.
created
)
assert_equals
(
attempt
.
status
,
"created"
)
# These should all fail because we're in the wrong starting state.
...
...
@@ -29,29 +141,169 @@ class TestPhotoVerification(TestCase):
assert_raises
(
VerificationException
,
attempt
.
deny
)
# Now let's fill in some values so that we can pass the mark_ready() call
attempt
.
face_image_url
=
"http://fake.edx.org/face.jpg"
attempt
.
photo_id_image_url
=
"http://fake.edx.org/photo_id.jpg"
attempt
.
mark_ready
()
assert_equals
(
attempt
.
name
,
user
.
profile
.
name
)
# Move this to another test
assert_equals
(
attempt
.
status
,
"ready"
)
# Once again, state transitions should fail here. We can't approve or
# deny anything until it's been placed into the submitted state -- i.e.
# the user has clicked on whatever agreements, or given payment, or done
# whatever the application requires before it agrees to process their
# attempt.
# ready (can't approve or deny unless it's "submitted")
assert_raises
(
VerificationException
,
attempt
.
approve
)
assert_raises
(
VerificationException
,
attempt
.
deny
)
# Now we submit
#attempt.submit()
#assert_equals(attempt.status, "submitted")
DENY_ERROR_MSG
=
'[{"photoIdReasons": ["Not provided"]}]'
# must_retry
attempt
.
status
=
"must_retry"
attempt
.
system_error
(
"System error"
)
attempt
.
approve
()
attempt
.
status
=
"must_retry"
attempt
.
deny
(
DENY_ERROR_MSG
)
# submitted
attempt
.
status
=
"submitted"
attempt
.
deny
(
DENY_ERROR_MSG
)
attempt
.
status
=
"submitted"
attempt
.
approve
()
# approved
assert_raises
(
VerificationException
,
attempt
.
submit
)
attempt
.
approve
()
# no-op
attempt
.
system_error
(
"System error"
)
# no-op, something processed it without error
attempt
.
deny
(
DENY_ERROR_MSG
)
# denied
assert_raises
(
VerificationException
,
attempt
.
submit
)
attempt
.
deny
(
DENY_ERROR_MSG
)
# no-op
attempt
.
system_error
(
"System error"
)
# no-op, something processed it without error
attempt
.
approve
()
def
test_name_freezing
(
self
):
"""
You can change your name prior to marking a verification attempt ready,
but changing your name afterwards should not affect the value in the
in the attempt record. Basically, we want to always know what your name
was when you submitted it.
"""
user
=
UserFactory
.
create
()
user
.
profile
.
name
=
u"Jack
\u01B4
"
# gratuious non-ASCII char to test encodings
attempt
=
SoftwareSecurePhotoVerification
(
user
=
user
)
user
.
profile
.
name
=
u"Clyde
\u01B4
"
attempt
.
mark_ready
()
# So we should be able to both approve and deny
#attempt.approve()
#assert_equals(attempt.status, "approved")
user
.
profile
.
name
=
u"Rusty
\u01B4
"
assert_equals
(
u"Clyde
\u01B4
"
,
attempt
.
name
)
def
create_and_submit
(
self
):
"""Helper method to create a generic submission and send it."""
user
=
UserFactory
.
create
()
attempt
=
SoftwareSecurePhotoVerification
(
user
=
user
)
user
.
profile
.
name
=
u"Rust
\u01B4
"
attempt
.
upload_face_image
(
"Just pretend this is image data"
)
attempt
.
upload_photo_id_image
(
"Hey, we're a photo ID"
)
attempt
.
mark_ready
()
attempt
.
submit
()
return
attempt
def
test_submissions
(
self
):
"""Test that we set our status correctly after a submission."""
# Basic case, things go well.
attempt
=
self
.
create_and_submit
()
assert_equals
(
attempt
.
status
,
"submitted"
)
# We post, but Software Secure doesn't like what we send for some reason
with
patch
(
'verify_student.models.requests.post'
,
new
=
mock_software_secure_post_error
):
attempt
=
self
.
create_and_submit
()
assert_equals
(
attempt
.
status
,
"must_retry"
)
# We try to post, but run into an error (in this case a newtork connection error)
with
patch
(
'verify_student.models.requests.post'
,
new
=
mock_software_secure_post_unavailable
):
attempt
=
self
.
create_and_submit
()
assert_equals
(
attempt
.
status
,
"must_retry"
)
def
test_active_for_user
(
self
):
"""
Make sure we can retrive a user's active (in progress) verification
attempt.
"""
user
=
UserFactory
.
create
()
# This user has no active at the moment...
assert_is_none
(
SoftwareSecurePhotoVerification
.
active_for_user
(
user
))
# Create an attempt and mark it ready...
attempt
=
SoftwareSecurePhotoVerification
(
user
=
user
)
attempt
.
mark_ready
()
assert_equals
(
attempt
,
SoftwareSecurePhotoVerification
.
active_for_user
(
user
))
# A new user won't see this...
user2
=
UserFactory
.
create
()
user2
.
save
()
assert_is_none
(
SoftwareSecurePhotoVerification
.
active_for_user
(
user2
))
# If it's got a different status, it doesn't count
for
status
in
[
"submitted"
,
"must_retry"
,
"approved"
,
"denied"
]:
attempt
.
status
=
status
attempt
.
save
()
assert_is_none
(
SoftwareSecurePhotoVerification
.
active_for_user
(
user
))
# But if we create yet another one and mark it ready, it passes again.
attempt_2
=
SoftwareSecurePhotoVerification
(
user
=
user
)
attempt_2
.
mark_ready
()
assert_equals
(
attempt_2
,
SoftwareSecurePhotoVerification
.
active_for_user
(
user
))
# And if we add yet another one with a later created time, we get that
# one instead. We always want the most recent attempt marked ready()
attempt_3
=
SoftwareSecurePhotoVerification
(
user
=
user
,
created_at
=
attempt_2
.
created_at
+
timedelta
(
days
=
1
)
)
attempt_3
.
save
()
# We haven't marked attempt_3 ready yet, so attempt_2 still wins
assert_equals
(
attempt_2
,
SoftwareSecurePhotoVerification
.
active_for_user
(
user
))
# Now we mark attempt_3 ready and expect it to come back
attempt_3
.
mark_ready
()
assert_equals
(
attempt_3
,
SoftwareSecurePhotoVerification
.
active_for_user
(
user
))
def
test_user_is_verified
(
self
):
"""
Test to make sure we correctly answer whether a user has been verified.
"""
user
=
UserFactory
.
create
()
attempt
=
SoftwareSecurePhotoVerification
(
user
=
user
)
attempt
.
save
()
# If it's any of these, they're not verified...
for
status
in
[
"created"
,
"ready"
,
"denied"
,
"submitted"
,
"must_retry"
]:
attempt
.
status
=
status
attempt
.
save
()
assert_false
(
SoftwareSecurePhotoVerification
.
user_is_verified
(
user
),
status
)
attempt
.
status
=
"approved"
attempt
.
save
()
assert_true
(
SoftwareSecurePhotoVerification
.
user_is_verified
(
user
),
status
)
def
test_user_has_valid_or_pending
(
self
):
"""
Determine whether we have to prompt this user to verify, or if they've
already at least initiated a verification submission.
"""
user
=
UserFactory
.
create
()
attempt
=
SoftwareSecurePhotoVerification
(
user
=
user
)
#attempt.deny("Could not read name on Photo ID")
#assert_equals(attempt.status, "denied")
# If it's any of these statuses, they don't have anything outstanding
for
status
in
[
"created"
,
"ready"
,
"denied"
]:
attempt
.
status
=
status
attempt
.
save
()
assert_false
(
SoftwareSecurePhotoVerification
.
user_has_valid_or_pending
(
user
),
status
)
# Any of these, and we are. Note the benefit of the doubt we're giving
# -- must_retry, and submitted both count until we hear otherwise
for
status
in
[
"submitted"
,
"must_retry"
,
"approved"
]:
attempt
.
status
=
status
attempt
.
save
()
assert_true
(
SoftwareSecurePhotoVerification
.
user_has_valid_or_pending
(
user
),
status
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment