@scalekit-sdk/node 2.0.0 → 2.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 (79) hide show
  1. package/jest.config.js +15 -0
  2. package/lib/core.d.ts +1 -1
  3. package/lib/core.js +31 -31
  4. package/lib/core.js.map +1 -1
  5. package/lib/errors/base-exception.d.ts +32 -0
  6. package/lib/errors/base-exception.js +238 -0
  7. package/lib/errors/base-exception.js.map +1 -0
  8. package/lib/errors/index.d.ts +2 -0
  9. package/lib/errors/index.js +20 -0
  10. package/lib/errors/index.js.map +1 -0
  11. package/lib/errors/specific-exceptions.d.ts +39 -0
  12. package/lib/errors/specific-exceptions.js +90 -0
  13. package/lib/errors/specific-exceptions.js.map +1 -0
  14. package/lib/index.d.ts +1 -0
  15. package/lib/index.js +1 -0
  16. package/lib/index.js.map +1 -1
  17. package/lib/pkg/grpc/scalekit/v1/commons/commons_pb.d.ts +34 -87
  18. package/lib/pkg/grpc/scalekit/v1/commons/commons_pb.js +31 -120
  19. package/lib/pkg/grpc/scalekit/v1/commons/commons_pb.js.map +1 -1
  20. package/lib/pkg/grpc/scalekit/v1/connections/connections_connect.d.ts +19 -10
  21. package/lib/pkg/grpc/scalekit/v1/connections/connections_connect.js +18 -9
  22. package/lib/pkg/grpc/scalekit/v1/connections/connections_connect.js.map +1 -1
  23. package/lib/pkg/grpc/scalekit/v1/connections/connections_pb.d.ts +209 -6
  24. package/lib/pkg/grpc/scalekit/v1/connections/connections_pb.js +272 -5
  25. package/lib/pkg/grpc/scalekit/v1/connections/connections_pb.js.map +1 -1
  26. package/lib/pkg/grpc/scalekit/v1/domains/domains_pb.d.ts +29 -0
  27. package/lib/pkg/grpc/scalekit/v1/domains/domains_pb.js +40 -1
  28. package/lib/pkg/grpc/scalekit/v1/domains/domains_pb.js.map +1 -1
  29. package/lib/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.d.ts +25 -0
  30. package/lib/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.js +38 -1
  31. package/lib/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.js.map +1 -1
  32. package/lib/pkg/grpc/scalekit/v1/organizations/organizations_connect.d.ts +21 -1
  33. package/lib/pkg/grpc/scalekit/v1/organizations/organizations_connect.js +20 -0
  34. package/lib/pkg/grpc/scalekit/v1/organizations/organizations_connect.js.map +1 -1
  35. package/lib/pkg/grpc/scalekit/v1/organizations/organizations_pb.d.ts +110 -5
  36. package/lib/pkg/grpc/scalekit/v1/organizations/organizations_pb.js +164 -5
  37. package/lib/pkg/grpc/scalekit/v1/organizations/organizations_pb.js.map +1 -1
  38. package/lib/pkg/grpc/scalekit/v1/users/users_connect.d.ts +48 -1
  39. package/lib/pkg/grpc/scalekit/v1/users/users_connect.js +47 -0
  40. package/lib/pkg/grpc/scalekit/v1/users/users_connect.js.map +1 -1
  41. package/lib/pkg/grpc/scalekit/v1/users/users_pb.d.ts +280 -4
  42. package/lib/pkg/grpc/scalekit/v1/users/users_pb.js +449 -11
  43. package/lib/pkg/grpc/scalekit/v1/users/users_pb.js.map +1 -1
  44. package/lib/scalekit.d.ts +29 -8
  45. package/lib/scalekit.js +78 -28
  46. package/lib/scalekit.js.map +1 -1
  47. package/lib/types/scalekit.d.ts +5 -0
  48. package/lib/types/user.d.ts +1 -1
  49. package/lib/user.d.ts +10 -3
  50. package/lib/user.js +26 -5
  51. package/lib/user.js.map +1 -1
  52. package/package.json +6 -2
  53. package/src/core.ts +31 -32
  54. package/src/errors/base-exception.ts +262 -0
  55. package/src/errors/index.ts +3 -0
  56. package/src/errors/specific-exceptions.ts +88 -0
  57. package/src/index.ts +3 -1
  58. package/src/pkg/grpc/scalekit/v1/commons/commons_pb.ts +49 -129
  59. package/src/pkg/grpc/scalekit/v1/connections/connections_connect.ts +19 -10
  60. package/src/pkg/grpc/scalekit/v1/connections/connections_pb.ts +377 -8
  61. package/src/pkg/grpc/scalekit/v1/domains/domains_pb.ts +44 -0
  62. package/src/pkg/grpc/scalekit/v1/errdetails/errdetails_pb.ts +49 -0
  63. package/src/pkg/grpc/scalekit/v1/organizations/organizations_connect.ts +21 -1
  64. package/src/pkg/grpc/scalekit/v1/organizations/organizations_pb.ts +218 -5
  65. package/src/pkg/grpc/scalekit/v1/users/users_connect.ts +48 -1
  66. package/src/pkg/grpc/scalekit/v1/users/users_pb.ts +558 -6
  67. package/src/scalekit.ts +95 -30
  68. package/src/types/scalekit.ts +6 -0
  69. package/src/types/user.ts +1 -1
  70. package/src/user.ts +34 -7
  71. package/tests/README.md +25 -0
  72. package/tests/connection.test.ts +42 -0
  73. package/tests/directory.test.ts +46 -0
  74. package/tests/organization.test.ts +65 -0
  75. package/tests/passwordless.test.ts +108 -0
  76. package/tests/scalekit.test.ts +104 -0
  77. package/tests/setup.ts +34 -0
  78. package/tests/users.test.ts +168 -0
  79. package/tests/utils/test-data.ts +248 -0
