docpouch-client 0.8.8 → 0.8.10

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.
@@ -0,0 +1,205 @@
1
+ import { Socket } from "socket.io-client";
2
+ /**
3
+ * Client for interacting with docPouch API.
4
+ */
5
+ export default class docPouchClient {
6
+ /**
7
+ * The base URL of the server.
8
+ *
9
+ * @type {string}
10
+ */
11
+ baseUrl: string;
12
+ /**
13
+ * Socket.IO socket instance for real-time communication with the server.
14
+ *
15
+ * @type {Socket}
16
+ */
17
+ socket: Socket;
18
+ /**
19
+ * Callback function to handle socket events.
20
+ *
21
+ * @type {(event: I_EventString, data: I_WsMessage) => void}
22
+ */
23
+ callbackFunction: ((event: I_EventString, data: I_WsMessage) => void) | undefined;
24
+ /**
25
+ * Flag indicating whether real-time synchronization is enabled.
26
+ *
27
+ * @type {boolean}
28
+ */
29
+ realTimeSync: boolean;
30
+ /**
31
+ * Authentication token used to authorize requests.
32
+ *
33
+ * @private
34
+ * @type {string | null}
35
+ */
36
+ private authToken;
37
+ /**
38
+ * Flag indicating whether a connection attempt is in progress.
39
+ *
40
+ * @private
41
+ * @type {boolean}
42
+ */
43
+ private connectionInProgress;
44
+ /**
45
+ * Creates an instance of docPouchClient.
46
+ *
47
+ * @param {string} host - The base URL for the server.
48
+ * @param {number} [port=80] - The port number to connect to (default is 80).
49
+ * @param {(event: I_EventString, data: I_WsMessage) => void} [callback] - Optional callback function for socket events.
50
+ */
51
+ constructor(host: string, port?: number, callback?: (event: I_EventString, data: I_WsMessage) => void);
52
+ /**
53
+ * Sets the real-time synchronization status.
54
+ *
55
+ * @param {boolean} newRealTimeSync - The new real-time sync setting (true/false).
56
+ */
57
+ setRealTimeSync(newRealTimeSync: boolean): void;
58
+ login(credentials: I_UserLogin): Promise<I_LoginResponse | null>;
59
+ listUsers(): Promise<I_UserEntry[]>;
60
+ updateUser(userID: string, userData: I_UserUpdate): Promise<void>;
61
+ createUser(userData: I_UserCreation): Promise<I_UserDisplay>;
62
+ removeUser(userID: string): Promise<void>;
63
+ createDocument(document: I_DocumentEntry): Promise<I_DocumentEntry>;
64
+ listDocuments(): Promise<I_DocumentEntry[]>;
65
+ fetchDocument(queryObject: I_DocumentQuery): Promise<I_DocumentEntry[]>;
66
+ updateDocument(documentID: string, documentData: I_DocumentEntry): Promise<void>;
67
+ removeDocument(documentID: string): Promise<void>;
68
+ createStructure(structure: I_StructureCreation): Promise<I_DataStructure>;
69
+ getStructures(): Promise<I_DataStructure[]>;
70
+ updateStructure(structureID: string, structureData: I_DataStructure): Promise<void>;
71
+ removeStructure(structureID: string): Promise<void>;
72
+ createType(type: I_DocumentType): Promise<I_DocumentType>;
73
+ removeType(typeID: string): Promise<void>;
74
+ getTypes(): Promise<I_DocumentType[]>;
75
+ updateType(updatedType: I_DocumentType): Promise<void>;
76
+ setToken(token: string | null): void;
77
+ getVersion(): string;
78
+ debugSocketConnection(): void;
79
+ /**
80
+ * Sets up permanent socket listeners for the client.
81
+ *
82
+ * @private
83
+ */
84
+ private setupPermanentSocketListeners;
85
+ /**
86
+ * Initializes the WebSocket connection with the server.
87
+ *
88
+ * @private
89
+ */
90
+ private initWebSocket;
91
+ private request;
92
+ }
93
+ export interface I_UserEntry extends I_UserCreation {
94
+ _id: string;
95
+ }
96
+ export interface I_UserLogin {
97
+ name: string;
98
+ password: string;
99
+ }
100
+ export interface I_UserCreation {
101
+ name: string;
102
+ password: string;
103
+ email?: string;
104
+ department: string;
105
+ group: string;
106
+ isAdmin: boolean;
107
+ }
108
+ export interface I_UserUpdate {
109
+ name?: string;
110
+ password?: string;
111
+ email?: string;
112
+ department?: string;
113
+ group?: string;
114
+ isAdmin?: boolean;
115
+ }
116
+ export interface I_UserDisplay {
117
+ _id: string;
118
+ username: string;
119
+ department: string;
120
+ group: string;
121
+ email?: string;
122
+ }
123
+ export interface I_LoginResponse {
124
+ token: string;
125
+ isAdmin: boolean;
126
+ userName: string;
127
+ }
128
+ export interface I_DocumentEntry extends I_DocumentCreationOwned {
129
+ _id: string;
130
+ }
131
+ export interface I_DocumentCreation {
132
+ title: string;
133
+ description?: string;
134
+ type: number;
135
+ subType: number;
136
+ content: any;
137
+ shareWithGroup: boolean;
138
+ shareWithDepartment: boolean;
139
+ }
140
+ export interface I_DocumentCreationOwned extends I_DocumentCreation {
141
+ owner: string;
142
+ }
143
+ export interface I_DocumentUpdate extends I_DocumentQuery {
144
+ content?: any;
145
+ description?: string;
146
+ }
147
+ export interface I_DocumentQuery {
148
+ _id?: string;
149
+ owner?: string;
150
+ title?: string;
151
+ type?: number;
152
+ subType?: number;
153
+ shareWithGroup?: boolean;
154
+ shareWithDepartment?: boolean;
155
+ }
156
+ export interface I_DataStructure {
157
+ _id?: string | undefined;
158
+ name: string;
159
+ description: string;
160
+ fields: I_StructureField[];
161
+ }
162
+ export interface I_StructureField {
163
+ name: string;
164
+ type: string;
165
+ items?: string;
166
+ }
167
+ export interface I_StructureEntry {
168
+ _id?: string;
169
+ name: string;
170
+ description: string;
171
+ fields: I_StructureField[];
172
+ }
173
+ export interface I_StructureCreation {
174
+ name: string;
175
+ description?: string;
176
+ fields: I_StructureField[];
177
+ }
178
+ export interface I_StructureUpdate {
179
+ name?: string;
180
+ description?: string;
181
+ fields?: I_StructureField[];
182
+ }
183
+ export interface I_DocumentType {
184
+ _id?: string;
185
+ type: number;
186
+ subType: number;
187
+ name: string;
188
+ description?: string;
189
+ defaultStructureID?: string;
190
+ }
191
+ export type I_EventString = 'heartbeatPong' | "heartbeatPing" | "newDocument" | "newStructure" | "newUser" | "newType" | "removedID" | "changedDocument" | "changedStructure" | "changedUser" | "changedType" | "removedUser" | "removedStructure" | "removedDocument" | "removedType";
192
+ export interface I_WsMessage {
193
+ newDocument?: I_DocumentEntry;
194
+ newStructure?: I_StructureEntry;
195
+ newUser?: I_UserEntry;
196
+ removedID?: string;
197
+ changedDocument?: I_DocumentUpdate;
198
+ changedStructure?: I_StructureUpdate;
199
+ changedUser?: I_UserUpdate;
200
+ confirmSubscription?: boolean;
201
+ confirmUnsubscription?: boolean;
202
+ heartbeatPing?: number;
203
+ heartbeatPong?: number;
204
+ newType?: I_DocumentType;
205
+ }
package/dist/index.js ADDED
@@ -0,0 +1,308 @@
1
+ import { io } from "socket.io-client";
2
+ import packetJson from '../package.json';
3
+ /**
4
+ * Client for interacting with docPouch API.
5
+ */
6
+ export default class docPouchClient {
7
+ /**
8
+ * Creates an instance of docPouchClient.
9
+ *
10
+ * @param {string} host - The base URL for the server.
11
+ * @param {number} [port=80] - The port number to connect to (default is 80).
12
+ * @param {(event: I_EventString, data: I_WsMessage) => void} [callback] - Optional callback function for socket events.
13
+ */
14
+ constructor(host, port = 80, callback) {
15
+ /**
16
+ * Flag indicating whether real-time synchronization is enabled.
17
+ *
18
+ * @type {boolean}
19
+ */
20
+ this.realTimeSync = false;
21
+ /**
22
+ * Authentication token used to authorize requests.
23
+ *
24
+ * @private
25
+ * @type {string | null}
26
+ */
27
+ this.authToken = null;
28
+ /**
29
+ * Flag indicating whether a connection attempt is in progress.
30
+ *
31
+ * @private
32
+ * @type {boolean}
33
+ */
34
+ this.connectionInProgress = false;
35
+ this.baseUrl = host;
36
+ const socketUrl = host.includes('://') ? host : `https://${host}`;
37
+ const socketUrlWithPort = socketUrl.includes(':') && !socketUrl.endsWith(':')
38
+ ? socketUrl
39
+ : `${socketUrl}:${port}`;
40
+ console.log(`Initializing Socket.IO with URL: ${socketUrlWithPort}, path: /socket.io`);
41
+ this.socket = io(`${socketUrlWithPort}`, {
42
+ autoConnect: false,
43
+ transports: ['websocket'], // Try websocket only first
44
+ reconnection: true,
45
+ reconnectionAttempts: 5,
46
+ reconnectionDelay: 1000,
47
+ forceNew: true, // Force a new connection
48
+ auth: {
49
+ token: null // Will be set later
50
+ },
51
+ path: '/socket.io'
52
+ });
53
+ this.callbackFunction = callback;
54
+ this.setupPermanentSocketListeners();
55
+ }
56
+ /**
57
+ * Sets the real-time synchronization status.
58
+ *
59
+ * @param {boolean} newRealTimeSync - The new real-time sync setting (true/false).
60
+ */
61
+ setRealTimeSync(newRealTimeSync) {
62
+ console.log(`Setting realtime sync to: ${newRealTimeSync}. Current setting: ${this.realTimeSync}`);
63
+ // Skip if the setting isn't changing
64
+ if (newRealTimeSync === this.realTimeSync) {
65
+ console.log("Realtime sync setting unchanged, skipping");
66
+ return;
67
+ }
68
+ this.realTimeSync = newRealTimeSync;
69
+ if (newRealTimeSync && this.authToken) {
70
+ console.log("Activating realtime updates");
71
+ // Ensure we're not in the middle of another connection attempt
72
+ if (this.connectionInProgress) {
73
+ console.log("Connection already in progress, waiting before initializing");
74
+ setTimeout(() => this.initWebSocket(), 500);
75
+ }
76
+ else {
77
+ this.initWebSocket();
78
+ }
79
+ }
80
+ else if (!newRealTimeSync) {
81
+ console.log("Deactivating realtime updates");
82
+ if (this.socket.connected) {
83
+ console.log("Disconnecting socket");
84
+ this.socket.disconnect();
85
+ }
86
+ }
87
+ }
88
+ // User Administration Endpoints
89
+ async login(credentials) {
90
+ const response = await this.request('/users/login', 'POST', credentials, false);
91
+ if (response.token) {
92
+ this.authToken = response.token;
93
+ // Reconnect websocket with new token if realtime sync is enabled
94
+ if (this.realTimeSync) {
95
+ this.initWebSocket();
96
+ }
97
+ return { token: response.token, isAdmin: response.isAdmin, userName: response.userName };
98
+ }
99
+ return null;
100
+ }
101
+ async listUsers() {
102
+ return await this.request('/users/list', 'GET');
103
+ }
104
+ async updateUser(userID, userData) {
105
+ await this.request(`/users/update/${userID}`, 'PATCH', userData);
106
+ }
107
+ async createUser(userData) {
108
+ return await this.request('/users/create', 'POST', userData);
109
+ }
110
+ async removeUser(userID) {
111
+ await this.request(`/users/remove/${userID}`, 'DELETE');
112
+ }
113
+ // Document Management Endpoints
114
+ async createDocument(document) {
115
+ return await this.request('/docs/create', 'POST', document);
116
+ }
117
+ async listDocuments() {
118
+ return await this.request('/docs/list', 'GET');
119
+ }
120
+ async fetchDocument(queryObject) {
121
+ return await this.request(`/docs/fetch/`, 'POST', queryObject);
122
+ }
123
+ async updateDocument(documentID, documentData) {
124
+ await this.request(`/docs/update/${documentID}`, 'PATCH', documentData);
125
+ }
126
+ async removeDocument(documentID) {
127
+ await this.request(`/docs/remove/${documentID}`, 'DELETE');
128
+ }
129
+ // Data Structure Endpoints
130
+ async createStructure(structure) {
131
+ return await this.request('/structures/create', 'POST', structure);
132
+ }
133
+ async getStructures() {
134
+ return await this.request('/structures/list', 'GET');
135
+ }
136
+ async updateStructure(structureID, structureData) {
137
+ await this.request(`/structures/update/${structureID}`, 'PATCH', structureData);
138
+ }
139
+ async removeStructure(structureID) {
140
+ await this.request(`/structures/remove/${structureID}`, 'DELETE');
141
+ }
142
+ // Data Type Endpoints
143
+ async createType(type) {
144
+ return await this.request('/types/write', 'POST', type);
145
+ }
146
+ async removeType(typeID) {
147
+ return await this.request(`/types/remove/${typeID}`, 'DELETE');
148
+ }
149
+ async getTypes() {
150
+ return await this.request('/types/list', 'GET');
151
+ }
152
+ async updateType(updatedType) {
153
+ await this.request(`/types/write`, 'POST', updatedType);
154
+ }
155
+ setToken(token) {
156
+ console.log("Setting token to:", token ? "***token***" : "null");
157
+ const tokenChanged = this.authToken !== token;
158
+ this.authToken = token;
159
+ if (!tokenChanged) {
160
+ console.log("Token unchanged, no need to reconnect");
161
+ return;
162
+ }
163
+ // If we have a new token and realtime sync is enabled
164
+ if (token && this.realTimeSync) {
165
+ console.log("New token set, will initialize WebSocket");
166
+ // Ensure any existing connection is closed first
167
+ if (this.socket.connected) {
168
+ console.log("Disconnecting existing socket before reconnecting with new token");
169
+ this.socket.disconnect();
170
+ }
171
+ // Wait a moment for the disconnect to complete
172
+ setTimeout(() => {
173
+ console.log("Initializing WebSocket with new token");
174
+ this.initWebSocket();
175
+ }, 300);
176
+ }
177
+ // If token was cleared or realtime sync is disabled
178
+ else if (this.socket.connected) {
179
+ console.log("Token cleared or realtime sync disabled, disconnecting");
180
+ this.socket.disconnect();
181
+ }
182
+ }
183
+ getVersion() {
184
+ return packetJson.version;
185
+ }
186
+ debugSocketConnection() {
187
+ console.log("Socket connection debug info:");
188
+ console.log("- Connected:", this.socket.connected);
189
+ console.log("- Socket ID:", this.socket.id);
190
+ console.log("- Auth token present:", !!this.authToken);
191
+ console.log("- Connection in progress:", this.connectionInProgress);
192
+ console.log("- Realtime sync enabled:", this.realTimeSync);
193
+ console.log("- Socket options:", this.socket.io.opts);
194
+ // Try to force reconnection
195
+ if (!this.socket.connected && this.authToken && this.realTimeSync) {
196
+ console.log("Attempting to force reconnection...");
197
+ this.socket.auth = { token: this.authToken };
198
+ this.socket.connect();
199
+ }
200
+ }
201
+ /**
202
+ * Sets up permanent socket listeners for the client.
203
+ *
204
+ * @private
205
+ */
206
+ setupPermanentSocketListeners() {
207
+ // These are permanent listeners that won't be removed
208
+ this.socket.on('connect_error', (error) => {
209
+ console.error('Socket connection error:', error.message);
210
+ this.connectionInProgress = false;
211
+ });
212
+ this.socket.on('connect', () => {
213
+ console.log('Socket connected successfully with ID:', this.socket.id);
214
+ this.connectionInProgress = false;
215
+ });
216
+ this.socket.on('disconnect', (reason) => {
217
+ console.log('Socket disconnected. Reason:', reason);
218
+ this.connectionInProgress = false;
219
+ });
220
+ this.socket.on('error', (error) => {
221
+ console.error('Socket error:', error);
222
+ this.connectionInProgress = false;
223
+ });
224
+ }
225
+ /**
226
+ * Initializes the WebSocket connection with the server.
227
+ *
228
+ * @private
229
+ */
230
+ initWebSocket() {
231
+ console.log("initWebSocket called. Auth token present:", !!this.authToken, "Connection in progress:", this.connectionInProgress, "Socket connected:", this.socket.connected);
232
+ if (!this.authToken) {
233
+ console.log("Skipping WebSocket initialization: No auth token");
234
+ return;
235
+ }
236
+ if (this.connectionInProgress) {
237
+ console.log("Connection already in progress, skipping initialization");
238
+ return;
239
+ }
240
+ if (this.socket.connected) {
241
+ console.log("Socket already connected with ID:", this.socket.id);
242
+ return;
243
+ }
244
+ this.connectionInProgress = true;
245
+ try {
246
+ console.log("Setting up WebSocket connection with token");
247
+ // Update the auth token
248
+ this.socket.auth = { token: this.authToken };
249
+ // Remove any dynamic event listeners that might have been added
250
+ this.socket.offAny();
251
+ // Set up event handler for application events
252
+ this.socket.onAny((event, data) => {
253
+ if (event === "heartbeatPing") {
254
+ console.log("Ping event received:", data);
255
+ this.socket.emit("heartbeatPong", Date.now());
256
+ }
257
+ else if (this.callbackFunction) {
258
+ this.callbackFunction(event, data);
259
+ }
260
+ });
261
+ // Connect to the server
262
+ console.log("Connecting socket with auth token");
263
+ this.socket.connect();
264
+ // Add a timeout to detect if connection is taking too long
265
+ setTimeout(() => {
266
+ if (this.connectionInProgress) {
267
+ console.warn("Socket connection attempt timed out after 5 seconds");
268
+ this.connectionInProgress = false;
269
+ // If we're still not connected after the timeout, try again with polling
270
+ if (!this.socket.connected) {
271
+ console.log("Retrying connection with polling transport");
272
+ this.socket.io.opts.transports = ['polling', 'websocket'];
273
+ this.socket.connect();
274
+ }
275
+ }
276
+ }, 5000);
277
+ }
278
+ catch (error) {
279
+ console.error('Error in initWebSocket:', error);
280
+ this.connectionInProgress = false;
281
+ }
282
+ }
283
+ async request(endpoint, method, body, requiresAuth = true) {
284
+ const headers = {
285
+ 'Content-Type': 'application/json',
286
+ };
287
+ if (requiresAuth && this.authToken)
288
+ headers['Authorization'] = `Bearer ${this.authToken}`;
289
+ if (this.socket.id)
290
+ headers['X-Socket-ID'] = this.socket.id;
291
+ const options = {
292
+ method,
293
+ headers,
294
+ body: body ? JSON.stringify(body) : undefined
295
+ };
296
+ const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
297
+ const normalizedBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
298
+ const url = `${normalizedBaseUrl}${normalizedEndpoint}`;
299
+ const response = await fetch(url, options);
300
+ if (!response.ok) {
301
+ if (response.status === 401 || response.status === 403) {
302
+ this.authToken = null;
303
+ }
304
+ throw new Error(`API error: ${response.status} ${response.statusText}`);
305
+ }
306
+ return await response.json();
307
+ }
308
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docpouch-client",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "exports": {
@@ -27,10 +27,10 @@
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/jest": "^30.0.0",
30
- "@types/node": "^24.0.7",
31
- "jest": "^30.0.3",
32
- "ts-jest": "^29.4.0",
33
- "typescript": "^5.8.3"
30
+ "@types/node": "^24.3.0",
31
+ "jest": "^30.0.5",
32
+ "ts-jest": "^29.4.1",
33
+ "typescript": "^5.9.2"
34
34
  },
