@nosto/nosto-cli 1.0.4 → 1.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/.github/copilot-instructions.md +2 -2
- package/.github/dependabot.yml +12 -1
- package/README.md +2 -2
- package/eslint.config.js +11 -1
- package/package.json +8 -8
- package/src/api/retry.ts +24 -8
- package/src/config/authConfig.ts +2 -2
- package/src/config/config.ts +7 -1
- package/src/errors/InvalidLoginResponseError.ts +1 -1
- package/src/errors/withErrorHandler.ts +10 -14
- package/src/filesystem/homeDirectory.ts +3 -0
- package/src/filesystem/processInBatches.ts +1 -0
- package/src/modules/login.ts +10 -4
- package/src/modules/search-templates/push.ts +1 -21
- package/src/modules/setup.ts +1 -3
- package/src/modules/status.ts +1 -1
- package/src/vite-env.d.ts +1 -0
- package/test/api/retry.test.ts +41 -1
- package/test/commander.test.ts +31 -2
- package/test/config/authConfig.test.ts +62 -0
- package/test/config/config.test.ts +71 -0
- package/test/console/logger.test.ts +16 -0
- package/test/console/userPrompt.test.ts +30 -0
- package/test/errors/withErrorHandler.test.ts +80 -5
- package/test/filesystem/asserts/assertGitRepo.test.ts +35 -0
- package/test/filesystem/asserts/assertNostoTemplate.test.ts +53 -0
- package/test/filesystem/filesystem.test.ts +17 -1
- package/test/filesystem/plugins.test.ts +73 -2
- package/test/filesystem/processInBatches.test.ts +34 -0
- package/test/modules/login.test.ts +38 -0
- package/test/modules/logout.test.ts +24 -0
- package/test/modules/search-templates/build.legacy.test.ts +25 -0
- package/test/modules/search-templates/build.modern.test.ts +5 -1
- package/test/modules/search-templates/dev.legacy.test.ts +23 -0
- package/test/modules/search-templates/pull.test.ts +38 -1
- package/test/modules/search-templates/push.test.ts +145 -1
- package/test/modules/setup.test.ts +16 -0
- package/test/modules/status.test.ts +45 -4
- package/test/setup.ts +11 -0
- package/test/utils/generateEndpointMock.ts +10 -3
- package/test/utils/mockAuthServer.ts +72 -0
- package/test/utils/mockConsole.ts +20 -0
- package/test/utils/mockFileSystem.ts +46 -12
- package/test/utils/mockStarterManifest.ts +3 -2
- package/vitest.config.ts +13 -5
|
@@ -4,7 +4,7 @@ import { pushSearchTemplate } from "#modules/search-templates/push.ts"
|
|
|
4
4
|
import { setupMockConfig } from "#test/utils/mockConfig.ts"
|
|
5
5
|
import { setupMockConsole } from "#test/utils/mockConsole.ts"
|
|
6
6
|
import { setupMockFileSystem } from "#test/utils/mockFileSystem.ts"
|
|
7
|
-
import { mockPutSourceFile, setupMockServer } from "#test/utils/mockServer.ts"
|
|
7
|
+
import { mockFetchSourceFile, mockPutSourceFile, setupMockServer } from "#test/utils/mockServer.ts"
|
|
8
8
|
|
|
9
9
|
const fs = setupMockFileSystem()
|
|
10
10
|
const server = setupMockServer()
|
|
@@ -106,4 +106,148 @@ describe("Push Search Template", () => {
|
|
|
106
106
|
|
|
107
107
|
await pushSearchTemplate({ paths: [], force: true })
|
|
108
108
|
})
|
|
109
|
+
|
|
110
|
+
it("should abort if remote template is already up to date", async () => {
|
|
111
|
+
fs.writeFile("index.js", "old content")
|
|
112
|
+
mockFetchSourceFile(server, {
|
|
113
|
+
path: "build/hash",
|
|
114
|
+
response: "34a780ad578b997db55b260beb60b501f3e04d30ba1a51fcf43cd8dd1241780d",
|
|
115
|
+
contentType: "raw"
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await pushSearchTemplate({ paths: [], force: false })
|
|
119
|
+
expect(terminal.getSpy("success")).toHaveBeenCalledWith("Remote template is already up to date.")
|
|
120
|
+
fs.expectFile("/.nostocache/hash").toContain("34a780ad578b997db55b260beb60b501f3e04d30ba1a51fcf43cd8dd1241780d")
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("should skip prompt when remote and last seen hashes match", async () => {
|
|
124
|
+
fs.writeFile("index.js", "content with @nosto/preact")
|
|
125
|
+
fs.writeFile(".nostocache/hash", "matching-hash")
|
|
126
|
+
|
|
127
|
+
mockFetchSourceFile(server, {
|
|
128
|
+
path: "build/hash",
|
|
129
|
+
response: "matching-hash",
|
|
130
|
+
contentType: "raw"
|
|
131
|
+
})
|
|
132
|
+
mockPutSourceFile(server, { path: "index.js" })
|
|
133
|
+
mockPutSourceFile(server, { path: "build/hash" })
|
|
134
|
+
|
|
135
|
+
await pushSearchTemplate({ paths: [], force: false })
|
|
136
|
+
|
|
137
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Pushing template from: /")
|
|
138
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Found 2 files to push (1 source, 1 built, 0 ignored).")
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it("should handle build/hash file already existing", async () => {
|
|
142
|
+
fs.writeFile("index.js", "content with @nosto/preact")
|
|
143
|
+
fs.writeFile(".nostocache/hash", "matching-hash")
|
|
144
|
+
fs.writeFile("build/hash", "matching-hash")
|
|
145
|
+
|
|
146
|
+
mockFetchSourceFile(server, {
|
|
147
|
+
path: "build/hash",
|
|
148
|
+
response: "matching-hash",
|
|
149
|
+
contentType: "raw"
|
|
150
|
+
})
|
|
151
|
+
mockPutSourceFile(server, { path: "index.js" })
|
|
152
|
+
mockPutSourceFile(server, { path: "build/hash" })
|
|
153
|
+
|
|
154
|
+
await pushSearchTemplate({ paths: [], force: false })
|
|
155
|
+
|
|
156
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Pushing template from: /")
|
|
157
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Found 2 files to push (1 source, 1 built, 1 ignored).")
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it("should prompt for confirmation when remote hash exists but no last seen hash", async () => {
|
|
161
|
+
fs.writeFile("index.js", "content with @nosto/preact")
|
|
162
|
+
|
|
163
|
+
mockFetchSourceFile(server, {
|
|
164
|
+
path: "build/hash",
|
|
165
|
+
response: "remote-hash",
|
|
166
|
+
contentType: "raw"
|
|
167
|
+
})
|
|
168
|
+
mockPutSourceFile(server, { path: "index.js" })
|
|
169
|
+
mockPutSourceFile(server, { path: "build/hash" })
|
|
170
|
+
|
|
171
|
+
terminal.setUserResponse("y")
|
|
172
|
+
|
|
173
|
+
await pushSearchTemplate({ paths: [], force: false })
|
|
174
|
+
|
|
175
|
+
terminal.expect.user.toHaveBeenPromptedWith(
|
|
176
|
+
"It seems that this is the first time you are pushing to this environment. Please make sure your local copy is up to date. Continue? (y/N):"
|
|
177
|
+
)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it("should prompt for confirmation when remote hash differs from last seen hash", async () => {
|
|
181
|
+
fs.writeFile("index.js", "content with @nosto/preact")
|
|
182
|
+
fs.writeFile(".nostocache/hash", "old-hash")
|
|
183
|
+
|
|
184
|
+
mockFetchSourceFile(server, {
|
|
185
|
+
path: "build/hash",
|
|
186
|
+
response: "different-remote-hash",
|
|
187
|
+
contentType: "raw"
|
|
188
|
+
})
|
|
189
|
+
mockPutSourceFile(server, { path: "index.js" })
|
|
190
|
+
mockPutSourceFile(server, { path: "build/hash" })
|
|
191
|
+
|
|
192
|
+
terminal.setUserResponse("y")
|
|
193
|
+
|
|
194
|
+
await pushSearchTemplate({ paths: [], force: false })
|
|
195
|
+
|
|
196
|
+
terminal.expect.user.toHaveBeenPromptedWith(
|
|
197
|
+
"It seems that the template has been changed since your last push. Are you sure you want to continue? (y/N):"
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it("should prompt for confirmation when no remote hash exists", async () => {
|
|
202
|
+
fs.writeFile("index.js", "content with @nosto/preact")
|
|
203
|
+
fs.writeFile(".nostocache/hash", "some-hash")
|
|
204
|
+
|
|
205
|
+
mockFetchSourceFile(server, {
|
|
206
|
+
path: "build/hash",
|
|
207
|
+
error: { status: 404, message: "Not Found" }
|
|
208
|
+
})
|
|
209
|
+
mockPutSourceFile(server, { path: "index.js" })
|
|
210
|
+
mockPutSourceFile(server, { path: "build/hash" })
|
|
211
|
+
|
|
212
|
+
terminal.setUserResponse("y")
|
|
213
|
+
|
|
214
|
+
await pushSearchTemplate({ paths: [], force: false })
|
|
215
|
+
|
|
216
|
+
terminal.expect.user.toHaveBeenPromptedWith(
|
|
217
|
+
"It seems that this is the first time you are pushing to this environment. Please make sure your local copy is up to date. Continue? (y/N):"
|
|
218
|
+
)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("should cancel operation when user declines prompt for first-time push", async () => {
|
|
222
|
+
fs.writeFile("index.js", "content with @nosto/preact")
|
|
223
|
+
|
|
224
|
+
mockFetchSourceFile(server, {
|
|
225
|
+
path: "build/hash",
|
|
226
|
+
response: "remote-hash",
|
|
227
|
+
contentType: "raw"
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
terminal.setUserResponse("N")
|
|
231
|
+
|
|
232
|
+
await pushSearchTemplate({ paths: [], force: false })
|
|
233
|
+
|
|
234
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Push operation cancelled by user.")
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it("should cancel operation when user declines prompt for conflicting changes", async () => {
|
|
238
|
+
fs.writeFile("index.js", "content with @nosto/preact")
|
|
239
|
+
fs.writeFile(".nostocache/hash", "old-hash")
|
|
240
|
+
|
|
241
|
+
mockFetchSourceFile(server, {
|
|
242
|
+
path: "build/hash",
|
|
243
|
+
response: "different-remote-hash",
|
|
244
|
+
contentType: "raw"
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
terminal.setUserResponse("N")
|
|
248
|
+
|
|
249
|
+
await pushSearchTemplate({ paths: [], force: false })
|
|
250
|
+
|
|
251
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Push operation cancelled by user.")
|
|
252
|
+
})
|
|
109
253
|
})
|
|
@@ -31,6 +31,22 @@ describe("Setup Module", () => {
|
|
|
31
31
|
fs.expectFile(".nosto.json").toExist()
|
|
32
32
|
})
|
|
33
33
|
|
|
34
|
+
it("should create config file with merchant param", async () => {
|
|
35
|
+
await printSetupHelp(".", { merchant: "test-merchant" })
|
|
36
|
+
|
|
37
|
+
fs.expectFile(".nosto.json").toExist()
|
|
38
|
+
fs.expectFile(".nosto.json").toContain(expect.stringContaining('"merchant": "test-merchant"'))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("should include environmental variables in created config file", async () => {
|
|
42
|
+
process.env.NOSTO_API_KEY = "env-variable-key"
|
|
43
|
+
terminal.setUserResponse("Y")
|
|
44
|
+
|
|
45
|
+
await printSetupHelp(".", {})
|
|
46
|
+
|
|
47
|
+
fs.expectFile(".nosto.json").toContain(expect.stringContaining('"apiKey": "env-variable-key"'))
|
|
48
|
+
})
|
|
49
|
+
|
|
34
50
|
it("should not create config file when user declines", async () => {
|
|
35
51
|
terminal.setUserResponse("N")
|
|
36
52
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest"
|
|
1
|
+
import { describe, expect, it, vi } from "vitest"
|
|
2
2
|
|
|
3
|
+
import * as config from "#config/config.ts"
|
|
3
4
|
import { printStatus } from "#modules/status.ts"
|
|
4
5
|
import { setupMockConfig } from "#test/utils/mockConfig.ts"
|
|
5
6
|
import { setupMockConsole } from "#test/utils/mockConsole.ts"
|
|
@@ -12,11 +13,51 @@ describe("Status Module", () => {
|
|
|
12
13
|
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Configuration is not valid:")
|
|
13
14
|
})
|
|
14
15
|
|
|
15
|
-
it("should indicate valid configuration", async () => {
|
|
16
|
-
setupMockConfig({ merchant: "test-merchant" })
|
|
16
|
+
it("should indicate valid configuration for api key", async () => {
|
|
17
|
+
setupMockConfig({ merchant: "test-merchant", apiKey: "test-key" })
|
|
17
18
|
|
|
18
19
|
await expect(printStatus(".")).resolves.not.toThrow()
|
|
19
20
|
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Configuration seems to be valid:")
|
|
20
|
-
expect(terminal.getSpy("
|
|
21
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith(expect.stringContaining("Using API key for authentication"))
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("should indicate valid configuration for user auth", async () => {
|
|
25
|
+
setupMockConfig({
|
|
26
|
+
merchant: "test-merchant",
|
|
27
|
+
apiKey: "",
|
|
28
|
+
auth: { user: "test", token: "test", expiresAt: new Date(Date.now() + 3600 * 1000) }
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
await expect(printStatus(".")).resolves.not.toThrow()
|
|
32
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Configuration seems to be valid:")
|
|
33
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith(
|
|
34
|
+
expect.stringContaining("Using user account for authentication")
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("should indicate invalid user auth", async () => {
|
|
39
|
+
setupMockConfig({ merchant: "test-merchant", apiKey: "", auth: { user: "", token: "", expiresAt: new Date(0) } })
|
|
40
|
+
|
|
41
|
+
await expect(printStatus(".")).resolves.not.toThrow()
|
|
42
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Configuration is not valid:")
|
|
43
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith(expect.stringContaining("Missing authentication"))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("should indicate expired user auth", async () => {
|
|
47
|
+
setupMockConfig({
|
|
48
|
+
merchant: "test-merchant",
|
|
49
|
+
apiKey: "",
|
|
50
|
+
auth: { user: "test", token: "test", expiresAt: new Date(1) }
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
await expect(printStatus(".")).resolves.not.toThrow()
|
|
54
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith("Configuration is not valid:")
|
|
55
|
+
expect(terminal.getSpy("info")).toHaveBeenCalledWith(expect.stringContaining("Authentication token expired"))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("should rethrow unknown errors in config loading", async () => {
|
|
59
|
+
vi.spyOn(config, "loadConfig").mockRejectedValueOnce(new Error("Unknown error"))
|
|
60
|
+
|
|
61
|
+
await expect(printStatus(".")).rejects.toThrow(/Unknown error/)
|
|
21
62
|
})
|
|
22
63
|
})
|
package/test/setup.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Volume } from "memfs"
|
|
|
2
2
|
import { beforeEach } from "vitest"
|
|
3
3
|
import { vi } from "vitest"
|
|
4
4
|
|
|
5
|
+
import { mockHttpServer } from "./utils/mockAuthServer.ts"
|
|
5
6
|
import { mockedConsoleIn, mockedConsoleOut } from "./utils/mockConsole.ts"
|
|
6
7
|
|
|
7
8
|
export const testVolume = Volume.fromJSON({}, "/")
|
|
@@ -26,3 +27,13 @@ vi.mock("#/console/logger.ts", () => mockedConsoleOut)
|
|
|
26
27
|
vi.mock("node:test", () => {
|
|
27
28
|
throw new Error("You seem to have accidentally imported node:test instead of vitest.")
|
|
28
29
|
})
|
|
30
|
+
|
|
31
|
+
vi.mock("open", () => {
|
|
32
|
+
return {
|
|
33
|
+
default: vi.fn()
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
vi.mock("node:http", () => ({
|
|
38
|
+
default: mockHttpServer
|
|
39
|
+
}))
|
|
@@ -4,14 +4,19 @@ import type { SetupServer } from "msw/node"
|
|
|
4
4
|
type HttpMethod = keyof typeof http
|
|
5
5
|
|
|
6
6
|
export type MockParams<ResponseT extends DefaultBodyType | void> =
|
|
7
|
-
| (ResponseT extends void ? object : { response: ResponseT })
|
|
7
|
+
| (ResponseT extends void ? object : { response: ResponseT; contentType?: "json" | "raw" })
|
|
8
8
|
| {
|
|
9
9
|
error: { status: number; message: string }
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export const generateEndpointMock = (
|
|
13
13
|
server: SetupServer,
|
|
14
|
-
{
|
|
14
|
+
{
|
|
15
|
+
method,
|
|
16
|
+
path,
|
|
17
|
+
contentType,
|
|
18
|
+
...params
|
|
19
|
+
}: { method: HttpMethod; path: string; contentType?: "json" | "raw" } & MockParams<DefaultBodyType>
|
|
15
20
|
) => {
|
|
16
21
|
let invocations: unknown[] = []
|
|
17
22
|
|
|
@@ -36,7 +41,9 @@ export const generateEndpointMock = (
|
|
|
36
41
|
|
|
37
42
|
const requestsWithBody = ["POST", "PUT", "PATCH"]
|
|
38
43
|
invocations.push(requestsWithBody.includes(request.method) ? await toBody(request) : {})
|
|
39
|
-
|
|
44
|
+
if (contentType === "raw") {
|
|
45
|
+
return HttpResponse.text(returnedResponse as string, { status })
|
|
46
|
+
}
|
|
40
47
|
return HttpResponse.json(returnedResponse, { status })
|
|
41
48
|
})
|
|
42
49
|
server.use(handler)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { IncomingMessage, RequestListener, ServerResponse } from "node:http"
|
|
2
|
+
import { AddressInfo } from "node:net"
|
|
3
|
+
|
|
4
|
+
import { vi } from "vitest"
|
|
5
|
+
|
|
6
|
+
import { AuthConfig } from "#config/schema.ts"
|
|
7
|
+
|
|
8
|
+
// Mock the entire http module at the top level
|
|
9
|
+
let queuedServerRequest: Partial<IncomingMessage> | null = null
|
|
10
|
+
const queuedServerResponse = {
|
|
11
|
+
writeHead: vi.fn(),
|
|
12
|
+
end: vi.fn()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let mockedAddressFn: () => string | AddressInfo | null
|
|
16
|
+
export const mockHttpServer: {
|
|
17
|
+
createServer: (listener: RequestListener) => unknown
|
|
18
|
+
} = {
|
|
19
|
+
createServer: listener => {
|
|
20
|
+
process.nextTick(() => {
|
|
21
|
+
if (queuedServerRequest) {
|
|
22
|
+
listener(queuedServerRequest as IncomingMessage, queuedServerResponse as unknown as ServerResponse)
|
|
23
|
+
queuedServerRequest = null
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
return {
|
|
27
|
+
listen: (port: number, hostname?: string | (() => void), callback?: () => void) => {
|
|
28
|
+
const actualCallback = typeof hostname === "function" ? hostname : callback
|
|
29
|
+
if (actualCallback) {
|
|
30
|
+
process.nextTick(actualCallback)
|
|
31
|
+
}
|
|
32
|
+
return {}
|
|
33
|
+
},
|
|
34
|
+
close: (callback?: () => void) => {
|
|
35
|
+
if (callback) {
|
|
36
|
+
process.nextTick(callback)
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
address: () => {
|
|
40
|
+
if (mockedAddressFn) {
|
|
41
|
+
return mockedAddressFn()
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
port: 3000, // Mock port number
|
|
45
|
+
address: "localhost",
|
|
46
|
+
family: "IPv4"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function setupMockAuthServer() {
|
|
54
|
+
return {
|
|
55
|
+
mockValidResponse: (res: AuthConfig) => {
|
|
56
|
+
const params = new URLSearchParams({
|
|
57
|
+
user: res.user,
|
|
58
|
+
token: res.token,
|
|
59
|
+
expiresAt: res.expiresAt.toISOString()
|
|
60
|
+
})
|
|
61
|
+
queuedServerRequest = {
|
|
62
|
+
url: `/?${params.toString()}`
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
mockGenericResponse: (res: Partial<IncomingMessage>) => {
|
|
66
|
+
queuedServerRequest = res
|
|
67
|
+
},
|
|
68
|
+
mockServerAddress: (callback: string | AddressInfo | null) => {
|
|
69
|
+
mockedAddressFn = () => callback
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -19,6 +19,7 @@ export const mockedConsoleOut = {
|
|
|
19
19
|
merchantId: "",
|
|
20
20
|
isDryRun: false
|
|
21
21
|
},
|
|
22
|
+
success: vi.fn(),
|
|
22
23
|
raw: vi.fn(),
|
|
23
24
|
debug: vi.fn(),
|
|
24
25
|
info: vi.fn(),
|
|
@@ -36,9 +37,28 @@ export function setupMockConsole() {
|
|
|
36
37
|
setUserResponse: (response: string) => {
|
|
37
38
|
mockedConsoleIn.userResponse = response
|
|
38
39
|
},
|
|
40
|
+
setContext: (context: Partial<typeof mockedConsoleOut.Logger.context>) => {
|
|
41
|
+
mockedConsoleOut.Logger.context = {
|
|
42
|
+
...mockedConsoleOut.Logger.context,
|
|
43
|
+
...context
|
|
44
|
+
}
|
|
45
|
+
},
|
|
39
46
|
clearPrompts: () => {
|
|
40
47
|
mockedConsoleIn.recordedPrompts = []
|
|
41
48
|
},
|
|
49
|
+
resetMocks: () => {
|
|
50
|
+
mockedConsoleOut.Logger.success.mockReset()
|
|
51
|
+
mockedConsoleOut.Logger.raw.mockReset()
|
|
52
|
+
mockedConsoleOut.Logger.debug.mockReset()
|
|
53
|
+
mockedConsoleOut.Logger.info.mockReset()
|
|
54
|
+
mockedConsoleOut.Logger.warn.mockReset()
|
|
55
|
+
mockedConsoleOut.Logger.error.mockReset()
|
|
56
|
+
mockedConsoleOut.Logger.context = {
|
|
57
|
+
logLevel: "info",
|
|
58
|
+
merchantId: "",
|
|
59
|
+
isDryRun: false
|
|
60
|
+
}
|
|
61
|
+
},
|
|
42
62
|
getSpy: (method: Exclude<keyof typeof mockedConsoleOut.Logger, "context">) => {
|
|
43
63
|
return mockedConsoleOut.Logger[method]
|
|
44
64
|
},
|
|
@@ -2,30 +2,64 @@ import fs from "fs"
|
|
|
2
2
|
import path from "path"
|
|
3
3
|
import { expect } from "vitest"
|
|
4
4
|
|
|
5
|
+
import { getDefaultConfig } from "#config/config.ts"
|
|
6
|
+
import { PersistentConfig } from "#config/schema.ts"
|
|
7
|
+
import { HomeDirectory } from "#filesystem/homeDirectory.ts"
|
|
5
8
|
import { testVolume } from "#test/setup.ts"
|
|
6
9
|
|
|
7
10
|
export function setupMockFileSystem() {
|
|
8
11
|
const fs = testVolume
|
|
12
|
+
const authFilePath = path.join(HomeDirectory, ".nosto", ".auth.json")
|
|
13
|
+
|
|
14
|
+
function writeFileContent(targetFile: string, content: string) {
|
|
15
|
+
const dir = path.join("/", targetFile.substring(0, targetFile.lastIndexOf("/")))
|
|
16
|
+
const filePath = path.join("/", targetFile)
|
|
17
|
+
// Ensure parent directory exists
|
|
18
|
+
if (dir && !fs.existsSync(dir)) {
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
20
|
+
}
|
|
21
|
+
if (fs.existsSync(filePath)) {
|
|
22
|
+
fs.unlinkSync(filePath)
|
|
23
|
+
}
|
|
24
|
+
fs.writeFileSync(filePath, content)
|
|
25
|
+
}
|
|
26
|
+
|
|
9
27
|
return {
|
|
10
|
-
writeFile: (targetFile: string, content: string) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
28
|
+
writeFile: (targetFile: string, content: unknown | string) => {
|
|
29
|
+
if (typeof content === "string") {
|
|
30
|
+
writeFileContent(targetFile, content)
|
|
31
|
+
} else {
|
|
32
|
+
writeFileContent(targetFile, JSON.stringify(content))
|
|
16
33
|
}
|
|
17
|
-
if (fs.existsSync(filePath)) {
|
|
18
|
-
fs.unlinkSync(filePath)
|
|
19
|
-
}
|
|
20
|
-
fs.writeFileSync(filePath, content)
|
|
21
34
|
},
|
|
22
35
|
writeFolder: (targetFolder: string) => {
|
|
23
36
|
const dir = path.join("/", targetFolder)
|
|
24
37
|
fs.mkdirSync(dir, { recursive: true })
|
|
25
38
|
},
|
|
39
|
+
chmod: (targetFile: string, mode: number) => {
|
|
40
|
+
const filePath = path.join("/", targetFile)
|
|
41
|
+
fs.chmodSync(filePath, mode)
|
|
42
|
+
},
|
|
26
43
|
expectFile: (targetFile: string) => {
|
|
27
44
|
const filePath = path.join("/", targetFile)
|
|
28
45
|
return makeFileMatcher(filePath)
|
|
46
|
+
},
|
|
47
|
+
mockConfigFile: (overrides: Partial<PersistentConfig> = {}) => {
|
|
48
|
+
const content = {
|
|
49
|
+
...getDefaultConfig(),
|
|
50
|
+
merchant: "test-merchant",
|
|
51
|
+
apiKey: "test-api-key",
|
|
52
|
+
...overrides
|
|
53
|
+
}
|
|
54
|
+
writeFileContent(".nosto.json", JSON.stringify(content))
|
|
55
|
+
},
|
|
56
|
+
mockUserAuthentication: () => {
|
|
57
|
+
const userAuth = { user: "test", token: "test", expiresAt: new Date(Date.now() + 1000 * 60 * 60) }
|
|
58
|
+
fs.mkdirSync(path.dirname(authFilePath), { recursive: true })
|
|
59
|
+
writeFileContent(authFilePath, JSON.stringify(userAuth))
|
|
60
|
+
},
|
|
61
|
+
paths: {
|
|
62
|
+
authFile: authFilePath
|
|
29
63
|
}
|
|
30
64
|
}
|
|
31
65
|
}
|
|
@@ -34,7 +68,7 @@ export function makeFileMatcher(path: string) {
|
|
|
34
68
|
return {
|
|
35
69
|
toContain: (expectedContent: string) => {
|
|
36
70
|
const content = fs.readFileSync(path, "utf8")
|
|
37
|
-
expect(content, `File ${path} has
|
|
71
|
+
expect(content, `File ${path} actually has\n ${content}`).toEqual(expectedContent)
|
|
38
72
|
},
|
|
39
73
|
toExist: () => {
|
|
40
74
|
return expect(fs.existsSync(path), `File ${path} does not exist when it was expected to`).toBe(true)
|
|
@@ -42,7 +76,7 @@ export function makeFileMatcher(path: string) {
|
|
|
42
76
|
not: {
|
|
43
77
|
toContain: (expectedContent: string) => {
|
|
44
78
|
const content = fs.readFileSync(path, "utf8")
|
|
45
|
-
expect(content, `File ${path} has
|
|
79
|
+
expect(content, `File ${path} actually has\n ${content}`).not.toEqual(expectedContent)
|
|
46
80
|
},
|
|
47
81
|
toExist: () => {
|
|
48
82
|
return expect(fs.existsSync(path), `File ${path} exists when it was not expected to`).toBe(false)
|
|
@@ -2,6 +2,7 @@ import path from "path"
|
|
|
2
2
|
import { vi } from "vitest"
|
|
3
3
|
|
|
4
4
|
import { SearchTemplatesConfig } from "#config/schema.ts"
|
|
5
|
+
import { Logger } from "#console/logger.ts"
|
|
5
6
|
import { makeConfig } from "#exports.ts"
|
|
6
7
|
|
|
7
8
|
import { setupMockFileSystem } from "./mockFileSystem.ts"
|
|
@@ -15,10 +16,10 @@ export function setupMockStarterManifest({ projectPath, mockScript }: Props = {}
|
|
|
15
16
|
const fs = setupMockFileSystem()
|
|
16
17
|
const manifest = makeConfig({
|
|
17
18
|
onBuild: async () => {
|
|
18
|
-
|
|
19
|
+
Logger.debug("Building...")
|
|
19
20
|
},
|
|
20
21
|
onBuildWatch: async () => {
|
|
21
|
-
|
|
22
|
+
Logger.debug("Watching...")
|
|
22
23
|
},
|
|
23
24
|
...mockScript
|
|
24
25
|
})
|
package/vitest.config.ts
CHANGED
|
@@ -13,18 +13,26 @@ export default defineConfig({
|
|
|
13
13
|
include: ["test/**/*.test.ts"],
|
|
14
14
|
exclude: ["node_modules", "dist"],
|
|
15
15
|
setupFiles: ["test/setup.ts"],
|
|
16
|
+
mockReset: true,
|
|
16
17
|
restoreMocks: true,
|
|
17
18
|
reporters: ["default"],
|
|
18
19
|
coverage: {
|
|
19
20
|
provider: "v8",
|
|
20
21
|
reporter: ["text", "json", "html"],
|
|
21
22
|
thresholds: {
|
|
22
|
-
statements:
|
|
23
|
-
branches:
|
|
24
|
-
functions:
|
|
25
|
-
lines:
|
|
23
|
+
statements: 90,
|
|
24
|
+
branches: 90,
|
|
25
|
+
functions: 90,
|
|
26
|
+
lines: 90
|
|
26
27
|
},
|
|
27
|
-
exclude: [
|
|
28
|
+
exclude: [
|
|
29
|
+
"node_modules/",
|
|
30
|
+
"test/",
|
|
31
|
+
"vitest.config.ts",
|
|
32
|
+
"src/bootstrap.sh",
|
|
33
|
+
"*.config.js",
|
|
34
|
+
"src/filesystem/homeDirectory.ts"
|
|
35
|
+
]
|
|
28
36
|
}
|
|
29
37
|
},
|
|
30
38
|
esbuild: {
|