summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-02-18 23:20:28 -0600
committerCraig Jennings <c@cjennings.net>2026-02-18 23:20:28 -0600
commitb19ad7899fecfb6835e19e23a7069233654c9fc7 (patch)
treea8f1747db2a1254d029234a29a45578088305ffc
parent6bf2688d0b3e4fef82671fb70e1aa883b0c90018 (diff)
feat(restclient): add REST API client for interactive API exploration
restclient.el + restclient-jq integration with SkyFi API templates, tutorial file, auto key injection from authinfo, 17 ERT tests.
-rw-r--r--data/skyfi-api.rest85
-rw-r--r--data/tutorial-api.rest140
-rw-r--r--init.el1
-rw-r--r--modules/restclient-config.el104
-rw-r--r--tests/test-restclient-config-inject-skyfi-key.el147
-rw-r--r--tests/test-restclient-config-new-buffer.el62
-rw-r--r--tests/test-restclient-config-skyfi-buffer.el38
-rw-r--r--todo.org24
8 files changed, 601 insertions, 0 deletions
diff --git a/data/skyfi-api.rest b/data/skyfi-api.rest
new file mode 100644
index 00000000..9a5a4267
--- /dev/null
+++ b/data/skyfi-api.rest
@@ -0,0 +1,85 @@
+# -*- restclient -*-
+#
+# SkyFi Satellite Imagery API
+# https://app.skyfi.com/platform-api/redoc
+#
+# KEY INJECTION:
+# The :skyfi-key variable below is auto-populated from authinfo.gpg
+# when you open this file via C-; R s. The key is NEVER stored on disk.
+# If you see PLACEHOLDER, run C-; R s again or check your authinfo.gpg.
+#
+# authinfo.gpg entry format:
+# machine app.skyfi.com login apikey password YOUR_API_KEY_HERE
+
+:skyfi-key = PLACEHOLDER
+:skyfi-base = https://app.skyfi.com/platform-api
+
+#
+# ============================================================
+# Archive Search — find available satellite imagery
+# ============================================================
+#
+
+# Search for imagery over an area of interest (AOI)
+# Adjust the GeoJSON polygon, date range, and cloud cover as needed
+POST :skyfi-base/archive/search
+Content-Type: application/json
+APIKey: :skyfi-key
+
+{
+ "aoi": {
+ "type": "Polygon",
+ "coordinates": [[
+ [-90.10, 29.95],
+ [-90.05, 29.95],
+ [-90.05, 29.98],
+ [-90.10, 29.98],
+ [-90.10, 29.95]
+ ]]
+ },
+ "dateFrom": "2025-01-01T00:00:00Z",
+ "dateTo": "2025-12-31T23:59:59Z",
+ "cloudCover": 20
+}
+
+#
+# ============================================================
+# Pricing — get cost estimate for an archive item
+# ============================================================
+#
+
+# Replace ARCHIVE_ID with an ID from the search results above
+# TIP: Use jq to extract IDs: -> jq-set-var :archive-id .[0].id
+POST :skyfi-base/pricing
+Content-Type: application/json
+APIKey: :skyfi-key
+
+{
+ "archiveId": "ARCHIVE_ID_HERE",
+ "aoi": {
+ "type": "Polygon",
+ "coordinates": [[
+ [-90.10, 29.95],
+ [-90.05, 29.95],
+ [-90.05, 29.98],
+ [-90.10, 29.98],
+ [-90.10, 29.95]
+ ]]
+ }
+}
+
+#
+# ============================================================
+# Orders — place and check orders
+# ============================================================
+#
+
+# Check order status (replace ORDER_ID)
+GET :skyfi-base/orders/ORDER_ID_HERE
+APIKey: :skyfi-key
+
+#
+
+# List recent orders
+GET :skyfi-base/orders
+APIKey: :skyfi-key
diff --git a/data/tutorial-api.rest b/data/tutorial-api.rest
new file mode 100644
index 00000000..6820cd87
--- /dev/null
+++ b/data/tutorial-api.rest
@@ -0,0 +1,140 @@
+# -*- restclient -*-
+#
+# REST API Tutorial — Free Public APIs
+#
+# QUICK START:
+# 1. Place cursor on any request line (GET, POST, etc.)
+# 2. C-c C-c — execute request, results appear below
+# 3. C-c C-p — jump to previous request
+# 4. C-c C-n — jump to next request
+# 5. TAB — hide/show response body
+#
+# SYNTAX BASICS:
+# - Lines starting with # are comments
+# - Blank line separates comment/header blocks from the request
+# - :var = value defines a variable, use it as :var in requests
+# - Requests: METHOD URL, then headers, then body after blank line
+#
+# VARIABLES:
+# Define once, reuse everywhere. Variables persist across requests
+# in the same buffer.
+
+:jsonplaceholder = https://jsonplaceholder.typicode.com
+:httpbin = https://httpbin.org
+
+#
+# ============================================================
+# JSONPlaceholder — fake REST API for testing
+# ============================================================
+#
+
+# GET a single post
+GET :jsonplaceholder/posts/1
+
+#
+
+# GET all posts by user 1
+GET :jsonplaceholder/posts?userId=1
+
+#
+
+# POST a new post (returns 201 with fake ID)
+POST :jsonplaceholder/posts
+Content-Type: application/json
+
+{
+ "title": "Testing from Emacs",
+ "body": "restclient.el is great for API exploration.",
+ "userId": 1
+}
+
+#
+
+# PUT (full update) — replaces post 1
+PUT :jsonplaceholder/posts/1
+Content-Type: application/json
+
+{
+ "id": 1,
+ "title": "Updated Title",
+ "body": "Updated body text.",
+ "userId": 1
+}
+
+#
+
+# PATCH (partial update) — only update the title
+PATCH :jsonplaceholder/posts/1
+Content-Type: application/json
+
+{
+ "title": "Just the title changed"
+}
+
+#
+
+# DELETE a post
+DELETE :jsonplaceholder/posts/1
+
+#
+# ============================================================
+# httpbin — HTTP echo service
+# ============================================================
+#
+
+# Echo back request headers (great for debugging auth)
+GET :httpbin/headers
+
+#
+
+# Send custom headers and see them echoed back
+GET :httpbin/headers
+X-Custom-Header: hello-from-emacs
+Accept: application/json
+
+#
+
+# Test Basic Auth (user: testuser, pass: testpass)
+# httpbin checks credentials and returns 200 or 401
+GET :httpbin/basic-auth/testuser/testpass
+Authorization: Basic dGVzdHVzZXI6dGVzdHBhc3M=
+
+#
+
+# See your external IP
+GET :httpbin/ip
+
+#
+
+# Test different status codes (change 418 to any HTTP status)
+GET :httpbin/status/418
+
+#
+
+# POST with form data
+POST :httpbin/post
+Content-Type: application/x-www-form-urlencoded
+
+name=Craig&tool=restclient
+
+#
+# ============================================================
+# Tips & Tricks
+# ============================================================
+#
+# JQ FILTERING (requires jq installed + restclient-jq):
+# Add -> jq-set-var :varname .path after a request to capture
+# a value from the JSON response into a restclient variable.
+#
+# MULTI-LINE BODIES:
+# Just write the JSON/XML body after a blank line. restclient
+# sends everything until the next # comment line.
+#
+# FILE ORGANIZATION:
+# Save related requests together in .rest files (e.g., per API
+# or per project). Open them with C-; R o.
+#
+# WORKFLOW:
+# 1. Start with C-; R n (scratch buffer) for quick experiments
+# 2. Save working requests to a .rest file for reuse
+# 3. Use variables for base URLs and auth tokens
diff --git a/init.el b/init.el
index c98f49fb..21e46309 100644
--- a/init.el
+++ b/init.el
@@ -125,6 +125,7 @@
;; -------------------------- AI Integration And Tools -------------------------
(require 'ai-config) ;; LLM integration with GPTel and friends
+(require 'restclient-config) ;; REST API client for API exploration
(with-eval-after-load 'gptel
(add-to-list 'load-path "~/.emacs.d/gptel-tools")
diff --git a/modules/restclient-config.el b/modules/restclient-config.el
new file mode 100644
index 00000000..650bb781
--- /dev/null
+++ b/modules/restclient-config.el
@@ -0,0 +1,104 @@
+;;; restclient-config.el --- REST API Client Configuration -*- lexical-binding: t; coding: utf-8; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+;; Integrates restclient.el for interactive API exploration from within Emacs.
+;;
+;; Write HTTP requests in plain text buffers, execute with C-c C-c, see
+;; results inline. Supports .rest files with variable substitution for
+;; reusable API templates.
+;;
+;; Includes SkyFi satellite imagery API integration with automatic key
+;; injection from authinfo.gpg (key never stored on disk).
+;;
+;; Keybindings (C-; R prefix):
+;; - C-; R n : New scratch *restclient* buffer
+;; - C-; R o : Open a .rest file (defaults to data/)
+;; - C-; R s : Open SkyFi API template
+
+;;; Code:
+
+;; --------------------------------- Constants ---------------------------------
+
+(defvar cj/restclient-data-dir (expand-file-name "data/" user-emacs-directory)
+ "Directory containing .rest API template files.")
+
+;; -------------------------------- use-package --------------------------------
+
+(use-package restclient
+ :ensure t
+ :defer t
+ :mode ("\\.rest\\'" . restclient-mode))
+
+(use-package restclient-jq
+ :ensure t
+ :if (executable-find "jq")
+ :after restclient)
+
+;; ----------------------------- Private Helpers -------------------------------
+
+(defun cj/restclient--inject-skyfi-key ()
+ "Replace the :skyfi-key variable line with the real key from authinfo.
+Only acts when the buffer is in `restclient-mode', the filename ends
+in \"skyfi-api.rest\", and a :skyfi-key line exists. If the auth
+lookup returns nil, leaves the buffer unchanged."
+ (when (and (derived-mode-p 'restclient-mode)
+ buffer-file-name
+ (string-match-p "skyfi-api\\.rest\\'" buffer-file-name))
+ (let ((key (condition-case nil
+ (cj/skyfi-api-key)
+ (error nil))))
+ (when key
+ (save-excursion
+ (goto-char (point-min))
+ (when (re-search-forward "^:skyfi-key = .*$" nil t)
+ (replace-match (format ":skyfi-key = %s" key))))))))
+
+;; ----------------------------- Public Functions ------------------------------
+
+(defun cj/skyfi-api-key ()
+ "Fetch SkyFi API key from authinfo.gpg."
+ (cj/auth-source-secret "app.skyfi.com" "apikey"))
+
+(defun cj/restclient-new-buffer ()
+ "Open a scratch *restclient* buffer in `restclient-mode'."
+ (interactive)
+ (let ((buf (get-buffer-create "*restclient*")))
+ (switch-to-buffer buf)
+ (unless (derived-mode-p 'restclient-mode)
+ (restclient-mode))))
+
+(defun cj/restclient-open-file ()
+ "Prompt for a .rest file to open, defaulting to the data/ directory."
+ (interactive)
+ (let ((file (read-file-name "Open .rest file: " cj/restclient-data-dir nil t nil
+ (lambda (f)
+ (or (file-directory-p f)
+ (string-match-p "\\.rest\\'" f))))))
+ (find-file file)))
+
+(defun cj/restclient-skyfi-buffer ()
+ "Open the SkyFi API template file.
+Runs the key-injection hook after opening."
+ (interactive)
+ (let ((skyfi-file (expand-file-name "skyfi-api.rest" cj/restclient-data-dir)))
+ (unless (file-exists-p skyfi-file)
+ (user-error "SkyFi template not found: %s" skyfi-file))
+ (find-file skyfi-file)
+ (cj/restclient--inject-skyfi-key)))
+
+;; -------------------------------- Keybindings --------------------------------
+
+(global-set-key (kbd "C-; R n") #'cj/restclient-new-buffer)
+(global-set-key (kbd "C-; R o") #'cj/restclient-open-file)
+(global-set-key (kbd "C-; R s") #'cj/restclient-skyfi-buffer)
+
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-; R" "REST client"
+ "C-; R n" "new scratch buffer"
+ "C-; R o" "open .rest file"
+ "C-; R s" "SkyFi API template"))
+
+(provide 'restclient-config)
+;;; restclient-config.el ends here
diff --git a/tests/test-restclient-config-inject-skyfi-key.el b/tests/test-restclient-config-inject-skyfi-key.el
new file mode 100644
index 00000000..d471b913
--- /dev/null
+++ b/tests/test-restclient-config-inject-skyfi-key.el
@@ -0,0 +1,147 @@
+;;; test-restclient-config-inject-skyfi-key.el --- Tests for cj/restclient--inject-skyfi-key -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/restclient--inject-skyfi-key function.
+;; Replaces the :skyfi-key placeholder in a restclient buffer with the real
+;; API key from authinfo. Tests cover Normal, Boundary, and Error cases.
+
+;;; Code:
+
+(when noninteractive
+ (package-initialize))
+
+(require 'ert)
+(require 'restclient-config)
+
+;; ---------------------------------------------------------------------------
+;; Test Helpers
+;; ---------------------------------------------------------------------------
+
+(defvar test-inject--fake-key "sk_test_fake_key_12345"
+ "Fake API key used in tests.")
+
+(defmacro test-inject--with-skyfi-buffer (content &rest body)
+ "Create a temp buffer with CONTENT simulating skyfi-api.rest, then run BODY.
+Sets buffer-file-name to skyfi-api.rest and activates restclient-mode.
+Binds `cj/skyfi-api-key' to return `test-inject--fake-key'."
+ (declare (indent 1))
+ `(with-temp-buffer
+ (insert ,content)
+ (setq buffer-file-name (expand-file-name "data/skyfi-api.rest" user-emacs-directory))
+ (restclient-mode)
+ (cl-letf (((symbol-function 'cj/skyfi-api-key)
+ (lambda () test-inject--fake-key)))
+ ,@body)))
+
+;; ---------------------------------------------------------------------------
+;;; Normal Cases
+;; ---------------------------------------------------------------------------
+
+(ert-deftest test-inject-skyfi-key-replaces-placeholder ()
+ "Replaces :skyfi-key = PLACEHOLDER with real key."
+ (test-inject--with-skyfi-buffer
+ ":skyfi-key = PLACEHOLDER\n#\nGET https://example.com\n"
+ (cj/restclient--inject-skyfi-key)
+ (goto-char (point-min))
+ (should (string-match-p (format ":skyfi-key = %s" test-inject--fake-key)
+ (buffer-string)))))
+
+(ert-deftest test-inject-skyfi-key-preserves-other-content ()
+ "Rest of buffer content unchanged after injection."
+ (let ((rest-content "# SkyFi API\nGET https://app.skyfi.com/platform-api/\nAPIKey: :skyfi-key\n"))
+ (test-inject--with-skyfi-buffer
+ (concat ":skyfi-key = PLACEHOLDER\n" rest-content)
+ (cj/restclient--inject-skyfi-key)
+ (should (string-match-p "# SkyFi API" (buffer-string)))
+ (should (string-match-p "APIKey: :skyfi-key" (buffer-string))))))
+
+(ert-deftest test-inject-skyfi-key-only-replaces-skyfi-key-line ()
+ "Does not modify other restclient variable lines."
+ (test-inject--with-skyfi-buffer
+ ":skyfi-key = PLACEHOLDER\n:other-var = keep-me\n"
+ (cj/restclient--inject-skyfi-key)
+ (should (string-match-p ":other-var = keep-me" (buffer-string)))))
+
+;; ---------------------------------------------------------------------------
+;;; Boundary Cases
+;; ---------------------------------------------------------------------------
+
+(ert-deftest test-inject-skyfi-key-no-key-line-no-error ()
+ "Buffer with no :skyfi-key line — no change, no error."
+ (test-inject--with-skyfi-buffer
+ "# Just comments\nGET https://example.com\n"
+ (let ((before (buffer-string)))
+ (cj/restclient--inject-skyfi-key)
+ (should (string= before (buffer-string))))))
+
+(ert-deftest test-inject-skyfi-key-already-has-value ()
+ "Buffer where :skyfi-key already has a real value — still replaces (idempotent)."
+ (test-inject--with-skyfi-buffer
+ ":skyfi-key = old_real_key_abc\n"
+ (cj/restclient--inject-skyfi-key)
+ (should (string-match-p (format ":skyfi-key = %s" test-inject--fake-key)
+ (buffer-string)))))
+
+(ert-deftest test-inject-skyfi-key-empty-buffer ()
+ "Empty buffer — no error."
+ (test-inject--with-skyfi-buffer ""
+ (cj/restclient--inject-skyfi-key)
+ (should (string= "" (buffer-string)))))
+
+(ert-deftest test-inject-skyfi-key-only-first-occurrence ()
+ "Multiple :skyfi-key lines — only first replaced."
+ (test-inject--with-skyfi-buffer
+ ":skyfi-key = PLACEHOLDER\n:skyfi-key = SECOND\n"
+ (cj/restclient--inject-skyfi-key)
+ (let ((content (buffer-string)))
+ (should (string-match-p (format ":skyfi-key = %s" test-inject--fake-key) content))
+ (should (string-match-p ":skyfi-key = SECOND" content)))))
+
+;; ---------------------------------------------------------------------------
+;;; Error Cases
+;; ---------------------------------------------------------------------------
+
+(ert-deftest test-inject-skyfi-key-wrong-mode-no-replacement ()
+ "Wrong major mode — no replacement happens."
+ (with-temp-buffer
+ (insert ":skyfi-key = PLACEHOLDER\n")
+ (setq buffer-file-name (expand-file-name "data/skyfi-api.rest" user-emacs-directory))
+ (fundamental-mode)
+ (let ((before (buffer-string)))
+ (cj/restclient--inject-skyfi-key)
+ (should (string= before (buffer-string))))))
+
+(ert-deftest test-inject-skyfi-key-wrong-filename-no-replacement ()
+ "Wrong filename — no replacement happens."
+ (with-temp-buffer
+ (insert ":skyfi-key = PLACEHOLDER\n")
+ (setq buffer-file-name "/tmp/other-file.rest")
+ (restclient-mode)
+ (let ((before (buffer-string)))
+ (cj/restclient--inject-skyfi-key)
+ (should (string= before (buffer-string))))))
+
+(ert-deftest test-inject-skyfi-key-no-filename-no-replacement ()
+ "No filename (scratch buffer) — no replacement happens."
+ (with-temp-buffer
+ (insert ":skyfi-key = PLACEHOLDER\n")
+ (restclient-mode)
+ (setq buffer-file-name nil)
+ (let ((before (buffer-string)))
+ (cj/restclient--inject-skyfi-key)
+ (should (string= before (buffer-string))))))
+
+(ert-deftest test-inject-skyfi-key-auth-returns-nil-no-error ()
+ "Auth-source returns nil — no error, no replacement."
+ (with-temp-buffer
+ (insert ":skyfi-key = PLACEHOLDER\n")
+ (setq buffer-file-name (expand-file-name "data/skyfi-api.rest" user-emacs-directory))
+ (restclient-mode)
+ (cl-letf (((symbol-function 'cj/skyfi-api-key)
+ (lambda () nil)))
+ (let ((before (buffer-string)))
+ (cj/restclient--inject-skyfi-key)
+ (should (string= before (buffer-string)))))))
+
+(provide 'test-restclient-config-inject-skyfi-key)
+;;; test-restclient-config-inject-skyfi-key.el ends here
diff --git a/tests/test-restclient-config-new-buffer.el b/tests/test-restclient-config-new-buffer.el
new file mode 100644
index 00000000..a89ec3a3
--- /dev/null
+++ b/tests/test-restclient-config-new-buffer.el
@@ -0,0 +1,62 @@
+;;; test-restclient-config-new-buffer.el --- Tests for cj/restclient-new-buffer -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/restclient-new-buffer function.
+;; Creates a scratch *restclient* buffer in restclient-mode.
+;; Covers Normal and Boundary cases.
+
+;;; Code:
+
+(when noninteractive
+ (package-initialize))
+
+(require 'ert)
+(require 'restclient-config)
+
+;;; Normal Cases
+
+(ert-deftest test-restclient-new-buffer-creates-buffer ()
+ "Creates a buffer named *restclient*."
+ (unwind-protect
+ (progn
+ (cj/restclient-new-buffer)
+ (should (get-buffer "*restclient*")))
+ (when (get-buffer "*restclient*")
+ (kill-buffer "*restclient*"))))
+
+(ert-deftest test-restclient-new-buffer-sets-mode ()
+ "Buffer is in restclient-mode."
+ (unwind-protect
+ (progn
+ (cj/restclient-new-buffer)
+ (with-current-buffer "*restclient*"
+ (should (eq major-mode 'restclient-mode))))
+ (when (get-buffer "*restclient*")
+ (kill-buffer "*restclient*"))))
+
+(ert-deftest test-restclient-new-buffer-switches-to-buffer ()
+ "Switches to the *restclient* buffer."
+ (unwind-protect
+ (progn
+ (cj/restclient-new-buffer)
+ (should (string= (buffer-name (current-buffer)) "*restclient*")))
+ (when (get-buffer "*restclient*")
+ (kill-buffer "*restclient*"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-restclient-new-buffer-reuses-existing ()
+ "Reuses existing *restclient* buffer instead of creating a duplicate."
+ (unwind-protect
+ (let ((buf (get-buffer-create "*restclient*")))
+ (with-current-buffer buf
+ (restclient-mode)
+ (insert "# existing content"))
+ (cj/restclient-new-buffer)
+ (should (eq (current-buffer) buf))
+ (should (string-match-p "existing content" (buffer-string))))
+ (when (get-buffer "*restclient*")
+ (kill-buffer "*restclient*"))))
+
+(provide 'test-restclient-config-new-buffer)
+;;; test-restclient-config-new-buffer.el ends here
diff --git a/tests/test-restclient-config-skyfi-buffer.el b/tests/test-restclient-config-skyfi-buffer.el
new file mode 100644
index 00000000..7685c77c
--- /dev/null
+++ b/tests/test-restclient-config-skyfi-buffer.el
@@ -0,0 +1,38 @@
+;;; test-restclient-config-skyfi-buffer.el --- Tests for cj/restclient-skyfi-buffer -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Unit tests for cj/restclient-skyfi-buffer function.
+;; Opens the SkyFi API template file.
+;; Covers Normal and Error cases.
+
+;;; Code:
+
+(when noninteractive
+ (package-initialize))
+
+(require 'ert)
+(require 'restclient-config)
+
+;;; Normal Cases
+
+(ert-deftest test-restclient-skyfi-buffer-opens-file ()
+ "Opens existing skyfi-api.rest file and switches to it."
+ (let ((skyfi-file (expand-file-name "data/skyfi-api.rest" user-emacs-directory)))
+ (when (file-exists-p skyfi-file)
+ (unwind-protect
+ (progn
+ (cj/restclient-skyfi-buffer)
+ (should (string-match-p "skyfi-api\\.rest"
+ (buffer-file-name (current-buffer)))))
+ (when-let ((buf (get-file-buffer skyfi-file)))
+ (kill-buffer buf))))))
+
+;;; Error Cases
+
+(ert-deftest test-restclient-skyfi-buffer-missing-file-signals-error ()
+ "Signals user-error when skyfi-api.rest does not exist."
+ (let ((cj/restclient-data-dir "/tmp/nonexistent-restclient-test-dir/"))
+ (should-error (cj/restclient-skyfi-buffer) :type 'user-error)))
+
+(provide 'test-restclient-config-skyfi-buffer)
+;;; test-restclient-config-skyfi-buffer.el ends here
diff --git a/todo.org b/todo.org
index 71906cb5..dff634f6 100644
--- a/todo.org
+++ b/todo.org
@@ -320,6 +320,30 @@ Changes in progress (modules/auth-config.el):
- Use external pinentry (pinentry-dmenu) in GUI
- Requires env-terminal-p from host-environment module
+** API & Web Services
+
+*** VERIFY [#B] Test and review restclient.el implementation
+
+Test the new REST API client integration in a running Emacs session.
+
+**Keybindings to test:**
+- C-; R n — new scratch *restclient* buffer (should open in restclient-mode)
+- C-; R o — open .rest file (should default to data/ directory)
+- C-; R s — open SkyFi template (should auto-inject API key from authinfo)
+
+**Functional tests:**
+1. Open tutorial-api.rest, run JSONPlaceholder GET (C-c C-c) — verify response inline
+2. Run POST example — verify 201 response with fake ID
+3. Run httpbin header echo — verify custom headers echoed back
+4. Navigate between requests with C-c C-n / C-c C-p
+5. Test jq filtering (requires jq installed): restclient-jq loaded?
+6. Open scratch buffer (C-; R n), type a request manually, execute
+7. which-key shows "REST client" menu under C-; R
+
+**SkyFi key injection (if authinfo entry exists):**
+- C-; R s should replace :skyfi-key = PLACEHOLDER with real key
+- Key should NOT be written to disk (verify file still shows PLACEHOLDER)
+
* Emacs Resolved
** DONE [#B] Investigate missing yasnippet configuration
CLOSED: [2026-02-16 Mon]