(ns auto-ap.auth.oauth-test (:require [auto-ap.datomic :as datomic] [auto-ap.integration.util :refer [setup-test-data wrap-setup]] [auto-ap.routes.auth :as auth] [auto-ap.session-version :as session-version] [buddy.sign.jwt :as jwt] [clj-http.client :as http] [clj-time.core :as time] [clojure.test :refer [deftest is testing use-fixtures]] [config.core :refer [env]] [datomic.api :as dc])) (use-fixtures :each wrap-setup) ;; ============================================================================ ;; OAuth Behaviors (1.3 - 1.13) ;; ============================================================================ (deftest test-oauth-creates-new-user (testing "Behavior 1.5: It should create a new user account when the user logs in for the first time" (with-redefs [com.brunobonacci.mulog.core/log* (fn [& _] nil) http/post (fn [url & _] {:body {:access_token "fake-token" :token_type "Bearer"}}) http/get (fn [url & _] {:body {:id "google-123" :email "test@example.com" :name "Test User" :picture "http://example.com/pic.jpg"}})] (let [response (auth/oauth {:query-params {"code" "auth-code" "state" "/dashboard"} :headers {"host" "localhost:3000"}})] (is (= 301 (:status response))) ;; Verify user was created in database (let [user-id (ffirst (dc/q '[:find ?e :where [?e :user/provider "google"] [?e :user/provider-id "google-123"]] (dc/db datomic/conn)))] (is (some? user-id) "User should be created in database")))))) (deftest test-oauth-finds-existing-user (testing "Behavior 1.6: It should find the existing user account on subsequent logins" ;; Create user first @(dc/transact datomic/conn [{:db/id "existing-user" :user/provider "google" :user/provider-id "google-123" :user/email "test@example.com" :user/name "Existing User" :user/role :user-role/admin}]) (with-redefs [com.brunobonacci.mulog.core/log* (fn [& _] nil) http/post (fn [url & _] {:body {:access_token "fake-token" :token_type "Bearer"}}) http/get (fn [url & _] {:body {:id "google-123" :email "test@example.com" :name "Test User" :picture "http://example.com/pic.jpg"}})] (let [response (auth/oauth {:query-params {"code" "auth-code" "state" "/dashboard"} :headers {"host" "localhost:3000"}})] (is (= 301 (:status response))) ;; Verify only one user exists (let [user-count (count (dc/q '[:find ?e :where [?e :user/provider "google"] [?e :user/provider-id "google-123"]] (dc/db datomic/conn)))] (is (= 1 user-count) "Should not create duplicate user")))))) (deftest test-oauth-redirects-to-original-page (testing "Behavior 1.7: It should redirect to the original page after successful OAuth" (with-redefs [com.brunobonacci.mulog.core/log* (fn [& _] nil) http/post (fn [url & _] {:body {:access_token "fake-token" :token_type "Bearer"}}) http/get (fn [url & _] {:body {:id "google-123" :email "test@example.com" :name "Test User" :picture "http://example.com/pic.jpg"}})] (let [response (auth/oauth {:query-params {"code" "auth-code" "state" "/invoices"} :headers {"host" "localhost:3000"}})] (is (= 301 (:status response))) (let [location (get-in response [:headers "Location"])] (is (re-find #"^/invoices\?jwt=" location))))))) (deftest test-oauth-redirects-to-root-without-state (testing "Behavior 1.8: It should redirect to the root page when no return URL is provided" (with-redefs [com.brunobonacci.mulog.core/log* (fn [& _] nil) http/post (fn [url & _] {:body {:access_token "fake-token" :token_type "Bearer"}}) http/get (fn [url & _] {:body {:id "google-123" :email "test@example.com" :name "Test User" :picture "http://example.com/pic.jpg"}})] (let [response (auth/oauth {:query-params {"code" "auth-code"} :headers {"host" "localhost:3000"}})] (is (= 301 (:status response))) (let [location (get-in response [:headers "Location"])] (is (re-find #"^/\?jwt=" location))))))) (deftest test-oauth-establishes-session (testing "Behavior 1.9: It should establish a server-side session with user identity and version" (with-redefs [com.brunobonacci.mulog.core/log* (fn [& _] nil) http/post (fn [url & _] {:body {:access_token "fake-token" :token_type "Bearer"}}) http/get (fn [url & _] {:body {:id "google-123" :email "test@example.com" :name "Test User" :picture "http://example.com/pic.jpg"}})] (let [response (auth/oauth {:query-params {"code" "auth-code"} :headers {"host" "localhost:3000"}})] (let [session (:session response)] (is (some? (:identity session))) (is (= session-version/current-session-version (:version session)))))))) (deftest test-oauth-passes-jwt-in-query-string (testing "Behavior 1.10: It should pass the JWT token in the query string after successful OAuth" (with-redefs [com.brunobonacci.mulog.core/log* (fn [& _] nil) http/post (fn [url & _] {:body {:access_token "fake-token" :token_type "Bearer"}}) http/get (fn [url & _] {:body {:id "google-123" :email "test@example.com" :name "Test User" :picture "http://example.com/pic.jpg"}})] (let [response (auth/oauth {:query-params {"code" "auth-code"} :headers {"host" "localhost:3000"}})] (let [location (get-in response [:headers "Location"])] (is (re-find #"jwt=" location)) ;; Extract and verify the JWT (let [jwt-str (second (re-find #"jwt=([^\&]+)" location)) claims (jwt/unsign jwt-str (:jwt-secret env) {:alg :hs512})] (is (= "Test User" (:user claims))))))))) (deftest test-oauth-handles-no-email (testing "Behavior 1.12: It should handle users without email via Google provider ID" (with-redefs [com.brunobonacci.mulog.core/log* (fn [& _] nil) http/post (fn [url & _] {:body {:access_token "fake-token"}}) http/get (fn [url & _] {:body {:id "google-no-email" :name "No Email User"}})] (let [response (auth/oauth {:query-params {"code" "auth-code"} :headers {"host" "localhost:3000"}})] (is (= 301 (:status response))))))) (deftest test-oauth-missing-code (testing "Behavior 1.13: It should return 401 with error message when the OAuth code is missing" (with-redefs [com.brunobonacci.mulog.core/log* (fn [& _] nil)] (let [response (auth/oauth {:query-params {} :headers {"host" "localhost:3000"}})] (is (= 401 (:status response))) (is (re-find #"Couldn\'t authenticate" (:body response)))))))