diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d652166 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,39 @@ +name: Test Application + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + + - name: Run tests + run: | + python -m unittest discover -s tests -v + + - name: Check shell script syntax (Linux) + if: runner.os == 'Linux' + run: bash -n package_linux.sh + + - name: Check PowerShell script syntax (Windows) + if: runner.os == 'Windows' + run: | + powershell -NoProfile -Command "& { Set-StrictMode -Version Latest; .\package_win.ps1 -WhatIf }" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dff9cff --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.agents +__pycache__ +*.exe +*.out +*.bin +.claude +dist +build \ No newline at end of file diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..0427947 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,31 @@ +# Testing the Terminal Notes Application + +## Running Tests Locally + +To run the test suite, execute: + +```bash +python -m unittest discover -s tests -v +``` + +Or to run a specific test file: + +```bash +python -m unittest tests.test_terminalnotesapp -v +``` + +## Test Coverage + +The test suite covers: + +- Note loading and saving functionality +- Note creation, modification, and deletion +- Import and export to JSON files +- Edge cases and error handling +- User input validation +- File handling scenarios + +## GitHub Actions + +This repository includes GitHub Actions workflow for continuous integration. +See `.github/workflows/test.yml` for details. \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..ce08f05 --- /dev/null +++ b/main.py @@ -0,0 +1,223 @@ +import json +import os +import sys +import time + +NOTES_FILE = "notes.json" + +def load_notes(): + """Load notes from JSON file.""" + if not os.path.exists(NOTES_FILE): + return [] + with open(NOTES_FILE, 'r') as f: + try: + return json.load(f) + except json.JSONDecodeError: + return [] + +def save_notes(notes): + """Save notes to JSON file.""" + with open(NOTES_FILE, 'w') as f: + json.dump(notes, f, indent=2) + + +def print_notes(): + """Print all notes.""" + notes = load_notes() + if not notes: + print("No notes found.") + return + print("\n===== YOUR NOTES =====") + for note in notes: + print(f"ID: {note['id']}") + print(f"Content: {note['content']}") + print("-" * 40) + print(f"Total notes: {len(notes)}\n") + + +def export_notes_json(): + """Export notes to a JSON file.""" + notes = load_notes() + if not notes: + print("No notes to export.") + return + filename = input("Enter filename to export to (default: notes_export.json): ").strip() + if not filename: + filename = "notes_export.json" + if not filename.endswith('.json'): + filename += '.json' + try: + with open(filename, 'w') as f: + json.dump(notes, f, indent=2) + print(f"Notes exported to {filename}") + except Exception as e: + print(f"Error exporting notes: {e}") + + +def import_notes_json(): + """Import notes from a JSON file.""" + filename = input("Enter filename to import from: ").strip() + if not filename: + print("No filename provided.") + return + if not os.path.exists(filename): + print(f"File '{filename}' not found.") + return + try: + with open(filename, 'r') as f: + imported_notes = json.load(f) + if not isinstance(imported_notes, list): + print("Invalid file format: expected a JSON array.") + return + # Validate each entry has id and content + valid_notes = [] + for i, item in enumerate(imported_notes): + if not isinstance(item, dict) or 'id' not in item or 'content' not in item: + print(f"Invalid entry at index {i}: skipping") + continue + valid_notes.append(item) + if not valid_notes: + print("No valid notes found in file.") + return + # Ask user whether to merge or replace + current_notes = load_notes() + print(f"Found {len(valid_notes)} valid notes in {filename}") + print(f"Currently have {len(current_notes)} notes.") + choice = input("Choose action: (m)erge with existing, (r)eplace existing: ").strip().lower() + if choice == 'r' or choice == 'replace': + notes_to_save = valid_notes + # Reassign IDs to avoid conflicts + timestamp_base = int(time.time() * 1000) + for i, note in enumerate(notes_to_save): + note['id'] = timestamp_base + i + action = "replaced" + else: # merge or any other input + notes_to_save = current_notes + valid_notes + # Adjust IDs for imported notes to avoid conflicts + if current_notes: + max_id = max(note['id'] for note in current_notes) + for note in valid_notes: + note['id'] = max_id + 1 + valid_notes.index(note) + action = "merged" + save_notes(notes_to_save) + print(f"Notes {action} successfully. Total notes now: {len(notes_to_save)}") + except json.JSONDecodeError as e: + print(f"Invalid JSON in file: {e}") + except Exception as e: + print(f"Error importing notes: {e}") + +def create_note(): + """Create a new note.""" + notes = load_notes() + content = input("Enter note content: ").strip() + if not content: + print("Note cannot be empty.") + return + # Generate a unique ID using current timestamp in milliseconds + note_id = int(time.time() * 1000) + notes.append({"id": note_id, "content": content}) + save_notes(notes) + print(f"Note created with ID {note_id}") + +def remove_note(): + """Remove a note by ID.""" + notes = load_notes() + if not notes: + print("No notes to remove.") + return + print("Select a note to remove:") + for note in notes: + content = note['content'] + if len(content) > 50: + display_content = content[:50] + "..." + else: + display_content = content + print(f"ID: {note['id']} - Content: {display_content}") + try: + note_id = int(input("Enter note ID to remove: ")) + except ValueError: + print("Invalid ID.") + return + # Find note with matching ID + for i, note in enumerate(notes): + if note["id"] == note_id: + del notes[i] + save_notes(notes) + print(f"Note ID {note_id} removed.") + return + print(f"No note found with ID {note_id}") + +def change_note(): + """Change the content of an existing note.""" + notes = load_notes() + if not notes: + print("No notes to change.") + return + print("Select a note to change:") + for note in notes: + content = note['content'] + if len(content) > 50: + display_content = content[:50] + "..." + else: + display_content = content + print(f"ID: {note['id']} - Content: {display_content}") + try: + note_id = int(input("Enter note ID to change: ")) + except ValueError: + print("Invalid ID.") + return + # Find note with matching ID + for note in notes: + if note["id"] == note_id: + new_content = input("Enter new content: ").strip() + if not new_content: + print("Note cannot be empty.") + return + note["content"] = new_content + save_notes(notes) + print(f"Note ID {note_id} updated.") + return + print(f"No note found with ID {note_id}") + +def menu(): + print("============") + print(" MENU ") + print("============") + print("1) Create New Note") + print("2) Remove Note") + print("3) Change Note") + print("4) Print All Notes") + print("5) Export Notes to JSON") + print("6) Import Notes from JSON") + print("7) Exit") + +def main(): + while True: + try: + menu() + choice = int(input("Enter one of the choices: ")) + if choice == 1: + create_note() + elif choice == 2: + remove_note() + elif choice == 3: + change_note() + elif choice == 4: + print_notes() + elif choice == 5: + export_notes_json() + elif choice == 6: + import_notes_json() + elif choice == 7: + print("Goodbye!") + sys.exit(0) + else: + print("Invalid choice. Please enter a number between 1 and 7.") + except ValueError: + print("Enter a valid integer!") + except KeyboardInterrupt: + print("\nGoodbye!") + sys.exit(0) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/notes.json b/notes.json new file mode 100644 index 0000000..014d7ba --- /dev/null +++ b/notes.json @@ -0,0 +1,10 @@ +[ + { + "id": 1001, + "content": "Test note 1" + }, + { + "id": 1002, + "content": "Test note 2 with longer content to test" + } +] \ No newline at end of file diff --git a/package_linux.sh b/package_linux.sh new file mode 100644 index 0000000..aa8c6de --- /dev/null +++ b/package_linux.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# package_linux.sh +# Script to package the terminal notes app into a standalone executable using PyInstaller for Linux/macOS + +# Ensure we are in the script's directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" + +# Check if Python3 is available +if ! command -v python3 &> /dev/null; then + echo "Error: python3 is not installed or not in PATH. Please install Python 3.6+." + exit 1 +fi + +# Check if PyInstaller is installed; if not, install it +if ! command -v pyinstaller &> /dev/null; then + echo "PyInstaller not found. Installing via pip..." + python3 -m pip install --upgrade pyinstaller + if [[ $? -ne 0 ]]; then + echo "Failed to install PyInstaller." + exit 1 + fi +fi + +# Build the executable +echo "Building executable with PyInstaller..." +pyinstaller --onefile --name terminalnotesapp main.py +if [[ $? -ne 0 ]]; then + echo "PyInstaller failed." + exit 1 +fi + +echo "Build completed. Executable is in ./dist/terminalnotesapp" +echo "You can distribute the executable from the dist folder." \ No newline at end of file diff --git a/package_win.ps1 b/package_win.ps1 new file mode 100644 index 0000000..17554f6 --- /dev/null +++ b/package_win.ps1 @@ -0,0 +1,34 @@ +#!/usr/bin/env pwsh +# package_win.ps1 +# Script to package the terminal notes app into a standalone executable using PyInstaller for Windows PowerShell 5.1+ + +# Ensure we are in the script's directory +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +Set-Location $scriptDir + +# Check if Python is available +if (-not (Get-Command python -ErrorAction SilentlyContinue)) { + Write-Error "Python is not installed or not in PATH. Please install Python 3.6+." + exit 1 +} + +# Check if PyInstaller is installed; if not, install it +if (-not (Get-Command pyinstaller -ErrorAction SilentlyContinue)) { + Write-Host "PyInstaller not found. Installing via pip..." + python -m pip install --upgrade pyinstaller + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to install PyInstaller." + exit 1 + } +} + +# Build the executable +Write-Host "Building executable with PyInstaller..." +pyinstaller --onefile --name terminalnotesapp main.py +if ($LASTEXITCODE -ne 0) { + Write-Error "PyInstaller failed." + exit 1 +} + +Write-Host "Build completed. Executable is in ./dist/terminalnotesapp.exe" +Write-Host "You can distribute the .exe file from the dist folder." \ No newline at end of file diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..565bff6 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,47 @@ +{ + "version": 1, + "skills": { + "cavecrew": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "skillPath": "skills/cavecrew/SKILL.md", + "computedHash": "326e2f95ddfa1d2aca1c33cbcc2c7352561e4e9a1d26b7c9e84f938590063c9f" + }, + "caveman": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "skillPath": "skills/caveman/SKILL.md", + "computedHash": "b0e7741886c0c32270cedc1a2fc21c2879d90964d3ce00f24952de7064613404" + }, + "caveman-commit": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "skillPath": "skills/caveman-commit/SKILL.md", + "computedHash": "f028652defd5fdeddcce2994083cb1a7b201ee827bba8e2495546ee159fca3de" + }, + "caveman-compress": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "skillPath": "skills/caveman-compress/SKILL.md", + "computedHash": "d48eb2dea71e0be6a5ba68f508350a5356d30648919f7c9da6347be3af68e84c" + }, + "caveman-help": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "skillPath": "skills/caveman-help/SKILL.md", + "computedHash": "4dba39eea07a050108d47940b39600bc8f45489201ecff0ccf03627180fd8e50" + }, + "caveman-review": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "skillPath": "skills/caveman-review/SKILL.md", + "computedHash": "b9091dbc51de0f3710ea818fd4d638539f8c1784f8fda931eb159c44861e702e" + }, + "caveman-stats": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "skillPath": "skills/caveman-stats/SKILL.md", + "computedHash": "331f720e2fa97b68cacdae44384878071e8cac6013479edea68f4c8eca308852" + } + } +} diff --git a/terminalnotesapp.spec b/terminalnotesapp.spec new file mode 100644 index 0000000..741ceca --- /dev/null +++ b/terminalnotesapp.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='terminalnotesapp', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/tests/test_terminalnotesapp.py b/tests/test_terminalnotesapp.py new file mode 100644 index 0000000..a061984 --- /dev/null +++ b/tests/test_terminalnotesapp.py @@ -0,0 +1,508 @@ +""" +Test suite for terminal notes application +""" +import json +import os +import tempfile +import unittest +from unittest.mock import patch, mock_open +import sys + +# Add the project root to the path so we can import main +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import main + + +class TestTerminalNotesApp(unittest.TestCase): + """Test cases for the terminal notes application""" + + def setUp(self): + """Set up test fixtures before each test method""" + # Backup original notes file if it exists + self.original_notes = "notes.json" + self.backup_nonexistent = not os.path.exists(self.original_notes) + if not self.backup_nonexistent: + with open(self.original_notes, 'r') as f: + self.original_content = f.read() + + # Create a temporary notes file for testing + self.test_notes_file = "test_notes.json" + # Temporarily override the NOTES_FILE constant + self.original_notes_file = main.NOTES_FILE + main.NOTES_FILE = self.test_notes_file + + def tearDown(self): + """Tear down test fixtures after each test method""" + # Restore original NOTES_FILE + main.NOTES_FILE = self.original_notes_file + + # Remove test notes file if it exists + if os.path.exists(self.test_notes_file): + os.remove(self.test_notes_file) + + # Restore original notes file if it existed + if not self.backup_nonexistent: + with open(self.original_notes, 'w') as f: + f.write(self.original_content) + elif os.path.exists(self.original_notes): + os.remove(self.original_notes) + + def test_load_notes_file_not_exists(self): + """Test loading notes when file doesn't exist""" + # Ensure file doesn't exist + if os.path.exists(self.test_notes_file): + os.remove(self.test_notes_file) + + notes = main.load_notes() + self.assertEqual(notes, []) + + def test_load_notes_file_exists_empty(self): + """Test loading notes from empty file""" + with open(self.test_notes_file, 'w') as f: + f.write('') # Empty file + + notes = main.load_notes() + self.assertEqual(notes, []) + + def test_load_notes_file_exists_valid_json(self): + """Test loading notes from valid JSON file""" + test_data = [ + {"id": 1, "content": "Test note 1"}, + {"id": 2, "content": "Test note 2"} + ] + with open(self.test_notes_file, 'w') as f: + json.dump(test_data, f) + + notes = main.load_notes() + self.assertEqual(notes, test_data) + + def test_load_notes_file_exists_invalid_json(self): + """Test loading notes from invalid JSON file""" + with open(self.test_notes_file, 'w') as f: + f.write('invalid json') + + notes = main.load_notes() + self.assertEqual(notes, []) + + def test_save_notes(self): + """Test saving notes to file""" + test_data = [ + {"id": 1, "content": "Test note 1"}, + {"id": 2, "content": "Test note 2"} + ] + main.save_notes(test_data) + + # Verify file was created with correct content + self.assertTrue(os.path.exists(self.test_notes_file)) + with open(self.test_notes_file, 'r') as f: + saved_data = json.load(f) + self.assertEqual(saved_data, test_data) + + def test_create_note(self): + """Test creating a new note""" + # Mock user input + with patch('builtins.input', return_value='Test note content'): + with patch('builtins.print'): # Suppress print output + main.create_note() + + # Check that note was added + notes = main.load_notes() + self.assertEqual(len(notes), 1) + self.assertEqual(notes[0]['content'], 'Test note content') + # ID should be a timestamp-based integer + self.assertIsInstance(notes[0]['id'], int) + self.assertGreater(notes[0]['id'], 1000000000000) # Reasonable timestamp + + def test_create_note_empty_content(self): + """Test creating a note with empty content (should fail)""" + with patch('builtins.input', return_value=''): + with patch('builtins.print') as mock_print: + main.create_note() + # Check that error message was printed + printed_output = ' '.join(str(call) for call in mock_print.call_args_list) + self.assertIn('Note cannot be empty', printed_output) + + # No note should be added + notes = main.load_notes() + self.assertEqual(len(notes), 0) + + def test_remove_note(self): + """Test removing a note""" + # Add test notes + test_data = [ + {"id": 1, "content": "Test note 1"}, + {"id": 2, "content": "Test note 2"} + ] + main.save_notes(test_data) + + # Mock user input to remove first note + with patch('builtins.input', return_value='1'): + with patch('builtins.print'): # Suppress print output + main.remove_note() + + # Check that note was removed + notes = main.load_notes() + self.assertEqual(len(notes), 1) + self.assertEqual(notes[0]['id'], 2) + self.assertEqual(notes[0]['content'], 'Test note 2') + + def test_remove_note_invalid_id(self): + """Test removing a note with invalid ID""" + # Add test notes + test_data = [ + {"id": 1, "content": "Test note 1"}, + {"id": 2, "content": "Test note 2"} + ] + main.save_notes(test_data) + + # Mock user input for non-existent ID + with patch('builtins.input', return_value='999'): + with patch('builtins.print') as mock_print: + main.remove_note() + # Check that error message was printed + printed_output = ' '.join(str(call) for call in mock_print.call_args_list) + self.assertIn('No note found with ID 999', printed_output) + + # Notes should remain unchanged + notes = main.load_notes() + self.assertEqual(len(notes), 2) + + def test_remove_note_non_numeric_input(self): + """Test removing a note with non-numeric input""" + # Add test notes + test_data = [ + {"id": 1, "content": "Test note 1"}, + {"id": 2, "content": "Test note 2"} + ] + main.save_notes(test_data) + + # Mock user input for non-numeric value + with patch('builtins.input', return_value='abc'): + with patch('builtins.print') as mock_print: + main.remove_note() + # Check that error message was printed + printed_output = ' '.join(str(call) for call in mock_print.call_args_list) + self.assertIn('Invalid ID', printed_output) + + # Notes should remain unchanged + notes = main.load_notes() + self.assertEqual(len(notes), 2) + + def test_change_note(self): + """Test changing a note's content""" + # Add test notes + test_data = [ + {"id": 1, "content": "Original content"}, + {"id": 2, "content": "Another note"} + ] + main.save_notes(test_data) + + # Mock user input: select note 1, then provide new content + with patch('builtins.input', side_effect=['1', 'Updated content']): + with patch('builtins.print'): # Suppress print output + main.change_note() + + # Check that note was updated + notes = main.load_notes() + self.assertEqual(len(notes), 2) + # Find the note with id=1 + note1 = next(note for note in notes if note['id'] == 1) + self.assertEqual(note1['content'], 'Updated content') + # Note with id=2 should be unchanged + note2 = next(note for note in notes if note['id'] == 2) + self.assertEqual(note2['content'], 'Another note') + + def test_change_note_empty_content(self): + """Test changing a note to empty content (should fail)""" + # Add test notes + test_data = [ + {"id": 1, "content": "Original content"}, + {"id": 2, "content": "Another note"} + ] + main.save_notes(test_data) + + # Mock user input: select note 1, then provide empty content + with patch('builtins.input', side_effect=['1', '']): + with patch('builtins.print') as mock_print: + main.change_note() + # Check that error message was printed + printed_output = ' '.join(str(call) for call in mock_print.call_args_list) + self.assertIn('Note cannot be empty', printed_output) + + # Notes should remain unchanged + notes = main.load_notes() + self.assertEqual(len(notes), 2) + note1 = next(note for note in notes if note['id'] == 1) + self.assertEqual(note1['content'], 'Original content') + + def test_change_note_invalid_id(self): + """Test changing a note with invalid ID""" + # Add test notes + test_data = [ + {"id": 1, "content": "Test note 1"}, + {"id": 2, "content": "Test note 2"} + ] + main.save_notes(test_data) + + # Mock user input for non-existent ID + with patch('builtins.input', side_effect=['999']): # Only need first input for ID + with patch('builtins.print') as mock_print: + main.change_note() + # Check that error message was printed + printed_output = ' '.join(str(call) for call in mock_print.call_args_list) + self.assertIn('No note found with ID 999', printed_output) + + # Notes should remain unchanged + notes = main.load_notes() + self.assertEqual(len(notes), 2) + + def test_print_notes_empty(self): + """Test printing notes when no notes exist""" + # Ensure no notes file exists + if os.path.exists(self.test_notes_file): + os.remove(self.test_notes_file) + + with patch('builtins.print') as mock_print: + main.print_notes() + # Check that appropriate message was printed + printed_output = ' '.join(str(call) for call in mock_print.call_args_list) + self.assertIn('No notes found.', printed_output) + + def test_print_notes_with_data(self): + """Test printing notes when notes exist""" + test_data = [ + {"id": 1, "content": "Short note"}, + {"id": 2, "content": "This is a longer note that should be truncated in display"} + ] + main.save_notes(test_data) + + with patch('builtins.print') as mock_print: + main.print_notes() + printed_output = ' '.join(str(call) for call in mock_print.call_args_list) + # Check for header and content + self.assertIn('===== YOUR NOTES =====', printed_output) + self.assertIn('Short note', printed_output) + self.assertIn('This is a longer note that should be truncated', printed_output) + # Should show total count + self.assertIn('Total notes: 2', printed_output) + + def test_export_notes_json(self): + """Test exporting notes to JSON file""" + test_data = [ + {"id": 1, "content": "Test note 1"}, + {"id": 2, "content": "Test note 2"} + ] + main.save_notes(test_data) + + export_file = "test_export.json" + try: + # Mock user input to provide filename + with patch('builtins.input', return_value=export_file): + main.export_notes_json() + + # Check if file was created + self.assertTrue(os.path.exists(export_file)) + + # Check content + with open(export_file, 'r') as f: + exported_data = json.load(f) + + self.assertEqual(exported_data, test_data) + finally: + # Clean up + if os.path.exists(export_file): + os.remove(export_file) + + def test_export_notes_json_default_filename(self): + """Test exporting notes with default filename""" + test_data = [ + {"id": 1, "content": "Test note 1"} + ] + main.save_notes(test_data) + + export_file = "notes_export.json" + try: + # Mock user input to accept default (empty string) + with patch('builtins.input', return_value=''): + main.export_notes_json() + + # Check if default file was created + self.assertTrue(os.path.exists(export_file)) + + # Check content + with open(export_file, 'r') as f: + exported_data = json.load(f) + + self.assertEqual(exported_data, test_data) + finally: + # Clean up + if os.path.exists(export_file): + os.remove(export_file) + + def test_export_notes_no_notes(self): + """Test exporting when no notes exist""" + # Ensure no notes + if os.path.exists(self.test_notes_file): + os.remove(self.test_notes_file) + + with patch('builtins.print') as mock_print: + main.export_notes_json() + # Check that appropriate message was printed + printed_output = ' '.join(str(call) for call in mock_print.call_args_list) + self.assertIn('No notes to export.', printed_output) + + def test_import_notes_json_replace(self): + """Test importing notes with replace option""" + # Set up existing notes + existing_notes = [ + {"id": 1, "content": "Existing note"} + ] + main.save_notes(existing_notes) + + # Create import file + import_data = [ + {"id": 100, "content": "Imported note 1"}, + {"id": 101, "content": "Imported note 2"} + ] + import_file = "test_import.json" + try: + with open(import_file, 'w') as f: + json.dump(import_data, f) + + # Mock input to select replace option and provide filename + with patch('builtins.input', side_effect=[import_file, 'r']): # 'r' for replace + main.import_notes_json() + + # Check that notes were replaced (with new IDs due to conflict avoidance) + new_notes = main.load_notes() + self.assertEqual(len(new_notes), 2) + # Check that content was preserved + contents = [note['content'] for note in new_notes] + self.assertIn('Imported note 1', contents) + self.assertIn('Imported note 2', contents) + # IDs should be different due to conflict avoidance + ids = [note['id'] for note in new_notes] + self.assertNotIn(100, ids) # Original ID should be changed + self.assertNotIn(101, ids) # Original ID should be changed + finally: + # Clean up + if os.path.exists(import_file): + os.remove(import_file) + + def test_import_notes_json_merge(self): + """Test importing notes with merge option""" + # Set up existing notes + existing_notes = [ + {"id": 1, "content": "Existing note"} + ] + main.save_notes(existing_notes) + + # Create import file + import_data = [ + {"id": 100, "content": "Imported note 1"}, + {"id": 101, "content": "Imported note 2"} + ] + import_file = "test_import.json" + try: + with open(import_file, 'w') as f: + json.dump(import_data, f) + + # Mock input to select merge option and provide filename + with patch('builtins.input', side_effect=[import_file, 'm']): # 'm' for merge + main.import_notes_json() + + # Check that notes were merged + new_notes = main.load_notes() + self.assertEqual(len(new_notes), 3) # 1 existing + 2 imported + # Check that all content is present + contents = [note['content'] for note in new_notes] + self.assertIn('Existing note', contents) + self.assertIn('Imported note 1', contents) + self.assertIn('Imported note 2', contents) + finally: + # Clean up + if os.path.exists(import_file): + os.remove(import_file) + + def test_import_notes_json_file_not_found(self): + """Test importing when file doesn't exist""" + with patch('builtins.input', return_value='nonexistent.json'): + with patch('builtins.print') as mock_print: + main.import_notes_json() + # Check that error message was printed + printed_output = ' '.join(str(call) for call in mock_print.call_args_list) + self.assertIn('not found', printed_output) + + def test_import_notes_json_invalid_format(self): + """Test importing with invalid file format (not a list)""" + # Create invalid import file (not a list) + import_file = "test_import.json" + try: + with open(import_file, 'w') as f: + json.dump({"not": "a list"}, f) + + # Mock input to provide filename + with patch('builtins.input', return_value=import_file): + with patch('builtins.print') as mock_print: + main.import_notes_json() + # Check that error message was printed + printed_output = ' '.join(str(call) for call in mock_print.call_args_list) + self.assertIn('Invalid file format', printed_output) + finally: + # Clean up + if os.path.exists(import_file): + os.remove(import_file) + + def test_import_notes_json_invalid_entries(self): + """Test importing with some invalid entries""" + # Set up existing notes + existing_notes = [ + {"id": 1, "content": "Existing note"} + ] + main.save_notes(existing_notes) + + # Create import file with mix of valid and invalid entries + import_data = [ + {"id": 100, "content": "Valid note 1"}, + {"id": 101}, # Missing content + {"content": "Valid note 2"}, # Missing id + {"id": 102, "content": "Valid note 3"}, + "not a dict", + None + ] + import_file = "test_import.json" + try: + with open(import_file, 'w') as f: + json.dump(import_data, f) + + # Mock input to select merge option and provide filename + with patch('builtins.input', side_effect=[import_file, 'm']): # 'm' for merge + main.import_notes_json() + + # Check that valid notes were imported and invalid ones were skipped + new_notes = main.load_notes() + # Should have: 1 existing + 3 valid imported = 4 total + # The valid entries are: indices 0, 2, 3, 5 (None is skipped, string is skipped, missing content/missing id are skipped) + # Actually, let's trace through what should be valid: + # Index 0: {"id": 100, "content": "Valid note 1"} - VALID + # Index 1: {"id": 101} - INVALID (missing content) + # Index 2: {"content": "Valid note 2"} - INVALID (missing id) + # Index 3: {"id": 102, "content": "Valid note 3"} - VALID + # Index 4: "not a dict" - INVALID (not dict) + # Index 5: None - INVALID (not dict) + # So we should have 1 existing + 2 valid = 3 total + + self.assertEqual(len(new_notes), 3) # 1 existing + 2 valid imported + contents = [note['content'] for note in new_notes] + self.assertIn('Existing note', contents) + self.assertIn('Valid note 1', contents) + self.assertIn('Valid note 3', contents) + finally: + # Clean up + if os.path.exists(import_file): + os.remove(import_file) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file