From f41b20acb35aace9e52dd8d9e0be3d03ed363b2e Mon Sep 17 00:00:00 2001 From: FredrikOseberg Date: Tue, 4 Feb 2025 12:56:09 +0100 Subject: [PATCH 1/6] feat: spec tests for hydration event --- specifications/19-delta-api-hydration.json | 61 ++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 specifications/19-delta-api-hydration.json diff --git a/specifications/19-delta-api-hydration.json b/specifications/19-delta-api-hydration.json new file mode 100644 index 0000000..517296c --- /dev/null +++ b/specifications/19-delta-api-hydration.json @@ -0,0 +1,61 @@ +{ + "name": "19-delta-api-hydration", + "state": { + "events": [ + { + "eventId": 1, + "type": "hydration", + "segments": [ + { + "id": 1, + "name": "delta.api.hydration.segment", + "constraints": [ + { + "values": [ + "abc", + "edf", + "gbd" + ], + "inverted": false, + "operator": "IN", + "contextName": "appName", + "caseInsensitive": false + } + ] + } + ], + "features": { + "impressionData": false, + "enabled": true, + "name": "delta.api.hydration", + "description": null, + "project": "default", + "stale": false, + "type": "release", + "variants": [], + "segments": [1], + "strategies": [ + { + "name": "flexibleRollout", + "constraints": [], + "parameters": { + "groupId": "delta.api.hydration", + "rollout": "100", + "stickiness": "default" + }, + "variants": [] + } + ] + } + } + ] + }, + "tests": [ + { + "description": "Hydration feature should be enabled when context criteria are met", + "context": { "appName": "abc" }, + "toggleName": "delta.api.hydration", + "expectedResult": true + } + ] +} From cd10dbef88d519ae396cdbd3ee604746cb2a9c21 Mon Sep 17 00:00:00 2001 From: FredrikOseberg Date: Tue, 4 Feb 2025 13:08:08 +0100 Subject: [PATCH 2/6] feat: spec tests for raw events --- specifications/20-delta-api-events.json | 111 ++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 specifications/20-delta-api-events.json diff --git a/specifications/20-delta-api-events.json b/specifications/20-delta-api-events.json new file mode 100644 index 0000000..24bdf46 --- /dev/null +++ b/specifications/20-delta-api-events.json @@ -0,0 +1,111 @@ +{ + "name": "19-delta-api-hydration", + "state": { + "events": [ + { + "eventId": 1, + "type": "feature-updated", + "feature": { + "impressionData": false, + "enabled": true, + "name": "delta.api.events.updated.1", + "description": null, + "project": "default", + "stale": false, + "type": "release", + "variants": [], + "strategies": [ + { + "name": "default" + } + ] + } + }, + { + "eventId": 2, + "type": "feature-updated", + "feature": { + "impressionData": false, + "enabled": true, + "name": "delta.api.events.updated.2", + "description": null, + "project": "default", + "stale": false, + "type": "release", + "variants": [], + "strategies": [ + { + "name": "default" + } + ] + } + }, + { + "eventId": 3, + "type": "feature-removed", + "featureName": "delta.api.events.updated.2" + }, + { + "eventId": 4, + "type": "segment-updated", + "segment": { + "id": 1, + "name": "my-segment", + "constraints": [ + { + "values": [ + "abc", + "edf", + "gbd" + ], + "inverted": false, + "operator": "IN", + "contextName": "appName", + "caseInsensitive": false + } + ] + } + }, + { + "eventId": 5, + "type": "feature-updated", + "feature": { + "impressionData": false, + "enabled": true, + "name": "delta.api.events.updated.3", + "description": null, + "project": "default", + "stale": false, + "type": "release", + "variants": [], + "strategies": [ + { + "name": "default", + "segments": [1] + } + ] + } + } + ] + }, + "tests": [ + { + "description": "delta.api.events.updated.1 should be enabled", + "context": {}, + "toggleName": "delta.api.events.updated.1", + "expectedResult": true + }, + { + "description": "delta.api.events.updated.2 should be disabled", + "context": {}, + "toggleName": "delta.api.events.updated.2", + "expectedResult": false + }, + { + "description": "delta.api.events.updated.3 should be enabled with segment", + "context": { "appName": "abc" }, + "toggleName": "delta.api.events.updated.3", + "expectedResult": true + } + ] +} From a5dbdeede44f7631e3602defb15ca57eee4332b7 Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Tue, 4 Feb 2025 14:14:58 +0200 Subject: [PATCH 3/6] chore: add delta tests to index file --- specifications/index.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/specifications/index.json b/specifications/index.json index 1f1a999..d2c446c 100644 --- a/specifications/index.json +++ b/specifications/index.json @@ -16,5 +16,7 @@ "15-global-constraints.json", "16-strategy-variants.json", "17-dependent-features.json", - "18-utf8-flag-names.json" -] + "18-utf8-flag-names.json", + "19-delta-api-hydration.json", + "20-delta-api-events.json" +] \ No newline at end of file From 68ac76d34d0983993a52db5a41a4f7272969faf4 Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Tue, 4 Feb 2025 15:08:13 +0200 Subject: [PATCH 4/6] fix: make features an array --- specifications/19-delta-api-hydration.json | 86 ++++++++++++---------- 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/specifications/19-delta-api-hydration.json b/specifications/19-delta-api-hydration.json index 517296c..cf49ed7 100644 --- a/specifications/19-delta-api-hydration.json +++ b/specifications/19-delta-api-hydration.json @@ -7,55 +7,61 @@ "type": "hydration", "segments": [ { - "id": 1, - "name": "delta.api.hydration.segment", - "constraints": [ - { - "values": [ - "abc", - "edf", - "gbd" - ], - "inverted": false, - "operator": "IN", - "contextName": "appName", - "caseInsensitive": false - } - ] + "id": 1, + "name": "delta.api.hydration.segment", + "constraints": [ + { + "values": [ + "abc", + "edf", + "gbd" + ], + "inverted": false, + "operator": "IN", + "contextName": "appName", + "caseInsensitive": false + } + ] } ], - "features": { - "impressionData": false, - "enabled": true, - "name": "delta.api.hydration", - "description": null, - "project": "default", - "stale": false, - "type": "release", - "variants": [], - "segments": [1], - "strategies": [ - { - "name": "flexibleRollout", - "constraints": [], - "parameters": { - "groupId": "delta.api.hydration", - "rollout": "100", - "stickiness": "default" - }, - "variants": [] - } - ] - } + "features": [ + { + "impressionData": false, + "enabled": true, + "name": "delta.api.hydration", + "description": null, + "project": "default", + "stale": false, + "type": "release", + "variants": [], + "segments": [ + 1 + ], + "strategies": [ + { + "name": "flexibleRollout", + "constraints": [], + "parameters": { + "groupId": "delta.api.hydration", + "rollout": "100", + "stickiness": "default" + }, + "variants": [] + } + ] + } + ] } ] }, "tests": [ { "description": "Hydration feature should be enabled when context criteria are met", - "context": { "appName": "abc" }, + "context": { + "appName": "abc" + }, "toggleName": "delta.api.hydration", "expectedResult": true } ] -} +} \ No newline at end of file From f72c9d1b5f1cd17fdca72b4a8b984dfc614e238d Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Wed, 5 Feb 2025 11:07:13 +0200 Subject: [PATCH 5/6] feat: add support for deltas api in the tests --- package.json | 2 +- schema/delta-event-schema.js | 29 ++++++++++++ schema/deltas-schema.js | 8 ++++ schema/feature-schema.js | 67 +++++++++++++++++++++++++++ schema/features-schema.js | 88 +++--------------------------------- schema/segment-schema.js | 18 ++++++++ schema/test-case-schema.js | 3 +- 7 files changed, 132 insertions(+), 83 deletions(-) create mode 100644 schema/delta-event-schema.js create mode 100644 schema/deltas-schema.js create mode 100644 schema/feature-schema.js create mode 100644 schema/segment-schema.js diff --git a/package.json b/package.json index 08e62e7..50374a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@unleash/client-specification", - "version": "5.1.9", + "version": "6.0.0", "description": "A collection of test specifications to guide client implementations in various languages", "scripts": { "test": "node index", diff --git a/schema/delta-event-schema.js b/schema/delta-event-schema.js new file mode 100644 index 0000000..e840181 --- /dev/null +++ b/schema/delta-event-schema.js @@ -0,0 +1,29 @@ +const Joi = require('joi'); +const featureSchema = require('./feature-schema'); +const segmentSchema = require('./segment-schema'); + +const deltaEventSchema = Joi.alternatives().try( + Joi.object({ + eventId: Joi.number().required(), + type: Joi.string().valid("hydration").required(), + segments: Joi.array().items(segmentSchema).optional(), + features: Joi.array().items(featureSchema).required(), + }), + Joi.object({ + eventId: Joi.number().required(), + type: Joi.string().valid("feature-updated").required(), + feature: featureSchema.required(), + }), + Joi.object({ + eventId: Joi.number().required(), + type: Joi.string().valid("feature-removed").required(), + featureName: Joi.string().required(), + }), + Joi.object({ + eventId: Joi.number().required(), + type: Joi.string().valid("segment-updated").required(), + segment: segmentSchema.required(), + }) +); + +module.exports = deltaEventSchema; diff --git a/schema/deltas-schema.js b/schema/deltas-schema.js new file mode 100644 index 0000000..a84879c --- /dev/null +++ b/schema/deltas-schema.js @@ -0,0 +1,8 @@ +const Joi = require('joi'); +const deltaEventSchema = require('./delta-event-schema.js'); + +const deltasSchema = Joi.object({ + events: Joi.array().items(deltaEventSchema).required(), +}); + +module.exports = deltasSchema; \ No newline at end of file diff --git a/schema/feature-schema.js b/schema/feature-schema.js new file mode 100644 index 0000000..0c63dea --- /dev/null +++ b/schema/feature-schema.js @@ -0,0 +1,67 @@ +const Joi = require('joi'); + +const featureSchema = Joi.object().keys({ + name: Joi.string().required(), + description: Joi.string().optional(), + enabled: Joi.boolean().required(), + project: Joi.string().optional(), + type: Joi.string().optional(), + description: Joi.string().optional().allow(null), + impressionData: Joi.boolean().optional(), + stale: Joi.boolean().optional(), + dependencies: Joi.array().optional().items( + Joi.object().keys({ + feature: Joi.string().required(), + enabled: Joi.bool().optional(), + variants: Joi.array().items(Joi.string()).optional() + }) + ), + segments: Joi.array().items(Joi.number()), + strategies: Joi.array().items( + Joi.object().keys({ + name: Joi.string().required(), + parameters: Joi.object(), + variants: Joi.array().items( + Joi.object().keys({ + name: Joi.string().required(), + payload: Joi.object().required().keys({ + type: Joi.string().required(), + value: Joi.string().required().allow(""), + }).optional(), + weight: Joi.number().min(0).max(100000), + stickiness: Joi.string().optional(), + }) + ).optional(), + constraints: Joi.array().items( + Joi.object().keys({ + contextName: Joi.string().required(), + operator: Joi.string().required(), + values: Joi.array().items(Joi.string()), + caseInsensitive: Joi.bool().optional(), + inverted: Joi.bool().optional(), + value: Joi.string().optional() + }).optional() + ), + segments: Joi.array().items(Joi.number()).optional() + }) + ), + variants: Joi.array().items( + Joi.object().keys({ + name: Joi.string().required(), + payload: Joi.object().required().keys({ + type: Joi.string().required(), + value: Joi.string().required().allow(""), + }).optional(), + weight: Joi.number().min(0).max(100000), + stickiness: Joi.string().optional(), + overrides: Joi.array().items( + Joi.object().keys({ + contextName: Joi.string().required(), + values: Joi.array().items(Joi.string()), + }).optional() + ), + }) + ) +}); + +module.exports = featureSchema; diff --git a/schema/features-schema.js b/schema/features-schema.js index c369a4a..2f20a1e 100644 --- a/schema/features-schema.js +++ b/schema/features-schema.js @@ -1,85 +1,11 @@ const Joi = require('joi'); +const featureSchema = require('./feature-schema'); +const segmentSchema = require('./segment-schema'); -const schema = Joi.object().keys({ - version: Joi.number() - .min(1) - .required(), - features: Joi.array().items( - Joi.object().keys({ - name: Joi.string().required(), - description: Joi.string().optional(), - enabled: Joi.boolean().required(), - dependencies: Joi.array().optional().items( - Joi.object().keys({ - feature: Joi.string().required(), - enabled: Joi.bool().optional(), - variants: Joi.array().items(Joi.string()).optional() - }) - ), - strategies: Joi.array().items( - Joi.object().keys({ - name: Joi.string().required(), - parameters: Joi.object(), - variants: Joi.array().items( - Joi.object().keys({ - name: Joi.string().required(), - payload: Joi.object().required().keys({ - type: Joi.string().required(), - value: Joi.string().required().allow(""), - }).optional(), - weight: Joi.number().min(0).max(100000), - stickiness: Joi.string().optional(), - })).optional(), - constraints: Joi.array().items( - Joi.object().keys({ - contextName: Joi.string().required(), - operator: Joi.string().required(), - values: Joi.array().items(Joi.string()), - caseInsensitive: Joi.bool().optional(), - inverted: Joi.bool().optional(), - value: Joi.string().optional() - }).optional() - ), - segments: Joi.array().items(Joi.number()).optional() - }) - ), - variants: Joi.array().items( - Joi.object().keys({ - name: Joi.string().required(), - payload: Joi.object().required().keys({ - type: Joi.string().required(), - value: Joi.string().required().allow(""), - }).optional(), - weight: Joi.number().min(0).max(100000), - stickiness: Joi.string().optional(), - overrides: Joi.array().items( - Joi - .object() - .keys({ - contextName: Joi.string().required(), - values: Joi.array().items(Joi.string()), - }) - .optional() - ), - }) - ) - }) - ), - segments: Joi.array().items( - Joi.object().keys({ - id: Joi.number().required(), - constraints: Joi.array().items( - Joi.object().keys({ - contextName: Joi.string().required(), - operator: Joi.string().required(), - values: Joi.array().items(Joi.string()), - caseInsensitive: Joi.bool().optional(), - inverted: Joi.bool().optional(), - value: Joi.string().optional() - }).optional() - ), - }).optional() - ) +const featuresSchema = Joi.object().keys({ + version: Joi.number().min(1).required(), + features: Joi.array().items(featureSchema), + segments: Joi.array().items(segmentSchema) }); -module.exports = schema; +module.exports = featuresSchema; \ No newline at end of file diff --git a/schema/segment-schema.js b/schema/segment-schema.js new file mode 100644 index 0000000..7f3b881 --- /dev/null +++ b/schema/segment-schema.js @@ -0,0 +1,18 @@ +const Joi = require('joi'); + +const segmentSchema = Joi.object().keys({ + id: Joi.number().required(), + name: Joi.string().optional().allow(null), + constraints: Joi.array().items( + Joi.object().keys({ + contextName: Joi.string().required(), + operator: Joi.string().required(), + values: Joi.array().items(Joi.string()), + caseInsensitive: Joi.bool().optional(), + inverted: Joi.bool().optional(), + value: Joi.string().optional() + }).optional() + ), +}); + +module.exports = segmentSchema; diff --git a/schema/test-case-schema.js b/schema/test-case-schema.js index b0201dd..1d44127 100644 --- a/schema/test-case-schema.js +++ b/schema/test-case-schema.js @@ -1,10 +1,11 @@ const Joi = require('joi'); const featuresSchema = require('./features-schema.js'); +const deltasSchema = require('./deltas-schema.js'); const contextSchema = require('./context-schema.js'); const schema = Joi.object().keys({ name: Joi.string(), - state: featuresSchema, + state: Joi.alternatives().try(featuresSchema, deltasSchema).required(), tests: Joi.array().items( Joi.object().keys({ description: Joi.string().required(), From e7168c4e84e761bbf7d0a3c31993fc73cea879a3 Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Wed, 5 Feb 2025 11:20:13 +0200 Subject: [PATCH 6/6] chore: rollback version, a minor bump makes more sense here --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 50374a9..d4d08d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@unleash/client-specification", - "version": "6.0.0", + "version": "5.2.0", "description": "A collection of test specifications to guide client implementations in various languages", "scripts": { "test": "node index",