Skip to content

Example tests for Solid Notifications and WebSocketSubscription2021 specifications #86

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Ensure container created correctly on slash semantic tests.
* Update default test subjects for latest releases.
* For tests posting to a non-existent target, 405 is a valid response if target is not a container.
* Add example tests for the Solid Notifications and WebSocketSubscription2021 specifications (disabled by default).

## Release 0.0.11
* Moved repository to `solid-contrib` organization.
Expand Down
7 changes: 7 additions & 0 deletions application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ sources:
- https://github.com/solid-contrib/specification-tests/blob/main/web-access-control/web-access-control-test-manifest.ttl
- https://github.com/solid-contrib/specification-tests/blob/main/web-access-control/requirement-comments.ttl

# Notifications spec & manifest
# Editor's draft (fully annotated)
- https://solid.github.io/notifications/protocol
- https://github.com/solid-contrib/specification-tests/blob/main/notifications/notifications-test-manifest.ttl
- https://solidproject.org/TR/websocket-subscription-2021
- https://github.com/solid-contrib/specification-tests/blob/main/notifications/websocket-test-manifest.ttl

# Published draft (not annotated)
# - https://solidproject.org/TR/2021/wac-20210711
# - https://github.com/solid-contrib/specification-tests/blob/main/web-access-control/web-access-control-test-manifest-20210711.ttl
Expand Down
13 changes: 13 additions & 0 deletions notifications/access/subscribe.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@ignore
Feature: Subscribe to a resource

# params are subscriptionEndpoint, subscriptionType, url
Scenario:
Given url subscriptionEndpoint
And headers clients.alice.getAuthHeaders('POST', subscriptionEndpoint)
And header Content-Type = 'application/ld+json'
And header Accept = 'application/ld+json'
And request {@context: ['https://www.w3.org/ns/solid/notification/v1'], type: '#(subscriptionType)', topic: '#(url)'}
When method POST
Then status 200
* def endpoint = response.endpoint
24 changes: 24 additions & 0 deletions notifications/access/subscription-access-controls.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@notifications
Feature: Notification subscription access controls

Background:
* def testContainer = rootTestContainer.createContainer()
* def setup = callonce read('../subscription-endpoint.feature')
* def subscription = call read('subscribe.feature') { subscriptionEndpoint: '#(setup.subscriptionEndpoint)', subscriptionType: '#(setup.subscriptionType)', url: '#(testContainer.url)' }

Scenario: Notifications are sent
* def containerSocket = karate.webSocket(subscription.endpoint, null, {subProtocol: 'solid-0.2'})
* assert containerSocket != null
* def resource = testContainer.createResource('.txt', 'Hello World!', 'text/plain');
* listen 5000
* def model = parse(listenResult, 'application/ld+json', testContainer.url)
* assert model.contains(null, iri(RDF, 'type'), iri(PROV, 'Activity'))
* assert model.contains(null, iri(RDF, 'type'), iri(AS, 'Update'))
* assert model.contains(null, iri(AS, 'object'), iri(testContainer.url))
* assert model.contains(null, iri(AS, 'published'), null)
# actor - currently returns container not webid
# * assert model.contains(null, iri('https://www.w3.org/ns/activitystreams#actor'), iri(webIds.alice))
* resource.delete()
* listen 5000
* def resourceModel = parse(listenResult, 'application/ld+json', testContainer.url)
* print resourceModel.asTriples()
27 changes: 27 additions & 0 deletions notifications/access/subscription-read-required.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@notifications
Feature: Notification subscription requires read access

Background:
* def testContainer = rootTestContainer.createContainer()
* def setup = callonce read('../subscription-endpoint.feature')
* def subscriptionEndpoint = setup.subscriptionEndpoint

Scenario: Bob can subscribe with read access to resource
* testContainer.accessDataset = testContainer.accessDatasetBuilder.setAgentAccess(testContainer.url, webIds.bob, ['read']).build()
Given url subscriptionEndpoint
And headers clients.bob.getAuthHeaders('POST', subscriptionEndpoint)
And header Content-Type = 'application/ld+json'
And header Accept = 'application/ld+json'
And request {@context: ['https://www.w3.org/ns/solid/notification/v1'], type: '#(setup.subscriptionType)', topic: '#(testContainer.url)'}
When method POST
Then status 200

