diff --git a/examples/form-validation/package.json b/examples/form-validation/package.json index 289e592..7dea089 100644 --- a/examples/form-validation/package.json +++ b/examples/form-validation/package.json @@ -2,7 +2,7 @@ "dependencies": { "filewatcher": "^3.0.1", "minimal-stylesheet": "^0.1.0", - "nbb": "0.0.97", + "nbb": "^1.2.180", "node-input-validator": "^4.4.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/examples/form-validation/webserver.cljs b/examples/form-validation/webserver.cljs index 779633e..79e897c 100644 --- a/examples/form-validation/webserver.cljs +++ b/examples/form-validation/webserver.cljs @@ -72,7 +72,7 @@ (email-form data)) (.send res rendered-html))) -(defn handle-csrf-error [err req res n] +(defn handle-csrf-error [err _req res n] (if (= (aget err "code") "EBADCSRFTOKEN") (-> res (.status 403) diff --git a/package.json b/package.json index d96ab64..058ba53 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "docs": "nbb bin/generate-docs.cljs", "pre-publish": "npm run sync-deps; nbb bin/update-readme-versions.cljs; npm run docs; echo Changes:; git log --oneline `git rev-list --tags --max-count=1`..; echo; echo 'Now commit changes and run `git tag vX.Y.Z`.'", "test": "rm -f ./tests.sqlite; SECRET=testing TESTING=1 DATABASE_URL=sqlite://./tests.sqlite npx shadow-cljs compile test", - "test-e2e": "nbb --classpath src src/sitefoxtest/e2etests.cljs", + "test-e2e": "NODE_OPTIONS='--experimental-fetch --no-warnings' nbb --classpath src src/sitefoxtest/e2etests.cljs", "watch": "SECRET=watching TESTING=1 shadow-cljs watch test" } } diff --git a/src/sitefoxtest/e2etests.cljs b/src/sitefoxtest/e2etests.cljs index eeffcb2..8ceefdf 100644 --- a/src/sitefoxtest/e2etests.cljs +++ b/src/sitefoxtest/e2etests.cljs @@ -13,8 +13,9 @@ ;(def browser-type pw/chromium) -(def host "127.0.0.1") +(def host "localhost") (def base-url (str "http://" host ":8000")) +(def browser-timeout 60000) (def log (j/call-in js/console [:log :bind] js/console " ---> ")) (def log-listeners (atom #{})) @@ -22,11 +23,11 @@ (j/assoc! env "BIND_ADDRESS" host) (defn run-server [path server-command port] - ; first run npm init in the folder - (log "Installing server deps.") - (spawnSync "npm i --no-save" #js {:cwd path - :stdio "inherit" - :shell true}) + ; delete any old database hanging around + (spawnSync "rm -f database.sqlite" + #js {:cwd path + :stdio "inherit" + :shell true}) ; now run the server (log "Spawning server.") (p/let [server (spawn server-command #js {:cwd path @@ -35,10 +36,13 @@ :detach true}) port-info (wait-for-port #js {:host host :port port}) pid (j/get server :pid)] + (log "Setting up stdout listener.") (j/call-in server [:stdout :on] "data" (fn [data] + ;(print data) (doseq [[re-string listener-fn] @log-listeners] - (let [matches (.match (.toString data) (js/RegExp. re-string "s"))] + (let [matches (.match (.toString data) + (js/RegExp. re-string "s"))] (when matches (listener-fn matches) (swap! log-listeners disj [re-string listener-fn])))) @@ -56,18 +60,20 @@ (defn get-browser [] (p/let [browser (.launch pw/chromium #js {:headless (not (nil? (j/get env "CI"))) - :timeout 3000}) + :timeout browser-timeout}) context (.newContext browser) page (.newPage context)] (.setDefaultTimeout page 3000) {:browser browser :context context :page page})) (defn catch-fail [err done server & [browser]] + (log "Caught test error.") (when err - (.error js/console (j/get err :stack))) + (.error js/console err)) (is (nil? err) (str "Error in test: " (.toString err))) - (j/call server :kill) + (when (and server (j/get server :kill)) + (j/call server :kill)) (when browser (.close browser)) (done)) @@ -76,14 +82,21 @@ (t/testing "Basic test of Sitefox on nbb." (async done (p/let [_ (log "Test: basic-site-test") - server (run-server "examples/nbb" "npm i --no-save; npm run serve" 8000)] + server (run-server "examples/nbb" + "npm i --no-save; npm run serve" + 8000)] (p/catch (p/let [res (js/fetch base-url) text (.text res)] - (is (j/get-in server [:process :pid]) "Server is running?") - (is (j/get server :open) "Server port is open?") - (is (j/get res :ok) "Was server response ok?") - (is (includes? text "Hello") "Server response includes 'Hello' text?") + (log "Starting test checks.") + (is (j/get-in server [:process :pid]) + "Server is running?") + (is (j/get server :open) + "Server port is open?") + (is (j/get res :ok) + "Was server response ok?") + (is (includes? text "Hello") + "Server response includes 'Hello' text?") (log "Test done. Killing server.") (j/call server :kill) (log "After server.") @@ -95,11 +108,55 @@ content (.content page)] (is (includes? content text) message))) +(defn check-for-no-text [page text selector message] + (p/let [_ (-> page (.waitForSelector selector)) + content (.content page)] + (is (not (includes? content text)) message))) + +(defn check-failed-sign-in + [page password] + (p/do! + ; sign in again + (.goto page base-url) + ; click "Sign in" + (-> page (.locator "a[href='/auth/sign-in']") .click) + + ; do a failed sign in + (-> page (.locator "input[name='email']") + (.fill "goober@example.com")) + (-> page (.locator "input[name='password']") + (.fill password)) + (-> page + (.locator "button:has-text('Sign in')") + .click) + (check-for-text page "Invalid email or password" + "Incorrect password shows a message."))) + +(defn sign-out [page] + ; click "Sign out" + (-> page (.locator "a[href='/auth/sign-out']") .click) + (check-for-no-text page "Signed in" "a[href='/auth/sign-in']" + "User is correctly signed out on homepage.")) + +(defn sign-in [page password] + (p/do! + ; successful sign in + (-> page (.locator "input[name='password']") + (.fill password)) + (-> page + (.locator "button:has-text('Sign in')") + .click) + (check-for-text + page "Signed in" + "User is correctly signed in again."))) + (deftest nbb-auth (t/testing "Auth against Sitefox on nbb tests." (async done (p/let [_ (log "Test: nbb-auth") - server (run-server "examples/nbb-auth" "npm i --no-save; npm run serve" 8000) + server (run-server "examples/nbb-auth" + "npm i --no-save; npm run serve" + 8000) {:keys [page browser]} (get-browser)] (p/catch (p/do! @@ -109,19 +166,81 @@ ; click "Sign up" (-> page (.locator "a[href='/auth/sign-up']") .click) ; fill out details and sign up - (-> page (.locator "input[name='email']") (.fill "goober@example.com")) - (-> page (.locator "input[name='email2']") (.fill "goober@example.com")) - (-> page (.locator "input[name='password']") (.fill "tester")) - (-> page (.locator "input[name='password2']") (.fill "tester")) + (-> page (.locator "input[name='email']") + (.fill "goober@example.com")) + (-> page (.locator "input[name='email2']") + (.fill "goober@example.com")) + (-> page (.locator "input[name='password']") + (.fill "tester")) + (-> page (.locator "input[name='password2']") + (.fill "tester")) - (p/let [[log-items] (p/all [(listen-to-log "verify-url (?http.*?)[\n$]") - (-> page (.locator "button:has-text('Sign up')") .click)]) + (p/let [[log-items] + (p/all [(listen-to-log + "verify-url (?http.*?)[\n$]") + (-> page + (.locator "button:has-text('Sign up')") + .click)]) url (j/get-in log-items [:groups :url])] ; click on the verification link (.goto page url) - (check-for-text page "Signed in" "User is correctly signed in after verification.") + (check-for-text + page "Signed in" + "User is correctly signed in after verification.") (.goto page base-url) - (check-for-text page "Signed in" "User is correctly signed in on homepage.")) + (check-for-text + page "Signed in" + "User is correctly signed in on homepage.")) + + (print "sign out") + (sign-out page) + + (print "check failed sign in") + (check-failed-sign-in page "testerwrong") + + (print "sign in again") + (sign-in page "tester") + + (print "forgot password") + ; test forgot password flow + (-> page (.locator "a[href='/auth/sign-out']") .click) + ; click "Sign in" + (-> page (.locator "a[href='/auth/sign-in']") .click) + ; click "Forgot password link" + (-> page (.locator "a[href='/auth/reset-password']") .click) + ; fill out the forgot password form + (-> page (.locator "input[name='email']") + (.fill "goober@example.com")) + + (p/let [[log-items] + (p/all [(listen-to-log + "verify-url (?http.*?)[\n$]") + (-> page + (.locator + "button:has-text('Reset password')") + .click)]) + url (j/get-in log-items [:groups :url])] + (check-for-text page "Reset password link sent" + "User has been notified of reset email.") + (.goto page url)) + + ; enter updated passwords + (-> page (.locator "input[name='password']") + (.fill "testagain")) + (-> page (.locator "input[name='password2']") + (.fill "testagain")) + (-> page + (.locator "button:has-text('Update password')") + .click) + (check-for-text + page "Signed in" + "User is correctly signed in again.") + + ; check sign in fails with old password + (sign-out page) + (check-failed-sign-in page "tester") + ; check successful sign in with new password + (sign-in page "testagain") (log "Closing resources.") (j/call server :kill) @@ -130,10 +249,95 @@ (done)) #(catch-fail % done server browser)))))) -#_ (defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m] - (print "report") - (if (cljs.test/successful? m) - (println "Success!") - (println "FAIL"))) +(defn check-form-submit [page] + (p/do! + ; fill out bad form details + (-> page (.locator "input[name='name']") + (.fill "Bilbo")) + (-> page (.locator "input[name='date']") + (.fill "2023-06-01")) + (-> page (.locator "input[name='count']") + (.fill "7")) + (-> page (.locator "button[type='submit']") .click) + (check-for-text + page "Form complete." + "Form submits sucessfully."))) + +(deftest nbb-forms + (t/testing "Sitefox forms and CSRF on nbb tests." + (async done + (p/let [_ (log "Test: nbb-forms") + server (run-server "examples/form-validation" + "npm i --no-save; npm run serve" + 8000) + {:keys [page context browser]} (get-browser)] + (p/catch + (p/do! + (.goto page base-url) + + ; fill out bad form details + (-> page (.locator "input[name='name']") + (.fill "")) + (-> page (.locator "input[name='date']") + (.fill "SEPTEMBER THE NOTHING")) + (-> page (.locator "input[name='count']") + (.fill "XYZ")) + + (-> page (.locator "button[type='submit']") .click) + + (check-for-text + page "You must enter a name between 5 and 20 characters." + "Name validation failed successfully.") + + (check-for-text + page "You must enter a valid date in YYYY-MM-DD format." + "Date validation failed successfully.") + + (check-for-text + page "You must enter a quantity between 5 and 10." + "Count validation failed successfully.") + + ; fill out form correctly + + (.goto page base-url) + + (check-form-submit page) + + ; fill out form correctly but fail csrf + (.goto page base-url) + + ; fill out bad form details + (-> page (.locator "input[name='name']") + (.fill "Bilbo")) + (-> page (.locator "input[name='date']") + (.fill "2023-06-01")) + (-> page (.locator "input[name='count']") + (.fill "7")) + ; modify csrf field + (-> page + (.evaluate "document.querySelector('input[name=\"_csrf\"]').value='BOGUS'")) + + (-> page (.locator "button[type='submit']") .click) + + (check-for-text + page "The form was tampered with." + "CSRF error caught sucessfully.") + + ; sanity check by running multiple CSRF checks in parallel forms + + ; open another form to get a new csrf token + (p/let [page2 (.newPage context)] + (.goto page2 base-url) + ; then reload the first page to get a new token + (.goto page (str base-url "?hello=1")) + ; check the second tab can still successfully submit + (check-form-submit page2)) + + (log "Closing resources.") + (j/call server :kill) + (.close browser) + (log "Resources closed.") + (done)) + #(catch-fail % done server browser)))))) (t/run-tests *ns*)