ops: add CI/CD pipeline, a/b rolling deploy, Gitea Actions workflow
Deploy to Production / deploy (push) Failing after 10s
Deploy to Production / deploy (push) Failing after 10s
- .gitea/workflows/deploy.yml — push-to-main triggers rolling deploy - scripts/deploy-bluegreen.sh — a-stack then b-stack restart; Maven runs in Docker (no JDK needed on runner host); Caddy reload at end - scripts/deploy-all.ps1 — emergency manual deploy from dev machine - infra/docker-compose.yml — a/b pairs per service; wget health checks; Gitea service; Prometheus/Grafana/DB ports restricted to localhost - infra/Caddyfile — dual upstreams with health-based routing - infra/Dockerfile.* — one per service - infra/prometheus.yml + grafana provisioning Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
Feature: IoT device connection profile
|
||||
As a service provider targeting IoT device portability
|
||||
I want to declare my device connection profile in the APIX registry
|
||||
So that migrating IoT devices can discover how to connect to my service
|
||||
|
||||
Background:
|
||||
Given a deprecated service "SmartHub Cloud" with locked set to false
|
||||
And a registered service "OpenHub" in stage "PRODUCTION" with O-level "IDENTITY_VERIFIED"
|
||||
And a registered service "CloudBridge" in stage "PRODUCTION" with O-level "LEGAL_ENTITY_VERIFIED"
|
||||
|
||||
Scenario: Provider adds an IoT profile to their service
|
||||
When "OpenHub" sets its IoT profile with hubUrl "wss://hub.openhub.io/v2" protocols "MQTT_5_0" deviceClasses "device.class.smart-home-hub"
|
||||
Then the response is HTTP 200
|
||||
And GET /services/{openHubId} includes an iotProfile
|
||||
And the iotProfile.hubUrl is "wss://hub.openhub.io/v2"
|
||||
And the iotProfile.protocols contains "MQTT_5_0"
|
||||
|
||||
Scenario: IoT profile is included in replacement candidates
|
||||
Given "OpenHub" has IoT profile hubUrl "wss://hub.openhub.io/v2" protocols "MQTT_5_0" deviceClasses "device.class.smart-home-hub"
|
||||
And "OpenHub" has declared compatibility with "SmartHub Cloud"
|
||||
When GET /services/{smartHubCloudId}/replacements is called
|
||||
Then the response is HTTP 200
|
||||
And the candidate "OpenHub" has an iotProfile with hubUrl "wss://hub.openhub.io/v2"
|
||||
|
||||
Scenario: iotReady=true excludes candidates without a profile
|
||||
Given "OpenHub" has declared compatibility with "SmartHub Cloud"
|
||||
And "CloudBridge" has IoT profile hubUrl "wss://hub.cloudbridge.io" protocols "MQTT_5_0" deviceClasses "device.class.smart-home-hub"
|
||||
And "CloudBridge" has declared compatibility with "SmartHub Cloud"
|
||||
When GET /services/{smartHubCloudId}/replacements?iotReady=true is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And the response contains 1 candidate
|
||||
And the candidate is "CloudBridge"
|
||||
|
||||
Scenario: deviceClass filter narrows replacement candidates
|
||||
Given "OpenHub" has IoT profile hubUrl "wss://hub.openhub.io" protocols "MQTT_5_0" deviceClasses "device.class.smart-home-hub"
|
||||
And "OpenHub" has declared compatibility with "SmartHub Cloud"
|
||||
And "CloudBridge" has IoT profile hubUrl "wss://hub.cloudbridge.io" protocols "MQTT_5_0" deviceClasses "device.class.industrial-sensor"
|
||||
And "CloudBridge" has declared compatibility with "SmartHub Cloud"
|
||||
When GET /services/{smartHubCloudId}/replacements?deviceClass=device.class.smart-home-hub is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And the response contains 1 candidate
|
||||
And the candidate is "OpenHub"
|
||||
|
||||
Scenario: IoT profile fields are partially updated on subsequent PATCH
|
||||
Given "OpenHub" has IoT profile hubUrl "wss://old.openhub.io" protocols "MQTT_5_0" deviceClasses "device.class.smart-home-hub"
|
||||
When "OpenHub" updates its IoT profile hubUrl to "wss://new.openhub.io"
|
||||
Then the response is HTTP 200
|
||||
And GET /services/{openHubId} includes an iotProfile
|
||||
And the iotProfile.hubUrl is "wss://new.openhub.io"
|
||||
And the iotProfile.protocols contains "MQTT_5_0"
|
||||
|
||||
Scenario: protocol filter narrows replacement candidates
|
||||
Given "OpenHub" has IoT profile hubUrl "wss://hub.openhub.io" protocols "MQTT_5_0" deviceClasses "device.class.smart-home-hub"
|
||||
And "OpenHub" has declared compatibility with "SmartHub Cloud"
|
||||
And "CloudBridge" has IoT profile hubUrl "wss://hub.cloudbridge.io" protocols "WEBSOCKET" deviceClasses "device.class.smart-home-hub"
|
||||
And "CloudBridge" has declared compatibility with "SmartHub Cloud"
|
||||
When GET /services/{smartHubCloudId}/replacements?protocol=MQTT_5_0 is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And the response contains 1 candidate
|
||||
And the candidate is "OpenHub"
|
||||
|
||||
Scenario: Full IoT profile round-trip persists all declared fields
|
||||
When "OpenHub" sets its full IoT profile
|
||||
Then the response is HTTP 200
|
||||
And GET /services/{openHubId} includes an iotProfile
|
||||
And the iotProfile field "hubUrl" is "wss://hub.openhub.io/v2"
|
||||
And the iotProfile field "tlsMinVersion" is "1.3"
|
||||
And the iotProfile field "provisioningEndpoint" is "https://onboard.openhub.io/v1/provision"
|
||||
And the iotProfile field "credentialFormat" is "JWT"
|
||||
And the iotProfile field "deviceIdField" is "serialNumber"
|
||||
And the iotProfile field "minFirmwareVersion" is "2.0.0"
|
||||
And the iotProfile field "firmwareUpdateUrl" is "https://firmware.openhub.io/update"
|
||||
And the iotProfile.estimatedMigrationMinutes is 5
|
||||
|
||||
Scenario: Device reads IoT connection profile anonymously without leaving a trace
|
||||
Given "OpenHub" has IoT profile hubUrl "wss://hub.openhub.io/v2" protocols "MQTT_5_0" deviceClasses "device.class.smart-home-hub"
|
||||
And "OpenHub" has declared compatibility with "SmartHub Cloud"
|
||||
When GET /services/{smartHubCloudId}/replacements is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And the candidate "OpenHub" has an iotProfile with hubUrl "wss://hub.openhub.io/v2"
|
||||
And the response headers contain no Set-Cookie
|
||||
And the response headers contain a Server-Timing header
|
||||
|
||||
Scenario: Server-Timing header is present on replacement search responses
|
||||
Given "OpenHub" has declared compatibility with "SmartHub Cloud"
|
||||
When GET /services/{smartHubCloudId}/replacements is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And the response headers contain a Server-Timing header
|
||||
|
||||
Scenario: Missing required fields on first IoT profile PATCH returns 422
|
||||
When "OpenHub" patches iotProfile with only hubUrl "wss://hub.openhub.io"
|
||||
Then the response is HTTP 422
|
||||
And the error message contains "iotProfile.protocols is required"
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
Feature: Device owner anonymity guarantee
|
||||
As an IoT device owner
|
||||
I must be able to use the full transition discovery process
|
||||
Without APIX storing or inferring any artefact that identifies me or my device
|
||||
|
||||
Background:
|
||||
Given a deprecated service "SmartHub Cloud" with locked set to false
|
||||
And at least one replacement candidate registered
|
||||
|
||||
Scenario: Status polling requires no authentication
|
||||
When GET /services/{smartHubCloudId} is called with no Authorization header
|
||||
Then the response is HTTP 200
|
||||
And locked and sunset_date are present in the response body
|
||||
|
||||
Scenario: Replacement discovery requires no authentication
|
||||
When GET /services/{smartHubCloudId}/replacements is called with no Authorization header
|
||||
Then the response is HTTP 200
|
||||
And the full replacement list is returned
|
||||
|
||||
Scenario: No session state is created during polling
|
||||
When GET /services/{smartHubCloudId}/replacements is called twice with no shared headers
|
||||
Then both responses are identical in content
|
||||
And neither response contains a Set-Cookie header
|
||||
And neither response contains a session reference
|
||||
|
||||
Scenario: Response contains no client-tracking artefacts
|
||||
When GET /services/{smartHubCloudId}/replacements is called
|
||||
Then the response headers contain no Set-Cookie
|
||||
And the response body contains no field that echoes client request details
|
||||
And the response body contains no correlation ID tied to the caller
|
||||
|
||||
Scenario: Polling endpoint is covered by the public cache
|
||||
When GET /services/{smartHubCloudId}/replacements is called twice within the cache TTL
|
||||
Then the second response is served from cache
|
||||
And the cache key does not incorporate any client-identifying header
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
Feature: Service decommissioning and historical record preservation
|
||||
As a template owner
|
||||
I want to decommission a service after its sunset date
|
||||
And as a device owner I must be able to access the historical record indefinitely
|
||||
|
||||
Background:
|
||||
Given a registered service "SmartHub Cloud" with endpoint "https://api.smarthub.example"
|
||||
And the service has capability "device.telemetry"
|
||||
And the service has locked set to true
|
||||
And the service is in stage "DEPRECATED" with a sunset_date set
|
||||
And the service has locked set to false
|
||||
And a registered service "OpenHub" with endpoint "https://api.openhub.example"
|
||||
And "OpenHub" has declared compatibility with "SmartHub Cloud"
|
||||
And the sunset_date of "SmartHub Cloud" has passed
|
||||
|
||||
Scenario: Template owner decommissions the service
|
||||
When the template owner sets service_stage to "DECOMMISSIONED"
|
||||
Then the service does not appear in any capability search results
|
||||
And GET /services/{id} returns HTTP 200 with the complete historical record
|
||||
And the response body contains service_stage "DECOMMISSIONED"
|
||||
And a version history entry of type "STAGE_CHANGED" exists for the service
|
||||
|
||||
Scenario: Decommissioned service with unreleased lock auto-releases for replacement discovery
|
||||
Given a deprecated service "NeverReleased" that reached DECOMMISSIONED without setting locked=false
|
||||
When GET /services/{neverReleasedId}/replacements is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And replacement candidates are returned regardless of the stored locked value
|
||||
|
||||
Scenario: Historical record survives indefinitely
|
||||
Given a decommissioned service
|
||||
When GET /services/{id} is called
|
||||
Then the response is HTTP 200
|
||||
And the full BSM payload is present in the response
|
||||
And all version history entries are accessible via GET /services/{id}/history
|
||||
|
||||
Scenario: Cannot decommission a service before its sunset date
|
||||
Given a deprecated service "FutureCloud" with a sunset_date 30 days from now
|
||||
When the template owner attempts to set service_stage to "DECOMMISSIONED"
|
||||
Then the response is HTTP 422
|
||||
And the error message contains "sunset_at has not passed"
|
||||
|
||||
Scenario: Full transition timeline is visible in version history
|
||||
Given "SmartHub Cloud" has completed the full lifecycle
|
||||
When GET /services/{id}/history is called
|
||||
Then the history contains an entry of type "REGISTERED"
|
||||
And the history contains an entry of type "SUNSET_DECLARED"
|
||||
And the history contains an entry of type "LOCK_RELEASED"
|
||||
And the history contains an entry of type "REPLACEMENT_DECLARED"
|
||||
And the history contains an entry of type "STAGE_CHANGED" with new value "DECOMMISSIONED"
|
||||
And all entries are ordered chronologically ascending
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
Feature: Replacement provider declares compatibility
|
||||
As a replacement service provider
|
||||
I want to declare that my service covers a deprecated template
|
||||
So that IoT device owners can discover me as a migration target
|
||||
|
||||
Background:
|
||||
Given a deprecated service "SmartHub Cloud" with locked set to false
|
||||
And a registered service "OpenHub" in stage "PRODUCTION" with O-level "IDENTITY_VERIFIED"
|
||||
|
||||
Scenario: Replacement provider declares compatibility with a deprecated service
|
||||
When "OpenHub" declares replacesServiceIds containing the ID of "SmartHub Cloud"
|
||||
Then GET /services/{smartHubCloudId}/replacements includes "OpenHub"
|
||||
And a version history entry of type "REPLACEMENT_DECLARED" exists for "SmartHub Cloud"
|
||||
And the service_replacements table contains the declared pair
|
||||
|
||||
Scenario: Declaration against a non-deprecated service is rejected
|
||||
Given a service "ActiveCloud" in stage "PRODUCTION" with locked set to true
|
||||
When "OpenHub" declares replacesServiceIds containing the ID of "ActiveCloud"
|
||||
Then the response is HTTP 422
|
||||
And the error message contains "target service is not deprecated"
|
||||
|
||||
Scenario: Declaration against a locked deprecated service is rejected
|
||||
Given a service "LockedCloud" in stage "DEPRECATED" with locked set to true
|
||||
When "OpenHub" declares replacesServiceIds containing the ID of "LockedCloud"
|
||||
Then the response is HTTP 422
|
||||
And the error message contains "target service lock has not been released"
|
||||
|
||||
Scenario: Multiple replacement providers for the same deprecated service
|
||||
Given a service "CloudBridge" in stage "PRODUCTION" with O-level "LEGAL_ENTITY_VERIFIED"
|
||||
When "OpenHub" declares replacesServiceIds containing the ID of "SmartHub Cloud"
|
||||
And "CloudBridge" declares replacesServiceIds containing the ID of "SmartHub Cloud"
|
||||
Then GET /services/{smartHubCloudId}/replacements returns 2 candidates
|
||||
And "CloudBridge" appears before "OpenHub" in the results
|
||||
And the ordering is by O-level descending
|
||||
|
||||
Scenario: Replacement provider retracts their compatibility declaration
|
||||
Given "OpenHub" has declared compatibility with "SmartHub Cloud"
|
||||
When "OpenHub" removes "SmartHub Cloud" from its replacesServiceIds
|
||||
Then GET /services/{smartHubCloudId}/replacements no longer includes "OpenHub"
|
||||
And the service_replacements row for this pair is deleted
|
||||
|
||||
Scenario: Duplicate declaration is idempotent
|
||||
Given "OpenHub" has declared compatibility with "SmartHub Cloud"
|
||||
When "OpenHub" declares replacesServiceIds containing the ID of "SmartHub Cloud" again
|
||||
Then the response is HTTP 200
|
||||
And GET /services/{smartHubCloudId}/replacements still returns exactly 1 candidate
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
Feature: Device owner discovers replacement services
|
||||
As an IoT device owner
|
||||
I want to discover compatible replacement services by polling APIX
|
||||
Without revealing my identity or device details
|
||||
|
||||
Background:
|
||||
Given a deprecated service "SmartHub Cloud" with locked set to false and a sunset_date set
|
||||
And "OpenHub" in stage "PRODUCTION" with O-level "IDENTITY_VERIFIED" has declared compatibility
|
||||
And "CloudBridge" in stage "PRODUCTION" with O-level "LEGAL_ENTITY_VERIFIED" has declared compatibility
|
||||
|
||||
Scenario: Device polls service status without authentication
|
||||
When GET /services/{smartHubCloudId} is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And the response body contains service_stage "DEPRECATED"
|
||||
And the response body contains locked false
|
||||
And the response body contains a sunset_date
|
||||
|
||||
Scenario: Device discovers replacement candidates without authentication
|
||||
When GET /services/{smartHubCloudId}/replacements is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And the response contains 2 candidates
|
||||
And "CloudBridge" appears before "OpenHub" in the results
|
||||
|
||||
Scenario: Device filters candidates by minimum O-level
|
||||
When GET /services/{smartHubCloudId}/replacements?minOLevel=LEGAL_ENTITY_VERIFIED is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And the response contains 1 candidate
|
||||
And the candidate is "CloudBridge"
|
||||
|
||||
Scenario: Device polls a still-locked deprecated service
|
||||
Given a deprecated service "LockedCloud" with locked set to true
|
||||
When GET /services/{lockedCloudId}/replacements is called with no authentication header
|
||||
Then the response is HTTP 200
|
||||
And the response body contains an empty candidates list
|
||||
And the response body contains locked true
|
||||
|
||||
Scenario: Default production search excludes deprecated services
|
||||
When GET /services?capability=device.telemetry is called with no authentication header
|
||||
Then "SmartHub Cloud" is not in the results
|
||||
And only services with stage "PRODUCTION" are returned
|
||||
|
||||
Scenario: Explicit deprecated filter includes deprecated services
|
||||
When GET /services?capability=device.telemetry&stage=deprecated is called
|
||||
Then "SmartHub Cloud" is in the results
|
||||
And each result has service_stage "DEPRECATED"
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
Feature: Sunset declaration and lock release
|
||||
As a template owner
|
||||
I want to declare a sunset date and release the device lock
|
||||
So that IoT device owners can prepare for migration in a predictable window
|
||||
|
||||
Background:
|
||||
Given a registered service "SmartHub Cloud" with endpoint "https://api.smarthub.example"
|
||||
And the service has capability "device.telemetry"
|
||||
And the service is in stage "PRODUCTION"
|
||||
And the service has locked set to true
|
||||
|
||||
Scenario: Template owner declares sunset date
|
||||
When the template owner updates the service with sunset_date 90 days from now and stage "DEPRECATED"
|
||||
Then the service stage is "DEPRECATED"
|
||||
And the service has a sunset_date set
|
||||
And a version history entry of type "SUNSET_DECLARED" exists for the service
|
||||
And the service does not appear in default production search results for capability "device.telemetry"
|
||||
And the service appears in search results when stage filter is "deprecated"
|
||||
|
||||
Scenario: Sunset declaration preserves the device lock
|
||||
When the template owner updates the service with sunset_date 90 days from now and stage "DEPRECATED"
|
||||
Then the service has locked set to true
|
||||
And GET /services/{id}/replacements returns HTTP 200 with an empty list
|
||||
|
||||
Scenario: Template owner releases the device lock after sunset is declared
|
||||
Given the service is in stage "DEPRECATED" with a sunset_date set
|
||||
When the template owner sets locked to false
|
||||
Then the service has locked set to false
|
||||
And a version history entry of type "LOCK_RELEASED" exists for the service
|
||||
And the version history entry contains the previous locked value true
|
||||
|
||||
Scenario: Lock cannot be released without a prior sunset declaration
|
||||
When the template owner attempts to set locked to false without a sunset_date
|
||||
Then the response is HTTP 422
|
||||
And the error message contains "sunset_at required before lock release"
|
||||
|
||||
Scenario: Sunset date cannot be set in the past
|
||||
When the template owner attempts to set sunset_date to yesterday
|
||||
Then the response is HTTP 422
|
||||
And the error message contains "sunset_at must be a future moment"
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
Feature: Organisation audit log
|
||||
# Immutable, append-only trail of all events affecting an organisation.
|
||||
# Two authorised views: org owner (transparency) and BSF admin (yearly reporting).
|
||||
# Unauthenticated callers are rejected.
|
||||
|
||||
Background:
|
||||
Given the organisation registry is empty
|
||||
|
||||
# ── Access control ────────────────────────────────────────────────────────────
|
||||
|
||||
Scenario: Org owner can read their own audit log
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
When the owner requests the audit log
|
||||
Then the response status is 200
|
||||
And the audit log contains at least 1 event
|
||||
|
||||
Scenario: BSF admin can read any org's audit log
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
When a BSF admin requests the audit log
|
||||
Then the response status is 200
|
||||
And the audit log contains at least 1 event
|
||||
|
||||
Scenario: Unauthenticated caller cannot read the audit log
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
When an unauthenticated caller requests the audit log
|
||||
Then the response status is 403
|
||||
|
||||
# ── Registration events ────────────────────────────────────────────────────────
|
||||
|
||||
Scenario: Audit log records registration
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
When the owner requests the audit log
|
||||
Then the audit log contains a "REGISTERED" event triggered by "SYSTEM"
|
||||
|
||||
# ── Key rotation events — TAN path ────────────────────────────────────────────
|
||||
|
||||
Scenario: TAN issuance is recorded in audit log
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the owner has initiated key rotation using the rotation secret
|
||||
When the owner requests the audit log
|
||||
Then the audit log contains a "TAN_ISSUED" event triggered by "SYSTEM"
|
||||
|
||||
Scenario: TAN-based key rotation completion is recorded in audit log
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the owner has initiated key rotation using the rotation secret
|
||||
When the owner confirms key rotation with the TAN
|
||||
And the owner requests the audit log with the new api key
|
||||
Then the audit log contains a "KEY_ROTATED" event triggered by "OWNER"
|
||||
|
||||
# ── Key rotation events — DNS path ────────────────────────────────────────────
|
||||
|
||||
Scenario: DNS challenge initiation is recorded in audit log
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the agent has initiated DNS-challenge key rotation
|
||||
When the owner requests the audit log
|
||||
Then the audit log contains a "DNS_ROTATION_INITIATED" event triggered by "SYSTEM"
|
||||
|
||||
Scenario: DNS-challenge key rotation completion is recorded in audit log
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the agent has initiated DNS-challenge key rotation
|
||||
And the agent has published the rotation challenge to dns
|
||||
When the agent confirms DNS-challenge key rotation
|
||||
And the owner requests the audit log with the new api key
|
||||
Then the audit log contains a "KEY_ROTATED" event triggered by "OWNER"
|
||||
|
||||
# ── Fraud lock events ─────────────────────────────────────────────────────────
|
||||
|
||||
Scenario: Fraud report is recorded in audit log
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the owner reports key rotation fraud using the registered email
|
||||
When the owner requests the audit log
|
||||
Then the audit log contains a "FRAUD_REPORTED" event triggered by "OWNER"
|
||||
|
||||
Scenario: Fraud lock clearance is recorded in audit log
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the owner reports key rotation fraud using the registered email
|
||||
When a BSF admin clears the fraud lock
|
||||
And a BSF admin requests the audit log
|
||||
Then the audit log contains a "FRAUD_LOCK_CLEARED" event triggered by "BSF_ADMIN"
|
||||
|
||||
# ── Event ordering ─────────────────────────────────────────────────────────────
|
||||
|
||||
Scenario: Audit log returns events newest first
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the owner has initiated key rotation using the rotation secret
|
||||
When the owner requests the audit log
|
||||
Then the first audit event is "TAN_ISSUED"
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
Feature: BSF admin actions — temp grants, revocation, TAN-based key rotation
|
||||
|
||||
Background:
|
||||
Given the organisation registry is empty
|
||||
|
||||
# ── Temporary O-level grants ──────────────────────────────────────────────────
|
||||
|
||||
Scenario: BSF admin grants a temporary elevated O-level
|
||||
Given an organisation has earned O-level "IDENTITY_VERIFIED" with target "IDENTITY_VERIFIED"
|
||||
When a BSF admin grants a temporary level "OPERATIONALLY_VERIFIED" expiring in 30 days
|
||||
Then the response status is 200
|
||||
And the effective O-level is "OPERATIONALLY_VERIFIED"
|
||||
And the earned O-level is "IDENTITY_VERIFIED"
|
||||
|
||||
Scenario: Effective O-level drops back to earned level after temp grant expires
|
||||
Given an organisation has earned O-level "IDENTITY_VERIFIED" with target "IDENTITY_VERIFIED"
|
||||
And a BSF admin grants a temporary level "OPERATIONALLY_VERIFIED" expiring in 2 hours
|
||||
When time advances by 3 hours
|
||||
And the caller reads the organisation
|
||||
Then the effective O-level is "IDENTITY_VERIFIED"
|
||||
|
||||
Scenario: BSF admin revokes an active temporary grant
|
||||
Given an organisation has earned O-level "IDENTITY_VERIFIED" with target "IDENTITY_VERIFIED"
|
||||
And a BSF admin grants a temporary level "OPERATIONALLY_VERIFIED" expiring in 30 days
|
||||
When a BSF admin revokes the temporary grant
|
||||
Then the response status is 200
|
||||
And the effective O-level is "IDENTITY_VERIFIED"
|
||||
|
||||
Scenario: Revoking a non-existent temporary grant returns 422
|
||||
Given an organisation has earned O-level "IDENTITY_VERIFIED" with target "IDENTITY_VERIFIED"
|
||||
When a BSF admin revokes the temporary grant
|
||||
Then the response status is 422
|
||||
|
||||
Scenario: Temp grant with expiry in the past is rejected
|
||||
Given an organisation has earned O-level "IDENTITY_VERIFIED" with target "IDENTITY_VERIFIED"
|
||||
When a BSF admin grants a temporary level "OPERATIONALLY_VERIFIED" expiring in the past
|
||||
Then the response status is 422
|
||||
|
||||
Scenario: Temp grant request with wrong admin key is rejected
|
||||
Given an organisation has earned O-level "IDENTITY_VERIFIED" with target "IDENTITY_VERIFIED"
|
||||
When an unauthorised caller grants a temporary level "OPERATIONALLY_VERIFIED"
|
||||
Then the response status is 403
|
||||
|
||||
# ── Admin: manual O-level assignment ─────────────────────────────────────────
|
||||
|
||||
Scenario: BSF admin assigns earned level manually (O-4 post review)
|
||||
Given an organisation is registered with target level "OPERATIONALLY_VERIFIED" for domain "example.com"
|
||||
When a BSF admin assigns earned level "OPERATIONALLY_VERIFIED"
|
||||
Then the response status is 200
|
||||
And the earned O-level is "OPERATIONALLY_VERIFIED"
|
||||
And the verification status is "ACHIEVED"
|
||||
|
||||
# ── Self-service key rotation (2FA: rotation secret + email TAN) ────────────────
|
||||
|
||||
Scenario: Owner rotates keys — rotation secret triggers TAN, TAN completes rotation
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
When the owner initiates key rotation using the rotation secret
|
||||
Then the response status is 200
|
||||
And the response contains a key rotation warning with the org name
|
||||
And a key rotation TAN has been sent
|
||||
When the owner confirms key rotation with the TAN
|
||||
Then the response status is 200
|
||||
And a new api key with prefix "apix_org_" is returned
|
||||
And a new rotation secret with prefix "apix_rot_" is returned
|
||||
And the old api key is no longer valid
|
||||
|
||||
Scenario: Key rotation initiation with wrong rotation secret is rejected
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
When the owner initiates key rotation using an invalid rotation secret
|
||||
Then the response status is 403
|
||||
|
||||
Scenario: Key rotation TAN expires before confirmation — rotation is rejected
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the owner has initiated key rotation using the rotation secret
|
||||
When time advances by 6 minutes
|
||||
And the owner confirms key rotation with the TAN
|
||||
Then the response status is 422
|
||||
|
||||
# ── Fraud report ─────────────────────────────────────────────────────────────
|
||||
|
||||
Scenario: Legitimate owner reports a key rotation they did not initiate
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
When the owner reports key rotation fraud using the registered email
|
||||
Then the response status is 200
|
||||
And the response confirms the fraud notification was received
|
||||
|
||||
Scenario: Fraud report with unrecognised email returns generic confirmation
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
When a caller reports key rotation fraud using "unknown@other.com"
|
||||
Then the response status is 200
|
||||
And the response confirms the fraud notification was received
|
||||
|
||||
Scenario: Fraud report cancels the pending TAN and locks the org against further rotation
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the owner has initiated key rotation using the rotation secret
|
||||
When the owner reports key rotation fraud using the registered email
|
||||
Then the response status is 200
|
||||
And the response confirms the fraud notification was received
|
||||
When the owner confirms key rotation with the TAN
|
||||
Then the response status is 422
|
||||
|
||||
Scenario: After fraud lock, rotation secret cannot initiate a new rotation
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the owner reports key rotation fraud using the registered email
|
||||
When the owner initiates key rotation using the rotation secret
|
||||
Then the response status is 422
|
||||
|
||||
Scenario: BSF admin clears fraud lock after investigation
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the owner reports key rotation fraud using the registered email
|
||||
When a BSF admin clears the fraud lock
|
||||
Then the response status is 200
|
||||
And the org is no longer fraud-locked
|
||||
When the owner initiates key rotation using the rotation secret
|
||||
Then the response status is 200
|
||||
|
||||
# ── Emergency TAN-based key rotation ─────────────────────────────────────────
|
||||
|
||||
Scenario: Owner requests a TAN and uses it to rotate keys
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
When the owner requests a TAN using the registered email
|
||||
Then the response status is 200
|
||||
And the response message confirms TAN was sent
|
||||
When the owner uses the TAN to rotate keys
|
||||
Then the response status is 200
|
||||
And a new api key with prefix "apix_org_" is returned
|
||||
And a new rotation secret with prefix "apix_rot_" is returned
|
||||
|
||||
Scenario: TAN request with unrecognised email returns generic message
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
When the owner requests a TAN using "unknown@other.com"
|
||||
Then the response status is 200
|
||||
And the response message confirms TAN was sent
|
||||
|
||||
Scenario: TAN is rejected after its 5-minute validity window
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the owner has requested a TAN using the registered email
|
||||
When time advances by 6 minutes
|
||||
And the owner uses the TAN to rotate keys
|
||||
Then the response status is 422
|
||||
|
||||
Scenario: TAN request rate limit — more than 3 requests within 24 hours is rejected
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the owner has requested a TAN 3 times within the last 24 hours
|
||||
When the owner requests a TAN using the registered email
|
||||
Then the response status is 422
|
||||
|
||||
Scenario: TAN request counter resets after 24 hours
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the owner has requested a TAN 3 times within the last 24 hours
|
||||
When time advances by 25 hours
|
||||
And the owner requests a TAN using the registered email
|
||||
Then the response status is 200
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
Feature: DNS-challenge key rotation — bot-friendly ACME DNS-01 pattern
|
||||
# Primary automated rotation path for agents with DNS API access.
|
||||
# Two factors: rotation secret (proves legitimate requester) + DNS TXT control
|
||||
# (proves domain ownership). No email parsing, no human inbox dependency.
|
||||
#
|
||||
# DNS record to publish: _apix-rotation.{domain} TXT "apix-rotate={challengeToken}"
|
||||
# Window: 15 minutes (accommodates DNS API propagation + polling cycle)
|
||||
# The challenge token is intentionally public — it has no value without the rotation secret.
|
||||
|
||||
Background:
|
||||
Given the organisation registry is empty
|
||||
|
||||
# ── Happy path ──────────────────────────────────────────────────────────────
|
||||
|
||||
Scenario: Agent initiates DNS-challenge rotation and confirms after publishing DNS record
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
When the agent initiates DNS-challenge key rotation
|
||||
Then the response status is 200
|
||||
And the response contains the dns record name "_apix-rotation.example.com"
|
||||
And the response contains the challenge token and dns value format
|
||||
And the response contains an expiry timestamp
|
||||
Given the agent has published the rotation challenge to dns
|
||||
When the agent confirms DNS-challenge key rotation
|
||||
Then the response status is 200
|
||||
And a new api key with prefix "apix_org_" is returned
|
||||
And a new rotation secret with prefix "apix_rot_" is returned
|
||||
And the old api key is no longer valid
|
||||
|
||||
# ── Failure paths ────────────────────────────────────────────────────────────
|
||||
|
||||
Scenario: Confirm fails when DNS record has not been published
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the agent has initiated DNS-challenge key rotation
|
||||
And the DNS rotation challenge record is absent
|
||||
When the agent confirms DNS-challenge key rotation
|
||||
Then the response status is 422
|
||||
|
||||
Scenario: Confirm fails when challenge token does not match DNS record
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the agent has initiated DNS-challenge key rotation
|
||||
And the DNS record "_apix-rotation.example.com" contains the wrong challenge token
|
||||
When the agent confirms DNS-challenge key rotation
|
||||
Then the response status is 422
|
||||
|
||||
Scenario: Confirm fails when the 15-minute challenge window has expired
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the agent has initiated DNS-challenge key rotation
|
||||
And the agent has published the rotation challenge to dns
|
||||
When time advances by 16 minutes
|
||||
And the agent confirms DNS-challenge key rotation
|
||||
Then the response status is 422
|
||||
|
||||
Scenario: Initiate fails with wrong rotation secret
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
When the agent initiates DNS-challenge key rotation with an invalid rotation secret
|
||||
Then the response status is 403
|
||||
|
||||
Scenario: Confirm fails with wrong rotation secret
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the agent has initiated DNS-challenge key rotation
|
||||
And the agent has published the rotation challenge to dns
|
||||
When the agent confirms DNS-challenge key rotation with an invalid rotation secret
|
||||
Then the response status is 403
|
||||
|
||||
Scenario: Confirm without a prior initiate is rejected
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
When the agent confirms DNS-challenge key rotation
|
||||
Then the response status is 422
|
||||
|
||||
# ── Fraud lock interaction ────────────────────────────────────────────────────
|
||||
|
||||
Scenario: Fraud-locked org cannot initiate DNS-challenge rotation
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the owner reports key rotation fraud using the registered email
|
||||
When the agent initiates DNS-challenge key rotation
|
||||
Then the response status is 422
|
||||
|
||||
Scenario: Fraud-locked org cannot confirm DNS-challenge rotation
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the agent has initiated DNS-challenge key rotation
|
||||
And the agent has published the rotation challenge to dns
|
||||
And the owner reports key rotation fraud using the registered email
|
||||
When the agent confirms DNS-challenge key rotation
|
||||
Then the response status is 422
|
||||
|
||||
# ── Isolation between paths ───────────────────────────────────────────────────
|
||||
|
||||
Scenario: DNS challenge and email TAN are independent — each has its own pending state
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the owner has requested a TAN using the registered email
|
||||
When the agent initiates DNS-challenge key rotation
|
||||
Then the response status is 200
|
||||
And the response contains the dns record name "_apix-rotation.example.com"
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
Feature: Organisation O-level transitions — owner-initiated upgrades and re-verification
|
||||
|
||||
Background:
|
||||
Given the organisation registry is empty
|
||||
|
||||
Scenario: Owner upgrades target level from O-0 to O-1 and triggers verification successfully
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
And the DoH MX record "example.com" returns at least one entry
|
||||
When the owner requests an upgrade to target level "IDENTITY_VERIFIED"
|
||||
Then the response status is 200
|
||||
And the verification status is "PENDING"
|
||||
And a dns verification token is returned
|
||||
And the DoH TXT record "_apix-verification.example.com" returns the org's dns token
|
||||
When the owner triggers verification
|
||||
Then the response status is 200
|
||||
And the earned O-level is "IDENTITY_VERIFIED"
|
||||
And the verification status is "ACHIEVED"
|
||||
|
||||
Scenario: Owner cannot downgrade target level below earned level
|
||||
Given an organisation has earned O-level "IDENTITY_VERIFIED" with target "IDENTITY_VERIFIED"
|
||||
When the owner requests an upgrade to target level "UNVERIFIED"
|
||||
Then the response status is 422
|
||||
|
||||
Scenario: Owner cannot set target level equal to earned level
|
||||
Given an organisation has earned O-level "IDENTITY_VERIFIED" with target "IDENTITY_VERIFIED"
|
||||
When the owner requests an upgrade to target level "IDENTITY_VERIFIED"
|
||||
Then the response status is 422
|
||||
|
||||
Scenario: Re-verification after previous failure resets the failure state
|
||||
Given an organisation is registered with target level "IDENTITY_VERIFIED" for domain "example.com"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns no records
|
||||
And the DoH MX record "example.com" returns at least one entry
|
||||
When the owner triggers verification
|
||||
Then the verification status is "FAILED"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns the org's dns token
|
||||
When the owner triggers verification
|
||||
Then the response status is 200
|
||||
And the earned O-level is "IDENTITY_VERIFIED"
|
||||
And the verification status is "ACHIEVED"
|
||||
|
||||
Scenario: Upgrade to O-4 sets status to MANUAL_REVIEW
|
||||
Given an organisation has earned O-level "HYGIENE_VERIFIED" with target "HYGIENE_VERIFIED"
|
||||
When the owner requests an upgrade to target level "OPERATIONALLY_VERIFIED"
|
||||
Then the response status is 200
|
||||
And the verification status is "MANUAL_REVIEW"
|
||||
|
||||
Scenario: Requesting upgrade with wrong API key is rejected
|
||||
Given an organisation is registered with target level "UNVERIFIED" for domain "example.com"
|
||||
When the owner requests an upgrade to target level "IDENTITY_VERIFIED" with an invalid api key
|
||||
Then the response status is 403
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
Feature: Organisation registration
|
||||
|
||||
Background:
|
||||
Given the organisation registry is empty
|
||||
|
||||
Scenario: Register with target O-0 (unverified) is immediately achieved
|
||||
When a service registers with target level "UNVERIFIED"
|
||||
Then the response status is 201
|
||||
And the response contains an organisation id
|
||||
And the response contains an api key with prefix "apix_org_"
|
||||
And the response contains a rotation secret with prefix "apix_rot_"
|
||||
And the earned O-level is "UNVERIFIED"
|
||||
And the verification status is "ACHIEVED"
|
||||
And no dns verification token is returned
|
||||
|
||||
Scenario: Register with target O-1 (identity verified) returns pending with DNS token
|
||||
When a service registers with target level "IDENTITY_VERIFIED"
|
||||
Then the response status is 201
|
||||
And the earned O-level is "UNVERIFIED"
|
||||
And the verification status is "PENDING"
|
||||
And a dns verification token is returned
|
||||
|
||||
Scenario: Register with target O-2 (legal entity verified) returns pending with DNS token
|
||||
When a service registers with target level "LEGAL_ENTITY_VERIFIED"
|
||||
Then the response status is 201
|
||||
And the earned O-level is "UNVERIFIED"
|
||||
And the verification status is "PENDING"
|
||||
And a dns verification token is returned
|
||||
|
||||
Scenario: Register with target O-3 (hygiene verified) returns pending with DNS token
|
||||
When a service registers with target level "HYGIENE_VERIFIED"
|
||||
Then the response status is 201
|
||||
And the earned O-level is "UNVERIFIED"
|
||||
And the verification status is "PENDING"
|
||||
And a dns verification token is returned
|
||||
|
||||
Scenario: Register with target O-4 (operationally verified) goes to manual review
|
||||
When a service registers with target level "OPERATIONALLY_VERIFIED"
|
||||
Then the response status is 201
|
||||
And the earned O-level is "UNVERIFIED"
|
||||
And the verification status is "MANUAL_REVIEW"
|
||||
|
||||
Scenario: Register with target O-5 (audited) goes to manual review
|
||||
When a service registers with target level "AUDITED"
|
||||
Then the response status is 201
|
||||
And the earned O-level is "UNVERIFIED"
|
||||
And the verification status is "MANUAL_REVIEW"
|
||||
|
||||
Scenario: Registration fails when registrant name is missing
|
||||
When a service registers without a registrant name
|
||||
Then the response status is 400
|
||||
|
||||
Scenario: Registration fails when email is invalid
|
||||
When a service registers with an invalid email
|
||||
Then the response status is 400
|
||||
|
||||
Scenario: Registration fails when domain is missing
|
||||
When a service registers without a domain
|
||||
Then the response status is 400
|
||||
|
||||
Scenario: Registration fails when target O-level is missing
|
||||
When a service registers without a target O-level
|
||||
Then the response status is 400
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
Feature: Organisation verification — O-1 Identity Verified (DNS)
|
||||
|
||||
Background:
|
||||
Given the organisation registry is empty
|
||||
|
||||
Scenario: Verification succeeds when DNS TXT and MX records are present
|
||||
Given an organisation is registered with target level "IDENTITY_VERIFIED" for domain "example.com"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns the org's dns token
|
||||
And the DoH MX record "example.com" returns at least one entry
|
||||
When the owner triggers verification
|
||||
Then the response status is 200
|
||||
And the earned O-level is "IDENTITY_VERIFIED"
|
||||
And the verification status is "ACHIEVED"
|
||||
|
||||
Scenario: Verification fails when DNS TXT record is absent
|
||||
Given an organisation is registered with target level "IDENTITY_VERIFIED" for domain "example.com"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns no records
|
||||
And the DoH MX record "example.com" returns at least one entry
|
||||
When the owner triggers verification
|
||||
Then the response status is 200
|
||||
And the earned O-level is "UNVERIFIED"
|
||||
And the verification status is "FAILED"
|
||||
And the verification step is "DNS_TXT"
|
||||
|
||||
Scenario: Verification fails when DNS TXT token does not match
|
||||
Given an organisation is registered with target level "IDENTITY_VERIFIED" for domain "example.com"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns "wrong-token"
|
||||
And the DoH MX record "example.com" returns at least one entry
|
||||
When the owner triggers verification
|
||||
Then the response status is 200
|
||||
And the earned O-level is "UNVERIFIED"
|
||||
And the verification status is "FAILED"
|
||||
And the verification step is "DNS_TXT"
|
||||
|
||||
Scenario: Verification fails when MX record is absent
|
||||
Given an organisation is registered with target level "IDENTITY_VERIFIED" for domain "example.com"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns the org's dns token
|
||||
And the DoH MX record "example.com" returns no records
|
||||
When the owner triggers verification
|
||||
Then the response status is 200
|
||||
And the earned O-level is "UNVERIFIED"
|
||||
And the verification status is "FAILED"
|
||||
And the verification step is "DNS_MX"
|
||||
|
||||
Scenario: Verify is idempotent when status is already ACHIEVED
|
||||
Given an organisation is registered with target level "IDENTITY_VERIFIED" for domain "example.com"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns the org's dns token
|
||||
And the DoH MX record "example.com" returns at least one entry
|
||||
When the owner triggers verification
|
||||
And the owner triggers verification again
|
||||
Then the response status is 200
|
||||
And the verification status is "ACHIEVED"
|
||||
|
||||
Scenario: Verification with wrong API key is rejected
|
||||
Given an organisation is registered with target level "IDENTITY_VERIFIED" for domain "example.com"
|
||||
When the owner triggers verification with an invalid api key
|
||||
Then the response status is 403
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
Feature: Organisation verification — O-2 Legal Entity Verified (GLEIF / OpenCorporates)
|
||||
|
||||
Background:
|
||||
Given the organisation registry is empty
|
||||
|
||||
Scenario: Verification succeeds via GLEIF when LEI is found for registrant name
|
||||
Given an organisation is registered with target level "LEGAL_ENTITY_VERIFIED" for domain "example.com" with name "Acme Corp"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns the org's dns token
|
||||
And the DoH MX record "example.com" returns at least one entry
|
||||
And the GLEIF API returns an LEI "529900HNOAA1KXQJUQ27" for name "Acme Corp"
|
||||
When the owner triggers verification
|
||||
Then the response status is 200
|
||||
And the earned O-level is "LEGAL_ENTITY_VERIFIED"
|
||||
And the verification status is "ACHIEVED"
|
||||
And the detected LEI is "529900HNOAA1KXQJUQ27"
|
||||
|
||||
Scenario: Verification falls back to OpenCorporates when GLEIF returns no results
|
||||
Given an organisation is registered with target level "LEGAL_ENTITY_VERIFIED" for domain "example.com" with name "Local Gmbh"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns the org's dns token
|
||||
And the DoH MX record "example.com" returns at least one entry
|
||||
And the GLEIF API returns no results for name "Local Gmbh"
|
||||
And the OpenCorporates API returns a company match for name "Local Gmbh"
|
||||
When the owner triggers verification
|
||||
Then the response status is 200
|
||||
And the earned O-level is "LEGAL_ENTITY_VERIFIED"
|
||||
And the verification status is "ACHIEVED"
|
||||
|
||||
Scenario: Verification fails when neither GLEIF nor OpenCorporates finds the company
|
||||
Given an organisation is registered with target level "LEGAL_ENTITY_VERIFIED" for domain "example.com" with name "Ghost Inc"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns the org's dns token
|
||||
And the DoH MX record "example.com" returns at least one entry
|
||||
And the GLEIF API returns no results for name "Ghost Inc"
|
||||
And the OpenCorporates API returns no results for name "Ghost Inc"
|
||||
When the owner triggers verification
|
||||
Then the response status is 200
|
||||
And the earned O-level is "IDENTITY_VERIFIED"
|
||||
And the verification status is "FAILED"
|
||||
And the verification step is "OPENCORPORATES"
|
||||
|
||||
Scenario: Verification fails at O-1 DNS step and does not reach GLEIF
|
||||
Given an organisation is registered with target level "LEGAL_ENTITY_VERIFIED" for domain "example.com" with name "Acme Corp"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns no records
|
||||
And the DoH MX record "example.com" returns at least one entry
|
||||
When the owner triggers verification
|
||||
Then the response status is 200
|
||||
And the earned O-level is "UNVERIFIED"
|
||||
And the verification status is "FAILED"
|
||||
And the verification step is "DNS_TXT"
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
Feature: Organisation verification — O-3 Hygiene Verified (security.txt, DMARC, SPF)
|
||||
|
||||
Background:
|
||||
Given the organisation registry is empty
|
||||
|
||||
Scenario: Hygiene verification succeeds when all three hygiene checks pass
|
||||
Given an organisation is registered with target level "HYGIENE_VERIFIED" for domain "example.com" with name "Acme Corp"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns the org's dns token
|
||||
And the DoH MX record "example.com" returns at least one entry
|
||||
And the GLEIF API returns an LEI "529900HNOAA1KXQJUQ27" for name "Acme Corp"
|
||||
And the security.txt endpoint for "example.com" returns HTTP 200
|
||||
And the DoH TXT record "_dmarc.example.com" returns a valid DMARC policy
|
||||
And the DoH TXT record "example.com" returns a valid SPF record
|
||||
When the owner triggers verification
|
||||
Then the response status is 200
|
||||
And the earned O-level is "HYGIENE_VERIFIED"
|
||||
And the verification status is "ACHIEVED"
|
||||
|
||||
Scenario: Hygiene verification fails when security.txt is missing
|
||||
Given an organisation is registered with target level "HYGIENE_VERIFIED" for domain "example.com" with name "Acme Corp"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns the org's dns token
|
||||
And the DoH MX record "example.com" returns at least one entry
|
||||
And the GLEIF API returns an LEI "529900HNOAA1KXQJUQ27" for name "Acme Corp"
|
||||
And the security.txt endpoint for "example.com" returns HTTP 404
|
||||
And the DoH TXT record "_dmarc.example.com" returns a valid DMARC policy
|
||||
And the DoH TXT record "example.com" returns a valid SPF record
|
||||
When the owner triggers verification
|
||||
Then the response status is 200
|
||||
And the earned O-level is "LEGAL_ENTITY_VERIFIED"
|
||||
And the verification status is "FAILED"
|
||||
And the verification step is "SECURITY_TXT"
|
||||
|
||||
Scenario: Hygiene verification fails when DMARC record is absent
|
||||
Given an organisation is registered with target level "HYGIENE_VERIFIED" for domain "example.com" with name "Acme Corp"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns the org's dns token
|
||||
And the DoH MX record "example.com" returns at least one entry
|
||||
And the GLEIF API returns an LEI "529900HNOAA1KXQJUQ27" for name "Acme Corp"
|
||||
And the security.txt endpoint for "example.com" returns HTTP 200
|
||||
And the DoH TXT record "_dmarc.example.com" returns no records
|
||||
And the DoH TXT record "example.com" returns a valid SPF record
|
||||
When the owner triggers verification
|
||||
Then the response status is 200
|
||||
And the earned O-level is "LEGAL_ENTITY_VERIFIED"
|
||||
And the verification status is "FAILED"
|
||||
And the verification step is "DMARC"
|
||||
|
||||
Scenario: Hygiene verification fails when SPF record is absent
|
||||
Given an organisation is registered with target level "HYGIENE_VERIFIED" for domain "example.com" with name "Acme Corp"
|
||||
And the DoH TXT record "_apix-verification.example.com" returns the org's dns token
|
||||
And the DoH MX record "example.com" returns at least one entry
|
||||
And the GLEIF API returns an LEI "529900HNOAA1KXQJUQ27" for name "Acme Corp"
|
||||
And the security.txt endpoint for "example.com" returns HTTP 200
|
||||
And the DoH TXT record "_dmarc.example.com" returns a valid DMARC policy
|
||||
And the DoH TXT record "example.com" returns no records
|
||||
When the owner triggers verification
|
||||
Then the response status is 200
|
||||
And the earned O-level is "LEGAL_ENTITY_VERIFIED"
|
||||
And the verification status is "FAILED"
|
||||
And the verification step is "SPF"
|
||||
Reference in New Issue
Block a user