@checkstack/backend-api 0.0.2
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/CHANGELOG.md +228 -0
- package/package.json +33 -0
- package/src/assertions.test.ts +345 -0
- package/src/assertions.ts +371 -0
- package/src/auth-strategy.ts +58 -0
- package/src/chart-metadata.ts +77 -0
- package/src/config-service.ts +71 -0
- package/src/config-versioning.ts +310 -0
- package/src/contract.ts +8 -0
- package/src/core-services.ts +45 -0
- package/src/email-layout.ts +246 -0
- package/src/encryption.ts +95 -0
- package/src/event-bus-types.ts +28 -0
- package/src/extension-point.ts +11 -0
- package/src/health-check.ts +68 -0
- package/src/hooks.ts +182 -0
- package/src/index.ts +23 -0
- package/src/markdown.test.ts +106 -0
- package/src/markdown.ts +104 -0
- package/src/notification-strategy.ts +436 -0
- package/src/oauth-handler.ts +442 -0
- package/src/plugin-admin-contract.ts +64 -0
- package/src/plugin-system.ts +103 -0
- package/src/rpc.ts +284 -0
- package/src/schema-utils.ts +79 -0
- package/src/service-ref.ts +15 -0
- package/src/test-utils.ts +65 -0
- package/src/types.ts +111 -0
- package/src/zod-config.ts +149 -0
- package/tsconfig.json +6 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# @checkstack/backend-api
|
|
2
|
+
|
|
3
|
+
## 0.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d20d274: Initial release of all @checkstack packages. Rebranded from Checkmate to Checkstack with new npm organization @checkstack and domain checkstack.dev.
|
|
8
|
+
- Updated dependencies [d20d274]
|
|
9
|
+
- @checkstack/common@0.0.2
|
|
10
|
+
- @checkstack/queue-api@0.0.2
|
|
11
|
+
- @checkstack/signal-common@0.0.2
|
|
12
|
+
|
|
13
|
+
## 1.1.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- a65e002: Add compile-time type safety for Lucide icon names
|
|
18
|
+
|
|
19
|
+
- Add `LucideIconName` type and `lucideIconSchema` Zod schema to `@checkstack/common`
|
|
20
|
+
- Update backend interfaces (`AuthStrategy`, `NotificationStrategy`, `IntegrationProvider`, `CommandDefinition`) to use `LucideIconName`
|
|
21
|
+
- Update RPC contracts to use `lucideIconSchema` for proper type inference across RPC boundaries
|
|
22
|
+
- Simplify `SocialProviderButton` to use `DynamicIcon` directly (removes 30+ lines of pascalCase conversion)
|
|
23
|
+
- Replace static `iconMap` in `SearchDialog` with `DynamicIcon` for dynamic icon rendering
|
|
24
|
+
- Add fallback handling in `DynamicIcon` when icon name isn't found
|
|
25
|
+
- Fix legacy kebab-case icon names to PascalCase: `mail`→`Mail`, `send`→`Send`, `github`→`Github`, `key-round`→`KeyRound`, `network`→`Network`, `AlertCircle`→`CircleAlert`
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- b4eb432: Fixed TypeScript generic contravariance issue in notification strategy registration.
|
|
30
|
+
|
|
31
|
+
The `register` and `addStrategy` methods now use generic type parameters instead of `unknown`, allowing notification strategy plugins with typed OAuth configurations to be registered without compiler errors. This fixes contravariance issues where function parameters in `StrategyOAuthConfig<TConfig>` could not be assigned when `TConfig` was a specific type.
|
|
32
|
+
|
|
33
|
+
- Updated dependencies [a65e002]
|
|
34
|
+
- @checkstack/common@0.2.0
|
|
35
|
+
- @checkstack/queue-api@1.0.1
|
|
36
|
+
- @checkstack/signal-common@0.1.1
|
|
37
|
+
|
|
38
|
+
## 1.0.0
|
|
39
|
+
|
|
40
|
+
### Major Changes
|
|
41
|
+
|
|
42
|
+
- 81f3f85: ## Breaking: Unified Versioned<T> Architecture
|
|
43
|
+
|
|
44
|
+
Refactored the versioning system to use a unified `Versioned<T>` class instead of separate `VersionedSchema`, `VersionedData`, and `VersionedConfig` types.
|
|
45
|
+
|
|
46
|
+
### Breaking Changes
|
|
47
|
+
|
|
48
|
+
- **`VersionedSchema<T>`** is replaced by `Versioned<T>` class
|
|
49
|
+
- **`VersionedData<T>`** is replaced by `VersionedRecord<T>` interface
|
|
50
|
+
- **`VersionedConfig<T>`** is replaced by `VersionedPluginRecord<T>` interface
|
|
51
|
+
- **`ConfigMigration<F, T>`** is replaced by `Migration<F, T>` interface
|
|
52
|
+
- **`MigrationChain<T>`** is removed (use `Migration<unknown, unknown>[]`)
|
|
53
|
+
- **`migrateVersionedData()`** is removed (use `versioned.parse()`)
|
|
54
|
+
- **`ConfigMigrationRunner`** is removed (migrations are internal to Versioned)
|
|
55
|
+
|
|
56
|
+
### Migration Guide
|
|
57
|
+
|
|
58
|
+
Before:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
const strategy: HealthCheckStrategy = {
|
|
62
|
+
config: {
|
|
63
|
+
version: 1,
|
|
64
|
+
schema: mySchema,
|
|
65
|
+
migrations: [],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const data = await migrateVersionedData(stored, 1, migrations);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
After:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const strategy: HealthCheckStrategy = {
|
|
75
|
+
config: new Versioned({
|
|
76
|
+
version: 1,
|
|
77
|
+
schema: mySchema,
|
|
78
|
+
migrations: [],
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
const data = await strategy.config.parse(stored);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Minor Changes
|
|
85
|
+
|
|
86
|
+
- ffc28f6: ### Anonymous Role and Public Access
|
|
87
|
+
|
|
88
|
+
Introduces a configurable "anonymous" role for managing permissions available to unauthenticated users.
|
|
89
|
+
|
|
90
|
+
**Core Changes:**
|
|
91
|
+
|
|
92
|
+
- Added `userType: "public"` - endpoints accessible by both authenticated users (with their permissions) and anonymous users (with anonymous role permissions)
|
|
93
|
+
- Renamed `userType: "both"` to `"authenticated"` for clarity
|
|
94
|
+
- Renamed `isDefault` to `isAuthenticatedDefault` on Permission interface
|
|
95
|
+
- Added `isPublicDefault` flag for permissions that should be granted to the anonymous role by default
|
|
96
|
+
|
|
97
|
+
**Backend Infrastructure:**
|
|
98
|
+
|
|
99
|
+
- New `anonymous` system role created during auth-backend initialization
|
|
100
|
+
- New `disabled_public_default_permission` table tracks admin-disabled public defaults
|
|
101
|
+
- `autoAuthMiddleware` now checks anonymous role permissions for unauthenticated public endpoint access
|
|
102
|
+
- `AuthService.getAnonymousPermissions()` with 1-minute caching for performance
|
|
103
|
+
- Anonymous role filtered from `getRoles` endpoint (not assignable to users)
|
|
104
|
+
- Validation prevents assigning anonymous role to users
|
|
105
|
+
|
|
106
|
+
**Catalog Integration:**
|
|
107
|
+
|
|
108
|
+
- `catalog.read` permission now has both `isAuthenticatedDefault` and `isPublicDefault`
|
|
109
|
+
- Read endpoints (`getSystems`, `getGroups`, `getEntities`) now use `userType: "public"`
|
|
110
|
+
|
|
111
|
+
**UI:**
|
|
112
|
+
|
|
113
|
+
- New `PermissionGate` component for conditionally rendering content based on permissions
|
|
114
|
+
|
|
115
|
+
- 71275dd: fix: Anonymous and non-admin user authorization
|
|
116
|
+
|
|
117
|
+
- Fixed permission metadata preservation in `plugin-manager.ts` - changed from outdated `isDefault` field to `isAuthenticatedDefault` and `isPublicDefault`
|
|
118
|
+
- Added `pluginId` to `RpcContext` to enable proper permission ID matching
|
|
119
|
+
- Updated `autoAuthMiddleware` to prefix contract permission IDs with the pluginId from context, ensuring that contract permissions (e.g., `catalog.read`) correctly match database permissions (e.g., `catalog-backend.catalog.read`)
|
|
120
|
+
- Route now uses `/api/:pluginId/*` pattern with Hono path parameters for clean pluginId extraction
|
|
121
|
+
|
|
122
|
+
- ae19ff6: Add configurable state thresholds for health check evaluation
|
|
123
|
+
|
|
124
|
+
**@checkstack/backend-api:**
|
|
125
|
+
|
|
126
|
+
- Added `VersionedData<T>` generic interface as base for all versioned data structures
|
|
127
|
+
- `VersionedConfig<T>` now extends `VersionedData<T>` and adds `pluginId`
|
|
128
|
+
- Added `migrateVersionedData()` utility function for running migrations on any `VersionedData` subtype
|
|
129
|
+
|
|
130
|
+
**@checkstack/backend:**
|
|
131
|
+
|
|
132
|
+
- Refactored `ConfigMigrationRunner` to use the new `migrateVersionedData` utility
|
|
133
|
+
|
|
134
|
+
**@checkstack/healthcheck-common:**
|
|
135
|
+
|
|
136
|
+
- Added state threshold schemas with two evaluation modes (consecutive, window)
|
|
137
|
+
- Added `stateThresholds` field to `AssociateHealthCheckSchema`
|
|
138
|
+
- Added `getSystemHealthStatus` RPC endpoint contract
|
|
139
|
+
|
|
140
|
+
**@checkstack/healthcheck-backend:**
|
|
141
|
+
|
|
142
|
+
- Added `stateThresholds` column to `system_health_checks` table
|
|
143
|
+
- Added `state-evaluator.ts` with health status evaluation logic
|
|
144
|
+
- Added `state-thresholds-migrations.ts` with migration infrastructure
|
|
145
|
+
- Added `getSystemHealthStatus` RPC handler
|
|
146
|
+
|
|
147
|
+
**@checkstack/healthcheck-frontend:**
|
|
148
|
+
|
|
149
|
+
- Updated `SystemHealthBadge` to use new backend endpoint
|
|
150
|
+
|
|
151
|
+
- b55fae6: Added realtime Signal Service for backend-to-frontend push notifications via WebSockets.
|
|
152
|
+
|
|
153
|
+
## New Packages
|
|
154
|
+
|
|
155
|
+
- **@checkstack/signal-common**: Shared types including `Signal`, `SignalService`, `createSignal()`, and WebSocket protocol messages
|
|
156
|
+
- **@checkstack/signal-backend**: `SignalServiceImpl` with EventBus integration and Bun WebSocket handler using native pub/sub
|
|
157
|
+
- **@checkstack/signal-frontend**: React `SignalProvider` and `useSignal()` hook for consuming typed signals
|
|
158
|
+
|
|
159
|
+
## Changes
|
|
160
|
+
|
|
161
|
+
- **@checkstack/backend-api**: Added `coreServices.signalService` reference for plugins to emit signals
|
|
162
|
+
- **@checkstack/backend**: Integrated WebSocket server at `/api/signals/ws` with session-based authentication
|
|
163
|
+
|
|
164
|
+
## Usage
|
|
165
|
+
|
|
166
|
+
Backend plugins can emit signals:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import { coreServices } from "@checkstack/backend-api";
|
|
170
|
+
import { NOTIFICATION_RECEIVED } from "@checkstack/notification-common";
|
|
171
|
+
|
|
172
|
+
const signalService = context.signalService;
|
|
173
|
+
await signalService.sendToUser(NOTIFICATION_RECEIVED, userId, { ... });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Frontend components subscribe to signals:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
import { useSignal } from "@checkstack/signal-frontend";
|
|
180
|
+
import { NOTIFICATION_RECEIVED } from "@checkstack/notification-common";
|
|
181
|
+
|
|
182
|
+
useSignal(NOTIFICATION_RECEIVED, (payload) => {
|
|
183
|
+
// Handle realtime notification
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
- b354ab3: # Strategy Instructions Support & Telegram Notification Plugin
|
|
188
|
+
|
|
189
|
+
## Strategy Instructions Interface
|
|
190
|
+
|
|
191
|
+
Added `adminInstructions` and `userInstructions` optional fields to the `NotificationStrategy` interface. These allow strategies to export markdown-formatted setup guides that are displayed in the configuration UI:
|
|
192
|
+
|
|
193
|
+
- **`adminInstructions`**: Shown when admins configure platform-wide strategy settings (e.g., how to create API keys)
|
|
194
|
+
- **`userInstructions`**: Shown when users configure their personal settings (e.g., how to link their account)
|
|
195
|
+
|
|
196
|
+
### Updated Components
|
|
197
|
+
|
|
198
|
+
- `StrategyConfigCard` now accepts an `instructions` prop and renders it before config sections
|
|
199
|
+
- `StrategyCard` passes `adminInstructions` to `StrategyConfigCard`
|
|
200
|
+
- `UserChannelCard` renders `userInstructions` when users need to connect
|
|
201
|
+
|
|
202
|
+
## New Telegram Notification Plugin
|
|
203
|
+
|
|
204
|
+
Added `@checkstack/notification-telegram-backend` plugin for sending notifications via Telegram:
|
|
205
|
+
|
|
206
|
+
- Uses [grammY](https://grammy.dev/) framework for Telegram Bot API integration
|
|
207
|
+
- Sends messages with MarkdownV2 formatting and inline keyboard buttons for actions
|
|
208
|
+
- Includes comprehensive admin instructions for bot setup via @BotFather
|
|
209
|
+
- Includes user instructions for account linking
|
|
210
|
+
|
|
211
|
+
### Configuration
|
|
212
|
+
|
|
213
|
+
Admins need to configure a Telegram Bot Token obtained from @BotFather.
|
|
214
|
+
|
|
215
|
+
### User Linking
|
|
216
|
+
|
|
217
|
+
The strategy uses `contactResolution: { type: "custom" }` for Telegram Login Widget integration. Full frontend integration for the Login Widget is pending future work.
|
|
218
|
+
|
|
219
|
+
### Patch Changes
|
|
220
|
+
|
|
221
|
+
- Updated dependencies [ffc28f6]
|
|
222
|
+
- Updated dependencies [e4d83fc]
|
|
223
|
+
- Updated dependencies [b55fae6]
|
|
224
|
+
- Updated dependencies [8e889b4]
|
|
225
|
+
- Updated dependencies [81f3f85]
|
|
226
|
+
- @checkstack/common@0.1.0
|
|
227
|
+
- @checkstack/queue-api@1.0.0
|
|
228
|
+
- @checkstack/signal-common@0.1.0
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/backend-api",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
8
|
+
"lint": "bun run lint:code",
|
|
9
|
+
"lint:code": "eslint . --max-warnings 0"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@checkstack/common": "workspace:*",
|
|
13
|
+
"@checkstack/queue-api": "workspace:*",
|
|
14
|
+
"@checkstack/signal-common": "workspace:*",
|
|
15
|
+
"@orpc/client": "^1.13.2",
|
|
16
|
+
"@orpc/openapi": "^1.13.2",
|
|
17
|
+
"@orpc/server": "^1.13.2",
|
|
18
|
+
"@orpc/zod": "^1.13.2",
|
|
19
|
+
"drizzle-orm": "^0.45.1",
|
|
20
|
+
"hono": "^4.0.0",
|
|
21
|
+
"marked": "^17.0.1",
|
|
22
|
+
"zod": "^4.2.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/bun": "latest",
|
|
26
|
+
"@checkstack/tsconfig": "workspace:*",
|
|
27
|
+
"@checkstack/scripts": "workspace:*"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"hono": "^4.0.0",
|
|
31
|
+
"drizzle-orm": "^0.45.1"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
evaluateAssertion,
|
|
4
|
+
evaluateAssertions,
|
|
5
|
+
evaluateJsonPathAssertions,
|
|
6
|
+
numericField,
|
|
7
|
+
timeThresholdField,
|
|
8
|
+
stringField,
|
|
9
|
+
booleanField,
|
|
10
|
+
enumField,
|
|
11
|
+
jsonPathField,
|
|
12
|
+
} from "./assertions";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
describe("Assertion Schema Factories", () => {
|
|
16
|
+
describe("numericField", () => {
|
|
17
|
+
it("creates a schema with numeric operators", () => {
|
|
18
|
+
const schema = numericField("packetLoss", { min: 0, max: 100 });
|
|
19
|
+
|
|
20
|
+
const valid = schema.safeParse({
|
|
21
|
+
field: "packetLoss",
|
|
22
|
+
operator: "lessThan",
|
|
23
|
+
value: 50,
|
|
24
|
+
});
|
|
25
|
+
expect(valid.success).toBe(true);
|
|
26
|
+
|
|
27
|
+
const invalid = schema.safeParse({
|
|
28
|
+
field: "packetLoss",
|
|
29
|
+
operator: "lessThan",
|
|
30
|
+
value: 150,
|
|
31
|
+
});
|
|
32
|
+
expect(invalid.success).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("timeThresholdField", () => {
|
|
37
|
+
it("only allows lessThan and lessThanOrEqual operators", () => {
|
|
38
|
+
const schema = timeThresholdField("latency");
|
|
39
|
+
|
|
40
|
+
const valid = schema.safeParse({
|
|
41
|
+
field: "latency",
|
|
42
|
+
operator: "lessThan",
|
|
43
|
+
value: 100,
|
|
44
|
+
});
|
|
45
|
+
expect(valid.success).toBe(true);
|
|
46
|
+
|
|
47
|
+
const invalid = schema.safeParse({
|
|
48
|
+
field: "latency",
|
|
49
|
+
operator: "greaterThan",
|
|
50
|
+
value: 100,
|
|
51
|
+
});
|
|
52
|
+
expect(invalid.success).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("stringField", () => {
|
|
57
|
+
it("creates a schema with string operators", () => {
|
|
58
|
+
const schema = stringField("banner");
|
|
59
|
+
|
|
60
|
+
const valid = schema.safeParse({
|
|
61
|
+
field: "banner",
|
|
62
|
+
operator: "contains",
|
|
63
|
+
value: "SSH",
|
|
64
|
+
});
|
|
65
|
+
expect(valid.success).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("booleanField", () => {
|
|
70
|
+
it("creates a schema with isTrue/isFalse operators", () => {
|
|
71
|
+
const schema = booleanField("isExpired");
|
|
72
|
+
|
|
73
|
+
const isTrue = schema.safeParse({
|
|
74
|
+
field: "isExpired",
|
|
75
|
+
operator: "isTrue",
|
|
76
|
+
});
|
|
77
|
+
expect(isTrue.success).toBe(true);
|
|
78
|
+
|
|
79
|
+
const isFalse = schema.safeParse({
|
|
80
|
+
field: "isExpired",
|
|
81
|
+
operator: "isFalse",
|
|
82
|
+
});
|
|
83
|
+
expect(isFalse.success).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("enumField", () => {
|
|
88
|
+
it("creates a schema for enum values", () => {
|
|
89
|
+
const schema = enumField("status", ["SERVING", "NOT_SERVING"] as const);
|
|
90
|
+
|
|
91
|
+
const valid = schema.safeParse({
|
|
92
|
+
field: "status",
|
|
93
|
+
operator: "equals",
|
|
94
|
+
value: "SERVING",
|
|
95
|
+
});
|
|
96
|
+
expect(valid.success).toBe(true);
|
|
97
|
+
|
|
98
|
+
const invalid = schema.safeParse({
|
|
99
|
+
field: "status",
|
|
100
|
+
operator: "equals",
|
|
101
|
+
value: "INVALID",
|
|
102
|
+
});
|
|
103
|
+
expect(invalid.success).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("generates JSON Schema with enum values for select rendering", () => {
|
|
107
|
+
const schema = enumField("status", [
|
|
108
|
+
"SERVING",
|
|
109
|
+
"NOT_SERVING",
|
|
110
|
+
"UNKNOWN",
|
|
111
|
+
] as const);
|
|
112
|
+
const jsonSchema = schema.toJSONSchema() as Record<string, unknown>;
|
|
113
|
+
const properties = jsonSchema.properties as Record<
|
|
114
|
+
string,
|
|
115
|
+
Record<string, unknown>
|
|
116
|
+
>;
|
|
117
|
+
|
|
118
|
+
// Field should have const for discriminator
|
|
119
|
+
expect(properties.field.const).toBe("status");
|
|
120
|
+
|
|
121
|
+
// Operator should have const "equals"
|
|
122
|
+
expect(properties.operator.const).toBe("equals");
|
|
123
|
+
|
|
124
|
+
// Value should have enum array for select rendering
|
|
125
|
+
expect(properties.value.enum).toEqual([
|
|
126
|
+
"SERVING",
|
|
127
|
+
"NOT_SERVING",
|
|
128
|
+
"UNKNOWN",
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("jsonPathField", () => {
|
|
134
|
+
it("creates a schema with dynamic operators", () => {
|
|
135
|
+
const schema = jsonPathField();
|
|
136
|
+
|
|
137
|
+
const valid = schema.safeParse({
|
|
138
|
+
path: "$.status",
|
|
139
|
+
operator: "equals",
|
|
140
|
+
value: "ok",
|
|
141
|
+
});
|
|
142
|
+
expect(valid.success).toBe(true);
|
|
143
|
+
|
|
144
|
+
const exists = schema.safeParse({ path: "$.data", operator: "exists" });
|
|
145
|
+
expect(exists.success).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("evaluateAssertion", () => {
|
|
151
|
+
describe("numeric operators", () => {
|
|
152
|
+
it("evaluates equals correctly", () => {
|
|
153
|
+
const result = evaluateAssertion(
|
|
154
|
+
{ field: "count", operator: "equals", value: 5 },
|
|
155
|
+
{ count: 5 }
|
|
156
|
+
);
|
|
157
|
+
expect(result.passed).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("evaluates lessThan correctly", () => {
|
|
161
|
+
const result = evaluateAssertion(
|
|
162
|
+
{ field: "latency", operator: "lessThan", value: 100 },
|
|
163
|
+
{ latency: 50 }
|
|
164
|
+
);
|
|
165
|
+
expect(result.passed).toBe(true);
|
|
166
|
+
|
|
167
|
+
const failed = evaluateAssertion(
|
|
168
|
+
{ field: "latency", operator: "lessThan", value: 100 },
|
|
169
|
+
{ latency: 150 }
|
|
170
|
+
);
|
|
171
|
+
expect(failed.passed).toBe(false);
|
|
172
|
+
expect(failed.message).toContain("less than");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("evaluates greaterThanOrEqual correctly", () => {
|
|
176
|
+
const result = evaluateAssertion(
|
|
177
|
+
{ field: "uptime", operator: "greaterThanOrEqual", value: 99 },
|
|
178
|
+
{ uptime: 99 }
|
|
179
|
+
);
|
|
180
|
+
expect(result.passed).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("string operators", () => {
|
|
185
|
+
it("evaluates contains correctly", () => {
|
|
186
|
+
const result = evaluateAssertion(
|
|
187
|
+
{ field: "stdout", operator: "contains", value: "OK" },
|
|
188
|
+
{ stdout: "Status: OK" }
|
|
189
|
+
);
|
|
190
|
+
expect(result.passed).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("evaluates startsWith correctly", () => {
|
|
194
|
+
const result = evaluateAssertion(
|
|
195
|
+
{ field: "banner", operator: "startsWith", value: "SSH-2.0" },
|
|
196
|
+
{ banner: "SSH-2.0-OpenSSH" }
|
|
197
|
+
);
|
|
198
|
+
expect(result.passed).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("evaluates matches correctly", () => {
|
|
202
|
+
const result = evaluateAssertion(
|
|
203
|
+
{ field: "version", operator: "matches", value: "v\\d+\\.\\d+" },
|
|
204
|
+
{ version: "v1.2.3" }
|
|
205
|
+
);
|
|
206
|
+
expect(result.passed).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("evaluates isEmpty correctly", () => {
|
|
210
|
+
const result = evaluateAssertion(
|
|
211
|
+
{ field: "stderr", operator: "isEmpty" },
|
|
212
|
+
{ stderr: "" }
|
|
213
|
+
);
|
|
214
|
+
expect(result.passed).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("boolean operators", () => {
|
|
219
|
+
it("evaluates isTrue correctly", () => {
|
|
220
|
+
const result = evaluateAssertion(
|
|
221
|
+
{ field: "connected", operator: "isTrue" },
|
|
222
|
+
{ connected: true }
|
|
223
|
+
);
|
|
224
|
+
expect(result.passed).toBe(true);
|
|
225
|
+
|
|
226
|
+
const failed = evaluateAssertion(
|
|
227
|
+
{ field: "connected", operator: "isTrue" },
|
|
228
|
+
{ connected: false }
|
|
229
|
+
);
|
|
230
|
+
expect(failed.passed).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("evaluates isFalse correctly", () => {
|
|
234
|
+
const result = evaluateAssertion(
|
|
235
|
+
{ field: "isExpired", operator: "isFalse" },
|
|
236
|
+
{ isExpired: false }
|
|
237
|
+
);
|
|
238
|
+
expect(result.passed).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("existence operators", () => {
|
|
243
|
+
it("evaluates exists correctly", () => {
|
|
244
|
+
const result = evaluateAssertion(
|
|
245
|
+
{ field: "data", operator: "exists" },
|
|
246
|
+
{ data: { foo: "bar" } }
|
|
247
|
+
);
|
|
248
|
+
expect(result.passed).toBe(true);
|
|
249
|
+
|
|
250
|
+
const notExists = evaluateAssertion(
|
|
251
|
+
{ field: "data", operator: "exists" },
|
|
252
|
+
{ data: null }
|
|
253
|
+
);
|
|
254
|
+
expect(notExists.passed).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("evaluates notExists correctly", () => {
|
|
258
|
+
const result = evaluateAssertion(
|
|
259
|
+
{ field: "error", operator: "notExists" },
|
|
260
|
+
{ error: undefined }
|
|
261
|
+
);
|
|
262
|
+
expect(result.passed).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("evaluateAssertions", () => {
|
|
268
|
+
it("returns null when all assertions pass", () => {
|
|
269
|
+
const assertions = [
|
|
270
|
+
{ field: "status", operator: "equals", value: 200 },
|
|
271
|
+
{ field: "latency", operator: "lessThan", value: 100 },
|
|
272
|
+
];
|
|
273
|
+
const result = evaluateAssertions(assertions, { status: 200, latency: 50 });
|
|
274
|
+
expect(result).toBe(null);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("returns the first failed assertion", () => {
|
|
278
|
+
const assertions = [
|
|
279
|
+
{ field: "status", operator: "equals", value: 200 },
|
|
280
|
+
{ field: "latency", operator: "lessThan", value: 100 },
|
|
281
|
+
];
|
|
282
|
+
const result = evaluateAssertions(assertions, {
|
|
283
|
+
status: 200,
|
|
284
|
+
latency: 150,
|
|
285
|
+
});
|
|
286
|
+
expect(result).toEqual({
|
|
287
|
+
field: "latency",
|
|
288
|
+
operator: "lessThan",
|
|
289
|
+
value: 100,
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("returns null for empty or undefined assertions", () => {
|
|
294
|
+
expect(evaluateAssertions([], {})).toBe(null);
|
|
295
|
+
expect(evaluateAssertions(undefined, {})).toBe(null);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("evaluateJsonPathAssertions", () => {
|
|
300
|
+
const extractPath = (path: string, json: unknown) => {
|
|
301
|
+
// Simple mock extractor for testing
|
|
302
|
+
if (path === "$.status") return (json as Record<string, unknown>)?.status;
|
|
303
|
+
if (path === "$.count") return (json as Record<string, unknown>)?.count;
|
|
304
|
+
return undefined;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
it("evaluates JSONPath assertions with string coercion", () => {
|
|
308
|
+
const assertions = [{ path: "$.status", operator: "equals", value: "ok" }];
|
|
309
|
+
const result = evaluateJsonPathAssertions(
|
|
310
|
+
assertions,
|
|
311
|
+
{ status: "ok" },
|
|
312
|
+
extractPath
|
|
313
|
+
);
|
|
314
|
+
expect(result).toBe(null);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("evaluates JSONPath assertions with numeric coercion", () => {
|
|
318
|
+
const assertions = [
|
|
319
|
+
{ path: "$.count", operator: "greaterThan", value: "5" },
|
|
320
|
+
];
|
|
321
|
+
const result = evaluateJsonPathAssertions(
|
|
322
|
+
assertions,
|
|
323
|
+
{ count: 10 },
|
|
324
|
+
extractPath
|
|
325
|
+
);
|
|
326
|
+
expect(result).toBe(null);
|
|
327
|
+
|
|
328
|
+
const failed = evaluateJsonPathAssertions(
|
|
329
|
+
assertions,
|
|
330
|
+
{ count: 3 },
|
|
331
|
+
extractPath
|
|
332
|
+
);
|
|
333
|
+
expect(failed).toEqual(assertions[0]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("evaluates existence checks", () => {
|
|
337
|
+
const assertions = [{ path: "$.status", operator: "exists" }];
|
|
338
|
+
const result = evaluateJsonPathAssertions(
|
|
339
|
+
assertions,
|
|
340
|
+
{ status: "ok" },
|
|
341
|
+
extractPath
|
|
342
|
+
);
|
|
343
|
+
expect(result).toBe(null);
|
|
344
|
+
});
|
|
345
|
+
});
|