@@ -0,0 +1,104 @@
1
+ import ScalekitClient from '../src/scalekit';
2
+ import { AuthenticationOptions } from '../src/types/scalekit';
3
+ import { describe, it, expect, beforeEach } from '@jest/globals';
4
+ import { TestDataGenerator } from './utils/test-data';
5
+
6
+ describe('ScalekitClient', () => {
7
+ let client: ScalekitClient;
8
+
9
+ beforeEach(() => {
10
+ // Use global client
11
+ client = global.client;
12
+ });
13
+
14
+ describe('constructor', () => {
15
+ it('should initialize with correct parameters', () => {
16
+ expect(client).toBeInstanceOf(ScalekitClient);
17
+ expect(client.organization).toBeDefined();
18
+ expect(client.user).toBeDefined();
19
+ expect(client.connection).toBeDefined();
20
+ expect(client.directory).toBeDefined();
21
+ expect(client.passwordless).toBeDefined();
22
+ expect(client.domain).toBeDefined();
23
+ });
24
+ });
25
+
26
+ describe('getAuthorizationUrl', () => {
27
+ it('should generate authorization URL with basic parameters', () => {
28
+ const redirectUri = 'https://example.com/callback';
29
+ const url = client.getAuthorizationUrl(redirectUri);
30
+
31
+ expect(url).toContain('oauth/authorize');
32
+ expect(url).toContain(`redirect_uri=${encodeURIComponent(redirectUri)}`);
33
+ expect(url).toContain('response_type=code');
34
+ });
35
+
36
+ it('should include optional parameters when provided', () => {
37
+ const redirectUri = 'https://example.com/callback';
38
+ const options = TestDataGenerator.generateAuthorizationUrlOptions();
39
+
40
+ const url = client.getAuthorizationUrl(redirectUri, options);
41
+
42
+ expect(url).toContain('scope=openid%20profile');
43
+ expect(url).toContain('state=test-state');
44
+ expect(url).toContain('nonce=test-nonce');
45
+ expect(url).toContain('prompt=login');
46
+ });
47
+
48
+ it('should handle PKCE parameters', () => {
49
+ const redirectUri = 'https://example.com/callback';
50
+ const options = TestDataGenerator.generatePKCEParams();
51
+
52
+ const url = client.getAuthorizationUrl(redirectUri, options);
53
+
54
+ expect(url).toContain('code_challenge=test-challenge');
55
+ expect(url).toContain('code_challenge_method=S256');
56
+ });
57
+ });
58
+
59
+ describe('verifyWebhookPayload', () => {
60
+ it('should verify valid webhook payload', () => {
61
+ const webhookData = TestDataGenerator.generateWebhookData();
62
+
63
+ const result = client.verifyWebhookPayload(webhookData.secret, webhookData.headers, webhookData.payload);
64
+ expect(result).toBe(true);
65
+ });
66
+
67
+ it('should throw error for invalid signature', () => {
68
+ const webhookData = TestDataGenerator.generateWebhookData();
69
+
70
+ // Generate invalid signature using wrong payload data
71
+ const crypto = require('crypto');
72
+ const wrongData = `${webhookData.webhookId}.${webhookData.timestamp}.wrong-payload`;
73
+ const hmac = crypto.createHmac('sha256', Buffer.from('test-secret', 'base64'));
74
+ hmac.update(wrongData);
75
+ const wrongSignature = hmac.digest('base64');
76
+ const signature = `v1,${wrongSignature}`;
77
+
78
+ const headers = {
79
+ 'webhook-id': webhookData.webhookId,
80
+ 'webhook-timestamp': webhookData.timestamp,
81
+ 'webhook-signature': signature
82
+ };
83
+
84
+ expect(() => {
85
+ client.verifyWebhookPayload(webhookData.secret, headers, webhookData.payload);
86
+ }).toThrow('Invalid Signature');
87
+ });
88
+ });
89
+
90
+ describe('validateAccessToken', () => {
91
+ it('should validate access token', async () => {
92
+ // Mock token for testing - expected to fail
93
+ const token = 'mock-token';
94
+
95
+ try {
96
+ const result = await client.validateAccessToken(token);
97
+ expect(typeof result).toBe('boolean');
98
+ } catch (error) {
99
+ // Expected failure with mock token
100
+ expect(error).toBeDefined();
101
+ }
102
+ });
103
+ });
104
+ });
package/tests/setup.ts ADDED
@@ -0,0 +1,34 @@
1
+ import ScalekitClient from '../src/scalekit';
2
+ import dotenv from 'dotenv';
3
+
4
+ // Import Jest globals
5
+ import { beforeAll } from '@jest/globals';
6
+
7
+ // Load environment variables
8
+ dotenv.config();
9
+
10
+ // Global test configuration
11
+ declare global {
12
+ var client: ScalekitClient;
13
+ }
14
+
15
+ beforeAll(() => {
16
+ // Validate required environment variables
17
+ const environmentUrl = process.env.SCALEKIT_ENVIRONMENT_URL;
18
+ const clientId = process.env.SCALEKIT_CLIENT_ID;
19
+ const clientSecret = process.env.SCALEKIT_CLIENT_SECRET;
20
+
21
+ // Check for required environment variables
22
+ if (!environmentUrl) {
23
+ throw new Error('SCALEKIT_ENVIRONMENT_URL environment variable is required');
24
+ }
25
+ if (!clientId) {
26
+ throw new Error('SCALEKIT_CLIENT_ID environment variable is required');
27
+ }
28
+ if (!clientSecret) {
29
+ throw new Error('SCALEKIT_CLIENT_SECRET environment variable is required');
30
+ }
31
+
32
+ // Initialize test client
33
+ global.client = new ScalekitClient(environmentUrl, clientId, clientSecret);
34
+ });
@@ -0,0 +1,168 @@
1
+ import ScalekitClient from '../src/scalekit';
2
+ import { CreateUserRequest, UpdateUserRequest } from '../src/types/user';
3
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
4
+ import { TestDataGenerator, TestOrganizationManager, TestUserManager } from './utils/test-data';
5
+
6
+ describe('Users', () => {
7
+ let client: ScalekitClient;
8
+ let testOrg: string;
9
+ let userId: string | null = null;
10
+ let sharedUserData: CreateUserRequest;
11
+
12
+ beforeEach(async () => {
13
+ // Use global client
14
+ client = global.client;
15
+
16
+ // Create test organization for each test
17
+ testOrg = await TestOrganizationManager.createTestOrganization(client);
18
+
19
+ // Create a shared user for testing
20
+ sharedUserData = TestDataGenerator.generateUserData();
21
+ const createResponse = await client.user.createUserAndMembership(testOrg, sharedUserData);
22
+ userId = createResponse.user?.id || null;
23
+ expect(userId).toBeDefined();
24
+ });
25
+
26
+ afterEach(async () => {
27
+ // Clean up test resources
28
+ if (userId) {
29
+ await TestUserManager.cleanupTestUser(client, testOrg, userId);
30
+ userId = null;
31
+ }
32
+
33
+ // Clean up test organization
34
+ await TestOrganizationManager.cleanupTestOrganization(client, testOrg);
35
+ });
36
+
37
+ describe('listOrganizationUsers', () => {
38
+ it('should list users by organization', async () => {
39
+ // List users in the organization
40
+ const usersList = await client.user.listOrganizationUsers(testOrg, TestDataGenerator.generatePaginationParams());
41
+
42
+ expect(usersList).toBeDefined();
43
+ expect(usersList.users).toBeDefined();
44
+ expect(Array.isArray(usersList.users)).toBe(true);
45
+ expect(usersList.users.length).toBeGreaterThan(0);
46
+
47
+ // Verify basic user attributes
48
+ const firstUser = usersList.users[0];
49
+ expect(firstUser.id).toBeDefined();
50
+ expect(firstUser.email).toBeDefined();
51
+ expect(firstUser.environmentId).toBeDefined();
52
+ });
53
+
54
+ it('should handle pagination', async () => {
55
+ const firstPage = await client.user.listOrganizationUsers(testOrg, TestDataGenerator.generatePaginationParams(5));
56
+
57
+ expect(firstPage).toBeDefined();
58
+ expect(firstPage.users.length).toBeLessThanOrEqual(5);
59
+
60
+ if (firstPage.nextPageToken) {
61
+ const secondPage = await client.user.listOrganizationUsers(testOrg, {
62
+ pageSize: 5,
63
+ pageToken: firstPage.nextPageToken
64
+ });
65
+
66
+ expect(secondPage).toBeDefined();
67
+ expect(secondPage.users).toBeDefined();
68
+ }
69
+ });
70
+ });
71
+
72
+ describe('getUser', () => {
73
+ it('should get user by ID', async () => {
74
+ // Retrieve the user by ID
75
+ const user = await client.user.getUser(userId!);
76
+
77
+ expect(user).toBeDefined();
78
+ expect(user.user).toBeDefined();
79
+ expect(user.user?.id).toBe(userId);
80
+ expect(user.user?.email).toBe(sharedUserData.email);
81
+ });
82
+ });
83
+
84
+ describe('createUserAndMembership', () => {
85
+ it('should create user and membership', async () => {
86
+ // Create a new user for this specific test
87
+ const userData = TestDataGenerator.generateUserData();
88
+ let newUserId: string | null = null;
89
+
90
+ try {
91
+ const response = await client.user.createUserAndMembership(testOrg, userData);
92
+
93
+ expect(response).toBeDefined();
94
+ expect(response.user).toBeDefined();
95
+ expect(response.user?.id).toBeDefined();
96
+ expect(response.user?.email).toBe(userData.email);
97
+ expect(response.user?.metadata?.source).toBe('test');
98
+
99
+ newUserId = response.user?.id || null;
100
+ } finally {
101
+ // Clean up the new user created in this test
102
+ if (newUserId) {
103
+ await TestUserManager.cleanupTestUser(client, testOrg, newUserId);
104
+ }
105
+ }
106
+ });
107
+
108
+ it('should throw error when email is missing', async () => {
109
+ const userData = TestDataGenerator.generateUserData({ email: '' }); // Empty email
110
+
111
+ await expect(
112
+ client.user.createUserAndMembership(testOrg, userData)
113
+ ).rejects.toThrow('email is required');
114
+ });
115
+
116
+ it('should throw error when organizationId is missing', async () => {
117
+ const userData = TestDataGenerator.generateUserData({ email: 'test@example.com' });
118
+
119
+ await expect(
120
+ client.user.createUserAndMembership('', userData)
121
+ ).rejects.toThrow('organizationId is required');
122
+ });
123
+ });
124
+
125
+ describe('updateUser', () => {
126
+ it('should update user', async () => {
127
+ // Modify the shared user
128
+ const updateData = TestDataGenerator.generateUserUpdateData();
129
+
130
+ const updatedUser = await client.user.updateUser(userId!, updateData);
131
+
132
+ expect(updatedUser).toBeDefined();
133
+ expect(updatedUser.user).toBeDefined();
134
+ expect(updatedUser.user?.id).toBe(userId);
135
+ expect(updatedUser.user?.userProfile?.firstName).toBe('Updated');
136
+ expect(updatedUser.user?.userProfile?.lastName).toBe('Name');
137
+ });
138
+ });
139
+
140
+ describe('resendInvite', () => {
141
+ it('should resend invite to user', async () => {
142
+ // Resend invite to the shared user
143
+ const resendResponse = await client.user.resendInvite(testOrg, userId!);
144
+
145
+ // Verify the response structure
146
+ expect(resendResponse).toBeDefined();
147
+ expect(resendResponse.invite).toBeDefined();
148
+ expect(resendResponse.invite?.userId).toBe(userId);
149
+ expect(resendResponse.invite?.organizationId).toBe(testOrg);
150
+ expect(resendResponse.invite?.status).toBe('PENDING_INVITE');
151
+ expect(resendResponse.invite?.createdAt).toBeDefined();
152
+ expect(resendResponse.invite?.expiresAt).toBeDefined();
153
+ expect(resendResponse.invite?.resentCount).toBe(1);
154
+ });
155
+
156
+ it('should throw error when organizationId is missing', async () => {
157
+ await expect(
158
+ client.user.resendInvite('', userId!)
159
+ ).rejects.toThrow('organizationId is required');
160
+ });
161
+
162
+ it('should throw error when userId is missing', async () => {
163
+ await expect(
164
+ client.user.resendInvite(testOrg, '')
165
+ ).rejects.toThrow('userId is required');
166
+ });
167
+ });
168
+ });
@@ -0,0 +1,248 @@
1
+ import { CreateUserRequest, UpdateUserRequest } from '../../src/types/user';
2
+ import { TemplateType } from '../../src/pkg/grpc/scalekit/v1/auth/passwordless_pb';
3
+
4
+ /**
5
+ * Test data generation utilities to reduce redundancy across test files
6
+ */
7
+
8
+ export class TestDataGenerator {
9
+ /**
10
+ * Generate a unique timestamp-based identifier
11
+ */
12
+ static generateUniqueId(): string {
13
+ return Date.now().toString();
14
+ }
15
+
16
+ /**
17
+ * Generate a unique email address for testing
18
+ */
19
+ static generateUniqueEmail(): string {
20
+ return `test.user.${this.generateUniqueId()}@example.com`;
21
+ }
22
+
23
+ /**
24
+ * Generate test organization data
25
+ */
26
+ static generateOrganizationData() {
27
+ const uniqueId = this.generateUniqueId();
28
+ return {
29
+ name: `Test Org ${uniqueId}`,
30
+ externalId: `ext_org_${uniqueId}`
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Generate test user data
36
+ */
37
+ static generateUserData(overrides: Partial<CreateUserRequest> = {}): CreateUserRequest {
38
+ const uniqueEmail = this.generateUniqueEmail();
39
+
40
+ return {
41
+ email: uniqueEmail,
42
+ userProfile: {
43
+ firstName: 'Test',
44
+ lastName: 'User'
45
+ },
46
+ metadata: {
47
+ source: 'test'
48
+ },
49
+ ...overrides
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Generate test user update data
55
+ */
56
+ static generateUserUpdateData(overrides: Partial<UpdateUserRequest> = {}): UpdateUserRequest {
57
+ return {
58
+ userProfile: {
59
+ firstName: 'Updated',
60
+ lastName: 'Name'
61
+ },
62
+ ...overrides
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Generate test passwordless email data
68
+ */
69
+ static generatePasswordlessEmailData(overrides: any = {}) {
70
+ return {
71
+ template: TemplateType.SIGNIN,
72
+ state: 'test-state',
73
+ expiresIn: 3600,
74
+ magiclinkAuthUri: 'https://example.com/auth/callback',
75
+ ...overrides
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Generate test passwordless email with template variables
81
+ */
82
+ static generatePasswordlessEmailWithTemplateData(overrides: any = {}) {
83
+ return {
84
+ template: TemplateType.SIGNUP,
85
+ templateVariables: {
86
+ companyName: 'Test Company',
87
+ appName: 'Test App'
88
+ },
89
+ magiclinkAuthUri: 'https://example.com/auth/callback',
90
+ ...overrides
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Generate test webhook data for verification
96
+ */
97
+ static generateWebhookData() {
98
+ const secret = 'whsec_test-secret';
99
+ const payload = '{"test": "data"}';
100
+ const timestamp = Math.floor(Date.now() / 1000).toString();
101
+ const webhookId = 'msg_test_webhook_id';
102
+
103
+ // Generate valid signature for testing
104
+ const crypto = require('crypto');
105
+ const data = `${webhookId}.${timestamp}.${payload}`;
106
+ const hmac = crypto.createHmac('sha256', Buffer.from('test-secret', 'base64'));
107
+ hmac.update(data);
108
+ const computedSignature = hmac.digest('base64');
109
+ const signature = `v1,${computedSignature}`;
110
+
111
+ return {
112
+ secret,
113
+ payload,
114
+ timestamp,
115
+ webhookId,
116
+ signature,
117
+ headers: {
118
+ 'webhook-id': webhookId,
119
+ 'webhook-timestamp': timestamp,
120
+ 'webhook-signature': signature
121
+ }
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Generate test authorization URL options
127
+ */
128
+ static generateAuthorizationUrlOptions(overrides: any = {}) {
129
+ return {
130
+ scopes: ['openid', 'profile'],
131
+ state: 'test-state',
132
+ nonce: 'test-nonce',
133
+ prompt: 'login',
134
+ ...overrides
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Generate test PKCE parameters
140
+ */
141
+ static generatePKCEParams() {
142
+ return {
143
+ codeChallenge: 'test-challenge',
144
+ codeChallengeMethod: 'S256'
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Generate test pagination parameters
150
+ */
151
+ static generatePaginationParams(pageSize: number = 10) {
152
+ return {
153
+ pageSize,
154
+ pageToken: ''
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Generate test credential data for passwordless verification
160
+ */
161
+ static generateCredentialData(type: 'code' | 'linkToken' = 'code') {
162
+ if (type === 'code') {
163
+ return { code: 'mock-code' };
164
+ } else {
165
+ return { linkToken: 'mock-link-token' };
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Test organization management utilities
172
+ */
173
+ export class TestOrganizationManager {
174
+ /**
175
+ * Create a test organization and return its ID
176
+ */
177
+ static async createTestOrganization(client: any): Promise<string> {
178
+ const orgData = TestDataGenerator.generateOrganizationData();
179
+ const orgResponse = await client.organization.createOrganization(
180
+ orgData.name,
181
+ { externalId: orgData.externalId }
182
+ );
183
+
184
+ const testOrg = orgResponse.organization?.id || '';
185
+ if (!testOrg) {
186
+ throw new Error('Failed to create test organization');
187
+ }
188
+
189
+ return testOrg;
190
+ }
191
+
192
+ /**
193
+ * Clean up a test organization
194
+ */
195
+ static async cleanupTestOrganization(client: any, testOrg: string): Promise<void> {
196
+ if (testOrg) {
197
+ try {
198
+ await client.organization.deleteOrganization(testOrg);
199
+ } catch (error) {
200
+ // Organization may already be deleted
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Test user management utilities
208
+ */
209
+ export class TestUserManager {
210
+ /**
211
+ * Create a test user and return user data
212
+ */
213
+ static async createTestUser(client: any, testOrg: string, overrides: Partial<CreateUserRequest> = {}) {
214
+ const userData = TestDataGenerator.generateUserData(overrides);
215
+ const createResponse = await client.user.createUserAndMembership(testOrg, userData);
216
+ const createdUserId = createResponse.user?.id;
217
+
218
+ if (!createdUserId) {
219
+ throw new Error('Failed to create test user');
220
+ }
221
+
222
+ return {
223
+ userId: createdUserId,
224
+ userData,
225
+ response: createResponse
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Clean up a test user
231
+ */
232
+ static async cleanupTestUser(client: any, testOrg: string, userId: string): Promise<void> {
233
+ if (userId) {
234
+ try {
235
+ // Remove membership if it exists
236
+ await client.user.deleteMembership(testOrg, userId);
237
+ } catch (error) {
238
+ // Membership may not exist
239
+ }
240
+
241
+ try {
242
+ await client.user.deleteUser(userId);
243
+ } catch (error) {
244
+ // User may already be deleted
245
+ }
246
+ }
247
+ }
248
+ }