Skip to content

Commit 76759b3

Browse files
committed
copy-test-files.js
1 parent ddcdeb7 commit 76759b3

File tree

3 files changed

+391
-30
lines changed

3 files changed

+391
-30
lines changed

epicshop/copy-test-files.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#!/usr/bin/env node
2+
3+
import { existsSync } from 'fs'
4+
import { readdir, stat, copyFile, mkdir } from 'fs/promises'
5+
import { join, dirname } from 'path'
6+
7+
const EXERCISES_DIR = join(process.cwd(), '..', 'exercises')
8+
9+
/**
10+
* Copy test files from solution directories to their corresponding problem directories
11+
*/
12+
async function copyTestFiles() {
13+
console.log('🔍 Scanning exercises directory...')
14+
15+
try {
16+
const exerciseDirs = await readdir(EXERCISES_DIR)
17+
const exerciseNumbers = exerciseDirs
18+
.filter((dir) => /^\d+\./.test(dir))
19+
.sort()
20+
21+
console.log(
22+
`📁 Found ${exerciseNumbers.length} exercise directories: ${exerciseNumbers.join(', ')}`,
23+
)
24+
25+
let totalCopied = 0
26+
let totalSkipped = 0
27+
28+
for (const exerciseDir of exerciseNumbers) {
29+
const exercisePath = join(EXERCISES_DIR, exerciseDir)
30+
const exerciseStats = await stat(exercisePath)
31+
32+
if (!exerciseStats.isDirectory()) continue
33+
34+
console.log(`\n📂 Processing ${exerciseDir}...`)
35+
36+
const exerciseContents = await readdir(exercisePath)
37+
const solutionDirs = exerciseContents.filter((item) =>
38+
item.includes('.solution.'),
39+
)
40+
41+
for (const solutionDir of solutionDirs) {
42+
const problemDir = solutionDir.replace('.solution.', '.problem.')
43+
44+
const solutionPath = join(exercisePath, solutionDir)
45+
const problemPath = join(exercisePath, problemDir)
46+
47+
// Check if both directories exist
48+
if (!existsSync(solutionPath) || !existsSync(problemPath)) {
49+
console.log(
50+
` ⚠️ Skipping ${solutionDir} - corresponding problem directory not found`,
51+
)
52+
continue
53+
}
54+
55+
const solutionTestPath = join(solutionPath, 'test')
56+
const problemTestPath = join(problemPath, 'test')
57+
58+
// Check if solution test directory exists
59+
if (!existsSync(solutionTestPath)) {
60+
console.log(
61+
` ⚠️ Skipping ${solutionDir} - no test directory in solution`,
62+
)
63+
continue
64+
}
65+
66+
// Ensure problem test directory exists
67+
if (!existsSync(problemTestPath)) {
68+
await mkdir(problemTestPath, { recursive: true })
69+
console.log(` 📁 Created test directory for ${problemDir}`)
70+
}
71+
72+
// Copy test files
73+
const testFiles = await readdir(solutionTestPath)
74+
let copiedInThisDir = 0
75+
76+
for (const testFile of testFiles) {
77+
const sourceFile = join(solutionTestPath, testFile)
78+
const destFile = join(problemTestPath, testFile)
79+
80+
try {
81+
await copyFile(sourceFile, destFile)
82+
console.log(
83+
` ✅ Copied ${testFile} from ${solutionDir} to ${problemDir}`,
84+
)
85+
copiedInThisDir++
86+
} catch (error) {
87+
console.log(` ❌ Failed to copy ${testFile}: ${error.message}`)
88+
}
89+
}
90+
91+
if (copiedInThisDir > 0) {
92+
totalCopied += copiedInThisDir
93+
console.log(` 📊 Copied ${copiedInThisDir} files for ${problemDir}`)
94+
} else {
95+
totalSkipped++
96+
}
97+
}
98+
}
99+
100+
console.log(`\n🎉 Copy operation completed!`)
101+
console.log(`📊 Total files copied: ${totalCopied}`)
102+
console.log(`⚠️ Directories skipped: ${totalSkipped}`)
103+
} catch (error) {
104+
console.error('❌ Error:', error.message)
105+
process.exit(1)
106+
}
107+
}
108+
109+
// Run the script
110+
if (import.meta.url === `file://${process.argv[1]}`) {
111+
copyTestFiles()
112+
}
113+
114+
export { copyTestFiles }

exercises/05.scopes/01.problem.check-scopes/test/index.test.ts

Lines changed: 256 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,261 @@ import { test, expect, inject } from 'vitest'
22
import { z } from 'zod'
33

44
const mcpServerPort = inject('mcpServerPort')
5-
const mcpServerUrl = `http://localhost:${mcpServerPort}`
5+
const EPIC_ME_AUTH_SERVER_URL = 'http://localhost:7788'
66

