189 lines
7.9 KiB
JavaScript
189 lines
7.9 KiB
JavaScript
import assert from 'node:assert/strict'
|
|||
|
|
import { readdir, readFile } from 'node:fs/promises'
|
||
|
|
import path from 'node:path'
|
||
|
|
import test from 'node:test'
|
||
|
|
import { fileURLToPath } from 'node:url'
|
||
|
|
|
||
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||
|
|
|
||
|
|
const serviceModules = [
|
||
|
|
'auth', 'iam', 'order', 'firstleg', 'finance', 'file', 'warehouse',
|
||
|
|
'integration-service', 'print', 'event-bridge-service', 'lastleg-service',
|
||
|
|
'warehouse-overseas-service'
|
||
|
|
]
|
||
|
|
|
||
|
|
const canonicalSource = new Map(serviceModules.map(name => [name, `${name}${name.endsWith('-service') ? '' : '-service'}`]))
|
||
|
|
|
||
|
|
const expectedFeignEdges = new Map(Object.entries({
|
||
|
|
'auth-service->iam-service': 2,
|
||
|
|
'file-service->finance-service': 1,
|
||
|
|
'file-service->firstleg-service': 1,
|
||
|
|
'file-service->iam-service': 2,
|
||
|
|
'file-service->integration-service': 1,
|
||
|
|
'file-service->order-service': 1,
|
||
|
|
'file-service->warehouse-service': 1,
|
||
|
|
'finance-service->file-service': 1,
|
||
|
|
'finance-service->firstleg-service': 2,
|
||
|
|
'finance-service->iam-service': 1,
|
||
|
|
'finance-service->warehouse-service': 1,
|
||
|
|
'firstleg-service->event-bridge-service': 1,
|
||
|
|
'firstleg-service->file-service': 7,
|
||
|
|
'firstleg-service->finance-service': 1,
|
||
|
|
'firstleg-service->iam-service': 1,
|
||
|
|
'firstleg-service->integration-service': 1,
|
||
|
|
'firstleg-service->order-service': 2,
|
||
|
|
'firstleg-service->print-service': 1,
|
||
|
|
'firstleg-service->warehouse-service': 4,
|
||
|
|
'iam-service->auth-service': 1,
|
||
|
|
'iam-service->event-bridge-service': 1,
|
||
|
|
'iam-service->firstleg-service': 1,
|
||
|
|
'iam-service->warehouse-service': 1,
|
||
|
|
'lastleg-service->event-bridge-service': 1,
|
||
|
|
'lastleg-service->warehouse-service': 1,
|
||
|
|
'order-service->file-service': 1,
|
||
|
|
'order-service->firstleg-service': 3,
|
||
|
|
'order-service->iam-service': 1,
|
||
|
|
'order-service->warehouse-service': 1
|
||
|
|
}))
|
||
|
|
|
||
|
|
const expectedRawHttpImplementations = [
|
||
|
|
'common-audit/src/main/java/com/baiyue/common/audit/HttpAuditSink.java',
|
||
|
|
'event-bridge-service/src/main/java/com/baiyue/eventbridge/service/HttpFirstlegEventClient.java',
|
||
|
|
'event-bridge-service/src/main/java/com/baiyue/eventbridge/service/HttpLastlegEventClient.java',
|
||
|
|
'gateway-overseas/src/main/java/com/baiyue/gateway/filter/HttpPdaSessionValidationClient.java',
|
||
|
|
'lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpUnloadPhotoFileGateway.java',
|
||
|
|
'lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java',
|
||
|
|
'lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java',
|
||
|
|
'lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/gateway/HttpRelabelPhotoFileGateway.java',
|
||
|
|
'integration-service/src/main/java/com/baiyue/integration/provider/EvergreenTrackingClient.java',
|
||
|
|
'integration-service/src/main/java/com/baiyue/integration/provider/HedeTrackingClient.java',
|
||
|
|
'integration-service/src/main/java/com/baiyue/integration/provider/MatsonTrackingClient.java',
|
||
|
|
'integration-service/src/main/java/com/baiyue/integration/provider/SmlTrackingClient.java',
|
||
|
|
'integration-service/src/main/java/com/baiyue/integration/provider/ZimTrackingClient.java',
|
||
|
|
'integration-service/src/main/java/com/baiyue/integration/provider/ocr/BaiduMeterClient.java'
|
||
|
|
]
|
||
|
|
|
||
|
|
const reviewedRawInternalEdges = [
|
||
|
|
'event-bridge-service->firstleg-service',
|
||
|
|
'event-bridge-service->lastleg-service',
|
||
|
|
'gateway-overseas->auth-service',
|
||
|
|
'lastleg-service->file-service',
|
||
|
|
'lastleg-service->warehouse-overseas-service'
|
||
|
|
]
|
||
|
|
|
||
|
|
const rawHttpPattern = /\b(?:RestClient|WebClient|HttpClient)\b/
|
||
|
|
|
||
|
|
async function javaFiles(directory) {
|
||
|
|
const result = []
|
||
|
|
async function visit(current) {
|
||
|
|
for (const entry of await readdir(current, { withFileTypes: true })) {
|
||
|
|
const target = path.join(current, entry.name)
|
||
|
|
if (entry.isDirectory()) await visit(target)
|
||
|
|
else if (entry.isFile() && entry.name.endsWith('.java')) result.push(target)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
await visit(directory)
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
async function discoverFeignEdges() {
|
||
|
|
const edges = new Map()
|
||
|
|
for (const module of serviceModules) {
|
||
|
|
const sourceRoot = path.join(root, module, 'src', 'main', 'java')
|
||
|
|
for (const file of await javaFiles(sourceRoot)) {
|
||
|
|
const source = await readFile(file, 'utf8')
|
||
|
|
if (!source.includes('@FeignClient')) continue
|
||
|
|
const annotation = source.match(/@FeignClient\(([\s\S]*?)\)\s*(?:public\s+)?interface/)
|
||
|
|
const target = annotation?.[1].match(/name\s*=\s*"([^"]+)"/)?.[1]
|
||
|
|
assert.ok(target, `cannot resolve Feign target from ${path.relative(root, file)}`)
|
||
|
|
const edge = `${canonicalSource.get(module)}->${target}`
|
||
|
|
edges.set(edge, (edges.get(edge) ?? 0) + 1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return edges
|
||
|
|
}
|
||
|
|
|
||
|
|
function sortedEntries(map) {
|
||
|
|
return [...map.entries()].sort(([left], [right]) => left.localeCompare(right))
|
||
|
|
}
|
||
|
|
|
||
|
|
function stronglyConnectedComponents(edgeNames) {
|
||
|
|
const adjacency = new Map()
|
||
|
|
for (const edge of edgeNames) {
|
||
|
|
const [from, to] = edge.split('->')
|
||
|
|
if (!adjacency.has(from)) adjacency.set(from, new Set())
|
||
|
|
if (!adjacency.has(to)) adjacency.set(to, new Set())
|
||
|
|
adjacency.get(from).add(to)
|
||
|
|
}
|
||
|
|
let index = 0
|
||
|
|
const indexes = new Map()
|
||
|
|
const lowLinks = new Map()
|
||
|
|
const stack = []
|
||
|
|
const onStack = new Set()
|
||
|
|
const components = []
|
||
|
|
|
||
|
|
function connect(node) {
|
||
|
|
indexes.set(node, index)
|
||
|
|
lowLinks.set(node, index++)
|
||
|
|
stack.push(node)
|
||
|
|
onStack.add(node)
|
||
|
|
for (const target of adjacency.get(node)) {
|
||
|
|
if (!indexes.has(target)) {
|
||
|
|
connect(target)
|
||
|
|
lowLinks.set(node, Math.min(lowLinks.get(node), lowLinks.get(target)))
|
||
|
|
} else if (onStack.has(target)) {
|
||
|
|
lowLinks.set(node, Math.min(lowLinks.get(node), indexes.get(target)))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (lowLinks.get(node) !== indexes.get(node)) return
|
||
|
|
const component = []
|
||
|
|
let current
|
||
|
|
do {
|
||
|
|
current = stack.pop()
|
||
|
|
onStack.delete(current)
|
||
|
|
component.push(current)
|
||
|
|
} while (current !== node)
|
||
|
|
components.push(component.sort())
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const node of adjacency.keys()) if (!indexes.has(node)) connect(node)
|
||
|
|
return components.filter(component => component.length > 1)
|
||
|
|
.sort((left, right) => left.join().localeCompare(right.join()))
|
||
|
|
}
|
||
|
|
|
||
|
|
test('all Feign clients remain covered by the reviewed synchronous edge baseline', async () => {
|
||
|
|
assert.deepEqual(sortedEntries(await discoverFeignEdges()), sortedEntries(expectedFeignEdges))
|
||
|
|
})
|
||
|
|
|
||
|
|
test('raw HTTP transports cannot be added without an explicit graph review', async () => {
|
||
|
|
const discovered = []
|
||
|
|
for (const module of ['common-audit', 'event-bridge-service', 'gateway-overseas', 'lastleg-service', 'integration-service']) {
|
||
|
|
for (const file of await javaFiles(path.join(root, module, 'src', 'main', 'java'))) {
|
||
|
|
const source = await readFile(file, 'utf8')
|
||
|
|
if (!rawHttpPattern.test(source)) continue
|
||
|
|
if (!/(?:private|final)\s+(?:RestClient|WebClient|HttpClient)\b|HttpClient\s+client\s*=/.test(source)) continue
|
||
|
|
discovered.push(path.relative(root, file).replaceAll('\\', '/'))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert.deepEqual(discovered.sort(), [...expectedRawHttpImplementations].sort())
|
||
|
|
})
|
||
|
|
|
||
|
|
test('the current synchronous service graph keeps its reviewed strongly connected component visible', async () => {
|
||
|
|
const feignEdges = (await discoverFeignEdges()).keys()
|
||
|
|
const components = stronglyConnectedComponents([...feignEdges, ...reviewedRawInternalEdges])
|
||
|
|
assert.deepEqual(components, [[
|
||
|
|
'auth-service', 'event-bridge-service', 'file-service', 'finance-service',
|
||
|
|
'firstleg-service', 'iam-service', 'lastleg-service', 'order-service'
|
||
|
|
]])
|
||
|
|
})
|
||
|
|
|
||
|
|
test('shared Feign timeout defaults and long-running export overrides do not drift silently', async () => {
|
||
|
|
for (const profile of ['local', 'cn', 'overseas']) {
|
||
|
|
const source = await readFile(path.join(root, 'config', 'nacos', profile, 'baiyue-common.yml'), 'utf8')
|
||
|
|
assert.match(source, /connect-timeout:\s*3000/)
|
||
|
|
assert.match(source, /read-timeout:\s*5000/)
|
||
|
|
}
|
||
|
|
const fileConfig = await readFile(path.join(root, 'file', 'src', 'main', 'resources', 'application.yml'), 'utf8')
|
||
|
|
assert.equal((fileConfig.match(/read-timeout:\s*15000/g) ?? []).length, 5)
|
||
|
|
})
|