ldap-rest 0.0.1 → 0.1.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.
Files changed (62) hide show
  1. package/README.md +38 -0
  2. package/bin/sync-app-accounts.mjs +276 -0
  3. package/dist/abstract/ldapFlat.js +1 -1
  4. package/dist/abstract/plugin.js +1 -1
  5. package/dist/bin/index.js +1 -1
  6. package/dist/config/args.js +1 -1
  7. package/dist/lib/ldapActions.js +1 -1
  8. package/dist/lib/utils.js +1 -1
  9. package/dist/logger/winston.js +1 -1
  10. package/dist/plugins/auth/hmac.js +1 -0
  11. package/dist/plugins/auth/totp.js +1 -0
  12. package/dist/plugins/demo/helloworld.js +1 -1
  13. package/dist/plugins/ldap/groups.js +1 -1
  14. package/dist/plugins/ldap/onChange.js +1 -1
  15. package/dist/plugins/static.js +1 -1
  16. package/dist/plugins/twake/appAccountsApi.js +1 -0
  17. package/dist/plugins/twake/appAccountsConsistency.js +1 -0
  18. package/dist/plugins/twake/james.js +1 -1
  19. package/dist/plugins/weblogs.js +1 -1
  20. package/dist/src/abstract/ldapFlat.d.ts +0 -5
  21. package/dist/src/abstract/plugin.d.ts +1 -0
  22. package/dist/src/bin/index.d.ts +1 -0
  23. package/dist/src/browser/shared/index.d.ts +1 -0
  24. package/dist/src/browser/shared/utils/hmac.d.ts +73 -0
  25. package/dist/src/browser/shared/utils/totp.d.ts +64 -0
  26. package/dist/src/config/args.d.ts +9 -1
  27. package/dist/src/hooks.d.ts +2 -2
  28. package/dist/src/lib/ldapActions.d.ts +3 -7
  29. package/dist/src/lib/utils.d.ts +17 -0
  30. package/dist/src/logger/winston.d.ts +5 -0
  31. package/dist/src/plugins/auth/hmac.d.ts +24 -0
  32. package/dist/src/plugins/auth/totp.d.ts +35 -0
  33. package/dist/src/plugins/ldap/onChange.d.ts +1 -0
  34. package/dist/src/plugins/static.d.ts +4 -0
  35. package/dist/src/plugins/twake/appAccountsApi.d.ts +52 -0
  36. package/dist/src/plugins/twake/appAccountsConsistency.d.ts +58 -0
  37. package/docs/DEVELOPER_GUIDE.md +1 -0
  38. package/docs/PLUGIN_DEPENDENCIES.md +19 -0
  39. package/docs/README.md +74 -30
  40. package/docs/authentication.md +600 -12
  41. package/docs/plugins/DEVELOPMENT.md +148 -76
  42. package/docs/testing-with-embedded-ldap.md +342 -0
  43. package/docs/twakeAppAccountsApi.md +485 -0
  44. package/docs/weblogs.md +36 -8
  45. package/examples/web/README.md +166 -0
  46. package/examples/web/ldap-tree-viewer.html +16 -2
  47. package/examples/web/totp-client.html +543 -0
  48. package/package.json +46 -1
  49. package/static/browser/shared/index.js +1 -0
  50. package/static/browser/shared/index.js.map +1 -1
  51. package/static/browser/shared/utils/hmac.js +202 -0
  52. package/static/browser/shared/utils/hmac.js.map +1 -0
  53. package/static/browser/shared/utils/totp.js +182 -0
  54. package/static/browser/shared/utils/totp.js.map +1 -0
  55. package/static/browser/shared.esm.js +181 -1
  56. package/static/browser/shared.esm.js.map +1 -1
  57. package/static/browser/shared.js +184 -0
  58. package/static/browser/shared.js.map +1 -1
  59. package/static/browser/shared.min.js +1 -1
  60. package/static/browser/shared.min.js.map +1 -1
  61. package/static/schemas/obm/organizations.json +24 -0
  62. package/static/schemas/obm/users.json +192 -0