35
35
  "dependencies": {
36
36
  "socket.io-client": "^4.8.1"
package/src/index.ts CHANGED
@@ -407,7 +407,6 @@ export interface I_UserCreation {
407
407
  }
408
408
 
409
409
  export interface I_UserUpdate {
410
- _id: string;
411
410
  name?: string;
412
411
  password?: string;
413
412
  email?: string;
@@ -451,7 +450,6 @@ export interface I_DocumentCreationOwned extends I_DocumentCreation {
451
450
  }
452
451
 
453
452
  export interface I_DocumentUpdate extends I_DocumentQuery {
454
- _id: string;
455
453
  content?: any;
456
454
  description?: string;
457
455
  }
@@ -496,7 +494,6 @@ export interface I_StructureCreation {
496
494
  }
497
495
 
498
496
  export interface I_StructureUpdate {
499
- _id?: string;
500
497
  name?: string;
501
498
  description?: string;
502
499
  fields?: I_StructureField[];
@@ -1,489 +0,0 @@
1
- import docPouchClient from '../index.js';
2
- import packetJson from '../../package.json';
3
- import {
4
- I_UserLogin,
5
- I_UserCreation,
6
- I_UserUpdate,
7
- I_DocumentEntry,
8
- I_DocumentQuery,
9
- I_StructureCreation,
10
- I_DataStructure,
11
- } from '../types.js';
12
-
13
- // Mock socket.io-client
14
- jest.mock('socket.io-client', () => {
15
- const mockSocket = {
16
- connect: jest.fn(),
17
- emit: jest.fn(),
18
- onAny: jest.fn(),
19
- };
20
- return {
21
- io: jest.fn(() => mockSocket),
22
- Socket: jest.requireActual('socket.io-client').Socket,
23
- };
24
- });
25
-
26
- // Mock fetch API
27
- global.fetch = jest.fn();
28
- const mockFetch = global.fetch as jest.Mock;
29
-
30
- describe('Index Class', () => {
31
- let index: docPouchClient;
32
- const baseUrl = 'http://example.com';
33
-
34
- beforeEach(() => {
35
- // Reset mocks before each test
36
- jest.clearAllMocks();
37
- mockFetch.mockClear();
38
-
39
- // Create a new instance of Index for each test
40
- index = new docPouchClient(baseUrl);
41
-
42
- // Mock successful fetch response
43
- mockFetch.mockImplementation(() =>
44
- Promise.resolve({
45
- ok: true,
46
- status: 200,
47
- statusText: 'OK',
48
- json: () => Promise.resolve({}),
49
- })
50
- );
51
- });
52
-
53
- // Test constructor
54
- describe('constructor', () => {
55
- it('should initialize with the provided baseUrl', () => {
56
- expect(index.baseUrl).toBe(baseUrl);
57
- });
58
-
59
- it('should initialize socket with autoConnect: false by default', () => {
60
- expect(require('socket.io-client').io).toHaveBeenCalledWith(baseUrl, { autoConnect: false });
61
- });
62
-
63
- it('should connect socket and set up event handler when callback is provided', () => {
64
- const mockCallback = jest.fn();
65
- const indexWithCallback = new docPouchClient(baseUrl, mockCallback);
66
-
67
- expect(indexWithCallback.socket.connect).toHaveBeenCalled();
68
- expect(indexWithCallback.socket.onAny).toHaveBeenCalled();
69
- });
70
- });
71
-
72
- // Test utility methods
73
- describe('utility methods', () => {
74
- it('should set and get token correctly', () => {
75
- const token = 'test-token';
76
- index.setToken(token);
77
- expect(index.getToken()).toBe(token);
78
- });
79
-
80
- it('should return null when token is not set', () => {
81
- expect(index.getToken()).toBeNull();
82
- });
83
-
84
- it('should return the correct version', () => {
85
- // Mock package.json
86
- jest.mock('../../package.json', () => ({ version: packetJson.version }), { virtual: true });
87
-
88
- // We need to re-create the index instance to pick up the mocked package.json
89
- const newIndex = new docPouchClient(baseUrl);
90
- expect(newIndex.getVersion()).toBe(packetJson.version);
91
- });
92
- });
93
-
94
- // Test user administration endpoints
95
- describe('user administration endpoints', () => {
96
- it('should login successfully and store token', async () => {
97
- const credentials: I_UserLogin = { name: 'testuser', password: 'password' };
98
- const mockResponse = { token: 'test-token', isAdmin: true };
99
-
100
- mockFetch.mockImplementationOnce(() =>
101
- Promise.resolve({
102
- ok: true,
103
- status: 200,
104
- statusText: 'OK',
105
- json: () => Promise.resolve(mockResponse),
106
- })
107
- );
108
-
109
- const result = await index.login(credentials);
110
-
111
- expect(mockFetch).toHaveBeenCalledWith(
112
- `${baseUrl}/users/login`,
113
- expect.objectContaining({
114
- method: 'POST',
115
- body: JSON.stringify(credentials),
116
- })
117
- );
118
- expect(result).toEqual(mockResponse);
119
- expect(index.getToken()).toBe('test-token');
120
- });
121
-
122
- it('should return null when login fails', async () => {
123
- const credentials: I_UserLogin = { name: 'testuser', password: 'wrong-password' };
124
-
125
- mockFetch.mockImplementationOnce(() =>
126
- Promise.resolve({
127
- ok: true,
128
- status: 200,
129
- statusText: 'OK',
130
- json: () => Promise.resolve({}),
131
- })
132
- );
133
-
134
- const result = await index.login(credentials);
135
-
136
- expect(result).toBeNull();
137
- });
138
-
139
- it('should list users', async () => {
140
- const mockUsers = [{ _id: '1', name: 'user1', password: 'pass1', department: 'dept1', group: 'group1', isAdmin: false }];
141
-
142
- mockFetch.mockImplementationOnce(() =>
143
- Promise.resolve({
144
- ok: true,
145
- status: 200,
146
- statusText: 'OK',
147
- json: () => Promise.resolve(mockUsers),
148
- })
149
- );
150
-
151
- const result = await index.listUsers();
152
-
153
- expect(mockFetch).toHaveBeenCalledWith(
154
- `${baseUrl}/users/list`,
155
- expect.objectContaining({
156
- method: 'GET',
157
- })
158
- );
159
- expect(result).toEqual(mockUsers);
160
- });
161
-
162
- it('should update a user', async () => {
163
- const userId = '1';
164
- const userData: I_UserUpdate = { name: 'updated-user', department: 'new-dept' };
165
-
166
- await index.updateUser(userId, userData);
167
-
168
- expect(mockFetch).toHaveBeenCalledWith(
169
- `${baseUrl}/users/update/${userId}`,
170
- expect.objectContaining({
171
- method: 'PATCH',
172
- body: JSON.stringify(userData),
173
- })
174
- );
175
- });
176
-
177
- it('should create a user', async () => {
178
- const userData: I_UserCreation = {
179
- name: 'newuser',
180
- password: 'password',
181
- department: 'dept',
182
- group: 'group',
183
- isAdmin: false
184
- };
185
- const mockResponse = { _id: '2', username: 'newuser', department: 'dept', group: 'group' };
186
-
187
- mockFetch.mockImplementationOnce(() =>
188
- Promise.resolve({
189
- ok: true,
190
- status: 200,
191
- statusText: 'OK',
192
- json: () => Promise.resolve(mockResponse),
193
- })
194
- );
195
-
196
- const result = await index.createUser(userData);
197
-
198
- expect(mockFetch).toHaveBeenCalledWith(
199
- `${baseUrl}/users/create`,
200
- expect.objectContaining({
201
- method: 'POST',
202
- body: JSON.stringify(userData),
203
- })
204
- );
205
- expect(result).toEqual(mockResponse);
206
- });
207
-
208
- it('should remove a user', async () => {
209
- const userId = '1';
210
-
211
- await index.removeUser(userId);
212
-
213
- expect(mockFetch).toHaveBeenCalledWith(
214
- `${baseUrl}/users/remove/${userId}`,
215
- expect.objectContaining({
216
- method: 'DELETE',
217
- })
218
- );
219
- });
220
- });
221
-
222
- // Test document management endpoints
223
- describe('document management endpoints', () => {
224
- it('should create a document', async () => {
225
- const document: I_DocumentEntry = {
226
- _id: '1',
227
- title: 'Test Document',
228
- type: 1,
229
- subType: 2,
230
- content: { data: 'test content' },
231
- owner: 'user1'
232
- };
233
-
234
- mockFetch.mockImplementationOnce(() =>
235
- Promise.resolve({
236
- ok: true,
237
- status: 200,
238
- statusText: 'OK',
239
- json: () => Promise.resolve(document),
240
- })
241
- );
242
-
243
- const result = await index.createDocument(document);
244
-
245
- expect(mockFetch).toHaveBeenCalledWith(
246
- `${baseUrl}/docs/create`,
247
- expect.objectContaining({
248
- method: 'POST',
249
- body: JSON.stringify(document),
250
- })
251
- );
252
- expect(result).toEqual(document);
253
- });
254
-
255
- it('should list documents', async () => {
256
- const mockDocuments = [
257
- { _id: '1', title: 'Doc1', type: 1, subType: 1, content: {}, owner: 'user1' },
258
- { _id: '2', title: 'Doc2', type: 2, subType: 2, content: {}, owner: 'user2' }
259
- ];
260
-
261
- mockFetch.mockImplementationOnce(() =>
262
- Promise.resolve({
263
- ok: true,
264
- status: 200,
265
- statusText: 'OK',
266
- json: () => Promise.resolve(mockDocuments),
267
- })
268
- );
269
-
270
- const result = await index.listDocuments();
271
-
272
- expect(mockFetch).toHaveBeenCalledWith(
273
- `${baseUrl}/docs/list`,
274
- expect.objectContaining({
275
- method: 'GET',
276
- })
277
- );
278
- expect(result).toEqual(mockDocuments);
279
- });
280
-
281
- it('should fetch documents based on query', async () => {
282
- const query: I_DocumentQuery = { type: 1, owner: 'user1' };
283
- const mockDocuments = [
284
- { _id: '1', title: 'Doc1', type: 1, subType: 1, content: {}, owner: 'user1' }
285
- ];
286
-
287
- mockFetch.mockImplementationOnce(() =>
288
- Promise.resolve({
289
- ok: true,
290
- status: 200,
291
- statusText: 'OK',
292
- json: () => Promise.resolve(mockDocuments),
293
- })
294
- );
295
-
296
- const result = await index.fetchDocument(query);
297
-
298
- expect(mockFetch).toHaveBeenCalledWith(
299
- `${baseUrl}/docs/fetch/`,
300
- expect.objectContaining({
301
- method: 'POST',
302
- body: JSON.stringify(query),
303
- })
304
- );
305
- expect(result).toEqual(mockDocuments);
306
- });
307
-
308
- it('should update a document', async () => {
309
- const documentId = '1';
310
- const documentData: I_DocumentEntry = {
311
- _id: '1',
312
- title: 'Updated Document',
313
- type: 1,
314
- subType: 2,
315
- content: { data: 'updated content' },
316
- owner: 'user1'
317
- };
318
-
319
- await index.updateDocument(documentId, documentData);
320
-
321
- expect(mockFetch).toHaveBeenCalledWith(
322
- `${baseUrl}/docs/update/${documentId}`,
323
- expect.objectContaining({
324
- method: 'PATCH',
325
- body: JSON.stringify(documentData),
326
- })
327
- );
328
- });
329
-
330
- it('should remove a document', async () => {
331
- const documentId = '1';
332
-
333
- await index.removeDocument(documentId);
334
-
335
- expect(mockFetch).toHaveBeenCalledWith(
336
- `${baseUrl}/docs/remove/${documentId}`,
337
- expect.objectContaining({
338
- method: 'DELETE',
339
- })
340
- );
341
- });
342
- });
343
-
344
- // Test data structure endpoints
345
- describe('data structure endpoints', () => {
346
- it('should create a structure', async () => {
347
- const structure: I_StructureCreation = {
348
- name: 'Test Structure',
349
- description: 'Test Description',
350
- fields: [{ name: 'field1', type: 'string' }]
351
- };
352
- const mockResponse: I_DataStructure = {
353
- _id: '1',
354
- name: 'Test Structure',
355
- description: 'Test Description',
356
- fields: [{ name: 'field1', type: 'string' }]
357
- };
358
-
359
- mockFetch.mockImplementationOnce(() =>
360
- Promise.resolve({
361
- ok: true,
362
- status: 200,
363
- statusText: 'OK',
364
- json: () => Promise.resolve(mockResponse),
365
- })
366
- );
367
-
368
- const result = await index.createStructure(structure);
369
-
370
- expect(mockFetch).toHaveBeenCalledWith(
371
- `${baseUrl}/structures/create`,
372
- expect.objectContaining({
373
- method: 'POST',
374
- body: JSON.stringify(structure),
375
- })
376
- );
377
- expect(result).toEqual(mockResponse);
378
- });
379
-
380
- it('should get structures', async () => {
381
- const mockStructures = [
382
- { _id: '1', name: 'Structure1', description: 'Desc1', fields: [] },
383
- { _id: '2', name: 'Structure2', description: 'Desc2', fields: [] }
384
- ];
385
-
386
- mockFetch.mockImplementationOnce(() =>
387
- Promise.resolve({
388
- ok: true,
389
- status: 200,
390
- statusText: 'OK',
391
- json: () => Promise.resolve(mockStructures),
392
- })
393
- );
394
-
395
- const result = await index.getStructures();
396
-
397
- expect(mockFetch).toHaveBeenCalledWith(
398
- `${baseUrl}/structures/list`,
399
- expect.objectContaining({
400
- method: 'GET',
401
- })
402
- );
403
- expect(result).toEqual(mockStructures);
404
- });
405
-
406
- it('should update a structure', async () => {
407
- const structureId = '1';
408
- const structureData: I_DataStructure = {
409
- name: 'Updated Structure',
410
- description: 'Updated Description',
411
- fields: [{ name: 'updatedField', type: 'number' }]
412
- };
413
-
414
- await index.updateStructure(structureId, structureData);
415
-
416
- expect(mockFetch).toHaveBeenCalledWith(
417
- `${baseUrl}/structures/update/${structureId}`,
418
- expect.objectContaining({
419
- method: 'PATCH',
420
- body: JSON.stringify(structureData),
421
- })
422
- );
423
- });
424
-
425
- it('should remove a structure', async () => {
426
- const structureId = '1';
427
-
428
- await index.removeStructure(structureId);
429
-
430
- expect(mockFetch).toHaveBeenCalledWith(
431
- `${baseUrl}/structures/remove/${structureId}`,
432
- expect.objectContaining({
433
- method: 'DELETE',
434
- })
435
- );
436
- });
437
- });
438
-
439
- // Test error handling
440
- describe('error handling', () => {
441
- it('should throw an error when API request fails', async () => {
442
- mockFetch.mockImplementationOnce(() =>
443
- Promise.resolve({
444
- ok: false,
445
- status: 500,
446
- statusText: 'Internal Server Error',
447
- })
448
- );
449
-
450
- await expect(index.listUsers()).rejects.toThrow('API error: 500 Internal Server Error');
451
- });
452
-
453
- it('should clear token when receiving 401 or 403 status', async () => {
454
- // Set a token first
455
- index.setToken('test-token');
456
-
457
- // Mock 401 response
458
- mockFetch.mockImplementationOnce(() =>
459
- Promise.resolve({
460
- ok: false,
461
- status: 401,
462
- statusText: 'Unauthorized',
463
- })
464
- );
465
-
466
- await expect(index.listUsers()).rejects.toThrow('API error: 401 Unauthorized');
467
- expect(index.getToken()).toBeNull();
468
- });
469
- });
470
-
471
- // Test WebSocket functionality
472
- describe('WebSocket functionality', () => {
473
- it('should connect socket when callback is provided', () => {
474
- const mockCallback = jest.fn();
475
- const indexWithCallback = new docPouchClient(baseUrl, mockCallback);
476
-
477
- // Verify that connect was called
478
- expect(indexWithCallback.socket.connect).toHaveBeenCalled();
479
- });
480
-
481
- it('should set up event handler when callback is provided', () => {
482
- const mockCallback = jest.fn();
483
- const indexWithCallback = new docPouchClient(baseUrl, mockCallback);
484
-
485
- // Verify that onAny was called
486
- expect(indexWithCallback.socket.onAny).toHaveBeenCalled();
487
- });
488
- });
489
- });