|
|
|
@@ -1,85 +1,55 @@
|
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { readFileSync, existsSync } from 'node:fs'
|
|
|
|
|
import { resolve } from 'node:path'
|
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
|
import { test } from 'vitest'
|
|
|
|
|
|
|
|
|
|
const config = readFileSync(resolve(process.cwd(), 'nginx/default.conf.template'), 'utf8').replace(/\r\n/g, '\n')
|
|
|
|
|
const testCompose = readFileSync(resolve(process.cwd(), 'docker-compose.test.yml'), 'utf8')
|
|
|
|
|
const developmentCompose = readFileSync(resolve(process.cwd(), 'docker-compose.development.yml'), 'utf8')
|
|
|
|
|
const productionCompose = readFileSync(resolve(process.cwd(), 'docker-compose.production.yml'), 'utf8')
|
|
|
|
|
const jenkinsfile = readFileSync(resolve(process.cwd(), 'Jenkinsfile'), 'utf8')
|
|
|
|
|
const dockerfile = readFileSync(resolve(process.cwd(), 'Dockerfile'), 'utf8')
|
|
|
|
|
const root = process.cwd()
|
|
|
|
|
const config = readFileSync(resolve(root, 'nginx/default.conf.template'), 'utf8')
|
|
|
|
|
const compose = readFileSync(resolve(root, 'docker-compose.test.yml'), 'utf8')
|
|
|
|
|
const jenkins = readFileSync(resolve(root, 'Jenkinsfile'), 'utf8')
|
|
|
|
|
const vite = readFileSync(resolve(root, 'vite.config.ts'), 'utf8')
|
|
|
|
|
|
|
|
|
|
describe('admin nginx deployment config', () => {
|
|
|
|
|
it('allows 50 MiB files plus multipart overhead through both bounded API proxies', () => {
|
|
|
|
|
expect(config.match(/client_max_body_size 55m;/g)).toHaveLength(3)
|
|
|
|
|
expect(config).not.toContain('client_max_body_size 12m;')
|
|
|
|
|
expect(config).not.toContain('client_max_body_size 0;')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('prevents the application shell from retaining stale chunk references', () => {
|
|
|
|
|
expect(config).toContain('location = /index.html')
|
|
|
|
|
expect(config).toContain('Cache-Control "no-cache, no-store, must-revalidate"')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('serves hashed assets immutably and never falls back missing chunks to HTML', () => {
|
|
|
|
|
expect(config).toContain('location /assets/')
|
|
|
|
|
expect(config).toContain('try_files $uri =404;')
|
|
|
|
|
expect(config).toContain('Cache-Control "public, max-age=31536000, immutable"')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps Vue history routes on the application shell without caching them', () => {
|
|
|
|
|
expect(config).toContain('try_files $uri $uri/ /index.html;')
|
|
|
|
|
expect(config).toContain('Cache-Control "no-cache"')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('proxies overseas API requests to the gateway API prefix', () => {
|
|
|
|
|
expect(config).toContain('location /overseas-api/')
|
|
|
|
|
expect(config).toContain('rewrite ^/overseas-api/(.*)$ /api/$1 break;')
|
|
|
|
|
expect(config).toContain('proxy_pass ${OVERSEAS_API_UPSTREAM};')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('declares an explicit overseas gateway upstream in container deployments', () => {
|
|
|
|
|
expect(testCompose).toContain('- "host.docker.internal:host-gateway"')
|
|
|
|
|
expect(testCompose).toContain('API_UPSTREAM: "${API_UPSTREAM:-http://host.docker.internal:9000}"')
|
|
|
|
|
expect(testCompose).toContain('OVERSEAS_API_UPSTREAM: "${OVERSEAS_API_UPSTREAM:-http://host.docker.internal:9300}"')
|
|
|
|
|
expect(testCompose).not.toContain('http://test-api-cn.buggoglobal.com')
|
|
|
|
|
expect(testCompose).not.toContain('http://test-api-global.buggoglobal.com')
|
|
|
|
|
expect(testCompose).toContain('CLIENT_WEB_URL: "${CLIENT_WEB_URL:-https://test-app.buggoglobal.com/}"')
|
|
|
|
|
expect(testCompose).not.toContain('CLIENT_WEB_URL: "http://8.163.')
|
|
|
|
|
expect(developmentCompose).toContain('CLIENT_WEB_URL: "${CLIENT_WEB_URL:-https://dev-app.buggoglobal.com/}"')
|
|
|
|
|
expect(developmentCompose).toContain('OVERSEAS_API_UPSTREAM: "http://firstleg-development-gateway-overseas:9300"')
|
|
|
|
|
expect(config).toContain('proxy_set_header Host $proxy_host;')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps local environments on target nodes and uses credential-managed SSH for production', () => {
|
|
|
|
|
expect(jenkinsfile).toContain("env.JOB_BASE_NAME?.endsWith('-prod') ? 'java-builder'")
|
|
|
|
|
expect(jenkinsfile).toContain("DEVELOPMENT_COMPOSE_PROJECT = 'admin-web'")
|
|
|
|
|
expect(jenkinsfile).toContain('docker compose -p "$DEVELOPMENT_COMPOSE_PROJECT" -f "$DEVELOPMENT_COMPOSE_FILE" up -d')
|
|
|
|
|
expect(jenkinsfile).not.toContain('/var/jenkins_home/.ssh')
|
|
|
|
|
expect(jenkinsfile).not.toContain('docker save')
|
|
|
|
|
expect(jenkinsfile).toContain("credentialsId: 'firstleg-production-ssh'")
|
|
|
|
|
expect(jenkinsfile).toContain('git rev-parse origin/main')
|
|
|
|
|
expect(productionCompose).toContain('127.0.0.1:${HOST_PORT:-9001}:80')
|
|
|
|
|
expect(productionCompose).toContain('API_UPSTREAM: "http://gateway-active:9000"')
|
|
|
|
|
expect(productionCompose).toContain('OVERSEAS_API_UPSTREAM: "http://gateway-overseas-active:9300"')
|
|
|
|
|
expect(productionCompose).toContain('name: firstleg-production-routing')
|
|
|
|
|
expect(productionCompose).toContain('name: firstleg-prod-infra-network')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('uses authenticated internal base images in CI instead of pulling from Docker Hub', () => {
|
|
|
|
|
expect(dockerfile).toContain('ARG NODE_IMAGE=node:22-alpine')
|
|
|
|
|
expect(dockerfile).toContain('ARG NGINX_IMAGE=nginx:1.27-alpine')
|
|
|
|
|
expect(dockerfile).toContain('FROM ${NODE_IMAGE} AS build')
|
|
|
|
|
expect(dockerfile).toContain('FROM ${NGINX_IMAGE}')
|
|
|
|
|
expect(jenkinsfile).toContain("NODE_BASE_IMAGE = '172.31.198.7:10082/java-project/node:22-alpine'")
|
|
|
|
|
expect(jenkinsfile).toContain("NGINX_BASE_IMAGE = '172.31.198.7:10082/java-project/nginx:1.27-alpine'")
|
|
|
|
|
expect(jenkinsfile).toContain('withCredentials([usernamePassword(')
|
|
|
|
|
expect(jenkinsfile).toContain('export DOCKER_CONFIG="${WORKSPACE}@tmp/docker-auth"')
|
|
|
|
|
expect(jenkinsfile).toContain('--build-arg VITE_CLIENT_WEB_URL="${CLIENT_WEB_URL:-https://dev-app.buggoglobal.com/}"')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('proxies Mojia measurement OpenAPI requests to the domestic gateway', () => {
|
|
|
|
|
expect(config).toContain('location /openapi/')
|
|
|
|
|
expect(config).toContain('location /openapi/ {\n client_max_body_size 55m;\n proxy_pass ${API_UPSTREAM};')
|
|
|
|
|
})
|
|
|
|
|
test('all business traffic uses one Gateway without rewriting API paths', () => {
|
|
|
|
|
assert.match(config, /location \/api\//)
|
|
|
|
|
assert.match(config, /proxy_pass \$\{API_UPSTREAM\};/)
|
|
|
|
|
assert.doesNotMatch(config, /overseas-api|OVERSEAS_API_UPSTREAM|rewrite/)
|
|
|
|
|
assert.match(config, /client_max_body_size 55m;/)
|
|
|
|
|
assert.match(config, /proxy_set_header Host \$proxy_host;/)
|
|
|
|
|
assert.doesNotMatch(vite, /OVERSEAS_API|AUTH_API_PROXY_TARGET|IAM_API_PROXY_TARGET|WAREHOUSE_API_PROXY_TARGET/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('test deployment requires an explicit shared network, Gateway and version', () => {
|
|
|
|
|
for (const key of ['IMAGE_TAG']) {
|
|
|
|
|
assert.ok(compose.includes('${' + key + ':?'))
|
|
|
|
|
}
|
|
|
|
|
assert.ok(compose.includes('${API_UPSTREAM:-http://gateway:9000}'))
|
|
|
|
|
assert.ok(compose.includes('${TEST_NETWORK:-firstleg-test-network}'))
|
|
|
|
|
assert.match(compose, /external: true/)
|
|
|
|
|
assert.match(compose, /127\.0\.0\.1:/)
|
|
|
|
|
assert.doesNotMatch(compose, /overseas|host\.docker\.internal|production|development/)
|
|
|
|
|
assert.match(jenkins, /label 'java-test'/)
|
|
|
|
|
assert.match(jenkins, /docker compose -f docker-compose\.test\.yml config --quiet/)
|
|
|
|
|
assert.match(jenkins, /docker network inspect/)
|
|
|
|
|
assert.doesNotMatch(jenkins, /docker-compose\.(production|development)|deploy-production|sshUserPrivateKey/)
|
|
|
|
|
for (const file of ['docker-compose.production.yml', 'docker-compose.development.yml', 'scripts/deploy-production.sh']) {
|
|
|
|
|
assert.equal(existsSync(resolve(root, file)), false)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('application shell is revalidated and missing chunks remain 404', () => {
|
|
|
|
|
assert.match(config, /location = \/index\.html/)
|
|
|
|
|
assert.match(config, /no-store/)
|
|
|
|
|
assert.match(config, /max-age=31536000, immutable/)
|
|
|
|
|
assert.match(config, /try_files \$uri =404;/)
|
|
|
|
|
assert.match(config, /try_files \$uri \$uri\/ \/index\.html;/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('container build excludes local environment values and waits for Nginx health', () => {
|
|
|
|
|
const ignore = readFileSync(resolve(root, '.dockerignore'), 'utf8')
|
|
|
|
|
assert.match(ignore, /^\.env$/m)
|
|
|
|
|
assert.match(ignore, /^\.env\.\*$/m)
|
|
|
|
|
assert.match(compose, /healthcheck:/)
|
|
|
|
|
assert.match(compose, /http:\/\/127\.0\.0\.1\/healthz/)
|
|
|
|
|
assert.match(jenkins, /up -d --wait --wait-timeout 120/)
|
|
|
|
|
})
|
|
|
|
|