Skip to content

Commit 96cc8de

Browse files
Merge main into feature/web-search
2 parents 5f5f0f0 + 5c8b795 commit 96cc8de

File tree

8 files changed

+505
-1
lines changed

8 files changed

+505
-1
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# ATX Transform Server Integration Tests
2+
3+
Integration tests for the ATX .NET Transform Language Server.
4+
5+
## Prerequisites
6+
7+
- Node.js 18+
8+
- Built LSP binary (`token-standalone.js`)
9+
- Valid SSO token with ATX access
10+
11+
## Environment Variables
12+
13+
| Variable | Description |
14+
|----------|-------------|
15+
| `TEST_SSO_TOKEN` | SSO access token |
16+
| `TEST_RUNTIME_FILE` | Path to LSP binary |
17+
| `TEST_SSO_START_URL` | SSO start URL (e.g., `https://d-90663aa166.awsapps.com/start`) |
18+
19+
## Setup
20+
21+
```bash
22+
# Install dependencies
23+
npm install
24+
25+
# Clone test fixture
26+
mkdir -p out/tests/testFixture
27+
git clone https://github.com/aws-samples/bobs-used-bookstore-classic.git out/tests/testFixture/bobs-used-bookstore-classic
28+
```
29+
30+
## Run Tests
31+
32+
```bash
33+
npm run test-integ
34+
```
35+
36+
## Tests
37+
38+
| Test | Command | Description |
39+
|------|---------|-------------|
40+
| TEST 1 | ListOrCreateWorkspace | Creates/retrieves ATX workspace |
41+
| TEST 2 | StartTransform | Starts transform job |
42+
| TEST 3 | GetTransform | Polls until AWAITING_HUMAN_INPUT |
43+
| TEST 4 | UploadPlan | Uploads plan and polls until complete |
44+
| TEST 5 | StopJob | Starts and stops a transform job |
45+
46+
## Timeouts
47+
48+
- TEST 3: 1 hour (reaching AWAITING_HUMAN_INPUT)
49+
- TEST 4: 3 hours (full transform completion)
50+
- TEST 5: 1 minute
51+
52+
## Notes
53+
54+
- Tests 2, 3, 4 share a single transform job
55+
- Test 5 creates a separate job for stop validation
56+
- Uses Bobs Used Bookstore Classic as the test fixture
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "@aws/atx-transform-server-integration-tests",
3+
"version": "0.0.1",
4+
"description": "Integration tests for ATX .NET Transform Language Server",
5+
"scripts": {
6+
"compile": "tsc --build",
7+
"test-integ": "npm run compile && mocha --timeout 7200000 \"./out/**/*.test.js\""
8+
},
9+
"devDependencies": {
10+
"@types/chai": "^4.3.5",
11+
"@types/mocha": "^10.0.9",
12+
"chai": "^4.3.7",
13+
"mocha": "^11.0.1",
14+
"typescript": "^5.0.0"
15+
}
16+
}
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import { expect } from 'chai'
2+
import * as path from 'path'
3+
import * as fs from 'fs'
4+
import { LspClient } from './lspClient'
5+
import { execSync } from 'child_process'
6+
7+
function getSourceFiles(
8+
dir: string,
9+
extensions: string[] = ['.cs', '.csproj', '.sln', '.config', '.json', '.cshtml', '.razor']
10+
): string[] {
11+
const files: string[] = []
12+
const excluded = ['.git', 'bin', 'obj', 'node_modules', '.vs', '.idea']
13+
14+
function walk(currentDir: string) {
15+
const entries = fs.readdirSync(currentDir, { withFileTypes: true })
16+
for (const entry of entries) {
17+
const fullPath = path.join(currentDir, entry.name)
18+
if (entry.isDirectory()) {
19+
if (!excluded.includes(entry.name)) walk(fullPath)
20+
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
21+
files.push(fullPath)
22+
}
23+
}
24+
}
25+
walk(dir)
26+
return files
27+
}
28+
29+
function sleep(ms: number): Promise<void> {
30+
return new Promise(resolve => setTimeout(resolve, ms))
31+
}
32+
33+
function refreshTokenFromSecretsManager(): string {
34+
const result = execSync(
35+
'aws secretsmanager get-secret-value --secret-id AtxSsoTokenSecret --query SecretString --output text',
36+
{ encoding: 'utf-8' }
37+
)
38+
const secret = JSON.parse(result)
39+
return secret.bearerToken.replace('Bearer ', '')
40+
}
41+
42+
describe('ATX .NET Transform Integration Tests', () => {
43+
let client: LspClient
44+
let workspaceId: string
45+
let transformationJobId: string
46+
let planPath: string | null = null
47+
let refreshInterval: NodeJS.Timeout
48+
49+
let testSsoToken = process.env.TEST_SSO_TOKEN || ''
50+
const runtimeFile = process.env.TEST_RUNTIME_FILE || ''
51+
const startUrl = process.env.TEST_SSO_START_URL || ''
52+
const testFixturePath = path.resolve(__dirname, 'testFixture', 'bobs-used-bookstore-classic')
53+
const solutionFilePath = path.join(testFixturePath, 'BobsBookstoreClassic.sln')
54+
const webProjectPath = path.join(testFixturePath, 'app', 'Bookstore.Web', 'Bookstore.Web.csproj')
55+
const commonProjectPath = path.join(testFixturePath, 'app', 'Bookstore.Common', 'Bookstore.Common.csproj')
56+
const dataProjectPath = path.join(testFixturePath, 'app', 'Bookstore.Data', 'Bookstore.Data.csproj')
57+
const domainProjectPath = path.join(testFixturePath, 'app', 'Bookstore.Domain', 'Bookstore.Domain.csproj')
58+
59+
const TOKEN_REFRESH_INTERVAL_MS = 25 * 60 * 1000
60+
61+
async function refreshToken(): Promise<void> {
62+
console.log('[Token Refresh] Refreshing SSO token...')
63+
testSsoToken = refreshTokenFromSecretsManager()
64+
await client.sendRequest('aws/credentials/token/update', {
65+
data: { token: testSsoToken },
66+
credentialkey: 'atx-bearer',
67+
metadata: { sso: { startUrl } },
68+
})
69+
console.log('[Token Refresh] Token updated successfully')
70+
}
71+
72+
function buildStartTransformRequest(jobName: string, sourceFiles: string[]) {
73+
return {
74+
command: 'aws/atxTransform/startTransform',
75+
WorkspaceId: workspaceId,
76+
JobName: jobName,
77+
StartTransformRequest: {
78+
SolutionRootPath: testFixturePath,
79+
SolutionFilePath: solutionFilePath,
80+
SelectedProjectPath: webProjectPath,
81+
ProgramLanguage: 'csharp',
82+
TargetFramework: 'net8.0',
83+
SolutionConfigPaths: [],
84+
ProjectMetadata: [
85+
{
86+
Name: 'Bookstore.Web',
87+
ProjectPath: webProjectPath,
88+
ProjectTargetFramework: 'net48',
89+
ProjectLanguage: 'csharp',
90+
ProjectType: 'Web',
91+
SourceCodeFilePaths: sourceFiles,
92+
ExternalReferences: [],
93+
},
94+
{
95+
Name: 'Bookstore.Common',
96+
ProjectPath: commonProjectPath,
97+
ProjectTargetFramework: 'net48',
98+
ProjectLanguage: 'csharp',
99+
ProjectType: 'Library',
100+
SourceCodeFilePaths: sourceFiles,
101+
ExternalReferences: [],
102+
},
103+
{
104+
Name: 'Bookstore.Data',
105+
ProjectPath: dataProjectPath,
106+
ProjectTargetFramework: 'net48',
107+
ProjectLanguage: 'csharp',
108+
ProjectType: 'Library',
109+
SourceCodeFilePaths: sourceFiles,
110+
ExternalReferences: [],
111+
},
112+
{
113+
Name: 'Bookstore.Domain',
114+
ProjectPath: domainProjectPath,
115+
ProjectTargetFramework: 'net48',
116+
ProjectLanguage: 'csharp',
117+
ProjectType: 'Library',
118+
SourceCodeFilePaths: sourceFiles,
119+
ExternalReferences: [],
120+
},
121+
],
122+
TransformNetStandardProjects: false,
123+
EnableRazorViewTransform: true,
124+
EnableWebFormsTransform: false,
125+
},
126+
}
127+
}
128+
129+
before(async () => {
130+
if (!testSsoToken) throw new Error('TEST_SSO_TOKEN not set')
131+
if (!runtimeFile) throw new Error('TEST_RUNTIME_FILE not set')
132+
if (!startUrl) throw new Error('TEST_SSO_START_URL not set')
133+
134+
client = new LspClient(runtimeFile)
135+
await client.initialize()
136+
await sleep(2000)
137+
138+
client.sendNotification('initialized', {})
139+
await sleep(1000)
140+
141+
await client.sendRequest('aws/credentials/token/update', {
142+
data: { token: testSsoToken },
143+
credentialkey: 'atx-bearer',
144+
metadata: { sso: { startUrl } },
145+
})
146+
await sleep(5000)
147+
148+
const profiles = await client.sendRequest('aws/getConfigurationFromServer', {
149+
section: 'aws.transformProfiles',
150+
})
151+
await sleep(5000)
152+
153+
const iadProfile = profiles?.find((p: any) => p.identityDetails?.region === 'us-east-1')
154+
if (!iadProfile) throw new Error('No us-east-1 profile found')
155+
console.log('Found IAD profile:', iadProfile.arn)
156+
157+
await client.sendRequest('aws/updateConfiguration', {
158+
section: 'aws.atx',
159+
settings: { profileArn: iadProfile.arn, applicationUrl: iadProfile.applicationUrl },
160+
})
161+
await sleep(3000)
162+
refreshInterval = setInterval(() => refreshToken(), TOKEN_REFRESH_INTERVAL_MS)
163+
})
164+
165+
after(() => {
166+
if (refreshInterval) clearInterval(refreshInterval)
167+
if (client) client.close()
168+
})
169+
170+
it('TEST 1: should list or create workspace', async () => {
171+
const result = await client.sendRequest('workspace/executeCommand', {
172+
command: 'aws/atxTransform/listOrCreateWorkspace',
173+
arguments: [],
174+
})
175+
176+
workspaceId = result?.CreatedWorkspace?.Id || result?.AvailableWorkspaces?.[0]?.Id
177+
expect(workspaceId).to.exist
178+
console.log('WorkspaceId:', workspaceId)
179+
})
180+
181+
it('TEST 2: should start transform job', async () => {
182+
const sourceFiles = getSourceFiles(testFixturePath)
183+
console.log(`Found ${sourceFiles.length} source files`)
184+
185+
const result = await client.sendRequest(
186+
'workspace/executeCommand',
187+
buildStartTransformRequest('IntegTest-BobsBookstore-' + Date.now(), sourceFiles)
188+
)
189+
190+
transformationJobId = result?.TransformationJobId
191+
expect(transformationJobId).to.exist
192+
console.log('TransformationJobId:', transformationJobId)
193+
})
194+
195+
it('TEST 3: should poll transform until AWAITING_HUMAN_INPUT', async function (this: Mocha.Context) {
196+
this.timeout(3600000)
197+
const maxPolls = 360
198+
let jobStatus = ''
199+
200+
for (let i = 0; i < maxPolls; i++) {
201+
const result = await client.sendRequest('workspace/executeCommand', {
202+
command: 'aws/atxTransform/getTransformInfo',
203+
TransformationJobId: transformationJobId,
204+
WorkspaceId: workspaceId,
205+
SolutionRootPath: testFixturePath,
206+
})
207+
208+
const job = result?.TransformationJob || {}
209+
jobStatus = job.Status || ''
210+
planPath = result?.PlanPath || null
211+
console.log(`Poll ${i + 1}: Status = ${jobStatus}`)
212+
213+
if (jobStatus === 'FAILED') {
214+
console.log('FAILED - Reason:', job.FailureReason || result?.ErrorString || 'unknown')
215+
}
216+
217+
if (jobStatus === 'AWAITING_HUMAN_INPUT' && planPath) break
218+
if (['COMPLETED', 'FAILED', 'STOPPED'].includes(jobStatus)) break
219+
220+
await sleep(10000)
221+
}
222+
223+
expect(jobStatus).to.equal('AWAITING_HUMAN_INPUT')
224+
expect(planPath).to.exist
225+
})
226+
227+
it('TEST 4: should upload plan and complete transform', async function (this: Mocha.Context) {
228+
this.timeout(10800000)
229+
if (!planPath) {
230+
this.skip()
231+
return
232+
}
233+
234+
const uploadResult = await client.sendRequest('workspace/executeCommand', {
235+
command: 'aws/atxTransform/uploadPlan',
236+
TransformationJobId: transformationJobId,
237+
WorkspaceId: workspaceId,
238+
PlanPath: planPath,
239+
})
240+
console.log('UploadPlan result:', uploadResult?.VerificationStatus)
241+
242+
const maxPolls = 1080
243+
let jobStatus = ''
244+
245+
for (let i = 0; i < maxPolls; i++) {
246+
const result = await client.sendRequest('workspace/executeCommand', {
247+
command: 'aws/atxTransform/getTransformInfo',
248+
TransformationJobId: transformationJobId,
249+
WorkspaceId: workspaceId,
250+
SolutionRootPath: testFixturePath,
251+
})
252+
253+
jobStatus = result?.TransformationJob?.Status || ''
254+
console.log(`Poll ${i + 1}: Status = ${jobStatus}`)
255+
256+
if (['COMPLETED', 'FAILED', 'STOPPED', 'PARTIALLY_COMPLETED'].includes(jobStatus)) break
257+
258+
await sleep(10000)
259+
}
260+
261+
expect(['COMPLETED', 'PARTIALLY_COMPLETED']).to.include(jobStatus)
262+
})
263+
264+
it('TEST 5: should stop a transform job', async function (this: Mocha.Context) {
265+
this.timeout(60000)
266+
const sourceFiles = getSourceFiles(testFixturePath)
267+
268+
const startResult = await client.sendRequest(
269+
'workspace/executeCommand',
270+
buildStartTransformRequest('IntegTest-ToStop-' + Date.now(), sourceFiles)
271+
)
272+
273+
const jobToStop = startResult?.TransformationJobId
274+
expect(jobToStop).to.exist
275+
console.log('Created job to stop:', jobToStop)
276+
277+
await sleep(5000)
278+
279+
const stopResult = await client.sendRequest('workspace/executeCommand', {
280+
command: 'aws/atxTransform/stopJob',
281+
WorkspaceId: workspaceId,
282+
JobId: jobToStop,
283+
})
284+
285+
console.log('StopJob result:', stopResult?.Status)
286+
expect(stopResult).to.exist
287+
})
288+
})

0 commit comments

Comments
 (0)