main
rb 72 lines 2.94 KB
Raw
1 # frozen_string_literal: true
2
3 require "rails_helper"
4
5 # The GitHub OAuth connect flow for imports: the callback must verify state,
6 # exchange the code, store the token encrypted, and never leak it to the client.
7 RSpec.describe "GitHub import connection", type: :request do
8 let(:user) { User.create!(smbcloud_id: 99, email: "conn@example.com", username: "connuser") }
9
10 before do
11 # Sign the user in (session-based auth) without going through smbCloud.
12 allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user)
13 allow(GithubOauthService).to receive(:configured?).and_return(true)
14 allow(GithubOauthService).to receive(:generate_state).and_return("state-xyz")
15 allow(GithubOauthService).to receive(:authorize_url).and_return("https://github.com/login/oauth/authorize?x=1")
16 end
17
18 # Establishes the CSRF state in the session, the way the real flow does.
19 def start_connect
20 get github_import_connect_path
21 expect(response).to redirect_to("https://github.com/login/oauth/authorize?x=1")
22 end
23
24 it "exchanges the code and stores the token encrypted on callback" do
25 start_connect
26 allow(GithubOauthService).to receive(:exchange_code)
27 .and_return(access_token: "gho_thetoken", scope: "repo", token_type: "bearer")
28 fake_client = instance_double(GithubApiClient, login: "octocat", list_repositories: [])
29 allow(GithubApiClient).to receive(:new).with("gho_thetoken").and_return(fake_client)
30
31 get github_import_callback_path, params: { code: "abc", state: "state-xyz" }
32
33 expect(response).to redirect_to(new_import_path)
34 conn = user.reload.github_connection
35 expect(conn).to be_present
36 expect(conn.access_token).to eq("gho_thetoken")
37 expect(conn.github_login).to eq("octocat")
38
39 # Ciphertext at rest, not the plaintext token.
40 raw = ActiveRecord::Base.connection.select_value(
41 "SELECT access_token FROM github_connections WHERE id = #{conn.id}"
42 )
43 expect(raw).not_to include("gho_thetoken")
44
45 # The token is never echoed back to the client.
46 expect(response.body).not_to include("gho_thetoken")
47 follow_redirect!
48 expect(response.body).not_to include("gho_thetoken")
49 end
50
51 it "rejects a callback whose state doesn't match (CSRF)" do
52 start_connect
53 expect(GithubOauthService).not_to receive(:exchange_code)
54
55 get github_import_callback_path, params: { code: "abc", state: "wrong-state" }
56
57 expect(response).to redirect_to(new_import_path)
58 expect(user.reload.github_connection).to be_nil
59 end
60
61 it "handles a user cancelling the GitHub authorization" do
62 get github_import_callback_path, params: { error: "access_denied" }
63 expect(response).to redirect_to(new_import_path)
64 expect(user.reload.github_connection).to be_nil
65 end
66
67 it "disconnects and removes the stored token" do
68 user.create_github_connection!(access_token: "gho_x", scope: "repo")
69 delete github_import_disconnect_path
70 expect(user.reload.github_connection).to be_nil
71 end
72 end