I am trying to write some integration tests for my FastAPI endpoints and am not sure what the best solution is to testing the endpoints that require a user to be logged in.
I am following the FastAPI authentication documentation for my auth flow which is just username password and then receiving a token.
How can I test endpoints using a logged in user?
Example endpoint to test:
#app.get("/lists/{id}", response_model=ListDto)
async def get_list(
id: int,
current_user: User = Depends(get_current_active_user),
):
usecase = GetList(list_id=id, current_user=current_user)
list = usecase()
if not llist:
raise HTTPException(status_code=404, detail=f"List with id:{id} not found")
return list
To test endpoints that require a user to be logged in, you will need to simulate a login process to obtain a valid token and then pass it as a header to your subsequent requests. Here is an example of how you can do this in your integration tests:
Create a fixture for your client and a test user
from fastapi.testclient import TestClient
from app.main import app
#pytest.fixture(scope="module")
def client():
with TestClient(app) as c:
yield c
#pytest.fixture(scope="module")
def test_user():
return {"username": "testuser", "password": "testpass"}
Write a test case for the login endpoint that retrieves a valid token for your test user.
def test_login(client, test_user):
response = client.post("/login", data=test_user)
assert response.status_code == 200
token = response.json()["access_token"]
assert token is not None
return token
Pass the token as a header in subsequent requests that require authentication. For example, here is how you can test the get_list endpoint:
def test_get_list(client, test_user):
token = test_login(client, test_user)
response = client.get("/lists/1", headers={"Authorization": f"Bearer {token}"})
assert response.status_code == 200
assert response.json()["id"] == 1
Related
I'm trying to use fastapi to return some basic ML models to users.
Currently, I secure user details with firebase auth. I want to use the JWT's users have when using the basic application to authenticate their request for the ML model.
With fastapi, there doesn't seem to be a straightforward answer to doing this.
I've followed two main threads as ways to work out how to do this, but am a bit lost as to how I can simply take the JWT from the header of a request and check it against firebase admin or whathave you?
Following this tutorial and using this package, I end up with something like this,
https://github.com/tokusumi/fastapi-cloudauth . This doesn't really do anything - it doesn't authenticate the JWT for me, bit confused as to if this package is actually worthwhile?
from fastapi import FastAPI, HTTPException, Header,Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi_cloudauth.firebase import FirebaseCurrentUser, FirebaseClaims
app = FastAPI()
security = HTTPBearer()
origins = [
xxxx
]
app.add_middleware(
xxxx
)
get_current_user = FirebaseCurrentUser(
project_id=os.environ["PROJECT_ID"]
)
#app.get("/user/")
def secure_user(current_user: FirebaseClaims = Depends(get_current_user)):
# ID token is valid and getting user info from ID token
return f"Hello, {current_user.user_id}"
Alternatively, looking at this,
https://github.com/tiangolo/fastapi/issues/4768
It seems like something like this would work,
security = HTTPBearer()
api = FastAPI()
security = HTTPBearer()
firebase_client = FirebaseClient(
firebase_admin_credentials_url=firebase_test_admin_credentials_url
# ...
)
user_roles = [test_role]
async def firebase_authentication(token: HTTPAuthorizationCredentials = Depends(security)) -> dict:
user = firebase_client.verify_token(token.credentials)
return user
async def firebase_authorization(user: dict = Depends(firebase_authentication)):
roles = firebase_client.get_user_roles(user)
for role in roles:
if role in user_roles:
return user
raise HTTPException(detail="User does not have the required roles", status_code=HTTPStatus.FORBIDDEN)
#api.get("/")
async def root(uid: str = Depends(firebase_authorization)):
return {"message": "Successfully authenticated & authorized!"}
But honestly I'm a bit confused about how I would set up the firebase environment variables, what packages I would need (firebaseadmin?)
Would love some helpers, thanks!
I hope this will help:
Create functions to work with Firebase admin, create credentials from Firebase as JSON file:
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import Depends, HTTPException, status, Response
from firebase_admin import auth, credentials, initialize_app
credential = credentials.Certificate('./key.json')
initialize_app(credential)
def get_user_token(res: Response, credential: HTTPAuthorizationCredentials=Depends(HTTPBearer(auto_error=False))):
if cred is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Bearer authentication is needed",
headers={'WWW-Authenticate': 'Bearer realm="auth_required"'},
)
try:
decoded_token = auth.verify_id_token(credential.credentials)
except Exception as err:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Invalid authentication from Firebase. {err}",
headers={'WWW-Authenticate': 'Bearer error="invalid_token"'},
)
res.headers['WWW-Authenticate'] = 'Bearer realm="auth_required"'
return decoded_token
Then put it inside your FastAPI main function:
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import Depends, HTTPException, status, Response, FastAPI, Depends
from firebase_admin import auth, credentials, initialize_app
credential = credentials.Certificate('./key.json')
initialize_app(credential)
def get_user_token(res: Response, credential: HTTPAuthorizationCredentials=Depends(HTTPBearer(auto_error=False))):
if cred is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Bearer authentication is needed",
headers={'WWW-Authenticate': 'Bearer realm="auth_required"'},
)
try:
decoded_token = auth.verify_id_token(credential.credentials)
except Exception as err:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Invalid authentication from Firebase. {err}",
headers={'WWW-Authenticate': 'Bearer error="invalid_token"'},
)
res.headers['WWW-Authenticate'] = 'Bearer realm="auth_required"'
return decoded_token
app = FastAPI()
#app.get("/api/")
async def hello():
return {"msg":"Hello, this is API server"}
#app.get("/api/user_token")
async def hello_user(user = Depends(get_user_token)):
return {"msg":"Hello, user","uid":user['uid']}
P.S. Do not forget to install requirements:
pip3 install firebase_admin
What I want to achieve? Have one service responsible for HTTP Basic Auth (access) and two services (a, b) where some endpoints are protected by access service.
Why? In scenario where there will be much more services with protected endpoints to not duplicate authorize function in each service. Also to do modification in one place in case of changing to OAuth2 (maybe in future).
What I did?
I followed guide on official website and created example service which works totally fine.
Problem occurs when I try to move authorization to separate service and then use it within few other
services with protected endpoints. I can't figure out how to do it. Could you please help me out?
I have tried different functions setup. Nothing helped, so far my code looks like this:
access-service
import os
import secrets
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
security = HTTPBasic()
def authorize(credentials: HTTPBasicCredentials = Depends(security)):
is_user_ok = secrets.compare_digest(credentials.username, os.getenv('LOGIN'))
is_pass_ok = secrets.compare_digest(credentials.password, os.getenv('PASSWORD'))
if not (is_user_ok and is_pass_ok):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='Incorrect email or password.',
headers={'WWW-Authenticate': 'Basic'},
)
app = FastAPI(openapi_url="/api/access/openapi.json", docs_url="/api/access/docs")
#app.get('/api/access/auth', dependencies=[Depends(authorize)])
def auth():
return {"Granted": True}
a-service
import httpx
import os
from fastapi import Depends, FastAPI, HTTPException, status
ACCESS_SERVICE_URL = os.getenv('ACCESS_SERVICE_URL')
app = FastAPI(openapi_url="/api/a/openapi.json", docs_url="/api/a/docs")
def has_access():
result = httpx.get(os.getenv('ACCESS_SERVICE_URL'))
if result.status_code == 401:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='No access to resource. Login first.',
)
#app.get('/api/a/unprotected_a')
async def unprotected_a():
return {"Protected": False}
#app.get('/api/a/protected_a', dependencies=[Depends(has_access)])
async def protected_a():
return {"Protected": True}
#app.get('/api/a/protected_b', dependencies=[Depends(has_access)])
async def protected_b():
return {"Protected": True}
The issue here is that, when you are calling Service_A with credentials it's making a call to the Access_Service in the has_access() function.
If you look closely,
result = httpx.get(os.getenv('ACCESS_SERVICE_URL'))
You are simply making a GET call without forwarding the credentials as headers for this request to the Access_Service.
Rewrite your has_access() in all the services to
from typing import Optional
from fastapi import Header
def has_access(authorization: Optional[str] = Header(None)):
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='No access to resource. Credentials missing!',
)
headers = {'Authorization': authorization}
result = httpx.get(os.getenv('ACCESS_SERVICE_URL'), headers=headers)
if result.status_code == 401:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='No access to resource. Login first.',
)
Inside your access service you have mistakenly typed True as true,
#app.get('/api/access/auth', dependencies=[Depends(authorize)])
def auth():
return {"Granted": True}
I have cloned your repo and tested it, it's working now. Please check and confirm.
[EDIT] Swagger does not allow authorization header for basic auth (https://github.com/tiangolo/fastapi/issues/612)
Work-Around (not recommended)
from fastapi.security import HTTPBasic, HTTPBasicCredentials
security = HTTPBasic()
def has_access(credentials: HTTPBasicCredentials = Depends(security), authorization: Optional[str] = Header(None)):
Thanks to Soumojit Ghosh answer and FastAPI Issue 1037 I figured out how should I modify my code. a-service after changes:
import httpx
import os
from fastapi import Depends, FastAPI, Header, HTTPException, status
from typing import Optional
from fastapi.security import HTTPBasicCredentials, HTTPBearer
security = HTTPBearer()
ACCESS_SERVICE_URL = os.getenv('ACCESS_SERVICE_URL')
app = FastAPI(openapi_url="/api/a/openapi.json", docs_url="/api/a/docs")
def has_access(credentials: HTTPBasicCredentials = Depends(security)):
response = httpx.get(os.getenv('ACCESS_SERVICE_URL'), headers={'Authorization': credentials.credentials})
if response.status_code == 401:
raise HTTPException(status_code=401)
#app.get('/api/a/unprotected_a')
async def unprotected_a():
return {"Protected": False}
#app.get('/api/a/protected_a', dependencies=[Depends(has_access)])
async def protected_a():
return {"Protected": True}
#app.get('/api/a/protected_b', dependencies=[Depends(has_access)])
async def protected_b():
return {"Protected": True}
Now header can be sent through SwaggerUI. Click Authorize and then enter it in Value field. To generate your header from login and password you can use for example this tool. It will look like: Basic YWRtaW46cGFzc3dvcmQ=.
I may have a bad understanding of how the flask session works, but I am trying to generate a Spotify API access token using SpotiPY with the Authorization Code Flow, and store it in Flask's session storage.
The program doesn't seem to be able to store it, and therefore later runs in to an error when trying to call it. Here is a visual explanation with images and captions: https://imgur.com/a/KiYZFiQ
Here is the main server script:
from flask import Flask, render_template, redirect, request, session, make_response,session,redirect
import spotipy
import spotipy.util as util
from credentz import *
app = Flask(__name__)
app.secret_key = SSK
#app.route("/")
def verify():
session.clear()
session['toke'] = util.prompt_for_user_token("", scope='playlist-modify-private,playlist-modify-public,user-top-read', client_id=CLI_ID, client_secret=CLI_SEC, redirect_uri="http://127.0.0.1:5000/index")
return redirect("/index")
#app.route("/index")
def index():
return render_template("index.html")
#app.route("/go", methods=['POST'])
def go():
data=request.form
sp = spotipy.Spotify(auth=session['toke'])
response = sp.current_user_top_artists(limit=data['num_tracks'], time_range=data['time_range'])
return render_template("results.html",data=data)
if __name__ == "__main__":
app.run(debug=True)
and here are the two html files: Index.html, Results.html
Some things worth noting:
Credentz stores all of the private info including SSK, CLI_SEC, and CLI_ID.
The spotipy request works fine if I do it in a non-flask environment with no web-browser interaction.
I am able to store other things in the session storage, and call it back later, just not the access token for some reason.
My best guess is that the page doesn't have time to store it before the Spotify api redirects the page, not sure though.
Any help is really appreciated, Thank You!
You are right, spotify is redirecting you before the token can be added therefore ending the execution of that function. So you need to add a callback step to grab the authentication token after you've been redirected.
Spotitpy's util.prompt_for_user_token method is not designed to be used in a web server so your best best is to implement the auth flow your self and then use spotipy once you have the token. Luckily the spotify authorization flow is pretty simple and easy to implement.
Method 1: Implementing auth flow our self:
from flask import Flask, render_template, redirect, request, session, make_response,session,redirect
import spotipy
import spotipy.util as util
from credentz import *
import requests
app = Flask(__name__)
app.secret_key = SSK
API_BASE = 'https://accounts.spotify.com'
# Make sure you add this to Redirect URIs in the setting of the application dashboard
REDIRECT_URI = "http://127.0.0.1:5000/api_callback"
SCOPE = 'playlist-modify-private,playlist-modify-public,user-top-read'
# Set this to True for testing but you probably want it set to False in production.
SHOW_DIALOG = True
# authorization-code-flow Step 1. Have your application request authorization;
# the user logs in and authorizes access
#app.route("/")
def verify():
auth_url = f'{API_BASE}/authorize?client_id={CLI_ID}&response_type=code&redirect_uri={REDIRECT_URI}&scope={SCOPE}&show_dialog={SHOW_DIALOG}'
print(auth_url)
return redirect(auth_url)
#app.route("/index")
def index():
return render_template("index.html")
# authorization-code-flow Step 2.
# Have your application request refresh and access tokens;
# Spotify returns access and refresh tokens
#app.route("/api_callback")
def api_callback():
session.clear()
code = request.args.get('code')
auth_token_url = f"{API_BASE}/api/token"
res = requests.post(auth_token_url, data={
"grant_type":"authorization_code",
"code":code,
"redirect_uri":"http://127.0.0.1:5000/api_callback",
"client_id":CLI_ID,
"client_secret":CLI_SEC
})
res_body = res.json()
print(res.json())
session["toke"] = res_body.get("access_token")
return redirect("index")
# authorization-code-flow Step 3.
# Use the access token to access the Spotify Web API;
# Spotify returns requested data
#app.route("/go", methods=['POST'])
def go():
data = request.form
sp = spotipy.Spotify(auth=session['toke'])
response = sp.current_user_top_artists(limit=data['num_tracks'], time_range=data['time_range'])
return render_template("results.html", data=data)
if __name__ == "__main__":
app.run(debug=True)
On the other hand we can cheat a little bit and use the methods that spotipy itself is using in order to save us some code.
Method 2: Implementing auth flow (along with custom token management) using spotipy.oauth2.SpotifyOAuth method:
from flask import Flask, render_template, redirect, request, session, make_response,session,redirect
import spotipy
import spotipy.util as util
from credentz import *
import time
import json
app = Flask(__name__)
app.secret_key = SSK
API_BASE = 'https://accounts.spotify.com'
# Make sure you add this to Redirect URIs in the setting of the application dashboard
REDIRECT_URI = "http://127.0.0.1:5000/api_callback"
SCOPE = 'playlist-modify-private,playlist-modify-public,user-top-read'
# Set this to True for testing but you probaly want it set to False in production.
SHOW_DIALOG = True
# authorization-code-flow Step 1. Have your application request authorization;
# the user logs in and authorizes access
#app.route("/")
def verify():
# Don't reuse a SpotifyOAuth object because they store token info and you could leak user tokens if you reuse a SpotifyOAuth object
sp_oauth = spotipy.oauth2.SpotifyOAuth(client_id = CLI_ID, client_secret = CLI_SEC, redirect_uri = REDIRECT_URI, scope = SCOPE)
auth_url = sp_oauth.get_authorize_url()
print(auth_url)
return redirect(auth_url)
#app.route("/index")
def index():
return render_template("index.html")
# authorization-code-flow Step 2.
# Have your application request refresh and access tokens;
# Spotify returns access and refresh tokens
#app.route("/api_callback")
def api_callback():
# Don't reuse a SpotifyOAuth object because they store token info and you could leak user tokens if you reuse a SpotifyOAuth object
sp_oauth = spotipy.oauth2.SpotifyOAuth(client_id = CLI_ID, client_secret = CLI_SEC, redirect_uri = REDIRECT_URI, scope = SCOPE)
session.clear()
code = request.args.get('code')
token_info = sp_oauth.get_access_token(code)
# Saving the access token along with all other token related info
session["token_info"] = token_info
return redirect("index")
# authorization-code-flow Step 3.
# Use the access token to access the Spotify Web API;
# Spotify returns requested data
#app.route("/go", methods=['POST'])
def go():
session['token_info'], authorized = get_token(session)
session.modified = True
if not authorized:
return redirect('/')
data = request.form
sp = spotipy.Spotify(auth=session.get('token_info').get('access_token'))
response = sp.current_user_top_tracks(limit=data['num_tracks'], time_range=data['time_range'])
# print(json.dumps(response))
return render_template("results.html", data=data)
# Checks to see if token is valid and gets a new token if not
def get_token(session):
token_valid = False
token_info = session.get("token_info", {})
# Checking if the session already has a token stored
if not (session.get('token_info', False)):
token_valid = False
return token_info, token_valid
# Checking if token has expired
now = int(time.time())
is_token_expired = session.get('token_info').get('expires_at') - now < 60
# Refreshing token if it has expired
if (is_token_expired):
# Don't reuse a SpotifyOAuth object because they store token info and you could leak user tokens if you reuse a SpotifyOAuth object
sp_oauth = spotipy.oauth2.SpotifyOAuth(client_id = CLI_ID, client_secret = CLI_SEC, redirect_uri = REDIRECT_URI, scope = SCOPE)
token_info = sp_oauth.refresh_access_token(session.get('token_info').get('refresh_token'))
token_valid = True
return token_info, token_valid
if __name__ == "__main__":
app.run(debug=True)
Either method is equally valid in my opinion, just go with whichever method you find easier to understand.
Make sure that you update the authorized Redirect URIs in your spotify application dashboard.
See the spotify docs for more info: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow
I'm trying to use the OAuth token and storing it in the session object as I move between different subdomains. So I have the application starting the OAuth1.0a workflow at flask.mydomain.com. It then redirects to sub.mydomain.com/initiate to fetch the request tokens and I want to save the oauth token secret using sessions (not sure if this is the best way), and then authorizes at sub.mydomain.com/authorize. Once the authorization is complete, it goes to the callback but the oauth token secret does not exist in the session. I'm also trying to save the OAuth1Session so that whenever we go to a new route, that data is saved. So I'm not sure how to handle it since the sessions are defined within the scope of the function.
I read that app.secret_key = os.urandom(someval) doesn't make it work which is what I don't have and I made the OAuth1Session to be global originally which doesn't sound like a good idea (happens in the callback).
#app.route("/initiate")
def initiate():
oauth_session = OAuth1Session(client_key=client_key,client_secret=client_secret,callback_uri=callback_uri)
fetch_response = oauth_session.fetch_request_token(request_token_url)
oauth_token = fetch_response.get('oauth_token')
oauth_token_secret = fetch_response.get('oauth_token_secret')
session['oauth_token_secret'] = oauth_token_secret
full_authorization_url = oauth_session.authorization_url(authorize_url, request_token=oauth_token)
return redirect(full_authorization_url)
#app.route("/callback")
def callback():
session.permanent = True
if ('oauth_token_secret' not in session):
return "There is no oauth token secret"
verifier = request.args.get("oauth_verifier")
oauth_token = request.args.get("oauth_token")
oauth = OAuth1Session(
client_key=client_key,
client_secret=client_secret,
resource_owner_key=oauth_token,
resource_owner_secret=session['oauth_token_secret'],
verifier=verifier) # I would like this oauth session to persist until a certain amount of time before having to reauthenticate
fetch_access_tokens= oauth.fetch_access_token(access_token_url)
return redirect(url_for('.getstatus'))
#app.route("/getStatus")
def getstatus():
r = oauth.get(webservice_url + "/statuses")
if r.status_code == 401:
return redirect(url_for('initiate'))
print(r.content)
return r.content
I fixed the issue, it was with the SERVER_NAME config for the application. The SERVER_NAME was set to flask.mydomain.com, so I just removed it entirely
I have the following class and am currently testing it via running a file which calls the methods in this file. I then use a mixture of print statements and checking the blog to make sure the code has worked.
I'd really like to write some pytest unit tests for this and automate it all, but how can I do this? Also, if authentication doesn't exist or had become invalid it opens the browser and prompts the user to input the auth code. Later on this will be handled by a gui form. Pytest doesn't take user input, and rightly so; it wouldn't be automated.
class BloggerInterface(object):
"""Connects to blogger api and authorises client."""
def get_credentials(self):
"""Gets google api credentials, or generates new credentials
if they don't exist or are invalid."""
scope = 'https://www.googleapis.com/auth/blogger'
flow = oauth2client.client.flow_from_clientsecrets(
'client_secret.json', scope,
redirect_uri='urn:ietf:wg:oauth:2.0:oob')
storage = oauth2client.file.Storage('credentials.dat')
credentials = storage.get()
if not credentials or credentials.invalid:
auth_uri = flow.step1_get_authorize_url()
webbrowser.open(auth_uri)
auth_code = input('Enter the auth code: ')
credentials = flow.step2_exchange(auth_code)
storage.put(credentials)
return credentials
def get_service(self):
"""Returns an authorised blogger api service."""
credentials = self.get_credentials()
http = httplib2.Http()
http = credentials.authorize(http)
service = apiclient.discovery.build('blogger', 'v3', http=http)
return service
def get_blog(self, blog_id):
"""Gets the details ofthe blog withthe id blog_id"""
BlogDetails = collections.namedtuple('BlogDetails', 'blog_id, name, desc, url')
conn = self.get_service()
request = conn.blogs().get(blogId=blog_id, view='ADMIN')
response = request.execute()
name = response.get('name')
desc = response.get('description')
url = response.get('url')
blog = BlogDetails(blog_id=blog_id, name=name, desc=desc, url=url)
return blog
def get_posts(self, blog_id, status='live'):
"""Gets all posts from the blog with the id blog_id"""
posts = []
conn = self.get_service()
request = conn.posts().list(blogId=blog_id, view='ADMIN',
status=status)
#Responses are paginated, so a paging loop is required.
while request:
response = request.execute()
for post in response.get('items', []):
post_id = post.get('id')
title = post.get('title')
url = post.get('url')
status = post.get('status')
content = post.get('content')
posts.append({'post_id':post_id, 'title':title, 'url':url,
'status':status, 'content':content})
request = conn.posts().list_next(request, response)
return posts
def add_post(self, blog_id, post, is_draft=True):
"""Adds a new post to the blog with the id blog_id"""
conn = self.get_service()
#post is in the form {title, content, (labels), author_name, author_id.
title, content, author_name, author_id, labels = post
data = {
'kind': 'blogger#post',
'title': title,
'content': content,
'labels': labels,
'author': {'displayName':author_name, 'id':author_id}
}
request = conn.posts().insert(blogId=blog_id, body=data,
isDraft=is_draft)
response = request.execute()
post_id = response.get('id')
return post_id
Don't test the oauth2client or webbrowser projects. Test how your code reacts to input and output from other parts. Those are black boxes, which you replace with your own mocks, so you can see how your code responds to different return values.
Use the unittest.mock module to produce the mocks. If you are using Python < 3.3, install the backport mock project to do so.
For example, for BloggerInterface.get_credentials(), you mock out oauth2client.client.flow_from_clientsecrets(), oauth2client.file.Storage(), webbrowser.open() and input. You can then play with the response from storage.get() to force your code to use webbrowser.open(), and test if your code correctly tried to open a webbrowser, and then called storage.put() to store the credentials:
with mock.patch('oauth2client.client.flow_from_clientsecrets') as mockflow, \
mock.patch('oauth2client.file.Storage') as MockStorage, \
mock.patch('webbrowser.open') as mockwbopen, \
mock.patch('yourmodule.input') as mockinput:
# set the credentials to invalid
storage = MockStorage.return_value
credentials = storage.get.return_value
credentials.invalid = True
# run the method and see if we get what we want
result = BloggerInterface().get_credentials()
# check that the flow was initialised correctly
mockflow.assert_called_with(
'client_secret.json', 'https://www.googleapis.com/auth/blogger',
redirect_uri='urn:ietf:wg:oauth:2.0:oob')
MockStorage.assert_called_with('credentials.dat')
# With invalid credentials, the code should obtain a auth url from the
# flow, pass it to the browser. Then the authentication code should be taken
# from input and passed back to the flow for exchange. Test these
# interactions took place:
flow.step1_get_authorize_url.assert_called_once_with()
mockwbopen.assert_called_once_with(flow.step1_get_authorize_url.return_value)
flow.step2_exchange.assert_called_once_with(mockinput.return_value)
storage.put(flow.step2_exchange.return_value)
assert result == flow.step2_exchange.return_value