@cosmicdrift/kumiko-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/README.md +159 -0
- package/package.json +91 -0
- package/src/__tests__/anonymous-access.integration.ts +325 -0
- package/src/__tests__/error-contract.integration.ts +435 -0
- package/src/__tests__/field-access.integration.ts +269 -0
- package/src/__tests__/full-stack.integration.ts +914 -0
- package/src/__tests__/ownership.integration.ts +449 -0
- package/src/__tests__/reference-data.integration.ts +198 -0
- package/src/__tests__/transition-guard.integration.ts +340 -0
- package/src/api/__tests__/api.test.ts +337 -0
- package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
- package/src/api/__tests__/batch.integration.ts +404 -0
- package/src/api/__tests__/body-limit.test.ts +88 -0
- package/src/api/__tests__/csrf-middleware.test.ts +97 -0
- package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
- package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
- package/src/api/__tests__/nested-write.integration.ts +213 -0
- package/src/api/__tests__/readiness.test.ts +76 -0
- package/src/api/__tests__/request-id-middleware.test.ts +72 -0
- package/src/api/__tests__/sse-broker.test.ts +58 -0
- package/src/api/__tests__/sse-route.test.ts +112 -0
- package/src/api/anonymous-cookie.ts +60 -0
- package/src/api/api-constants.ts +64 -0
- package/src/api/auth-middleware.ts +418 -0
- package/src/api/auth-routes.ts +982 -0
- package/src/api/csrf-middleware.ts +77 -0
- package/src/api/index.ts +31 -0
- package/src/api/jwt.ts +66 -0
- package/src/api/observability-middleware.ts +89 -0
- package/src/api/readiness.ts +132 -0
- package/src/api/request-context.ts +49 -0
- package/src/api/request-id-middleware.ts +50 -0
- package/src/api/route-registrars.ts +195 -0
- package/src/api/routes.ts +135 -0
- package/src/api/server.ts +640 -0
- package/src/api/sse-broker.ts +71 -0
- package/src/api/sse-route.ts +62 -0
- package/src/api/tokens.ts +16 -0
- package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
- package/src/db/__tests__/compound-types.test.ts +114 -0
- package/src/db/__tests__/connection-options.test.ts +68 -0
- package/src/db/__tests__/cursor.test.ts +41 -0
- package/src/db/__tests__/db-helpers.test.ts +369 -0
- package/src/db/__tests__/dialect-instant.test.ts +50 -0
- package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
- package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
- package/src/db/__tests__/encryption.test.ts +39 -0
- package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
- package/src/db/__tests__/event-store-executor.integration.ts +235 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
- package/src/db/__tests__/located-timestamp.test.ts +184 -0
- package/src/db/__tests__/money.test.ts +199 -0
- package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
- package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
- package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
- package/src/db/__tests__/row-helpers.test.ts +59 -0
- package/src/db/__tests__/schema-migration.integration.ts +273 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
- package/src/db/__tests__/table-builder-required.test.ts +216 -0
- package/src/db/__tests__/tenant-db.integration.ts +606 -0
- package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
- package/src/db/apply-entity-event.ts +188 -0
- package/src/db/assert-exists-in.ts +59 -0
- package/src/db/compound-types.ts +47 -0
- package/src/db/connection.ts +104 -0
- package/src/db/cursor.ts +83 -0
- package/src/db/dialect.ts +109 -0
- package/src/db/eagerload.ts +174 -0
- package/src/db/encryption.ts +39 -0
- package/src/db/event-store-executor.ts +906 -0
- package/src/db/index.ts +55 -0
- package/src/db/located-timestamp.ts +114 -0
- package/src/db/money.ts +120 -0
- package/src/db/pg-error.ts +46 -0
- package/src/db/reference-data.ts +77 -0
- package/src/db/row-helpers.ts +53 -0
- package/src/db/schema-inspection.ts +25 -0
- package/src/db/table-builder.ts +475 -0
- package/src/db/tenant-db.ts +434 -0
- package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
- package/src/engine/__tests__/boot-validator.test.ts +1865 -0
- package/src/engine/__tests__/build-app-schema.test.ts +154 -0
- package/src/engine/__tests__/claim-keys.test.ts +274 -0
- package/src/engine/__tests__/config-helpers.test.ts +236 -0
- package/src/engine/__tests__/effective-features.test.ts +86 -0
- package/src/engine/__tests__/engine.test.ts +1461 -0
- package/src/engine/__tests__/entity-handlers.test.ts +274 -0
- package/src/engine/__tests__/event-helpers.test.ts +68 -0
- package/src/engine/__tests__/extends-registrar.test.ts +159 -0
- package/src/engine/__tests__/factories-long-text.test.ts +84 -0
- package/src/engine/__tests__/factories-time.test.ts +158 -0
- package/src/engine/__tests__/field-predicates.test.ts +48 -0
- package/src/engine/__tests__/hook-phases.test.ts +132 -0
- package/src/engine/__tests__/identifiers.test.ts +35 -0
- package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
- package/src/engine/__tests__/nav.test.ts +267 -0
- package/src/engine/__tests__/ownership.test.ts +421 -0
- package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
- package/src/engine/__tests__/projection-helpers.test.ts +62 -0
- package/src/engine/__tests__/projection.test.ts +191 -0
- package/src/engine/__tests__/qualified-name.test.ts +264 -0
- package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
- package/src/engine/__tests__/run-in.test.ts +38 -0
- package/src/engine/__tests__/schema-builder.test.ts +380 -0
- package/src/engine/__tests__/screen.test.ts +408 -0
- package/src/engine/__tests__/state-machine.test.ts +148 -0
- package/src/engine/__tests__/system-user.test.ts +57 -0
- package/src/engine/__tests__/validation-hooks.test.ts +71 -0
- package/src/engine/access.ts +23 -0
- package/src/engine/boot-validator.ts +1528 -0
- package/src/engine/build-app-schema.ts +125 -0
- package/src/engine/config-helpers.ts +115 -0
- package/src/engine/constants.ts +85 -0
- package/src/engine/create-app.ts +98 -0
- package/src/engine/define-feature.ts +702 -0
- package/src/engine/define-handler.ts +78 -0
- package/src/engine/define-roles.ts +19 -0
- package/src/engine/effective-features.ts +87 -0
- package/src/engine/entity-handlers.ts +364 -0
- package/src/engine/event-helpers.ts +73 -0
- package/src/engine/factories.ts +328 -0
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
- package/src/engine/feature-ast/extractors.ts +2562 -0
- package/src/engine/feature-ast/index.ts +105 -0
- package/src/engine/feature-ast/parse.ts +369 -0
- package/src/engine/feature-ast/patch.ts +525 -0
- package/src/engine/feature-ast/patcher.ts +518 -0
- package/src/engine/feature-ast/patterns.ts +434 -0
- package/src/engine/feature-ast/render.ts +602 -0
- package/src/engine/feature-ast/source-location.ts +45 -0
- package/src/engine/field-access.ts +120 -0
- package/src/engine/index.ts +254 -0
- package/src/engine/ownership.ts +337 -0
- package/src/engine/parse-ref-target.ts +22 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
- package/src/engine/pattern-library/index.ts +24 -0
- package/src/engine/pattern-library/library.ts +1117 -0
- package/src/engine/pattern-library/types.ts +255 -0
- package/src/engine/projection-helpers.ts +85 -0
- package/src/engine/qualified-name.ts +122 -0
- package/src/engine/read-claim.ts +31 -0
- package/src/engine/registry.ts +1325 -0
- package/src/engine/resolve-config-or-param.ts +153 -0
- package/src/engine/run-in.ts +29 -0
- package/src/engine/schema-builder.ts +175 -0
- package/src/engine/screen-filter-ops.ts +51 -0
- package/src/engine/state-machine.ts +70 -0
- package/src/engine/system-user.ts +32 -0
- package/src/engine/types/config.ts +306 -0
- package/src/engine/types/event-type-map.ts +37 -0
- package/src/engine/types/feature.ts +574 -0
- package/src/engine/types/fields.ts +422 -0
- package/src/engine/types/handlers.ts +742 -0
- package/src/engine/types/hooks.ts +142 -0
- package/src/engine/types/http-route.ts +54 -0
- package/src/engine/types/identifiers.ts +47 -0
- package/src/engine/types/index.ts +208 -0
- package/src/engine/types/nav.ts +46 -0
- package/src/engine/types/projection.ts +132 -0
- package/src/engine/types/relations.ts +51 -0
- package/src/engine/types/screen.ts +452 -0
- package/src/engine/types/workspace.ts +42 -0
- package/src/engine/validation.ts +33 -0
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
- package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
- package/src/entrypoint/index.ts +442 -0
- package/src/errors/__tests__/classes.test.ts +371 -0
- package/src/errors/__tests__/write-failures.test.ts +109 -0
- package/src/errors/classes.ts +249 -0
- package/src/errors/i18n/de.yaml +83 -0
- package/src/errors/i18n/en.yaml +80 -0
- package/src/errors/index.ts +41 -0
- package/src/errors/kumiko-error.ts +67 -0
- package/src/errors/reasons.ts +36 -0
- package/src/errors/serialize.ts +136 -0
- package/src/errors/transition-details.ts +30 -0
- package/src/errors/write-error-info.ts +123 -0
- package/src/errors/zod-bridge.ts +49 -0
- package/src/event-store/__tests__/admin-api.integration.ts +361 -0
- package/src/event-store/__tests__/event-store.integration.ts +584 -0
- package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
- package/src/event-store/__tests__/perf.integration.ts +255 -0
- package/src/event-store/__tests__/snapshot.integration.ts +267 -0
- package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
- package/src/event-store/__tests__/upcaster.integration.ts +460 -0
- package/src/event-store/admin-api.ts +257 -0
- package/src/event-store/archive.ts +106 -0
- package/src/event-store/errors.ts +35 -0
- package/src/event-store/event-store.ts +405 -0
- package/src/event-store/events-schema.ts +90 -0
- package/src/event-store/index.ts +50 -0
- package/src/event-store/snapshot.ts +210 -0
- package/src/event-store/upcaster-dead-letter.ts +119 -0
- package/src/event-store/upcaster.ts +147 -0
- package/src/files/__tests__/content-disposition.test.ts +123 -0
- package/src/files/__tests__/file-field-column.integration.ts +103 -0
- package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
- package/src/files/__tests__/file-handle.test.ts +122 -0
- package/src/files/__tests__/files.integration.ts +830 -0
- package/src/files/__tests__/storage-tracking.integration.ts +153 -0
- package/src/files/content-disposition.ts +55 -0
- package/src/files/file-handle.ts +63 -0
- package/src/files/file-ref-table.ts +22 -0
- package/src/files/file-routes.ts +353 -0
- package/src/files/in-memory-provider.ts +62 -0
- package/src/files/index.ts +29 -0
- package/src/files/local-provider.ts +35 -0
- package/src/files/storage-tracking.ts +60 -0
- package/src/files/types.ts +118 -0
- package/src/i18n/__tests__/i18n.test.ts +72 -0
- package/src/i18n/index.ts +29 -0
- package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
- package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
- package/src/jobs/__tests__/jobs.integration.ts +566 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-runner.ts +574 -0
- package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
- package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
- package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
- package/src/lifecycle/index.ts +13 -0
- package/src/lifecycle/lifecycle.ts +160 -0
- package/src/lifecycle/signal-handlers.ts +62 -0
- package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
- package/src/logging/index.ts +3 -0
- package/src/logging/pino-logger.ts +64 -0
- package/src/logging/types.ts +7 -0
- package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
- package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
- package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
- package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
- package/src/migrations/index.ts +28 -0
- package/src/migrations/projection-detection.ts +149 -0
- package/src/migrations/rebuild-marker.ts +64 -0
- package/src/migrations/schema-drift.ts +395 -0
- package/src/observability/__tests__/console-provider.test.ts +67 -0
- package/src/observability/__tests__/metric-validator.test.ts +87 -0
- package/src/observability/__tests__/noop-provider.test.ts +82 -0
- package/src/observability/__tests__/observability.integration.ts +559 -0
- package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
- package/src/observability/__tests__/recording-meter.test.ts +101 -0
- package/src/observability/__tests__/recording-tracer.test.ts +110 -0
- package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
- package/src/observability/console-provider.ts +130 -0
- package/src/observability/context.ts +26 -0
- package/src/observability/fallback.ts +34 -0
- package/src/observability/ids.ts +25 -0
- package/src/observability/index.ts +79 -0
- package/src/observability/metric-validator.ts +86 -0
- package/src/observability/metrics-handle.ts +56 -0
- package/src/observability/noop-provider.ts +146 -0
- package/src/observability/prometheus-meter.ts +284 -0
- package/src/observability/recording-meter.ts +156 -0
- package/src/observability/recording-tracer.ts +198 -0
- package/src/observability/redis-wrapper.ts +132 -0
- package/src/observability/sensitive-filter.ts +108 -0
- package/src/observability/standard-metrics.ts +213 -0
- package/src/observability/types/index.ts +29 -0
- package/src/observability/types/metric.ts +56 -0
- package/src/observability/types/provider.ts +32 -0
- package/src/observability/types/span.ts +64 -0
- package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
- package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
- package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
- package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
- package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
- package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
- package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
- package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
- package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
- package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
- package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
- package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
- package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
- package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
- package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
- package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
- package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
- package/src/pipeline/append-event-core.ts +117 -0
- package/src/pipeline/auth-claims-resolver.ts +103 -0
- package/src/pipeline/cascade-handler.ts +113 -0
- package/src/pipeline/dispatcher.ts +1585 -0
- package/src/pipeline/distributed-lock.ts +37 -0
- package/src/pipeline/entity-cache.ts +113 -0
- package/src/pipeline/event-consumer-state.ts +108 -0
- package/src/pipeline/event-dedup.ts +23 -0
- package/src/pipeline/event-dispatcher.ts +1016 -0
- package/src/pipeline/event-retention.ts +154 -0
- package/src/pipeline/idempotency.ts +76 -0
- package/src/pipeline/index.ts +66 -0
- package/src/pipeline/lifecycle-pipeline.ts +409 -0
- package/src/pipeline/msp-rebuild.ts +242 -0
- package/src/pipeline/multi-stream-apply-context.ts +115 -0
- package/src/pipeline/projection-rebuild.ts +334 -0
- package/src/pipeline/projection-state.ts +72 -0
- package/src/pipeline/projections-runner.ts +56 -0
- package/src/pipeline/redis-keys.ts +11 -0
- package/src/pipeline/system-hooks.ts +190 -0
- package/src/random/__tests__/generate.test.ts +149 -0
- package/src/random/generate.ts +141 -0
- package/src/random/index.ts +8 -0
- package/src/random/words.ts +392 -0
- package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
- package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
- package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
- package/src/rate-limit/bucket.ts +36 -0
- package/src/rate-limit/index.ts +14 -0
- package/src/rate-limit/middleware.ts +152 -0
- package/src/rate-limit/resolver.ts +267 -0
- package/src/redis/__tests__/redis-options.test.ts +54 -0
- package/src/redis/index.ts +74 -0
- package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
- package/src/search/__tests__/search-adapter.test.ts +256 -0
- package/src/search/in-memory-adapter.ts +123 -0
- package/src/search/index.ts +12 -0
- package/src/search/meilisearch-adapter.ts +106 -0
- package/src/search/types.ts +39 -0
- package/src/secrets/__tests__/dek-cache.test.ts +213 -0
- package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
- package/src/secrets/__tests__/envelope.test.ts +74 -0
- package/src/secrets/__tests__/leak-guard.test.ts +92 -0
- package/src/secrets/__tests__/rotation.test.ts +149 -0
- package/src/secrets/dek-cache.ts +116 -0
- package/src/secrets/env-master-key-provider.ts +162 -0
- package/src/secrets/envelope.ts +55 -0
- package/src/secrets/index.ts +19 -0
- package/src/secrets/leak-guard.ts +87 -0
- package/src/secrets/rotation.ts +34 -0
- package/src/secrets/types.ts +107 -0
- package/src/stack/db.ts +104 -0
- package/src/stack/event-collector.ts +23 -0
- package/src/stack/index.ts +32 -0
- package/src/stack/redis.ts +44 -0
- package/src/stack/request-helper.ts +168 -0
- package/src/stack/table-helpers.ts +104 -0
- package/src/stack/test-stack.ts +357 -0
- package/src/stack/test-users.ts +37 -0
- package/src/testing/__tests__/e2e-generator.test.ts +230 -0
- package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
- package/src/testing/access-assertions.ts +15 -0
- package/src/testing/assertions.ts +35 -0
- package/src/testing/e2e-generator.ts +465 -0
- package/src/testing/expect-error.ts +25 -0
- package/src/testing/handler-context.ts +125 -0
- package/src/testing/http-cookies.ts +52 -0
- package/src/testing/index.ts +41 -0
- package/src/testing/late-bound.ts +39 -0
- package/src/testing/mutable-master-key-provider.ts +31 -0
- package/src/testing/observability-recorder.ts +54 -0
- package/src/testing/shared-entities.ts +49 -0
- package/src/testing/utils.ts +1 -0
- package/src/testing/wait-for.ts +31 -0
- package/src/time/__tests__/polyfill.test.ts +73 -0
- package/src/time/__tests__/tz-context.test.ts +121 -0
- package/src/time/index.ts +21 -0
- package/src/time/polyfill.ts +70 -0
- package/src/time/tz-context.ts +107 -0
- package/src/ui-types/app-schema.ts +57 -0
- package/src/ui-types/index.ts +65 -0
- package/src/utils/__tests__/assert.test.ts +17 -0
- package/src/utils/__tests__/env-parse.test.ts +54 -0
- package/src/utils/assert.ts +18 -0
- package/src/utils/env-parse.ts +16 -0
- package/src/utils/ids.ts +16 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/safe-json.ts +30 -0
- package/src/utils/serialization.ts +7 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createEntity, createRegistry, createTextField, defineFeature } from "../../engine";
|
|
4
|
+
import { buildServer } from "../server";
|
|
5
|
+
|
|
6
|
+
const JWT_SECRET = "test-secret-at-least-32-chars-long!!";
|
|
7
|
+
|
|
8
|
+
const testFeature = defineFeature("blob", (r) => {
|
|
9
|
+
r.entity("note", createEntity({ table: "Notes", fields: { body: createTextField() } }));
|
|
10
|
+
r.writeHandler(
|
|
11
|
+
"note:create",
|
|
12
|
+
z.object({ body: z.string() }),
|
|
13
|
+
async (event) => ({ isSuccess: true, data: { body: event.payload.body } }),
|
|
14
|
+
{ access: { openToAll: true } },
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function buildApp(maxRequestBytes?: number) {
|
|
19
|
+
const registry = createRegistry([testFeature]);
|
|
20
|
+
return buildServer({
|
|
21
|
+
registry,
|
|
22
|
+
context: {},
|
|
23
|
+
jwtSecret: JWT_SECRET,
|
|
24
|
+
maxRequestBytes,
|
|
25
|
+
}).app;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function postJson(app: ReturnType<typeof buildApp>, path: string, bytes: number) {
|
|
29
|
+
const body = JSON.stringify({ body: "x".repeat(bytes) });
|
|
30
|
+
return app.request(path, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
"Content-Length": String(body.length),
|
|
35
|
+
},
|
|
36
|
+
body,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("request body limit", () => {
|
|
41
|
+
test("rejects POST with body larger than maxRequestBytes with 413", async () => {
|
|
42
|
+
const app = buildApp(1024);
|
|
43
|
+
const res = await postJson(app, "/api/write", 2048);
|
|
44
|
+
expect(res.status).toBe(413);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("accepts POST with body within the limit (reaches auth layer)", async () => {
|
|
48
|
+
const app = buildApp(10_000);
|
|
49
|
+
const res = await postJson(app, "/api/write", 100);
|
|
50
|
+
// No JWT → 401. Point is: NOT 413.
|
|
51
|
+
expect(res.status).toBe(401);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("default limit rejects absurdly large payloads", async () => {
|
|
55
|
+
const app = buildApp(); // default 1 MB
|
|
56
|
+
const res = await postJson(app, "/api/write", 2_000_000); // 2 MB
|
|
57
|
+
expect(res.status).toBe(413);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("default limit accepts small payloads", async () => {
|
|
61
|
+
const app = buildApp();
|
|
62
|
+
const res = await postJson(app, "/api/write", 500);
|
|
63
|
+
expect(res.status).toBe(401); // auth required, but size is fine
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("limit is not applied to /api/files (uploads have their own cap)", async () => {
|
|
67
|
+
// /api/files isn't mounted on this test app (no storageProvider), so a POST
|
|
68
|
+
// results in 404 — the point is: NOT 413. A payload that exceeds the JSON
|
|
69
|
+
// cap must still reach the route layer for the files router to decide.
|
|
70
|
+
const app = buildApp(1024);
|
|
71
|
+
const body = JSON.stringify({ body: "x".repeat(4096) });
|
|
72
|
+
const res = await app.request("/api/files", {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: {
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
"Content-Length": String(body.length),
|
|
77
|
+
},
|
|
78
|
+
body,
|
|
79
|
+
});
|
|
80
|
+
expect(res.status).not.toBe(413);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("maxRequestBytes=0 disables the cap entirely", async () => {
|
|
84
|
+
const app = buildApp(0);
|
|
85
|
+
const res = await postJson(app, "/api/write", 50_000);
|
|
86
|
+
expect(res.status).toBe(401); // passes body-limit, reaches auth
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// csrf-middleware: double-submit token check against a Hono app that
|
|
2
|
+
// layers authMiddleware → csrfMiddleware → handler. Covers the paths that
|
|
3
|
+
// matter in production: cookie + state-changing, cookie + safe, bearer.
|
|
4
|
+
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import { TestUsers } from "../../stack";
|
|
8
|
+
import {
|
|
9
|
+
AUTH_COOKIE_NAME,
|
|
10
|
+
authMiddleware,
|
|
11
|
+
CSRF_COOKIE_NAME,
|
|
12
|
+
CSRF_HEADER_NAME,
|
|
13
|
+
} from "../auth-middleware";
|
|
14
|
+
import { csrfMiddleware } from "../csrf-middleware";
|
|
15
|
+
import { createJwtHelper } from "../jwt";
|
|
16
|
+
|
|
17
|
+
const JWT_SECRET = "csrf-middleware-test-secret-min-32-characters-long";
|
|
18
|
+
const CSRF = "csrf-token-fixed-for-test";
|
|
19
|
+
|
|
20
|
+
async function buildApp(): Promise<{ app: Hono; token: string }> {
|
|
21
|
+
const jwt = createJwtHelper(JWT_SECRET);
|
|
22
|
+
const token = await jwt.sign(TestUsers.user);
|
|
23
|
+
const app = new Hono();
|
|
24
|
+
app.use("/api/*", authMiddleware(jwt));
|
|
25
|
+
app.use("/api/*", csrfMiddleware());
|
|
26
|
+
app.get("/api/ping", (c) => c.json({ ok: true }));
|
|
27
|
+
app.post("/api/write", (c) => c.json({ ok: true }));
|
|
28
|
+
return { app, token };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("csrf-middleware", () => {
|
|
32
|
+
test("bearer transport skips csrf check even on POST", async () => {
|
|
33
|
+
const { app, token } = await buildApp();
|
|
34
|
+
const res = await app.request("/api/write", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
37
|
+
});
|
|
38
|
+
expect(res.status).toBe(200);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("cookie transport + GET → no csrf check (safe method)", async () => {
|
|
42
|
+
const { app, token } = await buildApp();
|
|
43
|
+
const res = await app.request("/api/ping", {
|
|
44
|
+
headers: { Cookie: `${AUTH_COOKIE_NAME}=${token}` },
|
|
45
|
+
});
|
|
46
|
+
expect(res.status).toBe(200);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("cookie transport + POST + matching csrf → ok", async () => {
|
|
50
|
+
const { app, token } = await buildApp();
|
|
51
|
+
const res = await app.request("/api/write", {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
Cookie: `${AUTH_COOKIE_NAME}=${token}; ${CSRF_COOKIE_NAME}=${CSRF}`,
|
|
55
|
+
[CSRF_HEADER_NAME]: CSRF,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
expect(res.status).toBe(200);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("cookie transport + POST + missing header → 403", async () => {
|
|
62
|
+
const { app, token } = await buildApp();
|
|
63
|
+
const res = await app.request("/api/write", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: {
|
|
66
|
+
Cookie: `${AUTH_COOKIE_NAME}=${token}; ${CSRF_COOKIE_NAME}=${CSRF}`,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
expect(res.status).toBe(403);
|
|
70
|
+
const body = (await res.json()) as { error: { code: string } };
|
|
71
|
+
expect(body.error.code).toBe("csrf_token_mismatch");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("cookie transport + POST + wrong header → 403", async () => {
|
|
75
|
+
const { app, token } = await buildApp();
|
|
76
|
+
const res = await app.request("/api/write", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: {
|
|
79
|
+
Cookie: `${AUTH_COOKIE_NAME}=${token}; ${CSRF_COOKIE_NAME}=${CSRF}`,
|
|
80
|
+
[CSRF_HEADER_NAME]: "wrong-value",
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
expect(res.status).toBe(403);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("cookie transport + POST + missing csrf cookie → 403", async () => {
|
|
87
|
+
const { app, token } = await buildApp();
|
|
88
|
+
const res = await app.request("/api/write", {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
Cookie: `${AUTH_COOKIE_NAME}=${token}`,
|
|
92
|
+
[CSRF_HEADER_NAME]: CSRF,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
expect(res.status).toBe(403);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { createLiveDispatcher } from "@cosmicdrift/kumiko-dispatcher-live";
|
|
2
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { generateToken } from "../../api/tokens";
|
|
5
|
+
import { createEventStoreExecutor } from "../../db/event-store-executor";
|
|
6
|
+
import { buildDrizzleTable } from "../../db/table-builder";
|
|
7
|
+
import { createEntity, createTextField, defineFeature } from "../../engine";
|
|
8
|
+
import { createEntityTable, setupTestStack, type TestStack, TestUsers } from "../../stack";
|
|
9
|
+
import { generateId } from "../../utils";
|
|
10
|
+
|
|
11
|
+
// End-to-end: UI code would call `dispatcher.write("feat:write:item:create", ...)`.
|
|
12
|
+
// This test wires dispatcher-live against the real Kumiko HTTP stack via
|
|
13
|
+
// Hono's `app.request()` (in-memory, no port) and proves the whole path:
|
|
14
|
+
// dispatcher-live → JSON body + headers → Hono route → dispatcher →
|
|
15
|
+
// write-handler → crud-executor → DB → response → dispatcher-live →
|
|
16
|
+
// typed WriteResult.
|
|
17
|
+
//
|
|
18
|
+
// Server-side CSRF middleware is enabled by the normal server config, so
|
|
19
|
+
// the dispatcher must carry X-CSRF-Token correctly or these writes would
|
|
20
|
+
// land as 403. The test proves that wiring end-to-end.
|
|
21
|
+
|
|
22
|
+
const itemEntity = createEntity({
|
|
23
|
+
table: "dispatcher_live_items",
|
|
24
|
+
fields: { name: createTextField({ required: true }) },
|
|
25
|
+
});
|
|
26
|
+
const itemTable = buildDrizzleTable("item", itemEntity);
|
|
27
|
+
|
|
28
|
+
const itemFeature = defineFeature("dlive", (r) => {
|
|
29
|
+
r.entity("item", itemEntity);
|
|
30
|
+
|
|
31
|
+
r.writeHandler(
|
|
32
|
+
"item:create",
|
|
33
|
+
z.object({ name: z.string().min(1) }),
|
|
34
|
+
async (event, ctx) => {
|
|
35
|
+
const crud = createEventStoreExecutor(itemTable, itemEntity, { entityName: "item" });
|
|
36
|
+
return crud.create(event.payload, event.user, ctx.db);
|
|
37
|
+
},
|
|
38
|
+
{ access: { roles: ["Admin"] } },
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
r.queryHandler(
|
|
42
|
+
"item:list",
|
|
43
|
+
z.object({}).optional(),
|
|
44
|
+
async (_event, ctx) => {
|
|
45
|
+
return ctx.db.select().from(itemTable);
|
|
46
|
+
},
|
|
47
|
+
{ access: { roles: ["Admin"] } },
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
let stack: TestStack;
|
|
52
|
+
const admin = TestUsers.admin;
|
|
53
|
+
|
|
54
|
+
// Wire dispatcher-live's `fetch` to Hono's in-memory `app.request`. Also
|
|
55
|
+
// synthesizes the auth + CSRF cookies a real browser would send: the stack
|
|
56
|
+
// exposes the Hono app, but the normal login-flow-based session-cookie
|
|
57
|
+
// setup isn't in play here — we sign a JWT directly and set both the
|
|
58
|
+
// `kumiko_auth` (HttpOnly JWT) and `kumiko_csrf` cookies by hand. A
|
|
59
|
+
// real browser login does the same server-side via auth-routes.ts.
|
|
60
|
+
//
|
|
61
|
+
// GAP: this means the real POST /auth/login round-trip (Set-Cookie
|
|
62
|
+
// headers, CSRF-cookie generation on the server, SameSite/HttpOnly
|
|
63
|
+
// flags as actually emitted) is NOT exercised here. If we ever change
|
|
64
|
+
// the login endpoint's cookie-setting code, this file will not catch
|
|
65
|
+
// regressions — the dedicated auth-routes integration test owns that
|
|
66
|
+
// coverage. Keep this test focused on dispatcher-live's request-side
|
|
67
|
+
// behaviour (envelope parsing, error mapping, CSRF-header echo).
|
|
68
|
+
//
|
|
69
|
+
// The fetch wrapper echoes the csrf cookie back into the X-CSRF-Token
|
|
70
|
+
// header — that's the real dispatcher-live code path; the test just
|
|
71
|
+
// stages the cookies first.
|
|
72
|
+
async function buildFetch(): Promise<{
|
|
73
|
+
readonly fetch: typeof fetch;
|
|
74
|
+
readonly csrfToken: string;
|
|
75
|
+
readonly authJwt: string;
|
|
76
|
+
}> {
|
|
77
|
+
const authJwt = await stack.jwt.sign(admin);
|
|
78
|
+
const csrfToken = generateToken();
|
|
79
|
+
const cookieHeader = `kumiko_auth=${authJwt}; kumiko_csrf=${csrfToken}`;
|
|
80
|
+
|
|
81
|
+
// Cast via unknown: the native fetch interface (Bun's typing) includes a
|
|
82
|
+
// `preconnect` method we don't need and can't meaningfully implement
|
|
83
|
+
// against Hono's in-memory request handler. dispatcher-live calls the
|
|
84
|
+
// functional shape only — preconnect is a hint, not load-bearing.
|
|
85
|
+
const fetchImpl = (async (url: unknown, init: RequestInit | undefined) => {
|
|
86
|
+
const reqInit: RequestInit = {
|
|
87
|
+
...(init ?? {}),
|
|
88
|
+
headers: {
|
|
89
|
+
...(init?.headers ?? {}),
|
|
90
|
+
Cookie: cookieHeader,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
return stack.app.request(String(url), reqInit);
|
|
94
|
+
}) as unknown as typeof fetch;
|
|
95
|
+
return { fetch: fetchImpl, csrfToken, authJwt };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
beforeAll(async () => {
|
|
99
|
+
stack = await setupTestStack({ features: [itemFeature] });
|
|
100
|
+
await createEntityTable(stack.db, itemEntity);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterAll(async () => {
|
|
104
|
+
await stack.cleanup();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
beforeEach(async () => {
|
|
108
|
+
await stack.db.delete(itemTable);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("dispatcher-live (integration) — full path against Kumiko server", () => {
|
|
112
|
+
test("write: dispatches HTTP, server persists, response maps to typed WriteResult", async () => {
|
|
113
|
+
const { fetch, csrfToken } = await buildFetch();
|
|
114
|
+
const dispatcher = createLiveDispatcher({
|
|
115
|
+
fetch,
|
|
116
|
+
readCsrf: () => csrfToken,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const result = await dispatcher.write<{ data?: { name?: string } }>("dlive:write:item:create", {
|
|
120
|
+
name: "hello-live",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.isSuccess).toBe(true);
|
|
124
|
+
|
|
125
|
+
// Prove the server actually persisted.
|
|
126
|
+
const rows = await stack.db.select().from(itemTable);
|
|
127
|
+
expect(rows).toHaveLength(1);
|
|
128
|
+
expect(rows[0]?.["name"]).toBe("hello-live");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("write: validation failure surfaces as typed DispatcherError with field issues", async () => {
|
|
132
|
+
const { fetch, csrfToken } = await buildFetch();
|
|
133
|
+
const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => csrfToken });
|
|
134
|
+
|
|
135
|
+
const result = await dispatcher.write("dlive:write:item:create", { name: "" });
|
|
136
|
+
|
|
137
|
+
expect(result.isSuccess).toBe(false);
|
|
138
|
+
if (!result.isSuccess) {
|
|
139
|
+
expect(result.error.code).toBe("validation_error");
|
|
140
|
+
const fieldPaths = (result.error.details?.fields ?? []).map((f) => f.path);
|
|
141
|
+
expect(fieldPaths).toContain("name");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("missing CSRF token: server rejects — exercises Vorarbeit-A wiring", async () => {
|
|
146
|
+
const { fetch } = await buildFetch();
|
|
147
|
+
// Dispatcher with no csrf reader — the header won't be sent.
|
|
148
|
+
const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => undefined });
|
|
149
|
+
|
|
150
|
+
const result = await dispatcher.write("dlive:write:item:create", { name: "no-csrf" });
|
|
151
|
+
|
|
152
|
+
expect(result.isSuccess).toBe(false);
|
|
153
|
+
if (!result.isSuccess) {
|
|
154
|
+
// The CSRF middleware raises with code "csrf_token_mismatch".
|
|
155
|
+
expect(result.error.code).toBe("csrf_token_mismatch");
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("query: dispatches GET-style-POST (Kumiko uses POST for query too), returns data", async () => {
|
|
160
|
+
// Seed a row first.
|
|
161
|
+
await stack.db.insert(itemTable).values({
|
|
162
|
+
id: generateId(),
|
|
163
|
+
tenantId: admin.tenantId,
|
|
164
|
+
name: "seed",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const { fetch, csrfToken } = await buildFetch();
|
|
168
|
+
const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => csrfToken });
|
|
169
|
+
|
|
170
|
+
const result = await dispatcher.query<unknown[]>("dlive:query:item:list", {});
|
|
171
|
+
|
|
172
|
+
expect(result.isSuccess).toBe(true);
|
|
173
|
+
if (result.isSuccess) {
|
|
174
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
175
|
+
expect(result.data).toHaveLength(1);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("batch: multiple writes go through one HTTP call, atomic on the server", async () => {
|
|
180
|
+
const { fetch, csrfToken } = await buildFetch();
|
|
181
|
+
const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => csrfToken });
|
|
182
|
+
|
|
183
|
+
const result = await dispatcher.batch([
|
|
184
|
+
{ type: "dlive:write:item:create", payload: { name: "a" } },
|
|
185
|
+
{ type: "dlive:write:item:create", payload: { name: "b" } },
|
|
186
|
+
{ type: "dlive:write:item:create", payload: { name: "c" } },
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
expect(result.isSuccess).toBe(true);
|
|
190
|
+
|
|
191
|
+
const rows = await stack.db.select().from(itemTable);
|
|
192
|
+
expect(rows).toHaveLength(3);
|
|
193
|
+
const names = rows.map((r) => r["name"]).sort();
|
|
194
|
+
expect(names).toEqual(["a", "b", "c"]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("batch: mid-batch failure rolls back the prior writes — atomic guarantee preserved", async () => {
|
|
198
|
+
const { fetch, csrfToken } = await buildFetch();
|
|
199
|
+
const dispatcher = createLiveDispatcher({ fetch, readCsrf: () => csrfToken });
|
|
200
|
+
|
|
201
|
+
const result = await dispatcher.batch([
|
|
202
|
+
{ type: "dlive:write:item:create", payload: { name: "ok-1" } },
|
|
203
|
+
{ type: "dlive:write:item:create", payload: { name: "" } }, // fails validation
|
|
204
|
+
{ type: "dlive:write:item:create", payload: { name: "never-runs" } },
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
expect(result.isSuccess).toBe(false);
|
|
208
|
+
if (!result.isSuccess) {
|
|
209
|
+
expect(result.failedIndex).toBe(1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// DB must be empty — prior success within a failed batch rolls back.
|
|
213
|
+
const rows = await stack.db.select().from(itemTable);
|
|
214
|
+
expect(rows).toHaveLength(0);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { createEntity, createRegistry, createTextField, defineFeature } from "../../engine";
|
|
3
|
+
import {
|
|
4
|
+
createNoopProvider,
|
|
5
|
+
createPrometheusMeter,
|
|
6
|
+
type ObservabilityProvider,
|
|
7
|
+
} from "../../observability";
|
|
8
|
+
import { buildServer } from "../server";
|
|
9
|
+
|
|
10
|
+
const JWT = "metrics-endpoint-test-secret-minimum-32-chars!!";
|
|
11
|
+
|
|
12
|
+
const noopFeature = defineFeature("m", (r) => {
|
|
13
|
+
r.entity("widget", createEntity({ table: "Widgets", fields: { name: createTextField() } }));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Swap the NoopProvider's meter for a PrometheusMeter. Tracer + lifecycle
|
|
17
|
+
// stay noop — /metrics only reads the meter.
|
|
18
|
+
function makeProvider(): {
|
|
19
|
+
provider: ObservabilityProvider;
|
|
20
|
+
meter: ReturnType<typeof createPrometheusMeter>;
|
|
21
|
+
} {
|
|
22
|
+
const meter = createPrometheusMeter();
|
|
23
|
+
const base = createNoopProvider();
|
|
24
|
+
const provider: ObservabilityProvider = { ...base, meter };
|
|
25
|
+
return { provider, meter };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeApp(opts: {
|
|
29
|
+
metrics?: { token?: string; path?: string };
|
|
30
|
+
meter?: ReturnType<typeof createPrometheusMeter>;
|
|
31
|
+
provider?: ObservabilityProvider;
|
|
32
|
+
}) {
|
|
33
|
+
const registry = createRegistry([noopFeature]);
|
|
34
|
+
const build = opts.provider
|
|
35
|
+
? { provider: opts.provider, meter: opts.meter ?? null }
|
|
36
|
+
: makeProvider();
|
|
37
|
+
const args = {
|
|
38
|
+
registry,
|
|
39
|
+
context: {},
|
|
40
|
+
jwtSecret: JWT,
|
|
41
|
+
observability: build.provider,
|
|
42
|
+
...(opts.metrics ? { metrics: opts.metrics } : {}),
|
|
43
|
+
};
|
|
44
|
+
return { ...buildServer(args), meter: build.meter };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("/metrics endpoint", () => {
|
|
48
|
+
test("returns 404 when `metrics` option is not wired (opt-in)", async () => {
|
|
49
|
+
const { app } = makeApp({});
|
|
50
|
+
const res = await app.request("/metrics");
|
|
51
|
+
expect(res.status).toBe(404);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("returns OpenMetrics text when wired and meter is a PrometheusMeter", async () => {
|
|
55
|
+
const { app, meter } = makeApp({ metrics: {} });
|
|
56
|
+
if (!meter) throw new Error("meter missing");
|
|
57
|
+
// Seed a single metric so the output isn't empty.
|
|
58
|
+
meter.registerMetric({
|
|
59
|
+
name: "kumiko_test_total",
|
|
60
|
+
type: "counter",
|
|
61
|
+
description: "probe counter",
|
|
62
|
+
});
|
|
63
|
+
meter.counter("kumiko_test_total").inc(3);
|
|
64
|
+
|
|
65
|
+
const res = await app.request("/metrics");
|
|
66
|
+
expect(res.status).toBe(200);
|
|
67
|
+
expect(res.headers.get("Content-Type")).toMatch(/openmetrics-text/);
|
|
68
|
+
const body = await res.text();
|
|
69
|
+
expect(body).toContain("# HELP kumiko_test_total probe counter");
|
|
70
|
+
expect(body).toContain("# TYPE kumiko_test_total counter");
|
|
71
|
+
expect(body).toContain("kumiko_test_total 3");
|
|
72
|
+
expect(body).toMatch(/# EOF\n$/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("token-protected: rejects missing header with 401", async () => {
|
|
76
|
+
const { app } = makeApp({ metrics: { token: "scrape-secret-xyz" } });
|
|
77
|
+
const res = await app.request("/metrics");
|
|
78
|
+
expect(res.status).toBe(401);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("token-protected: rejects wrong token with 401", async () => {
|
|
82
|
+
const { app } = makeApp({ metrics: { token: "scrape-secret-xyz" } });
|
|
83
|
+
const res = await app.request("/metrics", {
|
|
84
|
+
headers: { Authorization: "Bearer wrong-token" },
|
|
85
|
+
});
|
|
86
|
+
expect(res.status).toBe(401);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("token-protected: accepts matching Bearer token", async () => {
|
|
90
|
+
const { app, meter } = makeApp({ metrics: { token: "scrape-secret-xyz" } });
|
|
91
|
+
if (!meter) throw new Error("meter missing");
|
|
92
|
+
meter.registerMetric({ name: "kumiko_probe", type: "counter" });
|
|
93
|
+
meter.counter("kumiko_probe").inc();
|
|
94
|
+
|
|
95
|
+
const res = await app.request("/metrics", {
|
|
96
|
+
headers: { Authorization: "Bearer scrape-secret-xyz" },
|
|
97
|
+
});
|
|
98
|
+
expect(res.status).toBe(200);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("custom path: /internal/metrics", async () => {
|
|
102
|
+
const { app, meter } = makeApp({ metrics: { path: "/internal/metrics" } });
|
|
103
|
+
if (!meter) throw new Error("meter missing");
|
|
104
|
+
meter.registerMetric({ name: "kumiko_probe", type: "counter" });
|
|
105
|
+
|
|
106
|
+
const atDefault = await app.request("/metrics");
|
|
107
|
+
expect(atDefault.status).toBe(404);
|
|
108
|
+
|
|
109
|
+
const atCustom = await app.request("/internal/metrics");
|
|
110
|
+
expect(atCustom.status).toBe(200);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("503 when meter lacks snapshot() (misconfig — non-Prometheus provider)", async () => {
|
|
114
|
+
// Build a provider whose meter is a raw non-Prometheus implementation —
|
|
115
|
+
// pretend it's a ConsoleProvider or an OTLP bridge without snapshot().
|
|
116
|
+
const { createNoopProvider } = await import("../../observability");
|
|
117
|
+
const provider = createNoopProvider();
|
|
118
|
+
// NoopProvider is "empty by design", register a metric so definitions
|
|
119
|
+
// isn't hollow, but snapshot() is still absent on the meter shape.
|
|
120
|
+
provider.meter.registerMetric({ name: "kumiko_noop", type: "counter" });
|
|
121
|
+
const { app } = makeApp({ provider, metrics: {} });
|
|
122
|
+
const res = await app.request("/metrics");
|
|
123
|
+
expect(res.status).toBe(503);
|
|
124
|
+
expect(await res.text()).toContain("PrometheusMeter");
|
|
125
|
+
});
|
|
126
|
+
});
|