Update docs and tests

This commit is contained in:
Matt Wright
2012-10-10 13:36:59 -04:00
parent 6b80aae7d1
commit a269930ec3
15 changed files with 824 additions and 506 deletions
+2 -2
View File
@@ -6,8 +6,8 @@ python:
install:
- pip install . --quiet --use-mirrors
- "if [[ $TRAVIS_PYTHON_VERSION != '2.7' ]]; then pip install importlib simplejson --quiet --use-mirrors; fi"
- pip install nose Flask-SQLAlchemy Flask-MongoEngine Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors
- "if [[ $TRAVIS_PYTHON_VERSION != '2.7' ]]; then pip install importlib --quiet --use-mirrors; fi"
- pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors
before_script:
- mysql -e 'create database flask_security_test;'
+297
View File
@@ -0,0 +1,297 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="184.621px" height="267.873px" viewBox="0 0 184.621 267.873" style="enable-background:new 0 0 184.621 267.873;"
xml:space="preserve">
<path d="M42.3,6.712c0.374-3.417,6.743-3.01,6.24,0C48.174,8.905,43.253,8.469,42.3,6.712z M46.14,7.432
c0.102-0.539,1.273-0.006,1.44-0.48c-0.011-0.331,0.113-0.527,0.24-0.72c-0.386-1.446-4.505-1.467-4.32,0.479
c0.463,0.287,1.217,0.915,1.68,0.48c0.409-0.099-0.051-0.31,0-0.72c0.689-0.053,1.668-1.192,1.92-0.24
C47.161,6.872,45.517,7.023,46.14,7.432z"/>
<path d="M61.02,8.872c-1.514,1.428-5.308,0.636-6.239-0.72C54.424,4.301,62.875,4.691,61.02,8.872z M55.98,8.152
c0.547,0.174,1.227,0.214,1.681,0.48c-0.31-1.429,1.293-0.947,2.16-1.2c0.022,0.377-0.032,0.832,0.479,0.72
C60.766,5.738,55.542,5.836,55.98,8.152z"/>
<path d="M75.18,11.272c-2.44,1.066-5.342-0.024-6.479-1.68C69,5.478,77.3,6.38,75.18,11.272z M70.14,9.352
c0.348,0.453,0.536,1.064,1.44,0.96c-0.42-1.412,1.778-1.107,2.16-0.96c-0.09,0.79-1.276,0.484-1.2,1.44
c1.447,0.248,1.821-0.578,2.16-1.44C74.528,7.553,70.227,7.19,70.14,9.352z"/>
<path d="M38.701,9.352c-0.632,0.249-1.473,0.287-2.4,0.24C36.181,8.614,38.749,8.071,38.701,9.352z"/>
<path d="M44.94,9.352c-0.336,1.452-3.19,0.276-4.319,0.24C41.392,8.351,43.547,9.639,44.94,9.352z"/>
<path d="M51.661,10.792c-2.162,0.242-3.118-0.722-5.28-0.48C46.883,9.482,51.405,9.765,51.661,10.792z"/>
<path d="M86.22,16.072c-2.685,0.164-5.312-0.956-5.28-2.88C81.011,8.895,91.422,11.543,86.22,16.072z M82.14,13.192
c0.572,1.569,2.509,2.415,4.32,1.44c0.099-1.059-0.065-1.855-0.48-2.4C84.701,11.79,82.343,11.814,82.14,13.192z"/>
<path d="M60.3,11.992c-1.742,0.618-4.228-0.089-5.76-0.48C55.288,10.927,59.201,11.604,60.3,11.992z"/>
<path d="M66.781,13.672c-1.624-0.216-3.132-0.548-4.561-0.96C63.243,11.997,65.987,12.678,66.781,13.672z"/>
<path d="M85.98,13.432c0.126,1.215-1.898,1.554-2.399,0.72C83.832,13.513,85.72,13.266,85.98,13.432z"/>
<path d="M70.14,14.392c3.491-0.13,7.434,1.367,11.04,1.92c-0.615,0.954-2.605-0.277-3.6-0.48
C74.54,15.913,72.676,14.816,70.14,14.392z"/>
<path d="M84.3,16.792c1.247,0.273,2.713,0.327,3.84,0.72C87.493,17.593,84.601,17.856,84.3,16.792z"/>
<path d="M43.5,20.632c-0.992,0.528-1.736,1.304-1.68,2.88c-0.07,1.03,1.131,0.789,1.199,1.68
C40.576,24.951,40.535,20.329,43.5,20.632z"/>
<path d="M43.98,20.872c2.421-0.874,2.685,4.746-0.479,4.08c0.299-0.661,1.226-0.694,1.68-1.2
C45.374,22.199,45.004,21.208,43.98,20.872z"/>
<path d="M32.46,29.752c-0.48,0.342,0.184,1.561,0.72,1.68c-1.511,0.933-1.827-2.226-0.96-3.12c3.597-2.01,5.532,3.849,1.44,3.6
C33.964,30.488,32.242,31.091,32.46,29.752z"/>
<path d="M25.741,33.112c1.15,0.084-0.513,0.719-0.48,1.2c0.018,1.183-0.203,2.604,0.96,2.64c-0.592-0.841,0.243-2.458,0.72-3.6
c2.731,0.658,1.781,4.199-0.239,4.32C24.472,37.806,23.329,34.219,25.741,33.112z"/>
<path d="M38.22,42.712c-0.253-1.106-1.293-1.427-0.96-3.12c-0.61,0.029-0.931,0.349-0.96,0.96
C35.887,35.461,44.143,41.79,38.22,42.712z"/>
<path d="M47.1,46.072c-1.321-2.109-1.041-5.914,2.88-4.8C52.478,43.065,50.231,46.405,47.1,46.072z M46.861,42.472
c0.049,0.751-0.162,1.762,0.239,2.16c0.739,0.179,0.462-0.659,1.2-0.48c-0.267,0.753-0.112,0.634,0.24,1.2
C52.363,44.767,48.946,39.561,46.861,42.472z"/>
<path d="M37.5,42.232C36.934,43.004,36.53,41.388,37.5,42.232L37.5,42.232z"/>
<path d="M30.781,42.952c0.56,0,1.12,0,1.68,0c1.986,1.451,0.51,4.935-2.4,3.6c-0.156-0.835-0.882-2.003-0.239-2.88
c0.47,0.97-0.006,2.885,1.68,2.64C31.005,45.534,30.001,44.231,30.781,42.952z"/>
<path d="M4.861,47.512c0.233,0.006,0.186,0.293,0.239,0.48c-0.204,1.471-1.334,2.713-1.92,4.08c-0.557,1.3-0.519,3.166-1.92,3.84
C1.854,52.506,3.317,49.968,4.861,47.512z"/>
<path d="M54.3,62.632c2.747,3.82-5.6,6.558-4.08,0.72C50.871,62.01,52.984,62.126,54.3,62.632z M53.821,63.592
c-1.709-2.567-5,1.958-2.16,1.92c-0.536-0.474,1.761-1.205,1.68,0C53.614,64.986,53.959,64.531,53.821,63.592z"/>
<path d="M53.821,85.192c0.891-1.578,4.269-2.012,5.279-0.24C60.584,90.521,52.591,90.472,53.821,85.192z M57.901,84.952
c-0.284-0.116-0.545-0.256-0.721-0.48c-2.651-0.412-3.296,2.625-1.68,3.84C57.95,88.96,58.699,86.766,57.901,84.952z"/>
<path d="M56.701,86.152c0.16,0,0.319,0,0.479,0c0,0.16,0,0.32,0,0.48c-0.16,0-0.319,0-0.479,0
C56.701,86.472,56.701,86.312,56.701,86.152z"/>
<path d="M55.5,87.352c0.621-0.306,1.539-0.306,2.16,0C57.558,88.531,55.73,88.054,55.5,87.352z"/>
<path d="M47.821,95.272c0.654,3.768-3.346,3.171-3.601,1.2C43.964,94.484,46.394,93.683,47.821,95.272z M45.901,97.432
c0.159-0.334-0.05-1.905-0.721-2.16C44.746,95.882,44.954,97.453,45.901,97.432z"/>
<path d="M40.861,98.632c0.105,1.306-0.608,1.792-0.96,2.64c-0.801,0-1.601,0-2.4,0C35.321,99.4,38.75,95.462,40.861,98.632z
M38.94,101.032c-0.208-0.752-0.594-1.325-1.199-1.68C37.674,100.378,38.437,100.576,38.94,101.032z"/>
<path d="M61.5,108.232c0.545,1.2,0.064,2.764-0.24,3.84C55.917,115.484,55.839,104.695,61.5,108.232z M57.661,110.632
c0.652,3.805,5.875-1.417,1.439-2.4C58.46,108.886,57.509,109.749,57.661,110.632z"/>
<path d="M40.621,113.512c-0.878-0.801-1.726-1.134-1.44-2.64c0.344-1.811,3.449-2.23,3.84-0.24
C43.258,111.84,42.221,112.906,40.621,113.512z M40.861,112.552c-0.223-0.578-0.438-1.162-0.721-1.68
c-0.075,0.325-0.342,0.458-0.239,0.96C40.671,111.621,40.229,112.623,40.861,112.552z"/>
<path d="M59.581,109.672c0.295,0.105,0.493,0.308,0.479,0.72c-0.388,0.068-0.413-0.227-0.72-0.24
C59.386,109.958,59.582,109.913,59.581,109.672z"/>
<path d="M58.621,110.632c0.744-0.024,1.146,0.293,1.68,0.48C60.05,111.75,58.254,111.922,58.621,110.632z"/>
<path d="M64.621,135.353c0.086-1.606-0.272-2.769-0.96-3.601c-3.562-0.15-7.859,0.044-11.28,0.72c0.133,0.987,0.688,1.552,0.72,2.64
c-0.504,0.299-1.229,1.1-1.92,0.479c-0.299-1.02,0.799-0.642,0.96-1.2c0.019-1.458-0.993-1.886-0.72-3.6
c1.929,0.088,2.346-0.335,3.36,0.24c0.645,0.165,0.464-0.496,0.72-0.72c0.646-0.165,0.464,0.496,0.72,0.72
c0.461-0.957,1.359-0.047,1.681-0.96c0.645-0.166,0.463,0.496,0.72,0.72c0.422-0.058,0.547-0.413,0.479-0.96
c0.505,0.528,0.937,0.528,1.44,0c0.523-0.043,0.764,0.196,0.72,0.72c0.946-0.028,0.734-1.043,1.68-0.479
c0.631-0.161,0.505,0.614,0.721,0.72c0.215,0.105,0.423-0.477,0.479-0.48c0.373-0.021,0.549,1.291,1.92,0.72
c0.171,1.864,0.423,2.749,0.721,4.08c3.811,0.014,0.37-4.601,0-6.24c-5.258-0.676-12.689,0.141-17.521,1.44
c0.228,1.728,1.729,3.344,1.2,5.041c-1.065-1.496-2.048-3.073-2.4-5.28c0.233-0.007,0.187-0.294,0.24-0.48
c4.899-1.854,12.457-2.218,18.48-2.16c0.611,0.645,1.104,1.897,1.68,3.36c0.457,1.163,1.41,2.914,1.2,3.84
C69.249,136.444,66.248,137.033,64.621,135.353z"/>
<path d="M44.22,138.231c0.101,2.525-3.294,3.207-5.04,2.4C39.008,138.113,42.058,137.625,44.22,138.231z M40.14,140.394
c1.207,0.246,1.316-0.604,1.92-0.961c0.345,0.366,1.251,0.233,1.2-0.479C42.089,138.349,39.939,138.825,40.14,140.394z"/>
<path d="M64.38,138.474c3.496-0.535,7.481,0.518,10.08,1.68c-0.366,1.195-1.776,0.125-2.4,0
C69.596,139.659,66.998,138.842,64.38,138.474z"/>
<path d="M95.581,143.271c-2.942,0.644-5.514-1.002-6-3.6C91.774,136.823,96.583,138.774,95.581,143.271z M91.98,139.192
c-0.631,0.644-1.162,2.258,0.24,2.398c-0.14-0.859,0.711-0.729,1.44-0.72c0.058,0.343,0.395,0.405,0.479,0.72
c-0.166,0.475-1.339-0.059-1.438,0.48c0.669,0.305,1.187,0.592,1.92,0.24C94.852,140.16,93.672,139.42,91.98,139.192z"/>
<path d="M32.22,145.192c-0.032,2.367-1.528,3.271-3.84,3.359C27.594,146.242,29.819,143.58,32.22,145.192z M29.1,147.832
c1.404-0.115,1.685-1.354,2.4-2.16C30.131,144.871,28.847,146.213,29.1,147.832z"/>
<path d="M86.701,146.151c1.792,0.688,3.44,1.52,4.56,2.881C89.319,148.494,87.486,147.847,86.701,146.151z"/>
<path d="M114.061,170.151c-0.939,0.369-1.713-1.262-2.16-2.159c-3-4.039-7.217-6.863-10.319-10.8
c-3.623-1.817-5.706-5.176-9.36-6.961c0.085-0.274,0.423-0.607,0.48-0.239c3.505,1.541,6.238,4.271,9.119,6.72
c1.355,1.153,2.771,2.489,4.08,3.841C108.777,163.521,112.146,165.974,114.061,170.151z"/>
<path d="M114.541,155.271c-2.948-0.092-4.854-1.227-4.8-4.318C112.334,147.631,117.725,152.144,114.541,155.271z M110.939,151.433
c0.082,1.918,1.41,2.592,3.121,2.881c0.135-0.347,0.375-0.586,0.721-0.722C115.197,151.412,112.15,149.688,110.939,151.433z"/>
<path d="M111.9,152.151c0.914,0.046,1.58,0.342,1.681,1.199C112.934,153.824,111.773,153.204,111.9,152.151z"/>
<path d="M130.621,163.912c-2.326,0.858-4.162-0.562-4.32-2.642C127.866,158.646,132.686,160.926,130.621,163.912z M127.741,161.992
c1.332-0.946,1.198,0.989,1.68,1.2c0.062-0.338,0.526-0.274,0.72-0.48c-0.092-0.788-0.701-1.059-0.959-1.68
C128.372,160.879,127.077,161.467,127.741,161.992z"/>
<path d="M146.939,179.992c-3.486,2.58-6.062-3.995-3.358-5.04C146.005,174.017,147.825,177.625,146.939,179.992z M145.741,179.512
c1.25-1.596-0.574-3.557-1.921-4.079C143.225,177.119,143.824,179.551,145.741,179.512z"/>
<path d="M144.061,177.353c0.805-0.164,0.877,0.402,0.959,0.961C144.27,178.424,143.949,178.104,144.061,177.353z"/>
<path d="M153.42,206.151c0.619,0.422,0.499,1.582,0.721,2.4C153.458,208.192,153.473,207.14,153.42,206.151z"/>
<path d="M170.701,263.271c1.24,0.039,1.096,1.465,2.64,1.201c0.423-0.059,0.548-0.414,0.479-0.961c0.088-1.207-2.1-0.139-1.68-1.68
c1.793-0.824,3.146,0.895,3.119,1.92C175.193,266.483,169.829,266.367,170.701,263.271z"/>
<path d="M166.621,263.271c0.523,0.037,0.516,0.605,0.479,1.201C166.387,264.522,166.254,263.616,166.621,263.271z"/>
<path d="M29.581,13.432c-1.118,1.762-2.535,3.225-4.561,4.08c-0.978,1.865-3.021,2.788-4.319,4.32
c-0.634,0.748-1.043,1.827-1.681,2.64c-0.617,0.788-1.547,1.365-2.159,2.16c-1.095,1.42-1.889,3.176-2.881,4.8
c-2.798,4.579-5.413,9.826-8.159,14.88c-0.323-1.007,0.498-2.206,0.96-3.12c1.375-2.724,2.946-5.975,4.56-8.64
c1.747-4.172,4.961-6.879,6.96-10.8c2.189-1.661,3.682-4.135,5.76-6c0.666-0.597,1.669-0.798,2.4-1.44
C27.522,15.38,27.938,13.633,29.581,13.432z"/>
<path d="M150.061,192.231c1.83,1.771,1.967,5.232,2.399,8.4C151.748,197.745,150.889,195.005,150.061,192.231z"/>
<path d="M152.46,200.872c0.647,1.271,0.92,2.92,0.96,4.8C152.854,204.317,152.702,202.551,152.46,200.872z"/>
<path d="M48.541,110.872c-3.429-3.012,2.593-5.527,2.88-2.16C51.547,110.201,50.337,110.811,48.541,110.872z M48.3,109.672
c0.668,0.385,0.664-1.036,0.24-0.96C48.465,109.037,48.199,109.17,48.3,109.672z"/>
<path d="M21.42,160.792c1.934-1.906,2.754-4.927,5.28-6.239c-2.298,3.544-5.058,6.622-6.48,11.039
c3.271-4.889,6.15-10.17,10.561-13.92c0.353-0.912-1.073-0.047-0.721-0.96c2.43-1.57,3.76-4.24,6.96-5.04
c-0.101,0.779-0.997,0.763-1.199,1.44c2.234-1.125,3.954-2.766,6.479-3.602c0.562-0.078,0.233-1.047,0.72-1.199
c3.039-1.44,6.383-2.578,10.561-2.881c0.624,0.633-0.388,0.189-0.24,0.961c0.72,0,1.44,0,2.16,0c-1.44-1.789-1.267-3.211,0.24-5.76
c0.573-0.972,1.111-2.216,1.92-2.4c0.204,2.628-2.214,3.833-1.92,5.76c1.9,0.694,4.79-0.966,6.96,0
c-0.317,1.442-3.182,0.339-4.32,0.96c6.704,0.176,12.813,0.945,17.521,3.12c3.945-1.339,7.001,2.024,9.6,3.6
c0.481,0.488,0.062,0.64-0.24,0.961c4.866,3.396,9.953,6.079,14.4,10.079c2.279,2.051,4.136,3.671,6.24,6
c0.603,0.668,1.541,1.234,2.158,1.921c0.623,0.689,0.996,1.632,1.682,2.399c1.046,1.172,2.52,1.681,2.88,3.601
c3.112,1.927,6.022,4.056,10.08,5.039c-2.345-1.815-5.804-2.518-8.16-4.319c1.046-0.662,1.854,0.629,2.64,0.961
c0.989,0.416,2.129,0.528,3.12,0.959c1.798,0.779,3.322,1.97,5.04,2.881c4.528,2.402,9.151,4.309,14.16,6.479
c-0.441,1-0.904,0.837,0.24,1.439c0.377-0.503,0.219-2.104,0-3.12c-0.719-3.334-2.582-6.436-4.08-9.602
c-1.439-3.043-3.422-6.012-3.84-9.6c0.725-1.034,2-1.52,3.359-1.92c-1.265-7.012-1.57-15.238,0.239-22.32
c0.979-3.827,2.716-7.71,4.08-11.76c1.356-4.028,2.086-8.338,3.36-12.48c1.813-5.897,4.24-11.626,6-17.76
c0.576-2.014,1.359-3.847,1.92-5.76c0.439-1.503,0.457-3.101,0.96-4.56c2.241-6.504,4.963-13.056,7.92-19.2
c1.229-2.554,2.055-5.326,3.358-7.44c0.334-0.541,1.066-0.881,1.44-1.44c0.547-0.818,0.683-1.998,1.2-2.88
c0.468-0.798,1.158-1.408,1.68-2.16c1.143-1.652,2.04-3.624,3.12-4.8c2.771-3.019,6.896-4.626,9.12-6.96
c-1.417-0.79-3.008-0.504-4.561-0.72c-15.819-2.198-31.188-5.902-45.119-10.08c-6.028-1.02-12.305-2.654-18-4.32
c-4.121-1.205-7.994-2.168-12-3.36c-1.084-0.322-2.811-0.442-3.36-1.68c-1.199,0.555-2.575-0.564-3.84-0.72
c-1.944-2.305-1.838-4.593-3.36-6.96c-2.579-4.011-10.535-4.885-16.8-6c-5.283-0.94-11.398-1.612-17.28-1.92
c-2.405-0.126-4.836,0.255-6.72,0c-0.96-0.13-1.744-0.689-2.64-0.72c-4.317-0.151-7.787,3.14-10.801,4.08
c1.681,8.378,5.883,16.13,8.16,24.72c0.181,0.679,0.077,1.48,0.24,2.16c1.192,4.97,2.028,11.915,2.88,18.24
c0.325,2.417,0.567,4.839,0.96,6.96c0.995,5.377,1.769,10.711,2.64,16.08c0.621,3.823,1.868,7.249,1.921,11.28
c0.023,1.867,0.833,3.842,1.199,5.76c1.145,5.99,1.941,11.323,3.12,17.76c0.664,3.625,1.143,6.605,1.92,10.56
c0.213,1.082,0.807,3.903,0.48,5.76c-1.362-0.676-0.612-2.755-0.72-3.84c-0.321-3.231-1.526-6.321-2.16-9.6
c-0.812-4.2-1.122-8.545-1.92-12.96c-0.743-4.109-1.776-8.241-2.4-12.48c-0.624-4.238-1.288-8.525-2.16-12.72
c-0.891-4.285-1.224-8.516-1.92-12.72c-1.09-6.581-2.213-13.001-2.88-19.44c-1.357-13.099-6.619-24.24-10.8-34.56
c-1.124,0.297-2.572,1.345-3.601,0.96c1.537-1.967,4.458-2.744,7.2-4.08c3.129-1.523,6.393-3.895,10.32-2.64
c9.006-3.378,18.614-3.365,29.04-2.88c3.564,0.166,6.843,0.097,10.08,0.72c2.062,0.397,4.294,0.803,6.479,1.44
c0.57,0.166,1.107,0.608,1.681,0.72c0.588,0.115,1.326-0.156,1.92,0c3.017,0.792,6.084,2.503,9.121,3.6
c9.119,3.295,17.252,8.793,24,14.16c13.316,2.752,27.009,5.719,39.84,9.12c3.088,0.819,6.115,2.127,9.119,3.12
c2.354,0.778,4.222,0.81,5.761,2.4c0,1.12,0,2.24,0,3.36c-1.306,1.416-2.587,2.854-4.8,3.36c-1.762,1.354-3.555,2.489-5.28,3.84
c-4.033,3.158-6.948,7.572-9.601,12.24c-2.646,4.656-5.75,9.431-6.961,14.88c-3.103,4.097-3.572,10.826-6,15.6
c-0.846,4.674-2.132,8.907-3.84,12.72c-3.023,12.093-7.494,23.404-10.8,36c-1.067,4.07-2.05,8.055-1.68,12.721
c0.192,2.447,1.304,4.592,0.96,7.92c2.38,2.41,5.91,1.207,6.479,4.32c0.149,0.815-0.469,1.732-0.238,2.641
c0.31,1.228,1.4,1.582,2.158,2.64c0.367,0.513,0.401,1.11,0.721,1.681c0.6,1.071,1.973,1.904,1.921,3.12
c-1.71-1.116-5.939-0.12-6.722-2.641c-0.326-1.056-0.051-2.962,0.961-3.84c-0.551-0.81-1.742-0.979-2.399-1.681
c-0.479,0.642-0.043,2.196-0.96,2.399c-1.686,0.006-2.42-0.94-3.36-1.681c2.378,6.023,5.962,10.84,7.2,18
c-0.499,0.521-0.775-0.028-1.2-0.239c0.568,1.512,1.103,3.06,2.642,3.601c0.24-0.32,0.396-0.725,0.72-0.961
c-0.04-1.24-2.19-0.369-1.92-1.92c2.896,1.111,8.071,0.932,6.72,5.521c1.604-0.086,1.508-1.227,1.439-2.16
c-0.116-1.637-1.732-1.745-2.159-2.641c2.247-0.848,2.809,1.961,3.6,3.359c1.896-1.439-0.931-3.092-0.72-4.08
c2.275-0.436,1.814,1.866,2.88,2.641c0.519-1.938-1.698-2.217-0.96-3.6c-1.312-0.848-1.551-2.77-2.16-4.32
c-0.871-0.131-1.943,0.078-2.88-0.479c-0.745-1.229-1.812-3.401-0.72-5.04c1.875-0.539,3.267,0.706,3.84,1.92
c0.294,0.623,0.039,1.396,0.24,2.16c0.467,1.771,1.71,3.521,1.92,5.04c1.044,0.637,2.349,1.011,2.88,2.16
c-0.806,3.459-0.021,6.384,0.96,10.319c0.884,3.547,1.48,6.951,2.4,10.319c0.781,2.868,1.963,7.187,2.64,10.56
c0.11,0.556-0.071,1.119,0,1.682c0.508,3.995,2.243,8.225,3.36,12.479c1.8,6.861,2.722,13.881,4.318,21.121
c0.688,3.112,2.145,5.914,2.4,8.879c6.355-0.617,5.865,10.313-1.44,8.16c-0.606-0.666-2.256-1.324-1.438-2.64
c1.234,1.05,1.816,1.95,3.6,1.681c1.979-0.301,3.092-2.835,1.92-4.801c-1.634-1.834-3.564,0.15-5.279-0.24
c-0.351-1.471,1.483-0.756,1.439-1.92c0.104-1.543-0.771-2.107-0.96-3.359c-0.397,0.242-0.261,1.021-0.479,1.439
c-1.474-0.113-1.64,1.081-2.881,1.2c-0.815-1.983-1.211-4.39-1.68-6.72c0.596-0.604,2.02-0.381,2.16-1.44
c0.73-0.011,0.934,0.507,1.439,0.72c0.184-1.303-0.297-1.943-0.24-3.119c-0.743,0.317-1.233,0.744-1.92,1.199
c-0.512,0.342-0.961,1.149-1.92,0.961c-0.873-1.607-0.926-4.035-1.199-6.24c1.027-0.469,2.085-1.998,3.6-1.199
c-0.049-2.191-0.803-3.678-0.96-5.762c-0.661,0.756,0.343,1.889,0,3.121c-0.632,0.172-0.909,1.42-1.68,0.721
c-0.278,0.762-0.327,1.752-1.44,1.68c-0.63-1.93-1.523-3.596-1.439-6.239c1.488-0.493,2.322-2.764,4.08-1.2
c-0.291-2.67-1.105-4.814-1.681-7.199c-0.632,0.961,0.533,2.18,0.24,3.6c-0.776,1.223-2.252,1.748-2.88,3.12
c-1.539-1.251-1.508-4.423-1.92-6.72c0.602-0.373,0.804-0.975,1.68-0.48c0.105-0.693,0.506-1.094,1.2-1.199
c0.582,0.297,1.116,0.643,1.438,1.199c-0.136-1.464-0.621-2.578-0.959-3.84c-1.062,0.138-0.719,1.682-1.92,1.68
c-0.482,0.398-0.183,1.578-0.961,1.68c-2.101-0.4-2.123-4.084-2.16-5.76c0.31-1.292,1.816-1.384,2.642-2.16
c1.34,0.42,1.255,2.267,1.92,3.36c-0.037-1.646-0.894-4.545-1.2-6.72c-1.124,0.044-3.02,1.266-3.6,2.641
c-0.644,0.646-0.913-0.307-1.44-0.48c0.653,3.693,1.729,7.789,2.64,11.761c0.703,3.063,1.023,6.019,2.4,7.92
c0.721,8.088,4.225,15.449,5.28,24c-1.283-1.156-1.265-3.088-1.681-4.562c-2.156-7.615-4.125-15.852-5.761-24.238
c-1.569-8.059-4.251-15.672-5.04-23.762c1.043,0.799,0.825,2.855,1.2,4.32c0.996-0.684,2.788-0.572,3.12-1.92
c0.891-0.17,1.029,0.41,1.92,0.24c-1.221-6.25-2.62-12.502-4.08-18.721c-0.438-1.863-0.727-4.033-1.92-5.521
c-1.1,0.021-1.098,1.144-1.92,1.44c-2.129-0.365-2.719,1.443-5.28,1.68c1.589,9.702,4.788,19.036,6.479,28.8
c0.302,1.738,0.367,3.38,0.721,5.041c0.557,2.615,1.475,5.364,2.16,8.64c2.146,10.254,6.004,21.727,8.16,32.399
c2.027,0.351,2.928-0.432,4.318-0.72c0.418,2.652-3.131,2.361-5.039,1.92c-6.931-24.35-12.082-50.479-18-75.84
c-2.002-1.438-2.99-3.891-4.801-5.521c-0.678-0.197-0.621,0.34-1.199,0.24c-4.386-2.818-9.349-4.729-14.16-7.199
c-4.744-2.436-9.889-4.223-14.159-7.201c-1.263-3.646-4.685-6.091-7.681-9.119c-2.334-2.357-4.869-4.594-7.439-6.721
c-2.421-2.001-5.087-3.658-8.4-5.76c-2.435-1.543-6.654-4.709-9.12-4.8c-1.204-0.044-2.162,0.894-3.359,1.44
c-1.265,0.575-2.666,0.76-3.84,1.438c-2.599,1.504-6.572,4.528-8.641,6.721c-0.7,0.742-1.223,2.046-1.92,3.119
c-1.686,2.597-2.355,3.652-3.6,6.961c-1.01,2.685-1.321,5.697-2.16,9.119c-0.304,1.24-0.88,3.11-0.96,5.28
c-0.161,4.354-0.081,9.812,0.479,14.16c0.47,3.643,1.634,5.873-1.439,7.199c1.128,5.896-2.103,8.493-7.2,7.92
c-1.755-0.196-3.433-1.703-4.8-1.439c-0.772,0.148-1.158,1.165-1.68,1.199c-0.012,2.816,3.443,8.568,3.119,12
c-0.345,3.664-5.084,4.781-8.159,2.4c-2.018-5.104-3.257-10.984-5.04-16.32c-1.104-1.088-2.422-0.971-4.32-1.68
c-0.448-0.168-0.741-0.642-1.2-0.721c-2.181-0.375-4.735,0.471-6.479-0.24c-1.671-0.68-1.605-2.477-2.881-3.359
c-2.689-13.207-0.238-23.942,3.36-34.08c0.355-1,1.099-1.922,1.44-2.879c0.316-0.893,0.148-1.949,0.479-2.881
c0.616-1.729,1.61-3.226,2.16-5.041c1.552-5.117,3.843-11.199,4.8-17.278c1.435-9.108,0.173-19.464-1.439-27.84
c-0.581-3.016-0.713-5.869-1.44-8.64c-0.86-3.279-2.544-6.25-3.36-9.36c-0.188-0.718,0.001-1.275-0.239-1.92
c-0.484-1.298-1.316-2.454-1.92-3.84c-0.254-0.583-0.681-1.277-0.96-1.92c-0.551-1.265-1.039-2.307-1.44-4.08
c-1-1.933-1.815-3.799-2.88-5.52c-3.057-4.945-6.939-9.452-10.561-14.16c-1.982-1.733-3.234-2.961-2.88-6.96
c0.488-5.49,3.643-10.933,6-16.32c2.413-5.511,5.669-10.261,8.16-13.92c-0.681-3.26,0.508-5.816,4.32-5.28
c1.648-1.083,0.856-4.006,2.88-4.8c0.818-0.321,1.811,0.003,2.4-0.24c1.817-0.749,1.167-2.514,2.399-3.36
c1.08-0.742,2.506-0.357,3.601,0c-0.578,2.316-2.125,4.626-5.04,3.84c0.284,2.28-1.472,3.962-3.12,5.04
c-0.904,0.519-1.017-0.376-1.92-0.479c-0.671,0.613,1.168,1.048,0.239,1.92c-0.883,2.076-2.336,3.584-4.56,4.32
c-3.07,3.984-6.422,8.912-8.64,14.16c-0.396,0.937-0.553,2.007-0.96,2.88c-0.777,1.662-1.557,3.044-2.4,5.28
c-0.337,0.892-0.643,1.788-0.96,2.88c-0.519,1.786-1.834,4.843-1.68,6.24c0.31,2.806,5.646,6.404,7.439,8.4
c1.528,1.702,3.668,5.2,5.28,7.68c1.484,2.283,3.561,5.287,4.8,8.16c0.76,1.76,1.024,3.335,1.68,4.8
c0.614,1.371,1.233,2.71,2.16,4.32c1.305,6.888,4.467,14.05,5.761,21.84c0.322,1.945,0.454,4.176,0.72,6.24
C30.615,132.855,26.544,148.64,21.42,160.792z M127.5,20.152c-5.646-3.555-10.348-8.052-17.521-10.08
c-0.875-1.489-2.273-2.128-4.078-2.88c-3.35-1.396-7.562-3.075-10.801-2.64c-1.82-1.175-4.289-0.83-6.721-1.2
c-2.28-0.347-4.444-1.439-6.72-1.68c-1.344-0.142-2.785,0.07-4.32,0c-7.497-0.34-13.854-0.484-19.68,0.48
c-1.943,0.321-3.91,0.105-4.8,1.2c12.866,0.814,25.084,2.275,35.04,6c2.174,1.906,3.231,4.928,4.56,7.68
c3.756,0.281,7.337,0.812,11.04,1.44c3.771,0.638,7.745,0.647,11.28,1.44c1.232,0.276,2.434,0.909,3.6,1.2
c1.477,0.369,2.948,0.386,4.32,0.72c3.857,0.938,7.084,3.071,10.8,3.6C132.611,22.562,128.557,22.856,127.5,20.152z M26.701,10.552
c-0.081,0.799-0.695,1.064-0.48,2.16c1.457-0.143,2.016-1.185,2.4-2.4C28.04,9.76,27.455,10.557,26.701,10.552z M21.18,17.752
c1.42-0.58,2.396-1.604,2.641-3.36C21.856,12.896,19.951,16.054,21.18,17.752z M15.901,19.192c-0.65,1.087-1.234,3.719,0.479,4.08
c1.305-0.855,2.313-2.007,2.64-3.84C18.334,18.765,16.911,18.93,15.901,19.192z M100.38,20.872c-0.043-0.69-1.745-0.956-2.159-0.48
C99.227,20.265,99.561,20.812,100.38,20.872z M119.581,26.152c8.754,2.136,16.763,4.572,25.2,6.72
c3.148,0.801,6.432,1.065,9.84,1.68c2.46,0.443,5.003,1.364,6.96,1.68c4.544,0.733,8.42,1.37,12.72,1.92
c2.854,0.364,6.668,1.791,9.12-0.72c-0.155-1.847-2.051-2.091-3.36-2.64c-9.778-4.101-21.683-7.286-32.64-8.88
c1.938,0.772,3.869,1.362,6,1.92c2.259,0.591,4.64,0.699,6.96,1.2c0.835,0.18,1.508,0.71,2.4,0.96
c0.902,0.253,1.931,0.237,2.88,0.48c3.551,0.907,6.789,2.409,9.84,3.6c1.567,0.612,3.974,0.669,4.319,2.64
c-1.635,2.107-4.569,0.608-6.959,0.24c-3.174-0.488-5.125-0.856-7.44-1.44c-4.142-1.043-8.385-1.535-12.72-2.4
c-5.418-1.082-10.871-2.907-16.32-4.08c-5.503-1.185-10.977-2.667-16.32-4.08c-2.738-0.725-5.652-0.984-8.398-1.68
c-3.6-0.912-7.153-2.42-10.561-2.16C106.85,22.779,113.795,24.741,119.581,26.152z M135.42,23.032c-1.168,0.12-2.3-1.049-3.12-0.48
C132.919,23.471,134.123,23.59,135.42,23.032z M137.581,23.512c-0.429,0.001-1.396-0.646-1.681,0
C136.659,23.182,137.35,24.106,137.581,23.512z M138.781,23.752C137.244,23.475,138.253,24.485,138.781,23.752L138.781,23.752z
M140.701,24.232c-0.418,0.018-0.747-0.054-0.96-0.24C139.355,24.661,140.777,24.656,140.701,24.232z M146.939,25.672
c-0.42,0.066-1.196-0.675-1.438,0C145.921,25.606,146.698,26.348,146.939,25.672z M50.46,140.872
c0.413,0.013,0.615-0.186,0.72-0.479C50.767,140.379,50.565,140.576,50.46,140.872z M83.34,145.192
c-0.599,0.171-0.986-1.058-1.439-0.24C82.156,145.124,83.005,145.89,83.34,145.192z M43.741,158.151
c-0.613,1.31-1.417,2.559-1.92,3.841c-1.446,3.681-2.29,8.405-3.36,12.239c-0.197,0.707-0.124,1.976-0.96,2.4
c0.58-5.143,1.586-9.746,3.12-13.921c1.579-4.298,3.745-8.028,4.8-12c-1.188,2.661-2.916,5.207-4.08,8.16
c-0.661,1.677-0.845,3.556-1.439,5.28c-0.392,1.133-1.079,2.006-1.44,3.119c-0.397,1.227-0.381,2.485-0.72,3.84
c-0.347,1.387-1.107,2.686-1.44,4.08c-0.313,1.313,0.229,3.015-1.2,3.841c0.68-4.237,1.386-8.304,2.4-12.24
c0.995-3.864,2.999-7.331,3.6-10.8c-2.037,3.979-3.683,9.367-4.8,13.68c-0.562,2.166-1.047,4.053-1.68,5.76
c-0.333,0.898-0.376,2.038-1.2,2.642c0.995-3.695,1.656-7.688,2.64-11.521c0.853-3.32,2.179-6.689,3.36-9.601
c0.623-1.534,1.54-2.929,1.68-4.56c-4.529,7.229-6.201,17.317-8.88,26.398c0.265-7.02,3.061-14.855,4.561-20.641
c-3.25,7.15-5.026,15.774-6.721,24.48c-0.278-3.48,0.833-6.847,0.721-9.361c-0.837,2.524-0.938,5.783-1.92,8.16
c0.249-8.07,2.284-14.354,4.079-20.879c-1.099,0.604-1.108,2.125-1.439,3.359c-0.33,1.229-0.892,2.562-1.2,3.84
c-0.954,3.941-1.397,8.85-2.399,12.24c-0.175,0.59,0.17,1.729-0.721,1.92c0.333-4.309,1.486-9.072,1.681-12.721
c-1.289,3.992-1.331,9.229-2.641,13.201c-0.339-2.184,0.607-4.351,0-6c0,0.56,0,1.118,0,1.68c-0.873,1.207-0.36,3.8-1.2,5.039
c-0.27-0.689-0.048-1.871-0.479-2.399c0.245,0.824-0.396,1.979-0.72,1.44c-0.22-1.982,1.028-3.769,0.239-5.521
c-0.602,3.717-0.384,8.256-1.92,11.039c-0.146-0.574,0.302-1.741-0.239-1.92c-0.188,0.625-0.413,2.451-0.961,1.92
c0.091-1.16,0.98-3.078,0-3.84c0.321,1.521-0.976,3.624-1.68,1.92c-0.085,1.595-0.108,3.252-0.479,4.562
c-0.516-0.525-0.113-1.969-0.721-2.4c-0.175,0.545,0.433,1.873-0.479,1.68c-1.05-2.633,0.628-7.181-0.48-9.84
c-1.418,6.688-0.179,14.127,0,21.6c2.076,1.42,3.499,0.545,5.761,0.721c1.369,0.106,3.233,0.787,5.04,1.439
c1.765,0.639,3.736,0.938,5.039,1.44c2.034,0.786,3.52,2.353,5.28,2.159c1.335-0.146,2.019-1.488,3.12-1.92
c2.678,1.703,7.881,2.992,10.32,0.239c1.113-1.255,1.084-3.026,0.479-5.278c0.331-1.271,1.749-1.451,2.4-2.4
c-2.405-9.068-1.622-20.285,0.479-28.561c0.488-1.918,0.75-4.174,1.44-6c0.134-0.353,0.766-0.771,0.96-1.199
c0.886-1.948,1.236-2.836,2.16-4.32c1.059-1.702,2.208-3.387,3.12-4.8c-2.922,1.388-4.375,5.03-5.521,7.681
c-0.801,1.852-1.863,3.79-2.399,5.279c-0.763,2.117-0.429,4.688-2.16,6.24c0.534-2.24,0.688-4.629,1.439-6.721
c0.263-0.728,0.917-1.197,1.2-1.921c0.23-0.586,0.055-1.355,0.24-1.92c0.108-0.33,0.553-0.344,0.72-0.72
c0.202-0.454,0.059-1.048,0.24-1.439c1.094-2.361,2.931-4.074,3.36-6.24c-0.611,0.59-1.02,1.382-1.92,1.681
c-0.104,1.799-1.573,2.836-2.4,4.319c-1.735,3.113-3.22,6.397-4.561,9.36c-0.674,1.49-1.021,3.412-2.399,4.56
c1.534-4.945,3.68-9.279,5.76-13.68c0.425-0.216,0.701-0.578,1.2-0.72c0.619-2.661,2.062-4.499,3.36-6.479
c-3.013,0.856-3.713,4.425-5.04,7.199c-0.45,0.94-1.206,1.77-1.681,2.641c-0.541,0.994-0.717,2.013-1.2,3.119
c-0.875,2.007-2.194,3.858-2.88,5.761c-0.434,1.204-0.337,3.796-1.92,3.84c1.629-3.503,2.437-7.214,3.84-10.8
c1.331-3.398,3.999-6.447,4.08-10.08c-3.672,5.197-6.271,12.139-8.399,18.48c-0.297,0.883-0.301,2.074-1.2,2.64
c0.978-7.423,4.625-13.454,6.479-19.2c-1.604,2.814-3.279,5.785-4.56,9.12c-1.296,3.375-1.818,7.194-3.36,10.56
c0.077-3.477,1.145-7.088,2.4-10.56c0.404-1.118,0.97-2.237,1.44-3.36c0.494-1.182,0.69-2.555,1.199-3.6
c0.804-1.65,2.572-2.846,2.641-4.801c-0.997,0.047-1.504,1.209-1.92,1.92c-3.448,5.897-4.921,13.352-7.44,19.681
c0.259-2.94,0.81-5.591,1.92-7.681c0.018-3.902,2.939-6.178,2.641-9.601c-1.471,2.734-2.318,5.974-3.36,9.121
c-1.07,3.234-1.825,6.58-3.12,9.601c0.832-8.607,3.912-14.969,6.48-21.84c0.947-1.293,2.015-2.466,2.159-4.562
c0.559-0.647,1.829-1.842,1.44-2.64C47.226,150.467,45.48,154.437,43.741,158.151z M61.26,149.032
c0.413,0.013,0.615-0.186,0.72-0.479C61.568,148.539,61.365,148.735,61.26,149.032z M32.46,149.752
c0.601,0.041,0.751-0.369,0.96-0.72C32.82,148.991,32.669,149.399,32.46,149.752z M49.5,150.712
C48.405,152.59,50.104,150.498,49.5,150.712L49.5,150.712z M52.14,154.553c0.787-1.134,1.448-2.393,2.16-3.601
C52.851,151.267,52.217,153.665,52.14,154.553z M135.18,164.633c-0.4-0.397-0.189-1.409-0.24-2.159
c0.188-0.054,0.279-0.201,0.24-0.48c0.703,0.062,0.737-0.543,1.44-0.48c0.905-0.104,1.127,0.475,1.68,0.721
c0.244-1.283-0.329-1.75-0.96-2.159c-1.496,0.184-2.255,1.104-3.6,1.438C133.689,162.928,134.73,164.515,135.18,164.633z
M139.5,161.992c0.439,0.462,0.812-0.205,1.2-0.479c-0.343-0.058-0.405-0.396-0.722-0.479
C139.977,161.509,139.326,161.338,139.5,161.992z M136.141,163.672c1.604-0.376,1.142,0.785,0.959,1.681
c0.455-0.054,0.529,0.271,0.961,0.239c-0.129-1.552-0.433-2.928-1.92-3.118C136.141,162.872,136.141,163.271,136.141,163.672z
M140.46,163.672c0.478,0.004,0.306,0.655,0.96,0.479c0.075-0.323,0.342-0.459,0.24-0.959
C141.232,163.324,140.664,163.315,140.46,163.672z M30.06,165.353c0.139-0.146,0.562-1.139,0-1.199
C30.373,164.748,29.356,165.147,30.06,165.353z M136.141,169.433c-0.297-1.623-1.065-2.773-1.92-3.841
C134.659,167.074,135.413,168.24,136.141,169.433z M29.821,166.553c0.064-0.327-0.149-0.938-0.24-0.479
C29.515,166.397,29.729,167.009,29.821,166.553z M27.661,168.474c0.73-0.082,0.964-1.9,0.479-2.4
C27.971,166.863,27.754,167.606,27.661,168.474z M141.9,167.512c0,0.24,0,0.48,0,0.722c0.324,0.326,1.676,0.457,1.681-0.24
C143.09,167.731,142.6,166.842,141.9,167.512z M19.02,168.712c0.07-0.146,0.598-0.924,0-0.96
C18.951,167.899,18.423,168.676,19.02,168.712z M31.26,172.553c-0.155-1.84,1.534-3.102,0.72-4.801
C31.852,168.927,30.532,171.974,31.26,172.553z M143.34,170.633c0.68,0.197,0.622-0.339,1.2-0.239
c0.076-0.558-0.039-0.922-0.24-1.201C143.473,169.164,143.184,169.676,143.34,170.633z M27.18,172.072
c0.136-0.677,0.835-2.029,0.24-2.642C27.301,170.125,26.843,171.719,27.18,172.072z M137.82,172.792
c-0.106-0.613-0.336-1.104-0.721-1.439C137.223,171.95,137.092,172.8,137.82,172.792z M25.5,173.271
c-0.171,4.112,0.753-0.395,0.24-0.719C25.703,172.834,25.782,173.233,25.5,173.271z M148.38,175.192
c1.005,0.115,1.526,0.713,1.44,1.92c1.74-0.732-0.082-3.19-1.2-3.36C148.344,174.035,148.35,174.601,148.38,175.192z M21.42,175.433
c0.038-0.166-0.084-1.396-0.24-0.721C20.969,175.626,21.309,175.914,21.42,175.433z M16.14,178.553c0.035-0.284,0.428-0.545,0-0.721
C16.106,178.115,15.713,178.376,16.14,178.553z M128.46,178.312c-0.315-0.244-0.885-0.235-1.2-0.479
C126.975,178.545,128.271,178.863,128.46,178.312z M22.861,179.032c0.064-0.327-0.149-0.938-0.24-0.479
C22.555,178.879,22.769,179.489,22.861,179.032z M130.861,179.992c0.285-0.714-1.012-1.031-1.2-0.479
C130.4,179.332,129.953,180.34,130.861,179.992z M22.621,181.433c0.038-0.166-0.084-1.396-0.24-0.721
C22.169,181.626,22.509,181.914,22.621,181.433z M138.3,182.872c-0.282-0.034-0.544-0.428-0.72,0
C137.863,182.906,138.125,183.299,138.3,182.872z M148.38,188.633c0.229-1.432-0.444-1.955-0.96-2.641
C141.995,184.959,145.773,191.787,148.38,188.633z M32.22,206.633c-0.457-0.104-0.603-0.518-1.2-0.479
C30.852,206.748,32.046,207.168,32.22,206.633z M32.94,208.072c0.286-0.714-1.011-1.031-1.199-0.48
C32.298,207.697,32.383,208.651,32.94,208.072z M35.821,214.553c0.402,1.761,1.146,5.343,2.64,5.039
c0.277-3.799-2.039-7.08-2.4-11.039c-0.57-0.086-1.38-1.157-1.92-0.479C35.256,209.742,35.267,212.132,35.821,214.553z
M33.18,209.512c-0.148,1.339,1.039,2.537,0.24,4.08c0.282,0.357,0.603,0.679,0.96,0.961c-1.081,0.765,1.131,2.709,0,3.601
c0.576,0.223,0.636,0.965,1.68,0.72c-0.979-3.261-1.55-6.931-2.64-10.08c-0.85-0.21-0.59-0.03-1.44-0.239
C31.747,209.506,33.322,208.649,33.18,209.512z M39.18,210.231c0.174-0.733-0.414-0.705-0.24-1.439c-0.399,0-0.8,0-1.199,0
C38.161,209.332,38.641,209.812,39.18,210.231z M38.22,211.433c0.68,0.041,1,0.439,1.681,0.479
C39.902,210.8,38.848,210.937,38.22,211.433z M38.701,213.112c1.781-0.084-0.676,0.596,0.239,0.96
c0.066-0.566,0.895,0.021,0.961,0.239c0.459,1.181-0.798,0.644-0.721,1.439c0.596-0.035,1.165-0.045,1.2,0.48
c0.206,0.766-0.378,0.742-0.479,1.199c0.728-0.089,0.498,0.781,0.72,1.2c-0.617,1.144-1.351,2.169-3.12,2.159
c-0.871-0.169-0.853-1.229-1.68-1.439c-0.346,0.135-0.586,0.375-0.721,0.721c1.11-0.18,0.98,0.271,0.721,0.96
c2.038,0.269,3.538,1.712,5.52,0.72c1.29-3.207-0.678-6.458-1.439-9.119C39.305,212.597,38.736,212.588,38.701,213.112z"/>
</svg>

