From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Mon, 16 Aug 2021 04:38:18 -0700 Subject: [PATCH 24/35] Standardize auth-source queries in ERC * lisp/erc/erc.el (erc-password): Deprecate variable only used by `erc-select-read-args'. Server passwords are primarily used as surrogates for other forms of authentication. Such use is common but nonstandard and often discouraged in favor of the de facto standard, SASL. Folks in the habit of invoking `erc(-tls)' interactively should be encouraged to use auth-source instead. (erc-select-read-args): Before this change, `erc-select-read-args' offered to use the value of a non-nil `erc-password' as the :password argument for `erc' and `erc-tls', referring to it as the "default" password. And when `erc-prompt-for-password' was nil and `erc-password' wasn't, the latter was passed along unconditionally. This only further complicated an already confusing situation for new users, who in most cases shouldn't be worried about sending a PASS command at all. Until SASL arrives, they should provide server passwords manually or learn to use auth-source. (erc-auth-source-server-function, erc-auth-source-join-function): New user options for retrieving a password externally, ostensibly by calling `auth-source-search'. (erc--auth-source-determine-params-defaults): New helper for `erc--auth-source-search' with potential for exporting publicly in the future. Favors :host and :port fields above others. Prioritizes network IDs over announced servers and dialed endpoints. (erc--auth-source-determine-params-merge): Add new function for merging contextual and default parameters. This is another contender for possible exporting. (erc--auth-source-search): New function for consulting auth-source and sorting the result as filtered and prioritized by the previously mentioned helpers. (erc-auth-source-search): New function to serve as default value for auth-source query-function options. (erc-server-join-channel): Use user option for consulting auth-source facility. Also accept nil for first argument (instead of server). (erc-cmd-JOIN): Use above-mentioned facilities when joining new channel. Omit server when calling `erc-server-join-channel'. Don't filter target buffers twice. Don't call `switch-to-buffer', which would create phantom buffers with names like target/server that were never used. IOW, only switch to existing target buffers. (erc--compute-server-password): Add new helper function for determining password. (erc-open, erc-determine-parameters): Move password figuring from the first to the latter. * lisp/erc/erc-services.el (erc-auth-source-services-function): Add new option for consulting auth-source in a NickServ context. (erc-nickserv-get-password): Pass network-context ID, when looking up password in `erc-nickserv-passwords' and when formatting prompt for user input. (erc-nickserv-passwords): Add comment to custom option definition type tag. * test/lisp/erc/erc-services-tests.el: Add new test file for above changes. For now, stash auth-source-related tests here until a suitable home can be found. * lisp/erc/erc-join.el (erc-autojoin--join): Don't pass session-like entity from `erc-autojoin-channels-alist' match to `erc-server-join-channel'. Allow that function to decide for itself which host to look up if necessary. --- lisp/erc/erc-join.el | 2 +- lisp/erc/erc-services.el | 53 +-- lisp/erc/erc.el | 197 +++++++--- test/lisp/erc/erc-services-tests.el | 574 ++++++++++++++++++++++++++++ 4 files changed, 754 insertions(+), 72 deletions(-) create mode 100644 test/lisp/erc/erc-services-tests.el diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el index d4edca236d..b4044548e8 100644 --- a/lisp/erc/erc-join.el +++ b/lisp/erc/erc-join.el @@ -145,7 +145,7 @@ erc-autojoin--join (let ((buf (erc-get-buffer chan erc-server-process))) (unless (and buf (with-current-buffer buf (erc--current-buffer-joined-p))) - (erc-server-join-channel match chan))))))) + (erc-server-join-channel nil chan))))))) (defun erc-autojoin-after-ident (_network _nick) "Autojoin channels in `erc-autojoin-channels-alist'. diff --git a/lisp/erc/erc-services.el b/lisp/erc/erc-services.el index cc5d5701e4..c43fac2f0a 100644 --- a/lisp/erc/erc-services.el +++ b/lisp/erc/erc-services.el @@ -174,6 +174,18 @@ erc-use-auth-source-for-nickserv-password :version "28.1" :type 'boolean) +(defcustom erc-auth-source-services-function #'erc-auth-source-search + "Function to retrieve NickServ password from auth-source. +Called with a subset of keyword parameters known to `auth-source-search' +and relevant to authenticating to nickname services. In return, ERC +expects a string to send as the password, or nil, to fall through to the +next method, such as prompting. See info node `(erc) Connecting' for +details." + :package-version '(ERC . "5.4.1") ; FIXME update when publishing to ELPA + :type '(choice (const erc-auth-source-search) + (const nil) + function)) + (defcustom erc-nickserv-passwords nil "Passwords used when identifying to NickServ automatically. `erc-prompt-for-nickserv-password' must be nil for these @@ -202,7 +214,7 @@ erc-nickserv-passwords (const QuakeNet) (const Rizon) (const SlashNET) - (symbol :tag "Network name")) + (symbol :tag "Network name or session ID")) (repeat :tag "Nickname and password" (cons :tag "Identity" (string :tag "Nick") @@ -431,31 +443,20 @@ erc-nickserv-get-password lookups stops and this function returns it (or returns nil if it is empty). Otherwise, no corresponding password was found, and it returns nil." - (let (network server port) - ;; Fill in local vars, switching to the server buffer once only - (erc-with-server-buffer - (setq network erc-network - server erc-session-server - port erc-session-port)) - (let ((ret - (or - (when erc-nickserv-passwords - (cdr (assoc nick - (cl-second (assoc network - erc-nickserv-passwords))))) - (when erc-use-auth-source-for-nickserv-password - (auth-source-pick-first-password - :require '(:secret) - :host server - ;; Ensure a string for :port - :port (format "%s" port) - :user nick)) - (when erc-prompt-for-nickserv-password - (read-passwd - (format "NickServ password for %s on %s (RET to cancel): " - nick network)))))) - (when (and ret (not (string= ret ""))) - ret)))) + (when-let* + ((nid (erc-networks--id-symbol erc-networks--id)) + (ret (or (when erc-nickserv-passwords + (assoc-default nick + (cadr (assq nid erc-nickserv-passwords)))) + (when (and erc-use-auth-source-for-nickserv-password + erc-auth-source-services-function) + (funcall erc-auth-source-services-function :user nick)) + (when erc-prompt-for-nickserv-password + (read-passwd + (format "NickServ password for %s on %s (RET to cancel): " + nick nid))))) + ((not (string-empty-p ret)))) + ret)) (defvar erc-auto-discard-away) diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index 463c497844..c2b7dacf84 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -228,9 +228,14 @@ erc-rename-buffers "old behavior when t now permanent" "29.1") (defvar erc-password nil - "Password to use when authenticating to an IRC server. -It is not strictly necessary to provide this, since ERC will -prompt you for it.") + "Password to use when authenticating to an IRC server interactively. + +This variable only exists for legacy reasons. It's not customizable and +is limited to a single server password. Users looking for similar +functionality should consider auth-source instead. See info +node `(auth) Top' and info node `(erc) Connecting'.") + +(make-obsolete-variable 'erc-password "use auth-source instead" "29.1") (defcustom erc-user-mode "+i" ;; +i "Invisible". Hides user from global /who and /names. @@ -241,7 +246,7 @@ erc-user-mode (defcustom erc-prompt-for-password t - "Asks before using the default password, or whether to enter a new one." + "Ask for a server password when invoking `erc-tls' interactively." :group 'erc :type 'boolean) @@ -2209,15 +2214,6 @@ erc-open (setq erc-logged-in nil) ;; The local copy of `erc-nick' - the list of nicks to choose (setq erc-default-nicks (if (consp erc-nick) erc-nick (list erc-nick))) - ;; password stuff - (setq erc-session-password - (or passwd - (auth-source-pick-first-password - :host server - :user nick - ;; secrets.el wouldn’t accept a number - :port (if (numberp port) (number-to-string port) port) - :require '(:secret)))) ;; client certificate (only useful if connecting over TLS) (setq erc-session-client-certificate client-certificate) (setq erc-networks--id (if connect @@ -2239,7 +2235,7 @@ erc-open (erc-display-prompt) (goto-char (point-max))) - (erc-determine-parameters server port nick full-name user) + (erc-determine-parameters server port nick full-name user passwd) ;; Saving log file on exit (run-hook-with-args 'erc-connect-pre-hook buffer) @@ -2337,11 +2333,9 @@ erc-select-read-args (setq server user-input) (setq passwd (if erc-prompt-for-password - (if (and erc-password - (y-or-n-p "Use the default password? ")) - erc-password - (read-passwd "Password: ")) - erc-password)) + (read-passwd "Server password: ") + (with-suppressed-warnings ((obsolete erc-password)) + erc-password))) (when (and passwd (string= "" passwd)) (setq passwd nil)) @@ -3354,18 +3348,130 @@ erc-cmd-HELP (defalias 'erc-cmd-H #'erc-cmd-HELP) (put 'erc-cmd-HELP 'process-not-needed t) +(defcustom erc-auth-source-server-function #'erc-auth-source-search + "Function to query auth-source for a server password. +Called with a subset of keyword parameters known to `auth-source-search' +and relevant to an opening \"PASS\" command, if any. In return, ERC +expects a string to send as the server password, or nil, to skip the +\"PASS\" command completely. An explicit `:password' argument to +entry-point commands `erc' and `erc-tls' also inhibits lookup, as does +setting this option to nil. See info node `(erc) Connecting' for +details." + :package-version '(ERC . "5.4.1") ; FIXME update when publishing to ELPA + :group 'erc + :type '(choice (const erc-auth-source-search) + (const nil) + function)) + +(defcustom erc-auth-source-join-function #'erc-auth-source-search + "Function to query auth-source on joining a channel. +Called with a subset of keyword arguments known to `auth-source-search' +and relevant to joining a password-protected channel. In return, ERC +expects a string to use as the channel \"key\", or nil to just join the +channel normally. Setting the option itself to nil tells ERC to always +forgo consulting auth-source for channel keys. For more information, +see info node `(erc) Connecting'." + :package-version '(ERC . "5.4.1") ; FIXME update when publishing to ELPA + :group 'erc + :type '(choice (const erc-auth-source-search) + (const nil) + function)) + +(defun erc--auth-source-determine-params-defaults () + (let* ((net (and-let* ((esid (erc-networks--id-symbol erc-networks--id)) + ((symbol-name esid))))) + (localp (and erc--target (erc--target-channel-local-p erc--target))) + (hosts (if localp + (list erc-server-announced-name erc-session-server net) + (list net erc-server-announced-name erc-session-server))) + (ports (list (cl-typecase erc-session-port + (integer (number-to-string erc-session-port)) + (string (and (string= erc-session-port "irc") + erc-session-port)) ; or nil + (t erc-session-port)) + "irc"))) + (list (cons :host (delq nil hosts)) + (cons :port (delq nil ports)) + (cons :require '(:secret))))) + +(defun erc--auth-source-determine-params-merge (&rest plist) + "Return a plist of merged keyword args to pass to `auth-source-search'. +Combine items in PLIST with others derived from the current connection +context, but prioritize the former. For keys not present in PLIST, +favor a network ID over an announced server unless `erc--target' is a +local channel. And treat the dialed server address as a fallback for +the announced name in both cases." + (let ((defaults (erc--auth-source-determine-params-defaults))) + `(,@(cl-loop for (key value) on plist by #'cddr + for default = (assq key defaults) + do (when default (setq defaults (delq default defaults))) + append `(,key ,(delete-dups + `(,@(if (consp value) value (list value)) + ,@(cdr default))))) + ,@(cl-loop for (k . v) in defaults append (list k v))))) + +(defun erc--auth-source-search (&rest defaults) + "Ask auth-source for a secret and return it if found. +Use DEFAULTS as keyword arguments for querying auth-source and as a +guide for narrowing results. Return a string if found or nil otherwise. +The ordering of DEFAULTS influences how results are filtered, as does +the ordering of the members of any individual composite values. If +necessary, the former takes priority. For example, if DEFAULTS were to +contain + + :host (\"foo\" \"bar\") :port (\"123\" \"456\") + +the secret from an auth-source entry of host foo and port 456 would be +chosen over another of host bar and port 123. However, if DEFAULTS +looked like + + :port (\"123\" \"456\") :host (\"foo\" \"bar\") + +the opposite would be true. In both cases, two entries with the same +host but different ports would result in the one with port 123 getting +the nod. Much the same would happen for entries sharing only a port: +the one with host foo would win." + (when-let* + ((priority (map-keys defaults)) + (test (lambda (a b) + (catch 'done + (dolist (key priority) + (let* ((d (plist-get defaults key)) + (defval (if (listp d) d (list d))) + ;; featurep 'seq via auth-source > json > map + (p (seq-position defval (plist-get a key))) + (q (seq-position defval (plist-get b key)))) + (unless (eql p q) + (throw 'done (when p (or (not q) (< p q)))))))))) + (plist (copy-sequence defaults))) + (unless (plist-get plist :max) + (setq plist (plist-put plist :max 5000))) ; `auth-source-netrc-parse' + (unless (plist-get defaults :require) + (setq plist (plist-put plist :require '(:secret)))) + (when-let* ((sorted (sort (apply #'auth-source-search plist) test)) + (secret (plist-get (car sorted) :secret))) + (if (functionp secret) (funcall secret) secret)))) + +(defun erc-auth-source-search (&rest plist) + "Call `auth-source-search', possibly with keyword params in PLIST." + ;; These exist as separate helpers in case folks should find them + ;; useful. If that's you, please request that they be exported. + (apply #'erc--auth-source-search + (apply #'erc--auth-source-determine-params-merge plist))) + (defun erc-server-join-channel (server channel &optional secret) - (let ((password - (or secret - (auth-source-pick-first-password - :host server - :port "irc" - :user channel)))) - (erc-log (format "cmd: JOIN: %s" channel)) - (erc-server-send (concat "JOIN " channel - (if password - (concat " " password) - ""))))) + "Join CHANNEL, optionally with SECRET. +Without SECRET, consult auth-source, possibly passing SERVER as the +`:host' query parameter." + (unless (or secret (not erc-auth-source-join-function)) + (unless server + (when (and erc-server-announced-name + (erc--valid-local-channel-p channel)) + (setq server erc-server-announced-name))) + (setq secret (apply erc-auth-source-join-function + `(,@(and server (list :host server)) :user ,channel)))) + (erc-log (format "cmd: JOIN: %s" channel)) + (erc-server-send (concat "JOIN " channel (and secret (concat " " secret))))) (defun erc--valid-local-channel-p (channel) "Non-nil when channel is server-local on a network that allows them." @@ -3387,19 +3493,12 @@ erc-cmd-JOIN (setq chnl (erc-ensure-channel-name channel))) (when chnl ;; Prevent double joining of same channel on same server. - (let* ((joined-channels - (mapcar (lambda (chanbuf) - (with-current-buffer chanbuf (erc-default-target))) - (erc-channel-list erc-server-process))) - (server (with-current-buffer (process-buffer erc-server-process) - (or erc-session-server erc-server-announced-name))) - (chnl-name (car (erc-member-ignore-case chnl joined-channels)))) - (if chnl-name - (switch-to-buffer (if (get-buffer chnl-name) - chnl-name - (concat chnl-name "/" server))) - (setq erc--server-last-reconnect-count 0) - (erc-server-join-channel server chnl key))))) + (if-let* ((existing (erc-get-buffer chnl erc-server-process)) + ((with-current-buffer existing + (erc-get-channel-user (erc-current-nick))))) + (switch-to-buffer existing) + (setq erc--server-last-reconnect-count 0) + (erc-server-join-channel nil chnl key)))) t) (defalias 'erc-cmd-CHANNEL #'erc-cmd-JOIN) @@ -6355,7 +6454,7 @@ erc-login ;; connection properties' heuristics -(defun erc-determine-parameters (&optional server port nick name user) +(defun erc-determine-parameters (&optional server port nick name user passwd) "Determine the connection and authentication parameters. Sets the buffer local variables: @@ -6364,12 +6463,14 @@ erc-determine-parameters - `erc-session-port' - `erc-session-user-full-name' - `erc-session-username' +- `erc-session-password' - `erc-server-current-nick'" (setq erc-session-connector erc-server-connect-function erc-session-server (erc-compute-server server) erc-session-port (or port erc-default-port) erc-session-user-full-name (erc-compute-full-name name) - erc-session-username (erc-compute-user user)) + erc-session-username (erc-compute-user user) + erc-session-password (erc--compute-server-password passwd nick)) (erc-set-current-nick (erc-compute-nick nick))) (defun erc-compute-server (&optional server) @@ -6406,6 +6507,12 @@ erc-compute-nick (getenv "IRCNICK") (user-login-name))) +(defun erc--compute-server-password (password nick) + "Maybe provide a PASSWORD argument for the IRC \"PASS\" command. +When `erc-auth-source-server-function' is non-nil, call it with NICK for +the user field and use whatever it returns as the server password." + (or password (and erc-auth-source-server-function + (funcall erc-auth-source-server-function :user nick)))) (defun erc-compute-full-name (&optional full-name) "Return user's full name. diff --git a/test/lisp/erc/erc-services-tests.el b/test/lisp/erc/erc-services-tests.el new file mode 100644 index 0000000000..8e2b8d2927 --- /dev/null +++ b/test/lisp/erc/erc-services-tests.el @@ -0,0 +1,574 @@ +;;; erc-services-tests.el --- Tests for erc-services. -*- lexical-binding:t -*- + +;; Copyright (C) 2020-2022 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published +;; by the Free Software Foundation, either version 3 of the License, +;; or (at your option) any later version. +;; +;; GNU Emacs is distributed in the hope that it will be useful, but +;; WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;; General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;; TODO: move the auth-source tests somewhere else. They've been +;; stashed here for pragmatic reasons. + +;;; Code: + +(require 'ert-x) +(require 'erc-services) +(require 'erc-compat) +(require 'secrets) + +;;;; Core auth-source + +(ert-deftest erc--auth-source-determine-params-merge () + (let ((erc-session-server "irc.gnu.org") + (erc-server-announced-name "my.gnu.org") + (erc-session-port 6697) + (erc-network 'fake) + (erc-server-current-nick "tester") + (erc-networks--id (erc-networks--id-create 'GNU.chat))) + + (should (equal (erc--auth-source-determine-params-merge) + '(:host ("GNU.chat" "my.gnu.org" "irc.gnu.org") + :port ("6697" "irc") + :require (:secret)))) + + (should (equal (erc--auth-source-determine-params-merge :host "fake") + '(:host ("fake" "GNU.chat" "my.gnu.org" "irc.gnu.org") + :port ("6697" "irc") + :require (:secret)))) + + (should (equal (erc--auth-source-determine-params-merge + :host '("fake") :require :host) + '(:host ("fake" "GNU.chat" "my.gnu.org" "irc.gnu.org") + :require (:host :secret) + :port ("6697" "irc")))) + + (should (equal (erc--auth-source-determine-params-merge + :host '("fake" "GNU.chat") :port "1234" :x "x") + '(:host ("fake" "GNU.chat" "my.gnu.org" "irc.gnu.org") + :port ("1234" "6697" "irc") + :x ("x") + :require (:secret)))))) + +;; Some of the following may be related to bug#23438. + +(defun erc-services-tests--auth-source-standard (search) + + (ert-info ("Session wins") + (let ((erc-session-server "irc.gnu.org") + (erc-server-announced-name "my.gnu.org") + (erc-session-port 6697) + (erc-network 'fake) + (erc-server-current-nick "tester") + (erc-networks--id (erc-networks--id-create 'GNU.chat))) + (should (string= (funcall search :user "#chan") "foo")))) + + (ert-info ("Network wins") + (let* ((erc-session-server "irc.gnu.org") + (erc-server-announced-name "my.gnu.org") + (erc-session-port 6697) + (erc-network 'GNU.chat) + (erc-server-current-nick "tester") + (erc-networks--id (erc-networks--id-create nil))) + (should (string= (funcall search :user "#chan") "foo")))) + + (ert-info ("Announced wins") + (let ((erc-session-server "irc.gnu.org") + (erc-server-announced-name "my.gnu.org") + (erc-session-port 6697) + erc-network + (erc-networks--id (erc-networks--id-create nil))) + (should (string= (funcall search :user "#chan") "baz"))))) + +(defun erc-services-tests--auth-source-announced (search) + (let* ((erc--isupport-params (make-hash-table)) + (erc-server-parameters '(("CHANTYPES" . "&#"))) + (erc--target (erc--target-from-string "&chan"))) + + (ert-info ("Announced prioritized") + + (ert-info ("Announced wins") + (let* ((erc-session-server "irc.gnu.org") + (erc-server-announced-name "my.gnu.org") + (erc-session-port 6697) + (erc-network 'GNU.chat) + (erc-server-current-nick "tester") + (erc-networks--id (erc-networks--id-create nil))) + (should (string= (funcall search :user "#chan") "baz")))) + + (ert-info ("Peer next") + (let* ((erc-server-announced-name "irc.gnu.org") + (erc-session-port 6697) + (erc-network 'GNU.chat) + (erc-server-current-nick "tester") + (erc-networks--id (erc-networks--id-create nil))) + (should (string= (funcall search :user "#chan") "bar")))) + + (ert-info ("Network used as fallback") + (let* ((erc-session-port 6697) + (erc-network 'GNU.chat) + (erc-server-current-nick "tester") + (erc-networks--id (erc-networks--id-create nil))) + (should (string= (funcall search :user "#chan") "foo"))))))) + +(defun erc-services-tests--auth-source-overrides (search) + (let* ((erc-session-server "irc.gnu.org") + (erc-server-announced-name "my.gnu.org") + (erc-network 'GNU.chat) + (erc-server-current-nick "tester") + (erc-networks--id (erc-networks--id-create nil)) + (erc-session-port 6667)) + + (ert-info ("Specificity and overrides") + + (ert-info ("More specific port") + (let ((erc-session-port 6697)) + (should (string= (funcall search :user "#chan") "spam")))) + + (ert-info ("More specific user (network loses)") + (should (string= (funcall search :user '("#fsf")) "42"))) + + (ert-info ("Actual override") + (should (string= (funcall search :port "6667") "sesame"))) + + (ert-info ("Overrides don't interfere with post-processing") + (should (string= (funcall search :host "MyHost") "123")))))) + +;; auth-source netrc backend + +(defvar erc-services-tests--auth-source-entries + '("machine irc.gnu.org port irc user \"#chan\" password bar" + "machine my.gnu.org port irc user \"#chan\" password baz" + "machine GNU.chat port irc user \"#chan\" password foo")) + +;; FIXME explain what this is for +(defun erc-services-tests--auth-source-shuffle (&rest extra) + (string-join `(,@(sort (append erc-services-tests--auth-source-entries extra) + (lambda (&rest _) (zerop (random 2)))) + "") + "\n")) + +(ert-deftest erc--auth-source-search--netrc-standard () + (ert-with-temp-file netrc-file + :prefix "erc--auth-source-search--standard" + :text (erc-services-tests--auth-source-shuffle) + + (let ((auth-sources (list netrc-file)) + (auth-source-do-cache nil)) + (erc-services-tests--auth-source-standard #'erc-auth-source-search)))) + +(ert-deftest erc--auth-source-search--netrc-announced () + (ert-with-temp-file netrc-file + :prefix "erc--auth-source-search--announced" + :text (erc-services-tests--auth-source-shuffle) + + (let ((auth-sources (list netrc-file)) + (auth-source-do-cache nil)) + (erc-services-tests--auth-source-announced #'erc-auth-source-search)))) + +(ert-deftest erc--auth-source-search--netrc-overrides () + (ert-with-temp-file netrc-file + :prefix "erc--auth-source-search--overrides" + :text (erc-services-tests--auth-source-shuffle + "machine GNU.chat port 6697 user \"#chan\" password spam" + "machine my.gnu.org port irc user \"#fsf\" password 42" + "machine irc.gnu.org port 6667 password sesame" + "machine MyHost port irc password 456" + "machine MyHost port 6667 password 123") + + (let ((auth-sources (list netrc-file)) + (auth-source-do-cache nil)) + (erc-services-tests--auth-source-overrides #'erc-auth-source-search)))) + +;; auth-source plstore backend + +(defun erc-services-test--call-with-plstore (&rest args) + (advice-add 'epg-decrypt-string :override + (lambda (&rest r) (prin1-to-string (cadr r))) + '((name . erc--auth-source-plstore))) + (advice-add 'epg-find-configuration :override + (lambda (&rest _) "" '((program . "/bin/true"))) + '((name . erc--auth-source-plstore))) + (unwind-protect + (apply #'erc-auth-source-search args) + (advice-remove 'epg-decrypt-string 'erc--auth-source-plstore) + (advice-remove 'epg-find-configuration 'erc--auth-source-plstore))) + +(defvar erc-services-tests--auth-source-plstore-standard-entries + '(("ba950d38118a76d71f9f0591bb373d6cb366a512" + :secret-secret t + :host "irc.gnu.org" + :user "#chan" + :port "irc") + ("7f17ca445d11158065e911a6d0f4cbf52ca250e3" + :secret-secret t + :host "my.gnu.org" + :user "#chan" + :port "irc") + ("fcd3c8bd6daf4509de0ad6ee98e744ce0fca9377" + :secret-secret t + :host "GNU.chat" + :user "#chan" + :port "irc"))) + +(defvar erc-services-tests--auth-source-plstore-standard-secrets + '(("ba950d38118a76d71f9f0591bb373d6cb366a512" :secret "bar") + ("7f17ca445d11158065e911a6d0f4cbf52ca250e3" :secret "baz") + ("fcd3c8bd6daf4509de0ad6ee98e744ce0fca9377" :secret "foo"))) + +(ert-deftest erc--auth-source-search--plstore-standard () + (ert-with-temp-file plstore-file + :suffix ".plist" + :text (concat ";;; public entries -*- mode: plstore -*- \n" + (prin1-to-string + erc-services-tests--auth-source-plstore-standard-entries) + "\n;;; secret entries\n" + (prin1-to-string + erc-services-tests--auth-source-plstore-standard-secrets) + "\n") + + (let ((auth-sources (list plstore-file)) + (auth-source-do-cache nil)) + (erc-services-tests--auth-source-standard + #'erc-services-test--call-with-plstore)))) + +(ert-deftest erc--auth-source-search--plstore-announced () + (ert-with-temp-file plstore-file + :suffix ".plist" + :text (concat ";;; public entries -*- mode: plstore -*- \n" + (prin1-to-string + erc-services-tests--auth-source-plstore-standard-entries) + "\n;;; secret entries\n" + (prin1-to-string + erc-services-tests--auth-source-plstore-standard-secrets) + "\n") + + (let ((auth-sources (list plstore-file)) + (auth-source-do-cache nil)) + (erc-services-tests--auth-source-announced + #'erc-services-test--call-with-plstore)))) + +(ert-deftest erc--auth-source-search--plstore-overrides () + (ert-with-temp-file plstore-file + :suffix ".plist" + :text (concat + ";;; public entries -*- mode: plstore -*- \n" + (prin1-to-string + `(,@erc-services-tests--auth-source-plstore-standard-entries + ("1b3fab249a8dff77a4d8fe7eb4b0171b25cc711a" + :secret-secret t :host "GNU.chat" :user "#chan" :port "6697") + ("6cbcdc39476b8cfcca6f3e9a7876f41ec3f708cc" + :secret-secret t :host "my.gnu.org" :user "#fsf" :port "irc") + ("a33e2b3bd2d6f33995a4b88710a594a100c5e41d" + :secret-secret t :host "irc.gnu.org" :port "6667") + ("ab2fd349b2b7d6a9215bb35a92d054261b0b1537" + :secret-secret t :host "MyHost" :port "irc") + ("61a6bd552059494f479ff720e8de33e22574650a" + :secret-secret t :host "MyHost" :port "6667"))) + "\n;;; secret entries\n" + (prin1-to-string + `(,@erc-services-tests--auth-source-plstore-standard-secrets + ("1b3fab249a8dff77a4d8fe7eb4b0171b25cc711a" :secret "spam") + ("6cbcdc39476b8cfcca6f3e9a7876f41ec3f708cc" :secret "42") + ("a33e2b3bd2d6f33995a4b88710a594a100c5e41d" :secret "sesame") + ("ab2fd349b2b7d6a9215bb35a92d054261b0b1537" :secret "456") + ("61a6bd552059494f479ff720e8de33e22574650a" :secret "123"))) + "\n") + + (let ((auth-sources (list plstore-file)) + (auth-source-do-cache nil)) + (erc-services-tests--auth-source-overrides + #'erc-services-test--call-with-plstore)))) + +;; auth-source JSON backend + +(defvar erc-services-tests--auth-source-json-standard-entries + [(:host "irc.gnu.org" :port "irc" :user "#chan" :secret "bar") + (:host "my.gnu.org" :port "irc" :user "#chan" :secret "baz") + (:host "GNU.chat" :port "irc" :user "#chan" :secret "foo")]) + +(ert-deftest erc--auth-source-search--json-standard () + (ert-with-temp-file json-store + :suffix ".json" + :text (let ((json-object-type 'plist)) + (json-encode + erc-services-tests--auth-source-json-standard-entries)) + (let ((auth-sources (list json-store)) + (auth-source-do-cache nil)) + (erc-services-tests--auth-source-standard #'erc-auth-source-search)))) + +(ert-deftest erc--auth-source-search--json-announced () + (ert-with-temp-file plstore-file + :suffix ".json" + :text (let ((json-object-type 'plist)) + (json-encode + erc-services-tests--auth-source-json-standard-entries)) + + (let ((auth-sources (list plstore-file)) + (auth-source-do-cache nil)) + (erc-services-tests--auth-source-announced #'erc-auth-source-search)))) + +(ert-deftest erc--auth-source-search--json-overrides () + (ert-with-temp-file json-file + :suffix ".json" + :text (let ((json-object-type 'plist)) + (json-encode + (vconcat + erc-services-tests--auth-source-json-standard-entries + [(:secret "spam" :host "GNU.chat" :user "#chan" :port "6697") + (:secret "42" :host "my.gnu.org" :user "#fsf" :port "irc") + (:secret "sesame" :host "irc.gnu.org" :port "6667") + (:secret "456" :host "MyHost" :port "irc") + (:secret "123" :host "MyHost" :port "6667")]))) + + (let ((auth-sources (list json-file)) + (auth-source-do-cache nil)) + (erc-services-tests--auth-source-overrides #'erc-auth-source-search)))) + +;; auth-source-secrets backend + +(defvar erc-services-tests--auth-source-secrets-standard-entries + '(("#chan@irc.gnu.org:irc" ; label + (:host . "irc.gnu.org") + (:user . "#chan") + (:port . "irc") + (:xdg:schema . "org.freedesktop.Secret.Generic")) + ("#chan@my.gnu.org:irc" + (:host . "my.gnu.org") + (:user . "#chan") + (:port . "irc") + (:xdg:schema . "org.freedesktop.Secret.Generic")) + ("#chan@GNU.chat:irc" + (:host . "GNU.chat") + (:user . "#chan") + (:port . "irc") + (:xdg:schema . "org.freedesktop.Secret.Generic")))) + +(defvar erc-services-tests--auth-source-secrets-standard-secrets + '(("#chan@irc.gnu.org:irc" . "bar") + ("#chan@my.gnu.org:irc" . "baz") + ("#chan@GNU.chat:irc" . "foo"))) + +(ert-deftest erc--auth-source-search--secrets-standard () + (skip-unless (bound-and-true-p secrets-enabled)) + (let ((auth-sources '("secrets:Test")) + (auth-source-do-cache nil) + (entries erc-services-tests--auth-source-secrets-standard-entries) + (secrets erc-services-tests--auth-source-secrets-standard-secrets)) + + (cl-letf (((symbol-function 'secrets-search-items) + (lambda (col &rest r) + (should (equal col "Test")) + (should (plist-get r :user)) + (map-keys entries))) + ((symbol-function 'secrets-get-secret) + (lambda (col label) + (should (equal col "Test")) + (assoc-default label secrets))) + ((symbol-function 'secrets-get-attributes) + (lambda (col label) + (should (equal col "Test")) + (assoc-default label entries)))) + + (erc-services-tests--auth-source-standard #'erc-auth-source-search)))) + +(ert-deftest erc--auth-source-search--secrets-announced () + (skip-unless (bound-and-true-p secrets-enabled)) + (let ((auth-sources '("secrets:Test")) + (auth-source-do-cache nil) + (entries erc-services-tests--auth-source-secrets-standard-entries) + (secrets erc-services-tests--auth-source-secrets-standard-secrets)) + + (cl-letf (((symbol-function 'secrets-search-items) + (lambda (col &rest r) + (should (equal col "Test")) + (should (plist-get r :user)) + (map-keys entries))) + ((symbol-function 'secrets-get-secret) + (lambda (col label) + (should (equal col "Test")) + (assoc-default label secrets))) + ((symbol-function 'secrets-get-attributes) + (lambda (col label) + (should (equal col "Test")) + (assoc-default label entries)))) + + (erc-services-tests--auth-source-announced #'erc-auth-source-search)))) + +(ert-deftest erc--auth-source-search--secrets-overrides () + (skip-unless (bound-and-true-p secrets-enabled)) + (let ((auth-sources '("secrets:Test")) + (auth-source-do-cache nil) + (entries `(,@erc-services-tests--auth-source-secrets-standard-entries + ("#chan@GNU.chat:6697" + (:host . "GNU.chat") (:user . "#chan") (:port . "6697") + (:xdg:schema . "org.freedesktop.Secret.Generic")) + ("#fsf@my.gnu.org:irc" + (:host . "my.gnu.org") (:user . "#fsf") (:port . "irc") + (:xdg:schema . "org.freedesktop.Secret.Generic")) + ("irc.gnu.org:6667" + (:host . "irc.gnu.org") (:port . "6667") + (:xdg:schema . "org.freedesktop.Secret.Generic")) + ("MyHost:irc" + (:host . "MyHost") (:port . "irc") + (:xdg:schema . "org.freedesktop.Secret.Generic")) + ("MyHost:6667" + (:host . "MyHost") (:port . "6667") + (:xdg:schema . "org.freedesktop.Secret.Generic")))) + (secrets `(,@erc-services-tests--auth-source-secrets-standard-secrets + ("#chan@GNU.chat:6697" . "spam") + ("#fsf@my.gnu.org:irc" . "42" ) + ("irc.gnu.org:6667" . "sesame") + ("MyHost:irc" . "456") + ("MyHost:6667" . "123")))) + + (cl-letf (((symbol-function 'secrets-search-items) + (lambda (col &rest _) + (should (equal col "Test")) + (map-keys entries))) + ((symbol-function 'secrets-get-secret) + (lambda (col label) + (should (equal col "Test")) + (assoc-default label secrets))) + ((symbol-function 'secrets-get-attributes) + (lambda (col label) + (should (equal col "Test")) + (assoc-default label entries)))) + + (erc-services-tests--auth-source-overrides #'erc-auth-source-search)))) + +;; auth-source-pass backend + +(require 'auth-source-pass) + +;; `auth-source-pass--find-match-unambiguous' returns something like: +;; +;; (list :host "irc.gnu.org" +;; :port "6697" +;; :user "rms" +;; :secret +;; #[0 "\301\302\300\"\207" +;; [((secret . "freedom")) auth-source-pass--get-attr secret] 3]) +;; +;; This function gives ^ (faked here to avoid gpg and file IO). See +;; `auth-source-pass--with-store' in ../auth-source-pass-tests.el +(defun erc-services-tests--asp-parse-entry (store entry) + (when-let ((found (cl-find entry store :key #'car :test #'string=))) + (list (assoc 'secret (cdr found))))) + +(defvar erc-join-tests--auth-source-pass-entries + '(("irc.gnu.org:irc/#chan" + ("port" . "irc") ("user" . "#chan") (secret . "bar")) + ("my.gnu.org:irc/#chan" + ("port" . "irc") ("user" . "#chan") (secret . "baz")) + ("GNU.chat:irc/#chan" + ("port" . "irc") ("user" . "#chan") (secret . "foo")))) + +(ert-deftest erc--auth-source-search--pass-standard () + (ert-skip "Pass backend not yet supported") + (let ((store erc-join-tests--auth-source-pass-entries) + (auth-sources '(password-store)) + (auth-source-do-cache nil)) + + (cl-letf (((symbol-function 'auth-source-pass-parse-entry) + (apply-partially #'erc-services-tests--asp-parse-entry store)) + ((symbol-function 'auth-source-pass-entries) + (lambda () (mapcar #'car store)))) + + (erc-services-tests--auth-source-standard #'erc-auth-source-search)))) + +(ert-deftest erc--auth-source-search--pass-announced () + (ert-skip "Pass backend not yet supported") + (let ((store erc-join-tests--auth-source-pass-entries) + (auth-sources '(password-store)) + (auth-source-do-cache nil)) + + (cl-letf (((symbol-function 'auth-source-pass-parse-entry) + (apply-partially #'erc-services-tests--asp-parse-entry store)) + ((symbol-function 'auth-source-pass-entries) + (lambda () (mapcar #'car store)))) + + (erc-services-tests--auth-source-announced #'erc-auth-source-search)))) + +(ert-deftest erc--auth-source-search--pass-overrides () + (ert-skip "Pass backend not yet supported") + (let ((store + `(,@erc-join-tests--auth-source-pass-entries + ("GNU.chat:6697/#chan" + ("port" . "6697") ("user" . "#chan") (secret . "spam")) + ("my.gnu.org:irc/#fsf" + ("port" . "irc") ("user" . "#fsf") (secret . "42")) + ("irc.gnu.org:6667" + ("port" . "6667") (secret . "sesame")) + ("MyHost:irc" + ("port" . "irc") (secret . "456")) + ("MyHost:6667" + ("port" . "6667") (secret . "123")))) + (auth-sources '(password-store)) + (auth-source-do-cache nil)) + + (cl-letf (((symbol-function 'auth-source-pass-parse-entry) + (apply-partially #'erc-services-tests--asp-parse-entry store)) + ((symbol-function 'auth-source-pass-entries) + (lambda () (mapcar #'car store)))) + + (erc-services-tests--auth-source-overrides #'erc-auth-source-search)))) + +;;;; The services module + +(ert-deftest erc-nickserv-get-password () + (should erc-prompt-for-nickserv-password) + (ert-with-temp-file netrc-file + :prefix "erc-nickserv-get-password" + :text (mapconcat 'identity + '("machine GNU/chat port 6697 user bob password spam" + "machine FSF.chat port 6697 user bob password sesame" + "machine MyHost port irc password 123") + "\n") + + (let* ((auth-sources (list netrc-file)) + (auth-source-do-cache nil) + (erc-nickserv-passwords '((FSF.chat (("alice" . "foo") + ("joe" . "bar"))))) + (erc-use-auth-source-for-nickserv-password t) + (erc-session-server "irc.gnu.org") + (erc-server-announced-name "my.gnu.org") + (erc-network 'FSF.chat) + (erc-server-current-nick "tester") + (erc-networks--id (erc-networks--id-create nil)) + (erc-session-port 6697)) + + (ert-info ("Lookup custom option") + (should (string= (erc-nickserv-get-password "alice") "foo"))) + + (ert-info ("Auth source") + (ert-info ("Network") + (should (string= (erc-nickserv-get-password "bob") "sesame"))) + + (ert-info ("Network ID") + (let ((erc-networks--id (erc-networks--id-create 'GNU/chat))) + (should (string= (erc-nickserv-get-password "bob") "spam"))))) + + (ert-info ("Read input") + (should (string= + (ert-simulate-keys "baz\r" (erc-nickserv-get-password "mike")) + "baz"))) + + (ert-info ("Failed") + (should-not (ert-simulate-keys "\r" + (erc-nickserv-get-password "fake"))))))) + + +;;; erc-services-tests.el ends here -- 2.36.1