Scenario: Bob cannot subscribe without read access to resource
* testContainer.accessDataset = testContainer.accessDatasetBuilder.setAgentAccess(testContainer.url, webIds.bob, ['write', 'append', 'control']).build()
Given url subscriptionEndpoint
And headers clients.bob.getAuthHeaders('POST', subscriptionEndpoint)
And header Content-Type = 'application/ld+json'
And header Accept = 'application/ld+json'
And request {@context: ['https://www.w3.org/ns/solid/notification/v1'], type: '#(setup.subscriptionType)', topic: '#(testContainer.url)'}
When method POST
Then status 403
67 changes: 67 additions & 0 deletions notifications/notifications-test-manifest.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix xsd: <http://www.w3.org/2001/XMLSchema#>
prefix dcterms: <http://purl.org/dc/terms/>
prefix td: <http://www.w3.org/2006/03/test-description#>
prefix spec: <http://www.w3.org/ns/spec#>

# Notifications Proposal
prefix sono: <https://solid.github.io/notifications/protocol#>

prefix manifest: <#>

manifest:notifications-discovery-serialization
a td:TestCase ;
spec:requirementReference sono:discovery-serialization ;
td:reviewStatus td:unreviewed ;
spec:testScript
<https://github.com/solid-contrib/specification-tests/blob/main/notifications/protocol/serialization.feature> .

manifest:notifications-subscription-resource-safe-methods
a td:TestCase ;
spec:requirementReference sono:server-subscription-resource-safe-methods ;
td:reviewStatus td:unreviewed ;
spec:testScript
<https://github.com/solid-contrib/specification-tests/blob/main/notifications/protocol/subscription-safe-methods.feature> .

manifest:notifications-subscription-creation
a td:TestCase ;
spec:requirementReference sono:server-subscription-creation ;
td:reviewStatus td:unreviewed ;
spec:testScript
<https://github.com/solid-contrib/specification-tests/blob/main/notifications/protocol/subscription-creation.feature> .

manifest:notifications-subscription-context
a td:TestCase ;
spec:requirementReference sono:server-subscription-context-value ;
td:reviewStatus td:unreviewed ;
spec:testScript
<https://github.com/solid-contrib/specification-tests/blob/main/notifications/protocol/subscription-context.feature> .

manifest:notifications-subscription-type
a td:TestCase ;
spec:requirementReference sono:server-subscription-type-value ;
td:reviewStatus td:unreviewed ;
spec:testScript
<https://github.com/solid-contrib/specification-tests/blob/main/notifications/protocol/subscription-type.feature> .

manifest:notifications-subscription-topic
a td:TestCase ;
spec:requirementReference sono:server-subscription-topic-value ;
td:reviewStatus td:unreviewed ;
spec:testScript
<https://github.com/solid-contrib/specification-tests/blob/main/notifications/protocol/subscription-topic.feature> .

manifest:notifications-subscription-access-controls
a td:TestCase ;
spec:requirementReference sono:server-subscription-access-controls ;
td:reviewStatus td:unreviewed ;
spec:testScript
<https://github.com/solid-contrib/specification-tests/blob/main/notifications/access/subscription-access-controls.feature> .

manifest:notifications-subscription-access-read
a td:TestCase ;
spec:requirementReference sono:server-subscription-access-read ;
td:reviewStatus td:unreviewed ;
spec:testScript
<https://github.com/solid-contrib/specification-tests/blob/main/notifications/access/subscription-read-required.feature> .
42 changes: 42 additions & 0 deletions notifications/protocol/serialization.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@notifications
Feature: Notification subscription metadata resource serialization

Background: Discover the notification channels
# TODO: The spec does not yet define how Notification Subscription Metadata should be discovered - this is an example approach
Given url resolveUri(rootTestContainer.url, '/.well-known/solid')
And header Accept = 'text/turtle'
When method GET
Then status 200

* def model = parse(response, 'text/turtle', rootTestContainer.url)
* def notificationGatewayPredicate = iri(SOLID, 'notificationGateway')
* assert model.contains(null, notificationGatewayPredicate, null)
* def notificationSubscriptionMetadata = model.objects(null, notificationGatewayPredicate)[0]

* def channelsHaveTypes =
"""
function(model) {
// get all channels and filter out those with an RDF type - the result should be empty
return model.objects(null, iri(NOTIFY, 'notificationChannel')).filter(nc => {
!model.contains(iri(nc), iri(RDF, 'type'), null)
}).length === 0
}
"""