After

Width:  |  Height:  |  Size: 30 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

+17
View File
@@ -0,0 +1,17 @@
<h3>About</h3>
<p>
Flask-Security is an opinionated Flask extension which adds basic
security and authentication features to your Flask apps quickly
and easily. Flask-Social can also be used to add "social" or OAuth
login and connection management.
</p>
<h3>Useful Links</h3>
<ul>
<li><a href="http://pypi.python.org/pypi/Flask-Security">Flask-Security @ PyPI</a></li>
<li><a href="http://github.com/mattupstate/flask-security">Flask-Security @ github</a></li>
<li><a href="http://github.com/jfinkels/flask-security/issues">Issue Tracker</a></li>
</ul>
<ul>
<li><a href="http://pypi.python.org/pypi/Flask-Social">Flask-Social</a></li>
<li><a href="http://github.com/mattupstate/flask-social">Flask-Social @ github</a></li>
</ul>
+3
View File
@@ -0,0 +1,3 @@
<p class="logo"><a href="{{ pathto(master_doc) }}">
<img class="logo" src="{{ pathto('_static/logo-helmet.png', 1) }}" alt="Logo"/>
</a></p>
-21
View File
@@ -50,27 +50,6 @@ Datastores
:inherited-members:
Exceptions
----------
.. autoexception:: flask_security.exceptions.BadCredentialsError
.. autoexception:: flask_security.exceptions.AuthenticationError
.. autoexception:: flask_security.exceptions.UserNotFoundError
.. autoexception:: flask_security.exceptions.RoleNotFoundError
.. autoexception:: flask_security.exceptions.UserDatastoreError
.. autoexception:: flask_security.exceptions.UserCreationError
.. autoexception:: flask_security.exceptions.RoleCreationError
.. autoexception:: flask_security.exceptions.ConfirmationError
.. autoexception:: flask_security.exceptions.ResetPasswordError
Signals
-------
See the documentation for the signals provided by the Flask-Login and
+7 -1
View File
@@ -100,6 +100,8 @@ html_theme = 'flask'
html_theme_options = {
#'github_fork': 'mattupstate/flask-security',
#'index_logo': False
'touch_icon': 'touch-icon.png',
'index_logo': 'logo-full.png'
}
# Add any paths that contain custom themes here, relative to this directory.
@@ -135,7 +137,11 @@ html_static_path = ['_static']
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
html_sidebars = {
'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'],
'**': ['sidebarlogo.html', 'localtoc.html', 'relations.html',
'sourcelink.html', 'searchbox.html']
}
# Additional templates that should be rendered to pages, maps page names to
# template names.
-1
View File
@@ -4,7 +4,6 @@ Contents
.. toctree::
:maxdepth: 1
overview
features
configuration
quickstart
+30 -1
View File
@@ -1,7 +1,36 @@
Flask-Security
==============
Flask-Security quickly adds security features to your Flask application.
Flask-Security allows you to quickly add common security mechanisms to your
Flask application. They include:
1. Session based authentication
2. Role management
3. Password encryption
4. Basic HTTP authentication
5. Token based authentication
6. Token based account activation (optional)
7. Token based password recovery/resetting (optional)
8. User registration (optional)
9. Login tracking (optional)
Many of these features are made possible by integrating various Flask extensions
and libraries. They include:
1. `Flask-Login <http://packages.python.org/Flask-Login/>`_
2. `Flask-Mail <http://packages.python.org/Flask-Mail/>`_
3. `Flask-Principal <http://packages.python.org/Flask-Principal/>`_
4. `Flask-Script <http://packages.python.org/Flask-Script/>`_
5. `Flask-WTF <http://packages.python.org/Flask-Mail/>`_
6. `itsdangerous <http://packages.python.org/itsdangerous/>`_
7. `passlib <http://packages.python.org/passlib/>`_
Additionally, it assumes you'll be using a common library for your database
connections and model definitions. Flask-Security supports the following Flask
extensions out of the box for data persistance:
1. `Flask-SQLAlchemy <http://packages.python.org/Flask-SQLAlchemy/>`_
2. `Flask-MongoEngine <http://packages.python.org/Flask-MongoEngine/>`_
.. include:: contents.rst.inc
-33
View File
@@ -1,33 +0,0 @@
Overview
========
Flask-Security allows you to quickly add common security mechanisms to your
Flask application. They include:
1. Session based authentication
2. Role management
3. Password encryption
4. Basic HTTP authentication
5. Token based authentication
6. Token based account activation (optional)
7. Token based password recovery/resetting (optional)
8. User registration (optional)
9. Login tracking (optional)
Many of these features are made possible by integrating various Flask extensions
and libraries. They include:
1. `Flask-Login <http://packages.python.org/Flask-Login/>`_
2. `Flask-Mail <http://packages.python.org/Flask-Mail/>`_
3. `Flask-Principal <http://packages.python.org/Flask-Principal/>`_
4. `Flask-Script <http://packages.python.org/Flask-Script/>`_
5. `Flask-WTF <http://packages.python.org/Flask-Mail/>`_
6. `itsdangerous <http://packages.python.org/itsdangerous/>`_
7. `passlib <http://packages.python.org/passlib/>`_
Additionally, it assumes you'll be using a common library for your database
connections and model definitions. Flask-Security supports the following Flask
extensions out of the box for data persistance:
1. `Flask-SQLAlchemy <http://packages.python.org/Flask-SQLAlchemy/>`_
2. `Flask-MongoEngine <http://packages.python.org/Flask-MongoEngine/>`_
+1 -1
View File
@@ -3,4 +3,4 @@ source-dir = docs/
build-dir = docs/_build
[upload_sphinx]
upload-dir = docs/_build/html
upload-dir = docs/_build
+3 -2
View File
@@ -36,7 +36,7 @@ setup(
install_requires=[
'Flask>=0.9',
'Flask-Login>=0.1.3',
'Flask-Mail>=0.6.1',
'Flask-Mail>=0.7.0',
'Flask-Principal>=0.3',
'Flask-WTF>=0.5.4',
'itsdangerous>=0.15',
@@ -47,7 +47,8 @@ setup(
'nose',
'Flask-SQLAlchemy',
'Flask-MongoEngine',
'py-bcrypt'
'py-bcrypt',
'simplejson'
],
classifiers=[
'Development Status :: 4 - Beta',
+463
View File
@@ -0,0 +1,463 @@
from __future__ import with_statement
import base64
import time
import simplejson as json
from flask.ext.security.utils import capture_registrations, \
capture_reset_password_requests, capture_passwordless_login_requests
from tests import SecurityTest
class ConfiguredPasswordHashSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORD_HASH': 'bcrypt',
'SECURITY_PASSWORD_SALT': 'so-salty',
'USER_COUNT': 1
}
def test_authenticate(self):
r = self.authenticate(endpoint="/login")
self.assertIn('Home Page', r.data)
class ConfiguredSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_REGISTERABLE': True,
'SECURITY_LOGOUT_URL': '/custom_logout',
'SECURITY_LOGIN_URL': '/custom_login',
'SECURITY_POST_LOGIN_VIEW': '/post_login',
'SECURITY_POST_LOGOUT_VIEW': '/post_logout',
'SECURITY_POST_REGISTER_VIEW': '/post_register',
'SECURITY_UNAUTHORIZED_VIEW': '/unauthorized',
'SECURITY_DEFAULT_HTTP_AUTH_REALM': 'Custom Realm'
}
def test_login_view(self):
r = self._get('/custom_login')
self.assertIn("<h1>Login</h1>", r.data)
def test_authenticate(self):
r = self.authenticate(endpoint="/custom_login")
self.assertIn('Post Login', r.data)
def test_logout(self):
self.authenticate(endpoint="/custom_login")
r = self.logout(endpoint="/custom_logout")
self.assertIn('Post Logout', r.data)
def test_register_view(self):
r = self._get('/register')
self.assertIn('<h1>Register</h1>', r.data)
def test_register(self):
data = dict(email='dude@lp.com',
password='password',
password_confirm='password')
r = self._post('/register', data=data, follow_redirects=True)
self.assertIn('Post Register', r.data)
def test_register_json(self):
r = self._post('/register',
data='{ "email": "dude@lp.com", "password": "password" }',
content_type='application/json')
data = json.loads(r.data)
self.assertEquals(data['meta']['code'], 200)
self.assertIn('authentication_token', data['response']['user'])
def test_register_existing_email(self):
data = dict(email='matt@lp.com',
password='password',
password_confirm='password')
r = self._post('/register', data=data, follow_redirects=True)
self.assertIn('matt@lp.com is already associated with an account', r.data)
def test_unauthorized(self):
self.authenticate("joe@lp.com", endpoint="/custom_auth")
r = self._get("/admin", follow_redirects=True)
msg = 'You are not allowed to access the requested resouce'
self.assertIn(msg, r.data)
def test_default_http_auth_realm(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus")
})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Custom Realm"',
r.headers['WWW-Authenticate'])
class BadConfiguredSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORD_HASH': 'bcrypt',
'USER_COUNT': 1
}
def test_bad_configuration_raises_runtimer_error(self):
self.assertRaises(RuntimeError, self.authenticate)
class RegisterableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_REGISTERABLE': True,
'USER_COUNT': 1
}
def test_register_valid_user(self):
data = dict(email='dude@lp.com',
password='password',
password_confirm='password')
self.client.post('/register', data=data, follow_redirects=True)
r = self.authenticate('dude@lp.com')
self.assertIn('Hello dude@lp.com', r.data)
class ConfirmableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'USER_COUNT': 1
}
def test_login_before_confirmation(self):
e = 'dude@lp.com'
self.register(e)
r = self.authenticate(email=e)
self.assertIn(self.get_message('CONFIRMATION_REQUIRED'), r.data)
def test_send_confirmation_of_already_confirmed_account(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
self.client.get('/confirm/' + token, follow_redirects=True)
self.logout()
r = self.client.post('/confirm', data=dict(email=e))
self.assertIn(self.get_message('ALREADY_CONFIRMED'), r.data)
def test_register_sends_confirmation_email(self):
e = 'dude@lp.com'
with self.app.extensions['mail'].record_messages() as outbox:
self.register(e)
self.assertEqual(len(outbox), 1)
self.assertIn(e, outbox[0].html)
def test_confirm_email(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
r = self.client.get('/confirm/' + token, follow_redirects=True)
msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
self.assertIn(msg, r.data)
def test_invalid_token_when_confirming_email(self):
r = self.client.get('/confirm/bogus', follow_redirects=True)
self.assertIn('Invalid confirmation token', r.data)
def test_send_confirmation_with_invalid_email(self):
r = self._post('/confirm', data=dict(email='bogus@bogus.com'))
self.assertIn('Specified user does not exist', r.data)
def test_resend_confirmation(self):
e = 'dude@lp.com'
self.register(e)
r = self._post('/confirm', data={'email': e})
msg = self.get_message('CONFIRMATION_REQUEST', email=e)
self.assertIn(msg, r.data)
class ExpiredConfirmationTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'SECURITY_CONFIRM_EMAIL_WITHIN': '1 milliseconds',
'USER_COUNT': 1
}
def test_expired_confirmation_token_sends_email(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
time.sleep(1.25)
with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.get('/confirm/' + token, follow_redirects=True)
self.assertEqual(len(outbox), 1)
self.assertNotIn(token, outbox[0].html)
expire_text = self.AUTH_CONFIG['SECURITY_CONFIRM_EMAIL_WITHIN']
msg = self.app.config['SECURITY_MSG_CONFIRMATION_EXPIRED'][0]
msg = msg % dict(within=expire_text, email=e)
self.assertIn(msg, r.data)
class LoginWithoutImmediateConfirmTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'SECURITY_LOGIN_WITHOUT_CONFIRMATION': True,
'USER_COUNT': 1
}
def test_register_valid_user_automatically_signs_in(self):
e = 'dude@lp.com'
p = 'password'
data = dict(email=e, password=p, password_confirm=p)
r = self.client.post('/register', data=data, follow_redirects=True)
self.assertIn(e, r.data)
class RecoverableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_RESET_PASSWORD_ERROR_VIEW': '/',
'SECURITY_POST_FORGOT_VIEW': '/'
}
def test_reset_view(self):
with capture_reset_password_requests() as requests:
r = self.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._get('/reset/' + t)
self.assertIn('<h1>Reset password</h1>', r.data)
def test_forgot_post_sends_email(self):
with capture_reset_password_requests():
with self.app.extensions['mail'].record_messages() as outbox:
self.client.post('/reset', data=dict(email='joe@lp.com'))
self.assertEqual(len(outbox), 1)
def test_forgot_password_invalid_email(self):
r = self.client.post('/reset',
data=dict(email='larry@lp.com'),
follow_redirects=True)
self.assertIn("Specified user does not exist", r.data)
def test_reset_password_with_valid_token(self):
with capture_reset_password_requests() as requests:
r = self.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._post('/reset/' + t, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
r = self.logout()
r = self.authenticate('joe@lp.com', 'newpassword')
self.assertIn('Hello joe@lp.com', r.data)
def test_reset_password_with_invalid_token(self):
r = self._post('/reset/bogus', data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn(self.get_message('INVALID_RESET_PASSWORD_TOKEN'), r.data)
class ExpiredResetPasswordTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_RESET_PASSWORD_WITHIN': '1 milliseconds'
}
def test_reset_password_with_expired_token(self):
with capture_reset_password_requests() as requests:
r = self.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
time.sleep(1)
r = self.client.post('/reset/' + t, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn('You did not reset your password within', r.data)
class TrackableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_TRACKABLE': True,
'USER_COUNT': 1
}
def test_did_track(self):
e = 'matt@lp.com'
self.authenticate(email=e)
self.logout()
self.authenticate(email=e)
with self.app.test_request_context('/profile'):
user = self.app.security.datastore.find_user(email=e)
self.assertIsNotNone(user.last_login_at)
self.assertIsNotNone(user.current_login_at)
self.assertEquals('untrackable', user.last_login_ip)
self.assertEquals('untrackable', user.current_login_ip)
self.assertEquals(2, user.login_count)
class PasswordlessTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True
}
def test_login_request_for_inactive_user(self):
msg = self.app.config['SECURITY_MSG_DISABLED_ACCOUNT'][0]
r = self.client.post('/login',
data=dict(email='tiya@lp.com'),
follow_redirects=True)
self.assertIn(msg, r.data)
def test_request_login_token_sends_email_and_can_login(self):
e = 'matt@lp.com'
r, user, token = None, None, None
with capture_passwordless_login_requests() as requests:
with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.post('/login',
data=dict(email=e),
follow_redirects=True)
self.assertEqual(len(outbox), 1)
self.assertEquals(1, len(requests))
self.assertIn('user', requests[0])
self.assertIn('login_token', requests[0])
user = requests[0]['user']
token = requests[0]['login_token']
msg = self.app.config['SECURITY_MSG_LOGIN_EMAIL_SENT'][0]
msg = msg % dict(email=user.email)
self.assertIn(msg, r.data)
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
self.assertIn(msg, r.data)
r = self.client.get('/profile')
self.assertIn('Profile Page', r.data)
def test_invalid_login_token(self):
msg = self.app.config['SECURITY_MSG_INVALID_LOGIN_TOKEN'][0]
r = self._get('/login/bogus', follow_redirects=True)
self.assertIn(msg, r.data)
def test_token_login_when_already_authenticated(self):
with capture_passwordless_login_requests() as requests:
self.client.post('/login',
data=dict(email='matt@lp.com'),
follow_redirects=True)
token = requests[0]['login_token']
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
self.assertIn(msg, r.data)
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
self.assertNotIn(msg, r.data)
def test_send_login_with_invalid_email(self):
r = self._post('/login', data=dict(email='bogus@bogus.com'))
self.assertIn('Specified user does not exist', r.data)
class ExpiredLoginTokenTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True,
'SECURITY_LOGIN_WITHIN': '1 milliseconds',
'USER_COUNT': 1
}
def test_expired_login_token_sends_email(self):
e = 'matt@lp.com'
with capture_passwordless_login_requests() as requests:
self.client.post('/login',
data=dict(email=e),
follow_redirects=True)
token = requests[0]['login_token']
time.sleep(1.25)
with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.get('/login/' + token, follow_redirects=True)
expire_text = self.AUTH_CONFIG['SECURITY_LOGIN_WITHIN']
msg = self.app.config['SECURITY_MSG_LOGIN_EXPIRED'][0]
msg = msg % dict(within=expire_text, email=e)
self.assertIn(msg, r.data)
self.assertEqual(len(outbox), 1)
self.assertIn(e, outbox[0].html)
self.assertNotIn(token, outbox[0].html)
class AsyncMailTaskTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'USER_COUNT': 1
}
def setUp(self):
super(AsyncMailTaskTests, self).setUp()
self.mail_sent = False
def test_send_email_task_is_called(self):
@self.app.security.send_mail_task
def send_email(msg):
self.mail_sent = True
self.client.post('/reset', data=dict(email='matt@lp.com'))
self.assertTrue(self.mail_sent)
class NoBlueprintTests(SecurityTest):
AUTH_CONFIG = {
'USER_COUNT': 1
}
def _create_app(self, auth_config):
return super(NoBlueprintTests, self)._create_app(auth_config, False)
def test_login_endpoint_is_404(self):
r = self._get('/login')
self.assertEqual(404, r.status_code)
def test_http_auth_without_blueprint(self):
auth = 'Basic ' + base64.b64encode("matt@lp.com:password")
r = self._get('/http', headers={'Authorization': auth})
self.assertIn('HTTP Authentication', r.data)
+1 -444
View File
@@ -3,17 +3,9 @@
from __future__ import with_statement
import base64
import time
import simplejson as json
from cookielib import Cookie
try:
import simplejson as json
except ImportError:
import json
from flask.ext.security.utils import capture_registrations, \
capture_reset_password_requests, capture_passwordless_login_requests
from werkzeug.utils import parse_cookie
from tests import SecurityTest
@@ -197,401 +189,6 @@ class DefaultSecurityTests(SecurityTest):
self.assertNotIn('BadSignature', r.data)
class ConfiguredPasswordHashSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORD_HASH': 'bcrypt',
'SECURITY_PASSWORD_SALT': 'so-salty',
'USER_COUNT': 1
}
def test_authenticate(self):
r = self.authenticate(endpoint="/login")
self.assertIn('Home Page', r.data)
class ConfiguredSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_REGISTERABLE': True,
'SECURITY_LOGOUT_URL': '/custom_logout',
'SECURITY_LOGIN_URL': '/custom_login',
'SECURITY_POST_LOGIN_VIEW': '/post_login',
'SECURITY_POST_LOGOUT_VIEW': '/post_logout',
'SECURITY_POST_REGISTER_VIEW': '/post_register',
'SECURITY_UNAUTHORIZED_VIEW': '/unauthorized',
'SECURITY_DEFAULT_HTTP_AUTH_REALM': 'Custom Realm'
}
def test_login_view(self):
r = self._get('/custom_login')
self.assertIn("<h1>Login</h1>", r.data)
def test_authenticate(self):
r = self.authenticate(endpoint="/custom_login")
self.assertIn('Post Login', r.data)
def test_logout(self):
self.authenticate(endpoint="/custom_login")
r = self.logout(endpoint="/custom_logout")
self.assertIn('Post Logout', r.data)
def test_register_view(self):
r = self._get('/register')
self.assertIn('<h1>Register</h1>', r.data)
def test_register(self):
data = dict(email='dude@lp.com',
password='password',
password_confirm='password')
r = self._post('/register', data=data, follow_redirects=True)
self.assertIn('Post Register', r.data)
def test_register_json(self):
r = self._post('/register',
data='{ "email": "dude@lp.com", "password": "password" }',
content_type='application/json')
data = json.loads(r.data)
self.assertEquals(data['meta']['code'], 200)
self.assertIn('authentication_token', data['response']['user'])
def test_register_existing_email(self):
data = dict(email='matt@lp.com',
password='password',
password_confirm='password')
r = self._post('/register', data=data, follow_redirects=True)
self.assertIn('matt@lp.com is already associated with an account', r.data)
def test_unauthorized(self):
self.authenticate("joe@lp.com", endpoint="/custom_auth")
r = self._get("/admin", follow_redirects=True)
msg = 'You are not allowed to access the requested resouce'
self.assertIn(msg, r.data)
def test_default_http_auth_realm(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("joe@lp.com:bogus")
})
self.assertIn('<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Custom Realm"', r.headers['WWW-Authenticate'])
class BadConfiguredSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORD_HASH': 'bcrypt',
'USER_COUNT': 1
}
def test_bad_configuration_raises_runtimer_error(self):
self.assertRaises(RuntimeError, self.authenticate)
class RegisterableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_REGISTERABLE': True,
'USER_COUNT': 1
}
def test_register_valid_user(self):
data = dict(email='dude@lp.com', password='password', password_confirm='password')
self.client.post('/register', data=data, follow_redirects=True)
r = self.authenticate('dude@lp.com')
self.assertIn('Hello dude@lp.com', r.data)
class ConfirmableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'USER_COUNT': 1
}
def test_login_before_confirmation(self):
e = 'dude@lp.com'
self.register(e)
r = self.authenticate(email=e)
self.assertIn(self.get_message('CONFIRMATION_REQUIRED'), r.data)
def test_send_confirmation_of_already_confirmed_account(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
self.client.get('/confirm/' + token, follow_redirects=True)
self.logout()
r = self.client.post('/confirm', data=dict(email=e))
self.assertIn(self.get_message('ALREADY_CONFIRMED'), r.data)
def test_register_sends_confirmation_email(self):
e = 'dude@lp.com'
with self.app.extensions['mail'].record_messages() as outbox:
self.register(e)
self.assertEqual(len(outbox), 1)
self.assertIn(e, outbox[0].html)
def test_confirm_email(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
r = self.client.get('/confirm/' + token, follow_redirects=True)
msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
self.assertIn(msg, r.data)
def test_invalid_token_when_confirming_email(self):
r = self.client.get('/confirm/bogus', follow_redirects=True)
self.assertIn('Invalid confirmation token', r.data)
def test_send_confirmation_with_invalid_email(self):
r = self._post('/confirm', data=dict(email='bogus@bogus.com'))
self.assertIn('Specified user does not exist', r.data)
def test_resend_confirmation(self):
e = 'dude@lp.com'
self.register(e)
r = self._post('/confirm', data={'email': e})
self.assertIn(self.get_message('CONFIRMATION_REQUEST', email=e), r.data)
class ExpiredConfirmationTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'SECURITY_CONFIRM_EMAIL_WITHIN': '1 milliseconds',
'USER_COUNT': 1
}
def test_expired_confirmation_token_sends_email(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
time.sleep(1.25)
with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.get('/confirm/' + token, follow_redirects=True)
self.assertEqual(len(outbox), 1)
self.assertNotIn(token, outbox[0].html)
expire_text = self.AUTH_CONFIG['SECURITY_CONFIRM_EMAIL_WITHIN']
msg = self.app.config['SECURITY_MSG_CONFIRMATION_EXPIRED'][0] % dict(within=expire_text, email=e)
self.assertIn(msg, r.data)
class LoginWithoutImmediateConfirmTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'SECURITY_LOGIN_WITHOUT_CONFIRMATION': True,
'USER_COUNT': 1
}
def test_register_valid_user_automatically_signs_in(self):
e = 'dude@lp.com'
p = 'password'
data = dict(email=e, password=p, password_confirm=p)
r = self.client.post('/register', data=data, follow_redirects=True)
self.assertIn(e, r.data)
class RecoverableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_RESET_PASSWORD_ERROR_VIEW': '/',
'SECURITY_POST_FORGOT_VIEW': '/'
}
def test_reset_view(self):
with capture_reset_password_requests() as requests:
r = self.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._get('/reset/' + t)
self.assertIn('<h1>Reset password</h1>', r.data)
def test_forgot_post_sends_email(self):
with capture_reset_password_requests():
with self.app.extensions['mail'].record_messages() as outbox:
self.client.post('/reset', data=dict(email='joe@lp.com'))
self.assertEqual(len(outbox), 1)
def test_forgot_password_invalid_email(self):
r = self.client.post('/reset',
data=dict(email='larry@lp.com'),
follow_redirects=True)
self.assertIn("Specified user does not exist", r.data)
def test_reset_password_with_valid_token(self):
with capture_reset_password_requests() as requests:
r = self.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._post('/reset/' + t, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
r = self.logout()
r = self.authenticate('joe@lp.com', 'newpassword')
self.assertIn('Hello joe@lp.com', r.data)
def test_reset_password_with_invalid_token(self):
r = self._post('/reset/bogus', data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn(self.get_message('INVALID_RESET_PASSWORD_TOKEN'), r.data)
class ExpiredResetPasswordTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_RESET_PASSWORD_WITHIN': '1 milliseconds'
}
def test_reset_password_with_expired_token(self):
with capture_reset_password_requests() as requests:
r = self.client.post('/reset',
data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
time.sleep(1)
r = self.client.post('/reset/' + t, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn('You did not reset your password within', r.data)
class TrackableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_TRACKABLE': True,
'USER_COUNT': 1
}
def test_did_track(self):
e = 'matt@lp.com'
self.authenticate(email=e)
self.logout()
self.authenticate(email=e)
with self.app.test_request_context('/profile'):
user = self.app.security.datastore.find_user(email=e)
self.assertIsNotNone(user.last_login_at)
self.assertIsNotNone(user.current_login_at)
self.assertEquals('untrackable', user.last_login_ip)
self.assertEquals('untrackable', user.current_login_ip)
self.assertEquals(2, user.login_count)
class PasswordlessTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True
}
def test_login_request_for_inactive_user(self):
msg = self.app.config['SECURITY_MSG_DISABLED_ACCOUNT'][0]
r = self.client.post('/login', data=dict(email='tiya@lp.com'), follow_redirects=True)
self.assertIn(msg, r.data)
def test_request_login_token_sends_email_and_can_login(self):
e = 'matt@lp.com'
r, user, token = None, None, None
with capture_passwordless_login_requests() as requests:
with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.post('/login', data=dict(email=e), follow_redirects=True)
self.assertEqual(len(outbox), 1)
self.assertEquals(1, len(requests))
self.assertIn('user', requests[0])
self.assertIn('login_token', requests[0])
user = requests[0]['user']
token = requests[0]['login_token']
msg = self.app.config['SECURITY_MSG_LOGIN_EMAIL_SENT'][0] % dict(email=user.email)
self.assertIn(msg, r.data)
r = self.client.get('/login/' + token, follow_redirects=True)
self.assertIn(self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'), r.data)
r = self.client.get('/profile')
self.assertIn('Profile Page', r.data)
def test_invalid_login_token(self):
msg = self.app.config['SECURITY_MSG_INVALID_LOGIN_TOKEN'][0]
r = self._get('/login/bogus', follow_redirects=True)
self.assertIn(msg, r.data)
def test_token_login_forwards_to_post_login_view_when_already_authenticated(self):
with capture_passwordless_login_requests() as requests:
self.client.post('/login', data=dict(email='matt@lp.com'), follow_redirects=True)
token = requests[0]['login_token']
r = self.client.get('/login/' + token, follow_redirects=True)
self.assertIn(self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'), r.data)
r = self.client.get('/login/' + token, follow_redirects=True)
self.assertNotIn(self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'), r.data)
def test_send_login_with_invalid_email(self):
r = self._post('/login', data=dict(email='bogus@bogus.com'))
self.assertIn('Specified user does not exist', r.data)
class ExpiredLoginTokenTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True,
'SECURITY_LOGIN_WITHIN': '1 milliseconds',
'USER_COUNT': 1
}
def test_expired_login_token_sends_email(self):
e = 'matt@lp.com'
with capture_passwordless_login_requests() as requests:
self.client.post('/login', data=dict(email=e), follow_redirects=True)
token = requests[0]['login_token']
time.sleep(1.25)
with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.get('/login/' + token, follow_redirects=True)
expire_text = self.AUTH_CONFIG['SECURITY_LOGIN_WITHIN']
msg = self.app.config['SECURITY_MSG_LOGIN_EXPIRED'][0] % dict(within=expire_text, email=e)
self.assertIn(msg, r.data)
self.assertEqual(len(outbox), 1)
self.assertIn(e, outbox[0].html)
self.assertNotIn(token, outbox[0].html)
class MongoEngineSecurityTests(DefaultSecurityTests):
def _create_app(self, auth_config):
@@ -627,43 +224,3 @@ class MongoEngineDatastoreTests(DefaultDatastoreTests):
def _create_app(self, auth_config):
from tests.test_app.mongoengine import create_app
return create_app(auth_config)
class AsyncMailTaskTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'USER_COUNT': 1
}
def setUp(self):
super(AsyncMailTaskTests, self).setUp()
self.mail_sent = False
def test_send_email_task_is_called(self):
@self.app.security.send_mail_task
def send_email(msg):
self.mail_sent = True
self.client.post('/reset', data=dict(email='matt@lp.com'))
self.assertTrue(self.mail_sent)
class NoBlueprintTests(SecurityTest):
AUTH_CONFIG = {
'USER_COUNT': 1
}
def _create_app(self, auth_config):
return super(NoBlueprintTests, self)._create_app(auth_config, False)
def test_login_endpoint_is_404(self):
r = self._get('/login')
self.assertEqual(404, r.status_code)
def test_http_auth_without_blueprint(self):
r = self._get('/http', headers={
'Authorization': 'Basic ' + base64.b64encode("matt@lp.com:password")
})
self.assertIn('HTTP Authentication', r.data)