Skip to content

Commit 01488d5

Browse files
committed
test: add infrastructure smoke tests for SST-deployed components
1 parent ea27c34 commit 01488d5

2 files changed

Lines changed: 1087 additions & 39 deletions

File tree

apps/api/src/deploy.test.ts

Lines changed: 200 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, test, expect } from 'bun:test'
2-
import { readFileSync } from 'node:fs'
2+
import { readFileSync, existsSync } from 'node:fs'
33
import { resolve } from 'node:path'
44

55
const ROOT = resolve(import.meta.dirname, '..', '..', '..')
@@ -9,6 +9,10 @@ function readFile(path: string): string {
99
return readFileSync(resolve(ROOT, path), 'utf-8')
1010
}
1111

12+
// ---------------------------------------------------------------------------
13+
// Deploy workflow (SST)
14+
// ---------------------------------------------------------------------------
15+
1216
describe('deploy.yml', () => {
1317
const workflow = readFile('.github/workflows/deploy.yml')
1418

@@ -17,45 +21,39 @@ describe('deploy.yml', () => {
1721
expect(workflow).toContain('workflow_dispatch')
1822
})
1923

20-
test('has migrate job that runs before deploys', () => {
24+
test('has migrate job that runs before deploy', () => {
2125
expect(workflow).toContain('migrate:')
2226
expect(workflow).toContain('Run database migrations')
2327
expect(workflow).toContain('bun run db:migrate:run')
2428
expect(workflow).toContain('DATABASE_URL')
2529
})
2630

27-
test('has deploy-api job using Fly.io', () => {
28-
expect(workflow).toContain('deploy-api:')
31+
test('has deploy job using SST', () => {
32+
expect(workflow).toContain('deploy:')
2933
expect(workflow).toContain('needs: [migrate]')
30-
expect(workflow).toContain('superfly/flyctl-actions/setup-flyctl')
31-
expect(workflow).toContain('flyctl deploy')
32-
expect(workflow).toContain('FLY_API_TOKEN')
34+
expect(workflow).toContain('bunx sst deploy')
3335
})
3436

35-
test('deploy-api references correct Dockerfile and fly.toml paths', () => {
36-
expect(workflow).toContain('--config apps/api/fly.toml')
37-
expect(workflow).toContain('--dockerfile apps/api/Dockerfile')
37+
test('deploy job assumes AWS role via OIDC', () => {
38+
expect(workflow).toContain('aws-actions/configure-aws-credentials@v4')
39+
expect(workflow).toContain('role-to-assume')
40+
expect(workflow).toContain('AWS_ROLE_ARN')
3841
})
3942

40-
test('has deploy-web job using Vercel', () => {
41-
expect(workflow).toContain('deploy-web:')
42-
expect(workflow).toContain('vercel deploy --prod')
43-
expect(workflow).toContain('VERCEL_TOKEN')
44-
expect(workflow).toContain('VERCEL_ORG_ID')
45-
expect(workflow).toContain('VERCEL_PROJECT_ID')
43+
test('requests id-token permission for OIDC', () => {
44+
expect(workflow).toContain('id-token: write')
45+
expect(workflow).toContain('contents: read')
4646
})
4747

48-
test('both deploy jobs depend on migrate', () => {
49-
const apiNeeds = workflow.match(/deploy-api:[\s\S]*?needs:\s*\[migrate\]/)
50-
const webNeeds = workflow.match(/deploy-web:[\s\S]*?needs:\s*\[migrate\]/)
51-
expect(apiNeeds).not.toBeNull()
52-
expect(webNeeds).not.toBeNull()
48+
test('supports stage selection via workflow_dispatch', () => {
49+
expect(workflow).toContain('stage:')
50+
expect(workflow).toContain('SST stage to deploy')
51+
expect(workflow).toMatch(/options:\s*\n\s*- dev\s*\n\s*- staging\s*\n\s*- production/)
5352
})
5453

55-
test('all jobs use production environment', () => {
56-
const envMatches = workflow.match(/environment:\s*production/g)
57-
expect(envMatches).not.toBeNull()
58-
expect(envMatches!.length).toBeGreaterThanOrEqual(3)
54+
test('uses concurrency group to prevent parallel deploys', () => {
55+
expect(workflow).toContain('concurrency:')
56+
expect(workflow).toContain('cancel-in-progress: false')
5957
})
6058

6159
test('has no TODO placeholders', () => {
@@ -64,6 +62,106 @@ describe('deploy.yml', () => {
6462
})
6563
})
6664

65+
// ---------------------------------------------------------------------------
66+
// Docker build workflow
67+
// ---------------------------------------------------------------------------
68+
69+
describe('docker-build.yml', () => {
70+
const workflow = readFile('.github/workflows/docker-build.yml')
71+
72+
test('triggers on API and dependency path changes', () => {
73+
expect(workflow).toContain('apps/api/**')
74+
expect(workflow).toContain('packages/contract/**')
75+
expect(workflow).toContain('packages/db/**')
76+
})
77+
78+
test('uses ECR for container registry', () => {
79+
expect(workflow).toContain('amazon-ecr-login@v2')
80+
expect(workflow).toContain('sandchest-api')
81+
})
82+
83+
test('references correct Dockerfile path', () => {
84+
expect(workflow).toContain('file: apps/api/Dockerfile')
85+
})
86+
87+
test('tags images with SHA and latest', () => {
88+
expect(workflow).toContain('github.sha')
89+
expect(workflow).toContain('sandchest-api:latest')
90+
})
91+
92+
test('uses GitHub Actions cache for buildx', () => {
93+
expect(workflow).toContain('cache-from: type=gha')
94+
expect(workflow).toContain('cache-to: type=gha,mode=max')
95+
})
96+
})
97+
98+
// ---------------------------------------------------------------------------
99+
// Rust build workflow
100+
// ---------------------------------------------------------------------------
101+
102+
describe('rust-build.yml', () => {
103+
const workflow = readFile('.github/workflows/rust-build.yml')
104+
105+
test('triggers on node daemon and proto changes', () => {
106+
expect(workflow).toContain('crates/sandchest-node/**')
107+
expect(workflow).toContain('packages/contract/proto/**')
108+
})
109+
110+
test('builds release binary for sandchest-node', () => {
111+
expect(workflow).toContain('cargo build --release --package sandchest-node')
112+
})
113+
114+
test('installs protobuf compiler', () => {
115+
expect(workflow).toContain('protobuf-compiler')
116+
})
117+
118+
test('uploads binary artifact and pushes to S3', () => {
119+
expect(workflow).toContain('upload-artifact@v4')
120+
expect(workflow).toContain('retention-days: 30')
121+
expect(workflow).toContain('aws s3 cp')
122+
expect(workflow).toContain('binaries/sandchest-node')
123+
})
124+
})
125+
126+
// ---------------------------------------------------------------------------
127+
// CI workflow
128+
// ---------------------------------------------------------------------------
129+
130+
describe('ci.yml', () => {
131+
const workflow = readFile('.github/workflows/ci.yml')
132+
133+
test('runs on PR and push to main', () => {
134+
expect(workflow).toContain('pull_request:')
135+
expect(workflow).toContain('branches: [main]')
136+
})
137+
138+
test('has typecheck and lint job', () => {
139+
expect(workflow).toContain('typecheck-and-lint')
140+
expect(workflow).toContain('bun run typecheck')
141+
expect(workflow).toContain('bun run lint')
142+
})
143+
144+
test('has TypeScript test job', () => {
145+
expect(workflow).toContain('test-ts')
146+
expect(workflow).toContain('bun run test')
147+
})
148+
149+
test('has Rust check job with clippy', () => {
150+
expect(workflow).toContain('check-rust')
151+
expect(workflow).toContain('cargo check --workspace')
152+
expect(workflow).toContain('cargo test --workspace')
153+
expect(workflow).toContain('cargo clippy --workspace -- -D warnings')
154+
})
155+
156+
test('cancels in-progress runs on same ref', () => {
157+
expect(workflow).toContain('cancel-in-progress: true')
158+
})
159+
})
160+
161+
// ---------------------------------------------------------------------------
162+
// Dockerfile
163+
// ---------------------------------------------------------------------------
164+
67165
describe('Dockerfile', () => {
68166
const dockerfile = readFileSync(resolve(API_DIR, 'Dockerfile'), 'utf-8')
69167

@@ -108,28 +206,91 @@ describe('Dockerfile', () => {
108206
})
109207
})
110208

111-
describe('fly.toml', () => {
112-
const flytoml = readFileSync(resolve(API_DIR, 'fly.toml'), 'utf-8')
209+
// ---------------------------------------------------------------------------
210+
// SST config
211+
// ---------------------------------------------------------------------------
212+
213+
describe('sst.config.ts', () => {
214+
const config = readFile('sst.config.ts')
215+
216+
test('exists and imports all infra modules', () => {
217+
expect(config).toContain('infra/alarms')
218+
expect(config).toContain('infra/app')
219+
expect(config).toContain('infra/bucket')
220+
expect(config).toContain('infra/cluster')
221+
expect(config).toContain('infra/node')
222+
expect(config).toContain('infra/oidc')
223+
expect(config).toContain('infra/redis')
224+
expect(config).toContain('infra/vpc')
225+
})
226+
227+
test('creates all core infrastructure resources', () => {
228+
expect(config).toContain('sst.aws.Vpc')
229+
expect(config).toContain('sst.aws.Redis')
230+
expect(config).toContain('sst.aws.Bucket')
231+
expect(config).toContain('sst.aws.Cluster')
232+
expect(config).toContain('sst.Secret')
233+
})
234+
235+
test('links secrets and resources to API service', () => {
236+
expect(config).toContain('link: [')
237+
expect(config).toContain('redis')
238+
expect(config).toContain('artifactBucket')
239+
expect(config).toContain('databaseUrl')
240+
expect(config).toContain('betterAuthSecret')
241+
expect(config).toContain('resendApiKey')
242+
})
243+
244+
test('creates node daemon EC2 instance', () => {
245+
expect(config).toContain('aws.ec2.Instance')
246+
expect(config).toContain('NodeInstance')
247+
})
248+
249+
test('creates GitHub OIDC provider and deploy role', () => {
250+
expect(config).toContain('aws.iam.OpenIdConnectProvider')
251+
expect(config).toContain('GitHubOidc')
252+
expect(config).toContain('DeployRole')
253+
})
113254

114-
test('sets app name', () => {
115-
expect(flytoml).toContain('app = "sandchest-api"')
255+
test('creates CloudWatch alarms', () => {
256+
expect(config).toContain('EcsRunningTaskAlarm')
257+
expect(config).toContain('EcsCpuAlarm')
258+
expect(config).toContain('EcsMemoryAlarm')
259+
expect(config).toContain('Alb5xxAlarm')
260+
expect(config).toContain('AlbResponseTimeAlarm')
261+
expect(config).toContain('RedisMemoryAlarm')
262+
expect(config).toContain('RedisEvictionAlarm')
263+
expect(config).toContain('NodeHeartbeatAlarm')
116264
})
117265

118-
test('configures HTTP service on port 3001', () => {
119-
expect(flytoml).toContain('internal_port = 3001')
120-
expect(flytoml).toContain('force_https = true')
266+
test('exports infrastructure outputs', () => {
267+
expect(config).toContain('vpcId')
268+
expect(config).toContain('redisHost')
269+
expect(config).toContain('artifactBucketName')
270+
expect(config).toContain('apiUrl')
271+
expect(config).toContain('nodeInstanceId')
272+
expect(config).toContain('deployRoleArn')
273+
expect(config).toContain('alarmTopicArn')
121274
})
122275

123-
test('has health check on /healthz', () => {
124-
expect(flytoml).toContain('path = "/healthz"')
276+
test('grants node IAM roles for S3 and CloudWatch', () => {
277+
expect(config).toContain('NodeS3Policy')
278+
expect(config).toContain('NodeCloudWatchPolicy')
279+
expect(config).toContain('s3:PutObject')
280+
expect(config).toContain('s3:GetObject')
281+
expect(config).toContain('cloudwatch:PutMetricData')
125282
})
126283

127-
test('configures auto-start and minimum machines', () => {
128-
expect(flytoml).toContain('auto_start_machines = true')
129-
expect(flytoml).toContain('min_machines_running = 1')
284+
test('references Dockerfile in correct location', () => {
285+
expect(config).toContain('dockerfile: "apps/api/Dockerfile"')
130286
})
131287

132-
test('references correct Dockerfile', () => {
133-
expect(flytoml).toContain('dockerfile = "Dockerfile"')
288+
test('required files exist', () => {
289+
expect(existsSync(resolve(ROOT, 'sst.config.ts'))).toBe(true)
290+
expect(existsSync(resolve(ROOT, 'apps/api/Dockerfile'))).toBe(true)
291+
expect(existsSync(resolve(ROOT, '.github/workflows/deploy.yml'))).toBe(true)
292+
expect(existsSync(resolve(ROOT, '.github/workflows/docker-build.yml'))).toBe(true)
293+
expect(existsSync(resolve(ROOT, '.github/workflows/rust-build.yml'))).toBe(true)
294+
expect(existsSync(resolve(ROOT, '.github/workflows/ci.yml'))).toBe(true)
134295
})
135296
})

0 commit comments

Comments
 (0)