@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
package/src/token.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Storage Module
|
|
3
|
+
*
|
|
4
|
+
* Handles secure storage and retrieval of GitHub access tokens.
|
|
5
|
+
*
|
|
6
|
+
* Storage strategy:
|
|
7
|
+
* 1. Primary: Plugin cookies (via moss-api getPluginCookie/setPluginCookie)
|
|
8
|
+
* 2. Fallback: In-memory cache (for current session)
|
|
9
|
+
*
|
|
10
|
+
* Tokens are used for GitHub REST API authentication.
|
|
11
|
+
* The git credential helper is checked opportunistically (works when git
|
|
12
|
+
* is installed, silently falls through to OAuth when it's not).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getPluginCookie, setPluginCookie, executeBinary } from "@symbiosis-lab/moss-api";
|
|
16
|
+
|
|
17
|
+
const GITHUB_HOST = "github.com";
|
|
18
|
+
const TOKEN_COOKIE_NAME = "__github_access_token";
|
|
19
|
+
|
|
20
|
+
// In-memory fallback cache
|
|
21
|
+
let cachedToken: string | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Format credentials for git credential helper input
|
|
25
|
+
* (Used for documentation and potential future stdin support)
|
|
26
|
+
*/
|
|
27
|
+
export function formatCredentialInput(
|
|
28
|
+
host: string,
|
|
29
|
+
protocol: string,
|
|
30
|
+
username?: string,
|
|
31
|
+
password?: string
|
|
32
|
+
): string {
|
|
33
|
+
const lines = [`protocol=${protocol}`, `host=${host}`];
|
|
34
|
+
if (username) lines.push(`username=${username}`);
|
|
35
|
+
if (password) lines.push(`password=${password}`);
|
|
36
|
+
lines.push(""); // Empty line to signal end of input
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse git credential helper output
|
|
42
|
+
*/
|
|
43
|
+
export function parseCredentialOutput(output: string): {
|
|
44
|
+
username?: string;
|
|
45
|
+
password?: string;
|
|
46
|
+
} {
|
|
47
|
+
const result: { username?: string; password?: string } = {};
|
|
48
|
+
|
|
49
|
+
for (const line of output.split("\n")) {
|
|
50
|
+
const [key, ...valueParts] = line.split("=");
|
|
51
|
+
const value = valueParts.join("="); // Handle = in values
|
|
52
|
+
|
|
53
|
+
if (key === "username") {
|
|
54
|
+
result.username = value;
|
|
55
|
+
} else if (key === "password") {
|
|
56
|
+
result.password = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Try to retrieve GitHub token from git credential helper
|
|
65
|
+
*
|
|
66
|
+
* Uses `git credential fill` with stdin input to query the system's
|
|
67
|
+
* configured credential helper (e.g., macOS Keychain, Windows Credential Manager).
|
|
68
|
+
*
|
|
69
|
+
* @returns The token if found in git credentials, null otherwise
|
|
70
|
+
*/
|
|
71
|
+
export async function getTokenFromGit(gitPath: string = "git"): Promise<string | null> {
|
|
72
|
+
try {
|
|
73
|
+
console.log(" Checking git credential helper for GitHub token...");
|
|
74
|
+
|
|
75
|
+
// Format the credential request for github.com
|
|
76
|
+
const input = formatCredentialInput(GITHUB_HOST, "https");
|
|
77
|
+
|
|
78
|
+
// Execute git credential fill with stdin
|
|
79
|
+
const result = await executeBinary({
|
|
80
|
+
binaryPath: gitPath,
|
|
81
|
+
args: ["credential", "fill"],
|
|
82
|
+
stdin: input,
|
|
83
|
+
timeoutMs: 5000,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
console.log(" No credentials found in git credential helper");
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Parse the credential output
|
|
92
|
+
const { password } = parseCredentialOutput(result.stdout);
|
|
93
|
+
|
|
94
|
+
if (password) {
|
|
95
|
+
console.log(" Found GitHub token in git credential helper");
|
|
96
|
+
return password;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(" Git credential helper returned no password");
|
|
100
|
+
return null;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.log(` Git credential helper failed: ${error}`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Store a GitHub access token
|
|
109
|
+
*
|
|
110
|
+
* Uses plugin cookie storage with in-memory fallback.
|
|
111
|
+
* Note: Plugin identity and project path are auto-detected from runtime context.
|
|
112
|
+
*/
|
|
113
|
+
export async function storeToken(token: string): Promise<boolean> {
|
|
114
|
+
try {
|
|
115
|
+
console.log(" Storing GitHub access token...");
|
|
116
|
+
|
|
117
|
+
// Store in plugin cookies
|
|
118
|
+
try {
|
|
119
|
+
await setPluginCookie([
|
|
120
|
+
{
|
|
121
|
+
name: TOKEN_COOKIE_NAME,
|
|
122
|
+
value: token,
|
|
123
|
+
domain: GITHUB_HOST,
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
console.log(" Token stored in plugin cookies");
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.warn(` Could not store in cookies: ${error}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Always cache in memory as fallback
|
|
132
|
+
cachedToken = token;
|
|
133
|
+
|
|
134
|
+
console.log(" Token stored successfully");
|
|
135
|
+
return true;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(` Error storing token: ${error}`);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Retrieve GitHub access token
|
|
144
|
+
*
|
|
145
|
+
* Checks plugin cookies first, then falls back to memory cache.
|
|
146
|
+
* Note: Plugin identity and project path are auto-detected from runtime context.
|
|
147
|
+
*/
|
|
148
|
+
export async function getToken(): Promise<string | null> {
|
|
149
|
+
// Check memory cache first (faster)
|
|
150
|
+
if (cachedToken) {
|
|
151
|
+
return cachedToken;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Try plugin cookies
|
|
155
|
+
try {
|
|
156
|
+
const cookies = await getPluginCookie();
|
|
157
|
+
const tokenCookie = cookies?.find((c) => c.name === TOKEN_COOKIE_NAME);
|
|
158
|
+
|
|
159
|
+
if (tokenCookie) {
|
|
160
|
+
cachedToken = tokenCookie.value;
|
|
161
|
+
return cachedToken;
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// Cookie retrieval failed, token not available
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Clear the cached token
|
|
172
|
+
*/
|
|
173
|
+
export function clearTokenCache(): void {
|
|
174
|
+
cachedToken = null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Remove GitHub access token
|
|
179
|
+
* Note: Plugin identity and project path are auto-detected from runtime context.
|
|
180
|
+
*/
|
|
181
|
+
export async function clearToken(): Promise<boolean> {
|
|
182
|
+
try {
|
|
183
|
+
console.log(" Clearing GitHub access token...");
|
|
184
|
+
|
|
185
|
+
// Clear from plugin cookies
|
|
186
|
+
try {
|
|
187
|
+
await setPluginCookie([]);
|
|
188
|
+
} catch {
|
|
189
|
+
// Ignore cookie clear errors
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Clear memory cache
|
|
193
|
+
cachedToken = null;
|
|
194
|
+
|
|
195
|
+
console.log(" Token cleared successfully");
|
|
196
|
+
return true;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(` Error clearing token: ${error}`);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin-specific type definitions for the GitHub Pages Deployer Plugin
|
|
3
|
+
*
|
|
4
|
+
* Common types (DeployContext, HookResult, PluginMessage, etc.) are imported
|
|
5
|
+
* from @symbiosis-lab/moss-api.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export SDK types for convenience
|
|
9
|
+
export type {
|
|
10
|
+
DeployContext,
|
|
11
|
+
ConfigureDomainContext,
|
|
12
|
+
HookResult,
|
|
13
|
+
DeploymentInfo,
|
|
14
|
+
ProjectInfo,
|
|
15
|
+
PluginMessage,
|
|
16
|
+
DnsTarget,
|
|
17
|
+
DnsRecord,
|
|
18
|
+
} from "@symbiosis-lab/moss-api";
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// GitHub OAuth Device Flow Types
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Response from GitHub's device code endpoint
|
|
26
|
+
* POST https://github.com/login/device/code
|
|
27
|
+
*/
|
|
28
|
+
export interface DeviceCodeResponse {
|
|
29
|
+
/** The device verification code (used for polling) */
|
|
30
|
+
device_code: string;
|
|
31
|
+
/** The user code to display to the user */
|
|
32
|
+
user_code: string;
|
|
33
|
+
/** The URL where user enters the code */
|
|
34
|
+
verification_uri: string;
|
|
35
|
+
/** Pre-filled URL with user_code embedded (user just clicks "Continue") */
|
|
36
|
+
verification_uri_complete?: string;
|
|
37
|
+
/** Seconds until the codes expire */
|
|
38
|
+
expires_in: number;
|
|
39
|
+
/** Minimum seconds between poll requests */
|
|
40
|
+
interval: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Response from GitHub's access token endpoint
|
|
45
|
+
* POST https://github.com/login/oauth/access_token
|
|
46
|
+
*/
|
|
47
|
+
export interface TokenResponse {
|
|
48
|
+
/** The access token (present on success) */
|
|
49
|
+
access_token?: string;
|
|
50
|
+
/** Token type, usually "bearer" */
|
|
51
|
+
token_type?: string;
|
|
52
|
+
/** Granted scopes */
|
|
53
|
+
scope?: string;
|
|
54
|
+
/** Error code (present on failure) */
|
|
55
|
+
error?: string;
|
|
56
|
+
/** Human-readable error description */
|
|
57
|
+
error_description?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* GitHub user response from /user endpoint
|
|
62
|
+
* Used to validate tokens
|
|
63
|
+
*/
|
|
64
|
+
export interface GitHubUser {
|
|
65
|
+
login: string;
|
|
66
|
+
id: number;
|
|
67
|
+
name?: string;
|
|
68
|
+
email?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Authentication state for the plugin
|
|
73
|
+
*/
|
|
74
|
+
export interface AuthState {
|
|
75
|
+
/** Whether user is authenticated */
|
|
76
|
+
isAuthenticated: boolean;
|
|
77
|
+
/** GitHub username if authenticated */
|
|
78
|
+
username?: string;
|
|
79
|
+
/** Token scopes if authenticated */
|
|
80
|
+
scopes?: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Configuration for the auth module
|
|
85
|
+
*/
|
|
86
|
+
export interface AuthConfig {
|
|
87
|
+
/** GitHub OAuth App Client ID */
|
|
88
|
+
clientId: string;
|
|
89
|
+
/** Required OAuth scopes */
|
|
90
|
+
scopes: string[];
|
|
91
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for the GitHub Deployer Plugin
|
|
3
|
+
*
|
|
4
|
+
* This module wraps SDK utilities with plugin-specific functionality.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
setMessageContext,
|
|
9
|
+
reportProgress as sdkReportProgress,
|
|
10
|
+
reportError as sdkReportError,
|
|
11
|
+
showToast as sdkShowToast,
|
|
12
|
+
dismissToast as sdkDismissToast,
|
|
13
|
+
closeBrowser as sdkCloseBrowser,
|
|
14
|
+
type ToastOptions,
|
|
15
|
+
} from "@symbiosis-lab/moss-api";
|
|
16
|
+
|
|
17
|
+
// Re-export ToastOptions type for convenience
|
|
18
|
+
export type { ToastOptions };
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Plugin Configuration
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
const PLUGIN_NAME = "github";
|
|
25
|
+
|
|
26
|
+
// Initialize message context on load
|
|
27
|
+
setMessageContext(PLUGIN_NAME, "deploy");
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Re-exports from SDK (with plugin context)
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Set the current hook name for message routing
|
|
35
|
+
*/
|
|
36
|
+
export function setCurrentHookName(name: string): void {
|
|
37
|
+
setMessageContext(PLUGIN_NAME, name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Report progress to moss during long-running operations
|
|
42
|
+
*/
|
|
43
|
+
export async function reportProgress(
|
|
44
|
+
phase: string,
|
|
45
|
+
current: number,
|
|
46
|
+
total: number,
|
|
47
|
+
message?: string
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
await sdkReportProgress(phase, current, total, message);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Report an error to moss during hook execution
|
|
54
|
+
*/
|
|
55
|
+
export async function reportError(
|
|
56
|
+
error: string,
|
|
57
|
+
context?: string,
|
|
58
|
+
fatal = false
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
await sdkReportError(error, context, fatal);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Toast Utilities
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Show a toast notification in the main moss UI
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* // Success toast with clickable action
|
|
73
|
+
* await showToast({
|
|
74
|
+
* message: "Deployed!",
|
|
75
|
+
* variant: "success",
|
|
76
|
+
* actions: [{ label: "View site", url: "https://..." }],
|
|
77
|
+
* duration: 8000
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export async function showToast(options: ToastOptions | string): Promise<void> {
|
|
82
|
+
await sdkShowToast(options);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Dismiss a toast by ID
|
|
87
|
+
*/
|
|
88
|
+
export async function dismissToast(id: string): Promise<void> {
|
|
89
|
+
await sdkDismissToast(id);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Close the action panel
|
|
94
|
+
*/
|
|
95
|
+
export async function closeBrowser(): Promise<void> {
|
|
96
|
+
await sdkCloseBrowser();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Async Utilities
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Sleep for specified milliseconds
|
|
105
|
+
*/
|
|
106
|
+
export function sleep(ms: number): Promise<void> {
|
|
107
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
108
|
+
}
|
package/src/workflow.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Actions workflow template and creation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { writeFile, fileExists } from "@symbiosis-lab/moss-api";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GitHub Actions workflow template for deploying to GitHub Pages
|
|
9
|
+
* This is extracted from the original deploy.rs
|
|
10
|
+
*/
|
|
11
|
+
export const WORKFLOW_TEMPLATE = `# moss GitHub Pages Deployment
|
|
12
|
+
# This workflow deploys your pre-built site from .moss/build/current/ to GitHub Pages
|
|
13
|
+
# Generated by moss - https://mosspub.com
|
|
14
|
+
|
|
15
|
+
name: Deploy to GitHub Pages
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
push:
|
|
19
|
+
branches: [BRANCH_PLACEHOLDER]
|
|
20
|
+
workflow_dispatch:
|
|
21
|
+
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
pages: write
|
|
25
|
+
id-token: write
|
|
26
|
+
|
|
27
|
+
concurrency:
|
|
28
|
+
group: "pages"
|
|
29
|
+
cancel-in-progress: false
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
deploy:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
environment:
|
|
35
|
+
name: github-pages
|
|
36
|
+
url: \${{ steps.deployment.outputs.page_url }}
|
|
37
|
+
steps:
|
|
38
|
+
- name: Checkout
|
|
39
|
+
uses: actions/checkout@v4
|
|
40
|
+
|
|
41
|
+
- name: Setup Pages
|
|
42
|
+
uses: actions/configure-pages@v4
|
|
43
|
+
|
|
44
|
+
- name: Upload artifact
|
|
45
|
+
uses: actions/upload-pages-artifact@v3
|
|
46
|
+
with:
|
|
47
|
+
path: .moss/build/current
|
|
48
|
+
|
|
49
|
+
- name: Deploy to GitHub Pages
|
|
50
|
+
id: deployment
|
|
51
|
+
uses: actions/deploy-pages@v4
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate the workflow content with the correct branch
|
|
56
|
+
*/
|
|
57
|
+
export function generateWorkflowContent(branch: string): string {
|
|
58
|
+
return WORKFLOW_TEMPLATE.replace("BRANCH_PLACEHOLDER", branch);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create the GitHub Actions workflow file
|
|
63
|
+
*/
|
|
64
|
+
export async function createWorkflowFile(branch: string): Promise<void> {
|
|
65
|
+
console.log(" Creating .github/workflows/moss-deploy.yml...");
|
|
66
|
+
|
|
67
|
+
const content = generateWorkflowContent(branch);
|
|
68
|
+
|
|
69
|
+
await writeFile(".github/workflows/moss-deploy.yml", content);
|
|
70
|
+
|
|
71
|
+
console.log(" Workflow file created");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if the workflow file already exists
|
|
76
|
+
*/
|
|
77
|
+
export async function workflowExists(): Promise<boolean> {
|
|
78
|
+
return fileExists(".github/workflows/moss-deploy.yml");
|
|
79
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock GitHub API helpers for integration testing
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities to mock GitHub OAuth Device Flow responses
|
|
5
|
+
* for testing authentication without hitting real APIs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DeviceCodeResponse, TokenResponse, GitHubUser } from "../src/types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Configuration for mocking GitHub API responses
|
|
12
|
+
*/
|
|
13
|
+
export interface MockGitHubConfig {
|
|
14
|
+
/** Response for POST /login/device/code */
|
|
15
|
+
deviceCodeResponse?: DeviceCodeResponse;
|
|
16
|
+
/** Response(s) for POST /login/oauth/access_token - can be a single response or array for sequence */
|
|
17
|
+
tokenResponse?: TokenResponse | TokenResponse[];
|
|
18
|
+
/** Response for GET /user (token validation) */
|
|
19
|
+
userResponse?: GitHubUser | null;
|
|
20
|
+
/** OAuth scopes to return in X-OAuth-Scopes header */
|
|
21
|
+
scopes?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default mock responses for successful auth flow
|
|
26
|
+
*/
|
|
27
|
+
export const defaultDeviceCodeResponse: DeviceCodeResponse = {
|
|
28
|
+
device_code: "test-device-code-123",
|
|
29
|
+
user_code: "ABCD-1234",
|
|
30
|
+
verification_uri: "https://github.com/login/device",
|
|
31
|
+
verification_uri_complete: "https://github.com/login/device?user_code=ABCD-1234",
|
|
32
|
+
expires_in: 900,
|
|
33
|
+
interval: 5,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const defaultTokenResponse: TokenResponse = {
|
|
37
|
+
access_token: "gho_test_token_abc123",
|
|
38
|
+
token_type: "bearer",
|
|
39
|
+
scope: "repo,workflow",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const defaultUserResponse: GitHubUser = {
|
|
43
|
+
login: "testuser",
|
|
44
|
+
id: 12345,
|
|
45
|
+
name: "Test User",
|
|
46
|
+
email: "test@example.com",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Error responses for various failure scenarios
|
|
51
|
+
*/
|
|
52
|
+
export const authorizationPendingResponse: TokenResponse = {
|
|
53
|
+
error: "authorization_pending",
|
|
54
|
+
error_description: "The authorization request is still pending.",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const expiredTokenResponse: TokenResponse = {
|
|
58
|
+
error: "expired_token",
|
|
59
|
+
error_description: "The device_code has expired.",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const accessDeniedResponse: TokenResponse = {
|
|
63
|
+
error: "access_denied",
|
|
64
|
+
error_description: "The user denied authorization.",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const slowDownResponse: TokenResponse = {
|
|
68
|
+
error: "slow_down",
|
|
69
|
+
error_description: "Too many requests. Please slow down.",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a mock Response object
|
|
74
|
+
*/
|
|
75
|
+
function createMockResponse(body: unknown, status = 200, headers: Record<string, string> = {}): Response {
|
|
76
|
+
return {
|
|
77
|
+
ok: status >= 200 && status < 300,
|
|
78
|
+
status,
|
|
79
|
+
statusText: status === 200 ? "OK" : "Error",
|
|
80
|
+
headers: new Headers(headers),
|
|
81
|
+
json: async () => body,
|
|
82
|
+
text: async () => JSON.stringify(body),
|
|
83
|
+
} as Response;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a mock fetch function for testing
|
|
88
|
+
*/
|
|
89
|
+
export function createMockFetch(config: MockGitHubConfig = {}) {
|
|
90
|
+
let tokenResponseIndex = 0;
|
|
91
|
+
|
|
92
|
+
return async (url: string | URL | Request, _init?: RequestInit): Promise<Response> => {
|
|
93
|
+
const urlString = typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url;
|
|
94
|
+
|
|
95
|
+
// Device code endpoint
|
|
96
|
+
if (urlString.includes("/login/device/code")) {
|
|
97
|
+
const response = config.deviceCodeResponse || defaultDeviceCodeResponse;
|
|
98
|
+
return createMockResponse(response);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Token endpoint
|
|
102
|
+
if (urlString.includes("/login/oauth/access_token")) {
|
|
103
|
+
let response: TokenResponse;
|
|
104
|
+
|
|
105
|
+
if (Array.isArray(config.tokenResponse)) {
|
|
106
|
+
// Return responses in sequence
|
|
107
|
+
response = config.tokenResponse[tokenResponseIndex] || config.tokenResponse[config.tokenResponse.length - 1];
|
|
108
|
+
tokenResponseIndex++;
|
|
109
|
+
} else {
|
|
110
|
+
response = config.tokenResponse || defaultTokenResponse;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return createMockResponse(response);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// User endpoint (token validation)
|
|
117
|
+
if (urlString.includes("/user")) {
|
|
118
|
+
if (config.userResponse === null) {
|
|
119
|
+
return createMockResponse({ message: "Bad credentials" }, 401);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const user = config.userResponse || defaultUserResponse;
|
|
123
|
+
const scopes = config.scopes || ["repo", "workflow"];
|
|
124
|
+
|
|
125
|
+
return createMockResponse(user, 200, {
|
|
126
|
+
"X-OAuth-Scopes": scopes.join(", "),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Unknown endpoint
|
|
131
|
+
throw new Error(`Unmocked URL: ${urlString}`);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Mock browser functions for testing
|
|
137
|
+
*/
|
|
138
|
+
export function createMockBrowser() {
|
|
139
|
+
let isOpen = false;
|
|
140
|
+
let currentUrl = "";
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
openBrowser: async (url: string) => {
|
|
144
|
+
isOpen = true;
|
|
145
|
+
currentUrl = url;
|
|
146
|
+
},
|
|
147
|
+
closeBrowser: async () => {
|
|
148
|
+
isOpen = false;
|
|
149
|
+
currentUrl = "";
|
|
150
|
+
},
|
|
151
|
+
isOpen: () => isOpen,
|
|
152
|
+
getCurrentUrl: () => currentUrl,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Mock git credential helper for testing
|
|
158
|
+
*/
|
|
159
|
+
export function createMockCredentialHelper() {
|
|
160
|
+
const store: Map<string, { username: string; password: string }> = new Map();
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
store: (host: string, username: string, password: string) => {
|
|
164
|
+
store.set(host, { username, password });
|
|
165
|
+
},
|
|
166
|
+
get: (host: string) => store.get(host),
|
|
167
|
+
clear: (host: string) => store.delete(host),
|
|
168
|
+
hasCredentials: (host: string) => store.has(host),
|
|
169
|
+
reset: () => store.clear(),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Setup GitHub API mocks using MockTauriContext.urlConfig
|
|
175
|
+
*
|
|
176
|
+
* This sets up URL responses for the GitHub OAuth Device Flow endpoints
|
|
177
|
+
* to work with moss-api's httpPost function which uses Tauri IPC.
|
|
178
|
+
*
|
|
179
|
+
* @param ctx - MockTauriContext from setupMockTauri()
|
|
180
|
+
* @param config - Mock response configuration
|
|
181
|
+
*/
|
|
182
|
+
export function setupGitHubApiMocks(
|
|
183
|
+
ctx: { urlConfig: { setResponse: (url: string, response: Record<string, unknown>) => void } },
|
|
184
|
+
config: MockGitHubConfig = {}
|
|
185
|
+
): void {
|
|
186
|
+
// Device code endpoint
|
|
187
|
+
const deviceCodeResponse = config.deviceCodeResponse || defaultDeviceCodeResponse;
|
|
188
|
+
ctx.urlConfig.setResponse("https://github.com/login/device/code", {
|
|
189
|
+
status: 200,
|
|
190
|
+
ok: true,
|
|
191
|
+
contentType: "application/json",
|
|
192
|
+
bodyBase64: btoa(JSON.stringify(deviceCodeResponse)),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Token endpoint - handle single response or first in sequence
|
|
196
|
+
const tokenResponse = Array.isArray(config.tokenResponse)
|
|
197
|
+
? config.tokenResponse[0]
|
|
198
|
+
: config.tokenResponse || defaultTokenResponse;
|
|
199
|
+
ctx.urlConfig.setResponse("https://github.com/login/oauth/access_token", {
|
|
200
|
+
status: 200,
|
|
201
|
+
ok: true,
|
|
202
|
+
contentType: "application/json",
|
|
203
|
+
bodyBase64: btoa(JSON.stringify(tokenResponse)),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// User endpoint (for token validation - GET request, uses fetch_url)
|
|
207
|
+
const userResponse = config.userResponse !== null
|
|
208
|
+
? config.userResponse || defaultUserResponse
|
|
209
|
+
: { message: "Bad credentials" };
|
|
210
|
+
const userStatus = config.userResponse === null ? 401 : 200;
|
|
211
|
+
ctx.urlConfig.setResponse("https://api.github.com/user", {
|
|
212
|
+
status: userStatus,
|
|
213
|
+
ok: userStatus === 200,
|
|
214
|
+
contentType: "application/json",
|
|
215
|
+
bodyBase64: btoa(JSON.stringify(userResponse)),
|
|
216
|
+
});
|
|
217
|
+
}
|