@cakemail-org/cakemail-cli 1.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +12 -0
- package/.env.example +40 -0
- package/.env.test.example +45 -0
- package/CHANGELOG.md +1031 -0
- package/README.md +319 -15
- package/audit-formats.js +128 -0
- package/cakemail.rb +20 -0
- package/dist/cli.js +27 -10
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +16 -6
- package/dist/client.js.map +1 -1
- package/dist/commands/account.js +1 -1
- package/dist/commands/account.js.map +1 -1
- package/dist/commands/attributes.js +1 -1
- package/dist/commands/attributes.js.map +1 -1
- package/dist/commands/campaigns.d.ts.map +1 -1
- package/dist/commands/campaigns.js +103 -8
- package/dist/commands/campaigns.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +63 -4
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/contacts.d.ts.map +1 -1
- package/dist/commands/contacts.js +91 -12
- package/dist/commands/contacts.js.map +1 -1
- package/dist/commands/emails.js +1 -1
- package/dist/commands/emails.js.map +1 -1
- package/dist/commands/interests.d.ts +5 -0
- package/dist/commands/interests.d.ts.map +1 -0
- package/dist/commands/interests.js +172 -0
- package/dist/commands/interests.js.map +1 -0
- package/dist/commands/lists.d.ts.map +1 -1
- package/dist/commands/lists.js +6 -8
- package/dist/commands/lists.js.map +1 -1
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +237 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/reports.js +1 -1
- package/dist/commands/reports.js.map +1 -1
- package/dist/commands/segments.js +1 -1
- package/dist/commands/segments.js.map +1 -1
- package/dist/commands/senders.d.ts.map +1 -1
- package/dist/commands/senders.js +11 -8
- package/dist/commands/senders.js.map +1 -1
- package/dist/commands/suppressed.js +1 -1
- package/dist/commands/suppressed.js.map +1 -1
- package/dist/commands/tags.d.ts +5 -0
- package/dist/commands/tags.d.ts.map +1 -0
- package/dist/commands/tags.js +124 -0
- package/dist/commands/tags.js.map +1 -0
- package/dist/commands/templates.js +1 -1
- package/dist/commands/templates.js.map +1 -1
- package/dist/commands/transactional-templates.d.ts +5 -0
- package/dist/commands/transactional-templates.d.ts.map +1 -0
- package/dist/commands/transactional-templates.js +354 -0
- package/dist/commands/transactional-templates.js.map +1 -0
- package/dist/commands/webhooks.js +1 -1
- package/dist/commands/webhooks.js.map +1 -1
- package/dist/utils/auth.d.ts +8 -1
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +39 -11
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/config-file.d.ts +7 -0
- package/dist/utils/config-file.d.ts.map +1 -1
- package/dist/utils/config-file.js +15 -0
- package/dist/utils/config-file.js.map +1 -1
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +12 -4
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/errors.js +1 -1
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/list-defaults.d.ts +33 -0
- package/dist/utils/list-defaults.d.ts.map +1 -0
- package/dist/utils/list-defaults.js +52 -0
- package/dist/utils/list-defaults.js.map +1 -0
- package/dist/utils/output.d.ts.map +1 -1
- package/dist/utils/output.js +36 -13
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/progress.d.ts.map +1 -1
- package/dist/utils/progress.js +32 -4
- package/dist/utils/progress.js.map +1 -1
- package/dist/utils/spinner.d.ts +17 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js +43 -0
- package/dist/utils/spinner.js.map +1 -0
- package/docs/DOCUMENTATION-STANDARD.md +1068 -0
- package/docs/README.md +161 -0
- package/docs/developer/ARCHITECTURE.md +516 -0
- package/docs/developer/AUTH.md +204 -0
- package/docs/developer/CONTRIBUTING.md +227 -0
- package/docs/developer/DOCUMENTATION_SUMMARY.md +346 -0
- package/docs/developer/PROJECT_INDEX.md +365 -0
- package/docs/planning/API_COVERAGE.md +1045 -0
- package/docs/planning/BACKLOG.md +1159 -0
- package/docs/planning/PROFILE_SYSTEM_TASKS.md +287 -0
- package/docs/planning/UX_IMPLEMENTATION_PLAN.md +691 -0
- package/docs/planning/archive/RELEASE_CHECKLIST_v1.3.0.md +332 -0
- package/docs/planning/archive/RELEASE_v1.3.0.md +428 -0
- package/docs/planning/archive/cakemail-cli-ux-improvements.md +438 -0
- package/docs/planning/cakemail-profile-system-plan.md +1121 -0
- package/docs/testing/AI_USER_SIMULATION_DESIGN.md +1342 -0
- package/docs/testing/KENOGAMI_BIDIRECTIONAL_FLOW.md +1517 -0
- package/docs/testing/KENOGAMI_TRUTH_RECONCILIATION_SYSTEM.md +1369 -0
- package/docs/user-manual/.obsidian/app.json +1 -0
- package/docs/user-manual/.obsidian/appearance.json +1 -0
- package/docs/user-manual/.obsidian/core-plugins.json +33 -0
- package/docs/user-manual/.obsidian/workspace.json +167 -0
- package/docs/user-manual/01-getting-started/01-installation.md +214 -0
- package/docs/user-manual/01-getting-started/02-quick-start.md +432 -0
- package/docs/user-manual/01-getting-started/03-authentication.md +448 -0
- package/docs/user-manual/01-getting-started/04-configuration.md +430 -0
- package/docs/user-manual/01-getting-started/05-output-formats.md +447 -0
- package/docs/user-manual/02-core-concepts/01-accounts.md +514 -0
- package/docs/user-manual/02-core-concepts/02-profile-system.md +771 -0
- package/docs/user-manual/02-core-concepts/03-smart-defaults.md +485 -0
- package/docs/user-manual/02-core-concepts/04-authentication-methods.md +435 -0
- package/docs/user-manual/02-core-concepts/05-pagination-filtering.md +600 -0
- package/docs/user-manual/02-core-concepts/06-error-handling.md +718 -0
- package/docs/user-manual/02-core-concepts/07-api-coverage.md +483 -0
- package/docs/user-manual/03-email-operations/01-senders.md +490 -0
- package/docs/user-manual/03-email-operations/02-templates.md +444 -0
- package/docs/user-manual/03-email-operations/03-transactional-emails.md +706 -0
- package/docs/user-manual/03-email-operations/04-email-tracking.md +407 -0
- package/docs/user-manual/04-campaign-management/01-campaigns-basics.md +394 -0
- package/docs/user-manual/04-campaign-management/02-campaign-scheduling.md +630 -0
- package/docs/user-manual/04-campaign-management/03-campaign-testing.md +997 -0
- package/docs/user-manual/04-campaign-management/04-campaign-lifecycle.md +709 -0
- package/docs/user-manual/04-campaign-management/05-campaign-links.md +934 -0
- package/docs/user-manual/05-contact-management/01-lists.md +836 -0
- package/docs/user-manual/05-contact-management/02-contacts.md +1035 -0
- package/docs/user-manual/05-contact-management/03-custom-attributes.md +788 -0
- package/docs/user-manual/05-contact-management/04-segments.md +1028 -0
- package/docs/user-manual/05-contact-management/05-contact-import-export.md +1031 -0
- package/docs/user-manual/06-analytics-reporting/01-campaign-analytics.md +867 -0
- package/docs/user-manual/06-analytics-reporting/02-account-reports.md +227 -0
- package/docs/user-manual/07-integrations/01-webhooks-integration.md +259 -0
- package/docs/user-manual/07-integrations/02-automation.md +326 -0
- package/docs/user-manual/08-advanced-usage/01-scripting-patterns.md +672 -0
- package/docs/user-manual/08-advanced-usage/02-bulk-operations.md +932 -0
- package/docs/user-manual/08-advanced-usage/03-ci-cd-integration.md +892 -0
- package/docs/user-manual/08-advanced-usage/04-performance-optimization.md +766 -0
- package/docs/user-manual/09-command-reference/01-config.md +776 -0
- package/docs/user-manual/09-command-reference/02-account.md +652 -0
- package/docs/user-manual/09-command-reference/03-lists.md +958 -0
- package/docs/user-manual/09-command-reference/04-contacts.md +1408 -0
- package/docs/user-manual/09-command-reference/05-attributes.md +617 -0
- package/docs/user-manual/09-command-reference/06-segments.md +894 -0
- package/docs/user-manual/09-command-reference/07-senders.md +803 -0
- package/docs/user-manual/09-command-reference/08-templates.md +818 -0
- package/docs/user-manual/09-command-reference/09-campaigns.md +1250 -0
- package/docs/user-manual/09-command-reference/10-emails.md +807 -0
- package/docs/user-manual/09-command-reference/11-reports.md +1135 -0
- package/docs/user-manual/09-command-reference/12-webhooks.md +773 -0
- package/docs/user-manual/09-command-reference/13-suppressed.md +797 -0
- package/docs/user-manual/09-command-reference/14-interests.md +630 -0
- package/docs/user-manual/09-command-reference/15-tags.md +584 -0
- package/docs/user-manual/09-command-reference/16-logs.md +656 -0
- package/docs/user-manual/09-command-reference/17-transactional-templates.md +850 -0
- package/docs/user-manual/10-troubleshooting/01-common-errors.md +457 -0
- package/docs/user-manual/10-troubleshooting/02-authentication-issues.md +558 -0
- package/docs/user-manual/10-troubleshooting/03-connection-problems.md +634 -0
- package/docs/user-manual/10-troubleshooting/04-debugging.md +725 -0
- package/docs/user-manual/11-appendix/04-faq.md +484 -0
- package/docs/user-manual/11-appendix/05-glossary.md +250 -0
- package/docs/user-manual/README.md +0 -0
- package/package.json +13 -47
- package/src/cli.ts +125 -0
- package/src/client.ts +16 -0
- package/src/commands/account.ts +267 -0
- package/src/commands/accounts.ts +78 -0
- package/src/commands/actions.ts +249 -0
- package/src/commands/attributes.ts +139 -0
- package/src/commands/campaign-blueprints.ts +106 -0
- package/src/commands/campaigns.ts +469 -0
- package/src/commands/config.ts +77 -0
- package/src/commands/contacts.ts +612 -0
- package/src/commands/custom-attributes.ts +127 -0
- package/src/commands/dkims.ts +117 -0
- package/src/commands/domains.ts +82 -0
- package/src/commands/email-apis.ts +569 -0
- package/src/commands/emails.ts +197 -0
- package/src/commands/forms.ts +283 -0
- package/src/commands/interests.ts +155 -0
- package/src/commands/links.ts +38 -0
- package/src/commands/lists.ts +406 -0
- package/src/commands/logos.ts +71 -0
- package/src/commands/logs.ts +386 -0
- package/src/commands/reports.ts +306 -0
- package/src/commands/segments.ts +158 -0
- package/src/commands/senders.ts +204 -0
- package/src/commands/sub-accounts.ts +271 -0
- package/src/commands/suppressed-emails.ts +234 -0
- package/src/commands/suppressed.ts +198 -0
- package/src/commands/system-emails.ts +85 -0
- package/src/commands/tags.ts +146 -0
- package/src/commands/tasks.ts +116 -0
- package/src/commands/templates.ts +189 -0
- package/src/commands/tokens.ts +83 -0
- package/src/commands/transactional-emails.ts +374 -0
- package/src/commands/transactional-templates.ts +385 -0
- package/src/commands/users.ts +506 -0
- package/src/commands/webhooks.ts +172 -0
- package/src/commands/workflow-blueprints.ts +123 -0
- package/src/commands/workflows.ts +265 -0
- package/src/types/profile.ts +93 -0
- package/src/utils/auth.ts +272 -0
- package/src/utils/config-file.ts +96 -0
- package/src/utils/config.ts +134 -0
- package/src/utils/confirm.ts +32 -0
- package/src/utils/defaults.ts +99 -0
- package/src/utils/errors.ts +116 -0
- package/src/utils/interactive.ts +91 -0
- package/src/utils/list-defaults.ts +74 -0
- package/src/utils/output.ts +190 -0
- package/src/utils/progress.ts +320 -0
- package/src/utils/spinner.ts +22 -0
- package/tests/IMPLEMENTATION_STATUS.md +258 -0
- package/tests/PTY_SETUP.md +118 -0
- package/tests/PTY_TESTING_GUIDE.md +507 -0
- package/tests/README.md +244 -0
- package/tests/fixtures/api-responses/campaigns.json +34 -0
- package/tests/fixtures/test-config.json +13 -0
- package/tests/helpers/cli-runner.ts +128 -0
- package/tests/helpers/mock-server.ts +301 -0
- package/tests/helpers/pty-runner.ts +181 -0
- package/tests/integration/campaigns-real-api.test.ts +196 -0
- package/tests/integration/setup-integration.ts +50 -0
- package/tests/pty/campaigns.test.ts +241 -0
- package/tests/setup.ts +34 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +28 -0
package/tests/README.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Cakemail CLI Test Suite
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This is a **hybrid test suite** combining fast mocked tests with real API integration tests, providing comprehensive coverage of all 136 commands.
|
|
6
|
+
|
|
7
|
+
## Test Strategy: Hybrid Approach ⭐
|
|
8
|
+
|
|
9
|
+
We use **two complementary testing strategies**:
|
|
10
|
+
|
|
11
|
+
### 1. **Mocked E2E Tests** (Fast - 90% of tests)
|
|
12
|
+
- ✅ Execute real CLI binary
|
|
13
|
+
- ✅ Mock API responses with nock
|
|
14
|
+
- ✅ Test command logic, output formatting, error handling
|
|
15
|
+
- ✅ Run in ~seconds (no network calls)
|
|
16
|
+
- 🎯 Use for: Unit-style testing of all commands
|
|
17
|
+
|
|
18
|
+
### 2. **Real API Integration Tests** (Slow - 10% of tests)
|
|
19
|
+
- ✅ Make actual HTTP requests to Cakemail API
|
|
20
|
+
- ✅ Test real authentication flow
|
|
21
|
+
- ✅ Test actual data creation/modification
|
|
22
|
+
- ✅ Test full command lifecycle
|
|
23
|
+
- 🎯 Use for: Critical paths and end-to-end validation
|
|
24
|
+
|
|
25
|
+
## Current Status
|
|
26
|
+
|
|
27
|
+
✅ **Phase 1 Complete: Hybrid Test Infrastructure**
|
|
28
|
+
- Test framework setup (Vitest)
|
|
29
|
+
- Mocked E2E tests with nock
|
|
30
|
+
- Real API integration tests
|
|
31
|
+
- Helper utilities (CLI runner, API mocking)
|
|
32
|
+
- Environment configuration (.env.test)
|
|
33
|
+
- First 26 tests (20 mocked + 6 integration)
|
|
34
|
+
|
|
35
|
+
### Test Results
|
|
36
|
+
- **Mocked E2E Tests**: 20 tests created
|
|
37
|
+
- **Integration Tests**: 6 tests created (skipped without credentials)
|
|
38
|
+
- **Total Coverage**: Campaigns command group (both mocked and real API)
|
|
39
|
+
|
|
40
|
+
## Setup
|
|
41
|
+
|
|
42
|
+
### For Mocked Tests (Fast)
|
|
43
|
+
No setup required! Just run:
|
|
44
|
+
```bash
|
|
45
|
+
npm test
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### For Integration Tests (Real API)
|
|
49
|
+
1. Copy environment template:
|
|
50
|
+
```bash
|
|
51
|
+
cp .env.test.example .env.test
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
2. Edit `.env.test` and add your test credentials:
|
|
55
|
+
```bash
|
|
56
|
+
CAKEMAIL_TEST_EMAIL=your-test@example.com
|
|
57
|
+
CAKEMAIL_TEST_PASSWORD=your-test-password
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
3. Run integration tests:
|
|
61
|
+
```bash
|
|
62
|
+
npm run test:integration
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
⚠️ **Important**: Use a TEST account, not production! Integration tests create/modify/delete data.
|
|
66
|
+
|
|
67
|
+
## Test Architecture
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
tests/
|
|
71
|
+
├── e2e/ # End-to-end CLI tests (20/136 commands)
|
|
72
|
+
│ └── campaigns.test.ts # ✅ Created (20 tests)
|
|
73
|
+
├── integration/ # API integration tests
|
|
74
|
+
├── unit/ # Unit tests for utilities
|
|
75
|
+
├── helpers/
|
|
76
|
+
│ ├── cli-runner.ts # ✅ CLI execution helper
|
|
77
|
+
│ └── mock-api.ts # ✅ API mocking utilities
|
|
78
|
+
└── fixtures/ # ✅ Test data
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Running Tests
|
|
82
|
+
|
|
83
|
+
### Mocked Tests (Fast)
|
|
84
|
+
```bash
|
|
85
|
+
# Run all mocked E2E tests
|
|
86
|
+
npm run test:e2e
|
|
87
|
+
|
|
88
|
+
# Run specific test file
|
|
89
|
+
npx vitest run tests/e2e/campaigns.test.ts
|
|
90
|
+
|
|
91
|
+
# Watch mode (during development)
|
|
92
|
+
npm run test:watch
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Integration Tests (Real API)
|
|
96
|
+
```bash
|
|
97
|
+
# Run all integration tests
|
|
98
|
+
npm run test:integration
|
|
99
|
+
|
|
100
|
+
# Run specific integration test
|
|
101
|
+
npx vitest run tests/integration/campaigns-real-api.test.ts
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Combined
|
|
105
|
+
```bash
|
|
106
|
+
# Run ALL tests (mocked + integration)
|
|
107
|
+
npm test
|
|
108
|
+
|
|
109
|
+
# Run with coverage report
|
|
110
|
+
npm run test:coverage
|
|
111
|
+
|
|
112
|
+
# CI/CD mode (with JUnit reporter)
|
|
113
|
+
npm run test:ci
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Test Coverage Goals
|
|
117
|
+
|
|
118
|
+
| Category | Commands | Tests | Status |
|
|
119
|
+
|----------|----------|-------|--------|
|
|
120
|
+
| Campaigns | 15 | 20 | 🟡 In Progress |
|
|
121
|
+
| Lists | 10 | 0 | ⚪ Pending |
|
|
122
|
+
| Contacts | 12 | 0 | ⚪ Pending |
|
|
123
|
+
| Templates | 8 | 0 | ⚪ Pending |
|
|
124
|
+
| Senders | 7 | 0 | ⚪ Pending |
|
|
125
|
+
| Webhooks | 5 | 0 | ⚪ Pending |
|
|
126
|
+
| Tags | 5 | 0 | ⚪ Pending |
|
|
127
|
+
| Interests | 7 | 0 | ⚪ Pending |
|
|
128
|
+
| Transactional Templates | 12 | 0 | ⚪ Pending |
|
|
129
|
+
| Logs | 3 | 0 | ⚪ Pending |
|
|
130
|
+
| Reports | 12 | 0 | ⚪ Pending |
|
|
131
|
+
| Segments | 6 | 0 | ⚪ Pending |
|
|
132
|
+
| Attributes | 4 | 0 | ⚪ Pending |
|
|
133
|
+
| Suppressed | 7 | 0 | ⚪ Pending |
|
|
134
|
+
| Account | 5 | 0 | ⚪ Pending |
|
|
135
|
+
| Emails | 8 | 0 | ⚪ Pending |
|
|
136
|
+
| Config | 6 | 0 | ⚪ Pending |
|
|
137
|
+
| **Total** | **136** | **20** | **15%** |
|
|
138
|
+
|
|
139
|
+
## Next Steps
|
|
140
|
+
|
|
141
|
+
### Immediate (Fix Failing Tests)
|
|
142
|
+
1. ✅ Install test dependencies
|
|
143
|
+
2. ✅ Create test infrastructure
|
|
144
|
+
3. ✅ Write first 20 E2E tests
|
|
145
|
+
4. 🔄 Fix nock/SDK integration issue
|
|
146
|
+
5. ⚪ Get all 20 campaigns tests passing
|
|
147
|
+
|
|
148
|
+
### Phase 2: Core Commands (Weeks 3-6)
|
|
149
|
+
- Lists commands (10 tests)
|
|
150
|
+
- Contacts commands (12 tests)
|
|
151
|
+
- Templates commands (8 tests)
|
|
152
|
+
- Senders commands (7 tests)
|
|
153
|
+
- Webhooks commands (5 tests)
|
|
154
|
+
|
|
155
|
+
### Phase 3: New Features (Weeks 7-8)
|
|
156
|
+
- Tags commands (5 tests)
|
|
157
|
+
- Interests commands (7 tests)
|
|
158
|
+
- Transactional templates (12 tests)
|
|
159
|
+
- Logs commands (3 tests)
|
|
160
|
+
|
|
161
|
+
### Phase 4: Advanced (Weeks 9-10)
|
|
162
|
+
- Reports & analytics (12 tests)
|
|
163
|
+
- Segments (6 tests)
|
|
164
|
+
- Attributes (4 tests)
|
|
165
|
+
- Profile system (15 tests)
|
|
166
|
+
- Interactive features (20 tests)
|
|
167
|
+
|
|
168
|
+
## Writing Tests
|
|
169
|
+
|
|
170
|
+
### Example Test
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
174
|
+
import { runCLISuccess, parseJSONOutput } from '../helpers/cli-runner';
|
|
175
|
+
import { mockCampaignsList } from '../helpers/mock-api';
|
|
176
|
+
|
|
177
|
+
describe('campaigns list', () => {
|
|
178
|
+
it('should list campaigns in JSON format', async () => {
|
|
179
|
+
mockCampaignsList([
|
|
180
|
+
{ id: 1, name: 'Test Campaign', status: 'draft' }
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
const result = await runCLISuccess(['campaigns', 'list']);
|
|
184
|
+
|
|
185
|
+
expect(result.exitCode).toBe(0);
|
|
186
|
+
const output = parseJSONOutput(result.stdout);
|
|
187
|
+
expect(output.data).toHaveLength(1);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Testing Patterns
|
|
193
|
+
|
|
194
|
+
1. **Command Execution**: Test that command runs without error
|
|
195
|
+
2. **Output Formats**: Test JSON, table, and compact formats
|
|
196
|
+
3. **Options**: Test all flags and parameters
|
|
197
|
+
4. **Error Handling**: Test API errors (401, 404, 500, etc.)
|
|
198
|
+
5. **Profile Support**: Test with developer/marketer/balanced profiles
|
|
199
|
+
6. **Interactive Features**: Test prompts and confirmations
|
|
200
|
+
|
|
201
|
+
## Troubleshooting
|
|
202
|
+
|
|
203
|
+
### "Cannot find module" errors
|
|
204
|
+
```bash
|
|
205
|
+
npm run build
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### "HTTP request not mocked" errors
|
|
209
|
+
Check that mock API is properly set up in test setup.
|
|
210
|
+
|
|
211
|
+
### Tests timing out
|
|
212
|
+
Increase timeout in vitest.config.ts:
|
|
213
|
+
```typescript
|
|
214
|
+
testTimeout: 60000 // 60 seconds
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Contributing
|
|
218
|
+
|
|
219
|
+
When adding new commands:
|
|
220
|
+
1. Create corresponding test file in `tests/e2e/`
|
|
221
|
+
2. Add at least 5 tests per command:
|
|
222
|
+
- Basic execution
|
|
223
|
+
- Output formats (3 tests)
|
|
224
|
+
- Error handling
|
|
225
|
+
3. Update coverage table in this README
|
|
226
|
+
4. Ensure all tests pass before committing
|
|
227
|
+
|
|
228
|
+
## CI/CD Integration
|
|
229
|
+
|
|
230
|
+
Tests run automatically on:
|
|
231
|
+
- Every pull request
|
|
232
|
+
- Every commit to main
|
|
233
|
+
- Scheduled nightly runs
|
|
234
|
+
|
|
235
|
+
Coverage reports are published to:
|
|
236
|
+
- Pull request comments
|
|
237
|
+
- GitHub Actions artifacts
|
|
238
|
+
- Coverage dashboard (TBD)
|
|
239
|
+
|
|
240
|
+
## Resources
|
|
241
|
+
|
|
242
|
+
- [Vitest Documentation](https://vitest.dev/)
|
|
243
|
+
- [Testing Best Practices](https://testingjavascript.com/)
|
|
244
|
+
- [CLI Testing Guide](https://github.com/testing-library/cli-testing-library)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"singleCampaign": {
|
|
3
|
+
"id": 123,
|
|
4
|
+
"name": "Test Campaign",
|
|
5
|
+
"status": "draft",
|
|
6
|
+
"list_id": 1,
|
|
7
|
+
"sender_id": 1,
|
|
8
|
+
"subject": "Test Subject",
|
|
9
|
+
"html": "<h1>Test</h1>",
|
|
10
|
+
"created_on": "2025-01-01T00:00:00Z",
|
|
11
|
+
"updated_on": "2025-01-01T00:00:00Z"
|
|
12
|
+
},
|
|
13
|
+
"campaignsList": {
|
|
14
|
+
"data": [
|
|
15
|
+
{
|
|
16
|
+
"id": 1,
|
|
17
|
+
"name": "Campaign 1",
|
|
18
|
+
"status": "delivered",
|
|
19
|
+
"list_id": 1,
|
|
20
|
+
"sender_id": 1,
|
|
21
|
+
"created_on": "2025-01-01T00:00:00Z"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": 2,
|
|
25
|
+
"name": "Campaign 2",
|
|
26
|
+
"status": "draft",
|
|
27
|
+
"list_id": 1,
|
|
28
|
+
"sender_id": 1,
|
|
29
|
+
"created_on": "2025-01-02T00:00:00Z"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"count": 2
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"testCredentials": {
|
|
3
|
+
"accessToken": "test-access-token",
|
|
4
|
+
"email": "test@example.com",
|
|
5
|
+
"password": "test-password"
|
|
6
|
+
},
|
|
7
|
+
"testAccount": {
|
|
8
|
+
"id": 1,
|
|
9
|
+
"email": "test@example.com",
|
|
10
|
+
"name": "Test Account"
|
|
11
|
+
},
|
|
12
|
+
"apiBase": "https://api.cakemail.dev"
|
|
13
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { execa, type ExecaReturnValue } from 'execa';
|
|
2
|
+
import stripAnsi from 'strip-ansi';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
export interface CLIResult {
|
|
6
|
+
exitCode: number;
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
rawStdout: string;
|
|
10
|
+
rawStderr: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CLIOptions {
|
|
14
|
+
stdin?: string;
|
|
15
|
+
env?: Record<string, string>;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Execute the CLI with given arguments
|
|
22
|
+
*/
|
|
23
|
+
export async function runCLI(
|
|
24
|
+
args: string[],
|
|
25
|
+
options: CLIOptions = {}
|
|
26
|
+
): Promise<CLIResult> {
|
|
27
|
+
const cliPath = join(process.cwd(), 'dist', 'cli.js');
|
|
28
|
+
|
|
29
|
+
const env = {
|
|
30
|
+
// Inherit process environment (for credentials)
|
|
31
|
+
...process.env,
|
|
32
|
+
// Default test environment
|
|
33
|
+
CAKEMAIL_BATCH_MODE: 'true',
|
|
34
|
+
FORCE_COLOR: '0', // Disable colors for easier testing
|
|
35
|
+
// Override with custom env
|
|
36
|
+
...options.env
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const result: ExecaReturnValue = await execa('node', [cliPath, ...args], {
|
|
41
|
+
env,
|
|
42
|
+
input: options.stdin,
|
|
43
|
+
cwd: options.cwd || process.cwd(),
|
|
44
|
+
timeout: options.timeout || 30000,
|
|
45
|
+
reject: false, // Don't throw on non-zero exit
|
|
46
|
+
all: true
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
exitCode: result.exitCode,
|
|
51
|
+
stdout: stripAnsi(result.stdout),
|
|
52
|
+
stderr: stripAnsi(result.stderr),
|
|
53
|
+
rawStdout: result.stdout,
|
|
54
|
+
rawStderr: result.stderr
|
|
55
|
+
};
|
|
56
|
+
} catch (error: any) {
|
|
57
|
+
// Handle execution errors (timeout, etc.)
|
|
58
|
+
return {
|
|
59
|
+
exitCode: error.exitCode || 1,
|
|
60
|
+
stdout: error.stdout ? stripAnsi(error.stdout) : '',
|
|
61
|
+
stderr: error.stderr ? stripAnsi(error.stderr) : error.message,
|
|
62
|
+
rawStdout: error.stdout || '',
|
|
63
|
+
rawStderr: error.stderr || error.message
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Run CLI and expect success
|
|
70
|
+
*/
|
|
71
|
+
export async function runCLISuccess(
|
|
72
|
+
args: string[],
|
|
73
|
+
options: CLIOptions = {}
|
|
74
|
+
): Promise<CLIResult> {
|
|
75
|
+
const result = await runCLI(args, options);
|
|
76
|
+
if (result.exitCode !== 0) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`CLI command failed with exit code ${result.exitCode}\n` +
|
|
79
|
+
`Command: cakemail ${args.join(' ')}\n` +
|
|
80
|
+
`Stderr: ${result.stderr}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Run CLI and expect failure
|
|
88
|
+
*/
|
|
89
|
+
export async function runCLIFailure(
|
|
90
|
+
args: string[],
|
|
91
|
+
options: CLIOptions = {}
|
|
92
|
+
): Promise<CLIResult> {
|
|
93
|
+
const result = await runCLI(args, options);
|
|
94
|
+
if (result.exitCode === 0) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`CLI command succeeded when failure was expected\n` +
|
|
97
|
+
`Command: cakemail ${args.join(' ')}\n` +
|
|
98
|
+
`Stdout: ${result.stdout}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parse JSON output from CLI
|
|
106
|
+
*/
|
|
107
|
+
export function parseJSONOutput(stdout: string): any {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(stdout);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new Error(`Failed to parse CLI output as JSON:\n${stdout}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Setup test environment
|
|
117
|
+
*/
|
|
118
|
+
export function setupTestEnv(): void {
|
|
119
|
+
// Build the CLI before running tests
|
|
120
|
+
// This should be done once before all tests
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Cleanup test environment
|
|
125
|
+
*/
|
|
126
|
+
export function cleanupTestEnv(): void {
|
|
127
|
+
// Clean up any test artifacts
|
|
128
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import express, { Express } from 'express';
|
|
2
|
+
import { Server } from 'http';
|
|
3
|
+
import { AddressInfo } from 'net';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a mock Cakemail API server for testing
|
|
7
|
+
*
|
|
8
|
+
* This is a REAL HTTP server that runs on localhost.
|
|
9
|
+
* The CLI subprocess can make real HTTP requests to it.
|
|
10
|
+
*/
|
|
11
|
+
export function createMockAPI(): Express {
|
|
12
|
+
const app = express();
|
|
13
|
+
app.use(express.json());
|
|
14
|
+
|
|
15
|
+
// OAuth2 Authentication
|
|
16
|
+
app.post('/token', (req, res) => {
|
|
17
|
+
const { username, password } = req.body;
|
|
18
|
+
|
|
19
|
+
if (username && password) {
|
|
20
|
+
res.json({
|
|
21
|
+
access_token: 'mock-access-token',
|
|
22
|
+
token_type: 'Bearer',
|
|
23
|
+
expires_in: 3600,
|
|
24
|
+
refresh_token: 'mock-refresh-token',
|
|
25
|
+
scope: 'read write'
|
|
26
|
+
});
|
|
27
|
+
} else {
|
|
28
|
+
res.status(401).json({
|
|
29
|
+
error: 'invalid_grant',
|
|
30
|
+
error_description: 'Invalid credentials'
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Account endpoints (for OAuth flow)
|
|
36
|
+
app.get('/accounts', (req, res) => {
|
|
37
|
+
res.json({
|
|
38
|
+
data: [
|
|
39
|
+
{ id: 1, name: 'Test Account', email: 'test@example.com', is_master: true }
|
|
40
|
+
],
|
|
41
|
+
count: 1
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
app.get('/account', (req, res) => {
|
|
46
|
+
res.json({
|
|
47
|
+
id: 1,
|
|
48
|
+
name: 'Test Account',
|
|
49
|
+
email: 'test@example.com',
|
|
50
|
+
is_master: true
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Campaigns
|
|
55
|
+
app.get('/campaigns', (req, res) => {
|
|
56
|
+
res.json({
|
|
57
|
+
data: [
|
|
58
|
+
{ id: 1, name: 'Test Campaign 1', status: 'draft', created_on: '2025-01-01' },
|
|
59
|
+
{ id: 2, name: 'Test Campaign 2', status: 'sent', created_on: '2025-01-02' }
|
|
60
|
+
],
|
|
61
|
+
count: 2,
|
|
62
|
+
_links: {}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
app.get('/campaigns/:id', (req, res) => {
|
|
67
|
+
const id = parseInt(req.params.id);
|
|
68
|
+
if (id === 999) {
|
|
69
|
+
return res.status(404).json({ error: 'Campaign not found' });
|
|
70
|
+
}
|
|
71
|
+
res.json({
|
|
72
|
+
id,
|
|
73
|
+
name: `Campaign ${id}`,
|
|
74
|
+
status: 'draft',
|
|
75
|
+
list_id: 1,
|
|
76
|
+
sender_id: 1,
|
|
77
|
+
created_on: '2025-01-01T00:00:00Z'
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
app.post('/campaigns', (req, res) => {
|
|
82
|
+
const { name, list_id, sender_id } = req.body;
|
|
83
|
+
if (!name) {
|
|
84
|
+
return res.status(422).json({ error: 'Name is required' });
|
|
85
|
+
}
|
|
86
|
+
res.status(201).json({
|
|
87
|
+
id: 123,
|
|
88
|
+
name,
|
|
89
|
+
list_id,
|
|
90
|
+
sender_id,
|
|
91
|
+
status: 'draft',
|
|
92
|
+
created_on: new Date().toISOString()
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
app.patch('/campaigns/:id', (req, res) => {
|
|
97
|
+
const id = parseInt(req.params.id);
|
|
98
|
+
res.json({
|
|
99
|
+
id,
|
|
100
|
+
...req.body,
|
|
101
|
+
updated_on: new Date().toISOString()
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
app.delete('/campaigns/:id', (req, res) => {
|
|
106
|
+
res.status(204).send();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Lists
|
|
110
|
+
app.get('/lists', (req, res) => {
|
|
111
|
+
res.json({
|
|
112
|
+
data: [
|
|
113
|
+
{ id: 1, name: 'Main List', status: 'active', active_contacts: 100 },
|
|
114
|
+
{ id: 2, name: 'VIP List', status: 'active', active_contacts: 25 }
|
|
115
|
+
],
|
|
116
|
+
count: 2
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
app.get('/lists/:id', (req, res) => {
|
|
121
|
+
const id = parseInt(req.params.id);
|
|
122
|
+
res.json({
|
|
123
|
+
id,
|
|
124
|
+
name: `List ${id}`,
|
|
125
|
+
status: 'active',
|
|
126
|
+
active_contacts: 100,
|
|
127
|
+
created_on: '2025-01-01T00:00:00Z'
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
app.post('/lists', (req, res) => {
|
|
132
|
+
const { name } = req.body;
|
|
133
|
+
res.status(201).json({
|
|
134
|
+
id: 456,
|
|
135
|
+
name,
|
|
136
|
+
status: 'active',
|
|
137
|
+
created_on: new Date().toISOString()
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
app.patch('/lists/:id', (req, res) => {
|
|
142
|
+
const id = parseInt(req.params.id);
|
|
143
|
+
res.json({ id, ...req.body });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
app.delete('/lists/:id', (req, res) => {
|
|
147
|
+
res.status(204).send();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Contacts
|
|
151
|
+
app.get('/contacts', (req, res) => {
|
|
152
|
+
res.json({
|
|
153
|
+
data: [
|
|
154
|
+
{ id: 1, email: 'user1@example.com', status: 'active' },
|
|
155
|
+
{ id: 2, email: 'user2@example.com', status: 'active' }
|
|
156
|
+
],
|
|
157
|
+
count: 2
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
app.get('/contacts/:id', (req, res) => {
|
|
162
|
+
const id = parseInt(req.params.id);
|
|
163
|
+
res.json({
|
|
164
|
+
id,
|
|
165
|
+
email: `user${id}@example.com`,
|
|
166
|
+
first_name: 'Test',
|
|
167
|
+
last_name: 'User',
|
|
168
|
+
status: 'active'
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
app.post('/contacts', (req, res) => {
|
|
173
|
+
const { email, list_ids } = req.body;
|
|
174
|
+
res.status(201).json({
|
|
175
|
+
id: 789,
|
|
176
|
+
email,
|
|
177
|
+
list_ids,
|
|
178
|
+
status: 'active',
|
|
179
|
+
created_on: new Date().toISOString()
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
app.patch('/contacts/:id', (req, res) => {
|
|
184
|
+
const id = parseInt(req.params.id);
|
|
185
|
+
res.json({ id, ...req.body });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
app.delete('/contacts/:id', (req, res) => {
|
|
189
|
+
res.status(204).send();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Senders
|
|
193
|
+
app.get('/senders', (req, res) => {
|
|
194
|
+
res.json({
|
|
195
|
+
data: [
|
|
196
|
+
{ id: 1, name: 'Support', email: 'support@example.com', confirmed: true },
|
|
197
|
+
{ id: 2, name: 'Marketing', email: 'marketing@example.com', confirmed: false }
|
|
198
|
+
],
|
|
199
|
+
count: 2
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
app.get('/senders/:id', (req, res) => {
|
|
204
|
+
const id = parseInt(req.params.id);
|
|
205
|
+
res.json({
|
|
206
|
+
id,
|
|
207
|
+
name: `Sender ${id}`,
|
|
208
|
+
email: `sender${id}@example.com`,
|
|
209
|
+
confirmed: true
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
app.post('/senders', (req, res) => {
|
|
214
|
+
const { name, email } = req.body;
|
|
215
|
+
res.status(201).json({
|
|
216
|
+
id: 999,
|
|
217
|
+
name,
|
|
218
|
+
email,
|
|
219
|
+
confirmed: false,
|
|
220
|
+
created_on: new Date().toISOString()
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
app.patch('/senders/:id', (req, res) => {
|
|
225
|
+
const id = parseInt(req.params.id);
|
|
226
|
+
res.json({ id, ...req.body });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
app.delete('/senders/:id', (req, res) => {
|
|
230
|
+
res.status(204).send();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Templates
|
|
234
|
+
app.get('/templates', (req, res) => {
|
|
235
|
+
res.json({
|
|
236
|
+
data: [
|
|
237
|
+
{ id: 1, name: 'Welcome Email', tags: ['onboarding'] },
|
|
238
|
+
{ id: 2, name: 'Newsletter', tags: ['marketing'] }
|
|
239
|
+
],
|
|
240
|
+
count: 2
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
app.get('/templates/:id', (req, res) => {
|
|
245
|
+
const id = parseInt(req.params.id);
|
|
246
|
+
res.json({
|
|
247
|
+
id,
|
|
248
|
+
name: `Template ${id}`,
|
|
249
|
+
html: '<h1>Hello</h1>',
|
|
250
|
+
text: 'Hello',
|
|
251
|
+
subject: 'Test Subject'
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
app.post('/templates', (req, res) => {
|
|
256
|
+
const { name } = req.body;
|
|
257
|
+
res.status(201).json({
|
|
258
|
+
id: 888,
|
|
259
|
+
name,
|
|
260
|
+
created_on: new Date().toISOString()
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
app.patch('/templates/:id', (req, res) => {
|
|
265
|
+
const id = parseInt(req.params.id);
|
|
266
|
+
res.json({ id, ...req.body });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
app.delete('/templates/:id', (req, res) => {
|
|
270
|
+
res.status(204).send();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return app;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Start mock server on random available port
|
|
278
|
+
*/
|
|
279
|
+
export async function startMockServer(): Promise<{ server: Server; port: number; url: string }> {
|
|
280
|
+
const app = createMockAPI();
|
|
281
|
+
|
|
282
|
+
return new Promise((resolve) => {
|
|
283
|
+
const server = app.listen(0, () => {
|
|
284
|
+
const port = (server.address() as AddressInfo).port;
|
|
285
|
+
const url = `http://localhost:${port}`;
|
|
286
|
+
resolve({ server, port, url });
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Stop mock server
|
|
293
|
+
*/
|
|
294
|
+
export async function stopMockServer(server: Server): Promise<void> {
|
|
295
|
+
return new Promise((resolve, reject) => {
|
|
296
|
+
server.close((err) => {
|
|
297
|
+
if (err) reject(err);
|
|
298
|
+
else resolve();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|