ldap-rest 0.0.1 → 0.1.1

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