@simplysm/storage 13.0.69 → 13.0.71

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,253 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { SftpStorageClient } from "../src/clients/sftp-storage-client";
3
+
4
+ // Mock ssh2-sftp-client module
5
+ const mockConnect = vi.fn().mockResolvedValue(undefined);
6
+ const mockMkdir = vi.fn().mockResolvedValue(undefined);
7
+ const mockRename = vi.fn().mockResolvedValue(undefined);
8
+ const mockExists = vi.fn().mockResolvedValue("-");
9
+ const mockList = vi.fn().mockResolvedValue([
10
+ { name: "file.txt", type: "-" },
11
+ { name: "dir", type: "d" },
12
+ ]);
13
+ const mockGet = vi.fn().mockResolvedValue(new TextEncoder().encode("test content"));
14
+ const mockDelete = vi.fn().mockResolvedValue(undefined);
15
+ const mockPut = vi.fn().mockResolvedValue(undefined);
16
+ const mockFastPut = vi.fn().mockResolvedValue(undefined);
17
+ const mockUploadDir = vi.fn().mockResolvedValue(undefined);
18
+ const mockEnd = vi.fn().mockResolvedValue(undefined);
19
+
20
+ vi.mock("ssh2-sftp-client", () => {
21
+ return {
22
+ default: class MockSftpClient {
23
+ connect = mockConnect;
24
+ mkdir = mockMkdir;
25
+ rename = mockRename;
26
+ exists = mockExists;
27
+ list = mockList;
28
+ get = mockGet;
29
+ delete = mockDelete;
30
+ put = mockPut;
31
+ fastPut = mockFastPut;
32
+ uploadDir = mockUploadDir;
33
+ end = mockEnd;
34
+ },
35
+ };
36
+ });
37
+
38
+ describe("SftpStorageClient", () => {
39
+ let client: SftpStorageClient;
40
+
41
+ beforeEach(() => {
42
+ vi.clearAllMocks();
43
+ client = new SftpStorageClient();
44
+ });
45
+
46
+ describe("connect", () => {
47
+ it("Should connect with connection settings", async () => {
48
+ await client.connect({
49
+ host: "sftp.example.com",
50
+ port: 22,
51
+ user: "user",
52
+ pass: "pass",
53
+ });
54
+
55
+ expect(mockConnect).toHaveBeenCalledWith({
56
+ host: "sftp.example.com",
57
+ port: 22,
58
+ username: "user",
59
+ password: "pass",
60
+ });
61
+ });
62
+
63
+ it("Should throw error when connect is called on already connected client", async () => {
64
+ await client.connect({ host: "test" });
65
+ await expect(client.connect({ host: "test" })).rejects.toThrow(
66
+ "SFTP server is already connected. Please call close() first.",
67
+ );
68
+ });
69
+
70
+ it("Should clean up client on connection failure", async () => {
71
+ mockConnect.mockRejectedValueOnce(new Error("Auth failed"));
72
+ await expect(client.connect({ host: "test", pass: "wrong" })).rejects.toThrow("Auth failed");
73
+ expect(mockEnd).toHaveBeenCalled();
74
+ });
75
+ });
76
+
77
+ describe("Method calls before connection", () => {
78
+ it("Should throw error when mkdir is called before connection", async () => {
79
+ await expect(client.mkdir("/test")).rejects.toThrow("Not connected to SFTP server.");
80
+ });
81
+
82
+ it("Should throw error when rename is called before connection", async () => {
83
+ await expect(client.rename("/from", "/to")).rejects.toThrow(
84
+ "Not connected to SFTP server.",
85
+ );
86
+ });
87
+
88
+ it("Should throw error when readdir is called before connection", async () => {
89
+ await expect(client.readdir("/")).rejects.toThrow("Not connected to SFTP server.");
90
+ });
91
+ });
92
+
93
+ describe("mkdir", () => {
94
+ it("Should create directory", async () => {
95
+ await client.connect({ host: "test" });
96
+ await client.mkdir("/test/dir");
97
+
98
+ expect(mockMkdir).toHaveBeenCalledWith("/test/dir", true);
99
+ });
100
+ });
101
+
102
+ describe("rename", () => {
103
+ it("Should rename file/directory", async () => {
104
+ await client.connect({ host: "test" });
105
+ await client.rename("/from", "/to");
106
+
107
+ expect(mockRename).toHaveBeenCalledWith("/from", "/to");
108
+ });
109
+ });
110
+
111
+ describe("exists", () => {
112
+ it("Should return true if file exists", async () => {
113
+ await client.connect({ host: "test" });
114
+ const result = await client.exists("/file.txt");
115
+
116
+ expect(result).toBe(true);
117
+ });
118
+
119
+ it("Should return false if file does not exist", async () => {
120
+ mockExists.mockResolvedValueOnce(false);
121
+ await client.connect({ host: "test" });
122
+ const result = await client.exists("/nonexistent.txt");
123
+
124
+ expect(result).toBe(false);
125
+ });
126
+
127
+ it("Should return true if directory exists (type='d')", async () => {
128
+ mockExists.mockResolvedValueOnce("d");
129
+ await client.connect({ host: "test" });
130
+ const result = await client.exists("/directory");
131
+
132
+ expect(result).toBe(true);
133
+ });
134
+
135
+ it("Should return true if symbolic link exists (type='l')", async () => {
136
+ mockExists.mockResolvedValueOnce("l");
137
+ await client.connect({ host: "test" });
138
+ const result = await client.exists("/symlink");
139
+
140
+ expect(result).toBe(true);
141
+ });
142
+
143
+ it("Should return false on error", async () => {
144
+ mockExists.mockRejectedValueOnce(new Error("Network error"));
145
+ await client.connect({ host: "test" });
146
+ const result = await client.exists("/test.txt");
147
+
148
+ expect(result).toBe(false);
149
+ });
150
+ });
151
+
152
+ describe("readdir", () => {
153
+ it("Should return directory list as FileInfo array", async () => {
154
+ await client.connect({ host: "test" });
155
+ const result = await client.readdir("/");
156
+
157
+ expect(result).toEqual([
158
+ { name: "file.txt", isFile: true },
159
+ { name: "dir", isFile: false },
160
+ ]);
161
+ });
162
+ });
163
+
164
+ describe("readFile", () => {
165
+ it("Should return file content as Uint8Array", async () => {
166
+ await client.connect({ host: "test" });
167
+ const result = await client.readFile("/file.txt");
168
+
169
+ expect(result).toBeInstanceOf(Uint8Array);
170
+ expect(new TextDecoder().decode(result)).toBe("test content");
171
+ });
172
+
173
+ it("Should convert string result to Uint8Array", async () => {
174
+ mockGet.mockResolvedValueOnce("string content");
175
+ await client.connect({ host: "test" });
176
+ const result = await client.readFile("/file.txt");
177
+
178
+ expect(result).toBeInstanceOf(Uint8Array);
179
+ expect(new TextDecoder().decode(result)).toBe("string content");
180
+ });
181
+
182
+ it("Should throw error on unexpected type", async () => {
183
+ mockGet.mockResolvedValueOnce({ unexpected: "object" });
184
+ await client.connect({ host: "test" });
185
+
186
+ await expect(client.readFile("/file.txt")).rejects.toThrow("Unexpected response type.");
187
+ });
188
+ });
189
+
190
+ describe("remove", () => {
191
+ it("Should delete file", async () => {
192
+ await client.connect({ host: "test" });
193
+ await client.remove("/file.txt");
194
+
195
+ expect(mockDelete).toHaveBeenCalledWith("/file.txt");
196
+ });
197
+ });
198
+
199
+ describe("put", () => {
200
+ it("Should upload from local path using fastPut", async () => {
201
+ await client.connect({ host: "test" });
202
+ await client.put("/local/file.txt", "/remote/file.txt");
203
+
204
+ expect(mockFastPut).toHaveBeenCalledWith("/local/file.txt", "/remote/file.txt");
205
+ });
206
+
207
+ it("Should upload from Uint8Array using put", async () => {
208
+ await client.connect({ host: "test" });
209
+ const bytes = new TextEncoder().encode("content");
210
+ await client.put(bytes, "/remote/file.txt");
211
+
212
+ // Converted and passed via Buffer.from
213
+ expect(mockPut).toHaveBeenCalled();
214
+ });
215
+ });
216
+
217
+ describe("uploadDir", () => {
218
+ it("Should upload directory", async () => {
219
+ await client.connect({ host: "test" });
220
+ await client.uploadDir("/local/dir", "/remote/dir");
221
+
222
+ expect(mockUploadDir).toHaveBeenCalledWith("/local/dir", "/remote/dir");
223
+ });
224
+ });
225
+
226
+ describe("close", () => {
227
+ it("Should exit without error when close is called before connection", async () => {
228
+ await expect(client.close()).resolves.toBeUndefined();
229
+ });
230
+
231
+ it("Should close connection", async () => {
232
+ await client.connect({ host: "test" });
233
+ await client.close();
234
+
235
+ expect(mockEnd).toHaveBeenCalled();
236
+ });
237
+
238
+ it("Should throw error when calling method after close", async () => {
239
+ await client.connect({ host: "test" });
240
+ await client.close();
241
+
242
+ await expect(client.mkdir("/test")).rejects.toThrow("Not connected to SFTP server.");
243
+ });
244
+
245
+ it("Should allow reconnection after close", async () => {
246
+ await client.connect({ host: "test" });
247
+ await client.close();
248
+ await client.connect({ host: "test" });
249
+
250
+ expect(mockConnect).toHaveBeenCalledTimes(2);
251
+ });
252
+ });
253
+ });
@@ -0,0 +1,172 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { StorageFactory } from "../src/storage-factory";
3
+
4
+ // Mocked functions
5
+ const mockFtpAccess = vi.fn().mockResolvedValue(undefined);
6
+ const mockFtpClose = vi.fn();
7
+
8
+ const mockSftpConnect = vi.fn().mockResolvedValue(undefined);
9
+ const mockSftpEnd = vi.fn().mockResolvedValue(undefined);
10
+
11
+ // Mock basic-ftp module
12
+ vi.mock("basic-ftp", () => {
13
+ return {
14
+ default: {
15
+ Client: class MockClient {
16
+ access = mockFtpAccess;
17
+ close = mockFtpClose;
18
+ },
19
+ },
20
+ };
21
+ });
22
+
23
+ // Mock ssh2-sftp-client module
24
+ vi.mock("ssh2-sftp-client", () => {
25
+ return {
26
+ default: class MockSftpClient {
27
+ connect = mockSftpConnect;
28
+ end = mockSftpEnd;
29
+ },
30
+ };
31
+ });
32
+
33
+ describe("StorageFactory", () => {
34
+ beforeEach(() => {
35
+ vi.clearAllMocks();
36
+ });
37
+
38
+ describe("connect", () => {
39
+ it("Should connect with FTP type", async () => {
40
+ const result = await StorageFactory.connect(
41
+ "ftp",
42
+ { host: "ftp.example.com" },
43
+ () => "result",
44
+ );
45
+
46
+ expect(result).toBe("result");
47
+ expect(mockFtpAccess).toHaveBeenCalledWith(
48
+ expect.objectContaining({
49
+ host: "ftp.example.com",
50
+ secure: false,
51
+ }),
52
+ );
53
+ });
54
+
55
+ it("Should connect with FTPS type", async () => {
56
+ const result = await StorageFactory.connect(
57
+ "ftps",
58
+ { host: "ftps.example.com" },
59
+ () => "result",
60
+ );
61
+
62
+ expect(result).toBe("result");
63
+ expect(mockFtpAccess).toHaveBeenCalledWith(
64
+ expect.objectContaining({
65
+ host: "ftps.example.com",
66
+ secure: true,
67
+ }),
68
+ );
69
+ });
70
+
71
+ it("Should connect with SFTP type", async () => {
72
+ const result = await StorageFactory.connect(
73
+ "sftp",
74
+ { host: "sftp.example.com" },
75
+ () => "result",
76
+ );
77
+
78
+ expect(result).toBe("result");
79
+ expect(mockSftpConnect).toHaveBeenCalledWith(
80
+ expect.objectContaining({
81
+ host: "sftp.example.com",
82
+ }),
83
+ );
84
+ });
85
+
86
+ it("Should return callback function result", async () => {
87
+ const result = await StorageFactory.connect("ftp", { host: "test" }, (storage) => {
88
+ expect(storage).toBeDefined();
89
+ return { data: "test" };
90
+ });
91
+
92
+ expect(result).toEqual({ data: "test" });
93
+ });
94
+
95
+ it("Should close connection and propagate error on error", async () => {
96
+ const error = new Error("Test error");
97
+
98
+ await expect(
99
+ StorageFactory.connect("ftp", { host: "test" }, () => {
100
+ throw error;
101
+ }),
102
+ ).rejects.toThrow("Test error");
103
+
104
+ expect(mockFtpClose).toHaveBeenCalled();
105
+ });
106
+
107
+ it("Should close connection after operation completes", async () => {
108
+ await StorageFactory.connect("ftp", { host: "test" }, () => {
109
+ return "done";
110
+ });
111
+
112
+ expect(mockFtpClose).toHaveBeenCalled();
113
+ });
114
+
115
+ it("Should close SFTP connection and propagate error on error", async () => {
116
+ const error = new Error("Test error");
117
+
118
+ await expect(
119
+ StorageFactory.connect("sftp", { host: "test" }, () => {
120
+ throw error;
121
+ }),
122
+ ).rejects.toThrow("Test error");
123
+
124
+ expect(mockSftpEnd).toHaveBeenCalled();
125
+ });
126
+
127
+ it("Should close SFTP connection after operation completes", async () => {
128
+ await StorageFactory.connect("sftp", { host: "test" }, () => {
129
+ return "done";
130
+ });
131
+
132
+ expect(mockSftpEnd).toHaveBeenCalled();
133
+ });
134
+
135
+ it("Should work independently for concurrent connections", async () => {
136
+ const executionOrder: string[] = [];
137
+
138
+ const promise1 = StorageFactory.connect("ftp", { host: "test" }, async () => {
139
+ executionOrder.push("task1-start");
140
+ await new Promise((resolve) => setTimeout(resolve, 10));
141
+ executionOrder.push("task1-end");
142
+ return "result1";
143
+ });
144
+
145
+ const promise2 = StorageFactory.connect("ftp", { host: "test" }, async () => {
146
+ executionOrder.push("task2-start");
147
+ await new Promise((resolve) => setTimeout(resolve, 5));
148
+ executionOrder.push("task2-end");
149
+ return "result2";
150
+ });
151
+
152
+ const [result1, result2] = await Promise.all([promise1, promise2]);
153
+
154
+ expect(result1).toBe("result1");
155
+ expect(result2).toBe("result2");
156
+ expect(executionOrder).toContain("task1-start");
157
+ expect(executionOrder).toContain("task2-start");
158
+ expect(executionOrder).toContain("task1-end");
159
+ expect(executionOrder).toContain("task2-end");
160
+ expect(mockFtpAccess).toHaveBeenCalledTimes(2);
161
+ expect(mockFtpClose).toHaveBeenCalledTimes(2);
162
+ });
163
+
164
+ it("Should propagate error on connection failure", async () => {
165
+ mockFtpAccess.mockRejectedValueOnce(new Error("Connection failed"));
166
+
167
+ await expect(StorageFactory.connect("ftp", { host: "test" }, () => "result")).rejects.toThrow(
168
+ "Connection failed",
169
+ );
170
+ });
171
+ });
172
+ });