7-
test(`TODO: update this test title to describe the important thing we're working on in this exercise step`, async () => {
8-
// TODO: implement this test
7+
// TypeScript interfaces for API responses
8+
interface AuthServerConfig {
9+
authorization_endpoint: string
10+
token_endpoint: string
11+
[key: string]: unknown
12+
}
13+
14+
interface ClientRegistration {
15+
client_id: string
16+
client_secret?: string
17+
[key: string]: unknown
18+
}
19+
20+
interface AuthResult {
21+
redirectTo: string
22+
[key: string]: unknown
23+
}
24+
25+
interface TokenResult {
26+
access_token: string
27+
token_type: string
28+
[key: string]: unknown
29+
}
30+
31+
// Helper function to generate PKCE challenge
32+
function generateCodeChallenge() {
33+
const codeVerifier = btoa(
34+
String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32))),
35+
)
36+
.replace(/\+/g, '-')
37+
.replace(/\//g, '_')
38+
.replace(/=/g, '')
39+
40+
return {
41+
codeVerifier,
42+
codeChallenge: codeVerifier, // For simplicity, using plain method
43+
codeChallengeMethod: 'plain',
44+
}
45+
}
46+
47+
test(`{test title that describes the important thing we're working on in this exercise step}`, async () => {
48+
const mcpServerUrl = `http://localhost:${mcpServerPort}`
49+
50+
const unauthorizedResponse = await fetch(`${mcpServerUrl}/mcp`, {
51+
method: 'POST',
52+
headers: {
53+
'content-type': 'application/json',
54+
accept: 'application/json, text/event-stream',
55+
},
56+
body: JSON.stringify({
57+
jsonrpc: '2.0',
58+
id: 1,
59+
method: 'tools/list',
60+
}),
61+
})
62+
63+
expect(
64+
unauthorizedResponse.status,
65+
'🚨 Expected 401 status for unauthorized request',
66+
).toBe(401)
67+
68+
const wwwAuthHeader = unauthorizedResponse.headers.get('WWW-Authenticate')
69+
expect(
70+
wwwAuthHeader,
71+
'🚨 WWW-Authenticate header should be present',
72+
).toBeTruthy()
73+
expect(
74+
wwwAuthHeader,
75+
'🚨 WWW-Authenticate header should contain Bearer realm',
76+
).toContain('Bearer realm="EpicMe"')
77+
78+
// Extract the resource_metadata url from the WWW-Authenticate header
79+
const resourceMetadataUrl = wwwAuthHeader
80+
?.split(',')
81+
.find((h) => h.includes('resource_metadata='))
82+
?.split('=')[1]
83+
84+
expect(
85+
resourceMetadataUrl,
86+
'🚨 Resource metadata URL should be present in WWW-Authenticate header',
87+
).toBeTruthy()
88+
89+
const resourceMetadataResponse = await fetch(resourceMetadataUrl!)
90+
expect(
91+
resourceMetadataResponse.ok,
92+
'🚨 fetching resource metadata should succeed',
93+
).toBe(true)
94+
95+
const resourceMetadataResult = z
96+
.object({
97+
resource: z.string(),
98+
authorization_servers: z.array(z.string()).length(1),
99+
scopes_supported: z.array(z.string()),
100+
})
101+
.safeParse(await resourceMetadataResponse.json())
102+
if (!resourceMetadataResult.success) {
103+
throw new Error(
104+
'🚨 Invalid resource metadata: ' + resourceMetadataResult.error.message,
105+
)
106+
}
107+
const resourceMetadata = resourceMetadataResult.data
108+
109+
const authorizationUrl = resourceMetadata.authorization_servers[0]!
110+
111+
// Step 1: Metadata discovery
112+
// Test OAuth Authorization Server discovery
113+
const authServerDiscoveryResponse = await fetch(
114+
`${authorizationUrl}/.well-known/oauth-authorization-server`,
115+
)
116+
expect(
117+
authServerDiscoveryResponse.ok,
118+
'🚨 OAuth authorization server discovery should succeed',
119+
).toBe(true)
120+
121+
const authServerConfig =
122+
(await authServerDiscoveryResponse.json()) as AuthServerConfig
123+
expect(
124+
authServerConfig.authorization_endpoint,
125+
'🚨 Authorization endpoint should be present in discovery',
126+
).toBeTruthy()
127+
expect(
128+
authServerConfig.token_endpoint,
129+
'🚨 Token endpoint should be present in discovery',
130+
).toBeTruthy()
131+
132+
// Step 2: Dynamic client registration
133+
const clientRegistrationResponse = await fetch(
134+
`${EPIC_ME_AUTH_SERVER_URL}/oauth/register`,
135+
{
136+
method: 'POST',
137+
headers: {
138+
'content-type': 'application/json',
139+
accept: 'application/json, text/event-stream',
140+
},
141+
body: JSON.stringify({
142+
client_name: 'Test MCP Client',
143+
redirect_uris: [`${mcpServerUrl}/mcp`],
144+
scope: 'read write',
145+
}),
146+
},
147+
)
148+
149+
expect(
150+
clientRegistrationResponse.ok,
151+
'🚨 Client registration should succeed',
152+
).toBe(true)
153+
const clientRegistration =
154+
(await clientRegistrationResponse.json()) as ClientRegistration
155+
expect(
156+
clientRegistration.client_id,
157+
'🚨 Client ID should be returned from registration',
158+
).toBeTruthy()
159+
160+
// Step 3: Preparing Authorization (getting the auth URL)
161+
const { codeVerifier, codeChallenge, codeChallengeMethod } =
162+
generateCodeChallenge()
163+
const state = crypto.randomUUID()
164+
const redirectUri = `${mcpServerUrl}/mcp`
165+
166+
// Step 4: Requesting the auth code programmatically
167+
const testAuthUrl = new URL(`${EPIC_ME_AUTH_SERVER_URL}/test-auth`)
168+
// Use the registered client ID instead of the one from the auth URL
169+
testAuthUrl.searchParams.set('client_id', clientRegistration.client_id)
170+
testAuthUrl.searchParams.set('redirect_uri', redirectUri)
171+
testAuthUrl.searchParams.set('response_type', 'code')
172+
testAuthUrl.searchParams.set('code_challenge', codeChallenge)
173+
testAuthUrl.searchParams.set('code_challenge_method', codeChallengeMethod)
174+
testAuthUrl.searchParams.set('scope', 'read write')
175+
testAuthUrl.searchParams.set('state', state)
176+
177+
const authCodeResponse = await fetch(testAuthUrl.toString())
178+
expect(authCodeResponse.ok, '🚨 Auth code request should succeed').toBe(true)
179+
180+
const authResult = (await authCodeResponse.json()) as AuthResult
181+
expect(
182+
authResult.redirectTo,
183+
'🚨 Redirect URL should be returned',
184+
).toBeTruthy()
185+
186+
// Step 5: Supplying the auth code (extract from redirect URL)
187+
const redirectUrl = new URL(authResult.redirectTo)
188+
const authCode = redirectUrl.searchParams.get('code')
189+
const returnedState = redirectUrl.searchParams.get('state')
190+
191+
expect(
192+
authCode,
193+
'🚨 Auth code should be present in redirect URL',
194+
).toBeTruthy()
195+
expect(returnedState, '🚨 State should be returned').toBe(state)
196+
197+
// Step 6: Requesting the token
198+
const tokenParams = new URLSearchParams({
199+
grant_type: 'authorization_code',
200+
code: authCode!,
201+
redirect_uri: redirectUri,
202+
client_id: clientRegistration.client_id, // Use registered client ID
203+
code_verifier: codeVerifier,
204+
})
205+
206+
// Add client_secret if provided during registration
207+
if (clientRegistration.client_secret) {
208+
tokenParams.set('client_secret', clientRegistration.client_secret)
209+
}
210+
211+
const tokenResponse = await fetch(`${EPIC_ME_AUTH_SERVER_URL}/oauth/token`, {
212+
method: 'POST',
213+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
214+
body: tokenParams,
215+
})
216+
217+
if (!tokenResponse.ok) {
218+
const errorText = await tokenResponse.text()
219+
console.error('Token exchange failed:', tokenResponse.status, errorText)
220+
}
221+
222+
expect(tokenResponse.ok, '🚨 Token exchange should succeed').toBe(true)
223+
const tokenResult = (await tokenResponse.json()) as TokenResult
224+
expect(
225+
tokenResult.access_token,
226+
'🚨 Access token should be returned',
227+
).toBeTruthy()
228+
expect(
229+
tokenResult.token_type?.toLowerCase(),
230+
'🚨 Token type should be Bearer',
231+
).toBe('bearer')
232+
233+
// Step 7: Performing authenticated requests (listing tools)
234+
// Verify the token works by making a simple authenticated request to the MCP server
235+
// We'll test that we get past the authentication (no 401) even if we get protocol errors
236+
const authTestResponse = await fetch(`${mcpServerUrl}/mcp`, {
237+
method: 'POST',
238+
headers: {
239+
'Content-Type': 'application/json',
240+
Accept: 'application/json, text/event-stream',
241+
Authorization: `Bearer ${tokenResult.access_token}`,
242+
},
243+
body: JSON.stringify({
244+
jsonrpc: '2.0',
245+
id: 1,
246+
method: 'initialize',
247+
params: {
248+
protocolVersion: '2024-11-05',
249+
capabilities: {},
250+
clientInfo: {
251+
name: 'Test Client',
252+
version: '1.0.0',
253+
},
254+
},
255+
}),
256+
})
257+
258+
expect(
259+
authTestResponse.status,
260+
'🚨 Should not get 401 Unauthorized with valid token',
261+
).not.toBe(401)
9262
})

0 commit comments

Comments
 (0)