@symbiosis-lab/moss-plugin-github 1.5.1
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/CHANGELOG.md +30 -0
- package/README.md +18 -0
- package/assets/icon.svg +3 -0
- package/assets/manifest.json +19 -0
- package/e2e/deploy-api.test.ts +1129 -0
- package/e2e/moss-cli.test.ts +478 -0
- package/features/auth/device-flow.feature +41 -0
- package/features/deploy/validation.feature +50 -0
- package/features/steps/auth.steps.ts +285 -0
- package/features/steps/deploy.steps.ts +354 -0
- package/package.json +51 -0
- package/src/__tests__/auth-flow.integration.test.ts +738 -0
- package/src/__tests__/auth.test.ts +147 -0
- package/src/__tests__/configure-domain.test.ts +263 -0
- package/src/__tests__/deploy.integration.test.ts +798 -0
- package/src/__tests__/git.test.ts +190 -0
- package/src/__tests__/github-api.test.ts +761 -0
- package/src/__tests__/github-deploy.test.ts +2411 -0
- package/src/__tests__/progress-timeout.test.ts +209 -0
- package/src/__tests__/repo-setup-progress.test.ts +367 -0
- package/src/__tests__/repo-setup.test.ts +370 -0
- package/src/__tests__/token.test.ts +152 -0
- package/src/__tests__/utils.test.ts +129 -0
- package/src/__tests__/workflow.test.ts +146 -0
- package/src/auth.ts +588 -0
- package/src/constants.ts +7 -0
- package/src/git.ts +60 -0
- package/src/github-api.ts +601 -0
- package/src/github-deploy.ts +593 -0
- package/src/main.ts +646 -0
- package/src/repo-setup.ts +685 -0
- package/src/token.ts +202 -0
- package/src/types.ts +91 -0
- package/src/utils.ts +108 -0
- package/src/workflow.ts +79 -0
- package/test-helpers/mock-github-api.ts +217 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +50 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for authentication module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
6
|
+
import { hasRequiredScopes, CLIENT_ID, REQUIRED_SCOPES } from "../auth";
|
|
7
|
+
import {
|
|
8
|
+
setupMockTauri,
|
|
9
|
+
type MockTauriContext,
|
|
10
|
+
} from "@symbiosis-lab/moss-api/testing";
|
|
11
|
+
|
|
12
|
+
// Track httpPost calls for header verification
|
|
13
|
+
let httpPostCalls: Array<{ url: string; body: any; options: any }> = [];
|
|
14
|
+
|
|
15
|
+
// Mock the moss-api module to track httpPost calls
|
|
16
|
+
vi.mock("@symbiosis-lab/moss-api", async (importOriginal) => {
|
|
17
|
+
const actual = await importOriginal<typeof import("@symbiosis-lab/moss-api")>();
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
httpPost: vi.fn(async (url: string, body: any, options: any) => {
|
|
21
|
+
httpPostCalls.push({ url, body, options });
|
|
22
|
+
return actual.httpPost(url, body, options);
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Mock the utils module to prevent actual IPC calls
|
|
28
|
+
vi.mock("../utils", () => ({
|
|
29
|
+
reportProgress: vi.fn().mockResolvedValue(undefined),
|
|
30
|
+
reportError: vi.fn().mockResolvedValue(undefined),
|
|
31
|
+
setCurrentHookName: vi.fn(),
|
|
32
|
+
sleep: vi.fn().mockResolvedValue(undefined),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
describe("auth", () => {
|
|
36
|
+
describe("configuration", () => {
|
|
37
|
+
it("has a valid client ID", () => {
|
|
38
|
+
expect(CLIENT_ID).toBeDefined();
|
|
39
|
+
expect(CLIENT_ID.length).toBeGreaterThan(10);
|
|
40
|
+
// GitHub OAuth client IDs start with "Ov23"
|
|
41
|
+
expect(CLIENT_ID).toMatch(/^Ov/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("requires repo scope for gh-pages deployment", () => {
|
|
45
|
+
// gh-pages deployment pushes directly - no GitHub Actions needed
|
|
46
|
+
expect(REQUIRED_SCOPES).toContain("repo");
|
|
47
|
+
expect(REQUIRED_SCOPES).not.toContain("workflow");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("hasRequiredScopes", () => {
|
|
52
|
+
it("returns true when all required scopes are present", () => {
|
|
53
|
+
const scopes = ["repo", "user"];
|
|
54
|
+
expect(hasRequiredScopes(scopes)).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns true with exact required scopes", () => {
|
|
58
|
+
const scopes = ["repo"];
|
|
59
|
+
expect(hasRequiredScopes(scopes)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns false when repo scope is missing", () => {
|
|
63
|
+
const scopes = ["user", "gist"];
|
|
64
|
+
expect(hasRequiredScopes(scopes)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns true with additional scopes beyond repo", () => {
|
|
68
|
+
const scopes = ["repo", "workflow", "user"];
|
|
69
|
+
expect(hasRequiredScopes(scopes)).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns false with empty scopes", () => {
|
|
73
|
+
expect(hasRequiredScopes([])).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns false with unrelated scopes only", () => {
|
|
77
|
+
const scopes = ["user", "read:org", "gist"];
|
|
78
|
+
expect(hasRequiredScopes(scopes)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ==========================================================================
|
|
83
|
+
// Phase 1: Origin Header Tests
|
|
84
|
+
// ==========================================================================
|
|
85
|
+
describe("OAuth requests include Origin header", () => {
|
|
86
|
+
let ctx: MockTauriContext;
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
ctx = setupMockTauri();
|
|
90
|
+
httpPostCalls = []; // Reset tracking
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
ctx.cleanup();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("requestDeviceCode includes Origin header", async () => {
|
|
98
|
+
// Setup mock response
|
|
99
|
+
ctx.urlConfig.setResponse("https://github.com/login/device/code", {
|
|
100
|
+
status: 200,
|
|
101
|
+
ok: true,
|
|
102
|
+
bodyBase64: btoa(JSON.stringify({
|
|
103
|
+
device_code: "test-device-code",
|
|
104
|
+
user_code: "TEST-CODE",
|
|
105
|
+
verification_uri: "https://github.com/login/device",
|
|
106
|
+
expires_in: 900,
|
|
107
|
+
interval: 5,
|
|
108
|
+
})),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const { requestDeviceCode } = await import("../auth");
|
|
112
|
+
await requestDeviceCode();
|
|
113
|
+
|
|
114
|
+
// Find the call to device code URL
|
|
115
|
+
const deviceCodeCall = httpPostCalls.find(
|
|
116
|
+
call => call.url === "https://github.com/login/device/code"
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(deviceCodeCall).toBeDefined();
|
|
120
|
+
expect(deviceCodeCall?.options?.headers?.Origin).toBe("https://github.com");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("pollForToken includes Origin header", async () => {
|
|
124
|
+
// Setup mock response
|
|
125
|
+
ctx.urlConfig.setResponse("https://github.com/login/oauth/access_token", {
|
|
126
|
+
status: 200,
|
|
127
|
+
ok: true,
|
|
128
|
+
bodyBase64: btoa(JSON.stringify({
|
|
129
|
+
access_token: "gho_test_token",
|
|
130
|
+
token_type: "bearer",
|
|
131
|
+
scope: "repo",
|
|
132
|
+
})),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const { pollForToken } = await import("../auth");
|
|
136
|
+
await pollForToken("test-device-code", 5);
|
|
137
|
+
|
|
138
|
+
// Find the call to token URL
|
|
139
|
+
const tokenCall = httpPostCalls.find(
|
|
140
|
+
call => call.url === "https://github.com/login/oauth/access_token"
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(tokenCall).toBeDefined();
|
|
144
|
+
expect(tokenCall?.options?.headers?.Origin).toBe("https://github.com");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for idempotent configure_domain hook
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator calls configure_domain multiple times:
|
|
5
|
+
* 1. After DNS is configured (site may not be live yet)
|
|
6
|
+
* 2. After the site is verified live via HTTP 200
|
|
7
|
+
*
|
|
8
|
+
* The hook must check current state and do only the next needed step:
|
|
9
|
+
* - Phase 1: CNAME not set -> set it (without HTTPS enforcement)
|
|
10
|
+
* - Phase 2: CNAME set, no HTTPS -> enforce HTTPS
|
|
11
|
+
* - Phase 3: Fully configured -> no-op
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Hoisted setup — runs before vi.mock factories and module imports
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
const { mockGetPages, mockSetCustomDomain, mockEnforceHttps } = vi.hoisted(() => {
|
|
21
|
+
// Provide `window` for main.ts module-scope plugin registration
|
|
22
|
+
// (main.ts: window.GithubPlugin = GithubPlugin)
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
(globalThis as any).window = globalThis;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
mockGetPages: vi.fn(),
|
|
28
|
+
mockSetCustomDomain: vi.fn(),
|
|
29
|
+
mockEnforceHttps: vi.fn(),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Mocks — must be declared before imports (vi.mock is hoisted)
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
vi.mock("../github-api", () => ({
|
|
38
|
+
getPages: (...args: unknown[]) => mockGetPages(...args),
|
|
39
|
+
setCustomDomain: (...args: unknown[]) => mockSetCustomDomain(...args),
|
|
40
|
+
enforceHttps: (...args: unknown[]) => mockEnforceHttps(...args),
|
|
41
|
+
// Stubs for other exports that main.ts imports
|
|
42
|
+
checkPagesStatus: vi.fn(),
|
|
43
|
+
ensurePagesSource: vi.fn(),
|
|
44
|
+
GITHUB_API_BASE: "https://api.github.com",
|
|
45
|
+
GITHUB_API_HEADERS: {},
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
vi.mock("../github-deploy", () => ({
|
|
49
|
+
getOriginOwnerRepo: vi.fn().mockResolvedValue({ owner: "testuser", repo: "testrepo" }),
|
|
50
|
+
verifyRepoExists: vi.fn(),
|
|
51
|
+
deployViaGitPush: vi.fn(),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
vi.mock("../token", () => ({
|
|
55
|
+
getToken: vi.fn().mockResolvedValue("test-token-123"),
|
|
56
|
+
getTokenFromGit: vi.fn().mockResolvedValue(null),
|
|
57
|
+
storeToken: vi.fn(),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
vi.mock("../auth", () => ({
|
|
61
|
+
promptLogin: vi.fn(),
|
|
62
|
+
validateToken: vi.fn(),
|
|
63
|
+
hasRequiredScopes: vi.fn(),
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
vi.mock("../repo-setup", () => ({
|
|
67
|
+
ensureGitHubRepo: vi.fn(),
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
vi.mock("../git", () => ({
|
|
71
|
+
buildPagesUrl: vi.fn().mockReturnValue("https://testuser.github.io/testrepo"),
|
|
72
|
+
parseGitHubUrl: vi.fn(),
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
vi.mock("../utils", () => ({
|
|
76
|
+
reportProgress: vi.fn(),
|
|
77
|
+
reportError: vi.fn(),
|
|
78
|
+
setCurrentHookName: vi.fn(),
|
|
79
|
+
showToast: vi.fn(),
|
|
80
|
+
closeBrowser: vi.fn(),
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
vi.mock("@symbiosis-lab/moss-api", () => ({
|
|
84
|
+
getTauriCore: () => ({
|
|
85
|
+
invoke: vi.fn().mockResolvedValue("/usr/bin/git"),
|
|
86
|
+
}),
|
|
87
|
+
setMessageContext: vi.fn(),
|
|
88
|
+
reportProgress: vi.fn(),
|
|
89
|
+
reportError: vi.fn(),
|
|
90
|
+
showToast: vi.fn(),
|
|
91
|
+
dismissToast: vi.fn(),
|
|
92
|
+
closeBrowser: vi.fn(),
|
|
93
|
+
getPluginCookie: vi.fn(),
|
|
94
|
+
setPluginCookie: vi.fn(),
|
|
95
|
+
executeBinary: vi.fn(),
|
|
96
|
+
listSiteFilesWithSizes: vi.fn(),
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Import the function under test (after mocks are set up)
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
import { configure_domain } from "../main";
|
|
104
|
+
import type { ConfigureDomainContext } from "../types";
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Test Helpers
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
function makeContext(domain: string): ConfigureDomainContext {
|
|
111
|
+
return {
|
|
112
|
+
domain,
|
|
113
|
+
deployment: {
|
|
114
|
+
method: "github-pages",
|
|
115
|
+
url: "https://testuser.github.io/testrepo",
|
|
116
|
+
deployed_at: "2026-01-01T00:00:00Z",
|
|
117
|
+
metadata: {},
|
|
118
|
+
},
|
|
119
|
+
project_info: {
|
|
120
|
+
name: "test-project",
|
|
121
|
+
version: "1.0.0",
|
|
122
|
+
} as ConfigureDomainContext["project_info"],
|
|
123
|
+
config: {},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Tests
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
describe("configure_domain (idempotent)", () => {
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
vi.clearAllMocks();
|
|
134
|
+
// Default mock: setCustomDomain succeeds
|
|
135
|
+
mockSetCustomDomain.mockResolvedValue(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// --------------------------------------------------------------------------
|
|
139
|
+
// 1. Pages not enabled
|
|
140
|
+
// --------------------------------------------------------------------------
|
|
141
|
+
it("returns failure when GitHub Pages is not enabled", async () => {
|
|
142
|
+
mockGetPages.mockResolvedValue(null);
|
|
143
|
+
|
|
144
|
+
const result = await configure_domain(makeContext("example.com"));
|
|
145
|
+
|
|
146
|
+
expect(result.success).toBe(false);
|
|
147
|
+
expect(result.message).toContain("Deploy first");
|
|
148
|
+
expect(mockSetCustomDomain).not.toHaveBeenCalled();
|
|
149
|
+
expect(mockEnforceHttps).not.toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// --------------------------------------------------------------------------
|
|
153
|
+
// 2. CNAME not set
|
|
154
|
+
// --------------------------------------------------------------------------
|
|
155
|
+
it("sets CNAME when pages exist but cname is null", async () => {
|
|
156
|
+
mockGetPages.mockResolvedValue({ cname: null, https_enforced: false });
|
|
157
|
+
|
|
158
|
+
const result = await configure_domain(makeContext("example.com"));
|
|
159
|
+
|
|
160
|
+
expect(result.success).toBe(true);
|
|
161
|
+
expect(mockSetCustomDomain).toHaveBeenCalledWith("testuser", "testrepo", "test-token-123", "example.com");
|
|
162
|
+
expect(mockEnforceHttps).not.toHaveBeenCalled();
|
|
163
|
+
expect(result.message).toContain("example.com");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// --------------------------------------------------------------------------
|
|
167
|
+
// 3. CNAME wrong
|
|
168
|
+
// --------------------------------------------------------------------------
|
|
169
|
+
it("sets CNAME when current cname differs from requested domain", async () => {
|
|
170
|
+
mockGetPages.mockResolvedValue({ cname: "old.com", https_enforced: false });
|
|
171
|
+
|
|
172
|
+
const result = await configure_domain(makeContext("new.com"));
|
|
173
|
+
|
|
174
|
+
expect(result.success).toBe(true);
|
|
175
|
+
expect(mockSetCustomDomain).toHaveBeenCalledWith("testuser", "testrepo", "test-token-123", "new.com");
|
|
176
|
+
expect(mockEnforceHttps).not.toHaveBeenCalled();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// --------------------------------------------------------------------------
|
|
180
|
+
// 4. CNAME set, HTTPS not enforced, enforce succeeds
|
|
181
|
+
// --------------------------------------------------------------------------
|
|
182
|
+
it("enforces HTTPS when CNAME is set but HTTPS is not enforced", async () => {
|
|
183
|
+
mockGetPages.mockResolvedValue({ cname: "example.com", https_enforced: false });
|
|
184
|
+
mockEnforceHttps.mockResolvedValue(true);
|
|
185
|
+
|
|
186
|
+
const result = await configure_domain(makeContext("example.com"));
|
|
187
|
+
|
|
188
|
+
expect(result.success).toBe(true);
|
|
189
|
+
expect(result.message).toContain("HTTPS enforced");
|
|
190
|
+
expect(mockSetCustomDomain).not.toHaveBeenCalled();
|
|
191
|
+
expect(mockEnforceHttps).toHaveBeenCalledWith("testuser", "testrepo", "test-token-123");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// --------------------------------------------------------------------------
|
|
195
|
+
// 5. CNAME set, HTTPS not enforced, enforce fails (cert pending)
|
|
196
|
+
// --------------------------------------------------------------------------
|
|
197
|
+
it("returns success with pending message when HTTPS enforcement fails (cert not ready)", async () => {
|
|
198
|
+
mockGetPages.mockResolvedValue({ cname: "example.com", https_enforced: false });
|
|
199
|
+
mockEnforceHttps.mockResolvedValue(false);
|
|
200
|
+
|
|
201
|
+
const result = await configure_domain(makeContext("example.com"));
|
|
202
|
+
|
|
203
|
+
// This is NOT a failure — cert will come eventually, orchestrator will retry
|
|
204
|
+
expect(result.success).toBe(true);
|
|
205
|
+
expect(result.message).toContain("pending");
|
|
206
|
+
expect(mockSetCustomDomain).not.toHaveBeenCalled();
|
|
207
|
+
expect(mockEnforceHttps).toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// --------------------------------------------------------------------------
|
|
211
|
+
// 6. Fully configured — no-op
|
|
212
|
+
// --------------------------------------------------------------------------
|
|
213
|
+
it("returns success without making API calls when fully configured", async () => {
|
|
214
|
+
mockGetPages.mockResolvedValue({ cname: "example.com", https_enforced: true });
|
|
215
|
+
|
|
216
|
+
const result = await configure_domain(makeContext("example.com"));
|
|
217
|
+
|
|
218
|
+
expect(result.success).toBe(true);
|
|
219
|
+
expect(result.message).toContain("already configured");
|
|
220
|
+
expect(mockSetCustomDomain).not.toHaveBeenCalled();
|
|
221
|
+
expect(mockEnforceHttps).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// --------------------------------------------------------------------------
|
|
225
|
+
// 7. Case-insensitive domain comparison
|
|
226
|
+
// --------------------------------------------------------------------------
|
|
227
|
+
it("treats domain comparison as case-insensitive", async () => {
|
|
228
|
+
mockGetPages.mockResolvedValue({ cname: "Example.COM", https_enforced: true });
|
|
229
|
+
|
|
230
|
+
const result = await configure_domain(makeContext("example.com"));
|
|
231
|
+
|
|
232
|
+
// Should NOT try to set CNAME — domain matches (case-insensitive)
|
|
233
|
+
expect(result.success).toBe(true);
|
|
234
|
+
expect(mockSetCustomDomain).not.toHaveBeenCalled();
|
|
235
|
+
expect(mockEnforceHttps).not.toHaveBeenCalled();
|
|
236
|
+
expect(result.message).toContain("already configured");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// --------------------------------------------------------------------------
|
|
240
|
+
// 8. setCustomDomain throws an error
|
|
241
|
+
// --------------------------------------------------------------------------
|
|
242
|
+
it("returns failure when setCustomDomain throws", async () => {
|
|
243
|
+
mockGetPages.mockResolvedValue({ cname: null, https_enforced: false });
|
|
244
|
+
mockSetCustomDomain.mockRejectedValue(new Error("GitHub Pages API error (500): Internal Server Error"));
|
|
245
|
+
|
|
246
|
+
const result = await configure_domain(makeContext("example.com"));
|
|
247
|
+
|
|
248
|
+
expect(result.success).toBe(false);
|
|
249
|
+
expect(result.message).toContain("GitHub Pages API error");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// --------------------------------------------------------------------------
|
|
253
|
+
// 9. getPages throws a network error
|
|
254
|
+
// --------------------------------------------------------------------------
|
|
255
|
+
it("returns failure when getPages throws a network error", async () => {
|
|
256
|
+
mockGetPages.mockRejectedValue(new Error("fetch failed"));
|
|
257
|
+
|
|
258
|
+
const result = await configure_domain(makeContext("example.com"));
|
|
259
|
+
|
|
260
|
+
expect(result.success).toBe(false);
|
|
261
|
+
expect(result.message).toContain("fetch failed");
|
|
262
|
+
});
|
|
263
|
+
});
|