aboutsummaryrefslogtreecommitdiff
path: root/tests/test-convert-org-contacts-birthdays.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-11-18 11:13:39 -0600
committerCraig Jennings <c@cjennings.net>2025-11-18 11:13:39 -0600
commit4835fadabf243b33fb78557e45428055675e7300 (patch)
tree2e8ccd7995ffa6f6dd99943d829fb8b7e3112874 /tests/test-convert-org-contacts-birthdays.el
downloadchime-4835fadabf243b33fb78557e45428055675e7300.tar.gz
chime-4835fadabf243b33fb78557e45428055675e7300.zip
changed repositories
Diffstat (limited to 'tests/test-convert-org-contacts-birthdays.el')
-rw-r--r--tests/test-convert-org-contacts-birthdays.el674
1 files changed, 674 insertions, 0 deletions
diff --git a/tests/test-convert-org-contacts-birthdays.el b/tests/test-convert-org-contacts-birthdays.el
new file mode 100644
index 0000000..960a998
--- /dev/null
+++ b/tests/test-convert-org-contacts-birthdays.el
@@ -0,0 +1,674 @@
+;;; test-convert-org-contacts-birthdays.el --- Tests for convert-org-contacts-birthdays.el -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Craig Jennings
+
+;; Author: Craig Jennings <c@cjennings.net>
+
+;; This program 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.
+
+;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Unit tests for convert-org-contacts-birthdays.el
+;; Tests the conversion of org-contacts BIRTHDAY properties to plain timestamps.
+
+;;; Code:
+
+;; Initialize package system for batch mode
+(when noninteractive
+ (package-initialize))
+
+(require 'ert)
+
+;; Load the conversion utility from parent directory
+(load (expand-file-name "../convert-org-contacts-birthdays.el") nil t)
+
+;;; Tests for birthday parsing
+
+(ert-deftest test-convert-normal-parse-birthday-with-year-returns-year-month-day ()
+ "Test parsing YYYY-MM-DD format returns (YEAR MONTH DAY)."
+ (let ((result (chime--parse-birthday "2000-03-15")))
+ (should (equal result '(2000 3 15)))))
+
+(ert-deftest test-convert-normal-parse-birthday-without-year-returns-nil-month-day ()
+ "Test parsing MM-DD format returns (nil MONTH DAY)."
+ (let ((result (chime--parse-birthday "03-15")))
+ (should (equal result '(nil 3 15)))))
+
+(ert-deftest test-convert-normal-parse-birthday-december-returns-correct-month ()
+ "Test parsing December date returns month 12."
+ (let ((result (chime--parse-birthday "1985-12-25")))
+ (should (equal result '(1985 12 25)))))
+
+(ert-deftest test-convert-normal-parse-birthday-january-returns-correct-month ()
+ "Test parsing January date returns month 1."
+ (let ((result (chime--parse-birthday "01-01")))
+ (should (equal result '(nil 1 1)))))
+
+(ert-deftest test-convert-error-parse-birthday-invalid-format-signals-error ()
+ "Test parsing invalid format signals user-error."
+ (should-error (chime--parse-birthday "2000/03/15") :type 'user-error)
+ (should-error (chime--parse-birthday "March 15, 2000") :type 'user-error)
+ (should-error (chime--parse-birthday "15-03-2000") :type 'user-error))
+
+;;; Tests for birthday formatting
+
+(ert-deftest test-convert-normal-format-birthday-timestamp-with-year-returns-yearly-repeater ()
+ "Test formatting with year returns yearly repeating timestamp."
+ (let ((result (chime--format-birthday-timestamp 2000 3 15)))
+ ;; Should contain year, date, and +1y repeater
+ (should (string-match-p "<2000-03-15 [A-Za-z]\\{3\\} \\+1y>" result))))
+
+(ert-deftest test-convert-normal-format-birthday-timestamp-without-year-uses-current-year ()
+ "Test formatting without year uses current year."
+ (let* ((current-year (nth 5 (decode-time)))
+ (result (chime--format-birthday-timestamp nil 3 15)))
+ ;; Should contain current year
+ (should (string-match-p (format "<%d-03-15 [A-Za-z]\\{3\\} \\+1y>" current-year) result))))
+
+(ert-deftest test-convert-boundary-format-birthday-timestamp-leap-day-returns-valid-timestamp ()
+ "Test formatting February 29 (leap day) returns valid timestamp."
+ (let ((result (chime--format-birthday-timestamp 2000 2 29)))
+ (should (string-match-p "<2000-02-29 [A-Za-z]\\{3\\} \\+1y>" result))))
+
+(ert-deftest test-convert-boundary-format-birthday-timestamp-december-31-returns-valid-timestamp ()
+ "Test formatting December 31 returns valid timestamp."
+ (let ((result (chime--format-birthday-timestamp 2000 12 31)))
+ (should (string-match-p "<2000-12-31 [A-Za-z]\\{3\\} \\+1y>" result))))
+
+;;; Tests for backup file creation
+
+(ert-deftest test-convert-normal-backup-file-creates-timestamped-backup ()
+ "Test that backup file is created with timestamp."
+ (let* ((temp-file (make-temp-file "test-contacts"))
+ (backup-path nil))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "* Test Contact\n:PROPERTIES:\n:BIRTHDAY: 2000-01-01\n:END:\n"))
+ (setq backup-path (chime--backup-contacts-file temp-file))
+ (should (file-exists-p backup-path))
+ (should (string-match-p "\\.backup-[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}-[0-9]\\{6\\}$" backup-path)))
+ (when (file-exists-p temp-file) (delete-file temp-file))
+ (when (and backup-path (file-exists-p backup-path)) (delete-file backup-path)))))
+
+(ert-deftest test-convert-normal-backup-file-preserves-content ()
+ "Test that backup file contains exact copy of original."
+ (let* ((temp-file (make-temp-file "test-contacts"))
+ (content "* Test Contact\n:PROPERTIES:\n:BIRTHDAY: 2000-01-01\n:END:\n")
+ (backup-path nil))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file (insert content))
+ (setq backup-path (chime--backup-contacts-file temp-file))
+ (with-temp-buffer
+ (insert-file-contents backup-path)
+ (should (string= (buffer-string) content))))
+ (when (file-exists-p temp-file) (delete-file temp-file))
+ (when (and backup-path (file-exists-p backup-path)) (delete-file backup-path)))))
+
+;;; Tests for timestamp insertion
+
+(ert-deftest test-convert-normal-insert-timestamp-after-drawer-adds-timestamp ()
+ "Test that timestamp is inserted after properties drawer."
+ (with-temp-buffer
+ (org-mode)
+ (insert "* Test Contact\n:PROPERTIES:\n:EMAIL: test@example.com\n:BIRTHDAY: 2000-03-15\n:END:\n")
+ (goto-char (point-min))
+ (re-search-forward "^\\* ")
+ (beginning-of-line)
+ (chime--insert-birthday-timestamp-after-drawer "2000-03-15")
+ (should (string-match-p "<2000-03-15 [A-Za-z]\\{3\\} \\+1y>" (buffer-string)))))
+
+(ert-deftest test-convert-normal-insert-timestamp-preserves-properties ()
+ "Test that inserting timestamp doesn't modify properties."
+ (with-temp-buffer
+ (org-mode)
+ (insert "* Test Contact\n:PROPERTIES:\n:EMAIL: test@example.com\n:BIRTHDAY: 2000-03-15\n:END:\n")
+ (goto-char (point-min))
+ (re-search-forward "^\\* ")
+ (beginning-of-line)
+ (chime--insert-birthday-timestamp-after-drawer "2000-03-15")
+ (should (string-match-p ":EMAIL: test@example.com" (buffer-string)))
+ (should (string-match-p ":BIRTHDAY: 2000-03-15" (buffer-string)))))
+
+(ert-deftest test-convert-boundary-insert-timestamp-without-year-uses-current ()
+ "Test that MM-DD format uses current year."
+ (let ((current-year (nth 5 (decode-time))))
+ (with-temp-buffer
+ (org-mode)
+ (insert "* Test Contact\n:PROPERTIES:\n:BIRTHDAY: 03-15\n:END:\n")
+ (goto-char (point-min))
+ (re-search-forward "^\\* ")
+ (beginning-of-line)
+ (chime--insert-birthday-timestamp-after-drawer "03-15")
+ (should (string-match-p (format "<%d-03-15" current-year) (buffer-string))))))
+
+;;; Tests for contact entry processing
+
+(ert-deftest test-convert-normal-process-contact-with-birthday-returns-true ()
+ "Test that processing contact with birthday returns t."
+ (with-temp-buffer
+ (org-mode)
+ (insert "* Test Contact\n:PROPERTIES:\n:BIRTHDAY: 2000-03-15\n:END:\n")
+ (goto-char (point-min))
+ (re-search-forward "^\\* ")
+ (beginning-of-line)
+ (should (eq t (chime--process-contact-entry)))))
+
+(ert-deftest test-convert-normal-process-contact-without-birthday-returns-nil ()
+ "Test that processing contact without birthday returns nil."
+ (with-temp-buffer
+ (org-mode)
+ (insert "* Test Contact\n:PROPERTIES:\n:EMAIL: test@example.com\n:END:\n")
+ (goto-char (point-min))
+ (re-search-forward "^\\* ")
+ (beginning-of-line)
+ (should (null (chime--process-contact-entry)))))
+
+;;; Tests for file conversion
+
+(ert-deftest test-convert-normal-convert-file-multiple-contacts-returns-correct-count ()
+ "Test converting file with multiple contacts returns correct count."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "* Alice Anderson\n:PROPERTIES:\n:BIRTHDAY: 1985-03-15\n:END:\n\n")
+ (insert "* Bob Baker\n:PROPERTIES:\n:BIRTHDAY: 1990-07-22\n:END:\n\n")
+ (insert "* Carol Chen\n:PROPERTIES:\n:BIRTHDAY: 12-25\n:END:\n"))
+ (let* ((result (chime--convert-contacts-file-in-place temp-file))
+ (count (car result))
+ (backup-file (cdr result)))
+ (should (= count 3))
+ (when (file-exists-p backup-file) (delete-file backup-file))))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+(ert-deftest test-convert-boundary-convert-file-no-contacts-returns-zero ()
+ "Test converting file with no contacts returns zero."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "#+TITLE: Empty File\n\nSome text but no contacts.\n"))
+ (let* ((result (chime--convert-contacts-file-in-place temp-file))
+ (count (car result))
+ (backup-file (cdr result)))
+ (should (= count 0))
+ (when (file-exists-p backup-file) (delete-file backup-file))))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+(ert-deftest test-convert-boundary-convert-file-contacts-without-birthdays-returns-zero ()
+ "Test converting file with contacts but no birthdays returns zero."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "* Alice Anderson\n:PROPERTIES:\n:EMAIL: alice@example.com\n:END:\n\n")
+ (insert "* Bob Baker\n:PROPERTIES:\n:EMAIL: bob@example.com\n:END:\n"))
+ (let* ((result (chime--convert-contacts-file-in-place temp-file))
+ (count (car result))
+ (backup-file (cdr result)))
+ (should (= count 0))
+ (when (file-exists-p backup-file) (delete-file backup-file))))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+(ert-deftest test-convert-boundary-convert-file-contact-without-properties-drawer ()
+ "Test converting contact without properties drawer is skipped."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "* Alice Anderson\nJust some text, no properties.\n"))
+ (let* ((result (chime--convert-contacts-file-in-place temp-file))
+ (count (car result))
+ (backup-file (cdr result)))
+ (should (= count 0))
+ (when (file-exists-p backup-file) (delete-file backup-file))))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+(ert-deftest test-convert-boundary-convert-file-preserves-existing-timestamps ()
+ "Test that existing timestamps in contact body are not modified."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "* Alice Anderson\n:PROPERTIES:\n:BIRTHDAY: 1985-03-15\n:END:\n")
+ (insert "Meeting scheduled <2025-11-15 Sat>\n"))
+ (chime--convert-contacts-file-in-place temp-file)
+ (with-temp-buffer
+ (insert-file-contents temp-file)
+ (let ((content (buffer-string)))
+ ;; Should have birthday timestamp
+ (should (string-match-p "<1985-03-15 [A-Za-z]\\{3\\} \\+1y>" content))
+ ;; Should still have meeting timestamp
+ (should (string-match-p "<2025-11-15 Sat>" content))
+ ;; Should have exactly 2 timestamps
+ (should (= 2 (how-many "<[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}" (point-min) (point-max)))))))
+ (when (file-exists-p temp-file) (delete-file temp-file))
+ (let ((backup (concat temp-file ".backup-")))
+ (dolist (file (directory-files (file-name-directory temp-file) t (regexp-quote (file-name-nondirectory backup))))
+ (delete-file file))))))
+
+(ert-deftest test-convert-normal-convert-file-timestamp-inserted-after-drawer ()
+ "Test that timestamp is inserted immediately after properties drawer."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "* Alice Anderson\n:PROPERTIES:\n:BIRTHDAY: 1985-03-15\n:END:\n")
+ (insert "Some notes about Alice.\n"))
+ (chime--convert-contacts-file-in-place temp-file)
+ (with-temp-buffer
+ (insert-file-contents temp-file)
+ (let ((content (buffer-string)))
+ ;; Timestamp should come after :END: but before notes
+ (should (string-match-p ":END:\n<1985-03-15 [A-Za-z]\\{3\\} \\+1y>\nSome notes" content)))))
+ (when (file-exists-p temp-file) (delete-file temp-file))
+ (let ((backup (concat temp-file ".backup-")))
+ (dolist (file (directory-files (file-name-directory temp-file) t (regexp-quote (file-name-nondirectory backup))))
+ (delete-file file))))))
+
+;;; Tests for year extraction
+
+(ert-deftest test-convert-normal-extract-birthday-year-with-year-returns-year ()
+ "Test extracting year from YYYY-MM-DD returns the year."
+ (let ((result (chime--extract-birthday-year "2000-03-15")))
+ (should (equal result 2000))))
+
+(ert-deftest test-convert-normal-extract-birthday-year-without-year-returns-nil ()
+ "Test extracting year from MM-DD returns nil."
+ (let ((result (chime--extract-birthday-year "03-15")))
+ (should (null result))))
+
+(ert-deftest test-convert-boundary-extract-birthday-year-very-old-date-returns-year ()
+ "Test extracting year from very old date (1900s) returns the year."
+ (let ((result (chime--extract-birthday-year "1920-01-01")))
+ (should (equal result 1920))))
+
+(ert-deftest test-convert-boundary-extract-birthday-year-future-date-returns-year ()
+ "Test extracting year from future date returns the year."
+ (let ((result (chime--extract-birthday-year "2100-12-31")))
+ (should (equal result 2100))))
+
+;;; Edge Case Tests
+
+(ert-deftest test-convert-edge-duplicate-timestamps-not-added ()
+ "Test that running conversion twice doesn't add duplicate timestamps."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "* Alice Anderson\n:PROPERTIES:\n:BIRTHDAY: 2000-01-01\n:END:\n"))
+
+ ;; Run conversion once
+ (let ((backup1 (cdr (chime--convert-contacts-file-in-place temp-file))))
+ ;; Clean up first backup to avoid file-already-exists error
+ (when (file-exists-p backup1) (delete-file backup1))
+
+ ;; Run conversion again
+ (let ((backup2 (cdr (chime--convert-contacts-file-in-place temp-file))))
+
+ (with-temp-buffer
+ (insert-file-contents temp-file)
+ (let ((content (buffer-string)))
+ ;; Should have exactly one birthday timestamp
+ (should (= 1 (how-many "<2000-01-01 [A-Za-z]\\{3\\} \\+1y>" (point-min) (point-max))))))
+
+ (when (file-exists-p backup2) (delete-file backup2)))))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+(ert-deftest test-convert-edge-malformed-properties-drawer-missing-end ()
+ "Test handling of properties drawer missing :END:."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "* Alice Anderson\n:PROPERTIES:\n:BIRTHDAY: 2000-01-01\nMissing END tag\n"))
+
+ (let* ((result (chime--convert-contacts-file-in-place temp-file))
+ (count (car result))
+ (backup-file (cdr result)))
+ ;; Should handle gracefully (likely returns 0 if drawer malformed)
+ (should (numberp count))
+ (when (file-exists-p backup-file) (delete-file backup-file))))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+(ert-deftest test-convert-edge-empty-birthday-property ()
+ "Test handling of empty BIRTHDAY property value."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "* Alice Anderson\n:PROPERTIES:\n:BIRTHDAY: \n:END:\n"))
+
+ (let* ((result (chime--convert-contacts-file-in-place temp-file))
+ (count (car result))
+ (backup-file (cdr result)))
+ ;; Should skip empty birthday
+ (should (= count 0))
+ (when (file-exists-p backup-file) (delete-file backup-file))))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+(ert-deftest test-convert-edge-whitespace-in-birthday ()
+ "Test handling of whitespace in BIRTHDAY property."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "* Alice Anderson\n:PROPERTIES:\n:BIRTHDAY: 2000-01-01 \n:END:\n"))
+
+ (chime--convert-contacts-file-in-place temp-file)
+
+ (with-temp-buffer
+ (insert-file-contents temp-file)
+ (let ((content (buffer-string)))
+ ;; Should handle whitespace and create timestamp
+ (should (string-match-p "<2000-01-01 [A-Za-z]\\{3\\} \\+1y>" content)))))
+ (when (file-exists-p temp-file) (delete-file temp-file))
+ (let ((backup (concat temp-file ".backup-")))
+ (dolist (file (directory-files (file-name-directory temp-file) t (regexp-quote (file-name-nondirectory backup))))
+ (delete-file file))))))
+
+(ert-deftest test-convert-edge-very-large-file ()
+ "Test conversion of file with many contacts (performance test)."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (dotimes (i 100)
+ (insert (format "* Contact %d\n:PROPERTIES:\n:BIRTHDAY: 1990-01-01\n:END:\n\n" i))))
+
+ (let* ((result (chime--convert-contacts-file-in-place temp-file))
+ (count (car result))
+ (backup-file (cdr result)))
+ (should (= count 100))
+ (when (file-exists-p backup-file) (delete-file backup-file))))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+;;; Integration Tests
+
+(ert-deftest test-convert-integration-full-workflow-with-realistic-contacts ()
+ "Integration test with realistic contacts file structure."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "#+TITLE: My Contacts\n#+STARTUP: overview\n\n")
+ (insert "* Alice Anderson\n")
+ (insert ":PROPERTIES:\n")
+ (insert ":EMAIL: alice@example.com\n")
+ (insert ":PHONE: 555-0101\n")
+ (insert ":BIRTHDAY: 1985-03-15\n")
+ (insert ":ADDRESS: 123 Main St\n")
+ (insert ":END:\n")
+ (insert "Met at conference 2023.\n\n")
+ (insert "* Bob Baker\n")
+ (insert ":PROPERTIES:\n")
+ (insert ":EMAIL: bob@example.com\n")
+ (insert ":BIRTHDAY: 1990-07-22\n")
+ (insert ":END:\n\n")
+ (insert "* Carol Chen\n")
+ (insert ":PROPERTIES:\n")
+ (insert ":EMAIL: carol@example.com\n")
+ (insert ":END:\n")
+ (insert "No birthday for Carol.\n"))
+
+ (let* ((result (chime--convert-contacts-file-in-place temp-file))
+ (count (car result))
+ (backup-file (cdr result)))
+
+ ;; Verify conversion count
+ (should (= count 2))
+
+ ;; Verify backup exists
+ (should (file-exists-p backup-file))
+
+ ;; Verify converted file content
+ (with-temp-buffer
+ (insert-file-contents temp-file)
+ (let ((content (buffer-string)))
+ ;; File header preserved
+ (should (string-search "#+TITLE: My Contacts" content))
+
+ ;; Alice's properties preserved
+ (should (string-search ":EMAIL: alice@example.com" content))
+ (should (string-search ":ADDRESS: 123 Main St" content))
+ (should (string-search ":BIRTHDAY: 1985-03-15" content))
+
+ ;; Alice's birthday timestamp added
+ (should (string-match-p "<1985-03-15 [A-Za-z]\\{3\\} \\+1y>" content))
+
+ ;; Alice's notes preserved
+ (should (string-search "Met at conference 2023" content))
+
+ ;; Bob's birthday timestamp added
+ (should (string-match-p "<1990-07-22 [A-Za-z]\\{3\\} \\+1y>" content))
+
+ ;; Carol has no timestamp added
+ (goto-char (point-min))
+ (re-search-forward "\\* Carol Chen")
+ (let ((carol-section-start (point))
+ (carol-section-end (or (re-search-forward "^\\* " nil t) (point-max))))
+ (goto-char carol-section-start)
+ (should-not (re-search-forward "<[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}" carol-section-end t)))))
+
+ ;; Clean up backup
+ (when (file-exists-p backup-file) (delete-file backup-file))))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+(ert-deftest test-convert-integration-mixed-birthday-formats ()
+ "Integration test with both YYYY-MM-DD and MM-DD formats."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org"))
+ (current-year (nth 5 (decode-time))))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "* Alice Anderson\n:PROPERTIES:\n:BIRTHDAY: 1985-03-15\n:END:\n\n")
+ (insert "* Bob Baker\n:PROPERTIES:\n:BIRTHDAY: 07-04\n:END:\n"))
+
+ (chime--convert-contacts-file-in-place temp-file)
+
+ (with-temp-buffer
+ (insert-file-contents temp-file)
+ (let ((content (buffer-string)))
+ ;; Alice has year from property
+ (should (string-match-p "<1985-03-15" content))
+ ;; Bob uses current year
+ (should (string-match-p (format "<%d-07-04" current-year) content)))))
+ (when (file-exists-p temp-file) (delete-file temp-file))
+ (let ((backup (concat temp-file ".backup-")))
+ (dolist (file (directory-files (file-name-directory temp-file) t (regexp-quote (file-name-nondirectory backup))))
+ (delete-file file))))))
+
+(ert-deftest test-convert-integration-file-with-existing-content-preserved ()
+ "Integration test verifying all existing content is preserved."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "#+TITLE: Contacts\n")
+ (insert "#+FILETAGS: :contacts:\n\n")
+ (insert "Some introductory text.\n\n")
+ (insert "* Alice Anderson\n")
+ (insert ":PROPERTIES:\n")
+ (insert ":BIRTHDAY: 2000-01-01\n")
+ (insert ":END:\n")
+ (insert "** Notes\n")
+ (insert "Some nested content.\n")
+ (insert "*** Deep nested\n")
+ (insert "More content.\n\n")
+ (insert "* Final Section\n")
+ (insert "Closing remarks.\n"))
+
+ (chime--convert-contacts-file-in-place temp-file)
+
+ (with-temp-buffer
+ (insert-file-contents temp-file)
+ (let ((content (buffer-string)))
+ ;; All content preserved
+ (should (string-search "Some introductory text" content))
+ (should (string-search "** Notes" content))
+ (should (string-search "Some nested content" content))
+ (should (string-search "*** Deep nested" content))
+ (should (string-search "Closing remarks" content))
+ (should (string-search "#+FILETAGS: :contacts:" content)))))
+ (when (file-exists-p temp-file) (delete-file temp-file))
+ (let ((backup (concat temp-file ".backup-")))
+ (dolist (file (directory-files (file-name-directory temp-file) t (regexp-quote (file-name-nondirectory backup))))
+ (delete-file file))))))
+
+;;; Workflow Tests (Backup → Convert → Verify)
+
+(ert-deftest test-convert-workflow-backup-created-before-modification ()
+ "Test that backup file is created before original is modified."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org"))
+ (original-content "* Alice Anderson\n:PROPERTIES:\n:BIRTHDAY: 2000-01-01\n:END:\n"))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert original-content))
+
+ (let* ((result (chime--convert-contacts-file-in-place temp-file))
+ (backup-file (cdr result)))
+
+ ;; Backup exists
+ (should (file-exists-p backup-file))
+
+ ;; Backup contains original content (unmodified)
+ (with-temp-buffer
+ (insert-file-contents backup-file)
+ (should (string= (buffer-string) original-content)))
+
+ ;; Original file is modified
+ (with-temp-buffer
+ (insert-file-contents temp-file)
+ (should (string-match-p "<2000-01-01 [A-Za-z]\\{3\\} \\+1y>" (buffer-string))))
+
+ (when (file-exists-p backup-file) (delete-file backup-file))))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+(ert-deftest test-convert-workflow-backup-content-matches-original ()
+ "Test that backup file contains exact copy of original content."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org"))
+ (original-content "#+TITLE: Contacts\n\n* Alice\n:PROPERTIES:\n:BIRTHDAY: 1985-03-15\n:END:\n"))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert original-content))
+
+ (let* ((result (chime--convert-contacts-file-in-place temp-file))
+ (backup-file (cdr result)))
+
+ ;; Backup content matches original
+ (with-temp-buffer
+ (insert-file-contents backup-file)
+ (should (string= (buffer-string) original-content)))
+
+ (when (file-exists-p backup-file) (delete-file backup-file))))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+(ert-deftest test-convert-workflow-original-modified-with-timestamps ()
+ "Test that original file is modified with timestamps after conversion."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "* Alice\n:PROPERTIES:\n:BIRTHDAY: 1985-03-15\n:END:\n")
+ (insert "* Bob\n:PROPERTIES:\n:BIRTHDAY: 1990-07-22\n:END:\n"))
+
+ ;; Get original content
+ (let ((original-content nil))
+ (with-temp-buffer
+ (insert-file-contents temp-file)
+ (setq original-content (buffer-string)))
+
+ ;; Should not have timestamps before conversion
+ (should-not (string-match-p "<[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} [A-Za-z]\\{3\\} \\+1y>" original-content))
+
+ ;; Convert
+ (let* ((result (chime--convert-contacts-file-in-place temp-file))
+ (count (car result))
+ (backup-file (cdr result)))
+
+ (should (= count 2))
+
+ ;; Modified content should have timestamps
+ (with-temp-buffer
+ (insert-file-contents temp-file)
+ (let ((modified-content (buffer-string)))
+ (should (string-match-p "<1985-03-15 [A-Za-z]\\{3\\} \\+1y>" modified-content))
+ (should (string-match-p "<1990-07-22 [A-Za-z]\\{3\\} \\+1y>" modified-content))))
+
+ (when (file-exists-p backup-file) (delete-file backup-file)))))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+(ert-deftest test-convert-workflow-rollback-via-backup ()
+ "Test that conversion can be rolled back by restoring from backup."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org"))
+ (original-content "* Alice\n:PROPERTIES:\n:BIRTHDAY: 2000-01-01\n:END:\n"))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert original-content))
+
+ ;; Convert
+ (let* ((result (chime--convert-contacts-file-in-place temp-file))
+ (backup-file (cdr result)))
+
+ ;; Verify file was modified
+ (with-temp-buffer
+ (insert-file-contents temp-file)
+ (should (string-match-p "<2000-01-01 [A-Za-z]\\{3\\} \\+1y>" (buffer-string))))
+
+ ;; Rollback by copying backup over original
+ (copy-file backup-file temp-file t)
+
+ ;; Verify rollback worked
+ (with-temp-buffer
+ (insert-file-contents temp-file)
+ (let ((content (buffer-string)))
+ (should (string= content original-content))
+ (should-not (string-match-p "<2000-01-01 [A-Za-z]\\{3\\} \\+1y>" content))))
+
+ (when (file-exists-p backup-file) (delete-file backup-file))))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+(ert-deftest test-convert-workflow-multiple-backups-distinct-timestamps ()
+ "Test that multiple conversions create backups with distinct timestamps."
+ (let ((temp-file (make-temp-file "test-contacts" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file temp-file
+ (insert "* Alice\n:PROPERTIES:\n:BIRTHDAY: 2000-01-01\n:END:\n"))
+
+ ;; First conversion
+ (let ((backup1 (cdr (chime--convert-contacts-file-in-place temp-file))))
+ ;; Small delay to ensure different timestamp
+ (sleep-for 1)
+ ;; Second conversion
+ (let ((backup2 (cdr (chime--convert-contacts-file-in-place temp-file))))
+
+ ;; Backup files should have different names
+ (should-not (string= backup1 backup2))
+
+ ;; Both should exist
+ (should (file-exists-p backup1))
+ (should (file-exists-p backup2))
+
+ (when (file-exists-p backup1) (delete-file backup1))
+ (when (file-exists-p backup2) (delete-file backup2)))))
+ (when (file-exists-p temp-file) (delete-file temp-file)))))
+
+(provide 'test-convert-org-contacts-birthdays)
+;;; test-convert-org-contacts-birthdays.el ends here