Scenario: Serialized as Turtle
Given url notificationSubscriptionMetadata
And header Accept = 'text/turtle'
When method GET
Then status 200
And match header Content-Type contains 'text/turtle'
* def model = parse(response, 'text/turtle', notificationGateway)
And assert channelsHaveTypes(model)

Scenario: Serialized as JSON-LD
Given url notificationSubscriptionMetadata
And header Accept = 'application/ld+json'
When method GET
Then status 200
And match header Content-Type contains 'application/ld+json'
* def model = parse(response, 'application/ld+json', notificationGateway)
And assert channelsHaveTypes(model)
16 changes: 16 additions & 0 deletions notifications/protocol/subscription-context.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@notifications
Feature: Notification subscription context field

Background:
* def testContainer = rootTestContainer.createContainer()
* def setup = callonce read('../subscription-endpoint.feature')
* def subscriptionEndpoint = setup.subscriptionEndpoint

Scenario: Subscription request must contain the correct context
Given url subscriptionEndpoint
And headers clients.alice.getAuthHeaders('POST', subscriptionEndpoint)
And header Content-Type = 'application/ld+json'
And header Accept = 'application/ld+json'
And request {@context: ['https://www.w3.org/ns/solid/notification/v1'], type: '#(setup.subscriptionType)', topic: '#(testContainer.url)'}
When method POST
Then match response['@context'] contains 'https://www.w3.org/ns/solid/notification/v1'
16 changes: 16 additions & 0 deletions notifications/protocol/subscription-creation.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@notifications
Feature: Notification subscription creation

Background:
* def testContainer = rootTestContainer.createContainer()
* def setup = callonce read('../subscription-endpoint.feature')
* def subscriptionEndpoint = setup.subscriptionEndpoint

Scenario: Subscription endpoint accepts POST
Given url subscriptionEndpoint
And headers clients.alice.getAuthHeaders('POST', subscriptionEndpoint)
And header Content-Type = 'application/ld+json'
And header Accept = 'application/ld+json'
And request {@context: ['https://www.w3.org/ns/solid/notification/v1'], type: '#(setup.subscriptionType)', topic: '#(testContainer.url)'}
When method POST
Then match responseStatus != 405
24 changes: 24 additions & 0 deletions notifications/protocol/subscription-safe-methods.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@notifications
Feature: Notification subscription safe methods

Background:
* def setup = callonce read('../subscription-endpoint.feature')
* def subscriptionEndpoint = setup.subscriptionEndpoint

Scenario: Subscription endpoint accepts GET
Given url subscriptionEndpoint
And headers clients.alice.getAuthHeaders('GET', subscriptionEndpoint)
When method GET
Then status 200

Scenario: Subscription endpoint accepts HEAD
Given url subscriptionEndpoint
And headers clients.alice.getAuthHeaders('HEAD', subscriptionEndpoint)
When method HEAD
Then status 200

Scenario: Subscription endpoint accepts OPTIONS
Given url subscriptionEndpoint
And headers clients.alice.getAuthHeaders('OPTIONS', subscriptionEndpoint)
When method OPTIONS
Then match [200, 204] contains responseStatus
34 changes: 34 additions & 0 deletions notifications/protocol/subscription-topic.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@notifications
Feature: Notification subscription topic field

Background:
* def testContainer = rootTestContainer.createContainer()
* def setup = callonce read('../subscription-endpoint.feature')
* def subscriptionEndpoint = setup.subscriptionEndpoint

Scenario: Subscription request must contain topic
Given url subscriptionEndpoint
And headers clients.alice.getAuthHeaders('POST', subscriptionEndpoint)
And header Content-Type = 'application/ld+json'
And header Accept = 'application/ld+json'
And request {@context: ['https://www.w3.org/ns/solid/notification/v1'], type: '#(setup.subscriptionType)', topic: '#(testContainer.url)'}
When method POST
Then status 200

# server should respond with an error if the topic is missing
Given url subscriptionEndpoint
And headers clients.alice.getAuthHeaders('POST', subscriptionEndpoint)
And header Content-Type = 'application/ld+json'
And header Accept = 'application/ld+json'
And request {@context: ['https://www.w3.org/ns/solid/notification/v1'], type: '#(setup.subscriptionType)'}
When method POST
Then assert responseStatus >= 400 && responseStatus < 500