@@ -61,6 +61,49 @@ export default class MyPlugin extends DmPlugin {
61
61
  - **this.logger**: Winston logger instance
62
62
  - **this.registeredHooks**: Access to global hook registry
63
63
 
64
+ ### Error Handling and Server Resilience
65
+
66
+ LDAP-Rest includes robust error handling to ensure the server remains operational even when plugins encounter errors:
67
+
68
+ #### Global Error Handling
69
+
70
+ The server automatically catches and handles:
71
+ - **Uncaught exceptions** in plugin code
72
+ - **Unhandled promise rejections**
73
+ - **Errors in async route handlers**
74
+
75
+ All errors are logged with full stack traces without crashing the server.
76
+
77
+ #### Using asyncHandler
78
+
79
+ For async route handlers, **always use the `asyncHandler` wrapper**:
80
+
81
+ ```typescript
82
+ import DmPlugin, { asyncHandler } from '../../abstract/plugin';
83
+
84
+ export default class MyPlugin extends DmPlugin {
85
+ api(app: Express): void {
86
+ // ✅ Correct: Errors are automatically caught
87
+ app.get('/api/data', asyncHandler(async (req, res) => {
88
+ const data = await someAsyncOperation();
89
+ res.json(data);
90
+ }));
91
+
92
+ // ❌ Wrong: Errors can crash the server
93
+ app.get('/api/data', async (req, res) => {
94
+ const data = await someAsyncOperation();
95
+ res.json(data);
96
+ });
97
+ }
98
+ }
99
+ ```
100
+
101
+ The `asyncHandler` is exported from:
102
+ - `ldap-rest/abstract/plugin` (recommended for plugins)
103
+ - `ldap-rest` (for external use)
104
+
105
+ See [Exposing API Endpoints](#exposing-api-endpoints) for detailed examples.
106
+
64
107
  ---
65
108
 
66
109
  ## Creating Your Plugin
@@ -78,7 +121,7 @@ Create a file in `src/plugins/<category>/<pluginName>.ts`:
78
121
  */
79
122
  import type { Express, Request, Response } from 'express';
80
123
 
81
- import DmPlugin, { type Role } from '../../abstract/plugin';
124
+ import DmPlugin, { type Role, asyncHandler } from '../../abstract/plugin';
82
125
  import type { DM } from '../../bin';
83
126
  import type { Hooks } from '../../hooks';
84
127
 
@@ -104,12 +147,15 @@ export default class WebhookNotifier extends DmPlugin {
104
147
 
105
148
  // Register API endpoints
106
149
  api(app: Express): void {
107
- app.get(`${this.config.api_prefix}/v1/webhook/status`, (req, res) => {
108
- res.json({
109
- status: 'active',
110
- url: this.config.webhook_url,
111
- });
112
- });
150
+ app.get(
151
+ `${this.config.api_prefix}/v1/webhook/status`,
152
+ asyncHandler(async (req, res) => {
153
+ res.json({
154
+ status: 'active',
155
+ url: this.config.webhook_url,
156
+ });
157
+ })
158
+ );
113
159
  }
114
160
 
115
161
  // Register hooks
@@ -152,15 +198,17 @@ Create a standalone file anywhere on your system, e.g., `/opt/custom-plugins/my-
152
198
  * External plugin example
153
199
  * This plugin can be loaded from anywhere
154
200
  */
155
- import type { Express } from 'express';
201
+ import type { Express, Request, Response } from 'express';
156
202
 
157
203
  // Import from ldap-rest (if installed as npm package)
158
- // import DmPlugin from 'ldap-rest/plugin';
204
+ // import DmPlugin, { asyncHandler } from 'ldap-rest/plugin';
159
205
 
160
206
  // Or use relative path to node_modules
161
207
  import DmPlugin from './node_modules/ldap-rest/dist/abstract/plugin.js';
162
208
  import type { DM } from './node_modules/ldap-rest/dist/bin/index.js';
163
209
  import type { Hooks } from './node_modules/ldap-rest/dist/hooks.js';
210
+ // Import asyncHandler for error handling
211
+ import { asyncHandler } from './node_modules/ldap-rest/dist/lib/utils.js';
164
212
 
165
213
  export default class ExternalAuditPlugin extends DmPlugin {
166
214
  name = 'externalAudit';
@@ -174,12 +222,15 @@ export default class ExternalAuditPlugin extends DmPlugin {
174
222
  }
175
223
 
176
224
  api(app: Express): void {
177
- app.get(`${this.config.api_prefix}/v1/audit/stats`, (req, res) => {
178
- res.json({
179
- message: 'External plugin API',
180
- auditEnabled: true,
181
- });
182
- });
225
+ app.get(
226
+ `${this.config.api_prefix}/v1/audit/stats`,
227
+ asyncHandler(async (req: Request, res: Response) => {
228
+ res.json({
229
+ message: 'External plugin API',
230
+ auditEnabled: true,
231
+ });
232
+ })
233
+ );
183
234
  }
184
235
 
185
236
  hooks: Hooks = {
@@ -551,81 +602,109 @@ hooks: Hooks = {
551
602
 
552
603
  ### Exposing API Endpoints
553
604
 
554
- Create REST API endpoints for your plugin:
605
+ Create REST API endpoints for your plugin. **Important:** Always use the `asyncHandler` wrapper for async routes to ensure errors are properly handled and don't crash the server.
555
606
 
556
607
  ```typescript
608
+ import DmPlugin, { asyncHandler } from '../../abstract/plugin';
609
+
557
610
  api(app: Express): void {
558
- // GET endpoint
611
+ // GET endpoint with asyncHandler
559
612
  app.get(
560
613
  `${this.config.api_prefix}/v1/myplugin/status`,
561
- async (req: Request, res: Response) => {
562
- try {
563
- const status = await this.getStatus();
564
- res.json({ success: true, data: status });
565
- } catch (error) {
566
- res.status(500).json({
567
- success: false,
568
- error: error.message
569
- });
570
- }
571
- }
614
+ asyncHandler(async (req: Request, res: Response) => {
615
+ const status = await this.getStatus();
616
+ res.json({ success: true, data: status });
617
+ })
572
618
  );
573
619
 
574
- // POST endpoint with body
620
+ // POST endpoint with body validation
575
621
  app.post(
576
622
  `${this.config.api_prefix}/v1/myplugin/process`,
577
- async (req: Request, res: Response) => {
578
- try {
579
- // Validate request body
580
- if (!req.body || !req.body.data) {
581
- return res.status(400).json({
582
- success: false,
583
- error: 'data is required'
584
- });
585
- }
586
-
587
- const result = await this.processData(req.body.data);
588
- res.json({ success: true, data: result });
589
- } catch (error) {
590
- this.logger.error('Process failed:', error);
591
- res.status(500).json({
623
+ asyncHandler(async (req: Request, res: Response) => {
624
+ // Validate request body
625
+ if (!req.body || !req.body.data) {
626
+ return res.status(400).json({
592
627
  success: false,
593
- error: error.message
628
+ error: 'data is required'
594
629
  });
595
630
  }
596
- }
631
+
632
+ const result = await this.processData(req.body.data);
633
+ res.json({ success: true, data: result });
634
+ })
597
635
  );
598
636
 
599
637
  // PUT endpoint
600
638
  app.put(
601
639
  `${this.config.api_prefix}/v1/myplugin/config`,
602
- async (req: Request, res: Response) => {
603
- try {
604
- await this.updateConfig(req.body);
605
- res.json({ success: true });
606
- } catch (error) {
607
- res.status(500).json({ success: false, error: error.message });
608
- }
609
- }
640
+ asyncHandler(async (req: Request, res: Response) => {
641
+ await this.updateConfig(req.body);
642
+ res.json({ success: true });
643
+ })
610
644
  );
611
645
 
612
646
  // DELETE endpoint
613
647
  app.delete(
614
648
  `${this.config.api_prefix}/v1/myplugin/cache`,
615
- async (req: Request, res: Response) => {
616
- try {
617
- await this.clearCache();
618
- res.json({ success: true, message: 'Cache cleared' });
619
- } catch (error) {
620
- res.status(500).json({ success: false, error: error.message });
621
- }
622
- }
649
+ asyncHandler(async (req: Request, res: Response) => {
650
+ await this.clearCache();
651
+ res.json({ success: true, message: 'Cache cleared' });
652
+ })
623
653
  );
624
654
 
625
655
  this.logger.info('MyPlugin API endpoints registered');
626
656
  }
627
657
  ```
628
658
 
659
+ #### Error Handling Best Practices
660
+
661
+ The `asyncHandler` wrapper automatically catches errors and passes them to the global error middleware. This ensures:
662
+
663
+ - **Server stability**: Exceptions in routes won't crash the server
664
+ - **Consistent error responses**: All errors return proper HTTP 500 responses
665
+ - **Automatic logging**: Errors are logged with full stack traces
666
+ - **Clean code**: No need for try/catch blocks in every route
667
+
668
+ **Without asyncHandler (DON'T DO THIS):**
669
+ ```typescript
670
+ // ❌ BAD: Unhandled async errors can crash the server
671
+ app.get('/api/data', async (req, res) => {
672
+ const data = await fetchData(); // If this throws, server crashes
673
+ res.json(data);
674
+ });
675
+ ```
676
+
677
+ **With asyncHandler (RECOMMENDED):**
678
+ ```typescript
679
+ // ✅ GOOD: Errors are caught and handled gracefully
680
+ app.get('/api/data', asyncHandler(async (req, res) => {
681
+ const data = await fetchData(); // If this throws, error is logged and 500 returned
682
+ res.json(data);
683
+ }));
684
+ ```
685
+
686
+ **Custom error handling:**
687
+ ```typescript
688
+ // You can still handle specific errors manually
689
+ app.post('/api/process', asyncHandler(async (req, res) => {
690
+ try {
691
+ const result = await processData(req.body);
692
+ res.json({ success: true, data: result });
693
+ } catch (error) {
694
+ if (error instanceof ValidationError) {
695
+ // Handle specific error types
696
+ return res.status(400).json({
697
+ success: false,
698
+ error: 'Validation failed',
699
+ details: error.details
700
+ });
701
+ }
702
+ // Re-throw to let asyncHandler handle it
703
+ throw error;
704
+ }
705
+ }));
706
+ ```
707
+
629
708
  ### Logging
630
709
 
631
710
  Use the provided logger with appropriate levels:
@@ -781,7 +860,7 @@ Here's a complete, production-ready plugin that sends webhook notifications on L
781
860
  import type { Express, Request, Response } from 'express';
782
861
  import crypto from 'crypto';
783
862
 
784
- import DmPlugin, { type Role } from '../../abstract/plugin';
863
+ import DmPlugin, { type Role, asyncHandler } from '../../abstract/plugin';
785
864
  import type { DM } from '../../bin';
786
865
  import type { Hooks } from '../../hooks';
787
866
  import type { AttributesList, ModifyRequest } from '../../lib/ldapActions';
@@ -854,7 +933,7 @@ export default class NotificationPlugin extends DmPlugin {
854
933
  // Get webhook stats
855
934
  app.get(
856
935
  `${this.config.api_prefix}/v1/notifications/stats`,
857
- (req: Request, res: Response) => {
936
+ asyncHandler(async (req: Request, res: Response) => {
858
937
  res.json({
859
938
  success: true,
860
939
  data: {
@@ -863,23 +942,16 @@ export default class NotificationPlugin extends DmPlugin {
863
942
  enabledEvents: Array.from(this.enabledEvents),
864
943
  },
865
944
  });
866
- }
945
+ })
867
946
  );
868
947
 
869
948
  // Test webhook
870
949
  app.post(
871
950
  `${this.config.api_prefix}/v1/notifications/test`,
872
- async (req: Request, res: Response) => {
873
- try {
874
- await this.sendWebhook('test', 'test', { message: 'Test webhook' });
875
- res.json({ success: true, message: 'Test webhook sent' });
876
- } catch (error) {
877
- res.status(500).json({
878
- success: false,
879
- error: (error as Error).message,
880
- });
881
- }
882
- }
951
+ asyncHandler(async (req: Request, res: Response) => {
952
+ await this.sendWebhook('test', 'test', { message: 'Test webhook' });
953
+ res.json({ success: true, message: 'Test webhook sent' });
954
+ })
883
955
  );
884
956
 
885
957
  this.logger.info('NotificationPlugin API registered');
@@ -0,0 +1,342 @@
1
+ # Testing with Embedded LDAP Server
2
+
3
+ ## Overview
4
+
5
+ The test suite now includes an **embedded LDAP server** that runs automatically in Docker when no external LDAP is configured. This provides:
6
+
7
+ - ✅ **Zero-configuration testing** - Works out of the box in CI/CD
8
+ - ✅ **Isolation** - Each test run gets a fresh LDAP instance
9
+ - ✅ **Flexibility** - Use either embedded or external LDAP
10
+ - ✅ **Fast** - Server starts in ~2-3 seconds
11
+
12
+ ## How It Works
13
+
14
+ ### Automatic Server Selection
15
+
16
+ The test framework automatically detects which LDAP server to use:
17
+
18
+ ```
19
+ ┌─────────────────────────────────────┐
20
+ │ Test starts │
21
+ └──────────────┬──────────────────────┘
22
+ │
23
+ ▼
24
+ ┌─────────────────────────────────────┐
25
+ │ Check environment variables: │
26
+ │ - DM_LDAP_URL │
27
+ │ - DM_LDAP_DN │
28
+ │ - DM_LDAP_PWD │
29
+ │ - DM_LDAP_BASE │
30
+ └──────────────┬──────────────────────┘
31
+ │
32
+ ┌──────┴──────┐
33
+ │ │
34
+ ▼ ▼
35
+ ┌─────────┐ ┌──────────┐
36
+ │ All set │ │ Missing │
37
+ └────┬────┘ └─────┬────┘
38
+ │ │
39
+ ▼ ▼
40
+ ┌─────────────┐ ┌────────────────┐
41
+ │ Use │ │ Start embedded │
42
+ │ external │ │ Docker LDAP │
43
+ │ LDAP │ │ server │
44
+ └─────────────┘ └────────────────┘
45
+ ```
46
+
47
+ ### Test Output Examples
48
+
49
+ **With embedded LDAP (default):**
50
+ ```
51
+ 🚀 Setting up global test environment...
52
+
53
+ ℹ️ No external LDAP configured, starting embedded LDAP server...
54
+
55
+ Starting LDAP test server on port 36759...
56
+ ✓ LDAP test server started on port 36759 (container: ldap-test-...)
57
+ Loading initial LDIF from .../base-structure.ldif...
58
+ ✓ Initial LDIF loaded
59
+ ✓ Embedded LDAP server ready
60
+ URL: ldap://localhost:36759
61
+ Base DN: dc=example,dc=com
62
+ ```
63
+
64
+ **With external LDAP:**
65
+ ```
66
+ 🚀 Setting up global test environment...
67
+
68
+ ✓ Using external LDAP server
69
+ URL: ldap://my-server:389
70
+ Base DN: dc=company,dc=com
71
+ ```
72
+
73
+ ## Usage
74
+
75
+ ### Running Tests
76
+
77
+ **Using embedded LDAP (no setup required):**
78
+ ```bash
79
+ npm test
80
+ npm run test:one test/plugins/ldap/flatGeneric.test.ts
81
+ ```
82
+
83
+ **Using external LDAP:**
84
+ ```bash
85
+ export DM_LDAP_URL=ldap://localhost:389
86
+ export DM_LDAP_DN=cn=admin,dc=example,dc=com
87
+ export DM_LDAP_PWD=secret
88
+ export DM_LDAP_BASE=dc=example,dc=com
89
+
90
+ npm test
91
+ ```
92
+
93
+ ### In CI/CD
94
+
95
+ **GitHub Actions example:**
96
+ ```yaml
97
+ name: Tests
98
+
99
+ on: [push, pull_request]
100
+
101
+ jobs:
102
+ test:
103
+ runs-on: ubuntu-latest
104
+
105
+ steps:
106
+ - uses: actions/checkout@v3
107
+
108
+ - name: Setup Node.js
109
+ uses: actions/setup-node@v3
110
+ with:
111
+ node-version: '20'
112
+
113
+ - name: Install dependencies
114
+ run: npm ci
115
+
116
+ - name: Run tests
117
+ run: npm test
118
+ # No LDAP setup needed - embedded server starts automatically!
119
+ ```
120
+
121
+ ## LDAP Fixtures
122
+
123
+ The embedded server loads pre-configured test data:
124
+
125
+ ### Base Structure (`test/fixtures/base-structure.ldif`)
126
+
127
+ - **Branches:**
128
+ - `ou=users,dc=example,dc=com` (B2C users)
129
+ - `ou=groups,dc=example,dc=com` (B2C groups)
130
+ - `ou=nomenclature,dc=example,dc=com` (reference data)
131
+
132
+ - **Test Users:**
133
+ - `john.doe@example.com` / password: `password123`
134
+ - `jane.smith@example.com` / password: `password123`
135
+
136
+ - **Test Groups:**
137
+ - `cn=admins` (contains john.doe, jane.smith)
138
+
139
+ - **Nomenclature Data:**
140
+ - Titles: Dr, Mr, Ms
141
+ - List Types: openList, memberRestrictedList
142
+ - Mailbox Types: group, mailingList, teamMailbox
143
+
144
+ ### B2B Organizations (`test/fixtures/b2b-organizations.ldif`)
145
+
146
+ For future B2B testing:
147
+
148
+ - **Acme Corporation** (`o=acme-corp`)
149
+ - Users: alice@acme-corp.example.com, bob@acme-corp.example.com
150
+ - Groups: engineering, management
151
+
152
+ - **Beta Industries** (`o=beta-industries`)
153
+ - Users: charlie@beta-industries.example.com
154
+ - Groups: sales
155
+
156
+ ## Implementation Details
157
+
158
+ ### Key Files
159
+
160
+ ```
161
+ test/
162
+ ├── setup.ts # Global test hooks
163
+ ├── helpers/
164
+ │ ├── ldapServer.ts # LDAP Docker server management
165
+ │ ├── env.ts # Environment detection
166
+ │ └── testSetup.ts # Helper to access server in tests
167
+ └── fixtures/
168
+ ├── base-structure.ldif # B2C test data
169
+ └── b2b-organizations.ldif # B2B test data (future)
170
+ ```
171
+
172
+ ### How It Works Internally
173
+
174
+ 1. **Startup** (`test/setup.ts:mochaHooks.beforeAll`):
175
+ - Check if `DM_LDAP_*` env vars are set
176
+ - If yes → use external LDAP
177
+ - If no → start Docker container with OpenLDAP
178
+ - Load fixtures from `test/fixtures/*.ldif`
179
+ - Set environment variables for DM
180
+
181
+ 2. **During Tests**:
182
+ - All tests use `process.env.DM_LDAP_*` transparently
183
+ - Works exactly the same whether using embedded or external LDAP
184
+
185
+ 3. **Cleanup** (`test/setup.ts:mochaHooks.afterAll`):
186
+ - Stop and remove Docker container (only if embedded)
187
+ - External LDAP is left untouched
188
+
189
+ ### Environment Variables Set
190
+
191
+ When using embedded LDAP, these are automatically set:
192
+
193
+ ```bash
194
+ DM_LDAP_URL=ldap://localhost:<random-port>
195
+ DM_LDAP_DN=cn=admin,dc=example,dc=com
196
+ DM_LDAP_PWD=adminpassword
197
+ DM_LDAP_BASE=dc=example,dc=com
198
+ DM_LDAP_TOP_ORGANIZATION=dc=example,dc=com
199
+ DM_JAMES_WEBADMIN_URL=http://localhost:8000
200
+ DM_JAMES_WEBADMIN_TOKEN=test-token
201
+ ```
202
+
203
+ ## Accessing the LDAP Server in Tests
204
+
205
+ ### Standard Usage (via DM)
206
+
207
+ Most tests don't need direct LDAP access - they test via the API:
208
+
209
+ ```typescript
210
+ import { DM } from '../../../src/bin';
211
+
212
+ describe('My plugin', () => {
213
+ let server: DM;
214
+
215
+ before(async () => {
216
+ server = new DM(); // Reads env vars automatically
217
+ await server.ready;
218
+ });
219
+
220
+ it('should work', async () => {
221
+ // Test via API
222
+ const res = await request.get('/api/v1/users');
223
+ expect(res.status).to.equal(200);
224
+ });
225
+ });
226
+ ```
227
+
228
+ ### Direct LDAP Access
229
+
230
+ For tests that need direct LDAP operations:
231
+
232
+ ```typescript
233
+ import { getTestLdapServer } from '../helpers/testSetup';
234
+
235
+ describe('Advanced LDAP test', () => {
236
+ it('should perform raw LDAP operations', async () => {
237
+ const server = getTestLdapServer();
238
+
239
+ // Direct LDAP search
240
+ const result = await server.search('(uid=john.doe)', ['cn', 'mail']);
241
+ expect(result).to.include('john.doe');
242
+
243
+ // Load additional test data
244
+ await server.loadLdif(`
245
+ dn: uid=test,ou=users,dc=example,dc=com
246
+ objectClass: inetOrgPerson
247
+ uid: test
248
+ cn: Test User
249
+ sn: User
250
+ mail: test@example.com
251
+ `);
252
+ });
253
+ });
254
+ ```
255
+
256
+ ## Requirements
257
+
258
+ - **Docker** must be installed and running
259
+ - Tests will fail gracefully if Docker is not available
260
+ - Port range 30000-40000 must be available (for random port assignment)
261
+
262
+ ## Troubleshooting
263
+
264
+ ### Container Won't Start
265
+
266
+ ```bash
267
+ # Check Docker is running
268
+ docker ps
269
+
270
+ # Check for conflicting containers
271
+ docker ps -a | grep ldap-test
272
+
273
+ # Clean up old test containers
274
+ docker rm -f $(docker ps -a | grep ldap-test | awk '{print $1}')
275
+ ```
276
+
277
+ ### Tests Timeout
278
+
279
+ Increase timeout in your test if LDAP is slow to start:
280
+
281
+ ```typescript
282
+ before(async function() {
283
+ this.timeout(120000); // 2 minutes
284
+ // ...
285
+ });
286
+ ```
287
+
288
+ ### Wrong LDAP Data
289
+
290
+ The embedded server loads fresh data on each run. If you see stale data, you're likely connected to an external LDAP server. Check:
291
+
292
+ ```bash
293
+ echo $DM_LDAP_URL
294
+ # Should be empty or ldap://localhost:<high-port>
295
+ ```
296
+
297
+ ## Migration Guide
298
+
299
+ ### Existing Tests
300
+
301
+ No changes needed! Tests using `skipIfMissingEnvVars()` will now always pass:
302
+
303
+ ```typescript
304
+ // Before: Would skip if no external LDAP
305
+ import { skipIfMissingEnvVars, LDAP_ENV_VARS } from '../../helpers/env';
306
+
307
+ describe('My test', () => {
308
+ before(function() {
309
+ skipIfMissingEnvVars(this, [...LDAP_ENV_VARS]); // Still works!
310
+ });
311
+ });
312
+
313
+ // Now: Runs with embedded LDAP automatically
314
+ ```
315
+
316
+ ### New Tests
317
+
318
+ Simply write tests without any LDAP setup:
319
+
320
+ ```typescript
321
+ import { DM } from '../../../src/bin';
322
+
323
+ describe('New plugin', () => {
324
+ let server: DM;
325
+
326
+ before(async () => {
327
+ server = new DM(); // That's it!
328
+ await server.ready;
329
+ });
330
+
331
+ it('should work', () => {
332
+ // Your test here
333
+ });
334
+ });
335
+ ```
336
+
337
+ ## Future Enhancements
338
+
339
+ - [ ] Support for loading custom fixtures per test suite
340
+ - [ ] B2B organization fixtures automatically loaded
341
+ - [ ] LDAP server connection pooling across test files
342
+ - [ ] Parallel test execution with multiple LDAP containers