diff --git a/Gemfile b/Gemfile index 5024a7d0..d3e33ea2 100644 --- a/Gemfile +++ b/Gemfile @@ -90,6 +90,6 @@ group :test do gem "capybara" gem "selenium-webdriver" gem "rspec_junit_formatter" - gem "simplecov" + gem 'simplecov', '~> 0.17.0', require: false gem "rails-controller-testing" end diff --git a/Gemfile.lock b/Gemfile.lock index 969dceae..b7752961 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -84,7 +84,7 @@ GEM base64 (0.2.0) bigdecimal (3.1.8) bindex (0.8.1) - bootsnap (1.18.3) + bootsnap (1.18.4) msgpack (~> 1.2) builder (3.3.0) capybara (3.40.0) @@ -96,8 +96,8 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - codeclimate-test-reporter (1.0.9) - simplecov (<= 0.13) + codeclimate-test-reporter (1.0.7) + simplecov concurrent-ruby (1.3.3) connection_pool (2.4.1) crack (1.0.0) @@ -111,18 +111,18 @@ GEM irb (~> 1.10) reline (>= 0.3.8) diff-lcs (1.5.1) - docile (1.1.5) + docile (1.4.1) drb (2.2.1) erubi (1.13.0) - faraday (2.10.0) + faraday (2.10.1) faraday-net_http (>= 2.0, < 3.2) logger - faraday-net_http (3.1.0) + faraday-net_http (3.1.1) net-http foreman (0.88.1) globalid (1.2.1) activesupport (>= 6.1) - hashdiff (1.1.0) + hashdiff (1.1.1) i18n (1.14.5) concurrent-ruby (~> 1.0) io-console (0.7.2) @@ -165,19 +165,23 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.3) - nokogiri (1.16.6-aarch64-linux) + nokogiri (1.16.7-aarch64-linux) racc (~> 1.4) - nokogiri (1.16.6-arm64-darwin) + nokogiri (1.16.7-arm-linux) racc (~> 1.4) - nokogiri (1.16.6-x86_64-darwin) + nokogiri (1.16.7-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.6-x86_64-linux) + nokogiri (1.16.7-x86-linux) + racc (~> 1.4) + nokogiri (1.16.7-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) parallel (1.25.1) parser (3.3.4.0) ast (~> 2.4.1) racc - pg (1.5.6) + pg (1.5.7) prism (0.30.0) propshaft (0.9.0) actionpack (>= 7.0.0) @@ -186,7 +190,7 @@ GEM railties (>= 7.0.0) psych (5.1.2) stringio - public_suffix (6.0.0) + public_suffix (6.0.1) puma (6.4.2) nio4r (~> 2.0) racc (1.8.1) @@ -272,7 +276,7 @@ GEM rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.3) + rubocop-ast (1.32.0) parser (>= 3.3.1.0) rubocop-performance (1.21.1) rubocop (>= 1.48.1, < 2.0) @@ -284,9 +288,9 @@ GEM rubocop-ast (>= 1.31.1, < 2.0) rubocop-rake (0.6.0) rubocop (~> 1.0) - rubocop-rspec (3.0.3) + rubocop-rspec (3.0.4) rubocop (~> 1.61) - ruby-lsp (0.17.9) + ruby-lsp (0.17.11) language_server-protocol (~> 3.17.0) prism (>= 0.29.0, < 0.31) rbs (>= 3, < 4) @@ -299,12 +303,12 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - simplecov (0.13.0) - docile (~> 1.1.0) + simplecov (0.17.1) + docile (~> 1.1) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) - sorbet-runtime (0.5.11492) + sorbet-runtime (0.5.11511) stimulus-rails (1.3.3) railties (>= 6.0.0) stringio (3.1.1) @@ -335,14 +339,14 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.16) + zeitwerk (2.6.17) PLATFORMS aarch64-linux - arm64-darwin-21 - arm64-darwin-22 - arm64-darwin-23 - x86_64-darwin-22 + arm-linux + arm64-darwin + x86-linux + x86_64-darwin x86_64-linux DEPENDENCIES @@ -371,7 +375,7 @@ DEPENDENCIES rubocop-rspec ruby-lsp selenium-webdriver - simplecov + simplecov (~> 0.17.0) stimulus-rails turbo-rails tzinfo-data @@ -382,4 +386,4 @@ RUBY VERSION ruby 3.2.4p170 BUNDLED WITH - 2.4.6 + 2.5.9 diff --git a/Rakefile b/Rakefile index 84b021d0..f925e9ee 100644 --- a/Rakefile +++ b/Rakefile @@ -9,8 +9,12 @@ Rails.application.load_tasks namespace :cf do desc "Only run on the first application instance" - task :on_first_instance do - instance_index = JSON.parse(ENV["VCAP_APPLICATION"])["instance_index"] rescue nil - exit(0) unless instance_index == 0 + task on_first_instance: :environment do + instance_index = begin + JSON.parse(ENV.fetch("VCAP_APPLICATION", nil))["instance_index"] + rescue + nil + end + exit(0) unless instance_index.zero? end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7944f9f9..34c3a93f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,34 @@ # frozen_string_literal: true class ApplicationController < ActionController::Base + helper_method :current_user, :logged_in? + + def current_user + return unless session[:userinfo] + + user_token = session["userinfo"][0]["sub"] + @current_user ||= User.find_by(token: user_token) if user_token + end + + def logged_in? + !!current_user + end + + def sign_in(login_userinfo) + user = User.user_from_userinfo(login_userinfo) + + @current_user = user + session[:userinfo] = login_userinfo + end + + def sign_out + @current_user = nil + session.delete(:userinfo) + end + + def redirect_if_logged_in(path = "/dashboard") + return unless logged_in? + + redirect_to path, notice: I18n.t("already_logged_in_notice") + end end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb new file mode 100644 index 00000000..6049155d --- /dev/null +++ b/app/controllers/dashboard_controller.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class DashboardController < ApplicationController + def index; end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 8795a792..e64fade8 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -12,17 +12,16 @@ def create redirect_to(login_gov.authorization_url, allow_other_host: true) end - def delete + def destroy login_gov = LoginGov.new - # TODO: update user session status, clear out JWT # TODO: add session duration to the security log - # TODO: delete session locally and Phoenix - redirect_to(login_gov.logout_url) + sign_out + redirect_to(login_gov.logout_url, allow_other_host: true) end def result - # TODO: store the user_info in the session - # session[:user_info] = @login_userinfo + sign_in(@login_userinfo) + redirect_to dashboard_path end private diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 08dc5379..4e9fa68d 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -2,4 +2,14 @@ class ApplicationRecord < ActiveRecord::Base primary_abstract_class + + attribute :inserted_at, :datetime, precision: 6 + attribute :updated_at, :datetime, precision: 6 + + # created_at timestamp is currently overridden to inserted_at due to shared Phoenix database + def self.timestamp_attributes_for_create + # only strings allowed here, symbols won't work, see below commit for more details + # https://github.com/rails/rails/commit/2b5dacb43dd92e98e1fd240a80c2a540ed380257 + super << 'inserted_at' + end end diff --git a/app/models/user.rb b/app/models/user.rb index 33049ec8..eccb03a5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -80,7 +80,55 @@ class User < ApplicationRecord attribute :renewal_request, :string - attribute :updated_at, :datetime, precision: 6 - validates :email, presence: true + + # Finds, creates, or updates user from userinfo + # Find in case of user with existing token matching userinfo["sub"] + # Create in case of no token or email matching in userinfo + # Update in case of matching email to userinfo["email"] but no token set + # TODO: Add relevant security log tracking here? + def self.user_from_userinfo(userinfo) + email = userinfo[0]["email"] + token = userinfo[0]["sub"] + + if (user = find_by(token:)) + user + elsif (user = find_by(email:)) + update_admin_added_user(user, userinfo) + else + create_user_from_userinfo(userinfo) + end + end + + def self.update_admin_added_user(user, userinfo) + update(user.id, { token: userinfo[0]["sub"] }) + end + + def self.create_user_from_userinfo(userinfo) + email = userinfo[0]["email"] + token = userinfo[0]["sub"] + + default_role, default_status = default_role_and_status_for_email(email) + + create({ + email:, + role: default_role, + token:, + terms_of_use: nil, + privacy_guidelines: nil, + status: default_status + }) + end + + def self.default_role_and_status_for_email(email) + if default_challenge_manager?(email) + %w[challenge_manager pending] + else + %w[solver active] + end + end + + def self.default_challenge_manager?(email) + /\.(gov|mil)$/.match?(email) + end end diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb new file mode 100644 index 00000000..576f6fed --- /dev/null +++ b/app/views/dashboard/index.html.erb @@ -0,0 +1,9 @@ +
+
+ <% if logged_in? %> + Logged In Dashboard Index + <% else %> + Logged Out Dashboard Index + <% end %> +
+
\ No newline at end of file diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb new file mode 100644 index 00000000..fdc90313 --- /dev/null +++ b/app/views/layouts/_header.html.erb @@ -0,0 +1,18 @@ +
+
+
+ +
+ +
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index e00cfa64..a9dfbf59 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -14,6 +14,7 @@ + <%= render "layouts/header" %> <%= yield %> diff --git a/config/environments/development.rb b/config/environments/development.rb index a10ac8b1..cd4e78a2 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -75,4 +75,4 @@ # Raise error when a before_action's only/except options reference missing actions config.action_controller.raise_on_missing_callback_actions = true -end +end \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index e0a9572e..86bbf980 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -31,3 +31,4 @@ en: hello: "Hello world" please_try_again: "Please try again." login_error: "There was an issue with logging in. Please try again." + already_logged_in_notice: "You are already logged in." diff --git a/config/routes.rb b/config/routes.rb index 12c114e8..7a1763af 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,9 @@ get 'auth/result', to: 'sessions#result' resource 'session', only: [:new, :create, :destroy] + get '/', to: "dashboard#index" + get '/dashboard', to: "dashboard#index" + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check diff --git a/db/structure.sql b/db/structure.sql index 379e3719..65453f93 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -24,29 +24,6 @@ CREATE TYPE public.oban_job_state AS ENUM ( ); --- --- Name: oban_jobs_notify(); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.oban_jobs_notify() RETURNS trigger - LANGUAGE plpgsql - AS $$ -DECLARE - channel text; - notice json; -BEGIN - IF NEW.state = 'available' THEN - channel = 'public.oban_insert'; - notice = json_build_object('queue', NEW.queue); - - PERFORM pg_notify(channel, notice::text); - END IF; - - RETURN NULL; -END; -$$; - - SET default_tablespace = ''; SET default_table_access_method = heap; @@ -121,18 +98,6 @@ CREATE SEQUENCE public.agency_members_id_seq ALTER SEQUENCE public.agency_members_id_seq OWNED BY public.agency_members.id; --- --- Name: ar_internal_metadata; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.ar_internal_metadata ( - key character varying NOT NULL, - value character varying, - created_at timestamp(6) without time zone NOT NULL, - updated_at timestamp(6) without time zone NOT NULL -); - - -- -- Name: certification_log; Type: TABLE; Schema: public; Owner: - -- @@ -175,6 +140,37 @@ CREATE SEQUENCE public.certification_log_id_seq ALTER SEQUENCE public.certification_log_id_seq OWNED BY public.certification_log.id; +-- +-- Name: challenge_managers; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.challenge_managers ( + id bigint NOT NULL, + challenge_id bigint, + user_id bigint, + revoked_at timestamp(0) without time zone +); + + +-- +-- Name: challenge_owners_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.challenge_owners_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: challenge_owners_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.challenge_owners_id_seq OWNED BY public.challenge_managers.id; + + -- -- Name: challenges; Type: TABLE; Schema: public; Owner: - -- @@ -505,7 +501,6 @@ CREATE TABLE public.oban_jobs ( cancelled_at timestamp without time zone, CONSTRAINT attempt_range CHECK (((attempt >= 0) AND (attempt <= max_attempts))), CONSTRAINT positive_max_attempts CHECK ((max_attempts > 0)), - CONSTRAINT priority_range CHECK (((priority >= 0) AND (priority <= 3))), CONSTRAINT queue_length CHECK (((char_length(queue) > 0) AND (char_length(queue) < 128))), CONSTRAINT worker_length CHECK (((char_length(worker) > 0) AND (char_length(worker) < 128))) ); @@ -515,7 +510,7 @@ CREATE TABLE public.oban_jobs ( -- Name: TABLE oban_jobs; Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON TABLE public.oban_jobs IS '11'; +COMMENT ON TABLE public.oban_jobs IS '12'; -- @@ -1072,6 +1067,13 @@ ALTER TABLE ONLY public.agency_members ALTER COLUMN id SET DEFAULT nextval('publ ALTER TABLE ONLY public.certification_log ALTER COLUMN id SET DEFAULT nextval('public.certification_log_id_seq'::regclass); +-- +-- Name: challenge_managers id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenge_managers ALTER COLUMN id SET DEFAULT nextval('public.challenge_owners_id_seq'::regclass); + + -- -- Name: challenges id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1236,19 +1238,19 @@ ALTER TABLE ONLY public.agency_members -- --- Name: ar_internal_metadata ar_internal_metadata_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: certification_log certification_log_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.ar_internal_metadata - ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key); +ALTER TABLE ONLY public.certification_log + ADD CONSTRAINT certification_log_pkey PRIMARY KEY (id); -- --- Name: certification_log certification_log_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: challenge_managers challenge_owners_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.certification_log - ADD CONSTRAINT certification_log_pkey PRIMARY KEY (id); +ALTER TABLE ONLY public.challenge_managers + ADD CONSTRAINT challenge_owners_pkey PRIMARY KEY (id); -- @@ -1307,6 +1309,14 @@ ALTER TABLE ONLY public.non_federal_partners ADD CONSTRAINT non_federal_partners_pkey PRIMARY KEY (id); +-- +-- Name: oban_jobs non_negative_priority; Type: CHECK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE public.oban_jobs + ADD CONSTRAINT non_negative_priority CHECK ((priority >= 0)) NOT VALID; + + -- -- Name: oban_jobs oban_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1506,10 +1516,75 @@ CREATE UNIQUE INDEX winners_phase_id_index ON public.phase_winners USING btree ( -- --- Name: oban_jobs oban_notify; Type: TRIGGER; Schema: public; Owner: - +-- Name: agencies agencies_parent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- -CREATE TRIGGER oban_notify AFTER INSERT ON public.oban_jobs FOR EACH ROW EXECUTE FUNCTION public.oban_jobs_notify(); +ALTER TABLE ONLY public.agencies + ADD CONSTRAINT agencies_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES public.agencies(id); + + +-- +-- Name: agency_members agency_members_agency_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.agency_members + ADD CONSTRAINT agency_members_agency_id_fkey FOREIGN KEY (agency_id) REFERENCES public.agencies(id); + + +-- +-- Name: agency_members agency_members_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.agency_members + ADD CONSTRAINT agency_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id); + + +-- +-- Name: certification_log certification_log_approver_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.certification_log + ADD CONSTRAINT certification_log_approver_id_fkey FOREIGN KEY (approver_id) REFERENCES public.users(id); + + +-- +-- Name: certification_log certification_log_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.certification_log + ADD CONSTRAINT certification_log_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id); + + +-- +-- Name: challenge_managers challenge_owners_challenge_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenge_managers + ADD CONSTRAINT challenge_owners_challenge_id_fkey FOREIGN KEY (challenge_id) REFERENCES public.challenges(id); + + +-- +-- Name: challenge_managers challenge_owners_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenge_managers + ADD CONSTRAINT challenge_owners_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id); + + +-- +-- Name: challenges challenges_agency_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenges + ADD CONSTRAINT challenges_agency_id_fkey FOREIGN KEY (agency_id) REFERENCES public.agencies(id); + + +-- +-- Name: challenges challenges_sub_agency_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenges + ADD CONSTRAINT challenges_sub_agency_id_fkey FOREIGN KEY (sub_agency_id) REFERENCES public.agencies(id); -- @@ -1520,6 +1595,14 @@ ALTER TABLE ONLY public.challenges ADD CONSTRAINT challenges_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id); +-- +-- Name: federal_partners federal_partners_agency_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.federal_partners + ADD CONSTRAINT federal_partners_agency_id_fkey FOREIGN KEY (agency_id) REFERENCES public.agencies(id); + + -- -- Name: federal_partners federal_partners_challenge_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1528,6 +1611,14 @@ ALTER TABLE ONLY public.federal_partners ADD CONSTRAINT federal_partners_challenge_id_fkey FOREIGN KEY (challenge_id) REFERENCES public.challenges(id); +-- +-- Name: federal_partners federal_partners_sub_agency_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.federal_partners + ADD CONSTRAINT federal_partners_sub_agency_id_fkey FOREIGN KEY (sub_agency_id) REFERENCES public.agencies(id); + + -- -- Name: message_context_statuses message_context_statuses_message_context_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1704,6 +1795,14 @@ ALTER TABLE ONLY public.timeline_events ADD CONSTRAINT timeline_events_challenge_id_fkey FOREIGN KEY (challenge_id) REFERENCES public.challenges(id); +-- +-- Name: users users_agency_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_agency_id_fkey FOREIGN KEY (agency_id) REFERENCES public.agencies(id); + + -- -- Name: phase_winners winners_phase_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f2694166..efe56047 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -35,6 +35,27 @@ require 'rails_helper' RSpec.describe User do + let(:gov_userinfo) do + [{ + "email" => "test@example.gov", + "sub" => SecureRandom.uuid + }] + end + + let(:mil_userinfo) do + [{ + "email" => "test@example.mil", + "sub" => SecureRandom.uuid + }] + end + + let(:non_gov_userinfo) do + [{ + "email" => "test@example.com", + "sub" => SecureRandom.uuid + }] + end + describe 'validations' do it_behaves_like 'a model with required attributes', [:email] end @@ -45,4 +66,93 @@ expect(user.active_session).to be_falsey end end + + describe 'timestamps' do + it 'properly sets inserted_at and updated_at' do + email = gov_userinfo[0]["email"] + token = gov_userinfo[0]["sub"] + + user = described_class.create!(email:, token:) + + expect(user.inserted_at).not_to be_nil + expect(user.updated_at).not_to be_nil + + expect(user.inserted_at).to be_within(1.second).of(Time.current) + expect(user.updated_at).to be_within(1.second).of(Time.current) + + original_inserted_at = user.inserted_at + original_updated_at = user.updated_at + + travel_to 1.hour.from_now do + user.update!(email: 'new-email@example.com') + + expect(user.inserted_at).to eq(original_inserted_at) + expect(user.updated_at).to be > original_updated_at + expect(user.updated_at).to be_within(1.second).of(Time.current) + end + end + end + + describe 'user_from_userinfo' do + it 'finds user if one matches token' do + email = gov_userinfo[0]["email"] + token = gov_userinfo[0]["sub"] + + user = described_class.create!(email:, token:) + + found_user = described_class.user_from_userinfo(gov_userinfo) + + expect(user).to eq(found_user) + end + + it 'creates pending challenge_manager user if no matching token or email and .gov email' do + email = gov_userinfo[0]["email"] + token = gov_userinfo[0]["sub"] + + created_user = described_class.user_from_userinfo(gov_userinfo) + + expect(created_user.email).to eq(email) + expect(created_user.token).to eq(token) + expect(created_user.role).to eq("challenge_manager") + expect(created_user.status).to eq("pending") + end + + it 'creates pending challenge_manager user if no matching token or email and .mil email' do + email = mil_userinfo[0]["email"] + token = mil_userinfo[0]["sub"] + + created_user = described_class.user_from_userinfo(mil_userinfo) + + expect(created_user.email).to eq(email) + expect(created_user.token).to eq(token) + expect(created_user.role).to eq("challenge_manager") + expect(created_user.status).to eq("pending") + end + + it 'creates active solver user if no matching token or email and non .gov email' do + email = non_gov_userinfo[0]["email"] + token = non_gov_userinfo[0]["sub"] + + created_user = described_class.user_from_userinfo(non_gov_userinfo) + + expect(created_user.email).to eq(email) + expect(created_user.token).to eq(token) + expect(created_user.role).to eq("solver") + expect(created_user.status).to eq("active") + end + + it 'update user with token if matching email but no token set (from admin creation)' do + email = gov_userinfo[0]["email"] + token = gov_userinfo[0]["sub"] + + user = described_class.create!(email:) + expect(user.token).to be_nil + + updated_user = described_class.user_from_userinfo(gov_userinfo) + + expect(updated_user.id).to eq(user.id) + expect(updated_user.email).to eq(email) + expect(updated_user.token).to eq(token) + end + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index fcdf974e..00dadbc6 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -66,4 +66,6 @@ config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + + config.include ActiveSupport::Testing::TimeHelpers end diff --git a/spec/requests/sessions_request_spec.rb b/spec/requests/sessions_request_spec.rb index 0e752824..8466e041 100644 --- a/spec/requests/sessions_request_spec.rb +++ b/spec/requests/sessions_request_spec.rb @@ -13,9 +13,8 @@ end it "delete session logs the user out" do - skip "not implemented" - delete "/session/:id" - assert_response :success + delete "/session" + assert_response :redirect end it "get /auth/result without params redirects to login" do @@ -34,9 +33,11 @@ code = "ABC123" login_gov = instance_double(LoginGov) allow(LoginGov).to receive(:new).and_return(login_gov) - allow(login_gov).to receive(:exchange_token_from_auth_result).with(code).and_return({ email: "test@example.com" }) + allow(login_gov).to receive(:exchange_token_from_auth_result).with(code).and_return( + [{ email: "test@example.com", sub: "sub" }] + ) get "/auth/result", params: { code: } - expect(response).to have_http_status(:ok) - expect(response).to render_template(:result) + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to("/dashboard") end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f528642d..9a0bfd4a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,6 @@ require 'simplecov' require 'webmock/rspec' +require 'securerandom' SimpleCov.command_name 'RSpec' diff --git a/yarn.lock b/yarn.lock index 75fed022..cb0ba9d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -173,11 +173,11 @@ integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg== "@types/node@*": - version "20.14.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.9.tgz#12e8e765ab27f8c421a1820c99f5f313a933b420" - integrity sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg== + version "22.1.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.1.0.tgz#6d6adc648b5e03f0e83c78dc788c2b037d0ad94b" + integrity sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw== dependencies: - undici-types "~5.26.4" + undici-types "~6.13.0" "@types/vinyl@^2.0.4": version "2.0.12" @@ -494,14 +494,14 @@ braces@^3.0.3, braces@~3.0.2: fill-range "^7.1.1" browserslist@^4.21.10: - version "4.23.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.1.tgz#ce4af0534b3d37db5c1a4ca98b9080f985041e96" - integrity sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw== + version "4.23.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" + integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== dependencies: - caniuse-lite "^1.0.30001629" - electron-to-chromium "^1.4.796" - node-releases "^2.0.14" - update-browserslist-db "^1.0.16" + caniuse-lite "^1.0.30001646" + electron-to-chromium "^1.5.4" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" buffer-builder@^0.2.0: version "0.2.0" @@ -549,10 +549,10 @@ camelcase@^3.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" integrity sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg== -caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001629: - version "1.0.30001637" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001637.tgz#d9fab531493d9ef46a8ff305e9812190ac463f21" - integrity sha512-1x0qRI1mD1o9e+7mBI7XtzFAP4XszbHaVWsMiGbSPLYekKTJF7K+FNk6AsXH4sUpc+qrsI3pVgf1Jdl/uGkuSQ== +caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001646: + version "1.0.30001646" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001646.tgz#d472f2882259ba032dd73ee069ff01bfd059b25d" + integrity sha512-dRg00gudiBDDTmUhClSdv3hqRfpbOnU28IpI1T6PBTLWa+kOj0681C8uML3PifYfREuBrVjDGhL3adYpBT6spw== cheerio-select@^2.1.0: version "2.1.0" @@ -948,10 +948,10 @@ each-props@^1.3.2: is-plain-object "^2.0.1" object.defaults "^1.1.0" -electron-to-chromium@^1.4.796: - version "1.4.812" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.812.tgz#21b78709c5a13af5d5c688d135a22dcea7617acf" - integrity sha512-7L8fC2Ey/b6SePDFKR2zHAy4mbdp1/38Yk5TsARO66W3hC5KEaeKMMHoxwtuH+jcu2AYLSn9QX04i95t6Fl1Hg== +electron-to-chromium@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz#cd477c830dd6fca41fbd5465c1ff6ce08ac22343" + integrity sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA== element-closest@^2.0.1: version "2.0.2" @@ -1629,9 +1629,9 @@ ignore@^5.2.0: integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== immutable@^4.0.0: - version "4.3.6" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447" - integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ== + version "4.3.7" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" + integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== indent-string@^4.0.0: version "4.0.0" @@ -1706,9 +1706,9 @@ is-buffer@^1.1.5: integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== is-core-module@^2.13.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.14.0.tgz#43b8ef9f46a6a08888db67b1ffd4ec9e3dfd59d1" - integrity sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A== + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" + integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== dependencies: hasown "^2.0.2" @@ -2138,10 +2138,10 @@ next-tick@^1.1.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== -node-releases@^2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" - integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== normalize-package-data@^2.3.2: version "2.5.0" @@ -3177,10 +3177,10 @@ undertaker@^1.2.1: object.reduce "^1.0.0" undertaker-registry "^1.0.0" -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.13.0.tgz#e3e79220ab8c81ed1496b5812471afd7cf075ea5" + integrity sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg== union-value@^1.0.0: version "1.0.1" @@ -3213,10 +3213,10 @@ upath@^1.1.1: resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== -update-browserslist-db@^1.0.16: - version "1.0.16" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz#f6d489ed90fb2f07d67784eb3f53d7891f736356" - integrity sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ== +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== dependencies: escalade "^3.1.2" picocolors "^1.0.1"