-
Notifications
You must be signed in to change notification settings - Fork 56
Implement /auth with IVLE token #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
I need some guidance on how to test functions that depend on external API calls (and functions that in turn depend on those). Referring to the module Update: Taking a look at ExVCR now. |
Pull Request Test Coverage Report for Build 432
💛 - Coveralls |
lib/cadet/accounts/ivle.ex
Outdated
| case HTTPoison.get(api_url(path, token)) do | ||
| {:ok, response} -> | ||
| if String.length(response.body) > 2 do | ||
| {:ok, String.replace(response.body, ~s("), "")} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there are other " characters in the response (apart from the start and end), I'm thinking it would be represented as \" as well. If this line replaces all the quotes, the \ might remain. Is this the case? Not sure about the ~s(") syntax here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, \ is not a literal character. When printed out to console, \" together represents a literal double quote within a string. ~s(") is a sigil denoting a string, it is equivalent to writing "\"".
iex(1)> ~s(") == "\""
trueThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String.replace looks a bit iffy -- how about Poison.decode!(response.body) == "" instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactor String.replace with Poison.decode! in 6120a44.
| changeset = Registration.changeset(%Registration{}, attrs) | ||
|
|
||
| if changeset.valid?() do | ||
| if changeset.valid? do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@indocomsoft The new style guide in #50 says that zero-arity calls should end with parenthesis, but I think predicate functions should be excluded, because their trailing ? obviously indicates that they are predicate functions, not variables. Which do we go with? Your call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, in the first place, changeset is a map (https://hexdocs.pm/ecto/Ecto.Changeset.html#t:t/0), so valid? is just a key in the map, so there should definitely be no paranthesis here.
indocomsoft
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow! Thank you so much for taking the time to make this pull request -- overall the code is well-done, just a few changes like with construct, pattern-matching in tests, etc.
README.md
Outdated
|
|
||
| Setup environment variables | ||
|
|
||
| export IVLE_KEY="1vl3keyy" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a real key right? Haha maybe you should put some directions like change this or sth. Also this should be in .env file instead, maybe create a .env.example
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| @@ -0,0 +1,29 @@ | |||
| [ | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to move the fixture directory to text/fixture instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with 518307c.
lib/cadet/accounts/accounts.ex
Outdated
| def sign_in(nusnet_id, token) do | ||
| auth = Repo.one(Query.nusnet_id(nusnet_id)) | ||
|
|
||
| if auth == nil do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should reverse the conditional
if auth do # equivalent to if auth != nil
else
endThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with 2b892a6.
lib/cadet/accounts/accounts.ex
Outdated
| if auth == nil do | ||
| nil | ||
| case Ivle.fetch_name(token) do | ||
| {:ok, name} -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use with construct instead to prevent nested case https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with c8ae66d.
lib/cadet/accounts/ivle.ex
Outdated
| case HTTPoison.get(api_url(path, token)) do | ||
| {:ok, response} -> | ||
| if String.length(response.body) > 2 do | ||
| {:ok, String.replace(response.body, ~s("), "")} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String.replace looks a bit iffy -- how about Poison.decode!(response.body) == "" instead?
| Guardian.encode_and_sign(user, %{}, token_type: "refresh", ttl: {52, :weeks}) | ||
|
|
||
| render(conn, "token.json", access_token: access_token, refresh_token: refresh_token) | ||
| case Ivle.fetch_nusnet_id(login.ivle_token) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with construct is better here to prevent pyramid of doom https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with b2834fc.
| response(400, "Missing parameter(s)") | ||
| response(403, "Wrong login attributes") | ||
| response(400, "Missing or invalid parameter") | ||
| response(500, "Internal server error") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eh, this happens?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, IVLE responds with internal server error we send a wrong API key, and I think setting an invalid API key on our end is internal-server-error-esque.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough
| role: :student | ||
| }) | ||
|
|
||
| assert user.name == "happy user" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use pattern matching instead, e.g. assert %{name: "happy user", role: :student} = user. Similarly for the rest
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with 735169f.
| assert {:ok, []} = Accounts.set_nusnet_id(user, "E012345") | ||
| end | ||
|
|
||
| # TODO: A user may not have multiple NUSNET_IDs? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a good idea to validate against this, but probably can be done in a separate PR. This PR is very big already haha
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. Didn't know that this TODO would get pass credo though. Guess it ignores the test files.
| alias CadetWeb.AuthController | ||
| alias Cadet.Auth.Guardian | ||
|
|
||
| @token String.replace(inspect(System.get_env("TOKEN")), ~s("), "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what this does, why not a simple System.get_env("TOKEN")? Also what is this TOKEN environment variable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The token is the authentication token from ivle for the user. Added a brief description in the module docs of tests using @token, and improved the readability with 7075f93.
8b91219 to
0573059
Compare
|
@indocomsoft Made the changes, rebased to master, let me know what you think. Thanks! |
indocomsoft
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking very good, just some small changes
.env
Outdated
| export CADET_WEBPACK_PORT=4001 | ||
|
|
||
| # Webpack entry filename | ||
| export CADET_WEBPACK_ENTRY=app |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The format for .env file is just
KEY1=value1
KEY2=value2
The comments are fine tho
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with 8d017f9.
|
|
||
| # Port which Webpack should be serving from in development | ||
| export CADET_WEBPACK_PORT=4001 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's better to rename this to .env.example and add instruction in README to rename this to .env and modify the content accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with 8d017f9, and README updated accordingly.
lib/cadet/accounts/accounts.ex
Outdated
| # Ivle.fetch_name/1 responds with :bad_request if token is invalid | ||
| {:error, :bad_request} | ||
|
|
||
| {:error, :internal_server_error} -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This clause is superfluous since {:error, _} -> will match this too. In elixir, case-style matching is done sequentially.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed with 2fbf69e. Meant to be explicit, but I'll use comments.
lib/cadet/accounts/ivle.ex
Outdated
|
|
||
| @api_url "https://ivle.nus.edu.sg/api/Lapi.svc" | ||
| @api_key System.get_env("IVLE_KEY") | ||
| @api_key Dotenv.load!().values["IVLE_KEY"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If your intention is just to obtain value from .env file, you should use Dotenv.load. This is because Dotenv.load! will export the value into system environment (this effect might or might not be desirable) which adds unnecessary overhead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed with 706363b.
| "body": "\"LEE NING YUAN\"", | ||
| "headers": { | ||
| "Cache-Control": "private", | ||
| "Content-Type": "application/json", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe move test/fixture to test/fixtures? Haha a bit confusing to have both directories
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad, typo in the config file. Done with 04d2a1a.
| {:ok, %{body: ~s(""), status_code: 200}} -> | ||
| # IVLE responsed 200 with body == ~s("") if token is invalid | ||
| {:error, :bad_request} | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should handle the case {:error, _}?
Or else the server thread would crash for no case clause error (or maybe this is the desired behaviour?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's the desired behavior. The rationale is to prevent 'silent' failures. Frankly, I'm not sure what the best practice is with elixir phoenix, or api-building, so I'll go with your judgement on this one. Let me know
lib/cadet/accounts/ivle.ex
Outdated
| @@ -0,0 +1,75 @@ | |||
| defmodule Cadet.Accounts.Ivle do | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Sorry I missed this during the first review) I think the module name should be Cadet.Accounts.IVLE https://github.com/lexmag/elixir-style-guide#camelcase-modules
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with 429ca34. Do you prefer IVLETest or IVLE_Test?
| response(400, "Missing parameter(s)") | ||
| response(403, "Wrong login attributes") | ||
| response(400, "Missing or invalid parameter") | ||
| response(500, "Internal server error") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough
As per the standard phoenix 1.3.0 structure. http://phoenixframework.org/blog/phoenix-1-3-0-released
|
@indocomsoft Second round of fixes done, do take a look, thanks! |
indocomsoft
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just need to add one test case
| end | ||
| else | ||
| send_resp(conn, :bad_request, "Missing parameters") | ||
| {:signin, {:error, reason}} -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This clause is missing test coverage hmm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with eaa77fc.
indocomsoft
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still not covered :/
I've analysed the cause -- let me know what you think
|
|
||
| {:ok, %{body: ~s(""), status_code: 200}} -> | ||
| # IVLE responsed 200 with body == ~s("") if token is invalid | ||
| {:error, :bad_request} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should handle the case {:error, _}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha, done with 0a9c589.
| use_cassette "auth_controller/v1/auth#1", custom: true do | ||
| conn = | ||
| post(conn, "/v1/auth", %{ | ||
| "login" => %{"ivle_token" => 'token'} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that in elixir 'charlist' and "binary" are 2 different ways of storing strings https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html
it should be "token" instead.
More to the point, it still doesn't cover: reading the report generated from running mix coveralls.html test/cadet_web/controllers/auth_controller_test.exs:77, the error is "caught" at {:fetch, {:ok, nusnet_id}} <- {:fetch, IVLE.fetch_nusnet_id(login.ivle_token)} since api_fetch/2 returns {:error, :bad_request} on %{body: ~s(""), status_code: 200}.
One way to solve this might be to handle {:error, _} in api_fetch/2, then purposely fail the request IVLE.fetch_name/1 if that makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad, I should've checked the coveralls link. Fixed with 59a315f using custom cassettes. The fix didn't need the catch "don't care" clause, but I'll leave that change in.
| {:ok, _} -> | ||
| # IVLE responds 200 with body == ~s("") if token is invalid | ||
| {:error, :bad_request} | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha what I meant was adding another clause here {:error, _} -> {:error, :internal_server_error} or something like that, unless the intention is to let the server thread just crash on connection failure (which I think would also return HTTP 500)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If they both return 500, I would rather let it crash. It'd be easier to catch unexpected bahaviour that way. Again, no experience with building APIs and web stuff here, so I'll default to your judgement. I've reverted it, but lmk what you'd prefer and I'll make the change.
This reverts commit 0a9c589.
indocomsoft
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
Since we're talking about Erlang/Elixir here, I guess just let it crash
|
Merged this for you -- maybe you could open issues in this PR's To-Do's |
* Move context files folders with the same name As per the standard phoenix 1.3.0 structure. http://phoenixframework.org/blog/phoenix-1-3-0-released * Change user fields to include NUSNET ID * Change /auth to take ivle_token (was email, pw) * Add HTTPoison dependency for requests to IVLE * Change email in authorizations to nusnet_id * Remove NUSNET ID from users * Remove password from registration, authorization * Remove comeonin tokens * Add IVLE API call to /auth * Register users automatically if first time login * Improve documentation, fix some credo --strict's * Squash migrations into a single file * Add tests for Ivle module via ExVCR * Add test for fetch_name in Cadet.Accounts.Ivle * Fine tune /auth error codes * Add tests for sign_in in Cadet.Accounts * Add tests for AuthController using ExVCR * Fix failing travis.ci tests * Remove password fields from registration test * Set IVLE_KEY in .env rather than environment var * Move ExVCR fixture to test/fixture * Fix Ivle module using env var instead of dot env * Reverse conditional for readability * Improve readability for Ivle's api_fetch/2 * Improve readability for AuthController's create/2 * Improve readability for Account's sign_in/2 * Improve tests with assert pattern match * Improve readability and docs for @token in tests * Fix credo offences due to ff89294 * Fix failing credo tests because of bad cassettes * Move .env file as .env.example, update README * Fix redundant pattern matching * Rename module Ivle -> IVLE * Move test/fixture -> test/fixtures * Change Dotenv.load! -> Dotenv.load * Add test for bad auth_controller create/2 sign_in * Catch all :errors for api_fetch * Fix missing coverage for signin error * Revert "Catch all :errors for api_fetch" This reverts commit 0a9c589.
Features
Cadet.Accounts.Form.LoginCadet.Accounts.User). Backend then responds with signedTokens./authwill implicitly register the NUSNET ID into the database if not already registered.Todos
I'm used to developing incrementally, especially for languages and frameworks that are quite new to me; so I'd prefer to open separate issues/PRs for these.
:student.Dev notes
IVLE_KEYvalue on compile time. This is reflected in the READMEs.(Postgrex.Error) ERROR 42703 (undefined_column): column "token" of relation "authorizations" does not existon testing. This is because an early migration makes use of ecto_enum atCadet.Accounts.Provider. Since this PR changes that Enum, and apparently modifying enums is tricky (?), you will need to manually reset your test databases (mix testonly migrates, not resets by default). Run the following:If you do not unset
MIX_ENV, you're gonna have a bad time (for that shell session at least).