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.
- package/README.md +38 -0
- package/bin/sync-app-accounts.mjs +276 -0
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/abstract/plugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/logger/winston.js +1 -1
- package/dist/plugins/auth/hmac.js +1 -0
- package/dist/plugins/auth/totp.js +1 -0
- package/dist/plugins/demo/helloworld.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/onChange.js +1 -1
- package/dist/plugins/static.js +1 -1
- package/dist/plugins/twake/appAccountsApi.js +1 -0
- package/dist/plugins/twake/appAccountsConsistency.js +1 -0
- package/dist/plugins/twake/james.js +1 -1
- package/dist/plugins/weblogs.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +0 -5
- package/dist/src/abstract/plugin.d.ts +1 -0
- package/dist/src/bin/index.d.ts +1 -0
- package/dist/src/browser/shared/index.d.ts +1 -0
- package/dist/src/browser/shared/utils/hmac.d.ts +73 -0
- package/dist/src/browser/shared/utils/totp.d.ts +64 -0
- package/dist/src/config/args.d.ts +9 -1
- package/dist/src/hooks.d.ts +2 -2
- package/dist/src/lib/ldapActions.d.ts +3 -7
- package/dist/src/lib/utils.d.ts +17 -0
- package/dist/src/logger/winston.d.ts +5 -0
- package/dist/src/plugins/auth/hmac.d.ts +24 -0
- package/dist/src/plugins/auth/totp.d.ts +28 -0
- package/dist/src/plugins/ldap/onChange.d.ts +1 -0
- package/dist/src/plugins/static.d.ts +4 -0
- package/dist/src/plugins/twake/appAccountsApi.d.ts +52 -0
- package/dist/src/plugins/twake/appAccountsConsistency.d.ts +58 -0
- package/docs/DEVELOPER_GUIDE.md +1 -0
- package/docs/PLUGIN_DEPENDENCIES.md +19 -0
- package/docs/README.md +76 -30
- package/docs/authentication.md +604 -12
- package/docs/plugins/DEVELOPMENT.md +162 -76
- package/docs/testing-with-embedded-ldap.md +347 -0
- package/docs/twakeAppAccountsApi.md +485 -0
- package/docs/weblogs.md +37 -8
- package/examples/web/README.md +175 -0
- package/examples/web/ldap-tree-viewer.html +16 -2
- package/examples/web/totp-client.html +541 -0
- package/package.json +132 -87
- package/static/browser/shared/index.js +1 -0
- package/static/browser/shared/index.js.map +1 -1
- package/static/browser/shared/utils/hmac.js +202 -0
- package/static/browser/shared/utils/hmac.js.map +1 -0
- package/static/browser/shared/utils/totp.js +182 -0
- package/static/browser/shared/utils/totp.js.map +1 -0
- package/static/browser/shared.esm.js +181 -1
- package/static/browser/shared.esm.js.map +1 -1
- package/static/browser/shared.js +184 -0
- package/static/browser/shared.js.map +1 -1
- package/static/browser/shared.min.js +1 -1
- package/static/browser/shared.min.js.map +1 -1
- package/static/schemas/obm/organizations.json +24 -0
- 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(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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:
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|