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.
- 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 +35 -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 +74 -30
- package/docs/authentication.md +600 -12
- package/docs/plugins/DEVELOPMENT.md +148 -76
- package/docs/testing-with-embedded-ldap.md +342 -0
- package/docs/twakeAppAccountsApi.md +485 -0
- package/docs/weblogs.md +36 -8
- package/examples/web/README.md +166 -0
- package/examples/web/ldap-tree-viewer.html +16 -2
- package/examples/web/totp-client.html +543 -0
- package/package.json +46 -1
- 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,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(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
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({
|
|
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:
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|