mega-framework 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +127 -0
- package/.env.example +186 -0
- package/.prettierrc.json +8 -0
- package/CHANGELOG.md +259 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/mega-ws-hub.js +15 -0
- package/bin/mega.js +38 -0
- package/docker-compose.yml +201 -0
- package/eslint.config.js +57 -0
- package/infra/otel-collector-config.yaml +43 -0
- package/jsconfig.json +18 -0
- package/package.json +121 -0
- package/sample/crud/.env +18 -0
- package/sample/crud/.env.example +50 -0
- package/sample/crud/README.md +85 -0
- package/sample/crud/apps/main/app.config.js +114 -0
- package/sample/crud/apps/main/channels/chat-bus.js +115 -0
- package/sample/crud/apps/main/channels/chat-channel.js +145 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
- package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
- package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
- package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
- package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
- package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
- package/sample/crud/apps/main/controllers/note-controller.js +116 -0
- package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
- package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
- package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
- package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
- package/sample/crud/apps/main/controllers/user-controller.js +34 -0
- package/sample/crud/apps/main/controllers/web-controller.js +137 -0
- package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
- package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
- package/sample/crud/apps/main/jobs/email-job.js +72 -0
- package/sample/crud/apps/main/locales/client/en.json +3 -0
- package/sample/crud/apps/main/locales/client/ko.json +3 -0
- package/sample/crud/apps/main/locales/server/en.json +316 -0
- package/sample/crud/apps/main/locales/server/ko.json +316 -0
- package/sample/crud/apps/main/middleware/web-auth.js +40 -0
- package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
- package/sample/crud/apps/main/models/note.js +71 -0
- package/sample/crud/apps/main/models/user.js +86 -0
- package/sample/crud/apps/main/public/css/app.css +101 -0
- package/sample/crud/apps/main/public/css/guide.css +137 -0
- package/sample/crud/apps/main/public/js/app.js +54 -0
- package/sample/crud/apps/main/public/js/perf.js +129 -0
- package/sample/crud/apps/main/public/js/theme-init.js +12 -0
- package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
- package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
- package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
- package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
- package/sample/crud/apps/main/routes/auth.js +15 -0
- package/sample/crud/apps/main/routes/cron.js +14 -0
- package/sample/crud/apps/main/routes/guide.js +25 -0
- package/sample/crud/apps/main/routes/jobs.js +14 -0
- package/sample/crud/apps/main/routes/logs.js +28 -0
- package/sample/crud/apps/main/routes/metrics.js +13 -0
- package/sample/crud/apps/main/routes/notes.js +19 -0
- package/sample/crud/apps/main/routes/perf.js +47 -0
- package/sample/crud/apps/main/routes/redis.js +14 -0
- package/sample/crud/apps/main/routes/tracing.js +14 -0
- package/sample/crud/apps/main/routes/upload.js +16 -0
- package/sample/crud/apps/main/routes/users.js +54 -0
- package/sample/crud/apps/main/routes/web.js +23 -0
- package/sample/crud/apps/main/routes/worker.js +15 -0
- package/sample/crud/apps/main/routes/ws.js +30 -0
- package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
- package/sample/crud/apps/main/services/auth-service.js +74 -0
- package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
- package/sample/crud/apps/main/services/guide-service.js +145 -0
- package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
- package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
- package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
- package/sample/crud/apps/main/services/note-service.js +75 -0
- package/sample/crud/apps/main/services/perf-service.js +302 -0
- package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
- package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
- package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
- package/sample/crud/apps/main/services/user-service.js +65 -0
- package/sample/crud/apps/main/views/auth/login.ejs +57 -0
- package/sample/crud/apps/main/views/auth/register.ejs +71 -0
- package/sample/crud/apps/main/views/cron/index.ejs +92 -0
- package/sample/crud/apps/main/views/guide/index.ejs +24 -0
- package/sample/crud/apps/main/views/guide/page.ejs +64 -0
- package/sample/crud/apps/main/views/home.ejs +82 -0
- package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
- package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
- package/sample/crud/apps/main/views/logs/index.ejs +80 -0
- package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
- package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
- package/sample/crud/apps/main/views/notes/list.ejs +74 -0
- package/sample/crud/apps/main/views/notes/new.ejs +45 -0
- package/sample/crud/apps/main/views/perf/index.ejs +90 -0
- package/sample/crud/apps/main/views/redis/index.ejs +65 -0
- package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
- package/sample/crud/apps/main/views/upload/index.ejs +79 -0
- package/sample/crud/apps/main/views/users/edit.ejs +48 -0
- package/sample/crud/apps/main/views/users/list.ejs +81 -0
- package/sample/crud/apps/main/views/users/new.ejs +48 -0
- package/sample/crud/apps/main/views/worker/index.ejs +70 -0
- package/sample/crud/apps/main/views/ws/index.ejs +62 -0
- package/sample/crud/apps/main/workers/hash-worker.js +17 -0
- package/sample/crud/apps/main/workers/hash.task.js +22 -0
- package/sample/crud/ecosystem.config.cjs +9 -0
- package/sample/crud/mega.config.js +105 -0
- package/sample/crud/package-lock.json +5665 -0
- package/sample/crud/package.json +28 -0
- package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
- package/sample/crud/test/apps/main/auth-service.test.js +93 -0
- package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
- package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
- package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
- package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
- package/sample/crud/test/apps/main/email-job.test.js +76 -0
- package/sample/crud/test/apps/main/guide-service.test.js +68 -0
- package/sample/crud/test/apps/main/hash-task.test.js +30 -0
- package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
- package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
- package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/note-service.test.js +68 -0
- package/sample/crud/test/apps/main/perf-service.test.js +121 -0
- package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
- package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
- package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
- package/sample/crud/test/apps/main/user-service.test.js +65 -0
- package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
- package/sample/crud/vitest.config.js +8 -0
- package/sample/crud/yarn.lock +2142 -0
- package/sample/simple/.env.example +15 -0
- package/sample/simple/README.md +52 -0
- package/sample/simple/apps/main/app.config.js +35 -0
- package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
- package/sample/simple/apps/main/locales/client/en.json +3 -0
- package/sample/simple/apps/main/locales/client/ko.json +3 -0
- package/sample/simple/apps/main/locales/server/en.json +23 -0
- package/sample/simple/apps/main/locales/server/ko.json +23 -0
- package/sample/simple/apps/main/public/css/app.css +101 -0
- package/sample/simple/apps/main/public/hello.txt +1 -0
- package/sample/simple/apps/main/public/js/app.js +54 -0
- package/sample/simple/apps/main/public/js/theme-init.js +12 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/simple/apps/main/routes/index.js +9 -0
- package/sample/simple/apps/main/routes/pages.js +12 -0
- package/sample/simple/apps/main/views/index.ejs +56 -0
- package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
- package/sample/simple/ecosystem.config.cjs +10 -0
- package/sample/simple/mega.config.js +27 -0
- package/sample/simple/package-lock.json +1851 -0
- package/sample/simple/package.json +25 -0
- package/sample/simple/test/apps/main/index.test.js +13 -0
- package/sample/simple/vitest.config.js +8 -0
- package/src/adapters/adapter-manager.js +305 -0
- package/src/adapters/adapter-options.js +208 -0
- package/src/adapters/file-adapter.js +350 -0
- package/src/adapters/file-session-adapter.js +363 -0
- package/src/adapters/index.js +38 -0
- package/src/adapters/maria-adapter.js +425 -0
- package/src/adapters/mega-adapter.js +511 -0
- package/src/adapters/mega-bus-adapter.js +81 -0
- package/src/adapters/mega-cache-adapter.js +94 -0
- package/src/adapters/mega-db-adapter.js +72 -0
- package/src/adapters/mega-lock-adapter.js +118 -0
- package/src/adapters/mega-log-sink-adapter.js +46 -0
- package/src/adapters/mega-session-adapter.js +72 -0
- package/src/adapters/mongo-adapter.js +396 -0
- package/src/adapters/nats-adapter.js +370 -0
- package/src/adapters/postgres-adapter.js +341 -0
- package/src/adapters/redis-adapter.js +331 -0
- package/src/adapters/redis-session-adapter.js +261 -0
- package/src/adapters/redlock-adapter.js +385 -0
- package/src/adapters/registry.js +157 -0
- package/src/adapters/sqlite-adapter.js +309 -0
- package/src/auth/index.js +103 -0
- package/src/cli/commands/console-cmd.js +56 -0
- package/src/cli/commands/new.js +101 -0
- package/src/cli/commands/routes.js +107 -0
- package/src/cli/commands/scaffold.js +120 -0
- package/src/cli/commands/test-cmd.js +45 -0
- package/src/cli/generators/index.js +368 -0
- package/src/cli/index.js +472 -0
- package/src/cli/template-engine.js +72 -0
- package/src/cli/ws-hub.js +582 -0
- package/src/core/ajv-mapper.js +80 -0
- package/src/core/boot.js +323 -0
- package/src/core/cluster-metrics.js +278 -0
- package/src/core/config-loader.js +115 -0
- package/src/core/config-validator.js +322 -0
- package/src/core/ctx-builder.js +253 -0
- package/src/core/envelope.js +88 -0
- package/src/core/error-mapper.js +116 -0
- package/src/core/formbody.js +69 -0
- package/src/core/hub-link.js +552 -0
- package/src/core/i18n.js +525 -0
- package/src/core/index.js +63 -0
- package/src/core/mega-app.js +1138 -0
- package/src/core/mega-cluster.js +232 -0
- package/src/core/mega-server.js +176 -0
- package/src/core/mega-service.js +41 -0
- package/src/core/migration-runner.js +196 -0
- package/src/core/multipart.js +282 -0
- package/src/core/openapi.js +114 -0
- package/src/core/router.js +388 -0
- package/src/core/routes-loader.js +57 -0
- package/src/core/scope-registry.js +53 -0
- package/src/core/security.js +275 -0
- package/src/core/services-loader.js +98 -0
- package/src/core/session-cleanup-schedule.js +57 -0
- package/src/core/session-store.js +55 -0
- package/src/core/session.js +414 -0
- package/src/core/static-assets.js +126 -0
- package/src/core/template.js +294 -0
- package/src/core/workers-manager.js +193 -0
- package/src/core/ws-compression.js +112 -0
- package/src/core/ws-controller.js +109 -0
- package/src/core/ws-message.js +176 -0
- package/src/core/ws-upgrade.js +445 -0
- package/src/errors/config-error.js +16 -0
- package/src/errors/http-errors.js +130 -0
- package/src/errors/index.js +19 -0
- package/src/errors/mega-error.js +34 -0
- package/src/eslint-plugin/index.js +15 -0
- package/src/eslint-plugin/no-direct-model-import.js +113 -0
- package/src/index.js +131 -0
- package/src/lib/asp/config.js +83 -0
- package/src/lib/asp/crypto.js +145 -0
- package/src/lib/asp/errors.js +49 -0
- package/src/lib/asp/nonce-cache.js +94 -0
- package/src/lib/asp/plugin.js +263 -0
- package/src/lib/asp/ws-terminator.js +101 -0
- package/src/lib/env-mapper.js +222 -0
- package/src/lib/hub-protocol.js +322 -0
- package/src/lib/index.js +42 -0
- package/src/lib/logger/telegram-core.js +150 -0
- package/src/lib/logger/telegram-transport.js +126 -0
- package/src/lib/mega-brute-force.js +225 -0
- package/src/lib/mega-circuit-breaker.js +412 -0
- package/src/lib/mega-cron.js +169 -0
- package/src/lib/mega-hash.js +179 -0
- package/src/lib/mega-health.js +91 -0
- package/src/lib/mega-job-queue.js +600 -0
- package/src/lib/mega-job-worker.js +295 -0
- package/src/lib/mega-job.js +140 -0
- package/src/lib/mega-logger.js +128 -0
- package/src/lib/mega-metrics.js +661 -0
- package/src/lib/mega-plugin.js +650 -0
- package/src/lib/mega-retry.js +95 -0
- package/src/lib/mega-schedule.js +507 -0
- package/src/lib/mega-shutdown.js +176 -0
- package/src/lib/mega-tracing.js +715 -0
- package/src/lib/mega-worker.js +653 -0
- package/src/lib/worker-runner/process-entry.js +30 -0
- package/src/lib/worker-runner/task-dispatch.js +72 -0
- package/src/lib/worker-runner/thread-entry.js +26 -0
- package/src/models/index.js +7 -0
- package/src/models/mega-model.js +151 -0
- package/src/test/index.js +288 -0
- package/templates/adapter/code.tpl +40 -0
- package/templates/adapter/test.tpl +13 -0
- package/templates/app/app.config.tpl +10 -0
- package/templates/app/route.tpl +10 -0
- package/templates/app/test.tpl +13 -0
- package/templates/channel/code.tpl +38 -0
- package/templates/channel/test.tpl +19 -0
- package/templates/controller/code.tpl +16 -0
- package/templates/controller/route.tpl +9 -0
- package/templates/controller/test.tpl +14 -0
- package/templates/job/code.tpl +23 -0
- package/templates/job/test.tpl +17 -0
- package/templates/locale/code.tpl +3 -0
- package/templates/locale/test.tpl +13 -0
- package/templates/middleware/code.tpl +13 -0
- package/templates/middleware/test.tpl +11 -0
- package/templates/migration/code.tpl +20 -0
- package/templates/migration/test.tpl +14 -0
- package/templates/model/code.tpl +21 -0
- package/templates/model/test.tpl +29 -0
- package/templates/project/app.config.tpl +8 -0
- package/templates/project/app.config.views.tpl +37 -0
- package/templates/project/ecosystem.config.tpl +10 -0
- package/templates/project/env.tpl +12 -0
- package/templates/project/gitignore.tpl +8 -0
- package/templates/project/locales/client/en.json.tpl +3 -0
- package/templates/project/locales/client/ko.json.tpl +3 -0
- package/templates/project/locales/server/en.json.tpl +17 -0
- package/templates/project/locales/server/ko.json.tpl +17 -0
- package/templates/project/mega.config.tpl +11 -0
- package/templates/project/package.tpl +25 -0
- package/templates/project/public/css/app.css +101 -0
- package/templates/project/public/js/app.js +54 -0
- package/templates/project/public/js/theme-init.js +12 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/templates/project/readme.tpl +48 -0
- package/templates/project/route.test.tpl +13 -0
- package/templates/project/route.test.views.tpl +15 -0
- package/templates/project/route.tpl +10 -0
- package/templates/project/route.views.tpl +10 -0
- package/templates/project/views/index.ejs.tpl +58 -0
- package/templates/project/views/layout.ejs.tpl +73 -0
- package/templates/project/vitest.config.tpl +8 -0
- package/templates/route/code.tpl +11 -0
- package/templates/route/test.tpl +26 -0
- package/templates/schedule/code.tpl +19 -0
- package/templates/schedule/test.tpl +17 -0
- package/templates/service/code.tpl +18 -0
- package/templates/service/test.tpl +17 -0
- package/templates/worker/code.tpl +14 -0
- package/templates/worker/task.tpl +13 -0
- package/templates/worker/test.tpl +18 -0
- package/vitest.config.js +33 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* TracingDemoService 단위 테스트 — zipkinBase 파생, 현재 trace 기록(있을 때만), snapshot 을 검증한다.
|
|
4
|
+
* MegaTracing(currentTraceIds/isEnabled)을 가짜로 갈음하고 redis native(lpush/ltrim/lrange)를 가짜로 둔다.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, test, expect, vi, beforeEach } from 'vitest'
|
|
7
|
+
|
|
8
|
+
vi.mock('mega-framework', async (importOriginal) => {
|
|
9
|
+
const actual = /** @type {any} */ (await importOriginal())
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
MegaTracing: { ...actual.MegaTracing, currentTraceIds: vi.fn(), isEnabled: vi.fn() },
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
import { MegaTracing } from 'mega-framework'
|
|
17
|
+
import { TracingDemoService } from '../../../apps/main/services/tracing-demo-service.js'
|
|
18
|
+
|
|
19
|
+
function makeCtx() {
|
|
20
|
+
/** @type {Map<string, string[]>} */
|
|
21
|
+
const lists = new Map()
|
|
22
|
+
const native = {
|
|
23
|
+
async lpush(/** @type {string} */ k, /** @type {string} */ v) {
|
|
24
|
+
const arr = lists.get(k) ?? []
|
|
25
|
+
arr.unshift(v)
|
|
26
|
+
lists.set(k, arr)
|
|
27
|
+
return arr.length
|
|
28
|
+
},
|
|
29
|
+
ltrim: vi.fn(async (/** @type {string} */ k, /** @type {number} */ _s, /** @type {number} */ stop) => {
|
|
30
|
+
lists.set(k, (lists.get(k) ?? []).slice(0, stop + 1))
|
|
31
|
+
return 'OK'
|
|
32
|
+
}),
|
|
33
|
+
async lrange(/** @type {string} */ k, /** @type {number} */ s, /** @type {number} */ e) {
|
|
34
|
+
return (lists.get(k) ?? []).slice(s, e + 1)
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
const ctx = { log: { debug() {} }, cache: (/** @type {string} */ a) => (a === 'demo' ? { native } : null) }
|
|
38
|
+
return { ctx, lists }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
vi.clearAllMocks()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('TracingDemoService — zipkinBase', () => {
|
|
46
|
+
test('MEGA_OTEL_ZIPKIN_API 에서 /api/v2 를 떼 UI 루트를 만든다', () => {
|
|
47
|
+
const prev = process.env.MEGA_OTEL_ZIPKIN_API
|
|
48
|
+
process.env.MEGA_OTEL_ZIPKIN_API = 'http://zip.example:9411/api/v2'
|
|
49
|
+
expect(TracingDemoService.zipkinBase()).toBe('http://zip.example:9411')
|
|
50
|
+
delete process.env.MEGA_OTEL_ZIPKIN_API
|
|
51
|
+
expect(TracingDemoService.zipkinBase()).toBe('http://localhost:9411')
|
|
52
|
+
if (prev !== undefined) process.env.MEGA_OTEL_ZIPKIN_API = prev
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('TracingDemoService — record', () => {
|
|
57
|
+
test('trace_id 가 있으면 이력 머리에 기록하고 traceId 를 돌려준다', async () => {
|
|
58
|
+
vi.mocked(MegaTracing.currentTraceIds).mockReturnValue({ traceId: 'abc', spanId: 'def' })
|
|
59
|
+
const { ctx, lists } = makeCtx()
|
|
60
|
+
const svc = new TracingDemoService(/** @type {any} */ (ctx))
|
|
61
|
+
const id = await svc.record('GET /demo/tracing')
|
|
62
|
+
expect(id).toBe('abc')
|
|
63
|
+
const stored = (lists.get(TracingDemoService.RECENT_KEY) ?? []).map((s) => JSON.parse(s))
|
|
64
|
+
expect(stored[0]).toMatchObject({ traceId: 'abc', spanId: 'def', route: 'GET /demo/tracing' })
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('trace_id 가 없으면(트레이싱 OFF) no-op + null', async () => {
|
|
68
|
+
vi.mocked(MegaTracing.currentTraceIds).mockReturnValue(null)
|
|
69
|
+
const { ctx, lists } = makeCtx()
|
|
70
|
+
const svc = new TracingDemoService(/** @type {any} */ (ctx))
|
|
71
|
+
const id = await svc.record('x')
|
|
72
|
+
expect(id).toBeNull()
|
|
73
|
+
expect(lists.get(TracingDemoService.RECENT_KEY)).toBeUndefined()
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('TracingDemoService — snapshot', () => {
|
|
78
|
+
test('enabled/current/recent/zipkinBase 를 돌려준다', async () => {
|
|
79
|
+
vi.mocked(MegaTracing.isEnabled).mockReturnValue(true)
|
|
80
|
+
vi.mocked(MegaTracing.currentTraceIds).mockReturnValue({ traceId: 't1', spanId: 's1' })
|
|
81
|
+
const { ctx } = makeCtx()
|
|
82
|
+
const svc = new TracingDemoService(/** @type {any} */ (ctx))
|
|
83
|
+
await svc.record('GET /demo/tracing')
|
|
84
|
+
const snap = await svc.snapshot()
|
|
85
|
+
expect(snap.enabled).toBe(true)
|
|
86
|
+
expect(snap.current).toEqual({ traceId: 't1', spanId: 's1' })
|
|
87
|
+
expect(snap.recent).toHaveLength(1)
|
|
88
|
+
expect(snap.zipkinBase).toMatch(/^https?:\/\//)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* UploadDemoService 단위 테스트 — saveUploads 결과 메타(파일명/크기/MIME)를 redis 이력에 기록하고 snapshot
|
|
4
|
+
* 으로 최신순 반환하는지 검증한다. redis native(lpush/ltrim/lrange)를 가짜로 둔다.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, test, expect, vi } from 'vitest'
|
|
7
|
+
import { UploadDemoService } from '../../../apps/main/services/upload-demo-service.js'
|
|
8
|
+
|
|
9
|
+
function makeCtx() {
|
|
10
|
+
/** @type {Map<string, string[]>} */
|
|
11
|
+
const lists = new Map()
|
|
12
|
+
const native = {
|
|
13
|
+
async lpush(/** @type {string} */ k, /** @type {string} */ v) {
|
|
14
|
+
const arr = lists.get(k) ?? []
|
|
15
|
+
arr.unshift(v)
|
|
16
|
+
lists.set(k, arr)
|
|
17
|
+
return arr.length
|
|
18
|
+
},
|
|
19
|
+
ltrim: vi.fn(async (/** @type {string} */ k, /** @type {number} */ _s, /** @type {number} */ stop) => {
|
|
20
|
+
lists.set(k, (lists.get(k) ?? []).slice(0, stop + 1))
|
|
21
|
+
return 'OK'
|
|
22
|
+
}),
|
|
23
|
+
async lrange(/** @type {string} */ k, /** @type {number} */ s, /** @type {number} */ e) {
|
|
24
|
+
return (lists.get(k) ?? []).slice(s, e + 1)
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
const ctx = { log: { debug() {} }, cache: (/** @type {string} */ a) => (a === 'demo' ? { native } : null) }
|
|
28
|
+
return { ctx, lists }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('UploadDemoService — record', () => {
|
|
32
|
+
test('파일별로 메타를 이력 머리에 쌓는다(최신이 앞)', async () => {
|
|
33
|
+
const { ctx, lists } = makeCtx()
|
|
34
|
+
const svc = new UploadDemoService(/** @type {any} */ (ctx))
|
|
35
|
+
await svc.record([
|
|
36
|
+
{ filename: 'a.png', bytes: 100, mimetype: 'image/png' },
|
|
37
|
+
{ filename: 'b.pdf', bytes: 200, mimetype: 'application/pdf' },
|
|
38
|
+
])
|
|
39
|
+
const stored = (lists.get(UploadDemoService.RECENT_KEY) ?? []).map((s) => JSON.parse(s))
|
|
40
|
+
expect(stored[0]).toMatchObject({ filename: 'b.pdf', bytes: 200, mimetype: 'application/pdf' })
|
|
41
|
+
expect(stored[1]).toMatchObject({ filename: 'a.png' })
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('빈 배열은 no-op', async () => {
|
|
45
|
+
const { ctx, lists } = makeCtx()
|
|
46
|
+
const svc = new UploadDemoService(/** @type {any} */ (ctx))
|
|
47
|
+
await svc.record([])
|
|
48
|
+
expect(lists.get(UploadDemoService.RECENT_KEY)).toBeUndefined()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('UploadDemoService — snapshot', () => {
|
|
53
|
+
test('최근 업로드 목록을 최신순으로 돌려준다', async () => {
|
|
54
|
+
const { ctx } = makeCtx()
|
|
55
|
+
const svc = new UploadDemoService(/** @type {any} */ (ctx))
|
|
56
|
+
await svc.record([{ filename: 'a.txt', bytes: 10, mimetype: 'text/plain' }])
|
|
57
|
+
const snap = await svc.snapshot()
|
|
58
|
+
expect(snap.recent).toHaveLength(1)
|
|
59
|
+
expect(snap.recent[0]).toMatchObject({ filename: 'a.txt', bytes: 10, mimetype: 'text/plain' })
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* UserService 단위 테스트 — 모델(static)을 스파이로 갈음해 DB 없이 비즈니스 로직(검증·404·409 매핑)을 검증.
|
|
4
|
+
* 실 postgres CRUD 는 레포 E2E(scaffold/sample 검증)에서 확인.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, test, expect, vi, afterEach } from 'vitest'
|
|
7
|
+
import { UserService } from '../../../apps/main/services/user-service.js'
|
|
8
|
+
import { User } from '../../../apps/main/models/user.js'
|
|
9
|
+
|
|
10
|
+
/** @returns {UserService} */
|
|
11
|
+
function makeService() {
|
|
12
|
+
return new UserService(/** @type {any} */ ({ log: { debug() {} } }))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
afterEach(() => vi.restoreAllMocks())
|
|
16
|
+
|
|
17
|
+
describe('UserService', () => {
|
|
18
|
+
test('list → User.list 위임', async () => {
|
|
19
|
+
vi.spyOn(User, 'list').mockResolvedValue([/** @type {any} */ ({ id: 1, name: 'a', email: 'a@b.c' })])
|
|
20
|
+
expect(await makeService().list()).toEqual([{ id: 1, name: 'a', email: 'a@b.c' }])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('get — 존재하면 반환', async () => {
|
|
24
|
+
vi.spyOn(User, 'findById').mockResolvedValue(/** @type {any} */ ({ id: 7 }))
|
|
25
|
+
expect(await makeService().get(7)).toEqual({ id: 7 })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('get — 없으면 MegaNotFoundError(404)', async () => {
|
|
29
|
+
vi.spyOn(User, 'findById').mockResolvedValue(null)
|
|
30
|
+
await expect(makeService().get(99)).rejects.toMatchObject({ status: 404, code: 'user.not_found' })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('create — name/email 누락 시 MegaValidationError(400)', async () => {
|
|
34
|
+
await expect(makeService().create({ name: '', email: '' })).rejects.toMatchObject({ status: 400, code: 'user.invalid' })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('create — 정상 생성', async () => {
|
|
38
|
+
const spy = vi.spyOn(User, 'create').mockResolvedValue(/** @type {any} */ ({ id: 1, name: 'a', email: 'a@b.c' }))
|
|
39
|
+
const r = await makeService().create({ name: ' a ', email: ' a@b.c ' })
|
|
40
|
+
expect(r).toEqual({ id: 1, name: 'a', email: 'a@b.c' })
|
|
41
|
+
expect(spy).toHaveBeenCalledWith({ name: 'a', email: 'a@b.c' }) // trim 적용
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('create — postgres 23505 → MegaConflictError(409)', async () => {
|
|
45
|
+
vi.spyOn(User, 'create').mockRejectedValue(Object.assign(new Error('dup'), { code: '23505' }))
|
|
46
|
+
await expect(makeService().create({ name: 'a', email: 'a@b.c' })).rejects.toMatchObject({ status: 409, code: 'user.email_taken' })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('update — 없으면 404', async () => {
|
|
50
|
+
vi.spyOn(User, 'update').mockResolvedValue(null)
|
|
51
|
+
await expect(makeService().update(99, { name: 'x' })).rejects.toMatchObject({ status: 404 })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('update — postgres 23505 → MegaConflictError(409)', async () => {
|
|
55
|
+
vi.spyOn(User, 'update').mockRejectedValue(Object.assign(new Error('dup'), { code: '23505' }))
|
|
56
|
+
await expect(makeService().update(1, { email: 'taken@b.c' })).rejects.toMatchObject({ status: 409, code: 'user.email_taken' })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('remove — 없으면 404, 있으면 deleted', async () => {
|
|
60
|
+
const spy = vi.spyOn(User, 'remove').mockResolvedValue(false)
|
|
61
|
+
await expect(makeService().remove(99)).rejects.toMatchObject({ status: 404 })
|
|
62
|
+
spy.mockResolvedValue(true)
|
|
63
|
+
expect(await makeService().remove(1)).toEqual({ deleted: true })
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* WS 채팅 + ASP 통합 테스트(ADR-158) — 실 sample/crud 부팅(listen) + ASP `ws` 클라이언트.
|
|
4
|
+
*
|
|
5
|
+
* 검증:
|
|
6
|
+
* - WS upgrade 세션 인증(makeWsRequireAuth/readSession) — 비로그인 401, 로그인 통과.
|
|
7
|
+
* - ASP E: 프레임 round-trip — 클라가 envelope `{v,id,type,ts,payload}` 를 wsEncrypt(E:) 로 보내고
|
|
8
|
+
* 서버가 복호→dispatch→E: 로 broadcast. (이 ws 클라의 wire 는 WASM MegaSocket `protocol:'envelope'`
|
|
9
|
+
* 모드와 byte 동일 — frame_encrypt==wsEncrypt, envelope 동일, ADR-160.)
|
|
10
|
+
* - broadcast — 두 로그인 클라가 같은 채널에서 서로의 메시지를 받는다.
|
|
11
|
+
*
|
|
12
|
+
* 인프라(pg·redis·mongo) + ASP_MASTER_SECRET env 가 없으면 통째로 skip.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, test, expect, beforeAll, afterAll } from 'vitest'
|
|
15
|
+
import { WebSocket } from 'ws'
|
|
16
|
+
import { bootApp, MegaShutdown, createWsMessage } from 'mega-framework'
|
|
17
|
+
import { MegaAspCrypto } from 'mega-framework/lib'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
19
|
+
import { dirname, resolve } from 'node:path'
|
|
20
|
+
import { User } from '../../../apps/main/models/user.js'
|
|
21
|
+
import { closeChatBus, ROSTER_KEY } from '../../../apps/main/channels/chat-bus.js'
|
|
22
|
+
|
|
23
|
+
const { wsEncrypt, wsDecrypt } = MegaAspCrypto
|
|
24
|
+
const PROJECT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')
|
|
25
|
+
|
|
26
|
+
const WS_PATH = '/ws/chat'
|
|
27
|
+
const HOSTNAME = 'localhost' // Host → 호스트 라우팅 + ASP domain 유도값(둘 다 'localhost').
|
|
28
|
+
const UA = 'MegaChatTest/1.0' // ASP 키 유도용 — 연결 헤더와 wsEncrypt 가 동일해야 복호 성공.
|
|
29
|
+
const SECRET = process.env.ASP_MASTER_SECRET ?? ''
|
|
30
|
+
|
|
31
|
+
const hasInfra = Boolean(
|
|
32
|
+
process.env.DATABASE_URL &&
|
|
33
|
+
process.env.REDIS_SESSION_URL &&
|
|
34
|
+
process.env.REDIS_RATE_URL &&
|
|
35
|
+
process.env.REDIS_DEMO_URL &&
|
|
36
|
+
process.env.MONGO_URL &&
|
|
37
|
+
process.env.SESSION_SECRET &&
|
|
38
|
+
SECRET,
|
|
39
|
+
)
|
|
40
|
+
const d = hasInfra ? describe : describe.skip
|
|
41
|
+
|
|
42
|
+
/** set-cookie → 쿠키 jar. @param {any} res @param {Record<string,string>} jar */
|
|
43
|
+
function applyCookies(res, jar) {
|
|
44
|
+
const raw = res.headers['set-cookie']
|
|
45
|
+
const arr = Array.isArray(raw) ? raw : raw ? [raw] : []
|
|
46
|
+
for (const c of arr) {
|
|
47
|
+
const pair = String(c).split(';')[0]
|
|
48
|
+
const eq = pair.indexOf('=')
|
|
49
|
+
if (eq === -1) continue
|
|
50
|
+
const name = pair.slice(0, eq).trim()
|
|
51
|
+
const val = pair.slice(eq + 1).trim()
|
|
52
|
+
if (val === '') delete jar[name]
|
|
53
|
+
else jar[name] = val
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @param {Record<string,string>} jar @returns {string} */
|
|
58
|
+
const cookieHeader = (jar) => Object.entries(jar).map(([k, v]) => `${k}=${v}`).join('; ')
|
|
59
|
+
|
|
60
|
+
/** @param {string} html @returns {string} */
|
|
61
|
+
function csrfFrom(html) {
|
|
62
|
+
const m = /name="_csrf" value="([^"]+)"/.exec(html)
|
|
63
|
+
if (!m) throw new Error('csrf token not found')
|
|
64
|
+
return m[1]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @param {Record<string,string>} fields @returns {string} */
|
|
68
|
+
const form = (fields) =>
|
|
69
|
+
Object.entries(fields).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&')
|
|
70
|
+
|
|
71
|
+
/** 회원가입(→자동 로그인)으로 세션 쿠키 jar 확보. @param {any} fastify @param {string} email @returns {Promise<Record<string,string>>} */
|
|
72
|
+
async function registerUser(fastify, email) {
|
|
73
|
+
const jar = {}
|
|
74
|
+
const reg = await fastify.inject({ method: 'GET', url: '/register' })
|
|
75
|
+
applyCookies(reg, jar)
|
|
76
|
+
const done = await fastify.inject({
|
|
77
|
+
method: 'POST',
|
|
78
|
+
url: '/register',
|
|
79
|
+
headers: { cookie: cookieHeader(jar), 'content-type': 'application/x-www-form-urlencoded' },
|
|
80
|
+
payload: form({ _csrf: csrfFrom(reg.body), name: `User ${email}`, email, password: 'secret-pass-123' }),
|
|
81
|
+
})
|
|
82
|
+
applyCookies(done, jar)
|
|
83
|
+
return jar
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* ASP envelope ws 클라이언트(WASM `protocol:'envelope'` 모드 byte-equivalent).
|
|
88
|
+
* @param {number} port @param {Record<string,string>} jar
|
|
89
|
+
* @returns {Promise<any>}
|
|
90
|
+
*/
|
|
91
|
+
function openClient(port, jar) {
|
|
92
|
+
const url = `ws://${HOSTNAME}:${port}${WS_PATH}`
|
|
93
|
+
const socket = /** @type {any} */ (
|
|
94
|
+
new WebSocket(url, /** @type {any} */ ({ headers: { 'user-agent': UA, cookie: cookieHeader(jar) } }))
|
|
95
|
+
)
|
|
96
|
+
/** @type {object[]} */ const queue = []
|
|
97
|
+
/** @type {Array<(m: object) => void>} */ const waiters = []
|
|
98
|
+
socket.lastEncrypted = false
|
|
99
|
+
socket.on('message', (/** @type {Buffer} */ data) => {
|
|
100
|
+
const frame = data.toString('utf8')
|
|
101
|
+
socket.lastEncrypted = frame.startsWith('E:')
|
|
102
|
+
const plain = frame.startsWith('E:')
|
|
103
|
+
? wsDecrypt(SECRET, HOSTNAME, WS_PATH, UA, frame.slice(2))
|
|
104
|
+
: frame.startsWith('P:')
|
|
105
|
+
? frame.slice(2)
|
|
106
|
+
: frame
|
|
107
|
+
const msg = JSON.parse(plain)
|
|
108
|
+
const w = waiters.shift()
|
|
109
|
+
if (w) w(msg)
|
|
110
|
+
else queue.push(msg)
|
|
111
|
+
})
|
|
112
|
+
/** 다음 메시지 1건(평문 envelope). */
|
|
113
|
+
socket.next = () =>
|
|
114
|
+
new Promise((res) => {
|
|
115
|
+
const m = queue.shift()
|
|
116
|
+
if (m !== undefined) res(m)
|
|
117
|
+
else waiters.push(res)
|
|
118
|
+
})
|
|
119
|
+
/** type 이 t 인 다음 메시지까지 소비. @param {string} t */
|
|
120
|
+
socket.nextOf = async (t) => {
|
|
121
|
+
for (let i = 0; i < 20; i++) {
|
|
122
|
+
const m = await socket.next()
|
|
123
|
+
if (m.type === t) return m
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`message type '${t}' not received`)
|
|
126
|
+
}
|
|
127
|
+
/** E: 암호 envelope 송신. @param {string} type @param {object} payload */
|
|
128
|
+
socket.sendChat = (type, payload) => {
|
|
129
|
+
const env = createWsMessage({ type, payload })
|
|
130
|
+
socket.send(`E:${wsEncrypt(SECRET, HOSTNAME, WS_PATH, UA, JSON.stringify(env))}`)
|
|
131
|
+
}
|
|
132
|
+
return new Promise((res, rej) => {
|
|
133
|
+
socket.once('open', () => res(socket))
|
|
134
|
+
socket.once('error', rej)
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
d('WS 채팅 + ASP E2E — sample/crud 실 인프라 (ADR-158)', () => {
|
|
139
|
+
/** @type {Awaited<ReturnType<typeof bootApp>>} */ let boot
|
|
140
|
+
/** @type {any} */ let fastify
|
|
141
|
+
/** @type {number} */ let port
|
|
142
|
+
/** @type {Record<string,string>} */ let jarA
|
|
143
|
+
/** @type {Record<string,string>} */ let jarB
|
|
144
|
+
const emailA = `ws-a-${Date.now()}@example.com`
|
|
145
|
+
const emailB = `ws-b-${Date.now()}@example.com`
|
|
146
|
+
|
|
147
|
+
beforeAll(async () => {
|
|
148
|
+
MegaShutdown._reset()
|
|
149
|
+
boot = await bootApp(PROJECT, { listen: true, port: 0, host: '127.0.0.1' })
|
|
150
|
+
const app = boot.megaApps.find((a) => a.name === 'main')
|
|
151
|
+
fastify = app?.fastify
|
|
152
|
+
port = /** @type {any} */ (boot.server)._httpServer.address().port
|
|
153
|
+
await User.query(
|
|
154
|
+
'CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT now())',
|
|
155
|
+
)
|
|
156
|
+
await User.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash TEXT')
|
|
157
|
+
await User.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ')
|
|
158
|
+
await boot.ctx.cache('demo').del(ROSTER_KEY) // 결정적 접속자 명단.
|
|
159
|
+
jarA = await registerUser(fastify, emailA)
|
|
160
|
+
jarB = await registerUser(fastify, emailB)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
afterAll(async () => {
|
|
164
|
+
if (!boot) return
|
|
165
|
+
await closeChatBus() // redis pub/sub 구독 연결 정리(이벤트루프 누수 방지).
|
|
166
|
+
await User.query('DELETE FROM users WHERE email = ANY($1)', [[emailA, emailB]]).catch(() => {})
|
|
167
|
+
await boot.ctx.cache('demo').del(ROSTER_KEY).catch(() => {})
|
|
168
|
+
await boot.server.close().catch(() => {})
|
|
169
|
+
const app = boot.megaApps.find((a) => a.name === 'main')
|
|
170
|
+
await app?.sessionStore?.disconnect().catch(() => {})
|
|
171
|
+
await boot.ctx.cache('rate').disconnect().catch(() => {})
|
|
172
|
+
await boot.ctx.cache('demo').disconnect().catch(() => {})
|
|
173
|
+
await boot.ctx.db('mongo').disconnect().catch(() => {})
|
|
174
|
+
await boot.ctx.db('primary').disconnect().catch(() => {})
|
|
175
|
+
MegaShutdown._reset()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('비로그인 WS upgrade 는 401 로 거부된다', async () => {
|
|
179
|
+
const url = `ws://${HOSTNAME}:${port}${WS_PATH}`
|
|
180
|
+
const status = await new Promise((res, rej) => {
|
|
181
|
+
const s = new WebSocket(url, /** @type {any} */ ({ headers: { 'user-agent': UA } }))
|
|
182
|
+
s.on('unexpected-response', (_req, response) => {
|
|
183
|
+
res(response.statusCode)
|
|
184
|
+
s.terminate()
|
|
185
|
+
})
|
|
186
|
+
s.on('open', () => {
|
|
187
|
+
s.close()
|
|
188
|
+
rej(new Error('로그인 없이 연결되면 안 됨'))
|
|
189
|
+
})
|
|
190
|
+
s.on('error', () => {}) // unexpected-response 뒤 따라오는 소켓 에러는 무시.
|
|
191
|
+
})
|
|
192
|
+
expect(status).toBe(401)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('로그인 클라는 연결 직후 ASP E: 로 chat.history 를 받는다', async () => {
|
|
196
|
+
const a = await openClient(port, jarA)
|
|
197
|
+
try {
|
|
198
|
+
const history = await a.nextOf('chat.history')
|
|
199
|
+
expect(a.lastEncrypted).toBe(true) // 수신 프레임이 E:(암호화) 였음.
|
|
200
|
+
expect(history.payload.me.userId).toBeTruthy()
|
|
201
|
+
expect(Array.isArray(history.payload.items)).toBe(true)
|
|
202
|
+
expect(history.payload.online).toBeGreaterThanOrEqual(1)
|
|
203
|
+
// roster(cluster-wide redis HASH) 에 내 이름이 있고, 워커 PID 가 실린다.
|
|
204
|
+
expect(history.payload.members).toContain(history.payload.me.userName)
|
|
205
|
+
expect(typeof history.payload.workerPid).toBe('number')
|
|
206
|
+
} finally {
|
|
207
|
+
a.close()
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('broadcast — A 가 보낸 메시지를 A(echo)와 B 가 모두 ASP E: 로 받는다', async () => {
|
|
212
|
+
const b = await openClient(port, jarB)
|
|
213
|
+
await b.nextOf('chat.history')
|
|
214
|
+
const a = await openClient(port, jarA)
|
|
215
|
+
await a.nextOf('chat.history')
|
|
216
|
+
// A 입장 → B 는 presence(join) 수신.
|
|
217
|
+
const join = await b.nextOf('chat.presence')
|
|
218
|
+
expect(join.payload.event).toBe('join')
|
|
219
|
+
|
|
220
|
+
const text = `hello-${Date.now()}`
|
|
221
|
+
a.sendChat('chat.send', { text })
|
|
222
|
+
|
|
223
|
+
const onA = await a.nextOf('chat.msg')
|
|
224
|
+
const onB = await b.nextOf('chat.msg')
|
|
225
|
+
expect(onA.payload.text).toBe(text)
|
|
226
|
+
expect(onB.payload.text).toBe(text)
|
|
227
|
+
expect(a.lastEncrypted && b.lastEncrypted).toBe(true) // 양쪽 모두 E: 암호 수신.
|
|
228
|
+
|
|
229
|
+
a.close()
|
|
230
|
+
b.close()
|
|
231
|
+
})
|
|
232
|
+
})
|