@api-client/core 0.15.1 → 0.16.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/TESTING_READY.md +114 -0
- package/TESTING_SETUP.md +198 -0
- package/build/src/modeling/Semantics.d.ts +126 -2
- package/build/src/modeling/Semantics.d.ts.map +1 -1
- package/build/src/modeling/Semantics.js +281 -13
- package/build/src/modeling/Semantics.js.map +1 -1
- package/build/src/modeling/definitions/Calculated.d.ts +54 -0
- package/build/src/modeling/definitions/Calculated.d.ts.map +1 -0
- package/build/src/modeling/definitions/Calculated.js +31 -0
- package/build/src/modeling/definitions/Calculated.js.map +1 -0
- package/build/src/modeling/definitions/Categories.d.ts +60 -0
- package/build/src/modeling/definitions/Categories.d.ts.map +1 -0
- package/build/src/modeling/definitions/Categories.js +33 -0
- package/build/src/modeling/definitions/Categories.js.map +1 -0
- package/build/src/modeling/definitions/Derived.d.ts +54 -0
- package/build/src/modeling/definitions/Derived.d.ts.map +1 -0
- package/build/src/modeling/definitions/Derived.js +31 -0
- package/build/src/modeling/definitions/Derived.js.map +1 -0
- package/build/src/modeling/definitions/Description.d.ts +36 -0
- package/build/src/modeling/definitions/Description.d.ts.map +1 -0
- package/build/src/modeling/definitions/Description.js +28 -0
- package/build/src/modeling/definitions/Description.js.map +1 -0
- package/build/src/modeling/definitions/Email.d.ts +66 -0
- package/build/src/modeling/definitions/Email.d.ts.map +1 -0
- package/build/src/modeling/definitions/Email.js +33 -0
- package/build/src/modeling/definitions/Email.js.map +1 -0
- package/build/src/modeling/definitions/GeospatialCoordinates.d.ts +212 -0
- package/build/src/modeling/definitions/GeospatialCoordinates.d.ts.map +1 -0
- package/build/src/modeling/definitions/GeospatialCoordinates.js +129 -0
- package/build/src/modeling/definitions/GeospatialCoordinates.js.map +1 -0
- package/build/src/modeling/definitions/HTML.d.ts +88 -0
- package/build/src/modeling/definitions/HTML.d.ts.map +1 -0
- package/build/src/modeling/definitions/HTML.js +42 -0
- package/build/src/modeling/definitions/HTML.js.map +1 -0
- package/build/src/modeling/definitions/Markdown.d.ts +84 -0
- package/build/src/modeling/definitions/Markdown.d.ts.map +1 -0
- package/build/src/modeling/definitions/Markdown.js +41 -0
- package/build/src/modeling/definitions/Markdown.js.map +1 -0
- package/build/src/modeling/definitions/Password.d.ts +112 -0
- package/build/src/modeling/definitions/Password.d.ts.map +1 -0
- package/build/src/modeling/definitions/Password.js +57 -0
- package/build/src/modeling/definitions/Password.js.map +1 -0
- package/build/src/modeling/definitions/Phone.d.ts +83 -0
- package/build/src/modeling/definitions/Phone.d.ts.map +1 -0
- package/build/src/modeling/definitions/Phone.js +39 -0
- package/build/src/modeling/definitions/Phone.js.map +1 -0
- package/build/src/modeling/definitions/Price.d.ts +102 -0
- package/build/src/modeling/definitions/Price.d.ts.map +1 -0
- package/build/src/modeling/definitions/Price.js +99 -0
- package/build/src/modeling/definitions/Price.js.map +1 -0
- package/build/src/modeling/definitions/PublicUniqueName.d.ts +69 -0
- package/build/src/modeling/definitions/PublicUniqueName.d.ts.map +1 -0
- package/build/src/modeling/definitions/PublicUniqueName.js +34 -0
- package/build/src/modeling/definitions/PublicUniqueName.js.map +1 -0
- package/build/src/modeling/definitions/SKU.d.ts +127 -0
- package/build/src/modeling/definitions/SKU.d.ts.map +1 -0
- package/build/src/modeling/definitions/SKU.js +142 -0
- package/build/src/modeling/definitions/SKU.js.map +1 -0
- package/build/src/modeling/definitions/Status.d.ts +150 -0
- package/build/src/modeling/definitions/Status.d.ts.map +1 -0
- package/build/src/modeling/definitions/Status.js +60 -0
- package/build/src/modeling/definitions/Status.js.map +1 -0
- package/build/src/modeling/definitions/Summary.d.ts +53 -0
- package/build/src/modeling/definitions/Summary.d.ts.map +1 -0
- package/build/src/modeling/definitions/Summary.js +50 -0
- package/build/src/modeling/definitions/Summary.js.map +1 -0
- package/build/src/modeling/definitions/Tags.d.ts +52 -0
- package/build/src/modeling/definitions/Tags.d.ts.map +1 -0
- package/build/src/modeling/definitions/Tags.js +32 -0
- package/build/src/modeling/definitions/Tags.js.map +1 -0
- package/build/src/modeling/definitions/URL.d.ts +68 -0
- package/build/src/modeling/definitions/URL.d.ts.map +1 -0
- package/build/src/modeling/definitions/URL.js +37 -0
- package/build/src/modeling/definitions/URL.js.map +1 -0
- package/build/src/modeling/validation/semantic_validation.d.ts +4 -0
- package/build/src/modeling/validation/semantic_validation.d.ts.map +1 -1
- package/build/src/modeling/validation/semantic_validation.js +32 -1
- package/build/src/modeling/validation/semantic_validation.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/data/models/example-generator-api.json +11 -11
- package/package.json +1 -1
- package/src/modeling/Semantics.ts +297 -14
- package/src/modeling/definitions/Calculated.ts +76 -0
- package/src/modeling/definitions/Categories.ts +84 -0
- package/src/modeling/definitions/Derived.ts +76 -0
- package/src/modeling/definitions/Description.ts +55 -0
- package/src/modeling/definitions/Email.ts +90 -0
- package/src/modeling/definitions/GeospatialCoordinates.ts +274 -0
- package/src/modeling/definitions/HTML.ts +121 -0
- package/src/modeling/definitions/Markdown.ts +116 -0
- package/src/modeling/definitions/Password.ts +156 -0
- package/src/modeling/definitions/Phone.ts +116 -0
- package/src/modeling/definitions/Price.examples.md +158 -0
- package/src/modeling/definitions/Price.ts +180 -0
- package/src/modeling/definitions/PublicUniqueName.ts +98 -0
- package/src/modeling/definitions/SKU.examples.md +230 -0
- package/src/modeling/definitions/SKU.ts +254 -0
- package/src/modeling/definitions/Status.ts +227 -0
- package/src/modeling/definitions/Summary.ts +73 -0
- package/src/modeling/definitions/Tags.ts +75 -0
- package/src/modeling/definitions/URL.ts +96 -0
- package/src/modeling/validation/semantic_validation.ts +35 -1
- package/tests/example-test-setup.ts +133 -0
- package/tests/template-node.spec.ts +75 -0
- package/tests/test-utils.ts +293 -0
- package/tests/unit/modeling/definitions/calculated.spec.ts +33 -0
- package/tests/unit/modeling/definitions/categories.spec.ts +38 -0
- package/tests/unit/modeling/definitions/derived.spec.ts +34 -0
- package/tests/unit/modeling/definitions/description.spec.ts +38 -0
- package/tests/unit/modeling/definitions/email.spec.ts +38 -0
- package/tests/unit/modeling/definitions/geospatial-coordinates.spec.ts +41 -0
- package/tests/unit/modeling/definitions/html.spec.ts +38 -0
- package/tests/unit/modeling/definitions/markdown.spec.ts +38 -0
- package/tests/unit/modeling/definitions/password.spec.ts +347 -0
- package/tests/unit/modeling/definitions/phone.spec.ts +38 -0
- package/tests/unit/modeling/definitions/price.spec.ts +465 -0
- package/tests/unit/modeling/definitions/public-unique-name.spec.ts +38 -0
- package/tests/unit/modeling/definitions/sku.spec.ts +240 -0
- package/tests/unit/modeling/definitions/status.spec.ts +37 -0
- package/tests/unit/modeling/definitions/summary.spec.ts +36 -0
- package/tests/unit/modeling/definitions/tags.spec.ts +38 -0
- package/tests/unit/modeling/definitions/url.spec.ts +38 -0
- package/tests/unit/modeling/domain_property.spec.ts +106 -0
- package/tests/unit/modeling/domain_validation.spec.ts +5 -5
- package/tests/unit/modeling/semantic-configs.spec.ts +569 -0
- package/tests/unit/modeling/semantics.spec.ts +52 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { AppliedDataSemantic } from '../Semantics.js'
|
|
2
|
+
import { SemanticType } from '../Semantics.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for the URL semantic.
|
|
6
|
+
* Controls validation, allowed protocols, and formatting.
|
|
7
|
+
*/
|
|
8
|
+
export interface URLConfig {
|
|
9
|
+
/**
|
|
10
|
+
* List of allowed protocols (e.g., ['http', 'https']).
|
|
11
|
+
*/
|
|
12
|
+
allowedProtocols?: string[]
|
|
13
|
+
/**
|
|
14
|
+
* Whether to require HTTPS protocol (default: false).
|
|
15
|
+
*/
|
|
16
|
+
requireHttps?: boolean
|
|
17
|
+
/**
|
|
18
|
+
* Whether to allow query parameters in the URL.
|
|
19
|
+
*/
|
|
20
|
+
allowQueryParams?: boolean
|
|
21
|
+
/**
|
|
22
|
+
* Whether to allow URL fragments (e.g., #section).
|
|
23
|
+
*/
|
|
24
|
+
allowFragments?: boolean
|
|
25
|
+
/**
|
|
26
|
+
* Whether to allow internationalized domain names.
|
|
27
|
+
*/
|
|
28
|
+
allowInternational?: boolean
|
|
29
|
+
/**
|
|
30
|
+
* Whether to allow IP addresses as hostnames.
|
|
31
|
+
*/
|
|
32
|
+
allowIP?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* Whether to allow port numbers in the URL.
|
|
35
|
+
*/
|
|
36
|
+
allowPort?: boolean
|
|
37
|
+
/**
|
|
38
|
+
* Whether to allow authentication info (user:pass@host).
|
|
39
|
+
*/
|
|
40
|
+
allowAuthentication?: boolean
|
|
41
|
+
/**
|
|
42
|
+
* Custom metadata for the URL field.
|
|
43
|
+
*/
|
|
44
|
+
metadata?: Record<string, unknown>
|
|
45
|
+
/**
|
|
46
|
+
* Index signature to allow additional properties.
|
|
47
|
+
*/
|
|
48
|
+
[key: string]: unknown
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Type-safe configuration for URL semantic.
|
|
53
|
+
*/
|
|
54
|
+
export interface AppliedURLSemantic extends AppliedDataSemantic {
|
|
55
|
+
id: SemanticType.URL
|
|
56
|
+
config?: URLConfig
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Type guard to check if a semantic is a URL semantic.
|
|
61
|
+
*/
|
|
62
|
+
export const isURLSemantic = (semantic: AppliedDataSemantic): semantic is AppliedURLSemantic => {
|
|
63
|
+
return semantic.id === SemanticType.URL
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Helper function to create a URL semantic with configuration.
|
|
68
|
+
*/
|
|
69
|
+
export const createURLSemantic = (config: URLConfig = {}): AppliedURLSemantic => {
|
|
70
|
+
const mergedConfig = {
|
|
71
|
+
...DEFAULT_URL_CONFIG,
|
|
72
|
+
...config,
|
|
73
|
+
}
|
|
74
|
+
if (config.metadata) {
|
|
75
|
+
mergedConfig.metadata = { ...config.metadata }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
id: SemanticType.URL,
|
|
80
|
+
config: mergedConfig,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Default configuration for URL semantic.
|
|
86
|
+
*/
|
|
87
|
+
export const DEFAULT_URL_CONFIG: URLConfig = {
|
|
88
|
+
allowedProtocols: ['http', 'https'],
|
|
89
|
+
requireHttps: false,
|
|
90
|
+
allowQueryParams: true,
|
|
91
|
+
allowFragments: true,
|
|
92
|
+
allowInternational: true,
|
|
93
|
+
allowIP: true,
|
|
94
|
+
allowPort: true,
|
|
95
|
+
allowAuthentication: false,
|
|
96
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DomainEntityKind } from '../../models/kinds.js'
|
|
2
2
|
import type { DataDomain } from '../DataDomain.js'
|
|
3
|
-
import { SemanticType } from '../Semantics.js'
|
|
3
|
+
import { DataSemantics, isPropertySemantic, SemanticType } from '../Semantics.js'
|
|
4
4
|
import type { DomainValidation } from './rules.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -29,6 +29,10 @@ export class SemanticValidation {
|
|
|
29
29
|
const softDeleteSemantics = this.validateSoftDeleteSemantics()
|
|
30
30
|
results.push(...softDeleteSemantics)
|
|
31
31
|
|
|
32
|
+
// Validate property semantics data types
|
|
33
|
+
const propertySemanticsDataTypes = this.validatePropertySemanticsDataTypes()
|
|
34
|
+
results.push(...propertySemanticsDataTypes)
|
|
35
|
+
|
|
32
36
|
return results
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -142,4 +146,34 @@ export class SemanticValidation {
|
|
|
142
146
|
|
|
143
147
|
return results
|
|
144
148
|
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Validates if property semantics are applied to properties with compatible data types.
|
|
152
|
+
*/
|
|
153
|
+
private validatePropertySemanticsDataTypes(): DomainValidation[] {
|
|
154
|
+
const results: DomainValidation[] = []
|
|
155
|
+
|
|
156
|
+
for (const entity of this.domain.listEntities()) {
|
|
157
|
+
for (const property of entity.listProperties()) {
|
|
158
|
+
for (const appliedSemantic of property.semantics) {
|
|
159
|
+
const semanticDefinition = DataSemantics[appliedSemantic.id]
|
|
160
|
+
if (isPropertySemantic(semanticDefinition) && semanticDefinition.applicableDataTypes) {
|
|
161
|
+
if (!semanticDefinition.applicableDataTypes.includes(property.type)) {
|
|
162
|
+
results.push({
|
|
163
|
+
field: 'semantics',
|
|
164
|
+
rule: 'type_mismatch',
|
|
165
|
+
message: `The "${property.info.getLabel()}" property has the "${semanticDefinition.displayName}" semantic applied, but its type "${property.type}" is not compatible.`,
|
|
166
|
+
help: `The "${semanticDefinition.displayName}" semantic can only be applied to properties of type(s): ${semanticDefinition.applicableDataTypes.join(', ')}.`,
|
|
167
|
+
severity: 'error',
|
|
168
|
+
key: property.key,
|
|
169
|
+
kind: property.kind,
|
|
170
|
+
parent: entity.key,
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return results
|
|
178
|
+
}
|
|
145
179
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example test setup and usage guide for API Client Core
|
|
3
|
+
*
|
|
4
|
+
* This file demonstrates how to use the testing infrastructure
|
|
5
|
+
* and provides working examples for both Node.js and browser tests.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Node.js test example using Japa
|
|
9
|
+
import { test } from '@japa/runner'
|
|
10
|
+
import { TestUtils } from './test-utils.js'
|
|
11
|
+
|
|
12
|
+
// Example: Testing a utility function
|
|
13
|
+
test.group('Example Node.js Tests', (group) => {
|
|
14
|
+
group.each.setup(() => {
|
|
15
|
+
// Setup before each test
|
|
16
|
+
// eslint-disable-next-line no-console
|
|
17
|
+
console.log('Setting up test...')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('should demonstrate basic testing', ({ assert }) => {
|
|
21
|
+
const result = 'hello world'
|
|
22
|
+
assert.equal(result, 'hello world')
|
|
23
|
+
assert.typeOf(result, 'string')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('should test async operations', async ({ assert }) => {
|
|
27
|
+
const data = TestUtils.generateTestData()
|
|
28
|
+
assert.typeOf(data.id, 'string')
|
|
29
|
+
assert.typeOf(data.timestamp, 'number')
|
|
30
|
+
assert.typeOf(data.randomBoolean, 'boolean')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('should test HTTP mocking', async ({ assert }) => {
|
|
34
|
+
const mockResponse = TestUtils.createMockResponse(200, { success: true })
|
|
35
|
+
const cleanup = TestUtils.mockFetch(mockResponse)
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch('https://api.example.com/test')
|
|
39
|
+
const data = await response.json()
|
|
40
|
+
assert.isTrue(data.success)
|
|
41
|
+
assert.equal(response.status, 200)
|
|
42
|
+
} finally {
|
|
43
|
+
cleanup()
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('should test error handling', async ({ assert }) => {
|
|
48
|
+
const error = await TestUtils.assertThrows(() => {
|
|
49
|
+
throw new Error('Test error')
|
|
50
|
+
}, 'Test error')
|
|
51
|
+
assert.equal(error.message, 'Test error')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('should validate object properties', ({ assert }) => {
|
|
55
|
+
const testObj = { name: 'test', value: 42 }
|
|
56
|
+
|
|
57
|
+
// This should not throw
|
|
58
|
+
TestUtils.validateRequiredProperties(testObj, ['name', 'value'])
|
|
59
|
+
|
|
60
|
+
// This should throw
|
|
61
|
+
assert.throws(() => {
|
|
62
|
+
// @ts-expect-error: Intentionally missing property
|
|
63
|
+
TestUtils.validateRequiredProperties(testObj, ['name', 'missing'])
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('should create test URLs with parameters', ({ assert }) => {
|
|
68
|
+
const url = TestUtils.createTestUrl('/api/users', {
|
|
69
|
+
page: '1',
|
|
70
|
+
limit: '10',
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
assert.equal(url.pathname, '/api/users')
|
|
74
|
+
assert.equal(url.searchParams.get('page'), '1')
|
|
75
|
+
assert.equal(url.searchParams.get('limit'), '10')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('should create test requests', ({ assert }) => {
|
|
79
|
+
const request = TestUtils.createTestRequest(
|
|
80
|
+
'POST',
|
|
81
|
+
'https://api.example.com/users',
|
|
82
|
+
JSON.stringify({ name: 'John' }),
|
|
83
|
+
{ Authorization: 'Bearer token123' }
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
assert.equal(request.method, 'POST')
|
|
87
|
+
assert.equal(request.url, 'https://api.example.com/users')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('should create mock auth config', ({ assert }) => {
|
|
91
|
+
const config = TestUtils.createMockAuthConfig()
|
|
92
|
+
assert.typeOf(config.clientId, 'string')
|
|
93
|
+
assert.typeOf(config.clientSecret, 'string')
|
|
94
|
+
assert.isArray(config.scopes)
|
|
95
|
+
assert.include(config.scopes, 'read')
|
|
96
|
+
assert.include(config.scopes, 'write')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('should test with sinon mocks', ({ sinon, assert }) => {
|
|
100
|
+
const mockObj = {
|
|
101
|
+
getValue: () => 'original',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const stub = sinon.stub(mockObj, 'getValue')
|
|
105
|
+
stub.returns('mocked')
|
|
106
|
+
|
|
107
|
+
assert.equal(mockObj.getValue(), 'mocked')
|
|
108
|
+
|
|
109
|
+
stub.restore()
|
|
110
|
+
assert.equal(mockObj.getValue(), 'original')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('should test timeout behavior', async ({ assert }) => {
|
|
114
|
+
const start = Date.now()
|
|
115
|
+
await TestUtils.sleep(100)
|
|
116
|
+
const elapsed = Date.now() - start
|
|
117
|
+
|
|
118
|
+
assert.isAtLeast(elapsed, 90) // Allow some variance
|
|
119
|
+
assert.isAtMost(elapsed, 150)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
export default {
|
|
124
|
+
setupNodeTests: () => {
|
|
125
|
+
// eslint-disable-next-line no-console
|
|
126
|
+
console.log('Node.js test environment initialized')
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
setupBrowserTests: () => {
|
|
130
|
+
// eslint-disable-next-line no-console
|
|
131
|
+
console.log('Browser test environment initialized')
|
|
132
|
+
},
|
|
133
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template for Node.js tests using Japa
|
|
3
|
+
* Copy this file and rename it to create new test suites.
|
|
4
|
+
*/
|
|
5
|
+
import { test } from '@japa/runner'
|
|
6
|
+
|
|
7
|
+
test.group('YourModuleName', (group) => {
|
|
8
|
+
// Setup runs before each test in the group
|
|
9
|
+
group.each.setup(() => {
|
|
10
|
+
// Initialize test data, mocks, etc.
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
// Teardown runs after each test in the group
|
|
14
|
+
group.each.teardown(() => {
|
|
15
|
+
// Clean up resources, restore mocks, etc.
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('should describe what this test does', ({ assert }) => {
|
|
19
|
+
// Arrange
|
|
20
|
+
const input = 'test-input'
|
|
21
|
+
|
|
22
|
+
// Act
|
|
23
|
+
const result = input.toUpperCase()
|
|
24
|
+
|
|
25
|
+
// Assert
|
|
26
|
+
assert.equal(result, 'TEST-INPUT')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('should test async operations', async ({ assert }) => {
|
|
30
|
+
// Arrange
|
|
31
|
+
const promise = Promise.resolve('async-result')
|
|
32
|
+
|
|
33
|
+
// Act
|
|
34
|
+
const result = await promise
|
|
35
|
+
|
|
36
|
+
// Assert
|
|
37
|
+
assert.equal(result, 'async-result')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('should test error cases', async ({ assert }) => {
|
|
41
|
+
// Assert that function throws
|
|
42
|
+
await assert.rejects(() => Promise.reject(new Error('Expected error')), 'Expected error')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('should use sinon for mocking', ({ sinon, assert }) => {
|
|
46
|
+
// Arrange
|
|
47
|
+
const mockObject = {
|
|
48
|
+
method: () => 'original',
|
|
49
|
+
}
|
|
50
|
+
const stub = sinon.stub(mockObject, 'method')
|
|
51
|
+
stub.returns('mocked')
|
|
52
|
+
|
|
53
|
+
// Act
|
|
54
|
+
const result = mockObject.method()
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
assert.equal(result, 'mocked')
|
|
58
|
+
|
|
59
|
+
// Cleanup (optional - sinon auto-restores)
|
|
60
|
+
stub.restore()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('should test with custom timeout', async ({ assert }) => {
|
|
64
|
+
// This test has a longer timeout
|
|
65
|
+
const start = Date.now()
|
|
66
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
67
|
+
const elapsed = Date.now() - start
|
|
68
|
+
assert.isAtLeast(elapsed, 90)
|
|
69
|
+
}).timeout(5000) // 5 second timeout
|
|
70
|
+
|
|
71
|
+
test('should be skipped', ({ assert }) => {
|
|
72
|
+
// This test is skipped
|
|
73
|
+
assert.fail('This should not run')
|
|
74
|
+
}).skip(true)
|
|
75
|
+
})
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { TestContext } from '@japa/runner/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Common test utilities for the API Client Core
|
|
5
|
+
*/
|
|
6
|
+
export const TestUtils = {
|
|
7
|
+
/**
|
|
8
|
+
* Creates a mock HTTP response
|
|
9
|
+
*/
|
|
10
|
+
createMockResponse(status = 200, body: unknown = {}, headers: Record<string, string> = {}): Response {
|
|
11
|
+
return new Response(JSON.stringify(body), {
|
|
12
|
+
status,
|
|
13
|
+
headers: {
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
...headers,
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a test URL with optional query parameters
|
|
22
|
+
*/
|
|
23
|
+
createTestUrl(path = '/', params: Record<string, string> = {}): URL {
|
|
24
|
+
const url = new URL(path, 'https://api.example.com')
|
|
25
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
26
|
+
url.searchParams.set(key, value)
|
|
27
|
+
})
|
|
28
|
+
return url
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Waits for a specified amount of time
|
|
33
|
+
*/
|
|
34
|
+
async sleep(ms: number): Promise<void> {
|
|
35
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates a test request object
|
|
40
|
+
*/
|
|
41
|
+
createTestRequest(
|
|
42
|
+
method = 'GET',
|
|
43
|
+
url: string | URL = 'https://api.example.com',
|
|
44
|
+
body?: BodyInit,
|
|
45
|
+
headers: Record<string, string> = {}
|
|
46
|
+
): Request {
|
|
47
|
+
return new Request(url, {
|
|
48
|
+
method,
|
|
49
|
+
body,
|
|
50
|
+
headers: {
|
|
51
|
+
'Content-Type': 'application/json',
|
|
52
|
+
...headers,
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generates random test data
|
|
59
|
+
*/
|
|
60
|
+
generateTestData() {
|
|
61
|
+
return {
|
|
62
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
63
|
+
timestamp: Date.now(),
|
|
64
|
+
randomString: Math.random().toString(36).substr(2, 10),
|
|
65
|
+
randomNumber: Math.floor(Math.random() * 1000),
|
|
66
|
+
randomBoolean: Math.random() > 0.5,
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates a mock authorization configuration
|
|
72
|
+
*/
|
|
73
|
+
createMockAuthConfig() {
|
|
74
|
+
return {
|
|
75
|
+
clientId: 'test-client-id',
|
|
76
|
+
clientSecret: 'test-client-secret',
|
|
77
|
+
authorizationUri: 'https://auth.example.com/oauth/authorize',
|
|
78
|
+
accessTokenUri: 'https://auth.example.com/oauth/token',
|
|
79
|
+
scopes: ['read', 'write'],
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Creates a mock API project structure
|
|
85
|
+
*/
|
|
86
|
+
createMockProject() {
|
|
87
|
+
return {
|
|
88
|
+
kind: 'Project',
|
|
89
|
+
key: 'test-project',
|
|
90
|
+
info: {
|
|
91
|
+
name: 'Test Project',
|
|
92
|
+
version: '1.0.0',
|
|
93
|
+
description: 'A test API project',
|
|
94
|
+
},
|
|
95
|
+
definitions: {},
|
|
96
|
+
environments: [],
|
|
97
|
+
requests: [],
|
|
98
|
+
folders: [],
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Asserts that an error is thrown with specific message
|
|
104
|
+
*/
|
|
105
|
+
async assertThrows(fn: () => Promise<unknown> | unknown, expectedMessage?: string | RegExp): Promise<Error> {
|
|
106
|
+
try {
|
|
107
|
+
await fn()
|
|
108
|
+
throw new Error('Expected function to throw an error')
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const typedError = error as Error
|
|
111
|
+
if (expectedMessage) {
|
|
112
|
+
if (typeof expectedMessage === 'string') {
|
|
113
|
+
if (!typedError.message.includes(expectedMessage)) {
|
|
114
|
+
throw new Error(`Expected error message to include "${expectedMessage}", got "${typedError.message}"`)
|
|
115
|
+
}
|
|
116
|
+
} else if (expectedMessage instanceof RegExp) {
|
|
117
|
+
if (!expectedMessage.test(typedError.message)) {
|
|
118
|
+
throw new Error(`Expected error message to match ${expectedMessage}, got "${typedError.message}"`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return typedError
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validates that an object has required properties
|
|
128
|
+
*/
|
|
129
|
+
validateRequiredProperties<T extends Record<string, unknown>>(obj: T, requiredProps: (keyof T)[]): void {
|
|
130
|
+
for (const prop of requiredProps) {
|
|
131
|
+
if (!(prop in obj)) {
|
|
132
|
+
throw new Error(`Missing required property: ${String(prop)}`)
|
|
133
|
+
}
|
|
134
|
+
if (obj[prop] === undefined || obj[prop] === null) {
|
|
135
|
+
throw new Error(`Property ${String(prop)} cannot be null or undefined`)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Creates a test environment configuration
|
|
142
|
+
*/
|
|
143
|
+
createTestEnvironment() {
|
|
144
|
+
return {
|
|
145
|
+
variables: [
|
|
146
|
+
{ name: 'baseUrl', value: 'https://api.example.com' },
|
|
147
|
+
{ name: 'apiKey', value: 'test-api-key' },
|
|
148
|
+
{ name: 'version', value: 'v1' },
|
|
149
|
+
],
|
|
150
|
+
oauth2: {
|
|
151
|
+
port: 8080,
|
|
152
|
+
issuer: 'https://oauth.example.com',
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Mocks the fetch function with a custom response
|
|
159
|
+
*/
|
|
160
|
+
mockFetch(response: Response | ((input: RequestInfo | URL, init?: RequestInit) => Response)): () => void {
|
|
161
|
+
const originalFetch = globalThis.fetch
|
|
162
|
+
|
|
163
|
+
globalThis.fetch =
|
|
164
|
+
typeof response === 'function'
|
|
165
|
+
? (input: RequestInfo | URL, init?: RequestInit) =>
|
|
166
|
+
Promise.resolve((response as (input: RequestInfo | URL, init?: RequestInit) => Response)(input, init))
|
|
167
|
+
: () => Promise.resolve(response)
|
|
168
|
+
|
|
169
|
+
// Return cleanup function
|
|
170
|
+
return () => {
|
|
171
|
+
globalThis.fetch = originalFetch
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Test decorators and helpers for Japa tests
|
|
178
|
+
*/
|
|
179
|
+
export const JapaTestHelpers = {
|
|
180
|
+
/**
|
|
181
|
+
* Timeout decorator for tests that may take longer
|
|
182
|
+
*/
|
|
183
|
+
timeout(ms: number) {
|
|
184
|
+
return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
185
|
+
const originalMethod = descriptor.value
|
|
186
|
+
descriptor.value = function (...args: unknown[]) {
|
|
187
|
+
const [context] = args
|
|
188
|
+
// Set timeout on test context (implementation may vary)
|
|
189
|
+
if (context && typeof context === 'object' && 'timeout' in context) {
|
|
190
|
+
;(context as { timeout: number }).timeout = ms
|
|
191
|
+
}
|
|
192
|
+
return originalMethod.apply(this, args)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Skip test conditionally
|
|
199
|
+
*/
|
|
200
|
+
skipIf(condition: boolean, reason?: string) {
|
|
201
|
+
return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
202
|
+
if (condition) {
|
|
203
|
+
descriptor.value = function (context: TestContext) {
|
|
204
|
+
context.test.skip(true, reason || 'Skipped conditionally')
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Run test only in specific environment
|
|
212
|
+
*/
|
|
213
|
+
onlyInEnvironment(env: 'node' | 'browser') {
|
|
214
|
+
return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
215
|
+
// eslint-disable-next-line no-restricted-globals
|
|
216
|
+
const isNode = typeof window === 'undefined'
|
|
217
|
+
const shouldRun = (env === 'node' && isNode) || (env === 'browser' && !isNode)
|
|
218
|
+
|
|
219
|
+
if (!shouldRun) {
|
|
220
|
+
descriptor.value = function (context: TestContext) {
|
|
221
|
+
context.test.skip(true, `Only runs in ${env} environment`)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Browser-specific test utilities
|
|
230
|
+
*/
|
|
231
|
+
export const BrowserTestUtils = {
|
|
232
|
+
/**
|
|
233
|
+
* Simulates a browser event
|
|
234
|
+
*/
|
|
235
|
+
dispatchEvent(element: Element, eventType: string, options: EventInit = {}): Event {
|
|
236
|
+
const event = new Event(eventType, { bubbles: true, cancelable: true, ...options })
|
|
237
|
+
element.dispatchEvent(event)
|
|
238
|
+
return event
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Creates a mock localStorage implementation
|
|
243
|
+
*/
|
|
244
|
+
createMockLocalStorage(): Storage {
|
|
245
|
+
const storage: Record<string, string> = {}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
getItem: (key: string) => storage[key] || null,
|
|
249
|
+
setItem: (key: string, value: string) => {
|
|
250
|
+
storage[key] = value
|
|
251
|
+
},
|
|
252
|
+
removeItem: (key: string) => {
|
|
253
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
254
|
+
delete storage[key]
|
|
255
|
+
},
|
|
256
|
+
clear: () => {
|
|
257
|
+
Object.keys(storage).forEach((key) => {
|
|
258
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
259
|
+
delete storage[key]
|
|
260
|
+
})
|
|
261
|
+
},
|
|
262
|
+
key: (index: number) => Object.keys(storage)[index] || null,
|
|
263
|
+
get length() {
|
|
264
|
+
return Object.keys(storage).length
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Waits for DOM element to appear
|
|
271
|
+
*/
|
|
272
|
+
async waitForElement(selector: string, timeout = 5000): Promise<Element> {
|
|
273
|
+
const startTime = Date.now()
|
|
274
|
+
|
|
275
|
+
while (Date.now() - startTime < timeout) {
|
|
276
|
+
// eslint-disable-next-line no-restricted-globals
|
|
277
|
+
const element = document.querySelector(selector)
|
|
278
|
+
if (element) {
|
|
279
|
+
return element
|
|
280
|
+
}
|
|
281
|
+
await TestUtils.sleep(50)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
throw new Error(`Element with selector "${selector}" not found within ${timeout}ms`)
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Creates a mock file for file upload testing
|
|
289
|
+
*/
|
|
290
|
+
createMockFile(name = 'test.txt', content = 'test content', type = 'text/plain'): File {
|
|
291
|
+
return new File([content], name, { type })
|
|
292
|
+
},
|
|
293
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { test } from '@japa/runner'
|
|
2
|
+
import {
|
|
3
|
+
createCalculatedSemantic,
|
|
4
|
+
isCalculatedSemantic,
|
|
5
|
+
type CalculatedConfig,
|
|
6
|
+
} from '../../../../src/modeling/definitions/Calculated.js'
|
|
7
|
+
import { SemanticType, type AppliedDataSemantic } from '../../../../src/modeling/Semantics.js'
|
|
8
|
+
|
|
9
|
+
test.group('Calculated Semantic Configuration', () => {
|
|
10
|
+
test('should create semantic with custom config', ({ assert }) => {
|
|
11
|
+
const config: CalculatedConfig = {
|
|
12
|
+
formula: 'price * quantity',
|
|
13
|
+
dependencies: ['price', 'quantity'],
|
|
14
|
+
recalculateOnUpdate: true,
|
|
15
|
+
allowManualOverride: true,
|
|
16
|
+
}
|
|
17
|
+
const semantic = createCalculatedSemantic(config)
|
|
18
|
+
assert.equal(semantic.id, SemanticType.Calculated)
|
|
19
|
+
assert.deepEqual(semantic.config, config)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('should identify calculated semantic', ({ assert }) => {
|
|
23
|
+
const config: CalculatedConfig = { formula: 'price * quantity' }
|
|
24
|
+
const semantic = createCalculatedSemantic(config)
|
|
25
|
+
assert.isTrue(isCalculatedSemantic(semantic))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('should not identify non-calculated semantic', ({ assert }) => {
|
|
29
|
+
// Simulate a different semantic
|
|
30
|
+
const fakeSemantic: AppliedDataSemantic = { id: SemanticType.Email, config: {} }
|
|
31
|
+
assert.isFalse(isCalculatedSemantic(fakeSemantic))
|
|
32
|
+
})
|
|
33
|
+
})
|