# server should respond with an error if the topic is invalid
Given url subscriptionEndpoint
And headers clients.alice.getAuthHeaders('POST', subscriptionEndpoint)
And header Content-Type = 'application/ld+json'
And header Accept = 'application/ld+json'
And request {@context: ['https://www.w3.org/ns/solid/notification/v1'], type: '#(setup.subscriptionType)', topic: 'BAD'}
When method POST
Then assert responseStatus >= 400 && responseStatus < 500
35 changes: 35 additions & 0 deletions notifications/protocol/subscription-type.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@notifications
Feature: Notification subscription type field

Background:
* def testContainer = rootTestContainer.createContainer()
* def setup = callonce read('../subscription-endpoint.feature')
* def subscriptionEndpoint = setup.subscriptionEndpoint

Scenario: Subscription request must contain subscription type
Given url subscriptionEndpoint
And headers clients.alice.getAuthHeaders('POST', subscriptionEndpoint)
And header Content-Type = 'application/ld+json'
And header Accept = 'application/ld+json'
And request {@context: ['https://www.w3.org/ns/solid/notification/v1'], type: '#(setup.subscriptionType)', topic: '#(testContainer.url)'}
When method POST
Then status 200
And match response.type == setup.subscriptionType

# server should respond with an error if the type is missing
Given url subscriptionEndpoint
And headers clients.alice.getAuthHeaders('POST', subscriptionEndpoint)
And header Content-Type = 'application/ld+json'
And header Accept = 'application/ld+json'
And request {@context: ['https://www.w3.org/ns/solid/notification/v1'], topic: '#(testContainer.url)'}
When method POST
Then assert responseStatus >= 400 && responseStatus < 500

# server should respond with an error if the type is unknown
Given url subscriptionEndpoint
And headers clients.alice.getAuthHeaders('POST', subscriptionEndpoint)
And header Content-Type = 'application/ld+json'
And header Accept = 'application/ld+json'
And request {@context: ['https://www.w3.org/ns/solid/notification/v1'], type: 'UNKNOWN', topic: '#(testContainer.url)'}
When method POST
Then assert responseStatus >= 400 && responseStatus < 500
63 changes: 63 additions & 0 deletions notifications/subscription-endpoint.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
@ignore
Feature: Routine to get a websocket endpoint from a notification gateway

# param: subscriptionType
Scenario:
# TODO: The spec does not yet define how Notification Subscription Metadata should be discovered - this is an example approach
Given url resolveUri(rootTestContainer.url, '/.well-known/solid')
And header Accept = 'text/turtle'
When method GET
Then status 200

* def model = parse(response, 'text/turtle', rootTestContainer.url)
* def notificationGatewayPredicate = iri('http://www.w3.org/ns/solid/terms#notificationGateway')
* assert model.contains(null, notificationGatewayPredicate, null)
* def notificationSubscriptionMetadata = model.objects(null, notificationGatewayPredicate)[0]

# NOTIFICATION GATEWAY IMPLEMENTATION
* def selectedType = karate.get('subscriptionType', 'WebSocketSubscription2021')
Given url notificationSubscriptionMetadata
And header Accept = 'application/ld+json'
And header Content-Type = 'application/ld+json'
And request {"@context": ["https://www.w3.org/ns/solid/notification/v1"], "type": ["#(selectedType)"], "protocols": ["ws"]}
When method POST
Then status 200
And match response.endpoint == '#notnull'
* def subscriptionEndpoint = response.endpoint
* def subscriptionType = selectedType

# NOTIFICATION CHANNEL DISCOVERY
# Given url notificationSubscriptionMetadata
# And header Accept = 'text/turtle'
# When method GET
# Then status 200
#
# # find the subscription endpoint for the given channel, or default to the first available
# * def findEndpoint =
# """
# function(model) {
# let channels;
# const selectedType = karate.get('subscriptionType')
# if (selectedType) {
# channels = model.subjects(iri(RDF, 'type'), iri(NOTIFY, selectedType));
# } else {
# channels = model.objects(null, iri(NOTIFY, 'notificationChannel'));
# }
# if (channels.length > 0) {
# const subscriptions = model.objects(channels[0], iri(NOTIFY, 'subscription'));
# if (subscriptions.length > 0) {
# if (!selectedType) {
# const types = model.objects(channels[0], iri(RDF, 'type'));
# if (types.length > 0) {
# karate.set('subscriptionType', types[0])
# }
# }
# return subscriptions[0]
# }
# }
# return null;
# }
# """
#
# * def model = parse(response, 'text/turtle', notificationSubscriptionMetadata)
# * def subscriptionEndpoint = findEndpoint(model)
Loading