{"id":23402623,"url":"https://github.com/martincastroalvarez/flask-mongodb-celery-messaging-api","last_synced_at":"2025-07-03T00:04:55.926Z","repository":{"id":39842439,"uuid":"201823239","full_name":"MartinCastroAlvarez/flask-mongodb-celery-messaging-api","owner":"MartinCastroAlvarez","description":"Chat server using Flask, MongoDB, Redis and Celery.","archived":false,"fork":false,"pushed_at":"2022-05-25T02:26:27.000Z","size":350,"stargazers_count":4,"open_issues_count":4,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-05T11:12:01.205Z","etag":null,"topics":["celery","chatserver","flask","mongodb","push-notifications","python3","redis"],"latest_commit_sha":null,"homepage":"https://martincastroalvarez.com","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MartinCastroAlvarez.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-08-11T22:49:05.000Z","updated_at":"2023-09-10T17:35:16.000Z","dependencies_parsed_at":"2022-08-27T19:22:14.639Z","dependency_job_id":null,"html_url":"https://github.com/MartinCastroAlvarez/flask-mongodb-celery-messaging-api","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/MartinCastroAlvarez/flask-mongodb-celery-messaging-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fflask-mongodb-celery-messaging-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fflask-mongodb-celery-messaging-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fflask-mongodb-celery-messaging-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fflask-mongodb-celery-messaging-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MartinCastroAlvarez","download_url":"https://codeload.github.com/MartinCastroAlvarez/flask-mongodb-celery-messaging-api/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fflask-mongodb-celery-messaging-api/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263234937,"owners_count":23434916,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["celery","chatserver","flask","mongodb","push-notifications","python3","redis"],"created_at":"2024-12-22T12:29:47.854Z","updated_at":"2025-07-03T00:04:55.762Z","avatar_url":"https://github.com/MartinCastroAlvarez.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Iguazu\nChat server using Flask, MongoDB, Redis and Celery.\n\n![image-alt](./iguazu.jpg)\n\n## References\n- [Celery official documentation](https://docs.celeryproject.org/en/latest/getting-started/first-steps-with-celery.html)\n- [Class-based tasks with Celery](https://stackoverflow.com/questions/41788017/register-celery-class-based-task)\n- [Redis with Flask](https://redis-py.readthedocs.io/en/latest/)\n- [JWT with Flask](https://codeburst.io/jwt-authorization-in-flask-c63c1acf4eeb)\n- [Bcrypt with Python](https://blog.ruanbekker.com/blog/2018/07/04/salt-and-hash-example-using-python-with-bcrypt-on-alpine/)\n- [Elasticsearch with Docker](https://www.elastic.co/guide/en/elasticsearch/reference/6.1/docker.html)\n- [Flask REST API](https://flask-restful.readthedocs.io/en/latest/)\n- [Redis connector with Flask](https://pypi.org/project/flask-redis/)\n- [Embedded documents with Mongo Engine](https://stackoverflow.com/questions/27846322)\n- [DockerHub Celery](https://hub.docker.com/_/celery)\n- [DockerHub MongoDB](https://hub.docker.com/_/mongo)\n- [DockerHub Redis](https://hub.docker.com/_/redis)\n- [Running MongoDB with Docker](https://www.thepolyglotdeveloper.com/2019/01/getting-started-mongodb-docker-container-deployment/)\n- [Setting MongoDB credentials](https://forums.docker.com/t/create-new-database-in-mongodb-with-docker-compose/58306/2)\n- [Requiring password in Redis with Docker](https://nickjanetakis.com/blog/docker-tip-27-setting-a-password-on-redis-without-a-custom-config)\n- [Flask, Celery and Redis integration](https://github.com/mattkohl/docker-flask-celery-redis)\n- [Mongo Engine with Flask](http://docs.mongoengine.org/projects/flask-mongoengine/en/latest/MONGODB_SETTINGS)\n- [Indexing with Mongo Engine](https://scalegrid.io/blog/handling-index-creation-with-mongoengine-in-python/)\n- [Pagination with MongoDB](https://www.codementor.io/arpitbhayani/fast-and-efficient-pagination-in-mongodb-9095flbqr)\n- [MongoEngine Documentation](https://buildmedia.readthedocs.org/media/pdf/mongoengine-odm/latest/mongoengine-odm.pdf)\n- [Authenticating to MongoDB with Docker](https://stackoverflow.com/questions/34559557)\n- [MongoDB projection with MongoEngine](https://stackoverflow.com/questions/27021095/mongoengine-limiting-number-of-responses-from-dbref)\n\n## API\n| Method | Endpoint         | Action                |\n| ------ | ---------------- | --------------------- |\n| GET    | /check           | Health Check          |\n| POST   | /check           | Health Check          |\n| GET    | /users           | List Users            |\n| POST   | /users           | Create User           |\n| POST   | /login           | Do Login              |\n| GET    | /messages        | List User Messages    |\n| POST   | /messages        | Send a Message        |\n| GET    | /notifications   | List Notifications    |\n\n## Instructions\n\n#### Installation\nInstall all the dependencies:\n```bash\nvirtualenv -p python3 .env\nsource .env/bin/activate\npip install -r requirements.txt\n```\nBuild the local Docker images:\n\n#### Setup\nRun all the services:\n```bash\nsudo docker-compose up \\\n    --detach \\\n    --build \\\n    --renew-anon-volumes\n```\n\n#### Health Checks\nValidate MongoDB is running:\n```bash\nsudo docker logs iguazu_nosql-server_1\n```\nYou should expect to see the following:\n```bash\n[...]\n[...] waiting for connections on port 27017\n[...]\n```\nValidate Redis is up and running:\n```bash\nsudo docker logs iguazu_cache-server_1\n```\nYou should expect to see the following:\n```bash\n[...]\n[...] Running mode=standalone, port=6379.\n[...]\n```\nValidate the web service is up and running:\n```bash\nsudo docker logs iguazu_web-server_1\n```\nYou should expect to see the following:\n```bash\n[...]\n[...] INFO success: uwsgi entered RUNNING state\n[...]\n```\nValidate that the app is up and running.\n```bash\ncurl -i -d '' -XPOST http://localhost:8080/check\n```\nYou should expect to see the following:\n```bash\n{\n    \"health\": \"ok\"\n}\n```\nValidate that Celery is up and running:\n```bash\nsudo docker logs iguazu_worker-1_1 --follow --tail 30\n```\nYou should expect to see the following:\n```bash\n[...]\n[...] mingle: searching for neighbors\n[...] mingle: all alone\n[...] celery@b0d4e3fc35bb ready.\n[...]\n```\n\n#### Development\nYou may perform changes to the Flask app and then run:\n```bash\nsudo docker restart iguazu_web-server_1 iguazu_worker-1_1\n```\nYou may tail the web server logs using this command:\n```bash\nsudo docker logs iguazu_web-server_1 --follow --tail 100\nsudo docker logs iguazu_web-worker_1 --follow --tail 100\n```\n\n#### Unit Tests\nExecute this command to run Unit Tests:\n```bash\nexport PYTHONPATH=\"$PYTHONPATH:$(pwd)\"\nnosetests \\\n    --cover-min-percentage 20 \\\n    --logging-level=DEBUG \\\n    -a \"unit_test=true\" \\\n    --with-coverage \\\n    --cover-erase \\\n    --detailed-errors \\\n    --cover-package ./app \\\n    ./tests\n```\nYou should expect something like this:\n```bash\nName                               Stmts   Miss  Cover\n------------------------------------------------------\napp/__init__.py                       46      9    80%\napp/api/__init__.py                   12      0   100%\napp/api/auth.py                       19      0   100%\napp/api/health.py                     15      0   100%\napp/api/messages.py                   27      3    89%\napp/api/notifications.py              17      0   100%\napp/api/users.py                      27      3    89%\napp/config.py                         34      0   100%\napp/controllers/__init__.py           28     12    57%\napp/controllers/health.py             31     16    48%\napp/controllers/messages.py           65     38    42%\napp/controllers/notifications.py      44     23    48%\napp/controllers/users.py              68     44    35%\napp/exceptions/__init__.py            20      0   100%\napp/exceptions/auth.py                13      0   100%\napp/exceptions/form.py                61      0   100%\napp/exceptions/health.py               8      0   100%\napp/exceptions/not_found.py            7      0   100%\napp/main.py                            4      4     0%\napp/models/__init__.py                 2      0   100%\napp/models/message.py                 41      1    98%\napp/models/notification.py            17      2    88%\napp/models/user.py                    19      1    95%\napp/security/__init__.py               0      0   100%\napp/security/encryption.py            21      9    57%\napp/security/login.py                 17      6    65%\napp/validations/__init__.py            6      2    67%\napp/validations/messages.py           61     31    49%\napp/validations/notifications.py      28     13    54%\napp/validations/pagination.py         11      5    55%\napp/validations/users.py              27     12    56%\napp/worker/__init__.py                 3      0   100%\napp/worker/main.py                     3      3     0%\napp/worker/messages.py                18      8    56%\napp/worker/tasks.py                    5      0   100%\napp/worker/users.py                   18      8    56%\n------------------------------------------------------\nTOTAL                                843    253    70%\n----------------------------------------------------------------------\nRan 12 tests in 0.138s\n\nOK\n```\n\n#### Tear Down\nYou can stop the services using this command:\n```bash\nsudo docker-compose down\n```\n\n## Deployment\nThis section is out of scope.\n\n## Regression Tests\n\n##### 1st test: Basic Health Check\nValidate that the app is up and running.\n```bash\ncurl -i -d '' -XPOST http://localhost:8080/check\n```\nExpect this response:\n```bash\n{\n    \"health\": \"ok\"\n}\n```\nIn case of errors, you would get something like this:\n```bash\n{\n    \"code\": 503,\n    \"subcode\": 5002,\n    \"error\": \"App Not Healthy\"\n}\n```\n\n##### 2nd test: Unauthorized Access\nSend an unauthorized request:\n```bash\ncurl -i -d '{\n    \"sender\": 0, \"recipient\": 1,\n    \"content\": {\"type\": \"video\", \n        \"source\": \"youtube\",\n        \"url\", \"https://www.youtube.com/watch?v=wbZZy9yogg8\"}}' \\\n    -H \"Content-Type: application/json\" \\\n    -XPOST http://localhost:8080/messages\n```\nExpect this response:\n```bash\n{\n  \"msg\": \"Missing Authorization Header\"\n}\n```\n\n##### 3nd test: Unauthorized Access\nSend an unauthorized request:\n```bash\ncurl -i -d '{\n    \"sender\": 0, \"recipient\": 1,\n    \"content\": {\"type\": \"video\", \n        \"source\": \"youtube\",\n        \"url\", \"https://www.youtube.com/watch?v=wbZZy9yogg8\"}}' \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Authorization: Bearer lorem-ipsum\" \\\n    -XPOST http://localhost:8080/messages\n```\nExpect this response:\n```bash\n{\n  \"msg\": \"Not enough segments\"\n}\n```\n\n##### 4th test: Login\nDo login:\n```bash\ncurl -i -d '{\"username\": \"daikiri\", \"password\": \"tekila\"}' \\\n    -H \"Content-Type: application/json\" \\\n    -XPOST http://localhost:8080/login\n```\nExpect this response:\n```bash\n{\n    \"id\": \"71c240bd63284b9ca5e69b5b7a6618e1\",\n    \"timestamp\": \"2019-08-11 04:47:02.602000\",\n    \"username\": \"daikiri\",\n    \"password\": \"...',\n    \"token\": \"...\",\n    \"refresh_token\": \"...\"\n}\n```\nYou may then save the token to a variable this way:\n```bash\nTOKEN=$(curl -d '{\"username\": \"daikiri\", \"password\": \"tekila\"}' \\ \n    -H \"Content-Type: application/json\" \\\n    -XPOST http://localhost:8080/login | jq -r '.token')\necho \"Token: $TOKEN\"\n```\n\n##### 5th test: User creation\nCreate a new user:\n```bash\nNEW_USERNAME=\"gin.$(date +%s)\"\necho \"New User: $NEW_USERNAME\"\ncurl -i -d '{\"username\": \"'$NEW_USERNAME'\", \"password\": \"tonic\"}' \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Authorization: Bearer $TOKEN\" \\\n    -XPOST http://localhost:8080/users\n```\nExpect this response:\n```bash\n{\n    \"job_id\": \"fd903510-3833-4668-bcf0-336e2cb533d4\"\n}\n```\nYou may send the request in this test twice to get a conflict error.\n\n##### 6th test: Notifications\nList your notifications:\n```bash\ncurl -i -d '{\"limit\": 2}' \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Authorization: Bearer $TOKEN\" \\\n    -XGET http://localhost:8080/notifications\n```\nExpect this response:\n```bash\n{\n    \"notifications\": [\n        {\n            \"message\": {\n                \"id\": \"5d506506e2ef969b83b36fdd\",\n                \"timestamp\": \"2019-08-11 18:57:10.149127\",\n                \"username\": \"gin.1565549830\",\n                \"password\": \"...\"\n            },\n            \"is_error\": false,\n            \"code\": 10099,\n            \"title\": \"User Creation\",\n            \"timestamp\": \"2019-08-11 18:57:10.379000\"\n        },\n        {\n            \"message\": {\n                \"code\": 400,\n                \"subcode\": 4004,\n                \"error\": \"Username Already Taken\"\n            },\n            \"is_error\": true,\n            \"code\": 10099,\n            \"title\": \"User Creation\",\n            \"timestamp\": \"2019-08-11 18:57:20.029000\"\n        }\n    ]\n}\n```\n\n##### 7th test: List existing users\nList users:\n```bash\ncurl -i -d '{\"limit\": 2}' \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Authorization: Bearer $TOKEN\" \\\n    -XGET http://localhost:8080/users\n```\nExpect this response:\n```bash\n{\n    \"users\": [\n        {\n            \"id\": \"5d5064ea5d65dc6eddb0d161\",\n            \"timestamp\": \"2019-08-11 18:56:41.969000\",\n            \"username\": \"daikiri\",\n            \"password\": ...\"\n        },\n        {\n            \"id\": \"5d5064fdcb3bd9f18db36fdd\",\n            \"timestamp\": \"2019-08-11 18:57:01.744000\",\n            \"username\": \"gin.1565549821\",\n            \"password\": \"...\"\n        }\n    ]\n}\n```\nYou may then store the IDs in env variables:\n```bash\nUSERS=$(curl -d '{\"limit\": 3}' -H \"Content-Type: application/json\" \\\n    -H \"Authorization: Bearer $TOKEN\" -XGET http://localhost:8080/users)\nUSER1=$(echo $USERS | jq -r '.users[0].id')\nUSER2=$(echo $USERS | jq -r '.users[1].id')\necho \"User1: $USER1\"\necho \"User2: $USER2\"\n```\n\n##### 8th test: Sending image messages\nSend a new image message:\n```bash\ncurl -i -d '{\n    \"sender_id\": \"'$USER1'\",\n    \"recipient_id\": \"'$USER2'\",\n    \"content\": {\n        \"type\": \"image\", \n        \"height\": 100,\n        \"width\": 100,\n        \"url\": \"https://via.placeholder.com/150\"\n    }}' \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Authorization: Bearer $TOKEN\" \\\n    -XPOST http://localhost:8080/messages\n```\nExpect this response:\n```bash\n{\n    \"job_id\": \"68e5369d-dcd1-4ea2-9f3d-56f2825771b1\"\n}\n```\n\n##### 9th test: Sending video messages\nSend a new video message:\n```bash\ncurl -i -d '{\n    \"sender_id\": \"'$USER1'\",\n    \"recipient_id\": \"'$USER2'\",\n    \"content\": {\n        \"type\": \"video\", \n        \"source\": \"youtube\",\n        \"url\": \"https://www.youtube.com/watch?v=wbZZy9yogg8\"\n    }}' \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Authorization: Bearer $TOKEN\" \\\n    -XPOST http://localhost:8080/messages\n```\nExpect this response:\n```bash\n{\n    \"job_id\": \"68e5369d-dcd1-4ea2-9f3d-56f2825771b1\"\n}\n```\n\n##### 10th test: Sending text messages\nSend a new text message:\n```bash\ncurl -i -d '{\n    \"sender_id\": \"'$USER1'\",\n    \"recipient_id\": \"'$USER2'\",\n    \"content\": {\n        \"type\": \"text\",\n        \"text\": \"Lorem Ipsum Dolor Sit Amet\"\n    }}' \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Authorization: Bearer $TOKEN\" \\\n    -XPOST http://localhost:8080/messages\n```\nExpect this response:\n```bash\n{\n    \"job_id\": \"68e5369d-dcd1-4ea2-9f3d-56f2825771b1\"\n}\n```\n\n##### 11th test: Notifications\nList your notifications:\n```bash\ncurl -i -d '{\"limit\": 3}' \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Authorization: Bearer $TOKEN\" \\\n    -XGET http://localhost:8080/notifications\n```\nExpect this response:\n```bash\n{\n    \"notifications\": [\n        {\n            \"message\": {\n                \"id\": \"5d5066fac18c26c62b30ae5a\",\n                \"sender\": \"5d5064ea5d65dc6eddb0d161\",\n                \"recipient\": \"5d5064fdcb3bd9f18db36fdd\",\n                \"timestamp\": \"2019-08-11 19:05:30.012855\",\n                \"content\": {\n                    \"type\": \"image\",\n                    \"url\": \"https://via.placeholder.com/150\",\n                    \"width\": 100,\n                    \"height\": 100\n                }\n            },\n            \"is_error\": false,\n            \"code\": 10095,\n            \"title\": \"New Message\",\n            \"timestamp\": \"2019-08-11 19:05:30.035000\"\n        },\n        {\n            \"message\": {\n                \"id\": \"5d5067093105de528230ae5a\",\n                \"sender\": \"5d5064ea5d65dc6eddb0d161\",\n                \"recipient\": \"5d5064fdcb3bd9f18db36fdd\",\n                \"timestamp\": \"2019-08-11 19:05:45.605418\",\n                \"content\": {\n                    \"type\": \"video\",\n                    \"url\": \"https://www.youtube.com/watch?v=wbZZy9yogg8\",\n                    \"source\": \"youtube\"\n                }\n            },\n            \"is_error\": false,\n            \"code\": 10095,\n            \"title\": \"New Message\",\n            \"timestamp\": \"2019-08-11 19:05:45.620000\"\n        },\n        {\n            \"message\": {\n                \"id\": \"5d50671d86bf3b84f330ae5a\",\n                \"sender\": \"5d5064ea5d65dc6eddb0d161\",\n                \"recipient\": \"5d5064fdcb3bd9f18db36fdd\",\n                \"timestamp\": \"2019-08-11 19:06:05.616069\",\n                \"content\": {\n                    \"type\": \"text\",\n                    \"text\": \"Lorem Ipsum Dolor Sit Amet\"\n                }\n            },\n            \"is_error\": false,\n            \"code\": 10095,\n            \"title\": \"New Message\",\n            \"timestamp\": \"2019-08-11 19:06:05.637000\"\n        }\n    ]\n}\n```\n\n##### 12nd test: Sending text messages with errors\nSend a new text message:\n```bash\ncurl -i -d '{\n    \"sender_id\": \"'$USER1'\",\n    \"recipient_id\": \"'$USER2'\",\n    \"content\": {\n        \"type\": \"text\"\n    }}' \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Authorization: Bearer $TOKEN\" \\\n    -XPOST http://localhost:8080/messages\n```\nExpect this response:\n```bash\n{\n    \"job_id\": \"68e5369d-dcd1-4ea2-9f3d-56f2825771b1\"\n}\n```\n\n##### 13rd test: Notifications\nList your notifications:\n```bash\ncurl -i -d '{\"limit\": 1}' \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Authorization: Bearer $TOKEN\" \\\n    -XGET http://localhost:8080/notifications\n```\nExpect this response:\n```bash\n{\n    \"notifications\": [\n        {\n            \"message\": {\n                \"code\": 400,\n                \"subcode\": 4010,\n                \"error\": \"Invalid Text\"\n            },\n            \"is_error\": true,\n            \"code\": 10095,\n            \"title\": \"New Message\",\n            \"timestamp\": \"2019-08-11 19:08:48.781000\"\n        }\n    ]\n}\n```\n\n##### 14th test: Listing Messages\nList messages:\n```bash\ncurl -i -d '{\n        \"sender_id\": \"'$USER1'\",\n        \"recipient_id\": \"'$USER2'\",\n        \"limit\": 3\n    }' \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Authorization: Bearer $TOKEN\" \\\n    -XGET http://localhost:8080/messages\n```\nExpect this response:\n```bash\n{\n    \"messages\": [\n        {\n            \"id\": \"5d5066fac18c26c62b30ae5a\",\n            \"sender\": \"5d5064ea5d65dc6eddb0d161\",\n            \"recipient\": \"5d5064fdcb3bd9f18db36fdd\",\n            \"timestamp\": \"2019-08-11 19:05:30.012000\",\n            \"content\": {\n                \"type\": \"image\",\n                \"url\": \"https://via.placeholder.com/150\",\n                \"width\": 100,\n                \"height\": 100\n            }\n        },\n        {\n            \"id\": \"5d5067093105de528230ae5a\",\n            \"sender\": \"5d5064ea5d65dc6eddb0d161\",\n            \"recipient\": \"5d5064fdcb3bd9f18db36fdd\",\n            \"timestamp\": \"2019-08-11 19:05:45.605000\",\n            \"content\": {\n                \"type\": \"video\",\n                \"url\": \"https://www.youtube.com/watch?v=wbZZy9yogg8\",\n                \"source\": \"youtube\"\n            }\n        },\n        {\n            \"id\": \"5d50671d86bf3b84f330ae5a\",\n            \"sender\": \"5d5064ea5d65dc6eddb0d161\",\n            \"recipient\": \"5d5064fdcb3bd9f18db36fdd\",\n            \"timestamp\": \"2019-08-11 19:06:05.616000\",\n            \"content\": {\n                \"type\": \"text\",\n                \"text\": \"Lorem Ipsum Dolor Sit Amet\"\n            }\n        }\n    ]\n}\n```\n\n```bash\nTOKEN=$(curl -d '{\"username\": \"daikiri\", \"password\": \"tekila\"}' \\ \n    -H \"Content-Type: application/json\" \\\n    -XPOST http://localhost:8080/login | jq -r '.token')\n```\n##### 15th test: Messages pagination\nList messages and record the last ID:\n```bash\nSEARCH=$(curl -d '{\n        \"sender_id\": \"'$USER1'\",\n        \"recipient_id\": \"'$USER2'\",\n        \"limit\": 2\n    }' \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Authorization: Bearer $TOKEN\" \\\n    -XGET http://localhost:8080/messages)\nLAST_MESSAGE_ID=$(echo $SEARCH | jq -r '.messages[-1].id')\necho $SEARCH | jq -r '.messages[] | \"\\(.id) \\(.timestamp)\"'\n```\nNow recursively fetch more messages:\n```bash\nSEARCH=$(curl -d '{\n        \"sender_id\": \"'$USER1'\",\n        \"recipient_id\": \"'$USER2'\",\n        \"start\": \"'$LAST_MESSAGE_ID'\",\n        \"limit\": 2\n    }' \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Authorization: Bearer $TOKEN\" \\\n    -XGET http://localhost:8080/messages)\nLAST_MESSAGE_ID=$(echo $SEARCH | jq -r '.messages[-1].id')\necho $SEARCH | jq -r '.messages[] | \"\\(.id) \\(.timestamp)\"'\n```\nYou should expect to see a reverse time series in your CLI:\n```bash\n5d50964307579d45c867dafd 2019-08-11 22:27:15.245000\n5d509642deab8b5e9e67dafb 2019-08-11 22:27:14.147000\n5d50964201405eb25b67dafd 2019-08-11 22:27:14.693000\n5d5096405504dc87a467dafd 2019-08-11 22:27:12.878000\n5d50964007579d45c867dafc 2019-08-11 22:27:12.518000\n5d50964001405eb25b67dafc 2019-08-11 22:27:12.043000\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmartincastroalvarez%2Fflask-mongodb-celery-messaging-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmartincastroalvarez%2Fflask-mongodb-celery-messaging-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmartincastroalvarez%2Fflask-mongodb-celery-messaging-api/lists"}