From 058319edcb3995c6201255163816714a1a94910b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 18:30:13 +0000 Subject: [PATCH 1/8] Initial plan From bea3abe842d029f5329a36d005d340707fca45ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 18:38:23 +0000 Subject: [PATCH 2/8] Add comprehensive unit testing infrastructure with sample data - Set up Jest testing framework - Create sample data fixtures for Slack and WordPress APIs - Add unit tests for StateManager (82% coverage) - Add unit tests for SlackService (82% coverage) - Add unit tests for WordPressService (80% coverage) - Add unit tests for MarkdownExporter (95% coverage) - Create TESTING.md documentation - Update README with testing information - All 86 tests passing Co-authored-by: falkorichter <50506+falkorichter@users.noreply.github.com> --- .gitignore | 6 + README.md | 16 + TESTING.md | 232 + __tests__/fixtures/slack-auth-test.json | 9 + __tests__/fixtures/slack-channel-history.json | 42 + __tests__/fixtures/slack-channel-info.json | 24 + __tests__/fixtures/slack-channels-list.json | 56 + __tests__/fixtures/slack-thread-replies.json | 35 + __tests__/fixtures/slack-user-info.json | 44 + .../fixtures/wordpress-post-created.json | 35 + .../fixtures/wordpress-post-updated.json | 35 + __tests__/fixtures/wordpress-user-me.json | 26 + __tests__/unit/markdownExporter.test.js | 272 + __tests__/unit/slackService.test.js | 323 + __tests__/unit/stateManager.test.js | 217 + __tests__/unit/wordpressService.test.js | 306 + jest.config.js | 21 + package-lock.json | 5583 +++++++++++++++++ package.json | 15 +- 19 files changed, 7293 insertions(+), 4 deletions(-) create mode 100644 TESTING.md create mode 100644 __tests__/fixtures/slack-auth-test.json create mode 100644 __tests__/fixtures/slack-channel-history.json create mode 100644 __tests__/fixtures/slack-channel-info.json create mode 100644 __tests__/fixtures/slack-channels-list.json create mode 100644 __tests__/fixtures/slack-thread-replies.json create mode 100644 __tests__/fixtures/slack-user-info.json create mode 100644 __tests__/fixtures/wordpress-post-created.json create mode 100644 __tests__/fixtures/wordpress-post-updated.json create mode 100644 __tests__/fixtures/wordpress-user-me.json create mode 100644 __tests__/unit/markdownExporter.test.js create mode 100644 __tests__/unit/slackService.test.js create mode 100644 __tests__/unit/stateManager.test.js create mode 100644 __tests__/unit/wordpressService.test.js create mode 100644 jest.config.js create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index f901bd9..b56f8c8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,9 @@ state.json data/ npm-debug.log .DS_Store + +# Test artifacts +coverage/ +__tests__/fixtures/test-*.json +__tests__/fixtures/test-markdown-* +*.log diff --git a/README.md b/README.md index d8cc6d3..66ae867 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,22 @@ For detailed setup instructions, including how to configure Slack and WordPress, For Slack bot permissions reference, see [SLACK_PERMISSIONS.md](SLACK_PERMISSIONS.md). +For testing and development, see [TESTING.md](TESTING.md). + +## Testing + +The project includes comprehensive unit tests with sample data fixtures: + +```bash +# Run tests +npm test + +# Run tests with coverage +npm run test:coverage +``` + +See [TESTING.md](TESTING.md) for detailed testing documentation. + ## Postman Collection A Postman collection is included for testing WordPress API endpoints independently: diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..0b010c9 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,232 @@ +# Testing Guide + +This document describes the testing infrastructure for the Slack-2-WordPress integration. + +## Overview + +The project uses **Jest** as the testing framework, with comprehensive unit tests and sample data fixtures for testing without requiring live Slack or WordPress connections. + +## Quick Start + +```bash +# Install dependencies (includes Jest) +npm install + +# Run all tests +npm test + +# Run tests in watch mode (auto-rerun on changes) +npm run test:watch + +# Run tests with coverage report +npm run test:coverage +``` + +## Test Structure + +``` +__tests__/ +├── fixtures/ # Sample data for testing +│ ├── slack-*.json # Mock Slack API responses +│ └── wordpress-*.json # Mock WordPress API responses +└── unit/ # Unit tests + ├── slackService.test.js + ├── wordpressService.test.js + ├── stateManager.test.js + └── markdownExporter.test.js +``` + +## Sample Data (Fixtures) + +The project includes sample data fixtures that simulate real Slack and WordPress API responses. This allows you to: + +- Run tests without live API credentials +- Test edge cases and error scenarios +- Develop and test offline +- Ensure consistent test results + +### Slack Fixtures + +Located in `__tests__/fixtures/`: + +- `slack-auth-test.json` - Slack authentication response +- `slack-channel-info.json` - Channel information +- `slack-channel-history.json` - Channel message history with threads +- `slack-thread-replies.json` - Thread replies/messages +- `slack-user-info.json` - User profile information +- `slack-channels-list.json` - List of available channels + +### WordPress Fixtures + +Located in `__tests__/fixtures/`: + +- `wordpress-user-me.json` - Current user information +- `wordpress-post-created.json` - Response when creating a post +- `wordpress-post-updated.json` - Response when updating a post + +## Writing Tests + +### Example: Testing a Module + +```javascript +const SlackService = require('../../src/modules/slackService'); + +describe('SlackService', () => { + let slackService; + + beforeEach(() => { + slackService = new SlackService('test-token'); + }); + + test('should extract title from message', () => { + const text = 'Title here\nMore content'; + const title = slackService.extractTitle(text); + expect(title).toBe('Title here'); + }); +}); +``` + +### Using Fixtures + +```javascript +const mockData = require('../fixtures/slack-thread-replies.json'); + +test('should parse thread replies', () => { + // Use mockData in your test + expect(mockData.messages).toHaveLength(3); +}); +``` + +## Test Coverage + +The project aims for **70% test coverage** across: +- Statements +- Branches +- Functions +- Lines + +Current coverage (as of last run): + +| Module | Coverage | +|--------|----------| +| StateManager | ~82% | +| SlackService | ~82% | +| WordPressService | ~80% | +| MarkdownExporter | ~95% | + +Run `npm run test:coverage` to see detailed coverage reports in the `coverage/` directory. + +## Mocking External Dependencies + +Tests use Jest's mocking capabilities to avoid real API calls: + +### Mocking Slack API + +```javascript +jest.mock('@slack/web-api', () => { + return { + WebClient: jest.fn().mockImplementation(() => ({ + auth: { test: jest.fn() }, + conversations: { + list: jest.fn(), + info: jest.fn(), + // ... other methods + } + })) + }; +}); +``` + +### Mocking WordPress API (Axios) + +```javascript +jest.mock('axios'); + +// In your test +const axios = require('axios'); +axios.get.mockResolvedValue({ data: mockData }); +``` + +## Continuous Integration + +Tests should be run: +- Before committing changes +- In CI/CD pipelines +- Before deploying to production + +## Adding New Tests + +When adding new functionality: + +1. **Create fixture data** in `__tests__/fixtures/` if needed +2. **Write unit tests** in `__tests__/unit/` +3. **Run tests** with `npm test` +4. **Check coverage** with `npm run test:coverage` +5. **Commit tests** along with your code changes + +## Troubleshooting + +### Tests Failing + +1. Check that all dependencies are installed: `npm install` +2. Ensure Node.js version is v14 or higher: `node --version` +3. Clear Jest cache: `npx jest --clearCache` +4. Check for console errors in test output + +### Coverage Not Meeting Thresholds + +If coverage is below 70%: +- Add more test cases for uncovered branches +- Test error scenarios and edge cases +- Check `coverage/lcov-report/index.html` for detailed coverage info + +### Mock Not Working + +- Ensure mocks are defined before importing the module under test +- Use `jest.clearAllMocks()` in `afterEach()` to reset mocks between tests +- Check Jest documentation for mock syntax + +## Best Practices + +1. **Test behavior, not implementation** - Focus on what the code does, not how +2. **Use descriptive test names** - Test names should describe the scenario +3. **Keep tests isolated** - Each test should be independent +4. **Use fixtures for consistent data** - Avoid hardcoding test data in tests +5. **Test error cases** - Don't just test the happy path +6. **Keep tests simple** - Complex tests are hard to maintain + +## Running Specific Tests + +```bash +# Run a specific test file +npm test -- stateManager.test.js + +# Run tests matching a pattern +npm test -- --testNamePattern="should create" + +# Run tests for a specific module +npm test -- slackService +``` + +## Test Output + +Jest provides clear output showing: +- ✓ Passed tests (green) +- ✗ Failed tests (red) +- Coverage percentages +- Execution time + +## Resources + +- [Jest Documentation](https://jestjs.io/docs/getting-started) +- [Jest Matchers](https://jestjs.io/docs/expect) +- [Jest Mock Functions](https://jestjs.io/docs/mock-functions) + +## Future Improvements + +Potential testing enhancements: +- Integration tests for full sync workflow +- End-to-end tests with test Slack workspace +- Performance tests for large thread volumes +- Snapshot tests for markdown output +- Visual regression tests for UI components diff --git a/__tests__/fixtures/slack-auth-test.json b/__tests__/fixtures/slack-auth-test.json new file mode 100644 index 0000000..64a3d08 --- /dev/null +++ b/__tests__/fixtures/slack-auth-test.json @@ -0,0 +1,9 @@ +{ + "ok": true, + "url": "https://test-workspace.slack.com/", + "team": "Test Workspace", + "user": "test_bot", + "team_id": "T1234567890", + "user_id": "U1234567890", + "bot_id": "B1234567890" +} diff --git a/__tests__/fixtures/slack-channel-history.json b/__tests__/fixtures/slack-channel-history.json new file mode 100644 index 0000000..53d97f0 --- /dev/null +++ b/__tests__/fixtures/slack-channel-history.json @@ -0,0 +1,42 @@ +{ + "ok": true, + "messages": [ + { + "type": "message", + "user": "U1234567890", + "text": "This is a test thread about Node.js best practices", + "ts": "1234567890.123456", + "thread_ts": "1234567890.123456", + "reply_count": 2, + "reply_users_count": 2, + "latest_reply": "1234567891.123457", + "reply_users": ["U1234567890", "U0987654321"], + "is_locked": false, + "subscribed": false + }, + { + "type": "message", + "user": "U0987654321", + "text": "Another thread about testing strategies", + "ts": "1234567892.123458", + "thread_ts": "1234567892.123458", + "reply_count": 1, + "reply_users_count": 1, + "latest_reply": "1234567893.123459", + "reply_users": ["U1234567890"], + "is_locked": false, + "subscribed": false + }, + { + "type": "message", + "user": "U1234567890", + "text": "Regular message without thread", + "ts": "1234567894.123460" + } + ], + "has_more": false, + "pin_count": 0, + "response_metadata": { + "next_cursor": "" + } +} diff --git a/__tests__/fixtures/slack-channel-info.json b/__tests__/fixtures/slack-channel-info.json new file mode 100644 index 0000000..2dec3e9 --- /dev/null +++ b/__tests__/fixtures/slack-channel-info.json @@ -0,0 +1,24 @@ +{ + "ok": true, + "channel": { + "id": "C1234567890", + "name": "test-channel", + "is_channel": true, + "is_group": false, + "is_im": false, + "is_member": true, + "is_private": false, + "created": 1234567890, + "creator": "U1234567890", + "is_archived": false, + "is_general": false, + "unlinked": 0, + "name_normalized": "test-channel", + "is_shared": false, + "is_org_shared": false, + "is_pending_ext_shared": false, + "pending_shared": [], + "context_team_id": "T1234567890", + "updated": 1234567890 + } +} diff --git a/__tests__/fixtures/slack-channels-list.json b/__tests__/fixtures/slack-channels-list.json new file mode 100644 index 0000000..3201a2a --- /dev/null +++ b/__tests__/fixtures/slack-channels-list.json @@ -0,0 +1,56 @@ +{ + "ok": true, + "channels": [ + { + "id": "C1234567890", + "name": "general", + "is_channel": true, + "is_group": false, + "is_im": false, + "is_member": true, + "is_private": false, + "created": 1234567890, + "is_archived": false, + "is_general": true, + "unlinked": 0, + "name_normalized": "general", + "is_shared": false, + "is_org_shared": false + }, + { + "id": "C0987654321", + "name": "test-channel", + "is_channel": true, + "is_group": false, + "is_im": false, + "is_member": true, + "is_private": false, + "created": 1234567890, + "is_archived": false, + "is_general": false, + "unlinked": 0, + "name_normalized": "test-channel", + "is_shared": false, + "is_org_shared": false + }, + { + "id": "C1111111111", + "name": "private-channel", + "is_channel": false, + "is_group": true, + "is_im": false, + "is_member": false, + "is_private": true, + "created": 1234567890, + "is_archived": false, + "is_general": false, + "unlinked": 0, + "name_normalized": "private-channel", + "is_shared": false, + "is_org_shared": false + } + ], + "response_metadata": { + "next_cursor": "" + } +} diff --git a/__tests__/fixtures/slack-thread-replies.json b/__tests__/fixtures/slack-thread-replies.json new file mode 100644 index 0000000..444a220 --- /dev/null +++ b/__tests__/fixtures/slack-thread-replies.json @@ -0,0 +1,35 @@ +{ + "ok": true, + "messages": [ + { + "type": "message", + "user": "U1234567890", + "text": "This is a test thread about Node.js best practices\nIt has multiple lines and discusses important topics.", + "ts": "1234567890.123456", + "thread_ts": "1234567890.123456", + "reply_count": 2, + "reply_users_count": 2, + "reply_users": ["U1234567890", "U0987654321"] + }, + { + "type": "message", + "user": "U0987654321", + "text": "Great point! I totally agree with this approach.", + "ts": "1234567890.123457", + "thread_ts": "1234567890.123456", + "parent_user_id": "U1234567890" + }, + { + "type": "message", + "user": "U1234567890", + "text": "Thanks for the feedback! Let's implement this.", + "ts": "1234567891.123458", + "thread_ts": "1234567890.123456", + "parent_user_id": "U1234567890" + } + ], + "has_more": false, + "response_metadata": { + "next_cursor": "" + } +} diff --git a/__tests__/fixtures/slack-user-info.json b/__tests__/fixtures/slack-user-info.json new file mode 100644 index 0000000..2c61517 --- /dev/null +++ b/__tests__/fixtures/slack-user-info.json @@ -0,0 +1,44 @@ +{ + "ok": true, + "user": { + "id": "U1234567890", + "team_id": "T1234567890", + "name": "john.doe", + "deleted": false, + "color": "9f69e7", + "real_name": "John Doe", + "tz": "America/New_York", + "tz_label": "Eastern Standard Time", + "tz_offset": -18000, + "profile": { + "title": "Software Engineer", + "phone": "", + "skype": "", + "real_name": "John Doe", + "real_name_normalized": "John Doe", + "display_name": "johndoe", + "display_name_normalized": "johndoe", + "status_text": "", + "status_emoji": "", + "status_expiration": 0, + "avatar_hash": "abc123", + "email": "john.doe@example.com", + "first_name": "John", + "last_name": "Doe", + "image_24": "https://avatars.slack-edge.com/...", + "image_32": "https://avatars.slack-edge.com/...", + "image_48": "https://avatars.slack-edge.com/...", + "image_72": "https://avatars.slack-edge.com/...", + "image_192": "https://avatars.slack-edge.com/...", + "image_512": "https://avatars.slack-edge.com/..." + }, + "is_admin": false, + "is_owner": false, + "is_primary_owner": false, + "is_restricted": false, + "is_ultra_restricted": false, + "is_bot": false, + "updated": 1234567890, + "is_app_user": false + } +} diff --git a/__tests__/fixtures/wordpress-post-created.json b/__tests__/fixtures/wordpress-post-created.json new file mode 100644 index 0000000..2a8bef4 --- /dev/null +++ b/__tests__/fixtures/wordpress-post-created.json @@ -0,0 +1,35 @@ +{ + "id": 123, + "date": "2024-01-15T10:30:00", + "date_gmt": "2024-01-15T10:30:00", + "guid": { + "rendered": "https://example.com/?p=123" + }, + "modified": "2024-01-15T10:30:00", + "modified_gmt": "2024-01-15T10:30:00", + "slug": "test-post", + "status": "draft", + "type": "post", + "link": "https://example.com/test-post/", + "title": { + "rendered": "This is a test thread about Node.js best practices" + }, + "content": { + "rendered": "
This is a test thread about Node.js best practices
It has multiple lines and discusses important topics.
Reply:
\nGreat point! I totally agree with this approach.
\nReply:
\nThanks for the feedback! Let's implement this.
\nThis is a test thread about Node.js best practices
\n", + "protected": false + }, + "author": 1, + "featured_media": 0, + "comment_status": "open", + "ping_status": "open", + "sticky": false, + "template": "", + "format": "standard", + "meta": [], + "categories": [1], + "tags": [] +} diff --git a/__tests__/fixtures/wordpress-post-updated.json b/__tests__/fixtures/wordpress-post-updated.json new file mode 100644 index 0000000..c584d2d --- /dev/null +++ b/__tests__/fixtures/wordpress-post-updated.json @@ -0,0 +1,35 @@ +{ + "id": 123, + "date": "2024-01-15T10:30:00", + "date_gmt": "2024-01-15T10:30:00", + "guid": { + "rendered": "https://example.com/?p=123" + }, + "modified": "2024-01-15T11:00:00", + "modified_gmt": "2024-01-15T11:00:00", + "slug": "test-post", + "status": "draft", + "type": "post", + "link": "https://example.com/test-post/", + "title": { + "rendered": "Updated: This is a test thread about Node.js best practices" + }, + "content": { + "rendered": "Updated content here
\n", + "protected": false + }, + "excerpt": { + "rendered": "Updated excerpt
\n", + "protected": false + }, + "author": 1, + "featured_media": 0, + "comment_status": "open", + "ping_status": "open", + "sticky": false, + "template": "", + "format": "standard", + "meta": [], + "categories": [1], + "tags": [] +} diff --git a/__tests__/fixtures/wordpress-user-me.json b/__tests__/fixtures/wordpress-user-me.json new file mode 100644 index 0000000..4ffba1a --- /dev/null +++ b/__tests__/fixtures/wordpress-user-me.json @@ -0,0 +1,26 @@ +{ + "id": 1, + "username": "testuser", + "name": "Test User", + "email": "testuser@example.com", + "url": "", + "description": "A test user for unit testing", + "link": "https://example.com/author/testuser/", + "slug": "testuser", + "avatar_urls": { + "24": "https://secure.gravatar.com/avatar/?s=24&d=mm&r=g", + "48": "https://secure.gravatar.com/avatar/?s=48&d=mm&r=g", + "96": "https://secure.gravatar.com/avatar/?s=96&d=mm&r=g" + }, + "roles": ["author"], + "capabilities": { + "read": true, + "level_0": true, + "level_1": true, + "level_2": true, + "edit_posts": true, + "delete_posts": true, + "publish_posts": true, + "upload_files": true + } +} diff --git a/__tests__/unit/markdownExporter.test.js b/__tests__/unit/markdownExporter.test.js new file mode 100644 index 0000000..039eabe --- /dev/null +++ b/__tests__/unit/markdownExporter.test.js @@ -0,0 +1,272 @@ +const MarkdownExporter = require('../../src/modules/markdownExporter'); +const fs = require('fs').promises; +const path = require('path'); + +describe('MarkdownExporter', () => { + let markdownExporter; + let testOutputDir; + + beforeEach(() => { + testOutputDir = path.join(__dirname, '../fixtures', `test-markdown-${Date.now()}`); + markdownExporter = new MarkdownExporter(testOutputDir); + }); + + afterEach(async () => { + // Clean up test directory + try { + await fs.rm(testOutputDir, { recursive: true, force: true }); + } catch (error) { + // Directory might not exist, ignore + } + }); + + describe('extractTitle', () => { + test('should extract title from first line', () => { + const text = 'This is the title\nThis is more content'; + const title = markdownExporter.extractTitle(text); + expect(title).toBe('This is the title'); + }); + + test('should remove markdown headers', () => { + const text = '## This is a header\nMore content'; + const title = markdownExporter.extractTitle(text); + expect(title).toBe('This is a header'); + }); + + test('should truncate long titles', () => { + const longText = 'a'.repeat(150); + const title = markdownExporter.extractTitle(longText); + expect(title).toHaveLength(103); // 100 chars + '...' + expect(title.endsWith('...')).toBe(true); + }); + + test('should return "Untitled" for empty text', () => { + const title = markdownExporter.extractTitle(''); + expect(title).toBe('Untitled'); + }); + }); + + describe('formatMessageText', () => { + test('should preserve line breaks', () => { + const text = 'Line 1\nLine 2\nLine 3'; + const formatted = markdownExporter.formatMessageText(text); + expect(formatted).toContain('\n'); + }); + + test('should convert Slack user mentions', () => { + const text = 'Hello <@U1234567890|johndoe>'; + const formatted = markdownExporter.formatMessageText(text); + expect(formatted).toBe('Hello @johndoe'); + }); + + test('should convert Slack channel mentions', () => { + const text = 'See <#C1234567890|general>'; + const formatted = markdownExporter.formatMessageText(text); + expect(formatted).toBe('See #general'); + }); + + test('should convert Slack URLs', () => { + const text = 'Main content
'); + expect(content).toContain('